//! 战斗应用编排。 //! //! 这里只返回结算结果与待处理事件,不直接写入其他上下文表。 use crate::commands::{ BattleStateInput, ResolveCombatActionInput, validate_resolve_combat_action_input, }; use crate::domain::{ BASIC_FIGHT_COUNTER_RATIO, BATTLE_STATE_ID_PREFIX, BattleMode, BattleStateSnapshot, BattleStatus, CombatOutcome, INITIAL_BATTLE_VERSION, MIN_FIGHT_COUNTER_DAMAGE, SPAR_MIN_HP, }; use crate::errors::CombatFieldError; use serde::{Deserialize, Serialize}; use shared_kernel::{build_prefixed_seed_id, 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 ResolveCombatActionResult { pub snapshot: BattleStateSnapshot, pub damage_dealt: i32, pub damage_taken: i32, pub outcome: CombatOutcome, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BattleStateProcedureResult { pub ok: bool, pub snapshot: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolveCombatActionProcedureResult { pub ok: bool, pub result: Option, pub error_message: Option, } pub fn build_battle_state_snapshot(input: BattleStateInput) -> BattleStateSnapshot { BattleStateSnapshot { battle_state_id: input.battle_state_id, story_session_id: input.story_session_id, runtime_session_id: input.runtime_session_id, actor_user_id: input.actor_user_id, chapter_id: input.chapter_id, target_npc_id: input.target_npc_id, target_name: input.target_name, battle_mode: input.battle_mode, status: BattleStatus::Ongoing, player_hp: input.player_hp, player_max_hp: input.player_max_hp, player_mana: input.player_mana, player_max_mana: input.player_max_mana, target_hp: input.target_hp, target_max_hp: input.target_max_hp, experience_reward: input.experience_reward, reward_items: input.reward_items, turn_index: 0, last_action_function_id: None, last_action_text: None, last_result_text: None, last_damage_dealt: 0, last_damage_taken: 0, last_outcome: CombatOutcome::Ongoing, version: INITIAL_BATTLE_VERSION, created_at_micros: input.created_at_micros, updated_at_micros: input.created_at_micros, } } pub fn resolve_combat_action( current: BattleStateSnapshot, input: ResolveCombatActionInput, ) -> Result { validate_resolve_combat_action_input(&input)?; if current.version == 0 { return Err(CombatFieldError::InvalidVersion); } if current.status != BattleStatus::Ongoing { return Err(CombatFieldError::BattleAlreadyResolved); } if current.player_mana < input.mana_cost.max(0) { return Err(CombatFieldError::InsufficientMana); } let action_text = if input.action_text.trim().is_empty() { input.function_id.clone() } else { normalize_required_string(input.action_text).unwrap_or_else(|| input.function_id.clone()) }; if input.function_id == "battle_escape_breakout" { let next = BattleStateSnapshot { status: BattleStatus::Resolved, turn_index: current.turn_index + 1, last_action_function_id: Some(input.function_id), last_action_text: Some(action_text), last_result_text: Some(format!("你抓住空当摆脱了{}的压制。", current.target_name)), last_damage_dealt: 0, last_damage_taken: 0, last_outcome: CombatOutcome::Escaped, version: current.version + 1, updated_at_micros: input.updated_at_micros, ..current }; return Ok(ResolveCombatActionResult { snapshot: next, damage_dealt: 0, damage_taken: 0, outcome: CombatOutcome::Escaped, }); } let mana_cost = input.mana_cost.max(0); let heal = input.heal.max(0); let mana_restore = input.mana_restore.max(0); let base_damage = input.base_damage.max(0); let mut next_player_hp = current.player_hp; let mut next_player_mana = (current.player_mana - mana_cost).max(0); let mut next_target_hp = current.target_hp; let mut damage_dealt = 0; let mut damage_taken = 0; next_player_hp = clamp_hp( current.battle_mode, next_player_hp + heal, current.player_max_hp, ); next_player_mana = clamp_mana(next_player_mana + mana_restore, current.player_max_mana); if base_damage > 0 { next_target_hp = clamp_target_hp_after_damage(current.battle_mode, current.target_hp, base_damage); damage_dealt = current.target_hp - next_target_hp; } let (status, outcome, result_text) = if is_target_resolved(current.battle_mode, next_target_hp) { let outcome = match current.battle_mode { BattleMode::Fight => CombatOutcome::Victory, BattleMode::Spar => CombatOutcome::SparComplete, }; ( BattleStatus::Resolved, outcome, build_resolved_result_text(&action_text, ¤t.target_name, outcome), ) } else { damage_taken = compute_counter_damage( current.battle_mode, current.target_max_hp, input.counter_multiplier_basis_points, ); next_player_hp = clamp_hp( current.battle_mode, next_player_hp - damage_taken, current.player_max_hp, ); ( BattleStatus::Ongoing, CombatOutcome::Ongoing, build_ongoing_result_text(&input.function_id, &action_text, ¤t.target_name), ) }; let next = BattleStateSnapshot { player_hp: next_player_hp, player_mana: next_player_mana, target_hp: next_target_hp, status, turn_index: current.turn_index + 1, last_action_function_id: Some(input.function_id), last_action_text: Some(action_text), last_result_text: Some(result_text), last_damage_dealt: damage_dealt, last_damage_taken: damage_taken, last_outcome: outcome, version: current.version + 1, updated_at_micros: input.updated_at_micros, ..current }; Ok(ResolveCombatActionResult { snapshot: next, damage_dealt, damage_taken, outcome, }) } pub fn generate_battle_state_id(seed_micros: i64) -> String { build_prefixed_seed_id(BATTLE_STATE_ID_PREFIX, seed_micros) } fn clamp_hp(mode: BattleMode, value: i32, max_hp: i32) -> i32 { let min_hp = match mode { BattleMode::Fight => 0, BattleMode::Spar => SPAR_MIN_HP, }; value.clamp(min_hp, max_hp) } fn clamp_mana(value: i32, max_mana: i32) -> i32 { value.clamp(0, max_mana) } fn clamp_target_hp_after_damage(mode: BattleMode, current_hp: i32, damage: i32) -> i32 { match mode { BattleMode::Fight => (current_hp - damage).max(0), BattleMode::Spar => (current_hp - damage).max(SPAR_MIN_HP), } } fn is_target_resolved(mode: BattleMode, target_hp: i32) -> bool { match mode { BattleMode::Fight => target_hp <= 0, BattleMode::Spar => target_hp <= SPAR_MIN_HP, } } fn compute_counter_damage( mode: BattleMode, target_max_hp: i32, counter_multiplier_basis_points: u32, ) -> i32 { match mode { BattleMode::Spar => 1, BattleMode::Fight => { let multiplier = counter_multiplier_basis_points as f32 / 10_000.0; let raw = (target_max_hp as f32 * BASIC_FIGHT_COUNTER_RATIO * multiplier).round() as i32; raw.max(MIN_FIGHT_COUNTER_DAMAGE) } } } fn build_resolved_result_text( action_text: &str, target_name: &str, outcome: CombatOutcome, ) -> String { match outcome { CombatOutcome::Victory => { format!( "{}命中了{},这轮战斗已经正式结束。", action_text, target_name ) } CombatOutcome::SparComplete => { format!( "{}压住了{}的节奏,这场切磋已经分出高下。", action_text, target_name ) } CombatOutcome::Escaped => { format!("{}后你成功脱离了当前战斗。", action_text) } CombatOutcome::Ongoing => format!("{}已完成结算。", action_text), } } fn build_ongoing_result_text(function_id: &str, action_text: &str, target_name: &str) -> String { match function_id { "battle_recover_breath" => { format!( "你先把伤势和气息稳住了一轮,但{}仍在持续逼近。", target_name ) } "battle_use_skill" => { format!( "{}命中了{},这一轮技能效果已经直接结算。", action_text, target_name ) } _ => format!( "{}命中了{},本次攻击已经完成结算。", action_text, target_name ), } }