# AGENTS.md 仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。 当前版本 **0.1.41**(《天命·兑现》:天命 flag 全量接线 15+ 项即时兑现 + NPC 演化种子派生零主 rng 消耗 + pushNews/purgeNpc 确定性修复 + 族史卷叙事层 chronicle-scroll.ts + 时效 flag 自动清理 + UI 建筑专精加成预览 + LegacyPanel 族史卷视图)。 ## 命令 ```bash npm test # vitest 引擎测试(tests/*.test.ts,全部纯 Node,无 DOM/DB 依赖) npx vitest run tests/world.test.ts # 单文件测试 npm run typecheck # tsc --noEmit(npm 会打 warn 噪音,用 npx tsc --noEmit 更干净) npm run dev # electron-vite 热更新(开发期 origin 为 http://localhost:5173) npm run build # 产物到 out/ npm run package # build + electron-builder(Linux 打 win 包可出产物,但未签名,Windows 上可能被 SmartScreen 静默拦截;正规发布在 Windows 侧重跑此命令) ``` 端到端冒烟(渲染 + OPFS 数据库 + 开局推进 + 可选截图): ```bash npm run build SMOKE_TEST=1 npx electron out/main/index.js --no-sandbox --disable-gpu # 退出码 0 = 全通 SMOKE_TEST=1 SMOKE_SHOTS_DIR=/tmp/opencode/shots npx electron ... # 附带 UI 截图 ``` 冒烟会写真实用户数据目录(Linux 下 `~/.config/Electron`),跑完删掉该目录以免脏数据。 无显示环境(WSLg 掉线)会打印 `[SMOKE-TIMEOUT]` 退出码 2 而非卡死;GUI 冒烟必须在 X11/WSLg 在线时跑(引擎与存储逻辑的回归请靠 vitest,不要依赖 GUI)。另注意:`npx electron` 可能拉取**新版** electron 缓存版(与本仓库 33.x 不同),二进制安装以 `node_modules/electron` 为准。 ## 环境坑 - npm 12 拦截 postinstall:首次安装后须 `npm install-scripts approve electron esbuild`,否则 Electron 二进制缺失。 - **`npm rebuild electron esbuild` 会删掉 `node_modules/electron/dist/electron`**(Linux 二进制),修复: `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node node_modules/electron/install.js` - 私有 registry 的 `_auth` token 只在用户级 `~/.npmrc`;项目 `.npmrc` 只保留 registry 映射,不要把 token 写进仓库。 - **打包版≠开发版(CSP 分野)**:dev(vite server 无 CSP)能跑不代表打包版能跑——CSP 性 bug(如 0.1.36 unsafe-eval)只出现在 out/;改完务必 build +(可能的话)真实 GUI 冒烟。 - **WSL 无显示环境调试**:`npm run dev:web`(vite.web.config.ts,端口 5179 + host 0.0.0.0)纯浏览器通道——Windows 浏览器开 `http://:5179`;window.api(导出/导入/MOD 扫描)缺失自动跳过,其余全功能等价(存档走浏览器 OPFS)。**注意**:部分浏览器(如 TabbitDance 后台 node)会劫持 localhost 端口段(5173/5179 全 502)——用 WSL 网卡 IP 绕过。 - Linux 无 emoji 字体:**所有图标一律用汉字印章字符**(renderer/ui/styles.css 的 `.s-icon/.res-icon/.bld-icon`),不要引入 emoji。 ## 架构(0.1.14 大重构后,目录与路径以此为准) ``` src/renderer/game/ 纯 TS 引擎(无 React/DOM,vitest 直测) engine/GameEngine.ts 唯一门面:advance/restore/engineFromSnapshot/about/resolvePending engine/kernel/ Kernel.ts(三合一:Clock+Rng+BUS 转发)+ clock/rng/plugin/names/format/guide/urgency/timesense engine/runtime/ World.ts(大对象+normalize+bus 转发)/clocks.ts(时轮注册)/ApiFacade.ts(GameFacade) Systems/(production/aging/cultivation/missions/events/diplomacy/marriage/tribulation/tournament/lifecycle/combat) capabilities.ts(12 能力卡)/pluginManager.ts/plugin-bootstrap.ts/modSchema.ts(MOD 范式)/modManager.ts(MOD→插件)/creation.ts/pcgen.ts engine/sim/ 世界自进化: WorldSim.ts / worldsim-data.ts(数值与常量唯一权威)/worldgen.ts(世界种子生成器)/Market.ts / worldsim-brief.ts engine/narrative/ 族谱/传记/定鼎/年轴/百年报告/族史卷(chronicle-scroll) data/ balance 表 + DataPackRegistry(registry.ts pack() 单源;PILL_RECIPES/FORGE_RECIPES/ERA_CONF 等) storage/ db(MetonaSqlark 单例)/slots(槽+快照+exportEnvelope)/migrate(CURRENT_SCHEMA=2,即 normalize 即迁移) types/domain.ts 全量领域类型(改状态结构先看它) src/renderer/ui/ React + zustand(store.ts 主状态;面板按 panel switch 挂载;订阅 revision 刷新) src/main/ Electron 主进程(app:// 协议 + IPC 存档导出/导入) ``` ## 引擎红线(0.1.14+) - **统一时轮**:月度 phase(production/aging/cultivation/missions/events/diplomacy/epilogue)+ 年首钩子均注册于 `runtime/clocks.ts`。**新增系统 = 注册一行,禁止手改 `advanceMonth` 本体**(World.ts:329)。 - **统一随机**:引擎只经 `World.rng`;UI 播种用 `RngHub.rollSeed()`、音效白噪用 `RngHub.audioNoise01()`;**不要**在逻辑里引入 Math.random()(引擎域 0 处)。渲染层 canvas 粒子散布的 Math.random 不进引擎状态,可留。 - **引擎单线程定论**(0.1.21 实测):`advanceMonth` 2160 月平均 **0.05ms/tick**——无 Worker 化必要;拖帧热点在 UI 渲染与存档(都已在 0.1.20/0.1.21 修)。 - **事件闸**:pending 事件唯一写入入口是 `events.ts` 的 `fire(w, id, priority)`(0=普通遇占放弃 / 1=高优 raid·渡劫·大比可顶替被占者,被顶替者入 `eventQueue` 下月兑现不丢失)。**禁止直写 `state.pendingEvent`**(渡劫/破境丹均已改走 fire)。`applyEventChoice` 前置校验 `pendingEvent===id`(陈旧 Modal 二次结算防线)。 - **禁 eval/字符串求值(0.1.36 CSP 红线)**:打包 renderer 的 CSP 禁 unsafe-eval——**严禁 `Function()`/`eval()` 求值表达式**(业务字符串一律走 `data/arith.ts` 纯解析器:数字/L/四则/括号/幂/负号白名单,非法 NaN 不抛;bonusOf/produceExpr/MOD 表达式已全量替换)。开发模式无 CSP 常掩盖此问题——凡是新表达式求值必须用 arith。 - **特效/音效语义出口**:`WorldEventBus.onFx?(em: FxEmit)` + `World.emitFx(kind, source?)`(零 rng 消耗);UI 的 `makeBus` 桥接 FxGate;`onLog kind` 音效映射保留作兜底。 - **插件架构**(0.1.8 起,路径已迁 engine/runtime|kernel):`kernel/plugin.ts` 协议、`runtime/pluginManager.ts` 安装管线(依赖校验/异常回滚/追踪型上下文——注销时钩子全摘)、`plugin-bootstrap.ts` 三核心插件(core-systems/core-data/core-events protected)。事件多池 `world.eventPools`;**findEvent 必须带 world 参数查池**。能力卡 12 张经 `viaCap` 注册;tournament/tribulation/apprentice/season 四卡各自内部检查 `w.sysEnabled`(无独立钩子)。 - **MOD 红线(0.1.26 规范)**:MOD 只含 JSON 零脚本(`docs/MOD_GUIDE.md`);**禁止新增 rng 消耗**(全局随机序列);不改核心时序(扩展走五口:事件池/模板/词库/灾因/配方);`modPreflight`+`validateMod` 先行(id 冲突用 `findEvent` 权威检测);MOD 只影响**新开档**世界(老档世界变形成档记录);MOD 状态随 `state.plugins` 持久化(对应 `worldGen.npcs` 落档保证读档永稳)。 - **门面**:`runtime/ApiFacade.ts` GameFacade(act 24 项/query 6 类/subscribe 退订协议/about);**UI 面板事实走 world 直调**(act 为测试/自动化通道),`actDirect` 与 ACT_CATALOG 语义对齐(0.1.20 补齐全 24 项);store 的 `facade` 必须在 openState/startNewGame 赋值。 - **金钟罩(双基线)**:`tests/clock.test.ts` 的 `GOLDEN`(直调冻结世界)+ `GOLDEN_RESOLVED`(自动 resolve 长跑"现实"世界),各 3 seed × 三档月数(560/1200/2160)共 18 值。当前为 **0.1.41 基线**(0.1.25/0.1.26/0.1.31 零漂;0.1.27/0.1.28/0.1.29/0.1.30/0.1.32/0.1.33 受控变更重算;0.1.39 purgeNpc 清理 worldGen.npcs/relations 僵尸定义——bell-seed-2 三档受控漂移;0.1.40 天命系统+NPC演化增强+建筑专精——金钟罩重算;0.1.41 NPC演化种子派生+pushNews确定性修复+天命flag全量接线——金钟罩重算)。指纹 `tests/fingerprint.helper.ts` 含全世界轴(era/stance/prosperity/relationsWithOthers/calamity/pendingEvent/eventQueue/worldAnnals/**worldGen 摘要/sagaAnnals/distress/declineYears 汇总**——0.1.27 生灭/世鉴路径真实锁定)。**有意变更时序/数值时**:指纹一并重算并在注释注明原因。 ## 世界自演进模型(0.1.14 sim/ 层,改数值勿动流程) - **市场实体化**:`marketPool` 是真库存——世界月供给(`worldSupplyRate` × 潮汐span × era.supplyMult)vs 常驻需求(`worldDemandRate`);NPC 按 `def.sells/buys` 逐月入池出池(姿态/区域灵势/灾年调制);玩家买卖经 `WorldSim.tradeSettle` 回写(买浅价涨/卖盈价跌);池深 < `tradeFloorPct(0.35)` 断供拒单(与 `poolFloorPct` 单常量一致)。`driftMarket` rebalance=0.025 弱锚定(让供需真实撬动价格,勿调回 0.1+)。 - **世纪弧 era**:盛世/平世/乱世/末法,`ERA_CONF`(灾年概率/供需/拍卖/tideBias)+ `ERA_DURA`(续航窗口);flow 权重和 <1(留白=延续档);**转移权重受世界温度调制**(`worldTemperature()` + `tempEraK`)——时代由天下兴衰孕育、又重塑天下。 - **NPC 定力**:`npcDyn.prosperity/stance/relationsWithOthers`——姿态(守成/扩张/隐忍/结盟)年首重估,调制 raid/互攻/贸量/外交;互攻按 power 加权 + 打残广播;玩家劫掠/结盟写关系网(蝴蝶效应)。 - **灾年**:持续 6 月(`calamityLeft`),减产乘子(CALAMITY_FAMILY)+ 疫病/兽潮体感 + NPC 贸易 ×0.7 + 群雄相噬 ×1.5;世界侧 `CALAMITY_EFFECT` 冲击池。 - **灾年** 补充:灾因注册表已聚合(`calamityNames/calamityFamilyOf/calamityEffectOf` + `addCalamity`——默认表不变,MOD/插件可注入灾因全链生效)。 - **家族生灭**:`npcDyn.declineYears`(power<58 且 景气<38 连续 8 年)→ 覆灭(最强邻分食+史官广播);家族数 < 开局目标(4~8)年首新贵补位(`greatNewbornChance`,乱世×2)。 - **世界种子**(0.1.24):`sim/worldgen.ts` `generateWorld(seed)`——NPC 4~8 家随机/初始关系网(1-2 世仇+1 盟友)/开局 era/市场 ±15% 偏移;**独立派生 rng**(`seed::worldgen`,World.rng 主序列零消耗);`state.worldGen` 摘要落档(含 npcs defs——读档幂等重注册,存档世界永稳)。 - **天下史表**:`worldSim.worldAnnals[]`(十年一鉴 80 页留档,免 newsFeed 裁剪丢失)。 - **物品六类生态纪律**(0.1.32):resource/material/pill/talisman/brew/artifact——**新资源必须四同步**(ITEMS 定义 + POOL_BASE + MARKET_IDS + worldgen marketOffset)才入世界循环(呼吸/NPC 贸易/灾年超卖);消耗类物品效果(符箓/灵酿)**零 rng 消耗**(自动消耗实现——金钟罩安全)。 - **生态闭环纪律**(0.1.33):丹方/铸器产出物**必须先在 ITEMS 立名**(幽灵物品不可见不可卖——A2 教训);灾年减产因子必须真正乘入生产(compute-and-drop 是 A3 教训);符箓坊为"消耗式"建筑(扣料产符——零 rng),新增消耗式生产勿用 produceTable 泛化(泛化只做纯产出)。 ## MetonaSqlark(@metona-team/metona-sqlark 0.7.4)陷阱 - SQL 方言为自有实现(不是 SQLite):建表列类型写 `string|number|boolean|date|json`,**不是** `INTEGER/TEXT`;不支持 `INSERT OR REPLACE`、`ON CONFLICT`、`rowid`——用「SELECT 判断 → UPDATE/INSERT」或直接 `DELETE+INSERT`。 - aria 引擎同库一个 tab 只能建一次连接(Web Locks → `ARIA_LOCKED`):存取必须走 `game/storage/db.ts` 的单例 `getSaveSlot(slot)` / `getSlotManager()`,**禁止**每次 `new SaveSlot/new SlotManager`。 - 数据在浏览器 OPFS(renderer 内),main 进程无 DB 逻辑;开发与打包后的 origin 不同,两环境存档不互通。 - 存储层通过 `SaveDbDriver` 抽象(`storage/slots.ts`),node 环境下无 OPFS,测试只覆盖引擎不覆盖 DB。 - 存档防线(0.1.35 起):**迁移链已整体铲除**——读档走 `validateLoadedState` 结构校验(seed/family/members 断言)+ World 构造器 normalize 防护(仅保留:npcs 重注册(世界永稳)/headId 修复/distress 防脏/era 合法校验);新字段初始职责归 `makeWorldSimState`(initSim 单源——改字段同步 initSim);无 schemaVersion 版本戳(SaveMeta.version 记游戏版本字符串展示用)。 ## 数值基调(勿当 bug 调回去) - **pacing.ts 的 MAJOR_RATE 是进度唯一生效因子**(qi 2.2、foundation 1.4、core 0.95、nascent 0.68、spirit 0.48);`realms.ts` 的 `expBase/expGrowth/maxRealmExp` 曲线**未参与计算**(历史摆设,勿以它反推)。 - 月率 base = `1.0 + perception*0.18`;65 岁起 ×0.72 衰减、8 岁以下 ×0.65(圣者护族 60+ 高境界者存在时乘 1.3)。 - **0.1.16 修为经济化**:练气以上修行者月耗灵草(闭关2/普通1),无草速修 ×0.6~1.0 衰减——灵草是第一战略资源。 - 寿元:75/150/220/340/520/850(凡人→化神)。 - 大境界晋升走**渡劫事件三选**(硬渡/护法/压制,12 月不应自动压制);破境丹在需渡劫时药力转 `tribBoost`(+12%) 入渡劫,**不可跳过**。 - 周期事件优先级:命运(百年/飞升)> 大比 > 传薪(年锚 flag)> 拍卖(era 概率调制)> 岁祷 > 回声。 - `family.flag` 年份键(auction-/prayerDone-/echoDone-/recruitDone-/legacyDone-)每 tick 剪 3 年前旧键。 ## 修改纪律(防覆盖——血泪教训,务必遵守) **背景**:曾多次出现「大改后我之前的修复被覆盖」:① 批量 python 替换命中旧文本、② `git checkout` 回滚临时参数时误恢复了已修复文件、③ 编辑器链式 replace 生成重复属性/孤行。均靠 tsc/vitest 事后兜住,但应防于未然。 **硬性规则**: 1. **改动前**:`git status` 确认工作区干净(或先提交当前成果,原子提交,一功一提交)。 2. **改动中**:批量替换一律用「先 assert 旧文本存在 → 替换 → 再全局 grep 断言新文本唯一」三步;禁止无检查的 sed/python 盲替。**写盘脚本严禁惰性路径**(0.1.32 教训:`open('p','w')` 撇笔把模块源码碎片写进根目录并被 git add 入库)——写文件必须用**显式路径变量**且写后立即 `git status` 检查工作区异常新文件。 3. **禁止 `git checkout -- ` / 整文件回滚**(这是历史事故元凶)。确需回滚时:`git show HEAD: > /tmp/bak` 手动 diff 恢复,恢复后跑全量测试确认无伤。 4. **优先小步快跑**:每个功能块改完立即 `npm test`(引擎)或 `tsc`(UI 类型),绿了再动下一处——绝不在错误状态上加新修改。 5. **高危文件白名单**(world.ts / events.ts / store.ts / styles.css / cultivation.ts / **WorldSim.ts / worldsim-data.ts**)改动后必须 `git diff --stat` 核对改动面与预期一致。 6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。 - 提交前:`npm test`(当前 108 套件 7058 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。 - 平衡数值集中在 `game/data/` 与 `engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。 - 发布产物 `release/`、`out/` 均已 gitignore,勿入库。