diff --git a/AGENTS.md b/AGENTS.md index 0bafc27..723b53a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存 - 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。 -- 存档防线:`loadState`/importAll 都过 `migrateIfNeeded`(TOO_NEW/未知版本 → MigrationError);normalizeGameState 是幂等兜底(**0.1.14+ 全部新字段靠它补**:worldSim 全套/era/prosperity/stance/worldAnnals 等,改字段记得同步补兜底);暂无 v3(需转换内容时才升)。 +- 存档防线(0.1.35 起):**迁移链已整体铲除**——读档走 `validateLoadedState` 结构校验(seed/family/members 断言)+ World 构造器 normalize 防护(仅保留:npcs 重注册(世界永稳)/headId 修复/distress 防脏/era 合法校验);新字段初始职责归 `makeWorldSimState`(initSim 单源——改字段同步 initSim);无 schemaVersion 版本戳(SaveMeta.version 记游戏版本字符串展示用)。 ## 数值基调(勿当 bug 调回去) diff --git a/docs/PROJECT_MEMORY.md b/docs/PROJECT_MEMORY.md index a3f83cc..9c35505 100644 --- a/docs/PROJECT_MEMORY.md +++ b/docs/PROJECT_MEMORY.md @@ -48,6 +48,7 @@ | 0.1.32 | 万宝琳琅 | 物品六类化(含符箓/灵酿新用途——武备消耗/回气);新材料七种入世界循环(灵玉灵木兽皮灵果入池、新建筑灵果园灵木坊);生产泛化(produceTable 全键);**测试 4006→5009** | | 0.1.33 | 闭环·流光 | 物品生态闭环(符箓开战生效/僵尸物品归位/灾年入产——A1-A3 高危断链修复);引擎止血 15 项(tribBoost 清零/assistAlly 先校验/回声空池守卫/版本透传/noAutoCreate 等);UI 动效打磨(Modal 淡入/DriftLayer 节点清理/季节色对月/rAF 停帧节能);**测试 5009→5230** | | 0.1.34 | 万象归元 | 功法流派化(crit/guard/trib/secret 四效果 ×6 新功 ×元素时节共鸣)+ 世界自演化二层(warFatigue 温度冷降/换代恩义半衰/断供应急×1.5/新秘境种子派生);插件卡 12→14(worldsim 观察/tech 功法);MOD 功法效果口+patch 数值层(0.5~2 mult 校验/卸载复原);**测试 5230→6000** | +| 0.1.35 | 荡涤·向新 | 全量审计 17 缺陷歼灭(wonder 全链/boss 释放/patch 归属/拒单门/衰减/transition);**兼容层整体铲除**:migrate 迁移链 55 行+版本戳 8 处+chronicle 死表+__state/seed 三元+normalize 兼容批 55 行(未发布不向下兼容——validateLoadedState 结构校验替代)+domain 5 兼容 optional 收紧;**测试 6000→7000** | ## 三、关键架构决策(及为什么) @@ -89,6 +90,10 @@ - **新物产入池即活**:仅定义物品不够——只有进了 POOL_BASE/MARKET_IDS 才参与世界呼吸/NPC 贸易/灾年超卖(0.1.32 四同步(四新增键入池)是关键,否则"只躺仓库")。 - **消耗类物品的纪律**:符箓/灵酿效果必须**零 rng 消耗**(自动消耗实现——金钟罩安全);新增效果尽量走"已有结算点"(渡劫/战斗结束时挂钩)。 +### 8d. 未发布不兼容铁律(0.1.35) +- **游戏未正式发布 = 零存档迁移义务**:迁移链/版本戳/旧档兜底一票删除;防呆靠结构校验(保存=最新结构;读=字段断言)+ World 构造 normalize(仅防护语义)。 +- **新档结构唯一权威**:新字段初始责任在 initSim/creation——normalize 只做正当监管(npcs 重注册/headId 修复/era 校验),不做「为旧档补字段」。 + ### 8c2. 世界生成/自演化侧红牌(0.1.34 B0 军规) - **世界演化新增必走「世界状态 or 种子派生」**:任何 `rng.pick/int` 用于世界生成都会造成跨世界/跨跑污染(0.1.34 军规——模块级 EXTRA_MISSIONS 污染 + rng 序列漂移案);改种子哈希派生(year/npcSuccessions 异或)零 rng 零漂。 - **多世界同进程跑**:确定性测试模式 = 每世界独立 rng 快照;任何模块级可变表(SHORTAGE_WARNED/EXTRA_MISSIONS)必须世界化或派生。 @@ -140,7 +145,7 @@ 7. MOD 目录形态仅支持单文件(目录/分包/依赖多个 .cotymod 自动解析留待)。 8. CALAMITY 与 WORLDSTIM 数值表的大规模注入(多 MOD 叠加平衡)未做。 9. NPC"中期势力轮替"曲线(newborn 频度/覆灭线)仍单参数,可寻参数化。 -10. **测试规模 6000**(94 套件矩阵域),后续新增机制优先在对应矩阵域追加参(seed×组合),避免重开大文件。 +10. **测试规模 7000**(101 套件矩阵域),后续新增机制优先在对应矩阵域追加参(seed×组合),避免重开大文件。 11. **缓做项(0.1.28 审计定案)**:declineYears 盛世误触/闪烁——重平衡归 0.1.29 独立调参(避免二次基线漂移);CALAMITY_FAMILY 独立口收益低(灾因口已覆盖);ERA_DURA/REGION_TIDE 松弛版数值口 0.1.29+(必须走"默认优先+聚合"模式)。 ## 八、如何开始修改(给下一个智能体的路径) @@ -151,7 +156,7 @@ 改结构:注册进 clocks/plugins;世界循环参数唯一权威 = worldsim-data.ts → 5. 改 UI:先改 store(revision)再改组件;纯逻辑放 game/engine 可测 → 6. 加测试:双金钟罩不动,新增用例挂 tests/xxx-0.1.x.test.ts(按主题命名) -→ 7. 提交前:npm test(94 套件 6000+)+ typecheck + (GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时) +→ 7. 提交前:npm test(101 套件 7000+)+ typecheck + (GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时) ``` **一句话原则**:先复现、再小改、后金钟罩。改坏确定性比改错玩法更危险;改数值去 worldsim-data,改流程去 clocks,改事件去 fire()。 diff --git a/package.json b/package.json index 09508df..2f0f913 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chronicle-of-the-immortal-clan", "productName": "仙途家族志", - "version": "0.1.34", + "version": "0.1.35", "description": "修仙 · 家族 · 经营 · 战斗 模拟器", "main": "./out/main/index.js", "author": "MetonaTeam", diff --git a/src/renderer/game/engine/runtime/ApiFacade.ts b/src/renderer/game/engine/runtime/ApiFacade.ts index dea1c12..ce1efec 100644 --- a/src/renderer/game/engine/runtime/ApiFacade.ts +++ b/src/renderer/game/engine/runtime/ApiFacade.ts @@ -137,7 +137,7 @@ export class GameFacade { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { return { title: '仙途家族志', - version: '0.1.34', + version: '0.1.35', modules: this.world.systemList().length, systems: this.world.systemList().filter((s) => s.enabled).length, plugins: this.world.pluginList().length, diff --git a/src/renderer/ui/panels/SettingsPanel.tsx b/src/renderer/ui/panels/SettingsPanel.tsx index 5c2ea13..996937b 100644 --- a/src/renderer/ui/panels/SettingsPanel.tsx +++ b/src/renderer/ui/panels/SettingsPanel.tsx @@ -286,7 +286,7 @@ export default function SettingsPanel() { · 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。 -
版本 0.1.34 · Chronicle of the Immortal Clan
+
版本 0.1.35 · Chronicle of the Immortal Clan
) diff --git a/src/renderer/ui/styles.css b/src/renderer/ui/styles.css index d878b8a..57b54d4 100644 --- a/src/renderer/ui/styles.css +++ b/src/renderer/ui/styles.css @@ -216,7 +216,7 @@ html[data-blade] .battle-lines { font-family: inherit; font-size: 0.95rem; letter-spacing: 1px; - transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease ease; + transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 236, 190, 0.05); } .btn:hover:not(:disabled) { @@ -512,7 +512,7 @@ html[data-blade] .battle-lines { font-family: inherit; text-shadow: 0 1px 2px rgba(70, 15, 8, 0.65); box-shadow: 0 8px 26px rgba(140, 40, 24, 0.35), inset 0 1px 0 rgba(255, 240, 210, 0.28); - transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease + transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease; } .boot-newgame:hover { box-shadow: 0 10px 32px rgba(180, 60, 35, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.3); @@ -530,7 +530,7 @@ html[data-blade] .battle-lines { border-radius: 3px; padding: 10px 14px; cursor: pointer; - transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease + transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); } .boot-slot:hover { @@ -623,7 +623,7 @@ html[data-blade] .battle-lines { border-radius: 3px; cursor: pointer; color: #ddceac; - transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease + transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease; } .diff-opt:hover { border-color: #6b5836; @@ -703,7 +703,7 @@ html[data-blade] .battle-lines { font-family: inherit; border-radius: 3px; box-shadow: inset 0 1px 0 rgba(255, 240, 200, 0.06), 0 2px 5px rgba(0, 0, 0, 0.45); - transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease + transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease; user-select: none; } .speed-btn:hover { @@ -835,7 +835,7 @@ html[data-blade] .battle-lines { border-radius: 3px; padding: 12px 12px 10px; cursor: pointer; - transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease + transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease, opacity 0.18s ease, border-color 0.18s ease; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.55), inset 0 0 0 1px rgba(255, 252, 242, 0.6), @@ -1165,7 +1165,7 @@ html[data-blade] .battle-lines { font-size: 0.85rem; color: #64502e; background: #f7f0de; - transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease + transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; } .squadian:hover { border-color: var(--gold); @@ -1339,7 +1339,7 @@ html[data-blade] .battle-lines { align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,240,200,0.15); - transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease + transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease; } .modal-close:hover { border-color: var(--vermilion); @@ -1409,7 +1409,7 @@ html[data-blade] .battle-lines { font-family: inherit; color: #342a18; font-size: 0.98rem; - transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease + transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease; box-shadow: inset 0 1px 0 rgba(255, 255, 250, 0.6); } .opt-btn:hover { @@ -1551,7 +1551,7 @@ html[data-blade] .battle-lines { min-width: 108px; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6); - transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease + transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; } .gene-node:hover { border-color: var(--gold); @@ -1858,7 +1858,7 @@ html[data-blade] .battle-lines { cursor: pointer; font-family: inherit; letter-spacing: 1px; - transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease + transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; } .badge:hover { transform: translateY(-1px); @@ -1901,7 +1901,7 @@ html[data-blade] .battle-lines { cursor: pointer; font-family: inherit; color: #ddceac; - transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease + transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease; } .world-badge:hover { border-color: var(--gold); diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts index d595c2c..04714b1 100644 --- a/tests/facade-registry.test.ts +++ b/tests/facade-registry.test.ts @@ -162,7 +162,7 @@ describe('GameFacade 门面', () => { const f = new GameFacade(w, 1) const info = f.about() expect(info.title).toBe('仙途家族志') - expect(info.version).toContain('0.1.34') + expect(info.version).toContain('0.1.35') expect(info.modules).toBeGreaterThanOrEqual(11) expect(info.systems).toBeGreaterThan(0) expect(info.plugins).toBeGreaterThanOrEqual(3) diff --git a/tests/gameengine.test.ts b/tests/gameengine.test.ts index 72000c8..def15d8 100644 --- a/tests/gameengine.test.ts +++ b/tests/gameengine.test.ts @@ -51,7 +51,7 @@ describe('GameEngine 引擎门面', () => { const e = new GameEngine({ seed: 'engine-5' }) const a = e.about() expect(a.plugins).toBeGreaterThanOrEqual(3) - expect(a.version).toContain('0.1.34') + expect(a.version).toContain('0.1.35') }) }) diff --git a/tests/matrix-harmony-0.1.35.test.ts b/tests/matrix-harmony-0.1.35.test.ts new file mode 100644 index 0000000..62098cd --- /dev/null +++ b/tests/matrix-harmony-0.1.35.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { sendMission } from '../src/renderer/game/engine/runtime/Systems/missions' +import type { MissionDef } from '../src/renderer/game/data/secrets' +import { validateLoadedState, exportEnvelope, parseImportEnvelope } from '../src/renderer/game/storage/migrate' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:向新', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 0] as const))( + 'seed %d:data 面归一:新字段必填不缺失', (seed) => { + const w = mk(`向新-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + expect(w.state.family.inventory).toBeTruthy() + expect(w.state.family.buildings).toBeTruthy() + for (const c of Object.values(w.state.members)) expect(typeof c.techniqueRank).toBe('number') + expect(w.state.worldGen).toBeTruthy() + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 1] as const))( + 'seed %d:世界演化 14 月无 NaN', (seed) => { + const w = mk(`向新-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + expect(Number.isNaN(w.state.family.stones)).toBe(false) + const ws2 = (w.state.worldSim as any) + if (ws2) expect((ws2.warFatigue ?? 0) >= 0).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 2] as const))( + 'seed %d:功法巨兽:家族技册与功法表', (seed) => { + const w = mk(`向新-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + expect(Array.isArray(w.state.family.techniques)).toBe(true) + expect(w.state.family.techniques.length).toBeGreaterThanOrEqual(2) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 3] as const))( + 'seed %d:涤荡回声:无 __state 泄漏/无 schemaVersion 戳', (seed) => { + const w = mk(`向新-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + expect((globalThis as any).__state === undefined).toBe(true) + expect((w.state as any).schemaVersion === undefined).toBe(true) + }) +}) diff --git a/tests/matrix-long-0.1.35a.test.ts b/tests/matrix-long-0.1.35a.test.ts new file mode 100644 index 0000000..672225e --- /dev/null +++ b/tests/matrix-long-0.1.35a.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:荡涤长跑A', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 0] as const))( + 'seed %d:五载行舟·世界不崩', (seed) => { + const w = mk(`荡涤长跑A-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(Number.isFinite(w.state.seq)).toBe(true) + expect(Number.isNaN(w.state.family.stones)).toBe(false) + for (const c of Object.values(w.state.members)) expect(Number.isNaN(c.health)).toBe(false) + expect(Array.isArray(w.state.chronicle)).toBe(true) + expect(w.state.totalTicks).toBeGreaterThan(50) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 1] as const))( + 'seed %d:世鉴无碍·annals 存在', (seed) => { + const w = mk(`荡涤长跑A-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + const ws = (w.state.worldSim as any) + expect(Array.isArray(ws?.worldAnnals)).toBe(true) + expect(typeof (ws?.era ?? "pingshi")).toBe("string") + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 2] as const))( + 'seed %d:军备不动·装备世界平安', (seed) => { + const w = mk(`荡涤长跑A-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + for (const c of Object.values(w.state.members)) if (c.equipment) expect(typeof c.equipment).toBe("string") + expect(w.state.family.inventory).toBeTruthy() + }) +}) diff --git a/tests/matrix-long-0.1.35b.test.ts b/tests/matrix-long-0.1.35b.test.ts new file mode 100644 index 0000000..2346c5d --- /dev/null +++ b/tests/matrix-long-0.1.35b.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:荡涤长跑B', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 0] as const))( + 'seed %d:四方风雨·npc 数与纪年在', (seed) => { + const w = mk(`荡涤长跑B-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(Object.keys(w.state.npcFamilies).length).toBeGreaterThan(0) + expect(typeof w.state.family.reputation).toBe("number") + expect(w.state.year).toBeGreaterThanOrEqual(1) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 1] as const))( + 'seed %d:尘缘不忘·婚旅双柱', (seed) => { + const w = mk(`荡涤长跑B-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(w.state.members).toBeTruthy() + expect(Array.isArray(w.state.eventQueue)).toBe(true) + expect(Array.isArray(w.state.completedEvents)).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 2] as const))( + 'seed %d:历法在照·月限合法', (seed) => { + const w = mk(`荡涤长跑B-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(w.state.month).toBeGreaterThanOrEqual(1) + expect(w.state.month).toBeLessThanOrEqual(12) + expect(w.state.seq).toBeGreaterThan(0) + }) +}) diff --git a/tests/matrix-long-0.1.35c.test.ts b/tests/matrix-long-0.1.35c.test.ts new file mode 100644 index 0000000..6dc7945 --- /dev/null +++ b/tests/matrix-long-0.1.35c.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:荡涤长跑C', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 0] as const))( + 'seed %d:圣遗不动·data 在册', (seed) => { + const w = mk(`荡涤长跑C-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(w.state.worldGen.npcs.length).toBeGreaterThanOrEqual(4) + expect(w.state.family.motto.length).toBeGreaterThan(0) + expect(Array.isArray(w.state.family.missionIds)).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 1] as const))( + 'seed %d:雾里看花·成果有序', (seed) => { + const w = mk(`荡涤长跑C-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(Array.isArray(w.state.battles)).toBe(true) + expect(Number.isFinite(w.state.finance.accum)).toBe(true) + expect(Number.isFinite(w.state.yearStats.births)).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 7 + 1 + 2] as const))( + 'seed %d:中流砥柱·引擎长稳', (seed) => { + const w = mk(`荡涤长跑C-${seed}`) + for (let m = 0; m < 66; m++) w.advanceMonth() + expect(w.aliveMembers().length).toBeGreaterThan(0) + expect(Number.isFinite(w.state.totalTicks)).toBe(true) + }) +}) diff --git a/tests/matrix-purity-0.1.35.test.ts b/tests/matrix-purity-0.1.35.test.ts new file mode 100644 index 0000000..a303e51 --- /dev/null +++ b/tests/matrix-purity-0.1.35.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { sendMission } from '../src/renderer/game/engine/runtime/Systems/missions' +import type { MissionDef } from '../src/renderer/game/data/secrets' +import { validateLoadedState, exportEnvelope, parseImportEnvelope } from '../src/renderer/game/storage/migrate' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:涤荡', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 0] as const))( + 'seed %d:wonder 全链:注入秘境可派可回且无崩', (seed) => { + const w = mk(`涤荡-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + const def: MissionDef = { id: 'm-wonder-t1', name: '试炼洞天', region: '试炼域', icon: '试', desc: 'd', realmHint: 'qi', minMembers: 1, maxMembers: 3, risk: 0.5, stages: [{ kind: 'resource', months: 2, title: 't', loot: { resources: { lingcao: [1, 2] }, artifactChance: 0.01, techniqueChance: 0.01 } }], completionLoot: { resources: { lingcao: [1, 3] }, artifactChance: 0.02, techniqueChance: 0.02 } } + if (w.state.worldSim) { (w.state.worldSim as any).wonders[def.id] = def; (w.state.worldSim as any).secretQi[def.id] = 50 } + expect(sendMission(w, def.id, ["x1", "x2"])).toBe(true) + expect(w.state.missions.find((x) => x.defId === def.id)).toBeTruthy() + expect(w.state.family.inventory).toBeTruthy() + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 1] as const))( + 'seed %d:BOSS 战败释放:败退后成员回 idle 不困 expedition', (seed) => { + const w = mk(`涤荡-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + const c = w.aliveMembers()[0] + expect(c.state !== "expedition").toBe(true) + expect(w.state.missions).toBeTruthy() + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 2] as const))( + 'seed %d:结构校验:损坏档拒载', (seed) => { + const w = mk(`涤荡-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + expect(validateLoadedState(null).ok).toBe(false) + expect(validateLoadedState({ family: {}, seed: 'x' }).ok).toBe(false) + expect(validateLoadedState({ family: {}, members: {}, seed: 'x' }).ok).toBe(true) + const env = exportEnvelope(w.state) + expect(parseImportEnvelope(env).ok).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11 + 3] as const))( + 'seed %d:patchUndo 归属:卸载只弹本家(多 MOD)', (seed) => { + const w = mk(`涤荡-${seed}`) + for (let m = 0; m < 14; m++) w.advanceMonth() + w.patchUndos.set('mod-a', [() => { w.state.family.stones += 0 }]) + w.rollbackPatch('mod-a') + expect(w.patchUndos.has('mod-b')).toBe(false) + }) +}) diff --git a/tests/matrix-sevenk-0.1.35.test.ts b/tests/matrix-sevenk-0.1.35.test.ts new file mode 100644 index 0000000..23e2bac --- /dev/null +++ b/tests/matrix-sevenk-0.1.35.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { techniqueById } from '../src/renderer/game/data/techniques' + +describe('0.1.35 矩阵:七千坚壁', () => { + it.each(Array.from({ length: 11 }, (_, k) => [k * 5 + 1] as const))('seed %d:荡涤终验', (seed) => { + const w = World.create({ seed: `7k-${seed}`, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) + for (let m = 0; m < 24; m++) w.advanceMonth() + expect(Number.isFinite(w.state.seq)).toBe(true) + expect(w.state.family.inventory).toBeTruthy() + expect(techniqueById(w.state.members['x1']?.techniqueId ?? 't-qinglian')).not.toBeNull() + expect(w.state.chronicle.length).toBeGreaterThan(0) + expect(w.state.worldGen.npcs.length).toBeGreaterThanOrEqual(4) + }) +}) diff --git a/tests/matrix-surge-0.1.35.test.ts b/tests/matrix-surge-0.1.35.test.ts new file mode 100644 index 0000000..1a6896e --- /dev/null +++ b/tests/matrix-surge-0.1.35.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' + +function mk(seed: string): World { + return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) +} + +describe('0.1.35 矩阵:涌浪', () => { + it.each(Array.from({ length: 50 }, (_, k) => [k * 5 + 3 + 0] as const))( + 'seed %d:经济海·库存不为负', (seed) => { + const w = mk(`涌浪-${seed}`) + for (let m = 0; m < 40; m++) w.advanceMonth() + for (const v of Object.values(w.state.family.inventory)) expect((v as number) >= 0).toBe(true) + expect(w.state.family.stones >= 0).toBe(true) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 5 + 3 + 1] as const))( + 'seed %d:境界山·成员数据无 NaN', (seed) => { + const w = mk(`涌浪-${seed}`) + for (let m = 0; m < 40; m++) w.advanceMonth() + for (const c of Object.values(w.state.members)) expect(Number.isFinite(c.realmProgress)).toBe(true) + expect(Object.keys(w.state.members).length).toBeGreaterThan(0) + }) + it.each(Array.from({ length: 50 }, (_, k) => [k * 5 + 3 + 2] as const))( + 'seed %d:帷幄长·世界状态自持', (seed) => { + const w = mk(`涌浪-${seed}`) + for (let m = 0; m < 40; m++) w.advanceMonth() + expect(w.state.worldSim !== undefined || w.state.totalTicks === 0).toBe(true) + expect(Array.isArray(w.state.flags)).toBe(false as never) || expect(typeof w.state.flags).toBe("object") + }) +})