Files
Genarrative/docs/technical/RPG_BATTLE_DEFEAT_OUTCOME_FIX_2026-04-27.md
高物 a9febe7678
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-28 10:57:40 +08:00

6.6 KiB
Raw Permalink Blame History

RPG 战斗败北结果修复2026-04-27

更新时间:2026-04-27

背景

线上出现了一个确定性战斗兼容链 bug

  1. 玩家在 RPG 战斗中已经被打死。
  2. 画面却突然跳到“对方已经败下阵来”的胜利收束。
  3. 最终走了战斗胜利固定流程,而不是死亡动画与三秒后复活。

这与既有战斗结束规则冲突:

  1. 玩家血量小于等于 0 时必须优先进入败北/死亡流程。
  2. 战斗胜利只允许在玩家仍然存活、且敌方被击败时成立。

根因

问题出在 server-rs/crates/module-runtime-story-compat/src/battle.rs 的 compat 战斗结算:

  1. apply_player_damage(...) 旧实现把玩家生命最低钳在 1,不会真正写出 0 血。
  2. resolve_battle_action(...) 旧实现只有 ongoing / victory / spar_complete 三种结果,没有正式 defeat 分支。
  3. 当同一回合里敌方也被打到 0 附近时,结算会只看敌方血量并直接归类到 victory

这会把“玩家应败北”的回合错误地写成“敌方已败下阵来”。

继续复测后又确认,作品测试 里还有第二层根因:

  1. 作品测试并不总是走服务端 compat 战斗结算;
  2. 一部分战斗点击仍会走前端本地 src/hooks/combat/battlePlan.ts
  3. 这条本地链原先会在玩家或同伴刚打掉最后一只敌人时,立刻:
    • 把敌方从 sceneHostileNpcs 移除;
    • currentNpcBattleOutcome 直接写成 fight_victory
  4. 这样会导致同一轮中原本还应该出手的敌方单位失去反击机会;
  5. 体感上就会变成“我明明该死了,但作品测试先把对方判成败下阵来”。

本次修复

1. 后端 compat 战斗结算补齐败北态

module-runtime-story-compat::battle 中补齐:

  1. 显式 BattleResolutionOutcomeongoing / victory / spar_complete / defeat
  2. apply_player_damage(...) 允许把 playerHp 写到 0
  3. 新增统一胜负决议:
    • 玩家 hp <= 0 时优先判定 defeat
    • 只有玩家仍存活时,敌方 hp <= 0 才能进入 victory / spar_complete

2. 服务端状态写回补齐 fight_defeat

当 compat 战斗判定为失败时:

  1. presentation.battle.outcome = "defeat"
  2. currentNpcBattleOutcome = "fight_defeat"
  3. 关闭战斗态,清理当前遭遇与敌对列表
  4. 不发放胜利经验、不累计 hostileNpcsDefeated

3. 前端服务端战斗回包分支补齐失败态

前端 storyChoiceRuntime 同步修正:

  1. 服务端回包若 playerHp <= 0,无论 battle outcome 是否同时包含敌方掉血,都优先走死亡动画与复活链
  2. 敌方是否播放死亡态时,defeat 不再被当成“目标已被击败”

4. 本地 battlePlan 改为整轮后统一收束

src/hooks/combat/battlePlan.ts 中同步修正:

  1. 普通战斗不再在玩家/同伴动作阶段即时写入 fight_victory
  2. 敌方被打到 0 血时,先只保留为“本轮已死亡但尚未清场”的状态
  3. 整轮 turn order 跑完后,再统一按以下优先级判定:
    • 玩家 playerHp <= 0fight_defeat
    • 否则若敌方全部 hp <= 0fight_victory
    • 否则继续战斗
  4. spar_complete 仍维持切磋的即时收束规则,不和正式战斗混用

类型同步

本轮同步扩展:

  1. NpcBattleOutcome:新增 fight_defeat
  2. StoryNpcChatState.combatContext.battleOutcome:新增 defeat
  3. aiService 的战斗上下文类型:新增 defeat

回归测试

新增以下回归:

  1. module-runtime-story-compat
    • 同回合双方都归零时,必须优先判定为 defeat
  2. 前端 storyChoiceRuntime
    • 服务端返回 defeatplayerHp = 0 时,必须进入死亡复活流程,不能进入胜利收束
  3. 前端 battlePlan
    • 作品测试 / 本地战斗链里,同一轮玩家先手打空敌方但随后自己被打死时,最终必须判定为 fight_defeat
  4. 前端 storyChoiceContinuation / useRpgRuntimeNpcInteraction
    • fight_defeat 不能再被当成“本地 NPC 战斗胜利”进入战后收束
    • 玩家死亡后必须直接走死亡复活链,且复活时重置到开局场景第一幕,不能先推进下一幕
  5. 前端 postBattleFlow
    • 复活回到开局场景时,必须重新走首幕 encounter preview 恢复链
    • 第一幕主交互 NPC 与同幕陪衬 NPC 要继续沿用既有场景槽位,不能退化成全部站成一排

继续收口2026-04-28

继续复测后又确认,作品测试里还残留一层“败北被当成胜利”的本地剧情续写问题:

  1. storyChoiceContinuation 旧判断把 currentNpcBattleOutcome 只要是 truthy 就当成本地 NPC 战斗可收束;
  2. 这会把 fight_defeat 也误送进 finalizeNpcBattleResult(...)
  3. 后续又因为 !nextState.inBattle 条件过宽,继续走到 buildPostBattleVictoryStory(...),表现为:
    • 死亡后仍显示“对方已经败下阵来”
    • 并且还会把场景幕推进到下一幕

本轮补充修正如下:

  1. 本地 NPC 战斗收束只接受 fight_victory / spar_complete
  2. fight_defeat 一律交回死亡复活主链
  3. NPC 战后结算 helper 显式拒绝 fight_defeat
  4. 本地 battle 的战后推进只允许:
    • 正式 NPC 战斗明确 fight_victory
    • 切磋明确 spar_complete
    • 非 NPC 通用敌对战斗 !inBattle
  5. 这样可以保证作品测试、幕预览与正式游戏在“死亡后回开局第一幕”这一口径上继续对齐

继续收口:复活后首幕 NPC 与站位恢复2026-04-28

在继续复测后,又确认死亡复活链还有一层表现问题:

  1. 角色虽然已经回到开局场景第一幕;
  2. 但复活态旧实现只是重置 currentSceneActState,没有重新恢复第一幕 encounter preview
  3. 于是画布只能把第一幕 NPC 都按普通 ambient 角色绘制;
  4. 视觉上就会表现为:
    • 主交互 NPC 没有按首幕重新成为前景目标
    • 同幕 NPC 失去原本的前后排关系
    • 最终看起来像“所有人站成一排”

本轮补充修正如下:

  1. buildRevivedFirstSceneState(...) 在重置到首幕之后,立即复用 ensureSceneEncounterPreview(...)
  2. 这样复活链与“开局进入世界 / 场景正常进场”继续共用同一套首幕恢复逻辑
  3. 第一幕主交互 NPC、同幕陪衬 NPC 与既有槽位会一起恢复,不再额外发明一套复活专用站位规则

结论

本次修复后RPG 战斗 compat 主链的胜负判定口径变为:

  1. 玩家死亡优先于敌方倒地胜利
  2. 胜利与败北都只走确定性固定流程
  3. 不再出现“玩家已死却结算成战斗胜利”的串线结果