Files
Genarrative/docs/audits/engineering/MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md
高物 ddcb5d5c8c
Some checks failed
CI / verify (push) Has been cancelled
Rework story engine flow and reorganize project docs
2026-04-06 23:19:00 +08:00

11 KiB
Raw Blame History

怪物-NPC 脚本统一整改审计

日期2026-04-06

核心结论

当前工程仍然没有真正落实“怪物就是初始好感度为负数的 NPC”这一原则。

现状不是“NPC 脚本里支持 hostile 状态”,而是同时存在两条并行链路:

  1. npc / encounter / npcStates / npcInteraction
  2. monster / hostileNpc / sceneMonsters / sceneHostileNpcs / hostileNpcPresets

这会直接导致:

  • 同一个敌对实体同时拥有 NPC 身份和 monster 身份。
  • 场景、战斗、渲染、提示词都在维护两套入口。
  • 后续修 bug 时,任何位置、死亡、血条、入场、掉落问题都要同时查两条链路。

本次文档的目标不是立刻改代码,而是先把应该删除的分叉脚本、应该降级成素材层的文件、以及必须合并的字段全部列清楚,作为后续统一改造的依据。

当前违背原则的根因

1. 场景数据仍然把“怪物”和“NPC”当成两类实体

当前场景层同时维护:

  • ScenePreset.monsterIds
  • SceneNpc[]
  • ScenePresetInfo.hostileNpcIds
  • SceneNpc.monsterPresetId / hostileNpcPresetId

这意味着场景里一个敌对单位既可以来自 monsterIds,也可以来自 npcs,甚至会被脚本再生成为 hostile scene npc。

2. 运行时仍然存在“怪物专用实体状态”

当前运行时仍然同时维护:

  • GameState.sceneMonsters
  • GameState.sceneHostileNpcs
  • SceneHostileNpc / SceneMonster

这和“怪物本质上只是 hostile NPC”是冲突的。真正统一后运行时只应该保留一套“场景 NPC / 战斗 NPC”状态。

3. 战斗脚本仍然把怪物当独立 actor

战斗层目前不是“NPC 战斗,只是 hostile 的那部分会出手”,而是显式写了:

  • TurnActor = 'player' | 'companion' | 'monster'
  • getClosestMonster
  • resetCombatPresentation(monsters, ...)
  • sceneMonsters 全链路结算

这会强制后面所有视觉、掉落、提示词、AI 上下文都跟着叫 monster。

4. 渲染层仍然有 monster 专属显示入口

画布层当前仍然依赖:

  • sceneMonsters
  • sceneHostileNpcs
  • monsterPresetId
  • HostileNpcAnimator

也就是“敌对 NPC 是否按 NPC 脚本渲染”这件事,到最终显示层仍然没有统一。

一级删除清单

下面这些文件属于“业务流程分叉脚本”,不是单纯资源适配层。后续统一时应优先删除或并入 NPC 主链路。

文件 当前分叉角色 处理建议
src/data/monsters.ts hostileNpcs.ts 的别名出口 直接删除,禁止继续保留 monster 专属入口名。
src/data/hostileNpcs.ts 负责 monster 创建、编队、距离、朝向、变化、落位 按 hostile NPC 运行时工具重写并并入 NPC 体系;原文件名不应继续保留。
src/data/sceneEncounterPreviews.ts 单独构造 hostile encounter group、auto battle、hostile preview 删除 monster 专线逻辑,改为“负好感 NPC 预览/入场/转战斗”。
src/hooks/combat/battlePlan.ts 使用 monster actor、sceneMonstersgetClosestMonster 改成统一的 hostile NPC combatant 规划脚本monster actor 概念应移除。
src/hooks/combat/playback.ts 使用 sceneMonsters 播放怪物战斗演出 改成统一 NPC 战斗回放;不再区分 monster 播放器。
src/components/game-canvas/GameCanvasRuntime.tsx 运行时按 sceneMonsters / sceneHostileNpcs 双数据源选敌方实体 删除双源兜底,统一成单一 hostile NPC 列表。
src/components/game-canvas/GameCanvasEntityLayer.tsx 敌方实体按 monsterPreset 分支渲染 改成同一套 NPC 实体渲染,视觉差异仅由 visual preset 决定。
src/components/game-canvas/GameCanvasShared.tsx 定义 sceneMonsters / sceneHostileNpcs props 与 monster 专属计算 删除这些字段与 helper改成统一 NPC 画布协议。
src/components/preset-editor/MonsterPresetPanel.tsx 独立怪物预设编辑面板 并回 NPC hostile visual preset 面板,或删除该独立编辑器入口。
src/components/preset-editor/MonsterPresetTab.tsx 独立怪物预设页签 与上面一并删除或并入 NPC preset 编辑器。
src/components/preset-editor/ScenePresetPanel.tsx 仍然单独编辑 monsterIds 删除 monsterIds 编辑项,只保留场景 NPC 列表。

二级归并清单

下面这些文件不一定需要物理删除,但它们当前仍然在放大 monster / NPC 分轨,必须在统一改造时一起收口。

文件 当前问题 处理建议
src/data/scenePresets.ts 通过 monsterIds 再生 hostile scene npc并区分 getSceneHostileNpcs / getSceneFriendlyNpcs 保留场景数据文件本身,但删除 monsterIds 体系,让敌对角色直接存在于 npcs 中。
src/data/customWorldNpcMonsters.ts 用单独脚本推导“怪物型 NPC”预设 可保留为 hostile visual preset 选择器,但不能再生成第二套实体语义。
src/data/hostileNpcPresets.ts 目前既是视觉预设库,也是独立 hostile 流程的数据源 降级为 hostile visual/combat preset 库;不再拥有独立实体生命周期。
src/components/HostileNpcAnimator.tsx 当前名字和调用语义都在暗示“独立怪物实体” 可以保留为贴图播放器,但应改为 hostile NPC 的视觉适配组件,而不是独立物种脚本。
src/components/AdventureEntityModal.tsx 详情弹窗仍会优先查 monster preset / hostileNpcPreset 统一读取 NPC 档案;视觉差异只通过 hostile preset 补充。
src/components/SkillEffectPreview.tsx 预览器直接使用 sceneMonsterscreateSceneMonstersFromIds 改成统一 hostile NPC 预览态。
src/components/StateFunctionEditor.tsx 编辑器里仍然直接造 monster battle preview 改成 hostile NPC preview。
src/components/preset-editor/SceneNpcPresetPanel.tsx 仍然暴露 monsterPresetId 字段 改成更明确的 hostile visual preset 字段避免“怪物类型”和“NPC 类型”双语义。
src/hooks/story/npcEncounterActions.ts 虽然入口叫 npc但内部仍然写 sceneMonsters / sceneHostileNpcs 改成统一 hostile NPC 战斗状态字段。
src/hooks/story/choiceActions.ts 仍然有 buildHostileNpcBattleRewardgetResolvedSceneHostileNpcs 这一层额外概念 统一到 hostile NPC 结算工具,不再把“敌对 NPC”和“monster”混称。
src/hooks/useStoryGeneration.ts 给 AI/剧情层传入 sceneMonsterssceneHostileNpcs 改成统一的 hostile NPC 上下文切片。
src/services/prompt.ts 仍然从 monsterIdscreateSceneMonstersFromIds 组 prompt 改成从场景 NPC 列表中筛出 hostile NPC。
src/services/questDirector.ts 仍然依赖 monsterPresetId 推导当前敌对目标 统一改为基于负好感或 hostile 标记的 NPC。
src/services/ai.ts 仍然混用 monsterIds、sceneNpc 的 hostile 判定 与场景统一后改成只读 NPC 列表。
src/services/questTypes.ts 仍然把 hostileNpcIds / monsterIds 当作 scene 快照字段 删除 monsterIds,保留 hostile NPC 语义。

可保留但必须降级为“素材/配置层”的内容

下面这些内容不一定要消失,但不能继续作为独立业务链路存在:

文件/内容 可以保留的原因 必须收口的边界
src/components/HostileNpcAnimator.tsx 怪物贴图是特殊资源,需要专门 sprite sheet 播放器 只负责画图,不再决定实体类型、战斗身份、交互入口。
src/data/hostileNpcPresets.ts hostile visual/combat preset 仍然有价值 只能作为 hostile NPC 的 visual/combat preset 库不再驱动另一套“monster 实体”。
src/data/hostileNpcOverrides.json 资源级 override 仍可继续用 不能再配套出独立 hostile 流程。
src/data/monsterOverrides.json 如果只是素材映射,可迁移到 hostile visual preset override 不应继续以 monster 专属命名长期存在。
src/data/customWorldNpcMonsters.ts 自定义世界里确实需要从文本匹配 hostile visual preset 只能产出“NPC 使用哪个 hostile visual preset”不能产出独立 monster 身份。

字段级必须合并的内容

后续改代码时,至少要把下面这些字段和类型一起收口:

当前字段/类型 问题 合并方向
ScenePreset.monsterIds 场景里额外保存一份怪物池 删除,只保留 npcs
ScenePresetInfo.hostileNpcIds 历史遗留双字段 直接由 npcs.filter(initialAffinity < 0 或 hostile) 推导。
Encounter.monsterPresetId 把 hostile NPC 再次物种化 改成 hostile visual preset 字段,或并入统一 visualRef。
Encounter.hostileNpcPresetId monsterPresetId 语义重叠 与上面合并为一个字段。
GameState.sceneMonsters 把敌对 NPC 单独塞进 monster 容器 改成统一 sceneNpcCombatants 或等价单一列表。
GameState.sceneHostileNpcs 历史兼容层,导致双数据源 删除。
SceneHostileNpc / SceneMonster 类型名直接固化了分轨 改成统一的 hostile NPC / scene combat NPC 类型。
SceneHostileNpcChange / SceneMonsterChange 继续复制同一套变更结构 合并成统一 NPC scene change。
SceneNpc.monsterPresetId / hostileNpcPresetId 同一实体上挂两套 preset 入口 收敛为一个 hostile visual/combat preset 字段。

本轮最优先的删除顺序

建议后续真正改代码时,按下面顺序删并,风险最低:

  1. 先删字段入口:monsterIds / sceneHostileNpcs / hostileNpcPresetId
  2. 再删运行时双轨:src/data/monsters.tssrc/data/hostileNpcs.tssrc/data/sceneEncounterPreviews.ts
  3. 再删战斗双轨:battlePlan.tsplayback.ts 里的 monster actor 与 sceneMonsters
  4. 再删画布双轨:GameCanvasRuntime.tsxGameCanvasEntityLayer.tsxGameCanvasShared.tsx
  5. 最后清编辑器和提示词:MonsterPresetPanel.tsxMonsterPresetTab.tsxprompt.tsquestDirector.ts

改造后的目标形态

统一后应只剩下这一套语义:

  • 场景中所有可见角色都放在 npcs
  • 怪物 = initialAffinity < 0hostile = true 的 NPC
  • hostile 的视觉差异只来自 hostile visual preset
  • 战斗中所有敌方单位都属于 hostile NPC combatant
  • AI、任务、渲染、详情、掉落都只读同一套 NPC 数据

如果后面代码里还出现下面这些关键词,基本都说明分轨没有删干净:

  • sceneMonsters
  • sceneHostileNpcs
  • monsterIds
  • hostileNpcPresetId
  • createSceneMonstersFromIds
  • getClosestMonster
  • TurnActor = 'monster'

这份文档的使用方式

后续正式开始改造时,建议把文件分成三批执行:

  1. “直接删掉”的入口脚本
  2. “改名并并回 NPC 主链路”的桥接脚本
  3. “仅保留素材职责”的 renderer / preset 文件

不要继续接受“名字叫 NPC但内部仍然先转成 monster 再跑”的中间态。