11 KiB
11 KiB
怪物-NPC 脚本统一整改审计
日期:2026-04-06
核心结论
当前工程仍然没有真正落实“怪物就是初始好感度为负数的 NPC”这一原则。
现状不是“NPC 脚本里支持 hostile 状态”,而是同时存在两条并行链路:
npc / encounter / npcStates / npcInteractionmonster / hostileNpc / sceneMonsters / sceneHostileNpcs / hostileNpcPresets
这会直接导致:
- 同一个敌对实体同时拥有 NPC 身份和 monster 身份。
- 场景、战斗、渲染、提示词都在维护两套入口。
- 后续修 bug 时,任何位置、死亡、血条、入场、掉落问题都要同时查两条链路。
本次文档的目标不是立刻改代码,而是先把应该删除的分叉脚本、应该降级成素材层的文件、以及必须合并的字段全部列清楚,作为后续统一改造的依据。
当前违背原则的根因
1. 场景数据仍然把“怪物”和“NPC”当成两类实体
当前场景层同时维护:
ScenePreset.monsterIdsSceneNpc[]ScenePresetInfo.hostileNpcIdsSceneNpc.monsterPresetId / hostileNpcPresetId
这意味着场景里一个敌对单位既可以来自 monsterIds,也可以来自 npcs,甚至会被脚本再生成为 hostile scene npc。
2. 运行时仍然存在“怪物专用实体状态”
当前运行时仍然同时维护:
GameState.sceneMonstersGameState.sceneHostileNpcsSceneHostileNpc / SceneMonster
这和“怪物本质上只是 hostile NPC”是冲突的。真正统一后,运行时只应该保留一套“场景 NPC / 战斗 NPC”状态。
3. 战斗脚本仍然把怪物当独立 actor
战斗层目前不是“NPC 战斗,只是 hostile 的那部分会出手”,而是显式写了:
TurnActor = 'player' | 'companion' | 'monster'getClosestMonsterresetCombatPresentation(monsters, ...)sceneMonsters全链路结算
这会强制后面所有视觉、掉落、提示词、AI 上下文都跟着叫 monster。
4. 渲染层仍然有 monster 专属显示入口
画布层当前仍然依赖:
sceneMonsterssceneHostileNpcsmonsterPresetIdHostileNpcAnimator
也就是“敌对 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、sceneMonsters、getClosestMonster |
改成统一的 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 |
预览器直接使用 sceneMonsters 和 createSceneMonstersFromIds |
改成统一 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 |
仍然有 buildHostileNpcBattleReward 和 getResolvedSceneHostileNpcs 这一层额外概念 |
统一到 hostile NPC 结算工具,不再把“敌对 NPC”和“monster”混称。 |
src/hooks/useStoryGeneration.ts |
给 AI/剧情层传入 sceneMonsters 或 sceneHostileNpcs |
改成统一的 hostile NPC 上下文切片。 |
src/services/prompt.ts |
仍然从 monsterIds 和 createSceneMonstersFromIds 组 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 字段。 |
本轮最优先的删除顺序
建议后续真正改代码时,按下面顺序删并,风险最低:
- 先删字段入口:
monsterIds / sceneHostileNpcs / hostileNpcPresetId - 再删运行时双轨:
src/data/monsters.ts、src/data/hostileNpcs.ts、src/data/sceneEncounterPreviews.ts - 再删战斗双轨:
battlePlan.ts、playback.ts里的monsteractor 与sceneMonsters - 再删画布双轨:
GameCanvasRuntime.tsx、GameCanvasEntityLayer.tsx、GameCanvasShared.tsx - 最后清编辑器和提示词:
MonsterPresetPanel.tsx、MonsterPresetTab.tsx、prompt.ts、questDirector.ts
改造后的目标形态
统一后应只剩下这一套语义:
- 场景中所有可见角色都放在
npcs - 怪物 =
initialAffinity < 0或hostile = true的 NPC - hostile 的视觉差异只来自 hostile visual preset
- 战斗中所有敌方单位都属于 hostile NPC combatant
- AI、任务、渲染、详情、掉落都只读同一套 NPC 数据
如果后面代码里还出现下面这些关键词,基本都说明分轨没有删干净:
sceneMonsterssceneHostileNpcsmonsterIdshostileNpcPresetIdcreateSceneMonstersFromIdsgetClosestMonsterTurnActor = 'monster'
这份文档的使用方式
后续正式开始改造时,建议把文件分成三批执行:
- “直接删掉”的入口脚本
- “改名并并回 NPC 主链路”的桥接脚本
- “仅保留素材职责”的 renderer / preset 文件
不要继续接受“名字叫 NPC,但内部仍然先转成 monster 再跑”的中间态。