1
This commit is contained in:
@@ -66,3 +66,14 @@
|
||||
1. 继续复用现有 `PuzzleRuntimeShell` 作为运行时承载组件,不新增平行页面。
|
||||
2. 设置弹层沿用现有像素风弹窗资源,不单独引入新的弹窗体系。
|
||||
3. 通关演出只作为前端表现层时序,不改动通关判定与排行榜数据来源。
|
||||
|
||||
### 5. 拖拽层级规则
|
||||
|
||||
正在被拖动的拼图片必须临时提升到拼图棋盘最上层,不允许在拖动过程中被其他单块或合并块遮挡。
|
||||
|
||||
交互约束如下:
|
||||
|
||||
1. 单块拖动时,提升该拼图片所属格子的堆叠层级,并同步提升拼图片自身层级。
|
||||
2. 合并块拖动时,直接提升整组容器层级,保证整组视觉保持完整。
|
||||
3. 松手、取消拖动、或丢失指针捕获后,必须立即恢复默认层级。
|
||||
4. 这条规则只属于前端表现层,不改变拼图交换、合并、拆分和落点判定逻辑。
|
||||
|
||||
127
docs/technical/RPG_BATTLE_DEFEAT_OUTCOME_FIX_2026-04-27.md
Normal file
127
docs/technical/RPG_BATTLE_DEFEAT_OUTCOME_FIX_2026-04-27.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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. 显式 `BattleResolutionOutcome`:`ongoing / 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 <= 0`:`fight_defeat`
|
||||
- 否则若敌方全部 `hp <= 0`:`fight_victory`
|
||||
- 否则继续战斗
|
||||
4. `spar_complete` 仍维持切磋的即时收束规则,不和正式战斗混用
|
||||
|
||||
## 类型同步
|
||||
|
||||
本轮同步扩展:
|
||||
|
||||
1. `NpcBattleOutcome`:新增 `fight_defeat`
|
||||
2. `StoryNpcChatState.combatContext.battleOutcome`:新增 `defeat`
|
||||
3. `aiService` 的战斗上下文类型:新增 `defeat`
|
||||
|
||||
## 回归测试
|
||||
|
||||
新增以下回归:
|
||||
|
||||
1. `module-runtime-story-compat`:
|
||||
- 同回合双方都归零时,必须优先判定为 `defeat`
|
||||
2. 前端 `storyChoiceRuntime`:
|
||||
- 服务端返回 `defeat` 且 `playerHp = 0` 时,必须进入死亡复活流程,不能进入胜利收束
|
||||
3. 前端 `battlePlan`:
|
||||
- 作品测试 / 本地战斗链里,同一轮玩家先手打空敌方但随后自己被打死时,最终必须判定为 `fight_defeat`
|
||||
4. 前端 `storyChoiceContinuation` / `useRpgRuntimeNpcInteraction`:
|
||||
- `fight_defeat` 不能再被当成“本地 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. 这样可以保证作品测试、幕预览与正式游戏在“死亡后回开局第一幕”这一口径上继续对齐
|
||||
|
||||
## 结论
|
||||
|
||||
本次修复后,RPG 战斗 compat 主链的胜负判定口径变为:
|
||||
|
||||
1. 玩家死亡优先于敌方倒地胜利
|
||||
2. 胜利与败北都只走确定性固定流程
|
||||
3. 不再出现“玩家已死却结算成战斗胜利”的串线结果
|
||||
@@ -0,0 +1,202 @@
|
||||
# RPG 作品测试与正式游戏运行态对齐方案
|
||||
|
||||
更新时间:`2026-04-27`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
本轮复测已经确认,战斗回合与同伴表现的局部修复虽然已经落地,但用户在“作品测试”入口中仍然能复现问题。这说明问题根因不只在战斗模块内部,还在于“作品测试”和“正式游戏”本身跑的不是同一条运行链。
|
||||
|
||||
旧实现中,作品测试会以 `runtimeMode: 'test'` 进入世界,而正式游戏使用 `runtimeMode: 'play'`。只要这条模式分叉继续存在,就会持续带来以下风险:
|
||||
|
||||
1. 战斗、剧情、选项、同伴、存档恢复等后续逻辑容易被模式判断继续分流。
|
||||
2. 某些修复只会覆盖正式链,作品测试仍保留旧行为,导致“结果页里测不准、正式入口里才正常”。
|
||||
3. 用户在创作结果页看到的世界与真正测试到的运行态体验不一致,测试结果失去参考意义。
|
||||
|
||||
因此本次必须把“作品测试”从“独立测试模式”收口为“正式运行态的一种启动方式”。
|
||||
|
||||
## 2. 对齐目标
|
||||
|
||||
### 2.1 核心原则
|
||||
|
||||
1. 作品测试进入世界后,游戏逻辑必须与正式游戏完全一致。
|
||||
2. 包括但不限于:
|
||||
- 战斗回合排序
|
||||
- 选项点击后的剧情推进
|
||||
- 同伴显示与参战
|
||||
- 运行时剧情刷新
|
||||
- 场景、遇敌、交互链路
|
||||
3. 作品测试不能再通过 `runtimeMode: 'test'` 驱动任何游戏行为差异。
|
||||
|
||||
### 2.2 唯一允许保留的差异
|
||||
|
||||
1. 从结果页启动作品测试后,退出运行态时应返回结果页。
|
||||
2. 作品测试阶段允许关闭运行态持久化,避免把临时试玩覆盖正式存档。
|
||||
3. “结束测试”按钮可以保留,但它只代表退出导航能力,不能代表另一套游戏模式。
|
||||
|
||||
## 3. 新的入口语义
|
||||
|
||||
### 3.1 作品测试入口
|
||||
|
||||
作品测试入口调整为:
|
||||
|
||||
1. 使用与正式游戏一致的 `mode: 'play'` 进入世界。
|
||||
2. 继续携带 `returnStage: 'custom-world-result'`,用于退出后回到结果页。
|
||||
3. 额外显式传递 `disablePersistence: true`,只影响是否自动持久化,不影响任何游戏逻辑。
|
||||
|
||||
### 3.2 正式进入世界
|
||||
|
||||
正式进入世界维持:
|
||||
|
||||
1. `mode: 'play'`
|
||||
2. 默认返回平台首页或原有正式入口目标
|
||||
3. 默认允许持久化
|
||||
|
||||
## 4. 运行态约束
|
||||
|
||||
### 4.1 runtimeMode 约束
|
||||
|
||||
1. 自定义世界从结果页进入作品测试时,`runtimeMode` 必须是 `play`。
|
||||
2. `test` 不再作为作品测试的真实运行模式。
|
||||
3. 编辑器幕预览过去使用过 `preview`;本轮起也统一改为复用 `play + runtimePersistenceDisabled = true` 的运行口径。
|
||||
4. 若仓库内还保留 `test / preview` 类型,只视为历史兼容标签,不能再作为当前作品测试主链依赖。
|
||||
|
||||
### 4.2 退出按钮约束
|
||||
|
||||
1. “结束测试”按钮是否显示,不再依赖 `gameState.runtimeMode`。
|
||||
2. 它应改为依赖“当前运行态是否提供测试退出能力”。
|
||||
3. 也就是:来自结果页测试启动时显示;普通正式入口不显示。
|
||||
|
||||
### 4.3 持久化约束
|
||||
|
||||
1. 自动存档与手动保存是否启用,统一由 `runtimePersistenceDisabled` 控制。
|
||||
2. 作品测试可通过 `disablePersistence: true` 关闭持久化。
|
||||
3. 正式运行保持 `runtimePersistenceDisabled: false`。
|
||||
4. 持久化开关不能再反向决定剧情、战斗、同伴或场景逻辑。
|
||||
|
||||
## 5. 验收口径
|
||||
|
||||
1. 从结果页点“作品测试”后,进入的运行态与正式游戏共用同一条逻辑链。
|
||||
2. 作品测试中点击战斗选项,不会再因为旧测试链分叉而跳过整场战斗。
|
||||
3. 作品测试中同伴站位、显示和按速度参战行为,与正式游戏一致。
|
||||
4. 作品测试退出后,仍能正确回到结果页。
|
||||
5. 作品测试默认不污染正式存档。
|
||||
|
||||
## 5.1 当前已落地的入口收口
|
||||
|
||||
本轮已经把以下入口统一到同一条 `play` 运行链:
|
||||
|
||||
1. 结果页 `作品测试`
|
||||
2. `RpgRuntimeApp` 自定义世界启动 intent
|
||||
3. `useRpgSessionBootstrap.handleCustomWorldSelect(...)`
|
||||
4. 编辑器内 `SceneActPreviewRuntime` 的幕预览启动状态
|
||||
|
||||
现在这些入口的唯一差异只剩:
|
||||
|
||||
1. `disablePersistence: true`
|
||||
2. `returnStage: 'custom-world-result'`
|
||||
3. 是否显示“结束测试”退出按钮
|
||||
|
||||
## 6. 本轮补充修正
|
||||
|
||||
在继续排查后,又确认了第二层根因:
|
||||
|
||||
1. 部分“作品测试 / 幕预览”链路在执行 `npc_fight / npc_spar` 后,服务端快照已经切换成战斗状态;
|
||||
2. 但返回的 snapshot 可能只带回 `currentBattleNpcId / currentNpcBattleMode / inBattle`;
|
||||
3. 没有同步把 `sceneHostileNpcs` 一起补齐;
|
||||
4. 这会导致前端本地 `battlePlan` 在点击 `battle_*` 时看到“当前战场为空”,从而直接把整场战斗判定为结束,体感上就像“点一下战斗选项直接跳过整场”。
|
||||
|
||||
因此除入口模式对齐外,还必须增加一层运行态网关兜底:
|
||||
|
||||
1. 当 `npc_fight / npc_spar` 返回的 snapshot 已经进入战斗;
|
||||
2. 且 `sceneHostileNpcs` 仍为空;
|
||||
3. 且前一刻仍握有当前 NPC 遭遇;
|
||||
4. 前端必须基于当前遭遇立即补建本地 NPC 战场单位,再交给后续本地逐轮结算链。
|
||||
|
||||
## 7. 本轮继续修正:幕预览敌方后排消失与站位突变
|
||||
|
||||
在继续复测后,又确认“作品测试 / 幕预览”里还有第二个更深层的问题:
|
||||
|
||||
1. 点击 `战斗` 后,对面后排角色会消失;
|
||||
2. 敌方会在开战瞬间整体跳位;
|
||||
3. 被保留下来的往往只剩一个前排目标;
|
||||
4. 这会进一步导致后排角色无法按速度参与回合。
|
||||
|
||||
### 7.1 根因梳理
|
||||
|
||||
这次不是单纯的画布渲染问题,而是战斗前后使用了两套不同的阵容真相:
|
||||
|
||||
1. 幕预览和平态里,当前交互目标是 `currentEncounter`;
|
||||
2. 同幕其余角色只是通过 `sceneActAmbientEncounters` 作为可见实体挂在画布后排;
|
||||
3. 一旦执行 `npc_fight / npc_spar`,运行态网关此前只会根据 `currentEncounter` 调一次 `createNpcBattleMonster(...)`;
|
||||
4. 这会把原本同幕存在的多角色阵容压缩成单个前排单位;
|
||||
5. 战斗播放随后再基于这份“新单体阵容”计算位置,就会表现为:
|
||||
- 后排消失
|
||||
- 开战瞬间跳位
|
||||
- 后排无法轮番出手
|
||||
|
||||
### 7.2 本次收口原则
|
||||
|
||||
本次修正不再允许“点击战斗”和“自动开战”各自定义自己的 NPC 战斗阵容。
|
||||
|
||||
统一规则如下:
|
||||
|
||||
1. 只要是 NPC 战斗,无论来自正式运行、结果页作品测试还是幕预览;
|
||||
2. 都必须先解析当前幕 active act 的 `encounterNpcIds`;
|
||||
3. 若当前幕存在多名同场角色,则统一生成完整敌方战斗编队;
|
||||
4. 编队位置固定为:
|
||||
- 主对手保留前排中心位
|
||||
- 同幕后排角色保留后排上下位
|
||||
5. runtime gateway 在服务端快照缺失 `sceneHostileNpcs` 时,优先复用这份完整编队;
|
||||
6. 若上一帧已经存在敌方战斗阵容,则优先沿用上一帧的 `xMeters / yOffset / facing`,避免位置突变。
|
||||
|
||||
### 7.3 落地结果
|
||||
|
||||
本轮已将以下两条链收口为同一套 NPC 战斗编队生成逻辑:
|
||||
|
||||
1. `sceneEncounterPreviews.ts`
|
||||
- 负责幕预览 / 自动开战时,把 active act 中的多名 encounter NPC 转成完整战斗编队
|
||||
2. `rpgRuntimeStoryGateway.ts`
|
||||
- 负责 `npc_fight / npc_spar` 返回空战场快照时,按同一套规则恢复完整敌方阵容
|
||||
|
||||
这样可以保证:
|
||||
|
||||
1. 后排角色不会在开战时丢失;
|
||||
2. 敌方整体不会因为单体兜底而跳到新的默认位置;
|
||||
3. 后排角色会进入 battle plan,按自身速度轮番出手;
|
||||
4. 作品测试、幕预览与正式运行的战斗开场表现继续收口一致。
|
||||
|
||||
## 8. 本轮继续修正:战斗开场敌方整队右下偏移
|
||||
|
||||
在用户直接通过 in-app 浏览器复现并停留在 bug 现场后,本轮没有再只依赖推断日志,而是直接对照当前战斗画面与渲染层实现继续下钻。
|
||||
|
||||
### 8.1 现场特征
|
||||
|
||||
本次现场有两个关键事实:
|
||||
|
||||
1. 浏览器控制台没有额外报错,说明不是运行时异常把敌人“删掉”;
|
||||
2. 敌方角色仍然存在,但会在进入战斗瞬间整体偏到右下,说明问题发生在战斗态画布定位阶段。
|
||||
|
||||
### 8.2 最终根因
|
||||
|
||||
继续对照 `GameCanvasEntityLayer.tsx` 后确认,真正导致“刚进入战斗敌方整队瞬移”的原因之一是:
|
||||
|
||||
1. 部分作品测试敌方角色使用了自定义世界场景立绘;
|
||||
2. 这类角色在 `getEncounterCharacterOpponentBottom(...)` 中已经按场景立绘脚底锚点完成过一次落地修正;
|
||||
3. 进入战斗后,`GameCanvasEntityLayer` 又把 `getSceneNpcVisualBottomOffsetPx(...)` 额外叠加到 battle entity bottom;
|
||||
4. 于是同一个 `78px` 的场景立绘下沉偏移被重复应用了一次;
|
||||
5. 视觉上就会表现为敌方整队在开战瞬间突然向右下坠落。
|
||||
|
||||
### 8.3 本次修正
|
||||
|
||||
本轮对战斗态敌方底边计算做了收口:
|
||||
|
||||
1. 若敌方是 `characterId` 对应的角色型 NPC,则战斗态不再重复叠加 `getSceneNpcVisualBottomOffsetPx(...)`;
|
||||
2. 保留 `getEncounterCharacterOpponentBottom(...)` 的单次场景立绘脚底修正;
|
||||
3. 非角色型怪物 / 通用 NPC 仍沿用既有底边偏移逻辑。
|
||||
|
||||
### 8.4 验证
|
||||
|
||||
本轮新增并通过了回归验证:
|
||||
|
||||
1. 自定义世界角色型敌人在战斗态不会再重复叠加场景立绘下沉偏移;
|
||||
2. 相关战斗编队、runtime gateway 与 battle plan 既有回归继续通过。
|
||||
Reference in New Issue
Block a user