//! 战斗写入命令。 //! //! 用于表达创建战斗、使用技能、逃离和结算等输入,不直接携带表行类型。 use crate::domain::{BattleMode, LEGACY_ATTACK_FUNCTION_IDS}; use crate::errors::CombatFieldError; use module_runtime_item::{ RuntimeItemRewardItemSnapshot, TreasureFieldError, normalize_reward_item_snapshot, }; use serde::{Deserialize, Serialize}; use shared_kernel::normalize_required_string; #[cfg(feature = "spacetime-types")] use spacetimedb::SpacetimeType; #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BattleStateInput { pub battle_state_id: String, pub story_session_id: String, pub runtime_session_id: String, pub actor_user_id: String, pub chapter_id: Option, pub target_npc_id: String, pub target_name: String, pub battle_mode: BattleMode, pub player_hp: i32, pub player_max_hp: i32, pub player_mana: i32, pub player_max_mana: i32, pub target_hp: i32, pub target_max_hp: i32, pub experience_reward: u32, pub reward_items: Vec, pub created_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolveCombatActionInput { pub battle_state_id: String, pub function_id: String, pub action_text: String, pub base_damage: i32, pub mana_cost: i32, pub heal: i32, pub mana_restore: i32, pub counter_multiplier_basis_points: u32, pub updated_at_micros: i64, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BattleStateQueryInput { pub battle_state_id: String, } pub fn validate_battle_state_input(input: &BattleStateInput) -> Result<(), CombatFieldError> { if normalize_required_string(&input.battle_state_id).is_none() { return Err(CombatFieldError::MissingBattleStateId); } if normalize_required_string(&input.story_session_id).is_none() { return Err(CombatFieldError::MissingStorySessionId); } if normalize_required_string(&input.runtime_session_id).is_none() { return Err(CombatFieldError::MissingRuntimeSessionId); } if normalize_required_string(&input.actor_user_id).is_none() { return Err(CombatFieldError::MissingActorUserId); } if normalize_required_string(&input.target_npc_id).is_none() { return Err(CombatFieldError::MissingTargetNpcId); } if normalize_required_string(&input.target_name).is_none() { return Err(CombatFieldError::MissingTargetName); } if input.player_max_hp <= 0 || input.player_hp <= 0 || input.player_hp > input.player_max_hp { return Err(CombatFieldError::InvalidPlayerVitals); } if input.player_max_mana < 0 || input.player_mana < 0 || input.player_mana > input.player_max_mana { return Err(CombatFieldError::InvalidPlayerVitals); } if input.target_max_hp <= 0 || input.target_hp <= 0 || input.target_hp > input.target_max_hp { return Err(CombatFieldError::InvalidTargetVitals); } for reward_item in input.reward_items.iter().cloned() { normalize_reward_item_snapshot(reward_item).map_err(map_reward_item_field_error)?; } Ok(()) } pub fn validate_resolve_combat_action_input( input: &ResolveCombatActionInput, ) -> Result<(), CombatFieldError> { if normalize_required_string(&input.battle_state_id).is_none() { return Err(CombatFieldError::MissingBattleStateId); } if normalize_required_string(&input.function_id).is_none() { return Err(CombatFieldError::MissingFunctionId); } if !is_supported_combat_function_id(&input.function_id) { return Err(CombatFieldError::UnsupportedFunctionId); } Ok(()) } pub fn build_battle_state_query_input( battle_state_id: String, ) -> Result { let input = BattleStateQueryInput { battle_state_id: normalize_required_string(battle_state_id).unwrap_or_default(), }; validate_battle_state_query_input(&input)?; Ok(input) } pub fn validate_battle_state_query_input( input: &BattleStateQueryInput, ) -> Result<(), CombatFieldError> { if normalize_required_string(&input.battle_state_id).is_none() { return Err(CombatFieldError::MissingBattleStateId); } Ok(()) } pub fn is_supported_combat_function_id(function_id: &str) -> bool { matches!( function_id, "battle_attack_basic" | "battle_recover_breath" | "battle_use_skill" | "battle_escape_breakout" ) || LEGACY_ATTACK_FUNCTION_IDS.contains(&function_id) } fn map_reward_item_field_error(error: TreasureFieldError) -> CombatFieldError { let message = match error { TreasureFieldError::MissingRewardItemId => { "battle_state.reward_items[].item_id 不能为空".to_string() } TreasureFieldError::MissingRewardItemCategory => { "battle_state.reward_items[].category 不能为空".to_string() } TreasureFieldError::MissingRewardItemName => { "battle_state.reward_items[].item_name 不能为空".to_string() } TreasureFieldError::InvalidRewardItemQuantity => { "battle_state.reward_items[].quantity 必须大于 0".to_string() } TreasureFieldError::MissingRewardItemStackKey => { "battle_state.reward_items[].stack_key 不能为空".to_string() } TreasureFieldError::RewardEquipmentItemCannotStack => { "battle_state.reward_items[] 可装备物品不能标记为 stackable".to_string() } TreasureFieldError::RewardNonStackableItemMustStaySingleQuantity => { "battle_state.reward_items[] 不可堆叠物品必须固定为单槽位单数量".to_string() } other => other.to_string(), }; CombatFieldError::InvalidRewardItem(message) }