Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
# 工程优化审查报告(2026-03-29)
|
||||
|
||||
## 说明
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`package.json`、`vite.config.ts`、`tsconfig.json`
|
||||
- 已执行校验:`npm run lint`、`npm run build`、`npm run check:content`
|
||||
- 本报告只从工程角度讨论结构、边界、质量门禁、可维护性与可扩展性
|
||||
- 按仓库说明,暂不讨论中文乱码本身
|
||||
|
||||
## 当前结论
|
||||
|
||||
项目当前**可构建、可运行、内容校验可通过**,说明基础功能链路是通的;但从工程视角看,已经出现明显的“单点过重、边界混杂、质量门禁偏弱、编辑器与运行时耦合”问题。继续叠需求会越来越依赖人工记忆和局部经验,回归风险会持续上升。
|
||||
|
||||
当前最值得优先处理的不是单个 UI 细节,而是以下四个工程主题:
|
||||
|
||||
1. 运行时主链路的职责拆分还不够,核心 hook / 组件已经过载
|
||||
2. 缺少真正的工程质量门禁,`lint` 目前本质上只是 `tsc`
|
||||
3. 编辑器、运行时、类后端能力都混在同一个 Vite 配置里
|
||||
4. 持久化、AI 调用、编辑器保存等基础设施仍然是“分散手写”
|
||||
|
||||
## 运行状态快照
|
||||
|
||||
- `npm run lint` 通过
|
||||
- `npm run build` 通过
|
||||
- `npm run check:content` 通过
|
||||
- 应用代码下未发现测试文件:`src/`、`scripts/`、`docs/` 内没有 `*.test.*` / `*.spec.*`
|
||||
- 构建产物已出现较大 chunk
|
||||
- `dist/assets/App-*.js` 约 `407 KB`
|
||||
- `dist/assets/itemCatalog-*.js` 约 `414 KB`
|
||||
- `dist/assets/PresetEditor-*.js` 约 `109 KB`
|
||||
|
||||
## 代码体征
|
||||
|
||||
下列文件已经明显进入“超大模块”区间:
|
||||
|
||||
| 文件 | 行数 | 观察 |
|
||||
| --- | ---: | --- |
|
||||
| `src/hooks/useStoryGeneration.ts` | 3304 | 同时管理剧情、NPC 交互、交易、送礼、招募、任务、角色聊天、道具/锻造接入 |
|
||||
| `src/components/PresetEditor.tsx` | 2244 | 多编辑器入口聚合在一个巨型组件中 |
|
||||
| `src/hooks/useCombatFlow.ts` | 1791 | 同时承担战斗推演、动画时序、逃跑演出、状态落地 |
|
||||
| `src/components/GameShell.tsx` | 1592 | 入口 UI、选角、世界选择、自定义世界、场景切换、浮层控制全部集中 |
|
||||
| `src/types.ts` | 663 | 运行时、AI、编辑器、自定义世界、背包、任务类型集中在一个总文件 |
|
||||
|
||||
补充信号:
|
||||
|
||||
- `src/components/GameShell.tsx` 内有 16 个 `useState`、10 个 `useEffect`、13 个 `useMemo`
|
||||
- `src/hooks/useStoryGeneration.ts` 虽然只有少量 React state,但内部累计 40+ 个函数,已经是“巨型流程控制器”
|
||||
- `src/hooks/useCombatFlow.ts` 内有大量时间常量、动画常量、`sleep + setGameState` 过程式循环,测试成本很高
|
||||
|
||||
## 优先级问题
|
||||
|
||||
## P0:运行时主链路职责过度集中
|
||||
|
||||
证据:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:868-930` 进入 hook 后立即开始定义交易、送礼、招募、角色聊天等子流程
|
||||
- `src/hooks/useStoryGeneration.ts:3191-3303` 返回对象同时暴露剧情、任务、NPC UI、角色聊天 UI、背包/锻造 UI
|
||||
- `src/components/GameShell.tsx:293-360` 组件 props 很多,内部 state 也很多,承担“壳层 + 流程 + 浮层 + 自定义世界生成 + 场景切换”
|
||||
- `src/hooks/useCombatFlow.ts:559-1787` 将战斗计算和战斗演出揉在同一层里
|
||||
|
||||
影响:
|
||||
|
||||
- 任何一个新需求都容易同时碰到剧情、UI、战斗、背包、NPC 关系四五条链路
|
||||
- 代码 review 很难聚焦,改动一处时往往需要脑内跟完整条大流程
|
||||
- 单元测试难写,因为逻辑不是纯函数,而是大量闭包 + 过程式状态推进
|
||||
- 长期会形成“只有熟悉历史上下文的人才能安全修改”的隐性门槛
|
||||
|
||||
建议:
|
||||
|
||||
- 将 `useStoryGeneration` 拆为“剧情推进”“NPC 交互”“角色聊天”“任务结算”“模态框控制”几个子域
|
||||
- 将 `useCombatFlow` 拆成“纯战斗结算引擎”和“战斗播放适配层”
|
||||
- 让 `GameShell` 回到壳层职责,只负责路由态、页面态、模态挂载与 props 编排
|
||||
- 以“领域职责”拆分,而不是按“文件太长了随便切一刀”拆分
|
||||
|
||||
## P0:缺少真正的工程质量门禁
|
||||
|
||||
证据:
|
||||
|
||||
- `package.json:11` 的 `lint` 实际只有 `tsc --noEmit`
|
||||
- `package.json` 中没有 `test`、`format`、`lint:fix` 等基础脚本
|
||||
- 根目录未发现 `.eslintrc*`、`.prettierrc*`、`.editorconfig`
|
||||
- 代码目录下没有测试文件
|
||||
|
||||
影响:
|
||||
|
||||
- 当前项目的“能过 lint”只代表类型没炸,不代表风格一致、依赖正确、Hooks 规则正确、死代码已清理
|
||||
- 大型 hook / 大型组件的重构几乎没有自动回归保护
|
||||
- 运行时行为、编辑器行为、AI fallback 行为主要依赖人工回归
|
||||
|
||||
建议:
|
||||
|
||||
- 补齐 ESLint、Prettier、EditorConfig,至少覆盖 React Hooks、import、unused code、复杂度基线
|
||||
- 引入 Vitest,先覆盖纯数据层与纯规则层
|
||||
- 为 `useCombatFlow`、`stateFunctions`、`npcInteractions`、`questFlow` 增加单元测试
|
||||
- 为“开局 -> 选世界 -> 选角色 -> 进入剧情 -> 战斗 -> 存档恢复”补最小 E2E smoke
|
||||
- CI 中至少串联:类型检查 + 单测 + build + 内容校验
|
||||
|
||||
## P1:编辑器、运行时、类后端能力全部耦合在 Vite 配置里
|
||||
|
||||
证据:
|
||||
|
||||
- `vite.config.ts:151-203` 在 Vite 插件里实现了 LLM 代理
|
||||
- `vite.config.ts:206-269` 在 Vite 插件里实现了通用 JSON 文件读写 API
|
||||
- `vite.config.ts:253` 直接写回 `src/data/*.json`
|
||||
- `vite.config.ts:265-266` 和 `vite.config.ts:400-401` 在 `preview` 阶段也挂了这些接口
|
||||
- `vite.config.ts:425-434` 启动时默认把这些“编辑器后端能力”全部注册进去
|
||||
|
||||
影响:
|
||||
|
||||
- 本地编辑器能力与运行时能力没有清晰边界
|
||||
- `preview` 环境仍可写源码文件,发布边界不清晰
|
||||
- 未来如果要做独立部署、多人协作、远程编辑、权限控制,会非常难迁移
|
||||
- Vite 配置同时扮演构建配置、代理层、文件服务层、编辑器后端,职责失衡
|
||||
|
||||
建议:
|
||||
|
||||
- 将编辑器读写 API 从 `vite.config.ts` 抽到独立的本地工具服务或独立脚本
|
||||
- 至少区分 `dev-only write api` 与 `preview/prod read-only api`
|
||||
- 对编辑器保存接口建立统一客户端 SDK,避免组件直接散落 `fetch('/api/...')`
|
||||
- LLM 代理也建议独立成 `server/` 或 `scripts/dev-server/`,不要继续长在构建配置里
|
||||
|
||||
## P1:持久化策略分散,且直接序列化大状态对象
|
||||
|
||||
证据:
|
||||
|
||||
- `src/hooks/useGamePersistence.ts:152-167` 会在状态变化时自动把完整快照写入 `localStorage`
|
||||
- `src/hooks/useGamePersistence.ts:157-163` 快照包含 `gameState + bottomTab + currentStory`
|
||||
- `src/hooks/useGamePersistence.ts:68-116` 恢复逻辑已经开始承担大量 schema 纠偏职责
|
||||
- `src/data/customWorldLibrary.ts:1-282` 自定义世界库单独维护一套 `localStorage` 读写与 normalize
|
||||
- `src/hooks/useGameSettings.ts` 也单独维护一套本地设置持久化
|
||||
|
||||
影响:
|
||||
|
||||
- 状态结构一旦继续膨胀,快照写入频率和反序列化成本都会增加
|
||||
- schema 迁移会越来越依赖手工 normalize 补丁
|
||||
- 不同持久化入口各写一套 parser / normalizer,风格和鲁棒性难统一
|
||||
- 当前保存的是“运行中大对象”,而不是“稳定领域快照”,长期会放大兼容成本
|
||||
|
||||
建议:
|
||||
|
||||
- 建立统一的 persistence 层,集中管理 key、version、migration、节流、序列化策略
|
||||
- 对 `GameState` 做“可持久化切片”和“运行时临时切片”分层
|
||||
- 自动保存增加节流/去抖,避免每次状态波动都全量落盘
|
||||
- 如果继续扩展角色聊天、自定义世界、编辑器草稿,建议评估 IndexedDB 替代 `localStorage`
|
||||
|
||||
## P1:运行时与编辑器仍在同一个前端入口体系中,包体继续膨胀
|
||||
|
||||
证据:
|
||||
|
||||
- `src/main.tsx:21-34` 通过 `window.location.pathname` 手写分发页面
|
||||
- `src/main.tsx:60` 只有“游戏”和“PresetEditor”两个大入口
|
||||
- `PresetEditor`、`ItemCatalogEditor`、`StateFunctionEditor` 都属于重型模块
|
||||
- 构建产物已经出现 `App` 约 `407 KB`、`itemCatalog` 约 `414 KB` 的 chunk
|
||||
|
||||
影响:
|
||||
|
||||
- 游戏端与编辑器端的演进节奏被绑定在一个 SPA 入口上
|
||||
- 编辑器相关数据和静态资源容易继续抬高构建体积
|
||||
- 未来增加更多编辑器页、更多世界模板、更多资源目录后,冷启动成本会更明显
|
||||
|
||||
建议:
|
||||
|
||||
- 将编辑器拆成独立入口,至少做成独立 route module,而不是单个 `PresetEditor`
|
||||
- 继续下钻按 tab 做懒加载,尤其是 `items/functions/npcs`
|
||||
- 将静态大数据、资源目录索引、编辑器专用预览逻辑做更细的 chunk 拆分
|
||||
- 如果项目后续会长期保留编辑器,建议直接分成 game app / editor app 两个 entry
|
||||
|
||||
## P2:编辑器基础设施重复实现较多
|
||||
|
||||
证据:
|
||||
|
||||
- `src/components/PresetEditor.tsx:111-181` 自己实现 `cloneValue`、`saveJsonObject`
|
||||
- `src/components/StateFunctionEditor.tsx:113-130` 再次实现 `cloneValue`、`SectionCard`
|
||||
- `src/components/ItemCatalogEditor.tsx:94` 再次实现保存请求
|
||||
- `src/hooks/useInventoryFlow.ts:8`、`src/hooks/useEquipmentFlow.ts:10`、`src/hooks/useForgeFlow.ts:12`、`src/hooks/useTreasureFlow.ts:10` 重复声明 `CommitGeneratedState`
|
||||
|
||||
影响:
|
||||
|
||||
- 修改保存行为、错误处理、深拷贝策略时需要多处同步
|
||||
- 编辑器 UI 风格与交互行为容易逐步漂移
|
||||
- 公共契约没有收拢到共享层,维护成本会逐步抬高
|
||||
|
||||
建议:
|
||||
|
||||
- 抽 `editor/shared/` 层,集中放保存 SDK、表单字段、卡片容器、克隆工具、错误处理
|
||||
- 抽通用的 `CommitGeneratedState` 类型定义
|
||||
- 将编辑器请求和覆盖保存逻辑统一走一个 client
|
||||
|
||||
## P2:类型系统已经出现“总文件过载”
|
||||
|
||||
证据:
|
||||
|
||||
- `src/types.ts` 共 663 行
|
||||
- `src/types.ts:1-260` 同时包含世界、动画、技能、对话、自定义世界、物品等类型
|
||||
- `src/types.ts:536-663` 又继续承接剧情、聊天、任务、`GameState`、AI 响应
|
||||
|
||||
影响:
|
||||
|
||||
- 任一领域类型变化都会增加总文件冲突概率
|
||||
- 新人理解类型边界成本高
|
||||
- 编辑器类型、运行时类型、AI 传输类型被放在一起,不利于演化
|
||||
|
||||
建议:
|
||||
|
||||
- 按领域拆分:`types/combat.ts`、`types/story.ts`、`types/item.ts`、`types/customWorld.ts`、`types/persistence.ts`
|
||||
- `GameState` 相关类型与 editor override 类型分开
|
||||
- AI request/response contract 单独收口,避免继续堆进总类型文件
|
||||
|
||||
## P2:AI 客户端层过厚,且重复了多套请求与解析逻辑
|
||||
|
||||
证据:
|
||||
|
||||
- `src/services/ai.ts` 共 1153 行
|
||||
- `src/services/ai.ts:540-605`、`608-678`、`745-790` 分别手写了 JSON completion、纯文本 completion、流式 completion
|
||||
- `src/services/ai.ts:680-697` 手写了多段 JSON 解析兜底
|
||||
- `src/services/ai.ts:76-78`、`591-594`、`662-666` 主要依赖 `console.*` 打日志
|
||||
|
||||
影响:
|
||||
|
||||
- LLM 行为扩展时容易继续复制请求模板、错误处理、超时逻辑
|
||||
- 错误分类不够稳定,观测主要停留在 console 层
|
||||
- prompt、transport、fallback、parse 被放在一起,后续测试和替换模型都不够轻
|
||||
|
||||
建议:
|
||||
|
||||
- 抽 `llmClient`,统一 transport、timeout、stream、error taxonomy
|
||||
- 抽 `llmParsers`,将 JSON parse / plain text parse / suggestion parse 独立
|
||||
- 为关键 prompt 输出建立 fixture 测试,至少覆盖 fallback 与异常响应
|
||||
- 如果后续要接多个模型,尽早把 provider 层和 prompt 层解耦
|
||||
|
||||
## P2:手写路由与死代码开始累积
|
||||
|
||||
证据:
|
||||
|
||||
- `src/main.tsx:21-34` 采用手写 `pathname.startsWith(...)`
|
||||
- `src/components/GameShell.tsx:1511` 存在 `false && showTeamModal`
|
||||
|
||||
影响:
|
||||
|
||||
- 路由能力不具备可扩展性,也不利于后续加 404、重定向、权限判断、嵌套路由
|
||||
- 死代码继续堆积后,会误导维护者对真实入口和真实 UI 状态的判断
|
||||
|
||||
建议:
|
||||
|
||||
- 引入正式路由层,哪怕只做轻量路由也比手写分发更清晰
|
||||
- 清理已经废弃的 UI 分支和不可达逻辑
|
||||
- 对“临时下线的功能”改为 feature flag 或明确注释,不要用 `false &&`
|
||||
|
||||
## 建议落地顺序
|
||||
|
||||
### 第一阶段:先补工程底座
|
||||
|
||||
- 增加 ESLint / Prettier / EditorConfig
|
||||
- 增加 `test` 脚本与 Vitest
|
||||
- 把 CI 最小闭环搭起来:类型检查、单测、build、内容校验
|
||||
|
||||
### 第二阶段:先拆边界,再拆大文件
|
||||
|
||||
- 先把 Vite 中的编辑器写文件接口、LLM 代理抽走
|
||||
- 再把 `GameShell`、`useStoryGeneration`、`useCombatFlow` 按职责拆域
|
||||
- 拆分时优先保持外部接口稳定,避免一次性全仓大改
|
||||
|
||||
### 第三阶段:收敛基础设施
|
||||
|
||||
- 统一 persistence 层
|
||||
- 统一 editor shared 层
|
||||
- 统一 AI client 层
|
||||
- 拆分 `types.ts`
|
||||
|
||||
### 第四阶段:降低发布成本
|
||||
|
||||
- 将 editor 与 game 做更明确的入口拆分
|
||||
- 优化 chunk 边界
|
||||
- 评估是否把编辑器做成独立 app
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这个仓库当前最需要优化的不是“再补几个功能”,而是**把已经验证有效的玩法与工具链,从“靠大文件和经验串起来”升级为“靠清晰边界、统一基础设施和自动化门禁支撑起来”**。只要这一步不做,后续每次加内容、加编辑器能力、加 AI 流程,工程成本都会持续上升。
|
||||
@@ -0,0 +1,290 @@
|
||||
# 工程优化审查报告(2026-03-30)
|
||||
|
||||
## 审查范围
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`.github/`、`package.json`、`tsconfig.json`、`vite.config.ts`
|
||||
- 实际执行:`npm run lint`、`npm run test`、`npm run build`、`npm run check:content`
|
||||
- 说明:按仓库要求,本报告不讨论中文乱码问题,只讨论工程结构、边界、质量门禁、可维护性和后续扩展成本
|
||||
|
||||
## 先说结论
|
||||
|
||||
这轮代码库相较 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` 已经有明显进展,项目不再是“所有能力都糊在一个入口文件里”的状态了,但整体仍然处于“重构过渡期”。
|
||||
|
||||
已经落地的积极变化:
|
||||
|
||||
- 入口路由已经从手写 `pathname` 分发,收敛到 `src/main.tsx` + `src/routing/appRoutes.tsx`
|
||||
- 持久化能力已经抽到 `src/persistence/`
|
||||
- 编辑器公共能力已经出现 `src/editor/shared/`
|
||||
- `CI + ESLint + Prettier + Vitest` 已经接入
|
||||
- 本地 API 插件已经从 `vite.config.ts` 抽走,落到 `scripts/dev-server/localApiPlugins.ts`
|
||||
- `preview` 环境里的 JSON 写入接口已经改成只读,这一点比上轮更安全
|
||||
|
||||
但当前仍然存在 5 个值得优先处理的工程问题:
|
||||
|
||||
1. 运行时主链仍然过于集中,`story/combat` 的真实边界还没有彻底拆开
|
||||
2. `src/services/ai.ts` 仍处于迁移中间态,存在重复实现和旧逻辑残留
|
||||
3. 编辑器主入口仍是大型聚合组件,迁移残留没有清干净
|
||||
4. 质量门禁已经有框架,但还不够“硬”,warning 和测试覆盖缺口仍然明显
|
||||
5. 运行时渲染层和构建体积仍偏重,重 UI 模块还没拆到合适粒度
|
||||
|
||||
## 当前运行状态
|
||||
|
||||
- `npm run test` 通过,6 个测试文件共 18 个测试全部通过
|
||||
- `npm run build` 通过
|
||||
- `npm run check:content` 通过
|
||||
- `npm run lint` 通过,但仍有 76 条 warning
|
||||
|
||||
当前构建产物里仍然存在较重 chunk:
|
||||
|
||||
- `dist/assets/GameCanvas-*.js` 约 `346.58 kB`
|
||||
- `dist/assets/App-*.js` 约 `326.89 kB`
|
||||
- `dist/assets/index-*.js` 约 `197.80 kB`
|
||||
- `dist/assets/index-*.css` 约 `117.37 kB`
|
||||
|
||||
## P0:运行时主链仍然过于集中,Story/Combat 边界还没有拆透
|
||||
|
||||
### 现状
|
||||
|
||||
虽然 `App.tsx` 已经明显瘦身,`GameShell` 也比之前更像壳层,但真正决定游戏推进的主逻辑仍然高度集中在两个大 hook 里:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:824`
|
||||
- `src/hooks/useCombatFlow.ts:382`
|
||||
|
||||
### 证据
|
||||
|
||||
`useStoryGeneration` 仍然同时编排了多个本应继续拆开的子领域:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:852` 接入 `useCharacterChatFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1583` 接入 `useTreasureFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1588` 接入 `useInventoryFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1593` 接入 `useEquipmentFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1597` 接入 `useForgeFlow`
|
||||
- 文件总长仍有约 `3240` 行
|
||||
- 结尾返回对象同时暴露剧情推进、地图旅行、NPC 交易/送礼/招募、角色聊天、背包与锻造 UI 能力,典型位置在 `src/hooks/useStoryGeneration.ts:3171-3219`
|
||||
|
||||
`useCombatFlow` 也不是纯计算层,它仍然同时承担:
|
||||
|
||||
- 战斗前后状态推导
|
||||
- 动画播放与时间推进
|
||||
- `setGameState` 驱动的可视化编排
|
||||
- 逃跑流程与 story 响应同步
|
||||
|
||||
关键位置:
|
||||
|
||||
- `src/hooks/useCombatFlow.ts:382` `useCombatFlow`
|
||||
- `src/hooks/useCombatFlow.ts:1195` `playEscapeSequenceWithStorySync`
|
||||
|
||||
### 影响
|
||||
|
||||
- 任何一个“剧情选项新增”都很容易同时碰到 battle、npc、quest、inventory、chat 五条链路
|
||||
- review 成本高,回归范围判断依赖人脑上下文
|
||||
- 单测很难往 hook 级别补,因为副作用、异步节奏和 UI 状态混在一起
|
||||
- 后续想继续做 camp、custom world、更多 companion 玩法时,改动会继续集中到这两个入口
|
||||
|
||||
### 建议
|
||||
|
||||
- 把 `useStoryGeneration` 继续下钻成“剧情推进 orchestrator + 领域 action service”
|
||||
- `useStoryGeneration` 自己只保留编排,不再直接维护 trade/gift/recruit/chat/inventory/forge 的全部细节
|
||||
- `useCombatFlow` 继续向“纯战斗结算”和“播放适配层”分离
|
||||
- 先稳定公开接口,再做内部拆分,避免一次性大改
|
||||
|
||||
## P1:AI 服务迁移只完成了一半,`src/services/ai.ts` 仍然存在双轨实现
|
||||
|
||||
### 现状
|
||||
|
||||
仓库已经新增了:
|
||||
|
||||
- `src/services/llmClient.ts`
|
||||
- `src/services/llmParsers.ts`
|
||||
- `src/services/aiFallbacks.ts`
|
||||
- `src/services/aiTypes.ts`
|
||||
|
||||
这说明拆层方向是对的。但 `src/services/ai.ts` 还没有真正变成“纯 orchestration 层”,里面仍然保留着一整套旧 transport / parse / fallback 逻辑。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/services/ai.ts:64-66` 已经开始导入 `llmClient`
|
||||
- `src/services/ai.ts:89-95` 仍然保留本地 `resolveTimeoutMs` 和超时常量
|
||||
- `src/services/ai.ts:647` 仍然保留 `_requestPlainTextCompletion`
|
||||
- `src/services/ai.ts:719` 仍然保留 `_parseJsonResponseText`
|
||||
- `src/services/ai.ts:739` 仍然保留 `_parseLineListContent`
|
||||
- `src/services/ai.ts:784` 仍然保留 `_streamPlainTextCompletion`
|
||||
- `src/services/ai.ts:885-904` 仍然保留一批旧的 `_buildOffline...` helper
|
||||
|
||||
与之对应,新的实现已经在下面这些文件里存在:
|
||||
|
||||
- `src/services/llmClient.ts`
|
||||
- `src/services/llmParsers.ts`
|
||||
|
||||
### 影响
|
||||
|
||||
- 同一类能力现在有两套真相源,后续改错误分类、超时策略、SSE 行为时容易漏改
|
||||
- 新同学读代码时很难判断应该继续改 `ai.ts`,还是应该去改 `llmClient.ts`
|
||||
- 迁移残留会拉高维护成本,也会让测试边界变得模糊
|
||||
|
||||
### 建议
|
||||
|
||||
- 把 `src/services/ai.ts` 收敛成“业务 prompt 编排 + fallback 选择”层
|
||||
- 彻底删掉未再需要的 `_requestPlainTextCompletion`、`_streamPlainTextCompletion`、`_parse*` 等旧 helper
|
||||
- 所有 transport / timeout / connectivity error / SSE 解析都只保留在 `llmClient.ts` 和 `llmParsers.ts`
|
||||
- 迁移完成后,给 `ai.ts` 增加一组 orchestration 级测试,防止 fallback 分支回归
|
||||
|
||||
## P1:编辑器主入口仍然太重,而且过渡态残留还在
|
||||
|
||||
### 现状
|
||||
|
||||
编辑器公共能力已经开始沉淀到 `src/editor/shared/`,这是好事;但主编辑器入口仍然比较重,且部分文件还保留着迁移过程里的死代码和注释块。
|
||||
|
||||
### 证据
|
||||
|
||||
`PresetEditor` 仍然是一个大型聚合组件:
|
||||
|
||||
- `src/components/PresetEditor.tsx:402` `CharacterPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1174` `SceneNpcPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1547` `ScenePresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1852` `MonsterPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:2218` `PresetEditor`
|
||||
- 文件总长仍有约 `2279` 行
|
||||
|
||||
同时,文件里还留着明显的过渡态残留:
|
||||
|
||||
- `src/components/PresetEditor.tsx:227` 仍然保留未使用的 `_SectionCard`
|
||||
- `src/components/NpcVisualEditor.tsx:684` 保留 `if (false)` 的旧保存路径
|
||||
- `src/components/NpcVisualEditor.tsx:685` 明确写着 “Deprecated inline save path kept only until the shared client migration is cleaned up.”
|
||||
- `src/components/NpcVisualEditor.tsx:724` 还有第二处 `if (false)` 残留
|
||||
|
||||
### 影响
|
||||
|
||||
- 编辑器后续继续扩展时,容易重新长回“大一统文件”
|
||||
- 过渡代码会误导维护者,以为旧保存链路仍然有效
|
||||
- 公共层虽然建起来了,但如果不清理旧代码,长期会形成“共享层 + 本地特例”并存
|
||||
|
||||
### 建议
|
||||
|
||||
- 以“一个 tab 一个容器”的方式,把 `PresetEditor` 再拆一层
|
||||
- 清理 `NpcVisualEditor` 里的废弃代码块,不要再保留 `if (false)` 分支
|
||||
- 对编辑器共享层设定明确规则:保存请求、克隆、Section 容器、错误提示都必须走 shared
|
||||
- 对编辑器做一次“小型迁移完成清扫”,优先删掉已经废弃但还挂在文件里的旧路径
|
||||
|
||||
## P1:质量门禁已经接上,但还不够硬
|
||||
|
||||
### 现状
|
||||
|
||||
基础设施已经比上轮完整很多,但当前门禁仍然偏“有检查,不够严格”。
|
||||
|
||||
### 证据
|
||||
|
||||
当前 lint 结果:
|
||||
|
||||
- 本次 `npm run lint` 实际输出 `76` 条 warning,虽然命令返回成功
|
||||
|
||||
脚本和规则层面的原因也很明确:
|
||||
|
||||
- `package.json:12` 的 `lint` 仍然是 `eslint . ... && tsc --noEmit`,没有 `--max-warnings 0`
|
||||
- `package.json:11` 的 `lint:guardrails` 虽然加了 `--max-warnings 0`,但它只覆盖一组显式 allowlist 文件
|
||||
- `package.json:18` 的 `check` 会先跑 `lint:guardrails`,再跑宽松版 `lint`
|
||||
- `.eslintrc.cjs:45-61` 里大量规则仍然是 `warn`
|
||||
- `.github/workflows/ci.yml:28-40` 已经把 `lint / guardrails / test / build / check:content` 都接到 CI,但 warning 仍能稳定进主干
|
||||
|
||||
测试覆盖也还是偏薄:
|
||||
|
||||
- `src/` 当前共有 `126` 个文件
|
||||
- 其中测试文件只有 `6` 个
|
||||
- 现有测试主要覆盖 `routing`、`persistence`、`jsonClient`、`llmParsers`、`battlePlan`
|
||||
- 关键主链如 `useStoryGeneration`、`useCombatFlow` 播放层、`GameShell` 集成链路、编辑器保存流程仍然没有直接测试
|
||||
|
||||
### 影响
|
||||
|
||||
- 代码库会持续积累“已知 warning,但先不处理”的债务
|
||||
- 工程信号会逐渐失真,lint 通过不代表代码足够干净
|
||||
- 大 hook 和大组件的重构依然主要依赖人工回归
|
||||
|
||||
### 建议
|
||||
|
||||
- 先把 warning 收敛到一个可控范围,再把全仓 `lint` 切成 `--max-warnings 0`
|
||||
- `lint:guardrails` 不要长期靠 allowlist,应该逐步扩大到全仓
|
||||
- 优先补三类测试:
|
||||
- `useStoryGeneration` 的状态推进和 modal 决策
|
||||
- `useCombatFlow` 播放层的关键分支
|
||||
- 编辑器保存链路和覆盖数据回写
|
||||
|
||||
## P2:运行时渲染层仍然偏重,chunk 也还没有拆到理想粒度
|
||||
|
||||
### 现状
|
||||
|
||||
入口已经有了 route lazy load,模态框也做了一部分懒加载,但核心运行时渲染层仍然比较重。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/components/AdventurePanel.tsx:470` 导出主组件,文件总长约 `1538` 行
|
||||
- `src/components/GameCanvas.tsx:472` 导出主组件,文件总长约 `1131` 行
|
||||
- `src/components/GameCanvas.tsx:768` 仍然存在 `false && companions.map(...)` 的死分支
|
||||
- 本次构建里 `GameCanvas` 和 `App` 仍然是最大 chunk 之一
|
||||
|
||||
### 影响
|
||||
|
||||
- 运行时页面的首屏与热区模块仍然偏重
|
||||
- 渲染逻辑、场景动画逻辑、实体选中逻辑继续堆在同一层,review 和测试都偏吃力
|
||||
- 清理死分支前,维护者对“哪些渲染路径是真实生效的”判断成本更高
|
||||
|
||||
### 建议
|
||||
|
||||
- `GameCanvas` 继续拆成 scene layer、entity layer、effect layer、overlay layer
|
||||
- `AdventurePanel` 继续下沉 quest/stats/settings/reward 子面板
|
||||
- 清理 `false &&` 与未使用辅助组件,避免假分支继续留在主路径文件中
|
||||
- 结合真实 chunk 数据做一次 route 内部分包,而不是只靠入口级 lazy
|
||||
|
||||
## P2:TypeScript 安全基线仍然偏宽松
|
||||
|
||||
### 现状
|
||||
|
||||
当前类型拆分方向是好的,`src/types.ts` 已经退化成 barrel,真实类型开始向 `src/types/` 下沉。但 TypeScript 编译配置还比较保守,类型系统还没有真正变成强约束。
|
||||
|
||||
### 证据
|
||||
|
||||
- `tsconfig.json:12` `skipLibCheck: true`
|
||||
- `tsconfig.json:16` `allowJs: true`
|
||||
- 当前没有启用 `strict`
|
||||
- 当前没有启用 `noUncheckedIndexedAccess`
|
||||
|
||||
### 影响
|
||||
|
||||
- 对大对象和字典访问的保护仍然偏弱
|
||||
- 新模块迁移到更细类型后,收益会被宽松编译选项部分抵消
|
||||
- “代码能过类型检查”并不等于边界足够安全
|
||||
|
||||
### 建议
|
||||
|
||||
- 不建议一次性全仓开严格模式
|
||||
- 可以先从 `src/services/`、`src/persistence/`、`src/hooks/combat/` 这些相对纯的目录启更严格约束
|
||||
- 至少先评估开启 `noUncheckedIndexedAccess` 和减少 `allowJs` 的必要性
|
||||
|
||||
## 建议的落地顺序
|
||||
|
||||
### 第一阶段:先把过渡态清干净
|
||||
|
||||
- 清理 `ai.ts` 的旧 transport / parser / fallback 实现
|
||||
- 清理 `NpcVisualEditor`、`GameCanvas`、`PresetEditor` 等文件里的 `if (false)`、未使用 helper、废弃注释块
|
||||
- 把 lint warning 数量先打下来
|
||||
|
||||
### 第二阶段:拆主链,不再让大 hook 继续膨胀
|
||||
|
||||
- 继续拆 `useStoryGeneration`
|
||||
- 继续拆 `useCombatFlow`
|
||||
- 优先把“领域动作”和“播放/展示编排”分开
|
||||
|
||||
### 第三阶段:补门禁
|
||||
|
||||
- 给主链补单测和少量集成 smoke
|
||||
- 让全仓 lint 朝 `--max-warnings 0` 收敛
|
||||
- 把 warning 从“长期存在”变成“短周期清零”
|
||||
|
||||
### 第四阶段:优化运行时体积
|
||||
|
||||
- 细化 `GameCanvas` 和 `AdventurePanel` 的模块边界
|
||||
- 按实际交互热区做 chunk 继续拆分
|
||||
- 用真实构建产物持续追踪是否降重
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这轮仓库已经从“完全依赖大文件硬扛”进步到“基础设施开始成形”,但当前最需要做的已经不是继续加功能,而是把这轮重构收尾做完整:继续拆主链、删掉迁移残留、把 lint/test 门禁变硬、再顺手压缩运行时大模块。只要这一步补上,后续加剧情、加编辑器能力、加自定义世界都会轻很多。
|
||||
@@ -0,0 +1,200 @@
|
||||
# 工程优化审查报告(2026-04-01)
|
||||
|
||||
## 审查范围
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`.github/`、`package.json`、`tsconfig*.json`、`vite.config.ts`、`vitest.config.ts`
|
||||
- 审查方式:阅读当前工作区代码结构,抽查核心运行时、编辑器、服务层与开发脚本,并执行工程命令验证现状
|
||||
- 当前快照说明:仓库存在大量未提交改动,本报告基于当前工作区状态,不假定这些改动都已经合入主分支
|
||||
- 说明:按仓库要求,不把中文乱码本身当成本次审查重点;只讨论工程结构、门禁、可维护性、可测试性和扩展成本
|
||||
|
||||
## 已执行检查
|
||||
|
||||
- `npm run lint:eslint`
|
||||
结果:失败。`src/components/ItemCatalogEditor.tsx:167` 存在未使用的 `isSearchPending` 和 `startTransition`
|
||||
- `npm run typecheck`
|
||||
结果:通过
|
||||
- `npm run test`
|
||||
结果:通过,默认套件实际执行 10 个测试文件、28 个测试
|
||||
- `npm run build`
|
||||
结果:通过,但 `src/services/customWorldPresentation.ts:163-169` 出现 duplicate key 警告
|
||||
- `npm run check:content`
|
||||
结果:通过
|
||||
|
||||
## 当前结论
|
||||
|
||||
这轮代码库已经明显比前几版更有工程骨架了,至少有这些积极变化:
|
||||
|
||||
- `src/main.tsx` + `src/routing/appRoutes.tsx` 已经承担了入口路由分发
|
||||
- `src/App.tsx` 已经比过去瘦很多,主流程开始交给 hook 和壳组件
|
||||
- `src/components/PresetEditor.tsx` 已经成为较薄的 lazy shell,而不是继续堆成巨型入口
|
||||
- `src/editor/shared/jsonClient.ts`、`src/persistence/`、`src/hooks/combat/`、`src/hooks/story/` 这些目录说明仓库已经开始做分层
|
||||
- CI、Vitest、ESLint、内容校验脚本都已经接上,不再是完全裸奔状态
|
||||
|
||||
但从工程角度看,当前最值得优先优化的,不是继续加功能,而是把“半完成的工程化”补齐。核心问题集中在 6 个方面。
|
||||
|
||||
## P0:质量门禁和真实风险点仍然脱节
|
||||
|
||||
### 现状
|
||||
|
||||
仓库已经引入了 lint、typecheck、test、build 和 content checks,但关键热区并没有真正纳入统一门禁。
|
||||
|
||||
### 证据
|
||||
|
||||
- `.eslintrc.cjs:47-63` 的 `ignorePatterns` 直接跳过了多个高复杂度核心文件:
|
||||
`src/components/AdventurePanel.tsx`、`src/components/NpcVisualEditor.tsx`、`src/components/preset-editor/PresetEditorPanels.tsx`、`src/hooks/useStoryGeneration.ts`、`src/services/customWorldPresentation.ts`
|
||||
- `tsconfig.typecheck-guardrails.json:6-15` 只对非常有限的一小组文件开启严格类型检查,远没有覆盖主运行时链路
|
||||
- `vitest.config.ts:8-10` 把 `customWorldPresentation` 映射到 stub,`vitest.config.ts:20` 还排除了真实存在的 `src/services/ai.test.ts`
|
||||
- 当前 `src/` 下共有 161 个文件,测试文件共有 11 个,但默认套件只执行其中 10 个
|
||||
- `npm run build` 已经能暴露 `src/services/customWorldPresentation.ts:163-169` 的 duplicate key 警告,但这块文件同时被 ESLint ignore、被 Vitest stub 掉,说明真实风险没有被完整看见
|
||||
|
||||
### 影响
|
||||
|
||||
- 工程信号不一致:`test` 绿、`build` 过,不代表关键模块真的健康
|
||||
- 复杂模块越是难测,越容易被长期豁免,最后演变成“最关键的地方最不受控”
|
||||
- 后续重构会缺乏可靠的回归保护,review 只能更多依赖人工记忆
|
||||
|
||||
### 建议
|
||||
|
||||
- 先缩小 `.eslintrc.cjs` 的 ignore 范围,优先把 `useStoryGeneration.ts`、`customWorldPresentation.ts`、`PresetEditorPanels.tsx` 拉回 lint
|
||||
- 把 `src/services/ai.test.ts` 重新纳入默认测试套件,除非有明确且短期的阻塞原因
|
||||
- 不要长期依赖 `tsconfig.typecheck-guardrails.json` 的 allowlist,至少把 `src/hooks/`、`src/services/`、`src/components/game-shell/` 逐步纳入 strict 范围
|
||||
- 对 build warning 建立明确策略:要么在 CI 中失败,要么把 warning 收敛到零
|
||||
|
||||
## P0:当前工作区不在真正的绿色基线
|
||||
|
||||
### 现状
|
||||
|
||||
当前代码不是“纯优化空间”问题,而是已经存在直接可见的门禁破口。
|
||||
|
||||
### 证据
|
||||
|
||||
- `package.json:11-15` 把 `lint:eslint` 和 `typecheck` 定义成正式脚本,说明它们本来就属于项目基线
|
||||
- 实际执行 `npm run lint:eslint` 时,`src/components/ItemCatalogEditor.tsx:167` 报出未使用变量错误
|
||||
- `src/components/ItemCatalogEditor.tsx:167` 引入了 `useTransition()` 返回值,但当前组件没有消费它
|
||||
- `npm run build` 虽然成功,但 `src/services/customWorldPresentation.ts:163-169` 仍然有重复 object key 警告
|
||||
|
||||
### 影响
|
||||
|
||||
- 团队会越来越难区分“可接受的技术债”和“已经破坏基线的问题”
|
||||
- 继续叠加功能会把问题扩散到更多文件,后面补起来成本更高
|
||||
|
||||
### 建议
|
||||
|
||||
- 先恢复工作区绿色基线,再继续推进大功能
|
||||
- 把“lint 零错误、build 零 warning”作为下一轮工程整理的硬目标
|
||||
|
||||
## P1:运行时主链路仍然被少数超级模块吸住
|
||||
|
||||
### 现状
|
||||
|
||||
入口已经变薄,但主复杂度仍集中在少数大文件里,尤其是故事推进、战斗同步和界面编排三层。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts` 当前约 2210 行
|
||||
- `src/hooks/useStoryGeneration.ts:694` 导出主 hook,`src/hooks/useStoryGeneration.ts:1416` 接入 `useTreasureFlow`,后面还继续承接 NPC 互动、库存、打字机、AI、历史推进和故事回写
|
||||
- `src/hooks/useCombatFlow.ts:134` 是主战斗 hook,`src/hooks/useCombatFlow.ts:796-832` 仍然负责逃跑流程与 story sync 的耦合
|
||||
- `src/components/GameShell.tsx` 当前约 791 行,`src/components/GameShell.tsx:260-269` 管理一组本地 UI 状态,`src/components/GameShell.tsx:482` 继续处理场景切换时的选择编排
|
||||
- 构建产物里 `dist/assets/App-*.js` 约 389 kB,`dist/assets/index-*.js` 约 198 kB,说明主运行时 chunk 仍然偏重
|
||||
|
||||
### 影响
|
||||
|
||||
- 任何一个功能变化都容易跨 story、combat、transition、panel 几条链一起改
|
||||
- hook 单测越来越难写,因为副作用、异步和 UI 编排仍然混在一起
|
||||
- App 主 chunk 偏重,会继续拖累首屏和回归速度
|
||||
|
||||
### 建议
|
||||
|
||||
- 继续把 `useStoryGeneration` 收敛成 orchestration 层,把 treasure、NPC、inventory、chat、typewriter、AI 回写拆成更纯的领域 action
|
||||
- 让 `useCombatFlow` 更明确地区分“战斗结算”和“播放同步”
|
||||
- 把 `GameShell` 进一步下沉为 scene transition、selection flow、overlay panel 三类 view-model
|
||||
|
||||
## P1:编辑器共享层只迁移了一半,重复基础设施还在
|
||||
|
||||
### 现状
|
||||
|
||||
编辑器入口已经做了 shell 化,但真正的复杂度仍然堆在大型面板组件里,而且共享层没有吃干净。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/components/PresetEditor.tsx:41` 的入口已经很薄,说明方向是对的
|
||||
- 但 `src/components/preset-editor/PresetEditorPanels.tsx` 仍然约 2163 行
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:55` 仍然自带 `cloneValue`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:117` 仍然自带 `saveJsonObject`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:189` 仍然自带 `SectionCard`
|
||||
- 与之对应,`src/editor/shared/jsonClient.ts:29-40` 已经提供了共享版 `fetchJson` / `saveJsonObject`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:364`、`:1128`、`:1500`、`:1806` 仍然把四个大型 panel 放在同一个文件里
|
||||
|
||||
### 影响
|
||||
|
||||
- 编辑器的保存、错误处理、基础 UI 容器会继续多处复制,后续很难统一行为
|
||||
- 迁移看起来开始了,但没有真正收尾,维护者仍然需要在“大文件 + 共享层”之间来回切换
|
||||
|
||||
### 建议
|
||||
|
||||
- 继续把 `PresetEditorPanels.tsx` 拆成按 tab 或按领域分文件
|
||||
- 统一复用 `src/editor/shared/` 下的保存客户端、基础容器、表单片段
|
||||
- 对编辑器做一次“小型迁移收尾”,目标是消灭重复的基础 helper
|
||||
|
||||
## P1:本地开发 API 层与构建工具耦合过深
|
||||
|
||||
### 现状
|
||||
|
||||
本地 API 插件已经把很多临时逻辑吸收进项目内部,这是好事;但它现在承担的职责太多,且全部挂在 Vite 插件层。
|
||||
|
||||
### 证据
|
||||
|
||||
- `vite.config.ts:7-18` 直接把 `createLocalApiPlugins(__dirname, env)` 注入到 Vite config
|
||||
- `scripts/dev-server/localApiPlugins.ts` 当前约 394 行
|
||||
- `scripts/dev-server/localApiPlugins.ts:150` 定义 LLM proxy 插件
|
||||
- `scripts/dev-server/localApiPlugins.ts:216` 定义通用 JSON 文件编辑插件
|
||||
- `scripts/dev-server/localApiPlugins.ts:265` 直接把编辑器保存结果写回 `src/data/*.json`
|
||||
- `scripts/dev-server/localApiPlugins.ts:429` 再统一把所有插件拼到一起
|
||||
|
||||
### 影响
|
||||
|
||||
- dev server、preview server、编辑器持久化和 LLM 代理被绑在一个文件里,测试与替换成本都偏高
|
||||
- 随着编辑器继续扩张,这个文件会继续演化成“隐形后端”
|
||||
- 生产与开发环境的边界容易模糊,问题排查也更依赖熟悉 Vite 插件机制的人
|
||||
|
||||
### 建议
|
||||
|
||||
- 至少先按职责把 `localApiPlugins.ts` 拆成 `llm-proxy`、`json-editor-api`、`asset-catalog` 三块
|
||||
- 下一阶段可以考虑把编辑器 API 抽成独立本地服务层,而不是继续塞在 Vite 插件里
|
||||
- 给 JSON 写入接口补 schema 校验,而不只是“是 object 就写入”
|
||||
|
||||
## P2:构建体积仍有继续优化空间
|
||||
|
||||
### 现状
|
||||
|
||||
路由 lazy load 和部分 modal lazy load 已经做了,但主游戏运行时包仍然偏大。
|
||||
|
||||
### 证据
|
||||
|
||||
- `dist/assets/App-*.js` 约 389 kB
|
||||
- `dist/assets/index-*.js` 约 198 kB
|
||||
- `dist/assets/index-*.css` 约 117 kB
|
||||
- `src/components/GameShell.tsx`、`src/hooks/useStoryGeneration.ts`、`src/services/prompt.ts` 仍然是较大的主链路文件
|
||||
|
||||
### 影响
|
||||
|
||||
- 新人本地启动、构建和回归阅读成本仍然偏高
|
||||
- 主运行时模块越重,越不利于后续继续做场景扩展和编辑器共存
|
||||
|
||||
### 建议
|
||||
|
||||
- 优先沿着“运行时 orchestration 拆分”去减主 chunk,而不是单纯追求更多 lazy import
|
||||
- 对 `prompt`、自定义世界、编辑器预览等非首屏关键代码继续做边界拆分
|
||||
- 每轮重构后用真实构建产物复测,而不是只凭代码体感判断
|
||||
|
||||
## 建议的落地顺序
|
||||
|
||||
1. 先恢复绿色基线:修掉 `ItemCatalogEditor` lint 错误,处理 `customWorldPresentation` 的 duplicate key warning
|
||||
2. 再补齐门禁:缩小 ESLint ignore、把 `ai.test.ts` 拉回默认测试、扩大 strict typecheck 覆盖
|
||||
3. 然后拆主链:优先处理 `useStoryGeneration`、`useCombatFlow`、`GameShell`
|
||||
4. 再做编辑器迁移收尾:拆 `PresetEditorPanels.tsx`,统一共享层
|
||||
5. 最后处理 dev API 分层和 bundle 体积
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这个仓库已经从“功能堆叠期”进入“工程收尾期”了。当前最值得做的不是再加一层玩法,而是把门禁补齐、把超级模块拆开、把半迁移状态收尾;只要这一步做稳,后续继续扩展剧情、编辑器和自定义世界的成本都会明显下降。
|
||||
@@ -0,0 +1,173 @@
|
||||
# 怪物-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、`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 字段。 |
|
||||
|
||||
## 本轮最优先的删除顺序
|
||||
|
||||
建议后续真正改代码时,按下面顺序删并,风险最低:
|
||||
|
||||
1. 先删字段入口:`monsterIds / sceneHostileNpcs / hostileNpcPresetId`
|
||||
2. 再删运行时双轨:`src/data/monsters.ts`、`src/data/hostileNpcs.ts`、`src/data/sceneEncounterPreviews.ts`
|
||||
3. 再删战斗双轨:`battlePlan.ts`、`playback.ts` 里的 `monster` actor 与 `sceneMonsters`
|
||||
4. 再删画布双轨:`GameCanvasRuntime.tsx`、`GameCanvasEntityLayer.tsx`、`GameCanvasShared.tsx`
|
||||
5. 最后清编辑器和提示词:`MonsterPresetPanel.tsx`、`MonsterPresetTab.tsx`、`prompt.ts`、`questDirector.ts`
|
||||
|
||||
## 改造后的目标形态
|
||||
|
||||
统一后应只剩下这一套语义:
|
||||
|
||||
- 场景中所有可见角色都放在 `npcs`
|
||||
- 怪物 = `initialAffinity < 0` 或 `hostile = 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 再跑”的中间态。
|
||||
19
docs/audits/engineering/README.md
Normal file
19
docs/audits/engineering/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 工程优化审查总览
|
||||
|
||||
这一组是同主题的连续审查记录,建议不要把它们当作三份彼此独立的文档来看。
|
||||
|
||||
## 当前推荐入口
|
||||
|
||||
1. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md)
|
||||
这一版最适合作为当前工程基线,重点从“是否真正绿色”“门禁有没有覆盖真实风险”来判断仓库状态。
|
||||
2. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md)
|
||||
适合回看运行时主链路、Story/Combat 边界、分层过渡期问题。
|
||||
3. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md)
|
||||
适合看第一轮系统性工程扫描,了解最早的问题基线。
|
||||
|
||||
## 融合结论
|
||||
|
||||
- 三轮结论是一致收敛的:问题不在“有没有开始工程化”,而在“工程化是否真正覆盖了最危险的主链路”。
|
||||
- 最新一轮已经把关注点集中到质量门禁、真实绿色基线、关键模块豁免和 build warning 上。
|
||||
- 如果只是为了判断现在先做什么,直接从 `2026-04-01` 开始即可。
|
||||
- 如果是要做长期重构方案,再按 `2026-03-29 -> 2026-03-30 -> 2026-04-01` 的顺序回看演进。
|
||||
Reference in New Issue
Block a user