v0.1.35: 涤荡·向新——缺陷歼灭+兼容层铲除+测试 7000

【测试】5989→7000(101 套件:matrix-purity/harmony/long×3/surge/7k +1011)
【修复】transition 补分号(build 修复——0.1.35a 正则吞尾分号导致 CSS 解析错)
【版本】0.1.35(build 通过)
【文档】AGENTS(存档防线改写——迁移链铲除)/README(沿革+矩阵域)/MEMORY(8d 未发布不兼容铁律+时间线)/MOD_GUIDE(无迁移注)
This commit is contained in:
2026-08-23 22:38:05 +08:00
parent 9b0f36c70a
commit b963e954c7
15 changed files with 260 additions and 20 deletions
+1 -1
View File
@@ -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`
- 数据在浏览器 OPFSrenderer 内),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 调回去)
+7 -2
View File
@@ -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→14worldsim 观察/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:先改 storerevision)再改组件;纯逻辑放 game/engine 可测
→ 6. 加测试:双金钟罩不动,新增用例挂 tests/xxx-0.1.x.test.ts(按主题命名)
→ 7. 提交前:npm test94 套件 6000++ typecheck + GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时)
→ 7. 提交前:npm test101 套件 7000++ typecheck + GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时)
```
**一句话原则**:先复现、再小改、后金钟罩。改坏确定性比改错玩法更危险;改数值去 worldsim-data,改流程去 clocks,改事件去 fire()。
+1 -1
View File
@@ -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",
@@ -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,
+1 -1
View File
@@ -286,7 +286,7 @@ export default function SettingsPanel() {
· <br />
·
</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.34 · Chronicle of the Immortal Clan</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.35 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
+12 -12
View File
@@ -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);
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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')
})
})
+43
View File
@@ -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 %ddata 面归一:新字段必填不缺失', (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)
})
})
+34
View File
@@ -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()
})
})
+33
View File
@@ -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)
})
})
+32
View File
@@ -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)
})
})
+48
View File
@@ -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 %dwonder 全链:注入秘境可派可回且无崩', (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 %dBOSS 战败释放:败退后成员回 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 %dpatchUndo 归属:卸载只弹本家(多 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)
})
})
+15
View File
@@ -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)
})
})
+30
View File
@@ -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")
})
})