Close DDD cleanup and tests-support closure
This commit is contained in:
@@ -1,3 +1,291 @@
|
||||
//! 战斗应用编排过渡落位。
|
||||
//! 战斗应用编排。
|
||||
//!
|
||||
//! 这里只返回结算结果与待处理事件,不直接写入其他上下文表。
|
||||
|
||||
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<BattleStateSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ResolveCombatActionProcedureResult {
|
||||
pub ok: bool,
|
||||
pub result: Option<ResolveCombatActionResult>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
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<ResolveCombatActionResult, CombatFieldError> {
|
||||
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
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user