Files
Genarrative/server-rs/crates/module-inventory/src/lib.rs

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");
}
}