use serde_json::{Map, Value, json}; use shared_contracts::runtime_story::{ RuntimeBattlePresentation, RuntimeStoryActionRequest, RuntimeStoryOptionView, RuntimeStoryPatch, }; use crate::{ StoryResolution, append_active_build_buffs, build_status_patch, clear_encounter_only, clear_encounter_state, current_encounter_id, current_encounter_name_from_battle, ensure_json_object, first_hostile_npc_string_field, grant_player_progression_experience, increment_runtime_stat, read_array_field, read_field, read_i32_field, read_object_field, read_optional_string_field, remove_player_inventory_item, resolve_action_text, write_bool_field, write_current_encounter_i32_field, write_first_hostile_npc_i32_field, write_i32_field, write_null_field, write_string_field, }; /// 战斗纯结算链已经不依赖 HTTP / AppState。 /// /// 这里同时承接 battle action 的状态结算、资源恢复和战斗选项编译, /// 让 `api-server` 只保留 HTTP 外壳与最终响应拼装。 struct BattleActionPlan { action_text: String, result_text: String, damage_dealt: i32, damage_taken: i32, heal: i32, mana_restore: i32, mana_cost: i32, cooldown_tick_turns: i32, cooldown_bonus_turns: i32, applied_skill_cooldown: Option<(String, i32)>, build_buffs: Vec, consumed_item_id: Option, } struct BattleSkillView { id: String, name: String, damage: i32, mana_cost: i32, cooldown_turns: i32, build_buffs: Vec, } pub struct BattleInventoryUseProfile { hp_restore: i32, mana_restore: i32, cooldown_reduction: i32, build_buffs: Vec, } struct BattleInventoryItemView { id: String, name: String, quantity: i32, use_profile: Option, } /// 战斗结算的胜负状态。 /// /// 这里显式补齐失败分支,避免“玩家已死但敌方也被打空时”被错误归类成胜利。 #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum BattleResolutionOutcome { Ongoing, Victory, SparComplete, Defeat, } impl BattleResolutionOutcome { fn as_battle_outcome(self) -> &'static str { match self { Self::Ongoing => "ongoing", Self::Victory => "victory", Self::SparComplete => "spar_complete", Self::Defeat => "defeat", } } fn as_status_outcome(self) -> Option<&'static str> { match self { Self::Ongoing => None, Self::Victory => Some("fight_victory"), Self::SparComplete => Some("spar_complete"), Self::Defeat => Some("fight_defeat"), } } } pub fn resolve_battle_action( game_state: &mut Value, request: &RuntimeStoryActionRequest, function_id: &str, ) -> Result { let target_id = current_encounter_id(game_state) .or_else(|| first_hostile_npc_string_field(game_state, "id")) .unwrap_or_else(|| "battle_target".to_string()); let target_name = current_encounter_name_from_battle(game_state); let battle_mode = read_optional_string_field(game_state, "currentNpcBattleMode") .unwrap_or_else(|| "fight".to_string()); if function_id == "battle_escape_breakout" { clear_encounter_state(game_state); return Ok(StoryResolution { action_text: resolve_action_text("强行脱离战斗", request), result_text: "你抓住空隙强行脱离战斗,把这一轮危险先甩在身后。".to_string(), story_text: None, presentation_options: None, saved_current_story: None, patches: vec![ RuntimeStoryPatch::BattleResolved { function_id: function_id.to_string(), target_id: Some(target_id.clone()), damage_dealt: Some(0), damage_taken: Some(0), outcome: "escaped".to_string(), }, build_status_patch(game_state), RuntimeStoryPatch::EncounterChanged { encounter_id: None }, ], battle: Some(RuntimeBattlePresentation { target_id: Some(target_id), target_name: Some(target_name), damage_dealt: Some(0), damage_taken: Some(0), outcome: Some("escaped".to_string()), }), toast: Some("已脱离战斗".to_string()), }); } let plan = build_battle_action_plan(game_state, request, function_id)?; spend_player_mana(game_state, plan.mana_cost); restore_player_resource(game_state, plan.heal, plan.mana_restore); tick_player_skill_cooldowns(game_state, plan.cooldown_tick_turns); reduce_player_skill_cooldowns(game_state, plan.cooldown_bonus_turns); if let Some((skill_id, turns)) = plan.applied_skill_cooldown.as_ref() { set_player_skill_cooldown(game_state, skill_id.as_str(), *turns); } if !plan.build_buffs.is_empty() { append_active_build_buffs(game_state, plan.build_buffs.clone()); } if let Some(item_id) = plan.consumed_item_id.as_ref() { remove_player_inventory_item(game_state, item_id.as_str(), 1); increment_runtime_stat(game_state, "itemsUsed", 1); } let player_hp = apply_player_damage(game_state, plan.damage_taken); let target_hp = apply_target_damage(game_state, plan.damage_dealt); let outcome = resolve_battle_resolution_outcome(player_hp, target_hp, battle_mode.as_str()); let victory_experience = if outcome == BattleResolutionOutcome::Victory { battle_victory_experience_reward(game_state) } else { 0 }; if outcome != BattleResolutionOutcome::Ongoing { write_bool_field(game_state, "inBattle", false); write_bool_field(game_state, "npcInteractionActive", false); write_null_field(game_state, "currentNpcBattleMode"); if let Some(status_outcome) = outcome.as_status_outcome() { write_string_field(game_state, "currentNpcBattleOutcome", status_outcome); } if outcome == BattleResolutionOutcome::Victory { clear_encounter_only(game_state); increment_runtime_stat(game_state, "hostileNpcsDefeated", 1); if victory_experience > 0 { grant_player_progression_experience(game_state, victory_experience, "hostile_npc"); } } else if outcome == BattleResolutionOutcome::Defeat { clear_encounter_state(game_state); } } let mut patches = vec![ RuntimeStoryPatch::BattleResolved { function_id: function_id.to_string(), target_id: Some(target_id.clone()), damage_dealt: Some(plan.damage_dealt), damage_taken: Some(plan.damage_taken), outcome: outcome.as_battle_outcome().to_string(), }, build_status_patch(game_state), ]; if outcome == BattleResolutionOutcome::Victory { patches.push(RuntimeStoryPatch::EncounterChanged { encounter_id: None }); } Ok(StoryResolution { action_text: resolve_action_text(plan.action_text.as_str(), request), result_text: if outcome == BattleResolutionOutcome::Ongoing { plan.result_text } else if outcome == BattleResolutionOutcome::SparComplete { format!("{target_name} 收住了最后一击,这场切磋已经分出结果。") } else if outcome == BattleResolutionOutcome::Defeat { format!("你在与 {target_name} 的交锋中被压制倒下,这场战斗以你的败北收束。") } else { format!("{target_name} 被你压制下去,眼前的战斗已经结束。") }, story_text: None, presentation_options: None, saved_current_story: None, patches, battle: Some(RuntimeBattlePresentation { target_id: Some(target_id), target_name: Some(target_name), damage_dealt: Some(plan.damage_dealt), damage_taken: Some(plan.damage_taken), outcome: Some(outcome.as_battle_outcome().to_string()), }), toast: battle_action_toast(function_id, request), }) } pub fn restore_player_resource(game_state: &mut Value, hp_restore: i32, mana_restore: i32) { let max_hp = read_i32_field(game_state, "playerMaxHp") .unwrap_or(1) .max(1); let max_mana = read_i32_field(game_state, "playerMaxMana") .unwrap_or(0) .max(0); let hp = read_i32_field(game_state, "playerHp").unwrap_or(max_hp); let mana = read_i32_field(game_state, "playerMana").unwrap_or(max_mana); write_i32_field(game_state, "playerHp", (hp + hp_restore).clamp(0, max_hp)); write_i32_field( game_state, "playerMana", (mana + mana_restore).clamp(0, max_mana), ); } pub fn build_battle_runtime_story_options(game_state: &Value) -> Vec { let mut options = vec![ RuntimeStoryOptionView { detail_text: Some(build_basic_attack_detail_text(game_state)), ..build_static_runtime_story_option("battle_attack_basic", "普通攻击", "combat") }, RuntimeStoryOptionView { detail_text: Some("回血 12 / 回蓝 9 / 冷却 -1".to_string()), ..build_static_runtime_story_option("battle_recover_breath", "恢复", "combat") }, ]; let preferred_item = pick_preferred_battle_inventory_item(game_state); if let Some(item) = preferred_item { let effect = item .use_profile .expect("preferred battle item must have use profile"); options.push(build_runtime_story_option_with_payload( "inventory_use", &format!("使用物品:{}", item.name), "combat", Some(build_battle_item_summary(&effect)), json!({ "itemId": item.id }), )); } else { options.push(build_disabled_runtime_story_option( "inventory_use", "使用物品", "combat", Some("当前没有可直接结算的战斗消耗品".to_string()), "暂无可用物品", None, )); } options.extend(build_battle_skill_runtime_story_options(game_state)); options.push(build_static_runtime_story_option( "battle_escape_breakout", "强行脱离战斗", "combat", )); options } fn spend_player_mana(game_state: &mut Value, mana_cost: i32) { if mana_cost <= 0 { return; } let mana = read_i32_field(game_state, "playerMana").unwrap_or(0); write_i32_field(game_state, "playerMana", (mana - mana_cost).max(0)); } fn apply_player_damage(game_state: &mut Value, damage: i32) -> i32 { let hp = read_i32_field(game_state, "playerHp").unwrap_or(1).max(0); let next_hp = if damage <= 0 { hp } else { (hp - damage).max(0) }; write_i32_field(game_state, "playerHp", next_hp); next_hp } fn apply_target_damage(game_state: &mut Value, damage: i32) -> i32 { let target_hp = read_object_field(game_state, "currentEncounter") .and_then(|encounter| { read_i32_field(encounter, "hp") .or_else(|| read_i32_field(encounter, "currentHp")) .or_else(|| read_i32_field(encounter, "targetHp")) }) .or_else(|| { read_array_field(game_state, "sceneHostileNpcs") .first() .and_then(|target| read_i32_field(target, "hp")) }) .unwrap_or(24); let next_hp = target_hp - damage.max(0); write_current_encounter_i32_field(game_state, "hp", next_hp); write_first_hostile_npc_i32_field(game_state, "hp", next_hp); next_hp } fn resolve_battle_resolution_outcome( player_hp: i32, target_hp: i32, battle_mode: &str, ) -> BattleResolutionOutcome { // 中文注释:玩家死亡优先级高于敌方倒地。 // 这样即便同一回合双方都被打到 0,也必须按玩家败北处理,不能误发胜利。 if player_hp <= 0 { return BattleResolutionOutcome::Defeat; } if target_hp <= 0 { if battle_mode == "spar" { BattleResolutionOutcome::SparComplete } else { BattleResolutionOutcome::Victory } } else { BattleResolutionOutcome::Ongoing } } fn read_player_skills(game_state: &Value) -> Vec { read_field(game_state, "playerCharacter") .map(|character| read_array_field(character, "skills")) .unwrap_or_default() .into_iter() .filter_map(|entry| { let id = read_optional_string_field(entry, "id")?; let name = read_optional_string_field(entry, "name").unwrap_or_else(|| id.clone()); Some(BattleSkillView { id, name, damage: read_i32_field(entry, "damage").unwrap_or(14).max(0), mana_cost: read_i32_field(entry, "manaCost").unwrap_or(0).max(0), cooldown_turns: read_i32_field(entry, "cooldownTurns").unwrap_or(0).max(0), build_buffs: read_array_field(entry, "buildBuffs") .into_iter() .cloned() .collect(), }) }) .collect() } fn find_player_skill_by_id(game_state: &Value, skill_id: &str) -> Option { read_player_skills(game_state) .into_iter() .find(|skill| skill.id == skill_id) } fn build_basic_attack_detail_text(game_state: &Value) -> String { let strength = read_field(game_state, "playerCharacter") .and_then(|character| read_field(character, "attributes")) .and_then(|attributes| read_i32_field(attributes, "strength")) .unwrap_or(8); let agility = read_field(game_state, "playerCharacter") .and_then(|character| read_field(character, "attributes")) .and_then(|attributes| read_i32_field(attributes, "agility")) .unwrap_or(0); let preview_damage = ((strength * 85 + agility * 45) / 100).max(8); format!("不耗蓝 / 伤害 {preview_damage}") } fn read_player_skill_cooldowns(game_state: &Value) -> std::collections::BTreeMap { read_object_field(game_state, "playerSkillCooldowns") .and_then(Value::as_object) .map(|cooldowns| { cooldowns .iter() .map(|(skill_id, turns)| { ( skill_id.clone(), turns .as_i64() .and_then(|value| i32::try_from(value).ok()) .unwrap_or(0) .max(0), ) }) .collect() }) .unwrap_or_default() } fn build_battle_skill_runtime_story_options(game_state: &Value) -> Vec { let cooldowns = read_player_skill_cooldowns(game_state); let player_mana = read_i32_field(game_state, "playerMana").unwrap_or(0); read_player_skills(game_state) .into_iter() .map(|skill| { let detail_text = Some(format!( "耗蓝 {} / 伤害 {} / 冷却 {}", skill.mana_cost.max(0), skill.damage.max(0), skill.cooldown_turns.max(0) )); let payload = Some(json!({ "skillId": skill.id })); let remaining_cooldown = cooldowns.get(skill.id.as_str()).copied().unwrap_or(0); if remaining_cooldown > 0 { return build_disabled_runtime_story_option( "battle_use_skill", &skill.name, "combat", detail_text, format!("冷却中,还需 {} 回合", remaining_cooldown).as_str(), payload, ); } if skill.mana_cost > player_mana { return build_disabled_runtime_story_option( "battle_use_skill", &skill.name, "combat", detail_text, "灵力不足", payload, ); } RuntimeStoryOptionView { detail_text, payload, ..build_static_runtime_story_option("battle_use_skill", &skill.name, "combat") } }) .collect() } fn tick_player_skill_cooldowns(game_state: &mut Value, turns: i32) { if turns <= 0 { return; } let root = ensure_json_object(game_state); let cooldowns = root .entry("playerSkillCooldowns".to_string()) .or_insert_with(|| Value::Object(Map::new())); if !cooldowns.is_object() { *cooldowns = Value::Object(Map::new()); } let cooldowns = cooldowns .as_object_mut() .expect("playerSkillCooldowns should be object"); for value in cooldowns.values_mut() { let current = value .as_i64() .and_then(|number| i32::try_from(number).ok()) .unwrap_or(0); *value = json!((current - turns).max(0)); } } fn reduce_player_skill_cooldowns(game_state: &mut Value, turns: i32) { if turns <= 0 { return; } tick_player_skill_cooldowns(game_state, turns); } fn set_player_skill_cooldown(game_state: &mut Value, skill_id: &str, turns: i32) { let root = ensure_json_object(game_state); let cooldowns = root .entry("playerSkillCooldowns".to_string()) .or_insert_with(|| Value::Object(Map::new())); if !cooldowns.is_object() { *cooldowns = Value::Object(Map::new()); } cooldowns .as_object_mut() .expect("playerSkillCooldowns should be object") .insert(skill_id.to_string(), json!(turns.max(0))); } fn read_player_inventory_items(game_state: &Value) -> Vec { read_array_field(game_state, "playerInventory") .into_iter() .filter_map(|entry| { let id = read_optional_string_field(entry, "id")?; let name = read_optional_string_field(entry, "name").unwrap_or_else(|| id.clone()); let use_profile = read_field(entry, "useProfile").map(|profile| BattleInventoryUseProfile { hp_restore: read_i32_field(profile, "hpRestore").unwrap_or(0).max(0), mana_restore: read_i32_field(profile, "manaRestore").unwrap_or(0).max(0), cooldown_reduction: read_i32_field(profile, "cooldownReduction") .unwrap_or(0) .max(0), build_buffs: read_array_field(profile, "buildBuffs") .into_iter() .cloned() .collect(), }); Some(BattleInventoryItemView { id, name, quantity: read_i32_field(entry, "quantity").unwrap_or(0).max(0), use_profile, }) }) .collect() } pub fn read_inventory_item_use_profile(item: &Value) -> Option { read_field(item, "useProfile").map(|profile| BattleInventoryUseProfile { hp_restore: read_i32_field(profile, "hpRestore").unwrap_or(0).max(0), mana_restore: read_i32_field(profile, "manaRestore").unwrap_or(0).max(0), cooldown_reduction: read_i32_field(profile, "cooldownReduction") .unwrap_or(0) .max(0), build_buffs: read_array_field(profile, "buildBuffs") .into_iter() .cloned() .collect(), }) } pub fn inventory_item_has_usable_effect(item: &Value) -> bool { read_inventory_item_use_profile(item).is_some_and(|effect| { effect.hp_restore > 0 || effect.mana_restore > 0 || effect.cooldown_reduction > 0 || !effect.build_buffs.is_empty() }) } fn find_player_inventory_item( game_state: &Value, item_id: &str, ) -> Option { read_player_inventory_items(game_state) .into_iter() .find(|item| item.id == item_id) } /// 旧前端一次只展示一个“推荐”战斗物品,这里继续用确定性打分,避免展示面漂移。 fn pick_preferred_battle_inventory_item(game_state: &Value) -> Option { let has_cooling_skill = read_player_skill_cooldowns(game_state) .values() .any(|remaining| *remaining > 0); let player_hp = read_i32_field(game_state, "playerHp").unwrap_or(0); let player_max_hp = read_i32_field(game_state, "playerMaxHp") .unwrap_or(1) .max(1); let player_mana = read_i32_field(game_state, "playerMana").unwrap_or(0); let player_max_mana = read_i32_field(game_state, "playerMaxMana") .unwrap_or(1) .max(1); let hp_low = player_hp * 100 <= player_max_hp * 45; let mana_low = player_mana * 100 <= player_max_mana * 45; read_player_inventory_items(game_state) .into_iter() .filter(|item| item.quantity > 0 && item.use_profile.is_some()) .filter_map(|item| { let effect = item.use_profile.as_ref()?; let mut score = effect.build_buffs.len() as i32 * 8; score += effect.hp_restore * if hp_low { 3 } else { 1 }; score += effect.mana_restore * if mana_low { 2 } else { 1 }; score += effect.cooldown_reduction * if has_cooling_skill { 18 } else { 6 }; Some((score, item)) }) .max_by(|left, right| { left.0 .cmp(&right.0) .then_with(|| left.1.name.cmp(&right.1.name).reverse()) }) .map(|(_, item)| item) } fn build_battle_item_summary(effect: &BattleInventoryUseProfile) -> String { let mut parts = Vec::new(); if effect.hp_restore > 0 { parts.push(format!("回血 {}", effect.hp_restore)); } if effect.mana_restore > 0 { parts.push(format!("回蓝 {}", effect.mana_restore)); } if effect.cooldown_reduction > 0 { parts.push(format!("冷却 -{}", effect.cooldown_reduction)); } if !effect.build_buffs.is_empty() { let buff_names = effect .build_buffs .iter() .filter_map(|buff| read_optional_string_field(buff, "name")) .collect::>(); if !buff_names.is_empty() { parts.push(format!("增益 {}", buff_names.join("、"))); } } if parts.is_empty() { "立即结算一次物品效果".to_string() } else { parts.join(" / ") } } fn build_static_runtime_story_option( function_id: &str, action_text: &str, scope: &str, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { function_id: function_id.to_string(), action_text: action_text.to_string(), detail_text: None, scope: scope.to_string(), interaction: None, payload: None, disabled: None, reason: None, } } fn build_runtime_story_option_with_payload( function_id: &str, action_text: &str, scope: &str, detail_text: Option, payload: Value, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { detail_text, payload: Some(payload), ..build_static_runtime_story_option(function_id, action_text, scope) } } fn build_disabled_runtime_story_option( function_id: &str, action_text: &str, scope: &str, detail_text: Option, reason: &str, payload: Option, ) -> RuntimeStoryOptionView { RuntimeStoryOptionView { detail_text, payload, disabled: Some(true), reason: Some(reason.to_string()), ..build_static_runtime_story_option(function_id, action_text, scope) } } fn battle_victory_experience_reward(game_state: &Value) -> i32 { let hostile = read_array_field(game_state, "sceneHostileNpcs") .first() .copied() .or_else(|| read_field(game_state, "currentEncounter")); let explicit_reward = hostile .and_then(|entry| read_i32_field(entry, "experienceReward")) .unwrap_or(0) .max(0); if explicit_reward > 0 { return explicit_reward; } let level = hostile .and_then(|entry| read_field(entry, "levelProfile")) .and_then(|profile| read_i32_field(profile, "level")) .unwrap_or(1) .max(1); 12 + 6 * (level - 1) } fn battle_action_numbers( function_id: &str, ) -> (i32, i32, i32, i32, i32, &'static str, &'static str) { match function_id { "battle_recover_breath" => ( 0, 0, 8, 6, 0, "恢复", "你先稳住呼吸,把状态从危险边缘拉回一点。", ), "battle_use_skill" => ( 14, 4, 0, 0, 4, "施放技能", "你调动灵力打出一记更重的攻势,同时也承受了对方的反扑。", ), "battle_all_in_crush" => ( 22, 8, 0, 0, 6, "全力压制", "你把这一轮节奏全部压上去,试图用最强硬的方式打穿对方防线。", ), "battle_feint_step" => ( 6, 2, 0, 0, 0, "佯攻换位", "你用一次短促佯攻换开角度,虽然伤害有限,但避开了更重的反击。", ), "battle_finisher_window" => ( 18, 3, 0, 0, 3, "抓住终结窗口", "你抓住破绽打出决定性的一击,战斗天平明显向你倾斜。", ), "battle_guard_break" => ( 12, 5, 0, 0, 2, "破开防守", "你顶住压力破开对方防守,为后续行动争到更直接的窗口。", ), "battle_probe_pressure" => ( 5, 1, 0, 0, 0, "试探压迫", "你没有贸然压上,而是用轻攻测试对方反应。", ), _ => ( 10, 4, 0, 0, 0, "普通攻击", "你抓住当前窗口打出一记直接攻击,对方也立刻做出反击。", ), } } fn build_battle_action_plan( game_state: &Value, request: &RuntimeStoryActionRequest, function_id: &str, ) -> Result { if function_id == "battle_use_skill" { return build_skill_battle_action_plan(game_state, request); } if function_id == "inventory_use" { return build_inventory_use_battle_action_plan(game_state, request); } let (damage_dealt, damage_taken, heal, mana_restore, mana_cost, action_text, result_text) = battle_action_numbers(function_id); Ok(BattleActionPlan { action_text: action_text.to_string(), result_text: result_text.to_string(), damage_dealt, damage_taken, heal, mana_restore, mana_cost, cooldown_tick_turns: 1, cooldown_bonus_turns: 0, applied_skill_cooldown: None, build_buffs: Vec::new(), consumed_item_id: None, }) } fn build_skill_battle_action_plan( game_state: &Value, request: &RuntimeStoryActionRequest, ) -> Result { let payload = request .action .payload .as_ref() .ok_or_else(|| "battle_use_skill 缺少 skillId".to_string())?; let skill_id = read_optional_string_field(payload, "skillId") .ok_or_else(|| "battle_use_skill 缺少 skillId".to_string())?; let skill = find_player_skill_by_id(game_state, skill_id.as_str()) .ok_or_else(|| format!("未找到技能:{skill_id}"))?; let cooldowns = read_player_skill_cooldowns(game_state); if cooldowns.get(skill_id.as_str()).copied().unwrap_or(0) > 0 { return Err(format!("{} 仍在冷却中", skill.name)); } if skill.mana_cost > read_i32_field(game_state, "playerMana").unwrap_or(0) { return Err("当前灵力不足,无法执行这个战斗动作".to_string()); } Ok(BattleActionPlan { action_text: skill.name.clone(), result_text: format!("{} 命中了敌人,这一轮技能效果已经直接结算。", skill.name), damage_dealt: skill.damage.max(1), damage_taken: 4, heal: 0, mana_restore: 0, mana_cost: skill.mana_cost.max(0), cooldown_tick_turns: 1, cooldown_bonus_turns: 0, applied_skill_cooldown: Some((skill.id, skill.cooldown_turns.max(0))), build_buffs: skill.build_buffs, consumed_item_id: None, }) } fn build_inventory_use_battle_action_plan( game_state: &Value, request: &RuntimeStoryActionRequest, ) -> Result { let payload = request .action .payload .as_ref() .ok_or_else(|| "inventory_use 缺少 itemId".to_string())?; let item_id = read_optional_string_field(payload, "itemId") .ok_or_else(|| "inventory_use 缺少 itemId".to_string())?; let item = find_player_inventory_item(game_state, item_id.as_str()) .ok_or_else(|| "未找到可用于战斗结算的物品".to_string())?; if item.quantity <= 0 { return Err("未找到可用于战斗结算的物品".to_string()); } if item.use_profile.is_none() { return Err(format!("{} 当前没有可直接结算的战斗效果", item.name)); } let effect = item.use_profile.expect("use_profile should exist"); if effect.hp_restore <= 0 && effect.mana_restore <= 0 && effect.cooldown_reduction <= 0 && effect.build_buffs.is_empty() { return Err(format!("{} 当前没有可直接结算的战斗效果", item.name)); } Ok(BattleActionPlan { action_text: format!("使用{}", item.name), result_text: format!("你立刻用下{},当前回合的物品效果已经生效。", item.name), damage_dealt: 0, damage_taken: 8, heal: effect.hp_restore.max(0), mana_restore: effect.mana_restore.max(0), mana_cost: 0, cooldown_tick_turns: 1, cooldown_bonus_turns: effect.cooldown_reduction.max(0), applied_skill_cooldown: None, build_buffs: effect.build_buffs, consumed_item_id: Some(item.id), }) } fn battle_action_toast(function_id: &str, request: &RuntimeStoryActionRequest) -> Option { if function_id != "inventory_use" { return None; } request .action .payload .as_ref() .and_then(|payload| read_optional_string_field(payload, "itemId")) .map(|_| "Build 增益已写回当前快照".to_string()) }