Close DDD cleanup and tests-support closure
This commit is contained in:
@@ -1,3 +1,564 @@
|
||||
//! 背包应用编排过渡落位。
|
||||
//! 背包应用编排。
|
||||
//!
|
||||
//! 这里只返回背包变更结果和领域事件,不直接访问持久化。
|
||||
|
||||
use crate::commands::{
|
||||
ConsumeInventoryItemInput, EquipInventoryItemInput, GrantInventoryItemInput, InventoryMutation,
|
||||
InventoryMutationInput, RuntimeInventoryStateQueryInput, UnequipInventoryItemInput,
|
||||
};
|
||||
use crate::domain::{
|
||||
InventoryContainerKind, InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSnapshot,
|
||||
InventoryItemSourceKind, InventorySlotSnapshot,
|
||||
};
|
||||
use crate::errors::InventoryMutationFieldError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
format_timestamp_micros, normalize_optional_string as normalize_shared_optional_string,
|
||||
normalize_required_string, normalize_string_list as normalize_shared_string_list,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateSnapshot {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub backpack_items: Vec<InventorySlotSnapshot>,
|
||||
pub equipment_items: Vec<InventorySlotSnapshot>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateProcedureResult {
|
||||
pub ok: bool,
|
||||
pub snapshot: Option<RuntimeInventoryStateSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeInventorySlotRecord {
|
||||
pub slot_id: String,
|
||||
pub container_kind: String,
|
||||
pub slot_key: String,
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: String,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<String>,
|
||||
pub source_kind: String,
|
||||
pub source_reference_id: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeInventoryStateRecord {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub backpack_items: Vec<RuntimeInventorySlotRecord>,
|
||||
pub equipment_items: Vec<RuntimeInventorySlotRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InventoryMutationOutcome {
|
||||
pub next_slots: Vec<InventorySlotSnapshot>,
|
||||
pub changed: bool,
|
||||
pub updated_slot_ids: Vec<String>,
|
||||
pub removed_slot_ids: Vec<String>,
|
||||
pub affected_equipment_slot: Option<InventoryEquipmentSlot>,
|
||||
}
|
||||
|
||||
pub fn normalize_optional_text(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
pub fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
normalize_shared_string_list(values)
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_query_input(
|
||||
runtime_session_id: String,
|
||||
actor_user_id: String,
|
||||
) -> Result<RuntimeInventoryStateQueryInput, InventoryMutationFieldError> {
|
||||
let input = RuntimeInventoryStateQueryInput {
|
||||
runtime_session_id: normalize_required_text(
|
||||
runtime_session_id,
|
||||
InventoryMutationFieldError::MissingRuntimeSessionId,
|
||||
)?,
|
||||
actor_user_id: normalize_required_text(
|
||||
actor_user_id,
|
||||
InventoryMutationFieldError::MissingActorUserId,
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_snapshot(
|
||||
input: RuntimeInventoryStateQueryInput,
|
||||
slots: Vec<InventorySlotSnapshot>,
|
||||
) -> RuntimeInventoryStateSnapshot {
|
||||
let mut backpack_items = Vec::new();
|
||||
let mut equipment_items = Vec::new();
|
||||
|
||||
for slot in slots {
|
||||
match slot.container_kind {
|
||||
InventoryContainerKind::Backpack => backpack_items.push(slot),
|
||||
InventoryContainerKind::Equipment => equipment_items.push(slot),
|
||||
}
|
||||
}
|
||||
|
||||
backpack_items.sort_by(|left, right| {
|
||||
left.slot_key
|
||||
.cmp(&right.slot_key)
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
equipment_items.sort_by(|left, right| {
|
||||
equipment_slot_order(left.equipment_slot_id)
|
||||
.cmp(&equipment_slot_order(right.equipment_slot_id))
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
|
||||
RuntimeInventoryStateSnapshot {
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
backpack_items,
|
||||
equipment_items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_inventory_mutation(
|
||||
current_slots: Vec<InventorySlotSnapshot>,
|
||||
input: InventoryMutationInput,
|
||||
) -> Result<InventoryMutationOutcome, InventoryMutationFieldError> {
|
||||
let _mutation_id = normalize_required_text(
|
||||
input.mutation_id,
|
||||
InventoryMutationFieldError::MissingMutationId,
|
||||
)?;
|
||||
let runtime_session_id = normalize_required_text(
|
||||
input.runtime_session_id,
|
||||
InventoryMutationFieldError::MissingRuntimeSessionId,
|
||||
)?;
|
||||
let actor_user_id = normalize_required_text(
|
||||
input.actor_user_id,
|
||||
InventoryMutationFieldError::MissingActorUserId,
|
||||
)?;
|
||||
let story_session_id = normalize_optional_text(input.story_session_id);
|
||||
|
||||
let mut slots = current_slots;
|
||||
for slot in &slots {
|
||||
if slot.runtime_session_id != runtime_session_id || slot.actor_user_id != actor_user_id {
|
||||
return Err(InventoryMutationFieldError::SlotScopeMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
let outcome = match input.mutation {
|
||||
InventoryMutation::GrantItem(grant) => apply_grant_item(
|
||||
&mut slots,
|
||||
runtime_session_id,
|
||||
story_session_id,
|
||||
actor_user_id,
|
||||
grant,
|
||||
input.updated_at_micros,
|
||||
)?,
|
||||
InventoryMutation::ConsumeItem(consume) => {
|
||||
apply_consume_item(&mut slots, consume, input.updated_at_micros)?
|
||||
}
|
||||
InventoryMutation::EquipItem(equip) => {
|
||||
apply_equip_item(&mut slots, equip, input.updated_at_micros)?
|
||||
}
|
||||
InventoryMutation::UnequipItem(unequip) => {
|
||||
apply_unequip_item(&mut slots, unequip, input.updated_at_micros)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(InventoryMutationOutcome {
|
||||
next_slots: sort_inventory_slots(slots),
|
||||
changed: outcome.changed,
|
||||
updated_slot_ids: sort_string_list(outcome.updated_slot_ids),
|
||||
removed_slot_ids: sort_string_list(outcome.removed_slot_ids),
|
||||
affected_equipment_slot: outcome.affected_equipment_slot,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct InventoryMutationInternalOutcome {
|
||||
changed: bool,
|
||||
updated_slot_ids: Vec<String>,
|
||||
removed_slot_ids: Vec<String>,
|
||||
affected_equipment_slot: Option<InventoryEquipmentSlot>,
|
||||
}
|
||||
|
||||
fn apply_grant_item(
|
||||
slots: &mut Vec<InventorySlotSnapshot>,
|
||||
runtime_session_id: String,
|
||||
story_session_id: Option<String>,
|
||||
actor_user_id: String,
|
||||
grant: GrantInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(grant.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let item = normalize_inventory_item_snapshot(grant.item)?;
|
||||
|
||||
if item.stackable {
|
||||
if let Some(existing) = slots.iter_mut().find(|slot| {
|
||||
slot.container_kind == InventoryContainerKind::Backpack
|
||||
&& slot.stackable
|
||||
&& slot.item_id == item.item_id
|
||||
&& slot.stack_key == item.stack_key
|
||||
}) {
|
||||
existing.category = item.category;
|
||||
existing.name = item.name;
|
||||
existing.description = item.description;
|
||||
existing.quantity += item.quantity;
|
||||
existing.rarity = item.rarity;
|
||||
existing.tags = item.tags;
|
||||
existing.stackable = item.stackable;
|
||||
existing.stack_key = item.stack_key;
|
||||
existing.equipment_slot_id = item.equipment_slot_id;
|
||||
existing.source_kind = item.source_kind;
|
||||
existing.source_reference_id = item.source_reference_id;
|
||||
existing.updated_at_micros = updated_at_micros;
|
||||
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![existing.slot_id.clone()],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
slots.push(InventorySlotSnapshot {
|
||||
slot_id: slot_id.clone(),
|
||||
runtime_session_id,
|
||||
story_session_id,
|
||||
actor_user_id,
|
||||
container_kind: InventoryContainerKind::Backpack,
|
||||
slot_key: build_backpack_slot_key(&slot_id),
|
||||
item_id: item.item_id,
|
||||
category: item.category,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
rarity: item.rarity,
|
||||
tags: item.tags,
|
||||
stackable: item.stackable,
|
||||
stack_key: item.stack_key,
|
||||
equipment_slot_id: item.equipment_slot_id,
|
||||
source_kind: item.source_kind,
|
||||
source_reference_id: item.source_reference_id,
|
||||
created_at_micros: updated_at_micros,
|
||||
updated_at_micros,
|
||||
});
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slot_id],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_consume_item(
|
||||
slots: &mut Vec<InventorySlotSnapshot>,
|
||||
consume: ConsumeInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(consume.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
if consume.quantity == 0 {
|
||||
return Err(InventoryMutationFieldError::InvalidQuantity);
|
||||
}
|
||||
|
||||
let slot_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
|
||||
if slots[slot_index].container_kind != InventoryContainerKind::Backpack {
|
||||
return Err(InventoryMutationFieldError::ItemNotInBackpack);
|
||||
}
|
||||
|
||||
if slots[slot_index].quantity < consume.quantity {
|
||||
return Err(InventoryMutationFieldError::InsufficientQuantity);
|
||||
}
|
||||
|
||||
if slots[slot_index].quantity == consume.quantity {
|
||||
slots.remove(slot_index);
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![],
|
||||
removed_slot_ids: vec![slot_id],
|
||||
affected_equipment_slot: None,
|
||||
});
|
||||
}
|
||||
|
||||
slots[slot_index].quantity -= consume.quantity;
|
||||
slots[slot_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slots[slot_index].slot_id.clone()],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_equip_item(
|
||||
slots: &mut [InventorySlotSnapshot],
|
||||
equip: EquipInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(equip.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let source_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
let target_slot = slots[source_index]
|
||||
.equipment_slot_id
|
||||
.ok_or(InventoryMutationFieldError::ItemNotEquippable)?;
|
||||
|
||||
if slots[source_index].stackable {
|
||||
return Err(InventoryMutationFieldError::EquipmentItemCannotStack);
|
||||
}
|
||||
if slots[source_index].quantity != 1 {
|
||||
return Err(InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
if slots[source_index].container_kind != InventoryContainerKind::Backpack {
|
||||
if slots[source_index].container_kind == InventoryContainerKind::Equipment {
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: false,
|
||||
updated_slot_ids: vec![],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: Some(target_slot),
|
||||
});
|
||||
}
|
||||
|
||||
return Err(InventoryMutationFieldError::ItemNotInBackpack);
|
||||
}
|
||||
|
||||
let occupied_index = slots.iter().position(|slot| {
|
||||
slot.container_kind == InventoryContainerKind::Equipment
|
||||
&& slot.slot_key == build_equipment_slot_key(target_slot)
|
||||
});
|
||||
|
||||
let mut updated_slot_ids = vec![slot_id.clone()];
|
||||
if let Some(occupied_index) = occupied_index {
|
||||
// 首版装备互换直接在同一条 slot 真相记录上切容器,不生成临时副本。
|
||||
slots[occupied_index].container_kind = InventoryContainerKind::Backpack;
|
||||
slots[occupied_index].slot_key = build_backpack_slot_key(&slots[occupied_index].slot_id);
|
||||
slots[occupied_index].updated_at_micros = updated_at_micros;
|
||||
updated_slot_ids.push(slots[occupied_index].slot_id.clone());
|
||||
}
|
||||
|
||||
slots[source_index].container_kind = InventoryContainerKind::Equipment;
|
||||
slots[source_index].slot_key = build_equipment_slot_key(target_slot);
|
||||
slots[source_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids,
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: Some(target_slot),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_unequip_item(
|
||||
slots: &mut [InventorySlotSnapshot],
|
||||
unequip: UnequipInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(unequip.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let slot_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
|
||||
if slots[slot_index].container_kind != InventoryContainerKind::Equipment {
|
||||
return Err(InventoryMutationFieldError::ItemNotEquipped);
|
||||
}
|
||||
|
||||
let affected_equipment_slot = slots[slot_index].equipment_slot_id;
|
||||
slots[slot_index].container_kind = InventoryContainerKind::Backpack;
|
||||
slots[slot_index].slot_key = build_backpack_slot_key(&slot_id);
|
||||
slots[slot_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slot_id],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_inventory_item_snapshot(
|
||||
item: InventoryItemSnapshot,
|
||||
) -> Result<InventoryItemSnapshot, InventoryMutationFieldError> {
|
||||
let item_id =
|
||||
normalize_required_text(item.item_id, InventoryMutationFieldError::MissingItemId)?;
|
||||
let category =
|
||||
normalize_required_text(item.category, InventoryMutationFieldError::MissingCategory)?;
|
||||
let name = normalize_required_text(item.name, InventoryMutationFieldError::MissingName)?;
|
||||
if item.quantity == 0 {
|
||||
return Err(InventoryMutationFieldError::InvalidQuantity);
|
||||
}
|
||||
|
||||
if !item.stackable && item.quantity != 1 {
|
||||
return Err(InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
|
||||
if item.equipment_slot_id.is_some() && item.stackable {
|
||||
return Err(InventoryMutationFieldError::EquipmentItemCannotStack);
|
||||
}
|
||||
|
||||
let stack_key = if item.stackable {
|
||||
normalize_required_text(item.stack_key, InventoryMutationFieldError::MissingStackKey)?
|
||||
} else {
|
||||
normalize_optional_text(Some(item.stack_key)).unwrap_or_else(|| item_id.clone())
|
||||
};
|
||||
|
||||
Ok(InventoryItemSnapshot {
|
||||
item_id,
|
||||
category,
|
||||
name,
|
||||
description: normalize_optional_text(item.description),
|
||||
quantity: item.quantity,
|
||||
rarity: item.rarity,
|
||||
tags: normalize_string_list(item.tags),
|
||||
stackable: item.stackable,
|
||||
stack_key,
|
||||
equipment_slot_id: item.equipment_slot_id,
|
||||
source_kind: item.source_kind,
|
||||
source_reference_id: normalize_optional_text(item.source_reference_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_required_text(
|
||||
value: String,
|
||||
error: InventoryMutationFieldError,
|
||||
) -> Result<String, InventoryMutationFieldError> {
|
||||
normalize_required_string(value).ok_or(error)
|
||||
}
|
||||
|
||||
fn sort_inventory_slots(mut slots: Vec<InventorySlotSnapshot>) -> Vec<InventorySlotSnapshot> {
|
||||
slots.sort_by(|left, right| {
|
||||
container_order(left.container_kind)
|
||||
.cmp(&container_order(right.container_kind))
|
||||
.then(left.slot_key.cmp(&right.slot_key))
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
slots
|
||||
}
|
||||
|
||||
fn sort_string_list(mut values: Vec<String>) -> Vec<String> {
|
||||
values.sort();
|
||||
values
|
||||
}
|
||||
|
||||
fn container_order(kind: InventoryContainerKind) -> u8 {
|
||||
match kind {
|
||||
InventoryContainerKind::Equipment => 0,
|
||||
InventoryContainerKind::Backpack => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn equipment_slot_order(slot: Option<InventoryEquipmentSlot>) -> u8 {
|
||||
match slot {
|
||||
Some(InventoryEquipmentSlot::Weapon) => 0,
|
||||
Some(InventoryEquipmentSlot::Armor) => 1,
|
||||
Some(InventoryEquipmentSlot::Relic) => 2,
|
||||
None => 3,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_backpack_slot_key(slot_id: &str) -> String {
|
||||
slot_id.to_string()
|
||||
}
|
||||
|
||||
fn build_equipment_slot_key(slot: InventoryEquipmentSlot) -> String {
|
||||
slot.as_str().to_string()
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_record(
|
||||
snapshot: RuntimeInventoryStateSnapshot,
|
||||
) -> RuntimeInventoryStateRecord {
|
||||
RuntimeInventoryStateRecord {
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
backpack_items: snapshot
|
||||
.backpack_items
|
||||
.into_iter()
|
||||
.map(build_runtime_inventory_slot_record)
|
||||
.collect(),
|
||||
equipment_items: snapshot
|
||||
.equipment_items
|
||||
.into_iter()
|
||||
.map(build_runtime_inventory_slot_record)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_runtime_inventory_slot_record(slot: InventorySlotSnapshot) -> RuntimeInventorySlotRecord {
|
||||
RuntimeInventorySlotRecord {
|
||||
slot_id: slot.slot_id,
|
||||
container_kind: format_inventory_container_kind(slot.container_kind).to_string(),
|
||||
slot_key: slot.slot_key,
|
||||
item_id: slot.item_id,
|
||||
category: slot.category,
|
||||
name: slot.name,
|
||||
description: slot.description,
|
||||
quantity: slot.quantity,
|
||||
rarity: format_inventory_item_rarity(slot.rarity).to_string(),
|
||||
tags: slot.tags,
|
||||
stackable: slot.stackable,
|
||||
stack_key: slot.stack_key,
|
||||
equipment_slot_id: slot
|
||||
.equipment_slot_id
|
||||
.map(|value| value.as_str().to_string()),
|
||||
source_kind: format_inventory_item_source_kind(slot.source_kind).to_string(),
|
||||
source_reference_id: slot.source_reference_id,
|
||||
created_at: format_timestamp_micros(slot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(slot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_container_kind(value: InventoryContainerKind) -> &'static str {
|
||||
match value {
|
||||
InventoryContainerKind::Backpack => "backpack",
|
||||
InventoryContainerKind::Equipment => "equipment",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_item_rarity(value: InventoryItemRarity) -> &'static str {
|
||||
match value {
|
||||
InventoryItemRarity::Common => "common",
|
||||
InventoryItemRarity::Uncommon => "uncommon",
|
||||
InventoryItemRarity::Rare => "rare",
|
||||
InventoryItemRarity::Epic => "epic",
|
||||
InventoryItemRarity::Legendary => "legendary",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_item_source_kind(value: InventoryItemSourceKind) -> &'static str {
|
||||
match value {
|
||||
InventoryItemSourceKind::StoryReward => "story_reward",
|
||||
InventoryItemSourceKind::QuestReward => "quest_reward",
|
||||
InventoryItemSourceKind::TreasureReward => "treasure_reward",
|
||||
InventoryItemSourceKind::NpcGift => "npc_gift",
|
||||
InventoryItemSourceKind::NpcTrade => "npc_trade",
|
||||
InventoryItemSourceKind::CombatDrop => "combat_drop",
|
||||
InventoryItemSourceKind::ForgeCraft => "forge_craft",
|
||||
InventoryItemSourceKind::ForgeReforge => "forge_reforge",
|
||||
InventoryItemSourceKind::ManualPatch => "manual_patch",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
//! 背包写入命令过渡落位。
|
||||
//! 背包写入命令。
|
||||
//!
|
||||
//! 用于表达授予物品、装备、卸下、消耗和整理等输入。
|
||||
|
||||
use crate::domain::InventoryItemSnapshot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GrantInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
pub item: InventoryItemSnapshot,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConsumeInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
pub quantity: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EquipInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UnequipInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryMutation {
|
||||
GrantItem(GrantInventoryItemInput),
|
||||
ConsumeItem(ConsumeInventoryItemInput),
|
||||
EquipItem(EquipInventoryItemInput),
|
||||
UnequipItem(UnequipInventoryItemInput),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryMutationInput {
|
||||
pub mutation_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: Option<String>,
|
||||
pub actor_user_id: String,
|
||||
pub mutation: InventoryMutation,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateQueryInput {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,111 @@
|
||||
//! 背包领域模型过渡落位。
|
||||
//! 背包领域模型。
|
||||
//!
|
||||
//! 后续迁移背包槽、装备槽、堆叠和消耗规则时,只保留物品状态变化;
|
||||
//! 本文件只承载背包槽、装备槽、堆叠和物品来源等稳定值对象;
|
||||
//! SpacetimeDB 表查询写回由 adapter 处理。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::build_prefixed_seed_id;
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const INVENTORY_SLOT_ID_PREFIX: &str = "invslot_";
|
||||
pub const INVENTORY_MUTATION_ID_PREFIX: &str = "invmut_";
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryContainerKind {
|
||||
Backpack,
|
||||
Equipment,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryItemRarity {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryEquipmentSlot {
|
||||
Weapon,
|
||||
Armor,
|
||||
Relic,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryItemSourceKind {
|
||||
StoryReward,
|
||||
QuestReward,
|
||||
TreasureReward,
|
||||
NpcGift,
|
||||
NpcTrade,
|
||||
CombatDrop,
|
||||
ForgeCraft,
|
||||
ForgeReforge,
|
||||
ManualPatch,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryItemSnapshot {
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: InventoryItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<InventoryEquipmentSlot>,
|
||||
pub source_kind: InventoryItemSourceKind,
|
||||
pub source_reference_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventorySlotSnapshot {
|
||||
pub slot_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: Option<String>,
|
||||
pub actor_user_id: String,
|
||||
pub container_kind: InventoryContainerKind,
|
||||
pub slot_key: String,
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: InventoryItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<InventoryEquipmentSlot>,
|
||||
pub source_kind: InventoryItemSourceKind,
|
||||
pub source_reference_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl InventoryEquipmentSlot {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Weapon => "weapon",
|
||||
Self::Armor => "armor",
|
||||
Self::Relic => "relic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_inventory_slot_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(INVENTORY_SLOT_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn generate_inventory_mutation_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(INVENTORY_MUTATION_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,58 @@
|
||||
//! 背包领域错误过渡落位。
|
||||
//! 背包领域错误。
|
||||
//!
|
||||
//! 错误保持可测试的业务语义,例如数量不足、槽位冲突和物品不存在。
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InventoryMutationFieldError {
|
||||
MissingMutationId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingActorUserId,
|
||||
MissingSlotId,
|
||||
MissingItemId,
|
||||
MissingCategory,
|
||||
MissingName,
|
||||
InvalidQuantity,
|
||||
MissingStackKey,
|
||||
NonStackableItemMustStaySingleQuantity,
|
||||
EquipmentItemCannotStack,
|
||||
SlotScopeMismatch,
|
||||
ItemNotFound,
|
||||
ItemNotInBackpack,
|
||||
ItemNotEquipped,
|
||||
InsufficientQuantity,
|
||||
ItemNotEquippable,
|
||||
}
|
||||
|
||||
impl fmt::Display for InventoryMutationFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingMutationId => f.write_str("inventory_mutation.mutation_id 不能为空"),
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("inventory_mutation.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingActorUserId => f.write_str("inventory_mutation.actor_user_id 不能为空"),
|
||||
Self::MissingSlotId => f.write_str("inventory_slot.slot_id 不能为空"),
|
||||
Self::MissingItemId => f.write_str("inventory_item.item_id 不能为空"),
|
||||
Self::MissingCategory => f.write_str("inventory_item.category 不能为空"),
|
||||
Self::MissingName => f.write_str("inventory_item.name 不能为空"),
|
||||
Self::InvalidQuantity => f.write_str("inventory_item.quantity 必须大于 0"),
|
||||
Self::MissingStackKey => f.write_str("可堆叠物品必须提供 stack_key"),
|
||||
Self::NonStackableItemMustStaySingleQuantity => {
|
||||
f.write_str("不可堆叠物品必须固定为单槽位单数量")
|
||||
}
|
||||
Self::EquipmentItemCannotStack => f.write_str("可装备物品不能标记为 stackable"),
|
||||
Self::SlotScopeMismatch => {
|
||||
f.write_str("当前 inventory_slot 不属于本次 mutation 作用域")
|
||||
}
|
||||
Self::ItemNotFound => f.write_str("目标 inventory_slot 不存在"),
|
||||
Self::ItemNotInBackpack => f.write_str("目标物品当前不在背包中"),
|
||||
Self::ItemNotEquipped => f.write_str("目标物品当前不在装备位上"),
|
||||
Self::InsufficientQuantity => f.write_str("当前背包数量不足,无法完成扣减"),
|
||||
Self::ItemNotEquippable => f.write_str("目标物品当前不可装备"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InventoryMutationFieldError {}
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
//! 背包领域事件过渡落位。
|
||||
//! 背包领域事件。
|
||||
//!
|
||||
//! 用于表达物品获得、物品消耗、装备变化和槽位投影变化等事实。
|
||||
|
||||
use crate::domain::InventoryEquipmentSlot;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryDomainEvent {
|
||||
ItemGranted(InventoryItemGrantedEvent),
|
||||
ItemConsumed(InventoryItemConsumedEvent),
|
||||
EquipmentChanged(InventoryEquipmentChangedEvent),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryItemGrantedEvent {
|
||||
pub slot_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryItemConsumedEvent {
|
||||
pub slot_id: String,
|
||||
pub quantity: u32,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryEquipmentChangedEvent {
|
||||
pub slot_id: String,
|
||||
pub equipment_slot: InventoryEquipmentSlot,
|
||||
pub occurred_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -4,768 +4,11 @@ mod domain;
|
||||
mod errors;
|
||||
mod events;
|
||||
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_kernel::{
|
||||
build_prefixed_seed_id, format_timestamp_micros,
|
||||
normalize_optional_string as normalize_shared_optional_string, normalize_required_string,
|
||||
normalize_string_list as normalize_shared_string_list,
|
||||
};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const INVENTORY_SLOT_ID_PREFIX: &str = "invslot_";
|
||||
pub const INVENTORY_MUTATION_ID_PREFIX: &str = "invmut_";
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryContainerKind {
|
||||
Backpack,
|
||||
Equipment,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryItemRarity {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryEquipmentSlot {
|
||||
Weapon,
|
||||
Armor,
|
||||
Relic,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryItemSourceKind {
|
||||
StoryReward,
|
||||
QuestReward,
|
||||
TreasureReward,
|
||||
NpcGift,
|
||||
NpcTrade,
|
||||
CombatDrop,
|
||||
ForgeCraft,
|
||||
ForgeReforge,
|
||||
ManualPatch,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryItemSnapshot {
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: InventoryItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<InventoryEquipmentSlot>,
|
||||
pub source_kind: InventoryItemSourceKind,
|
||||
pub source_reference_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventorySlotSnapshot {
|
||||
pub slot_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: Option<String>,
|
||||
pub actor_user_id: String,
|
||||
pub container_kind: InventoryContainerKind,
|
||||
pub slot_key: String,
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: InventoryItemRarity,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<InventoryEquipmentSlot>,
|
||||
pub source_kind: InventoryItemSourceKind,
|
||||
pub source_reference_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GrantInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
pub item: InventoryItemSnapshot,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConsumeInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
pub quantity: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EquipInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UnequipInventoryItemInput {
|
||||
pub slot_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InventoryMutation {
|
||||
GrantItem(GrantInventoryItemInput),
|
||||
ConsumeItem(ConsumeInventoryItemInput),
|
||||
EquipItem(EquipInventoryItemInput),
|
||||
UnequipItem(UnequipInventoryItemInput),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InventoryMutationInput {
|
||||
pub mutation_id: String,
|
||||
pub runtime_session_id: String,
|
||||
pub story_session_id: Option<String>,
|
||||
pub actor_user_id: String,
|
||||
pub mutation: InventoryMutation,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateQueryInput {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateSnapshot {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub backpack_items: Vec<InventorySlotSnapshot>,
|
||||
pub equipment_items: Vec<InventorySlotSnapshot>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeInventoryStateProcedureResult {
|
||||
pub ok: bool,
|
||||
pub snapshot: Option<RuntimeInventoryStateSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeInventorySlotRecord {
|
||||
pub slot_id: String,
|
||||
pub container_kind: String,
|
||||
pub slot_key: String,
|
||||
pub item_id: String,
|
||||
pub category: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub quantity: u32,
|
||||
pub rarity: String,
|
||||
pub tags: Vec<String>,
|
||||
pub stackable: bool,
|
||||
pub stack_key: String,
|
||||
pub equipment_slot_id: Option<String>,
|
||||
pub source_kind: String,
|
||||
pub source_reference_id: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeInventoryStateRecord {
|
||||
pub runtime_session_id: String,
|
||||
pub actor_user_id: String,
|
||||
pub backpack_items: Vec<RuntimeInventorySlotRecord>,
|
||||
pub equipment_items: Vec<RuntimeInventorySlotRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InventoryMutationOutcome {
|
||||
pub next_slots: Vec<InventorySlotSnapshot>,
|
||||
pub changed: bool,
|
||||
pub updated_slot_ids: Vec<String>,
|
||||
pub removed_slot_ids: Vec<String>,
|
||||
pub affected_equipment_slot: Option<InventoryEquipmentSlot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InventoryMutationFieldError {
|
||||
MissingMutationId,
|
||||
MissingRuntimeSessionId,
|
||||
MissingActorUserId,
|
||||
MissingSlotId,
|
||||
MissingItemId,
|
||||
MissingCategory,
|
||||
MissingName,
|
||||
InvalidQuantity,
|
||||
MissingStackKey,
|
||||
NonStackableItemMustStaySingleQuantity,
|
||||
EquipmentItemCannotStack,
|
||||
SlotScopeMismatch,
|
||||
ItemNotFound,
|
||||
ItemNotInBackpack,
|
||||
ItemNotEquipped,
|
||||
InsufficientQuantity,
|
||||
ItemNotEquippable,
|
||||
}
|
||||
|
||||
impl InventoryEquipmentSlot {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Weapon => "weapon",
|
||||
Self::Armor => "armor",
|
||||
Self::Relic => "relic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_inventory_slot_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(INVENTORY_SLOT_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn generate_inventory_mutation_id(seed_micros: i64) -> String {
|
||||
build_prefixed_seed_id(INVENTORY_MUTATION_ID_PREFIX, seed_micros)
|
||||
}
|
||||
|
||||
pub fn normalize_optional_text(value: Option<String>) -> Option<String> {
|
||||
normalize_shared_optional_string(value)
|
||||
}
|
||||
|
||||
pub fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
normalize_shared_string_list(values)
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_query_input(
|
||||
runtime_session_id: String,
|
||||
actor_user_id: String,
|
||||
) -> Result<RuntimeInventoryStateQueryInput, InventoryMutationFieldError> {
|
||||
let input = RuntimeInventoryStateQueryInput {
|
||||
runtime_session_id: normalize_required_text(
|
||||
runtime_session_id,
|
||||
InventoryMutationFieldError::MissingRuntimeSessionId,
|
||||
)?,
|
||||
actor_user_id: normalize_required_text(
|
||||
actor_user_id,
|
||||
InventoryMutationFieldError::MissingActorUserId,
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_snapshot(
|
||||
input: RuntimeInventoryStateQueryInput,
|
||||
slots: Vec<InventorySlotSnapshot>,
|
||||
) -> RuntimeInventoryStateSnapshot {
|
||||
let mut backpack_items = Vec::new();
|
||||
let mut equipment_items = Vec::new();
|
||||
|
||||
for slot in slots {
|
||||
match slot.container_kind {
|
||||
InventoryContainerKind::Backpack => backpack_items.push(slot),
|
||||
InventoryContainerKind::Equipment => equipment_items.push(slot),
|
||||
}
|
||||
}
|
||||
|
||||
backpack_items.sort_by(|left, right| {
|
||||
left.slot_key
|
||||
.cmp(&right.slot_key)
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
equipment_items.sort_by(|left, right| {
|
||||
equipment_slot_order(left.equipment_slot_id)
|
||||
.cmp(&equipment_slot_order(right.equipment_slot_id))
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
|
||||
RuntimeInventoryStateSnapshot {
|
||||
runtime_session_id: input.runtime_session_id,
|
||||
actor_user_id: input.actor_user_id,
|
||||
backpack_items,
|
||||
equipment_items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_inventory_mutation(
|
||||
current_slots: Vec<InventorySlotSnapshot>,
|
||||
input: InventoryMutationInput,
|
||||
) -> Result<InventoryMutationOutcome, InventoryMutationFieldError> {
|
||||
let _mutation_id = normalize_required_text(
|
||||
input.mutation_id,
|
||||
InventoryMutationFieldError::MissingMutationId,
|
||||
)?;
|
||||
let runtime_session_id = normalize_required_text(
|
||||
input.runtime_session_id,
|
||||
InventoryMutationFieldError::MissingRuntimeSessionId,
|
||||
)?;
|
||||
let actor_user_id = normalize_required_text(
|
||||
input.actor_user_id,
|
||||
InventoryMutationFieldError::MissingActorUserId,
|
||||
)?;
|
||||
let story_session_id = normalize_optional_text(input.story_session_id);
|
||||
|
||||
let mut slots = current_slots;
|
||||
for slot in &slots {
|
||||
if slot.runtime_session_id != runtime_session_id || slot.actor_user_id != actor_user_id {
|
||||
return Err(InventoryMutationFieldError::SlotScopeMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
let outcome = match input.mutation {
|
||||
InventoryMutation::GrantItem(grant) => apply_grant_item(
|
||||
&mut slots,
|
||||
runtime_session_id,
|
||||
story_session_id,
|
||||
actor_user_id,
|
||||
grant,
|
||||
input.updated_at_micros,
|
||||
)?,
|
||||
InventoryMutation::ConsumeItem(consume) => {
|
||||
apply_consume_item(&mut slots, consume, input.updated_at_micros)?
|
||||
}
|
||||
InventoryMutation::EquipItem(equip) => {
|
||||
apply_equip_item(&mut slots, equip, input.updated_at_micros)?
|
||||
}
|
||||
InventoryMutation::UnequipItem(unequip) => {
|
||||
apply_unequip_item(&mut slots, unequip, input.updated_at_micros)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(InventoryMutationOutcome {
|
||||
next_slots: sort_inventory_slots(slots),
|
||||
changed: outcome.changed,
|
||||
updated_slot_ids: sort_string_list(outcome.updated_slot_ids),
|
||||
removed_slot_ids: sort_string_list(outcome.removed_slot_ids),
|
||||
affected_equipment_slot: outcome.affected_equipment_slot,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct InventoryMutationInternalOutcome {
|
||||
changed: bool,
|
||||
updated_slot_ids: Vec<String>,
|
||||
removed_slot_ids: Vec<String>,
|
||||
affected_equipment_slot: Option<InventoryEquipmentSlot>,
|
||||
}
|
||||
|
||||
fn apply_grant_item(
|
||||
slots: &mut Vec<InventorySlotSnapshot>,
|
||||
runtime_session_id: String,
|
||||
story_session_id: Option<String>,
|
||||
actor_user_id: String,
|
||||
grant: GrantInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(grant.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let item = normalize_inventory_item_snapshot(grant.item)?;
|
||||
|
||||
if item.stackable {
|
||||
if let Some(existing) = slots.iter_mut().find(|slot| {
|
||||
slot.container_kind == InventoryContainerKind::Backpack
|
||||
&& slot.stackable
|
||||
&& slot.item_id == item.item_id
|
||||
&& slot.stack_key == item.stack_key
|
||||
}) {
|
||||
existing.category = item.category;
|
||||
existing.name = item.name;
|
||||
existing.description = item.description;
|
||||
existing.quantity += item.quantity;
|
||||
existing.rarity = item.rarity;
|
||||
existing.tags = item.tags;
|
||||
existing.stackable = item.stackable;
|
||||
existing.stack_key = item.stack_key;
|
||||
existing.equipment_slot_id = item.equipment_slot_id;
|
||||
existing.source_kind = item.source_kind;
|
||||
existing.source_reference_id = item.source_reference_id;
|
||||
existing.updated_at_micros = updated_at_micros;
|
||||
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![existing.slot_id.clone()],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
slots.push(InventorySlotSnapshot {
|
||||
slot_id: slot_id.clone(),
|
||||
runtime_session_id,
|
||||
story_session_id,
|
||||
actor_user_id,
|
||||
container_kind: InventoryContainerKind::Backpack,
|
||||
slot_key: build_backpack_slot_key(&slot_id),
|
||||
item_id: item.item_id,
|
||||
category: item.category,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
rarity: item.rarity,
|
||||
tags: item.tags,
|
||||
stackable: item.stackable,
|
||||
stack_key: item.stack_key,
|
||||
equipment_slot_id: item.equipment_slot_id,
|
||||
source_kind: item.source_kind,
|
||||
source_reference_id: item.source_reference_id,
|
||||
created_at_micros: updated_at_micros,
|
||||
updated_at_micros,
|
||||
});
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slot_id],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_consume_item(
|
||||
slots: &mut Vec<InventorySlotSnapshot>,
|
||||
consume: ConsumeInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(consume.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
if consume.quantity == 0 {
|
||||
return Err(InventoryMutationFieldError::InvalidQuantity);
|
||||
}
|
||||
|
||||
let slot_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
|
||||
if slots[slot_index].container_kind != InventoryContainerKind::Backpack {
|
||||
return Err(InventoryMutationFieldError::ItemNotInBackpack);
|
||||
}
|
||||
|
||||
if slots[slot_index].quantity < consume.quantity {
|
||||
return Err(InventoryMutationFieldError::InsufficientQuantity);
|
||||
}
|
||||
|
||||
if slots[slot_index].quantity == consume.quantity {
|
||||
slots.remove(slot_index);
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![],
|
||||
removed_slot_ids: vec![slot_id],
|
||||
affected_equipment_slot: None,
|
||||
});
|
||||
}
|
||||
|
||||
slots[slot_index].quantity -= consume.quantity;
|
||||
slots[slot_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slots[slot_index].slot_id.clone()],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_equip_item(
|
||||
slots: &mut [InventorySlotSnapshot],
|
||||
equip: EquipInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(equip.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let source_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
let target_slot = slots[source_index]
|
||||
.equipment_slot_id
|
||||
.ok_or(InventoryMutationFieldError::ItemNotEquippable)?;
|
||||
|
||||
if slots[source_index].stackable {
|
||||
return Err(InventoryMutationFieldError::EquipmentItemCannotStack);
|
||||
}
|
||||
if slots[source_index].quantity != 1 {
|
||||
return Err(InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
if slots[source_index].container_kind != InventoryContainerKind::Backpack {
|
||||
if slots[source_index].container_kind == InventoryContainerKind::Equipment {
|
||||
return Ok(InventoryMutationInternalOutcome {
|
||||
changed: false,
|
||||
updated_slot_ids: vec![],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: Some(target_slot),
|
||||
});
|
||||
}
|
||||
|
||||
return Err(InventoryMutationFieldError::ItemNotInBackpack);
|
||||
}
|
||||
|
||||
let occupied_index = slots.iter().position(|slot| {
|
||||
slot.container_kind == InventoryContainerKind::Equipment
|
||||
&& slot.slot_key == build_equipment_slot_key(target_slot)
|
||||
});
|
||||
|
||||
let mut updated_slot_ids = vec![slot_id.clone()];
|
||||
if let Some(occupied_index) = occupied_index {
|
||||
// 首版装备互换直接在同一条 slot 真相记录上切容器,不生成临时副本。
|
||||
slots[occupied_index].container_kind = InventoryContainerKind::Backpack;
|
||||
slots[occupied_index].slot_key = build_backpack_slot_key(&slots[occupied_index].slot_id);
|
||||
slots[occupied_index].updated_at_micros = updated_at_micros;
|
||||
updated_slot_ids.push(slots[occupied_index].slot_id.clone());
|
||||
}
|
||||
|
||||
slots[source_index].container_kind = InventoryContainerKind::Equipment;
|
||||
slots[source_index].slot_key = build_equipment_slot_key(target_slot);
|
||||
slots[source_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids,
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot: Some(target_slot),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_unequip_item(
|
||||
slots: &mut [InventorySlotSnapshot],
|
||||
unequip: UnequipInventoryItemInput,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<InventoryMutationInternalOutcome, InventoryMutationFieldError> {
|
||||
let slot_id =
|
||||
normalize_required_text(unequip.slot_id, InventoryMutationFieldError::MissingSlotId)?;
|
||||
let slot_index = slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == slot_id)
|
||||
.ok_or(InventoryMutationFieldError::ItemNotFound)?;
|
||||
|
||||
if slots[slot_index].container_kind != InventoryContainerKind::Equipment {
|
||||
return Err(InventoryMutationFieldError::ItemNotEquipped);
|
||||
}
|
||||
|
||||
let affected_equipment_slot = slots[slot_index].equipment_slot_id;
|
||||
slots[slot_index].container_kind = InventoryContainerKind::Backpack;
|
||||
slots[slot_index].slot_key = build_backpack_slot_key(&slot_id);
|
||||
slots[slot_index].updated_at_micros = updated_at_micros;
|
||||
|
||||
Ok(InventoryMutationInternalOutcome {
|
||||
changed: true,
|
||||
updated_slot_ids: vec![slot_id],
|
||||
removed_slot_ids: vec![],
|
||||
affected_equipment_slot,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_inventory_item_snapshot(
|
||||
item: InventoryItemSnapshot,
|
||||
) -> Result<InventoryItemSnapshot, InventoryMutationFieldError> {
|
||||
let item_id =
|
||||
normalize_required_text(item.item_id, InventoryMutationFieldError::MissingItemId)?;
|
||||
let category =
|
||||
normalize_required_text(item.category, InventoryMutationFieldError::MissingCategory)?;
|
||||
let name = normalize_required_text(item.name, InventoryMutationFieldError::MissingName)?;
|
||||
if item.quantity == 0 {
|
||||
return Err(InventoryMutationFieldError::InvalidQuantity);
|
||||
}
|
||||
|
||||
if !item.stackable && item.quantity != 1 {
|
||||
return Err(InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity);
|
||||
}
|
||||
|
||||
if item.equipment_slot_id.is_some() && item.stackable {
|
||||
return Err(InventoryMutationFieldError::EquipmentItemCannotStack);
|
||||
}
|
||||
|
||||
let stack_key = if item.stackable {
|
||||
normalize_required_text(item.stack_key, InventoryMutationFieldError::MissingStackKey)?
|
||||
} else {
|
||||
normalize_optional_text(Some(item.stack_key)).unwrap_or_else(|| item_id.clone())
|
||||
};
|
||||
|
||||
Ok(InventoryItemSnapshot {
|
||||
item_id,
|
||||
category,
|
||||
name,
|
||||
description: normalize_optional_text(item.description),
|
||||
quantity: item.quantity,
|
||||
rarity: item.rarity,
|
||||
tags: normalize_string_list(item.tags),
|
||||
stackable: item.stackable,
|
||||
stack_key,
|
||||
equipment_slot_id: item.equipment_slot_id,
|
||||
source_kind: item.source_kind,
|
||||
source_reference_id: normalize_optional_text(item.source_reference_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_required_text(
|
||||
value: String,
|
||||
error: InventoryMutationFieldError,
|
||||
) -> Result<String, InventoryMutationFieldError> {
|
||||
normalize_required_string(value).ok_or(error)
|
||||
}
|
||||
|
||||
fn sort_inventory_slots(mut slots: Vec<InventorySlotSnapshot>) -> Vec<InventorySlotSnapshot> {
|
||||
slots.sort_by(|left, right| {
|
||||
container_order(left.container_kind)
|
||||
.cmp(&container_order(right.container_kind))
|
||||
.then(left.slot_key.cmp(&right.slot_key))
|
||||
.then(left.slot_id.cmp(&right.slot_id))
|
||||
});
|
||||
slots
|
||||
}
|
||||
|
||||
fn sort_string_list(mut values: Vec<String>) -> Vec<String> {
|
||||
values.sort();
|
||||
values
|
||||
}
|
||||
|
||||
fn container_order(kind: InventoryContainerKind) -> u8 {
|
||||
match kind {
|
||||
InventoryContainerKind::Equipment => 0,
|
||||
InventoryContainerKind::Backpack => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn equipment_slot_order(slot: Option<InventoryEquipmentSlot>) -> u8 {
|
||||
match slot {
|
||||
Some(InventoryEquipmentSlot::Weapon) => 0,
|
||||
Some(InventoryEquipmentSlot::Armor) => 1,
|
||||
Some(InventoryEquipmentSlot::Relic) => 2,
|
||||
None => 3,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_backpack_slot_key(slot_id: &str) -> String {
|
||||
slot_id.to_string()
|
||||
}
|
||||
|
||||
fn build_equipment_slot_key(slot: InventoryEquipmentSlot) -> String {
|
||||
slot.as_str().to_string()
|
||||
}
|
||||
|
||||
pub fn build_runtime_inventory_state_record(
|
||||
snapshot: RuntimeInventoryStateSnapshot,
|
||||
) -> RuntimeInventoryStateRecord {
|
||||
RuntimeInventoryStateRecord {
|
||||
runtime_session_id: snapshot.runtime_session_id,
|
||||
actor_user_id: snapshot.actor_user_id,
|
||||
backpack_items: snapshot
|
||||
.backpack_items
|
||||
.into_iter()
|
||||
.map(build_runtime_inventory_slot_record)
|
||||
.collect(),
|
||||
equipment_items: snapshot
|
||||
.equipment_items
|
||||
.into_iter()
|
||||
.map(build_runtime_inventory_slot_record)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_runtime_inventory_slot_record(slot: InventorySlotSnapshot) -> RuntimeInventorySlotRecord {
|
||||
RuntimeInventorySlotRecord {
|
||||
slot_id: slot.slot_id,
|
||||
container_kind: format_inventory_container_kind(slot.container_kind).to_string(),
|
||||
slot_key: slot.slot_key,
|
||||
item_id: slot.item_id,
|
||||
category: slot.category,
|
||||
name: slot.name,
|
||||
description: slot.description,
|
||||
quantity: slot.quantity,
|
||||
rarity: format_inventory_item_rarity(slot.rarity).to_string(),
|
||||
tags: slot.tags,
|
||||
stackable: slot.stackable,
|
||||
stack_key: slot.stack_key,
|
||||
equipment_slot_id: slot
|
||||
.equipment_slot_id
|
||||
.map(|value| value.as_str().to_string()),
|
||||
source_kind: format_inventory_item_source_kind(slot.source_kind).to_string(),
|
||||
source_reference_id: slot.source_reference_id,
|
||||
created_at: format_timestamp_micros(slot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(slot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_container_kind(value: InventoryContainerKind) -> &'static str {
|
||||
match value {
|
||||
InventoryContainerKind::Backpack => "backpack",
|
||||
InventoryContainerKind::Equipment => "equipment",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_item_rarity(value: InventoryItemRarity) -> &'static str {
|
||||
match value {
|
||||
InventoryItemRarity::Common => "common",
|
||||
InventoryItemRarity::Uncommon => "uncommon",
|
||||
InventoryItemRarity::Rare => "rare",
|
||||
InventoryItemRarity::Epic => "epic",
|
||||
InventoryItemRarity::Legendary => "legendary",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_inventory_item_source_kind(value: InventoryItemSourceKind) -> &'static str {
|
||||
match value {
|
||||
InventoryItemSourceKind::StoryReward => "story_reward",
|
||||
InventoryItemSourceKind::QuestReward => "quest_reward",
|
||||
InventoryItemSourceKind::TreasureReward => "treasure_reward",
|
||||
InventoryItemSourceKind::NpcGift => "npc_gift",
|
||||
InventoryItemSourceKind::NpcTrade => "npc_trade",
|
||||
InventoryItemSourceKind::CombatDrop => "combat_drop",
|
||||
InventoryItemSourceKind::ForgeCraft => "forge_craft",
|
||||
InventoryItemSourceKind::ForgeReforge => "forge_reforge",
|
||||
InventoryItemSourceKind::ManualPatch => "manual_patch",
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InventoryMutationFieldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingMutationId => f.write_str("inventory_mutation.mutation_id 不能为空"),
|
||||
Self::MissingRuntimeSessionId => {
|
||||
f.write_str("inventory_mutation.runtime_session_id 不能为空")
|
||||
}
|
||||
Self::MissingActorUserId => f.write_str("inventory_mutation.actor_user_id 不能为空"),
|
||||
Self::MissingSlotId => f.write_str("inventory_slot.slot_id 不能为空"),
|
||||
Self::MissingItemId => f.write_str("inventory_item.item_id 不能为空"),
|
||||
Self::MissingCategory => f.write_str("inventory_item.category 不能为空"),
|
||||
Self::MissingName => f.write_str("inventory_item.name 不能为空"),
|
||||
Self::InvalidQuantity => f.write_str("inventory_item.quantity 必须大于 0"),
|
||||
Self::MissingStackKey => f.write_str("可堆叠物品必须提供 stack_key"),
|
||||
Self::NonStackableItemMustStaySingleQuantity => {
|
||||
f.write_str("不可堆叠物品必须固定为单槽位单数量")
|
||||
}
|
||||
Self::EquipmentItemCannotStack => f.write_str("可装备物品不能标记为 stackable"),
|
||||
Self::SlotScopeMismatch => {
|
||||
f.write_str("当前 inventory_slot 不属于本次 mutation 作用域")
|
||||
}
|
||||
Self::ItemNotFound => f.write_str("目标 inventory_slot 不存在"),
|
||||
Self::ItemNotInBackpack => f.write_str("目标物品当前不在背包中"),
|
||||
Self::ItemNotEquipped => f.write_str("目标物品当前不在装备位上"),
|
||||
Self::InsufficientQuantity => f.write_str("当前背包数量不足,无法完成扣减"),
|
||||
Self::ItemNotEquippable => f.write_str("目标物品当前不可装备"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InventoryMutationFieldError {}
|
||||
pub use application::*;
|
||||
pub use commands::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
pub use events::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
Reference in New Issue
Block a user