1
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# 战斗直结算与表现修复记录(2026-04-27)
|
||||
|
||||
更新时间:`2026-04-27`
|
||||
|
||||
## 1. 问题背景
|
||||
|
||||
本次修复针对 RPG 运行态战斗里两类直接可感知的问题:
|
||||
|
||||
1. 战斗面板中的“普通攻击”“技能释放”等选项点击后,体验上像是又走了一次剧情/模型推演,而不是立刻播动作并结算伤害。
|
||||
2. 伤害飘字与敌方头顶血条在战斗里没有稳定可见,尤其是最后一击和脱战前后的几帧容易直接丢失。
|
||||
|
||||
## 2. 本次约束
|
||||
|
||||
为避免后续回归,本次明确补充以下工程约束:
|
||||
|
||||
1. 只要已经处于 `inBattle = true` 的战斗面板阶段,`battle_*` 与战斗内 `inventory_use` 都必须走前端现有的本地确定性战斗播放链。
|
||||
2. 这类动作只负责“播放动作 -> 扣血/扣蓝/冷却 -> 刷新下一轮 options / 脱战收尾”,不能再回到服务端剧情推演分流。
|
||||
3. 脱战前最后一拍的画布表现必须保留,不能因为 `inBattle` 提前变成 `false` 就把伤害飘字、敌方血条和受击反馈一起关掉。
|
||||
|
||||
## 3. 落地改动
|
||||
|
||||
### 3.1 战斗动作分流
|
||||
|
||||
文件:`src/hooks/rpg-runtime-story/choiceActions.ts`
|
||||
|
||||
调整:
|
||||
|
||||
1. 新增“战斗直结算动作”判定。
|
||||
2. 当运行态已经处于战斗中时,`battle_*` 和 `inventory_use` 不再命中 `runServerRuntimeChoiceAction()`。
|
||||
3. 这些动作统一回到 `runLocalStoryChoiceContinuation()`,沿用现有 `buildBattlePlan + playback` 本地确定性链路。
|
||||
|
||||
结果:
|
||||
|
||||
1. 普通攻击、技能释放、调息、逃跑、战斗内用物品都会直接播动作并结算。
|
||||
2. UI 不再把这类按钮误导成“剧情推演中”的体验。
|
||||
|
||||
### 3.2 战斗画布表现保留
|
||||
|
||||
文件:`src/components/game-canvas/GameCanvasEntityLayer.tsx`
|
||||
|
||||
调整:
|
||||
|
||||
1. 新增 `hasCombatAfterimage` 判定,用于识别“虽然 `inBattle` 已经开始收尾,但敌方仍处于受击/死亡帧或刚产生过伤害反馈”的阶段。
|
||||
2. 伤害反馈样本采集、飘字事件保留、敌我血条展示统一改为依赖 `shouldRenderCombatPresentation`,而不是只看 `inBattle`。
|
||||
|
||||
结果:
|
||||
|
||||
1. 伤害数值飘字不会在最后一击前被提前清空。
|
||||
2. 敌方头顶血条能跟着受击与死亡过渡完整显示出来。
|
||||
|
||||
### 3.3 面板文案修正
|
||||
|
||||
文件:`src/components/rpg-runtime-panels/RpgAdventurePanel.tsx`
|
||||
|
||||
调整:
|
||||
|
||||
1. 运行态仍需锁输入的短暂阶段,如果当前处于战斗中,加载提示改为“战斗结算中...”。
|
||||
2. 非战斗态保留“剧情推演中...”。
|
||||
|
||||
结果:
|
||||
|
||||
1. 战斗按钮点击后的反馈语义与真实执行链路一致。
|
||||
2. 用户不会再误判为“点击普通攻击/技能后又触发了一次模型推理”。
|
||||
|
||||
## 4. 回归关注点
|
||||
|
||||
后续若继续改 `server-rs` / Runtime Story 的战斗 option 下发,需要继续遵守:
|
||||
|
||||
1. 服务端可以负责下发合法战斗选项和最终快照结构。
|
||||
2. 但战斗内直接动作的逐帧播放与即时结算,默认仍以前端确定性链路为准。
|
||||
3. 若未来要把这条链路整体迁回 `server-rs`,必须先补齐“无推理、可逐帧播放、可保留最终受击帧”的等价表现方案,再替换当前实现。
|
||||
|
||||
## 5. 追加修复(2026-04-27 晚)
|
||||
|
||||
本日后续排查又发现一处更直接的战斗跳过根因:
|
||||
|
||||
1. `src/hooks/combat/battlePlan.ts` 旧实现会在一次点击里继续跑完整段 `turnOrder`,而不是只结算“玩家一次声明动作 + 最多一次敌方反击”。
|
||||
2. `battle_attack_basic` 还会复用技能挑选逻辑,导致“普通攻击”可能被本地随机映射成别的技能动作。
|
||||
|
||||
本轮已追加修复:
|
||||
|
||||
1. 本地战斗计划严格收束为单回合结算,不再一次点击连跑多轮。
|
||||
2. `battle_attack_basic` 固定走基础攻击,不再随机选技能。
|
||||
3. 战斗内 `inventory_use` 在本地计划中按单次动作消费物品、应用恢复/冷却收益,不再落回攻击型分支。
|
||||
|
||||
追加验收口径:
|
||||
|
||||
1. 战斗中点击一次普通攻击 / 技能 / 物品 / 调息,不会直接把整场战斗过程跳过。
|
||||
2. 若敌人未被这一击打死,当前次结算结束后仍停留在战斗态,并刷新下一轮战斗选项。
|
||||
3. “普通攻击”不会显示成其他技能的动作与耗蓝结果。
|
||||
|
||||
## 6. 再追加修复(2026-04-27 夜间二次排查)
|
||||
|
||||
用户复测后又暴露出一类更隐蔽的问题:
|
||||
|
||||
1. 面板展示层可能仍在显示战斗选项,但逻辑态 `gameState.inBattle` 已短暂回落为 `false`。
|
||||
2. 旧实现里,`src/hooks/rpg-runtime-story/choiceActions.ts` 只要看到 `inBattle === false`,就会把 `battle_*` / `inventory_use` 重新分流回服务端 runtime。
|
||||
3. 这会让用户在“视觉上仍处于战斗”的窗口点击选项时,直接命中服务端快照结果,体感仍然是“点一下就把战斗过程跳过”。
|
||||
|
||||
本轮追加修正:
|
||||
|
||||
1. 战斗直结算判定不再只依赖 `gameState.inBattle`。
|
||||
2. 只要当前选项本身属于 `battle_*` / `inventory_use`,并且任一战斗上下文仍然存在
|
||||
`currentBattleNpcId`、`currentNpcBattleMode`、存活敌人、当前故事仍在显示战斗选项
|
||||
就必须继续走本地逐帧战斗链。
|
||||
3. 为此补充了回归测试,覆盖“`inBattle` 已短暂变成 `false`,但战斗展示仍在”的残留窗口。
|
||||
|
||||
新增验收口径:
|
||||
|
||||
1. 即使 `inBattle` 出现一帧级别的提前回落,只要玩家看到的仍是战斗选项,点击后也不会跳回服务端直结算。
|
||||
2. 战斗面板残留显示期间点击 `battle_*` / `inventory_use`,仍应播放本地战斗过程,而不是直接跳到结果快照。
|
||||
@@ -397,8 +397,8 @@ finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
|
||||
1. `build_initial_board_with_seed` 使用种子化洗牌生成初始棋盘,不再固定左移一格。
|
||||
2. 正式 run 的种子由 `runId + profileId + levelIndex + gridSize` 派生;由于每次进入都会创建新的 `runId`,同一作品多次进入也会得到不同打乱样式。
|
||||
3. 洗牌后若极端情况下仍为完成态,强制旋转一次,保证新关卡不是已完成局面。
|
||||
4. 初始棋盘不得存在任何已经正确相邻的两块;初始化会多次洗牌筛选,若极端情况下未命中,则使用反序排列兜底,避免开局自动出现合并块。
|
||||
5. `module-puzzle` 与本地 fallback 的测试都必须直接断言初始棋盘不存在正确相邻对,不能只检查 `mergedGroups = []`。
|
||||
4. 初始棋盘不得存在任何原图相邻块互相贴边;初始化会多次洗牌筛选,若极端情况下未命中,则使用确定性约束搜索兜底,避免开局出现局部连续结构。
|
||||
5. `module-puzzle` 与本地 fallback 的测试都必须直接断言初始棋盘不存在原图相邻贴边对,不能只检查 `mergedGroups = []`。
|
||||
|
||||
### 11.2 局部重算与合并
|
||||
|
||||
@@ -442,3 +442,15 @@ finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
|
||||
1. `dragState` 持续记录 `currentX/currentY`,拖动中按指针偏移对单块或合并块做 `translate3d` 跟手反馈。
|
||||
2. 棋盘与合并块交互层增加 `touch-none select-none`,避免移动端滚动、选中文本等默认行为打断拖动。
|
||||
3. 松手后仍只提交 `pieceId + targetRow + targetCol`,最终交换、合并、拆分和通关继续以后端/本地运行态快照为准。
|
||||
|
||||
### 12.3 合并块外轮廓描边修正
|
||||
|
||||
用户反馈“合并的块的边界显示要描边自己的块的边界,不要搞一个正方形或者矩形的边界”后,移除合并块外接矩形 `ring` 层。运行态现在按合并组真实占据格逐格判断四向邻居:某一边没有同组合并格时才画该边描边,同组内部相邻边不画线。这样 L 形、长条或其他非矩形合并块只显示自身外轮廓,拖动热区仍只覆盖真实拼块格。
|
||||
|
||||
后续反馈要求合并块边界也要圆角后,外轮廓描边补充按四个角判断:只有相邻两条外露边同时存在的真实外轮廓角才应用圆角,内部拼接角保持直角且不显示分界线。
|
||||
|
||||
### 12.4 第二关后打乱规则旁路修正
|
||||
|
||||
用户反馈“从第二关开始打乱规则像是完全相同”后,检查发现 `api-server` 的本地下一关 fallback 仍使用旧版 `build_local_puzzle_board` 固定左移一格,没有复用 `module-puzzle` 的种子化初始化规则。该路径会在图库/正式推荐不可用、由 API 临时构造下一关时触发。
|
||||
|
||||
修正后 `api-server` 本地下一关构造改为调用 `module_puzzle::build_initial_board_with_seed`,种子由 `runId + profileId + levelIndex + gridSize` 派生;因此第二关、第三关以及后续 fallback 关卡也会得到不同布局,并继续满足“开局没有原图相邻块贴边”的约束。
|
||||
|
||||
105
docs/technical/PUZZLE_RUNTIME_REAL_LEADERBOARD_2026-04-27.md
Normal file
105
docs/technical/PUZZLE_RUNTIME_REAL_LEADERBOARD_2026-04-27.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 拼图运行时真实排行榜落地说明
|
||||
|
||||
更新时间:`2026-04-27`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前拼图关卡结束弹窗里的排行榜数据并不是真实用户成绩。
|
||||
|
||||
问题根因有两层:
|
||||
|
||||
1. 前端本地运行态 `src/services/puzzle-runtime/puzzleLocalRuntime.ts` 在通关后会直接拼出几条演示昵称数据。
|
||||
2. `server-rs` 拼图运行时虽然已经预留了 `leaderboardEntries` 字段,但 `module-puzzle`、`spacetime-client`、`api-server` 还没有真实成绩表与聚合过程,因此接口层长期返回空数组。
|
||||
|
||||
这导致用户在结算弹窗里看到的是“看起来像真实排行榜,但实际上是本地假数据”的结果,和平台“真实用户数据”要求冲突。
|
||||
|
||||
## 2. 本次目标
|
||||
|
||||
本次改动只解决一个明确问题:
|
||||
|
||||
1. 拼图关卡结束后的排行榜必须使用真实用户成绩。
|
||||
2. 删除现有前端演示昵称、演示耗时等假数据。
|
||||
3. 不在 UI 中默认塞入任何说明型占位文案。
|
||||
|
||||
## 3. 本次落地边界
|
||||
|
||||
为了控制改动范围,本次不把整套拼图运行态全部迁回后端,而是在当前“本地棋盘运行态”基础上补一条真实成绩回写链路:
|
||||
|
||||
1. 拼图拖拽、交换、合并、拆分、通关判定,仍然沿用当前本地运行态。
|
||||
2. 玩家一旦通关,前端立即把当前关卡成绩提交到 `server-rs`。
|
||||
3. `server-rs` 将成绩写入 `SpacetimeDB` 成绩表,并返回该关卡的真实排行榜。
|
||||
4. 结算弹窗只展示后端返回的真实成绩榜单,不再混入本地演示数据。
|
||||
|
||||
这意味着:
|
||||
|
||||
1. 这次不是“完整后端裁决化”。
|
||||
2. 这次是“先把排行榜真相源收回后端”,满足真实成绩展示要求。
|
||||
|
||||
## 4. 成绩真相源设计
|
||||
|
||||
新增拼图成绩表,按“关卡作品 + 网格规格 + 用户”维护最佳成绩。
|
||||
|
||||
建议字段:
|
||||
|
||||
1. `entry_id`
|
||||
唯一主键。
|
||||
2. `profile_id`
|
||||
当前关卡作品 `profile_id`。
|
||||
3. `grid_size`
|
||||
当前成绩对应的拼图网格规格,至少区分 `3x3` 与 `4x4`。
|
||||
4. `user_id`
|
||||
成绩所属真实用户 ID。
|
||||
5. `nickname`
|
||||
成绩展示昵称。当前优先使用提交时的用户显示名快照。
|
||||
6. `best_elapsed_ms`
|
||||
用户在该关卡该规格下的最佳通关耗时。
|
||||
7. `last_run_id`
|
||||
最近一次刷新该最佳成绩的运行态 `run_id`。
|
||||
8. `updated_at`
|
||||
最后一次刷新时间。
|
||||
|
||||
## 5. 排行榜口径
|
||||
|
||||
排行榜必须遵守下面规则:
|
||||
|
||||
1. 只读真实成绩表。
|
||||
2. 同一用户在同一 `profile_id + grid_size` 下只保留 1 条最佳成绩。
|
||||
3. 排序按 `best_elapsed_ms` 从小到大。
|
||||
4. 同耗时按 `updated_at` 更早者优先,再按 `user_id` 稳定排序。
|
||||
5. 返回前 `N` 条,当前阶段固定 `10` 条即可。
|
||||
6. 当前用户如果在榜单内,需要标记 `isCurrentPlayer = true`。
|
||||
|
||||
## 6. 接口落地
|
||||
|
||||
新增拼图排行榜提交接口:
|
||||
|
||||
`POST /api/runtime/puzzle/runs/:runId/leaderboard`
|
||||
|
||||
请求体至少包含:
|
||||
|
||||
1. `profileId`
|
||||
2. `gridSize`
|
||||
3. `elapsedMs`
|
||||
4. `nickname`
|
||||
|
||||
返回体采用现有 `PuzzleRunResponse`,但要求:
|
||||
|
||||
1. `run.currentLevel.leaderboardEntries` 返回真实榜单。
|
||||
2. `run.leaderboardEntries` 同步返回当前关卡真实榜单,方便现有结算弹窗兼容读取。
|
||||
|
||||
## 7. 前端改动规则
|
||||
|
||||
1. 删除 `puzzleLocalRuntime.ts` 中本地演示榜单构造逻辑。
|
||||
2. 本地通关后,运行态只保留真实通关耗时,不再生成假昵称榜单。
|
||||
3. 结算弹窗显示时,如果真实榜单尚未回写完成,可以显示加载态;但不能回退到假数据。
|
||||
4. 下一关开始后,当前关卡榜单状态清空。
|
||||
|
||||
## 8. 测试要求
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. 通关后不会再生成本地假榜单。
|
||||
2. 同一用户重复通关同一关卡时,只保留更优成绩。
|
||||
3. 不同用户成绩会按耗时正确排序。
|
||||
4. `3x3` 与 `4x4` 不混榜。
|
||||
5. 下一关开启后上一关榜单不会污染新关卡。
|
||||
@@ -0,0 +1,97 @@
|
||||
# RPG 战斗回合顺序与同伴表现修复
|
||||
|
||||
更新时间:`2026-04-27`
|
||||
|
||||
## 1. 问题归因
|
||||
|
||||
本轮复测暴露出的战斗问题,不再是单一的“按钮分流错误”,而是同一条战斗链上有三处模型不一致:
|
||||
|
||||
1. `npc_fight / npc_spar` 的开战入口已经把运行态切进战斗,但后续本地回合计划仍停留在“玩家一下 + 敌人一下”的简化模型。
|
||||
2. 本地 `battlePlan` 虽然已经具备 `companion` 播放 step 类型,但实际上从未生成同伴出手步骤。
|
||||
3. 战斗入场后,同伴仍沿用和平探索态附近的锚点与层级,容易被主角遮住,体感上像“后面的角色消失了”。
|
||||
|
||||
因此本次修复必须同时覆盖开战后的回合规则与画布站位规则,而不是继续只补某个 `if` 分支。
|
||||
|
||||
## 2. 统一后的目标行为
|
||||
|
||||
### 2.1 开战入口
|
||||
|
||||
1. `npc_fight / npc_spar` 仍允许先走服务端 runtime action,把 `currentBattleNpcId / currentNpcBattleMode / inBattle` 等真实战斗态切好。
|
||||
2. 一旦战斗选项已经展示出来,后续 `battle_*` 与战斗内 `inventory_use` 必须继续走前端本地确定性回合链。
|
||||
3. 也就是说:
|
||||
- 服务端负责“进入什么战斗、当前战斗对象是谁”。
|
||||
- 前端本地链负责“这一轮谁先动、谁受伤、怎么播放、什么时候脱战”。
|
||||
|
||||
### 2.2 一次点击的结算边界
|
||||
|
||||
1. 每次点击战斗按钮,只结算当前这一整轮。
|
||||
2. 一整轮的定义是:当前仍存活、且本轮应出手的单位,各自按速度最多行动一次。
|
||||
3. 这一轮里如果有人先被击败,后续轮到他时直接跳过,不补行动。
|
||||
4. 一旦满足战斗结束条件,立刻停止后续轮内动作,不再补跑剩余单位。
|
||||
|
||||
### 2.3 正式战斗与切磋的区别
|
||||
|
||||
1. `fight`:
|
||||
- 玩家、存活同伴、敌方单位一起参与。
|
||||
- 胜利条件是敌方全部倒下。
|
||||
- 失败条件是主角生命降到 `0`。
|
||||
2. `spar`:
|
||||
- 只保留主角与当前切磋对象对打。
|
||||
- 同伴继续显示在场上,但不参与出手和承伤。
|
||||
- 切磋终止仍沿用现有“任一方被压到保底线即结束”的规则。
|
||||
|
||||
## 3. 回合顺序规则
|
||||
|
||||
### 3.1 速度来源
|
||||
|
||||
1. 玩家与同伴使用角色战斗属性解析后的 `turnSpeed`。
|
||||
2. 敌方单位使用当前 `SceneHostileNpc.speed`。
|
||||
3. 同速时使用稳定顺序打破平手:
|
||||
- 主角优先于同伴。
|
||||
- 同伴按队伍槽位顺序。
|
||||
- 敌方按当前战场数组顺序。
|
||||
|
||||
### 3.2 本轮排序
|
||||
|
||||
1. 开始结算当前点击时,先基于本轮起始快照收集所有可行动单位。
|
||||
2. 按速度从高到低生成本轮 `turnOrder`。
|
||||
3. 之后按这个顺序依次推进:
|
||||
- 若该单位轮到时已经死亡,则跳过。
|
||||
- 若战斗已结束,则直接停止本轮。
|
||||
|
||||
## 4. 玩家动作规则
|
||||
|
||||
1. `battle_attack_basic`:
|
||||
- 永远只执行基础攻击,不再随机映射到其他技能。
|
||||
2. `battle_use_skill`:
|
||||
- 使用 `runtimePayload.skillId` 指定的技能。
|
||||
3. `battle_recover_breath`:
|
||||
- 作为“消耗当前回合的一次非攻击动作”处理。
|
||||
- 恢复收益应发生在主角真正轮到自己的那个时点,而不是点击瞬间提前生效。
|
||||
4. `inventory_use`:
|
||||
- 同样作为“消耗当前回合的一次非攻击动作”处理。
|
||||
- 物品消耗、回血回蓝、冷却推进、build buff 增加,都发生在主角轮到时。
|
||||
|
||||
## 5. 同伴出手规则
|
||||
|
||||
1. 正式战斗中,所有存活同伴都应参与本轮排序。
|
||||
2. 每名同伴每轮最多出手一次。
|
||||
3. 同伴默认使用自己的可用技能;若当前没有可释放技能,则回退到基础攻击。
|
||||
4. 同伴的蓝量、技能冷却和受击血量都持续写回 `gameState.companions`。
|
||||
|
||||
## 6. 战斗画布中的同伴表现规则
|
||||
|
||||
1. 战斗态下,同伴不能继续和主角共用同一横向锚点。
|
||||
2. 战斗队形改为“主角前、同伴后排左侧展开”:
|
||||
- 仍保留上下两个槽位。
|
||||
- 横向偏移必须明显大于和平态,避免被主角主体遮挡。
|
||||
3. 同伴层级仍按场景脚底锚点参与排序,但战斗态需要给同伴少量前移权重,避免完全压在主角后面。
|
||||
4. 切磋模式同样保留同伴站位显示,只是不参与回合。
|
||||
|
||||
## 7. 验收口径
|
||||
|
||||
1. 点击一次战斗按钮后,不会再直接跳完整场,而是只播放当前一整轮。
|
||||
2. 若敌方速度高于主角,敌方可以在这一轮里先行动。
|
||||
3. 正式战斗里,同伴会参与轮番出手,并按速度插入行动顺序。
|
||||
4. 切磋里同伴不会出手,但也不会在开战后“消失”。
|
||||
5. 战斗入场后,主角后方同伴仍稳定可见,不会因为站位重叠被完全遮住。
|
||||
@@ -150,3 +150,86 @@ npm run typecheck -- --pretty false
|
||||
```
|
||||
|
||||
以上局部测试、局部 ESLint 与全量类型检查已通过。后端代码未在本轮修改中触碰,因此仍不需要执行 `npm run api-server:maincloud`。
|
||||
|
||||
## 2026-04-27 第四轮复查修正
|
||||
|
||||
用户复测后仍出现“进入作品测试没有显示幕配置角色”。本轮继续沿真实结果页入口复查,确认前几轮的第一幕解析已经覆盖 `oppositeNpcId -> primaryNpcId -> encounterNpcIds`,但作品测试入口仍可能在进入选角前拿到旧 profile。
|
||||
|
||||
定位结论:
|
||||
|
||||
1. 结果页展示和自动保存期望消费 `session.resultPreview.preview`,或者在缺少 resultPreview 时消费 `draftProfile.legacyResultProfile`。
|
||||
2. `rpgCreationPreviewAdapter.buildPreviewFromSession()` 原先优先规范化 `session.draftProfile`,会把基础草稿骨架当成运行态 profile。
|
||||
3. 当基础草稿骨架与结果页预览中的 `sceneChapterBlueprints`、角色、第一幕对面角色不一致时,作品测试即使后续严格读取第一幕,也会读取到旧世界数据。
|
||||
|
||||
本轮修正:
|
||||
|
||||
1. `src/services/rpg-creation/rpgCreationPreviewAdapter.ts`
|
||||
- 结果页 profile 解析顺序调整为:`session.resultPreview.preview -> draftProfile.legacyResultProfile -> draftProfile`。
|
||||
- 作品测试、结果页展示和自动保存使用同一份当前结果页 profile,避免选角后加载旧草稿骨架。
|
||||
2. `src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts`
|
||||
- 覆盖 `resultPreview.preview` 优先于 `draftProfile`。
|
||||
- 覆盖缺少 resultPreview 时回退到 `draftProfile.legacyResultProfile`。
|
||||
3. `src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx`
|
||||
- 将作品测试入口断言改为使用“结果页当前 profile”,保证入口语义与 UI 展示一致。
|
||||
|
||||
本轮语义补齐为:结果页点“作品测试”后,先用当前结果页 profile 进入角色选择;选完角色后再直接加载该 profile 的开局场景第一幕,并把第一幕 `oppositeNpcId` 作为对面 NPC 启动聊天。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
npm test -- --run src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx src/hooks/useGameFlow.customWorld.test.tsx src/data/sceneEncounterPreviews.test.ts src/hooks/rpg-runtime-story/storyChoiceRuntime.test.ts src/data/customWorldLibrary.test.ts
|
||||
npx eslint src/services/rpg-creation/rpgCreationPreviewAdapter.ts src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts src/components/rpg-entry/useRpgCreationEnterWorld.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx src/hooks/useGameFlow.customWorld.test.tsx src/data/sceneEncounterPreviews.ts src/data/sceneEncounterPreviews.test.ts src/hooks/rpg-runtime-story/storyChoiceRuntime.ts src/hooks/rpg-runtime-story/storyChoiceRuntime.test.ts src/data/customWorldLibrary.ts src/data/customWorldLibrary.test.ts src/data/scenePresets.ts src/services/customWorldSceneActRuntime.ts src/hooks/rpg-session/useRpgSessionBootstrap.ts src/services/customWorldRoleReferences.ts src/services/big-fish-gallery/bigFishGalleryClient.ts
|
||||
npm run typecheck -- --pretty false
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
以上相关测试、局部 ESLint、全量类型检查与编码检查均通过。后端代码未在本轮修改中触碰,因此未执行 `npm run api-server:maincloud`。
|
||||
|
||||
## 2026-04-27 第五轮误导链路闭口
|
||||
|
||||
第四轮修复后,继续清理会误导后续迭代的旧入口:
|
||||
|
||||
1. `src/services/rpg-creation/rpgCreationPreviewAdapter.ts`
|
||||
- 删除普通 `draftProfile -> CustomWorldProfile` 兜底。
|
||||
- Agent 结果页 profile 只允许来自 `session.resultPreview.preview`,或缺少 resultPreview 时来自明确的 `draftProfile.legacyResultProfile` 兼容快照。
|
||||
- 基础 `draftProfile` 不再能被静默当作运行态 profile,避免作品测试再次读到旧草稿骨架。
|
||||
2. `src/hooks/rpg-session/useRpgSessionBootstrap.ts`
|
||||
- 自定义世界选角后的开局状态已经显式构造第一幕 encounter 时,直接返回开局状态。
|
||||
- 不再把自定义世界开局交给 `ensureSceneEncounterPreview()` 二次推断,避免旧的友好 NPC / 场景预览链路覆盖第一幕 `oppositeNpcId`。
|
||||
3. `src/components/rpg-entry/useRpgCreationEnterWorld.ts` 与 `src/components/rpg-entry/useRpgCreationResultAutosave.ts`
|
||||
- 移除“只读 session.draftProfile / draftProfile 是真相源”这类已经误导本次排查的注释。
|
||||
- 明确作品测试读取当前结果页 profile,不静默回退到基础 draftProfile。
|
||||
|
||||
闭口后的主链路:结果页 profile -> 作品测试选角 -> 第一章第一幕 -> `oppositeNpcId` encounter。普通场景预览只作为非自定义世界或非开局场景的兜底,不再参与作品测试开局第一幕的角色裁决。
|
||||
|
||||
补充闭口:
|
||||
|
||||
1. Agent 结果页作品测试与发布入口要求存在当前结果页 profile。
|
||||
2. 若当前结果页 profile 缺失,入口直接停止,不再使用 `generatedCustomWorldProfile` 旧内存态兜底。
|
||||
|
||||
## 2026-04-27 第六轮入口引用与测试态收口
|
||||
|
||||
用户复测后再次出现“进入后没有正确显示幕配置角色,且没有进入聊天状态”。本轮继续把问题压回作品测试真实入口,确认前几轮在标准 `oppositeNpcId` 写法下可以正确进入聊天,但真实生成数据可能把第一幕角色引用写成运行时 NPC 形态,例如 `character-npc-角色id`,或混用角色 id、名称、标题、角色职责文本。旧引用解析只覆盖了部分标准形态,导致第一幕 encounter 解析失败后,运行态会退回普通开局剧情或其他场景角色,自然也不会进入该幕 NPC 聊天。
|
||||
|
||||
本轮修正:
|
||||
|
||||
1. `src/services/customWorldRoleReferences.ts`
|
||||
- 角色引用归一化新增 `character-npc-*`、`npc-*`、`story-*`、`playable-*` 等运行时/草稿前缀剥离。
|
||||
- 角色别名新增“职责+姓名”“姓名+职责”等组合,兼容生成器把 `role` 文本写入幕槽位的情况。
|
||||
2. `src/hooks/rpg-session/useRpgSessionBootstrap.ts`
|
||||
- 第一幕候选 NPC 解析在进入优先队列前先通过当前 profile 统一归一化。
|
||||
- 跳过当前玩家角色时,不再只比较 `character.id`,而是同时用角色引用解析比较 id/name,避免玩家本人被 `oppositeNpcId` 的别名误判为对面 NPC。
|
||||
- 自定义世界作品测试进入选择世界与选角后的运行态明确标记 `runtimeMode: 'test'`、`runtimePersistenceDisabled: true`,避免作品测试被普通游玩存档/自动保存链路污染。
|
||||
3. `src/hooks/useGameFlow.customWorld.test.tsx`
|
||||
- 增加复现测试:当第一幕 `oppositeNpcId` 写成 `character-npc-story-act-only` 时,选角后仍必须命中陆衡,并由陆衡主动进入聊天。
|
||||
- 增加作品测试态断言,确保测试入口不参与普通持久化。
|
||||
|
||||
补充验证命令:
|
||||
|
||||
```bash
|
||||
npm test -- --run src/hooks/useGameFlow.customWorld.test.tsx src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.test.tsx
|
||||
npx eslint src/hooks/rpg-session/useRpgSessionBootstrap.ts src/hooks/useGameFlow.customWorld.test.tsx src/services/customWorldRoleReferences.ts
|
||||
npm run typecheck -- --pretty false
|
||||
```
|
||||
|
||||
以上测试、ESLint 与类型检查已通过。后端代码未在本轮修改中触碰,因此仍不需要执行 `npm run api-server:maincloud`。
|
||||
|
||||
28
docs/technical/RPG_TEST_RUNTIME_END_BUTTON_2026-04-27.md
Normal file
28
docs/technical/RPG_TEST_RUNTIME_END_BUTTON_2026-04-27.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# RPG 作品测试结束按钮补齐(2026-04-27)
|
||||
|
||||
## 背景
|
||||
|
||||
世界创作结果页已经提供“作品测试”入口,但测试运行时此前缺少与“幕预览”一致的显式退出按钮。创作者进入测试后只能依赖浏览器返回、刷新或其他间接链路离开,不符合独立运行时面板的交互语义。
|
||||
|
||||
## 本次约束
|
||||
|
||||
1. “作品测试”进入的运行时必须显式标记为 `runtimeMode: "test"`。
|
||||
2. 测试态退出入口使用固定浮层按钮,文案为“结束测试”。
|
||||
3. “结束测试”不做保存,不写正式游玩存档。
|
||||
4. 从结果页进入作品测试后,结束测试必须返回当前结果页,而不是平台首页。
|
||||
5. 正式“进入世界 / 发布并进入世界”保持原有行为,不受本次退出按钮影响。
|
||||
|
||||
## 落地实现
|
||||
|
||||
1. `App.tsx` 为自定义世界运行时启动增加轻量 launch options,记录本次进入是 `play` 还是 `test`,以及测试结束后的返回 stage。
|
||||
2. `useRpgCreationEnterWorld.ts` 将结果页“作品测试”入口显式标记为 `mode: "test"`,并写入 `returnStage: "custom-world-result"`。
|
||||
3. `useRpgSessionBootstrap.ts` 支持自定义世界按启动模式写入 `runtimeMode` 与 `runtimePersistenceDisabled`:
|
||||
- `test`:禁存;
|
||||
- `play`:沿正式游玩链路运行。
|
||||
4. `RpgRuntimeShell.tsx` 在 `runtimeMode === "test"` 时叠加固定浮层按钮“结束测试”,点击后直接退出运行时并返回启动前页面。
|
||||
|
||||
## 验证
|
||||
|
||||
1. 结果页点击“作品测试”后可见“结束测试”按钮。
|
||||
2. 点击“结束测试”后返回结果页,且“作品测试 / 发布”按钮仍可继续操作。
|
||||
3. `useRpgSessionPersistence` 继续跳过测试态存档,不新增正式游玩记录。
|
||||
Reference in New Issue
Block a user