//! 运行时物品应用编排。 //! //! 这里只返回奖励结果、记录快照和待写入背包事件。 use crate::commands::TreasureResolveInput; use crate::domain::{ RuntimeItemEquipmentSlot, RuntimeItemRewardItemRarity, RuntimeItemRewardItemSnapshot, TreasureRecordSnapshot, }; use crate::errors::TreasureFieldError; use module_inventory::{ InventoryEquipmentSlot, InventoryItemRarity, InventoryItemSnapshot, InventoryItemSourceKind, }; use serde::{Deserialize, Serialize}; use shared_kernel::{ 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 TreasureRecordProcedureResult { pub ok: bool, pub record: Option, pub error_message: Option, } pub fn build_treasure_record_snapshot( input: TreasureResolveInput, ) -> Result { validate_treasure_input(&input)?; Ok(TreasureRecordSnapshot { treasure_record_id: input.treasure_record_id, runtime_session_id: input.runtime_session_id, story_session_id: input.story_session_id, actor_user_id: input.actor_user_id, encounter_id: input.encounter_id, encounter_name: input.encounter_name, scene_id: normalize_optional_value(input.scene_id), scene_name: normalize_optional_value(input.scene_name), action: input.action, reward_items: input .reward_items .into_iter() .map(normalize_reward_item) .collect::, _>>()?, reward_hp: input.reward_hp, reward_mana: input.reward_mana, reward_currency: input.reward_currency, story_hint: normalize_optional_value(input.story_hint), created_at_micros: input.created_at_micros, updated_at_micros: input.updated_at_micros, }) } pub fn build_inventory_item_snapshot_from_reward_item( treasure_record_id: &str, reward_item: RuntimeItemRewardItemSnapshot, ) -> Result { let treasure_record_id = normalize_required_value( treasure_record_id.to_string(), TreasureFieldError::MissingTreasureRecordId, )?; let reward_item = normalize_reward_item(reward_item)?; Ok(InventoryItemSnapshot { item_id: reward_item.item_id, category: reward_item.category, name: reward_item.item_name, description: reward_item.description, quantity: reward_item.quantity, rarity: map_reward_item_rarity(reward_item.rarity), tags: reward_item.tags, stackable: reward_item.stackable, stack_key: reward_item.stack_key, equipment_slot_id: reward_item .equipment_slot_id .map(map_reward_item_equipment_slot), source_kind: InventoryItemSourceKind::TreasureReward, source_reference_id: Some(treasure_record_id), }) } pub fn normalize_reward_item_snapshot( reward_item: RuntimeItemRewardItemSnapshot, ) -> Result { normalize_reward_item(reward_item) } fn validate_treasure_input(input: &TreasureResolveInput) -> Result<(), TreasureFieldError> { if input.treasure_record_id.trim().is_empty() { return Err(TreasureFieldError::MissingTreasureRecordId); } if input.runtime_session_id.trim().is_empty() { return Err(TreasureFieldError::MissingRuntimeSessionId); } if input.story_session_id.trim().is_empty() { return Err(TreasureFieldError::MissingStorySessionId); } if input.actor_user_id.trim().is_empty() { return Err(TreasureFieldError::MissingActorUserId); } if input.encounter_id.trim().is_empty() { return Err(TreasureFieldError::MissingEncounterId); } if input.encounter_name.trim().is_empty() { return Err(TreasureFieldError::MissingEncounterName); } Ok(()) } fn normalize_optional_value(value: Option) -> Option { normalize_shared_optional_string(value) } fn normalize_reward_item( mut item: RuntimeItemRewardItemSnapshot, ) -> Result { item.item_id = normalize_required_value(item.item_id, TreasureFieldError::MissingRewardItemId)?; item.category = normalize_required_value(item.category, TreasureFieldError::MissingRewardItemCategory)?; item.item_name = normalize_required_value(item.item_name, TreasureFieldError::MissingRewardItemName)?; item.description = normalize_optional_value(item.description); if item.quantity == 0 { return Err(TreasureFieldError::InvalidRewardItemQuantity); } if !item.stackable && item.quantity != 1 { return Err(TreasureFieldError::RewardNonStackableItemMustStaySingleQuantity); } if item.equipment_slot_id.is_some() && item.stackable { return Err(TreasureFieldError::RewardEquipmentItemCannotStack); } item.tags = normalize_string_list(item.tags); item.stack_key = if item.stackable { normalize_required_value( item.stack_key, TreasureFieldError::MissingRewardItemStackKey, )? } else { normalize_optional_value(Some(item.stack_key)).unwrap_or_else(|| item.item_id.clone()) }; Ok(item) } fn normalize_required_value( value: String, error: TreasureFieldError, ) -> Result { normalize_required_string(value).ok_or(error) } fn normalize_string_list(values: Vec) -> Vec { normalize_shared_string_list(values) } fn map_reward_item_rarity(rarity: RuntimeItemRewardItemRarity) -> InventoryItemRarity { match rarity { RuntimeItemRewardItemRarity::Common => InventoryItemRarity::Common, RuntimeItemRewardItemRarity::Uncommon => InventoryItemRarity::Uncommon, RuntimeItemRewardItemRarity::Rare => InventoryItemRarity::Rare, RuntimeItemRewardItemRarity::Epic => InventoryItemRarity::Epic, RuntimeItemRewardItemRarity::Legendary => InventoryItemRarity::Legendary, } } fn map_reward_item_equipment_slot(slot: RuntimeItemEquipmentSlot) -> InventoryEquipmentSlot { match slot { RuntimeItemEquipmentSlot::Weapon => InventoryEquipmentSlot::Weapon, RuntimeItemEquipmentSlot::Armor => InventoryEquipmentSlot::Armor, RuntimeItemEquipmentSlot::Relic => InventoryEquipmentSlot::Relic, } }