313 lines
12 KiB
Rust
313 lines
12 KiB
Rust
mod application;
|
|
mod commands;
|
|
mod domain;
|
|
mod errors;
|
|
mod events;
|
|
|
|
pub use application::*;
|
|
pub use commands::*;
|
|
pub use domain::*;
|
|
pub use errors::*;
|
|
pub use events::*;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn build_stackable_item(quantity: u32) -> InventoryItemSnapshot {
|
|
InventoryItemSnapshot {
|
|
item_id: "consumable_heal_potion".to_string(),
|
|
category: "消耗品".to_string(),
|
|
name: "疗伤药".to_string(),
|
|
description: Some("用于恢复少量气血。".to_string()),
|
|
quantity,
|
|
rarity: InventoryItemRarity::Common,
|
|
tags: vec!["healing".to_string()],
|
|
stackable: true,
|
|
stack_key: "heal_potion".to_string(),
|
|
equipment_slot_id: None,
|
|
source_kind: InventoryItemSourceKind::TreasureReward,
|
|
source_reference_id: Some("treasure_001".to_string()),
|
|
}
|
|
}
|
|
|
|
fn build_weapon_item(slot_id: &str, name: &str) -> InventorySlotSnapshot {
|
|
InventorySlotSnapshot {
|
|
slot_id: slot_id.to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
container_kind: InventoryContainerKind::Backpack,
|
|
slot_key: slot_id.to_string(),
|
|
item_id: format!("weapon:{slot_id}"),
|
|
category: "武器".to_string(),
|
|
name: name.to_string(),
|
|
description: Some("测试武器".to_string()),
|
|
quantity: 1,
|
|
rarity: InventoryItemRarity::Rare,
|
|
tags: vec!["weapon".to_string(), "快剑".to_string()],
|
|
stackable: false,
|
|
stack_key: format!("weapon:{slot_id}"),
|
|
equipment_slot_id: Some(InventoryEquipmentSlot::Weapon),
|
|
source_kind: InventoryItemSourceKind::StoryReward,
|
|
source_reference_id: Some("storyevt_001".to_string()),
|
|
created_at_micros: 1,
|
|
updated_at_micros: 1,
|
|
}
|
|
}
|
|
|
|
fn build_mutation_input(mutation: InventoryMutation) -> InventoryMutationInput {
|
|
InventoryMutationInput {
|
|
mutation_id: "invmut_001".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
mutation,
|
|
updated_at_micros: 10,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn grant_item_merges_existing_stackable_slot() {
|
|
let current = vec![InventorySlotSnapshot {
|
|
slot_id: "invslot_existing".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
container_kind: InventoryContainerKind::Backpack,
|
|
slot_key: "invslot_existing".to_string(),
|
|
item_id: "consumable_heal_potion".to_string(),
|
|
category: "消耗品".to_string(),
|
|
name: "疗伤药".to_string(),
|
|
description: None,
|
|
quantity: 2,
|
|
rarity: InventoryItemRarity::Common,
|
|
tags: vec!["healing".to_string()],
|
|
stackable: true,
|
|
stack_key: "heal_potion".to_string(),
|
|
equipment_slot_id: None,
|
|
source_kind: InventoryItemSourceKind::TreasureReward,
|
|
source_reference_id: Some("treasure_000".to_string()),
|
|
created_at_micros: 1,
|
|
updated_at_micros: 1,
|
|
}];
|
|
|
|
let outcome = apply_inventory_mutation(
|
|
current,
|
|
build_mutation_input(InventoryMutation::GrantItem(GrantInventoryItemInput {
|
|
slot_id: "invslot_new".to_string(),
|
|
item: build_stackable_item(3),
|
|
})),
|
|
)
|
|
.expect("grant should merge stackable row");
|
|
|
|
assert!(outcome.changed);
|
|
assert_eq!(outcome.next_slots.len(), 1);
|
|
assert_eq!(outcome.next_slots[0].quantity, 5);
|
|
assert_eq!(
|
|
outcome.updated_slot_ids,
|
|
vec!["invslot_existing".to_string()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grant_non_stackable_item_requires_single_quantity() {
|
|
let error = apply_inventory_mutation(
|
|
vec![],
|
|
build_mutation_input(InventoryMutation::GrantItem(GrantInventoryItemInput {
|
|
slot_id: "invslot_weapon".to_string(),
|
|
item: InventoryItemSnapshot {
|
|
item_id: "weapon_001".to_string(),
|
|
category: "武器".to_string(),
|
|
name: "试作短剑".to_string(),
|
|
description: None,
|
|
quantity: 2,
|
|
rarity: InventoryItemRarity::Rare,
|
|
tags: vec!["weapon".to_string()],
|
|
stackable: false,
|
|
stack_key: String::new(),
|
|
equipment_slot_id: Some(InventoryEquipmentSlot::Weapon),
|
|
source_kind: InventoryItemSourceKind::StoryReward,
|
|
source_reference_id: None,
|
|
},
|
|
})),
|
|
)
|
|
.expect_err("non-stackable item quantity must stay single");
|
|
|
|
assert_eq!(
|
|
error,
|
|
InventoryMutationFieldError::NonStackableItemMustStaySingleQuantity
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn consume_item_removes_slot_when_quantity_exhausted() {
|
|
let current = vec![InventorySlotSnapshot {
|
|
slot_id: "invslot_potion".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
container_kind: InventoryContainerKind::Backpack,
|
|
slot_key: "invslot_potion".to_string(),
|
|
item_id: "consumable_heal_potion".to_string(),
|
|
category: "消耗品".to_string(),
|
|
name: "疗伤药".to_string(),
|
|
description: None,
|
|
quantity: 1,
|
|
rarity: InventoryItemRarity::Common,
|
|
tags: vec!["healing".to_string()],
|
|
stackable: true,
|
|
stack_key: "heal_potion".to_string(),
|
|
equipment_slot_id: None,
|
|
source_kind: InventoryItemSourceKind::TreasureReward,
|
|
source_reference_id: None,
|
|
created_at_micros: 1,
|
|
updated_at_micros: 1,
|
|
}];
|
|
|
|
let outcome = apply_inventory_mutation(
|
|
current,
|
|
build_mutation_input(InventoryMutation::ConsumeItem(ConsumeInventoryItemInput {
|
|
slot_id: "invslot_potion".to_string(),
|
|
quantity: 1,
|
|
})),
|
|
)
|
|
.expect("consume should remove exhausted slot");
|
|
|
|
assert!(outcome.next_slots.is_empty());
|
|
assert_eq!(outcome.removed_slot_ids, vec!["invslot_potion".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn equip_item_swaps_existing_equipment_back_to_backpack() {
|
|
let equipped = InventorySlotSnapshot {
|
|
container_kind: InventoryContainerKind::Equipment,
|
|
slot_key: InventoryEquipmentSlot::Weapon.as_str().to_string(),
|
|
..build_weapon_item("invslot_old_weapon", "旧佩剑")
|
|
};
|
|
let backpack_weapon = build_weapon_item("invslot_new_weapon", "逐风短剑");
|
|
|
|
let outcome = apply_inventory_mutation(
|
|
vec![equipped, backpack_weapon],
|
|
build_mutation_input(InventoryMutation::EquipItem(EquipInventoryItemInput {
|
|
slot_id: "invslot_new_weapon".to_string(),
|
|
})),
|
|
)
|
|
.expect("equip should swap weapon");
|
|
|
|
assert!(outcome.changed);
|
|
assert_eq!(
|
|
outcome.affected_equipment_slot,
|
|
Some(InventoryEquipmentSlot::Weapon)
|
|
);
|
|
|
|
let weapon_slot = outcome
|
|
.next_slots
|
|
.iter()
|
|
.find(|slot| slot.slot_id == "invslot_new_weapon")
|
|
.expect("new weapon slot should exist");
|
|
assert_eq!(
|
|
weapon_slot.container_kind,
|
|
InventoryContainerKind::Equipment
|
|
);
|
|
assert_eq!(weapon_slot.slot_key, "weapon");
|
|
|
|
let old_weapon_slot = outcome
|
|
.next_slots
|
|
.iter()
|
|
.find(|slot| slot.slot_id == "invslot_old_weapon")
|
|
.expect("old weapon slot should exist");
|
|
assert_eq!(
|
|
old_weapon_slot.container_kind,
|
|
InventoryContainerKind::Backpack
|
|
);
|
|
assert_eq!(old_weapon_slot.slot_key, "invslot_old_weapon");
|
|
}
|
|
|
|
#[test]
|
|
fn unequip_item_moves_equipment_back_to_backpack() {
|
|
let equipped = InventorySlotSnapshot {
|
|
container_kind: InventoryContainerKind::Equipment,
|
|
slot_key: InventoryEquipmentSlot::Relic.as_str().to_string(),
|
|
equipment_slot_id: Some(InventoryEquipmentSlot::Relic),
|
|
..build_weapon_item("invslot_relic", "旧誓护符")
|
|
};
|
|
|
|
let outcome = apply_inventory_mutation(
|
|
vec![equipped],
|
|
build_mutation_input(InventoryMutation::UnequipItem(UnequipInventoryItemInput {
|
|
slot_id: "invslot_relic".to_string(),
|
|
})),
|
|
)
|
|
.expect("unequip should move relic back to backpack");
|
|
|
|
assert!(outcome.changed);
|
|
assert_eq!(
|
|
outcome.affected_equipment_slot,
|
|
Some(InventoryEquipmentSlot::Relic)
|
|
);
|
|
assert_eq!(outcome.next_slots.len(), 1);
|
|
assert_eq!(
|
|
outcome.next_slots[0].container_kind,
|
|
InventoryContainerKind::Backpack
|
|
);
|
|
assert_eq!(outcome.next_slots[0].slot_key, "invslot_relic");
|
|
}
|
|
|
|
#[test]
|
|
fn build_runtime_inventory_state_query_input_trims_scope_fields() {
|
|
let input = build_runtime_inventory_state_query_input(
|
|
" runtime_001 ".to_string(),
|
|
" user_001 ".to_string(),
|
|
)
|
|
.expect("query input should build");
|
|
|
|
assert_eq!(input.runtime_session_id, "runtime_001");
|
|
assert_eq!(input.actor_user_id, "user_001");
|
|
}
|
|
|
|
#[test]
|
|
fn build_runtime_inventory_state_snapshot_splits_backpack_and_equipment() {
|
|
let snapshot = build_runtime_inventory_state_snapshot(
|
|
RuntimeInventoryStateQueryInput {
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
actor_user_id: "user_001".to_string(),
|
|
},
|
|
vec![
|
|
InventorySlotSnapshot {
|
|
container_kind: InventoryContainerKind::Equipment,
|
|
slot_key: "weapon".to_string(),
|
|
..build_weapon_item("invslot_weapon", "逐风短剑")
|
|
},
|
|
InventorySlotSnapshot {
|
|
slot_id: "invslot_potion".to_string(),
|
|
runtime_session_id: "runtime_001".to_string(),
|
|
story_session_id: Some("storysess_001".to_string()),
|
|
actor_user_id: "user_001".to_string(),
|
|
container_kind: InventoryContainerKind::Backpack,
|
|
slot_key: "invslot_potion".to_string(),
|
|
item_id: "consumable_heal_potion".to_string(),
|
|
category: "消耗品".to_string(),
|
|
name: "疗伤药".to_string(),
|
|
description: Some("用于恢复少量气血。".to_string()),
|
|
quantity: 2,
|
|
rarity: InventoryItemRarity::Common,
|
|
tags: vec!["healing".to_string()],
|
|
stackable: true,
|
|
stack_key: "heal_potion".to_string(),
|
|
equipment_slot_id: None,
|
|
source_kind: InventoryItemSourceKind::TreasureReward,
|
|
source_reference_id: Some("treasure_001".to_string()),
|
|
created_at_micros: 1,
|
|
updated_at_micros: 2,
|
|
},
|
|
],
|
|
);
|
|
|
|
assert_eq!(snapshot.backpack_items.len(), 1);
|
|
assert_eq!(snapshot.equipment_items.len(), 1);
|
|
assert_eq!(snapshot.backpack_items[0].slot_id, "invslot_potion");
|
|
assert_eq!(snapshot.equipment_items[0].slot_id, "invslot_weapon");
|
|
}
|
|
}
|