v0.1.30: 兼济——人口-世界成本闭环 + 世界关注度 + MOD 生态成型 + 测试 3006
【世界自演进】 - W1 人口-世界成本闭环:人口 >20 修行耗草额外抽池(人多粮少——家族规模有世界级代价) - W2 世界关注度(匹夫无罪):战力超邻家均值×2 → 劫掠×1.5/拍卖×1.15(强族举世瞩目) - W3 生灭激活实证:decline 150 后 3×180 月——1/3 世界经历覆灭/新贵(灾难级频度合理固化) 【MOD 生态成型】 - M1 内置 MOD 仓库:风物集(词库+瘴雨灾因 brief)/战备解军(灵纹刃配方+worldNum 覆写) ——设置页一键安装,官方范式模板 - M2 冲突矩阵 UI:PluginStatus conflicts 展示(依赖/冲突行内可视化) - M3 NewGame 预览:已装 MOD 集影响标注(本局世界生成) - M4 plugin-public 0.1.30 完整 API 清单 + 回滚契约 【测试 3000 攻坚】2014 → 3006(81 套件): matrix-continuity(22)/economy-new(26)/combat-rel(17)/mod-cycle(9)/npc-life(17)/ rule-world(50)/depth(120)/deep(204)/final-circle(292)/rising(210)/peak30(25)/audit-0.1.29(6)+ ——全部语义真实矩阵;金钟罩 0.1.30 基线(人口成本/关注度受控变更) 【验证】81 套件 3006 全绿;build 通过
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
||||
当前版本 **0.1.29**(《续命》:第四轮审计——续代断链修复(人口稳态/代际轮转/天年 200)/传薪年锚/敌强度相对化/权重防垄断/启用过滤/并发存档修复/存储债清理)。
|
||||
当前版本 **0.1.30**(《兼济》:人口-世界成本闭环 + 世界关注度 + 内置 MOD 仓库 + 测试 3006)。
|
||||
|
||||
## 命令
|
||||
|
||||
@@ -104,6 +104,6 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
|
||||
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`(当前 69 套件 2008 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
|
||||
- 提交前:`npm test`(当前 81 套件 3006 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
|
||||
- 平衡数值集中在 `game/data/` 与 `engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。
|
||||
- 发布产物 `release/`、`out/` 均已 gitignore,勿入库。
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chronicle-of-the-immortal-clan",
|
||||
"productName": "仙途家族志",
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.30",
|
||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "MetonaTeam",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 内置 MOD 仓库(0.1.30 M1):开箱即用的示范包——
|
||||
* 风物包(词库+灾因 brief)与战备包(配方+世界数值覆写)。
|
||||
* 均为纯 JSON 语义(modSchema 同构),安装即得官方范式模版。
|
||||
*/
|
||||
import { ModPack } from '../engine/runtime/modSchema'
|
||||
|
||||
export const BUILTIN_MODS: ModPack[] = [
|
||||
{
|
||||
id: 'fenwu-ji',
|
||||
name: '风物集',
|
||||
version: '1.0.0',
|
||||
schema: 1,
|
||||
author: '官方示例',
|
||||
description: '江南风物入世:新增词库(吴语/江畔/烟雨)与灾因「瘴雨」(灵田减产之灾)。',
|
||||
data: {
|
||||
wordgen: undefined as never,
|
||||
worldgen: { fams: ['吴', '江', '雨'], regions: ['江南烟雨畔', '吴淞江湾'], styles: ['风雅世家', '渔隐门'] },
|
||||
calamities: [{ name: '瘴雨', brief: '瘴雨连绵,田间霉蛀——灵植减产。', family: { lingcao: 0.75 }, effect: { lingcao: -0.2 } }]
|
||||
}
|
||||
} as ModPack,
|
||||
{
|
||||
id: 'zhanbei-xv',
|
||||
name: '战备解军',
|
||||
version: '1.0.0',
|
||||
schema: 1,
|
||||
author: '官方示例',
|
||||
description: '战备支线:新增灵纹刃配方(灵矿+兽核),世界供需微调(供 0.018 需 0.014)。',
|
||||
data: {
|
||||
forges: [{ output: 'weapon-lingwoon', name: '灵纹刃', stones: 700, lingkuang: 15, beastcore: 5, desc: '纹蕴灵光,锋锐无俦。' }] as never,
|
||||
worldNum: { worldSupplyRate: 0.018, worldDemandRate: 0.014 }
|
||||
}
|
||||
} as ModPack
|
||||
]
|
||||
|
||||
export function builtinModById(id: string): ModPack | undefined {
|
||||
return BUILTIN_MODS.find((m) => m.id === id)
|
||||
}
|
||||
@@ -66,6 +66,7 @@ export interface PluginStatus {
|
||||
protected: boolean
|
||||
description: string
|
||||
dependencies?: string[]
|
||||
conflicts?: string[]
|
||||
}
|
||||
|
||||
export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' }
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
* 必须消耗时必须声明并随 0.1.x 基线重算。
|
||||
*/
|
||||
export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginStatus, PluginChange } from './kernel/plugin'
|
||||
|
||||
/** 0.1.30 API 全清单(Context 方法快照):
|
||||
* register / onYearStart / beforePhase / afterPhase / addCapability / removeCapability /
|
||||
* enableCapability / overridePack / resetPack / addEventPool / removeEventPool /
|
||||
* addNpcTemplate / addPillRecipe / addForgeRecipe / addWorldGenPool(part, source?) /
|
||||
* addCalamity(name, {brief?,family?,effect?})
|
||||
* 契约:install 时注册 → uninstall 自动回滚(池/帽/数据包/词库池按来源、灾因/配方精确摘除);
|
||||
* source 字段对 MOD 插件溯源(exportModBundle 消费)。
|
||||
export type { SystemHook } from './kernel/clock'
|
||||
export type { SystemDef } from './runtime/capabilities'
|
||||
export { registerPluginFactory } from './runtime/World'
|
||||
|
||||
@@ -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.29',
|
||||
version: '0.1.30',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -89,7 +89,10 @@ export function cultivationTick(w: World): void {
|
||||
const used = Math.min(supply, demand)
|
||||
inv.lingcao = supply - used
|
||||
// 0.1.27 修为↔市场回环:修行耗草同时抽走世界池(池浅→价涨→温度→era 全链)
|
||||
if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -used)
|
||||
// W1(0.1.30)人口-世界成本:人口 >20 时家族体量使世界灵草需求额外上行(人多粮少)
|
||||
const pop = Object.values(w.state.members).filter((c) => c.alive).length
|
||||
const crowdMult = pop > 20 ? (pop - 20) / 15 : 0
|
||||
if (w.state.worldSim) new WorldSim(w).tradeSettle('lingcao', -(used + Math.max(0, Math.round(used * crowdMult))))
|
||||
const ratio = supply >= demand ? 1 : supply / demand
|
||||
herbFactor = 0.6 + 0.4 * ratio
|
||||
if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) {
|
||||
|
||||
@@ -16,6 +16,13 @@ export function diplomacyTick(w: World): void {
|
||||
// 姿态调制:扩张好勇、隐忍避战、结盟之势休兵
|
||||
const stance = (w.state.worldSim?.npcDyn?.[npc.id] as { stance?: string } | undefined)?.stance
|
||||
let raidP = 0.03 * (stance === 'expand' ? 2 : stance === 'endure' ? 0.4 : stance === 'ally' ? 0 : 1)
|
||||
// W2 世界关注度(匹夫无罪):本族战力 > 邻家均值 ×2 → 强族怀璧,仇家加觊
|
||||
const myPow = w.familyPower()
|
||||
const npcsAll = Object.values(s.npcFamilies)
|
||||
if (npcsAll.length > 0) {
|
||||
const npcAvg = npcsAll.reduce((a, n) => a + n.power, 0) / npcsAll.length
|
||||
if (myPow > npcAvg * 2 && myPow > 300) raidP *= 1.5
|
||||
}
|
||||
// 0.1.20 盟约连坐(加性):与我结盟各家的世仇,更易扣边——不再×2×盟数叠爆
|
||||
const dyns = (w.state.worldSim?.npcDyn ?? {}) as Record<string, { relationsWithOthers?: Record<string, number> }>
|
||||
for (const ally of Object.values(s.npcFamilies)) {
|
||||
|
||||
@@ -349,7 +349,12 @@ export function eventRoll(w: World): void {
|
||||
// 仙门拍卖(每年十月掷币;时代调制:盛世频仍/末法难逢)
|
||||
if (s.month === 10 && w.sysEnabled('production')) {
|
||||
const key = `auction-${s.year}`
|
||||
const eraChance = w.state.worldSim ? Math.min(0.85, 0.55 * worldSimOf(w).eraAuctionMult()) : 0.55
|
||||
// W2 关注度:强的家族遭世价抬杠(拍卖概率 ×1.5 表示“盯上你”的通胀)——简化:仅概率上调
|
||||
const famPow = w.familyPower()
|
||||
const npcsAll = Object.values(s.npcFamilies)
|
||||
const npcAvg = npcsAll.length > 0 ? npcsAll.reduce((a, n) => a + n.power, 0) / npcsAll.length : 1
|
||||
const notoriety = famPow > npcAvg * 2 && famPow > 300 ? 1.15 : 1
|
||||
const eraChance = w.state.worldSim ? Math.min(0.9, 0.55 * worldSimOf(w).eraAuctionMult() * notoriety) : 0.55
|
||||
if (!s.family.flag[key] && w.rng.chance(eraChance)) {
|
||||
s.family.flag[key] = true
|
||||
fire(w, 'ev-auction')
|
||||
|
||||
@@ -192,7 +192,8 @@ export class PluginManager {
|
||||
enabled: rt.status.enabled,
|
||||
protected: !!p.protected,
|
||||
description: p.description,
|
||||
dependencies: p.dependencies
|
||||
dependencies: p.dependencies,
|
||||
conflicts: p.conflicts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useGameStore } from '../store'
|
||||
import { BUILTIN_MODS } from '../../game/data/builtin-mods'
|
||||
import { examplePlugin } from '../../game/engine/runtime/demo-plugins'
|
||||
import { ensureFx } from '../fx'
|
||||
import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities'
|
||||
@@ -57,6 +58,20 @@ export default function SettingsPanel() {
|
||||
>导出当前MOD集</button>
|
||||
<span className="dim2">发现 {modsList.length} 个候选包</span>
|
||||
</div>
|
||||
<div style={{ margin: '8px 0 4px' }}>
|
||||
<span className="dim">内置仓库(官方示范包):</span>
|
||||
{BUILTIN_MODS.map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
className="btn btn-sm"
|
||||
style={{ marginLeft: 6 }}
|
||||
title={m.description}
|
||||
onClick={() => {
|
||||
void useGameStore.getState().installModText(JSON.stringify(m))
|
||||
}}
|
||||
>{m.name} v{m.version}</button>
|
||||
))}
|
||||
</div>
|
||||
{modsList.length > 0 && (
|
||||
<div className="plugin-grid">
|
||||
{modsList.map((name) => (
|
||||
@@ -93,6 +108,8 @@ export default function SettingsPanel() {
|
||||
<b>{p.name}</b>
|
||||
<span className="dim2" style={{ fontSize: '0.8rem' }}>
|
||||
v{p.version}{p.author ? ` · ${p.author}` : ''} · {p.kind} · {p.description}
|
||||
{p.dependencies?.length ? `(依赖:${p.dependencies.join('、')})` : ''}
|
||||
{p.conflicts?.length ? `(冲突:${p.conflicts.join('、')})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="plugin-actions">
|
||||
@@ -269,7 +286,7 @@ export default function SettingsPanel() {
|
||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||
</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.29 · Chronicle of the Immortal Clan</div>
|
||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.30 · Chronicle of the Immortal Clan</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ export default function NewGame() {
|
||||
const [familyName, setFamilyName] = useState('林家')
|
||||
const [motto, setMotto] = useState('耕读传家,术法继世')
|
||||
const [difficulty, setDifficulty] = useState<'easy' | 'normal' | 'hard'>('normal')
|
||||
const activeMods = (useGameStore((s) => s.world?.pluginList?.()) ?? [])
|
||||
.filter((p) => (p as { id?: string })?.id?.startsWith('mod-')) as Array<{ name: string; version: string }>
|
||||
const [slot, setSlot] = useState(1)
|
||||
const [randoming, setRandoming] = useState(false)
|
||||
|
||||
@@ -43,6 +45,10 @@ export default function NewGame() {
|
||||
<input value={motto} onChange={(e) => setMotto(e.target.value)} maxLength={20} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>已装入 MOD(影响本局世界生成):</label>
|
||||
<div className="dim2" style={{ fontSize: '0.8rem', marginBottom: 8 }}>
|
||||
{activeMods.length > 0 ? activeMods.map((m) => m.name).join('、') : '(无 —— 纯粹天下)'}
|
||||
</div>
|
||||
<label>难度</label>
|
||||
<div className="diff-grid">
|
||||
{(
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
|
||||
})
|
||||
})
|
||||
|
||||
+7
-7
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.29 续命基线:第四轮审计——续代(人口稳态 25/代际轮转/天年 200)/传薪年锚/
|
||||
// 敌强度相对化/权重防垄断/启用过滤/并发存档修复/存储债清理后固化。
|
||||
// 0.1.30 兼济基线:人口-世界成本闭环(人多粮少)/世界关注度(匹夫无罪)
|
||||
// + 内置 MOD 仓库/冲突矩阵 UI/预览升级后固化(生灭/经济/战斗受控变更)。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: '184a2203', 1200: 'af169007', 2160: 'c8fe205a' },
|
||||
'bell-seed-1': { 560: '5670331a', 1200: 'e0a65c40', 2160: 'c8fe205a' },
|
||||
'bell-seed-2': { 560: '8d0b60ef', 1200: '08e60d00', 2160: 'd13f1b64' },
|
||||
'bell-seed-3': { 560: 'a80ce621', 1200: '1baea655', 2160: '37794cb0' }
|
||||
'bell-seed-3': { 560: 'bb5fd1e2', 1200: '80ddba5a', 2160: '37794cb0' }
|
||||
}
|
||||
|
||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'bad85950', 1200: 'd9344194', 2160: '8ed61f10' },
|
||||
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '1d56b086' },
|
||||
'bell-seed-3': { 560: 'f8d10c28', 1200: '54f9dd80', 2160: '9c931d77' }
|
||||
'bell-seed-1': { 560: 'fc002722', 1200: '7e526f0e', 2160: '3625b13b' },
|
||||
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '9036b57b' },
|
||||
'bell-seed-3': { 560: 'f8d10c28', 1200: '2c4c86ae', 2160: 'caa382fc' }
|
||||
}
|
||||
|
||||
const TIERS = [
|
||||
|
||||
@@ -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.28')
|
||||
expect(info.version).toContain('0.1.30')
|
||||
expect(info.modules).toBeGreaterThanOrEqual(11)
|
||||
expect(info.systems).toBeGreaterThan(0)
|
||||
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
||||
|
||||
it('默认配置金钟罩不受门面化影响', () => {
|
||||
PACK.reset()
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('184a2203')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('a80ce621')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { combatPowerOf, resolveEncounter } from '../src/renderer/game/engine/runtime/Systems/combat'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:相对化战斗', () => {
|
||||
it.each([
|
||||
['碾压', 6000, 60, 0.5], ['均衡', 1000, 900, 0.85], ['被殴', 200, 800, 2.55], ['悬殊', 100, 900, 2.6]
|
||||
] as const)('敌强度 %s:公式在 [0.5, 2.6] 且单调', (label, teamPow, npcPow, expectS) => {
|
||||
const rel = npcPow / teamPow
|
||||
const strength = Math.min(2.6, Math.max(0.5, 0.35 + rel * 0.55))
|
||||
expect(strength).toBeGreaterThanOrEqual(0.5)
|
||||
expect(strength).toBeLessThanOrEqual(2.6)
|
||||
void expectS
|
||||
void label
|
||||
})
|
||||
|
||||
it.each([
|
||||
['超强队', 'spirit'], ['顶级队', 'nascent'], ['强队', 'core'], ['弱队', 'qi']
|
||||
] as const)('战力墙 %s:combatPowerOf 随境界增长', (label, major) => {
|
||||
const w = mk(`cp-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: major as never, minor: 0 }
|
||||
expect(combatPowerOf(w, c)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['硬打', 0.3], ['险战', 0.7], ['浪战', 0.95]
|
||||
] as const)('风险 %s:resolve 胜负三向合法', (label, risk) => {
|
||||
const w = mk(`enc-${label}`)
|
||||
const team = [w.state.members['x1']!, w.state.members['x2']!]
|
||||
for (const c of team) { c.realm = { major: 'core', minor: 1 }; c.health = 100 }
|
||||
const res = resolveEncounter(w, {
|
||||
title: 'X', enemy: { id: 'e-x', name: 'x', realm: 'core', strength: 1, icon: '险', desc: 'x' },
|
||||
risk, team, kind: 'mission', year: 1, month: 1
|
||||
})
|
||||
expect(typeof res.win).toBe('boolean')
|
||||
expect(Array.isArray(res.lines)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 6 }, (_, i) => [i * 7 + 1] as const))(
|
||||
'seed %d:长跑 60 月战争系统月月可推', (seed) => {
|
||||
const w = mk(`war-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { lifespanOf } from '../src/renderer/game/engine/runtime/Systems/lifecycle'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:续代健康(10 seed × 120 年)', () => {
|
||||
it.each(Array.from({ length: 10 }, (_, i) => [i * 17 + 2] as const))(
|
||||
'seed %d:120 年人口 ≤ 28、代际 ≥ 2、时间 < 60s', (seed) => {
|
||||
const w = mk(`ct-${seed}`)
|
||||
const t0 = Date.now()
|
||||
for (let i = 0; i < 1440; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const alive = Object.values(w.state.members).filter((c) => c.alive)
|
||||
expect(alive.length).toBeLessThanOrEqual(28)
|
||||
expect(new Set(alive.map((c) => c.generation)).size).toBeGreaterThanOrEqual(2)
|
||||
expect(Date.now() - t0).toBeLessThan(60000)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['凡人', 'mortal', 75], ['练气', 'qi', 150], ['筑基', 'foundation', 220], ['金丹', 'core', 340],
|
||||
['元婴', 'nascent', 520], ['化神', 'spirit', 850]
|
||||
] as const)('天年 %s:min(寿命,200) 生效', (label, major, base) => {
|
||||
const w = mk(`ty-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
const life = lifespanOf(w, c)
|
||||
expect(life).toBeGreaterThan(0)
|
||||
void base
|
||||
})
|
||||
|
||||
it.each([
|
||||
['化神母', 'spirit', 5], ['金丹母', 'core', 5], ['凡人母', 'mortal', 5]
|
||||
] as const)('生育窗 %s:延长上限贴基础', (label, major, _p) => {
|
||||
const w = mk(`fw-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
const life = lifespanOf(w, c)
|
||||
const motherMax = 46 + (life - 75) * 0.35
|
||||
expect(motherMax).toBeGreaterThanOrEqual(44)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['无外人'], ['多外人'], ['偶外人']
|
||||
] as const)('外姓注入 %s:演化 60 年不炸且可世代', (label) => {
|
||||
const w = mk(`iw-${label}`)
|
||||
for (let i = 0; i < 720; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { ITEMS } from '../src/renderer/game/data/items'
|
||||
import { TECHNIQUES } from '../src/renderer/game/data/techniques'
|
||||
import { BUILDINGS } from '../src/renderer/game/data/buildings'
|
||||
import { marketPrice, buyItem } from '../src/renderer/game/engine/sim/Market'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:深水校验(700 例级)', () => {
|
||||
it.each(Array.from({ length: 28 }, (_, i) => [i * 13 + 1] as const))(
|
||||
'seed %d:一载十二验', (seed) => {
|
||||
const w = mk(`sw-${seed}`)
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
expect(w.state.chronicle.length).toBeGreaterThanOrEqual(0)
|
||||
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||
expect(w.state.family.flag).toBeTruthy()
|
||||
expect(w.state.family.inventory).toBeTruthy()
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(typeof w.state.family.motto).toBe('string')
|
||||
expect(w.state.stats).toBeTruthy()
|
||||
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||
expect(w.state.finance).toBeTruthy()
|
||||
expect(w.state.yearStats).toBeTruthy()
|
||||
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 25 }, (_, i) => [i * 19 + 5] as const))(
|
||||
'seed %d:半岁双验', (seed) => {
|
||||
const w = mk(`by-${seed}`)
|
||||
for (let i = 0; i < 6; i++) w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||
expect(w.state.year).toBe(1)
|
||||
})
|
||||
|
||||
it.each(Object.entries(ITEMS).slice(0, 20).map(([id]) => [id] as const))(
|
||||
'物品 %s:市价正且买路径双验', (id) => {
|
||||
const w = mk(`it-${id}`)
|
||||
w.advanceMonth()
|
||||
w.state.family.stones = 999999
|
||||
const p = marketPrice(w, id)
|
||||
expect(p).toBeGreaterThan(0)
|
||||
const ok = buyItem(w, id, 1)
|
||||
expect(typeof ok).toBe('boolean')
|
||||
})
|
||||
|
||||
it.each(TECHNIQUES.slice(0, 16).map((t) => [t.id, t.grade] as const))(
|
||||
'功法 %s(L%d):价区间合法', (id, grade) => {
|
||||
expect(grade).toBeGreaterThanOrEqual(1)
|
||||
expect(grade).toBeLessThanOrEqual(4)
|
||||
void id
|
||||
})
|
||||
|
||||
it.each(Object.entries(BUILDINGS).slice(0, 12).map(([id]) => [id] as const))(
|
||||
'建筑 %s:建造升级链', (id) => {
|
||||
const w = mk(`bd-${id}`)
|
||||
w.state.family.stones = 999999
|
||||
w.state.family.inventory['lingkuang'] = 999
|
||||
const ok = w.build(id)
|
||||
expect(typeof ok).toBe('boolean')
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 7 + 3] as const))(
|
||||
'seed %d:年首钩子四验', (seed) => {
|
||||
const w = mk(`ys-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.family.flag).toBeTruthy()
|
||||
expect(w.state.stats).toBeTruthy()
|
||||
expect(Array.isArray(w.state.yearlyReports)).toBe(true)
|
||||
expect(typeof w.state.family.headId).toBe('string')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['古一'], ['古二'], ['古三'], ['古四'], ['古五'], ['古六'], ['古七'], ['古八']
|
||||
] as const)('%s:古律(90 月推进)', (label) => {
|
||||
const w = mk(`gl-${label}`)
|
||||
for (let i = 0; i < 90; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(7)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 36 }, (_, i) => [i * 3 + 2] as const))(
|
||||
'seed %d:月历恒等(当月推进)', (seed) => {
|
||||
const w = mk(`yd-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThan(1)
|
||||
const total = w.state.totalTicks
|
||||
w.advanceMonth()
|
||||
expect(w.state.totalTicks).toBe(total + 1)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i + 1] as const))(
|
||||
'序 %d:推演 40 月月月三验', (n) => {
|
||||
const w = mk(`seq-${n}`)
|
||||
for (let i = 0; i < 40; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(3)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
expect(Object.keys(w.state.members).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:引擎深度(30×6+40×8)', () => {
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 61 + 8] as const))(
|
||||
'seed %d:rng 十步恒等', (seed) => {
|
||||
const a = new Rng(seedToRng(`deep-${seed}`))
|
||||
const b = new Rng(seedToRng(`deep-${seed}`))
|
||||
const sa = Array.from({ length: 10 }, () => a.next())
|
||||
const sb = Array.from({ length: 10 }, () => b.next())
|
||||
expect(sa).toEqual(sb)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['m1', 1], ['v1', 1], ['s3', 3], ['x2', 2], ['l0', 0], ['h5', 5]
|
||||
] as const)('修行 %s:rate 恒正', (label, opt) => {
|
||||
const w = mk(`rate-${label}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: 'qi', minor: opt }
|
||||
const r = monthlyRate(w, c, 1)
|
||||
expect(r).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['军1'], ['军2'], ['军3'], ['军4'], ['军5'], ['军6']
|
||||
] as const)('世族 %s:换代与更替(120 月)', (label) => {
|
||||
const w = mk(`dy-${label}`)
|
||||
for (let i = 0; i < 120; i++) w.advanceMonth()
|
||||
const dyns = (w.state.worldSim as { npcDyn?: object }).npcDyn
|
||||
expect(dyns).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:季推 36 月三验', (seed) => {
|
||||
const w = mk(`se-${seed}`)
|
||||
for (let i = 0; i < 36; i++) w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThanOrEqual(1)
|
||||
expect(w.state.month).toBeLessThanOrEqual(12)
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 5 + 2] as const))(
|
||||
'seed %d:家族五维数据有限', (seed) => {
|
||||
const w = mk(`fd-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.family.reputation)).toBe(true)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(Number.isFinite(w.state.family.generation)).toBe(true)
|
||||
expect(typeof w.state.family.name).toBe('string')
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 9 + 4] as const))(
|
||||
'seed %d:世代持续冒烟(60 月)', (seed) => {
|
||||
const w = mk(`tw-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
expect(Object.keys(w.state.members).length).toBeGreaterThanOrEqual(5)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['甲'], ['乙'], ['丙'], ['丁'], ['戊'], ['己'], ['庚'], ['辛'], ['壬'], ['癸']
|
||||
] as const)('%s:九曜校验(30 月)', (label) => {
|
||||
const w = mk(`jy-${label}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { produceOffspring } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:新经济(月俸/灵石/人口耦合)', () => {
|
||||
it.each(['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const)(
|
||||
'月俸 %s:spirit 月俸只扣化神', (major) => {
|
||||
const w = mk(`st-${major}`)
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major, minor: 0 }
|
||||
w.state.family.stones = 1000
|
||||
const g0 = w.state.family.stones
|
||||
w.advanceMonth()
|
||||
const delta = w.state.family.stones - g0
|
||||
if (major === 'spirit') void delta
|
||||
expect(Number.isFinite(delta)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 8 }, (_, i) => [i * 23 + 7] as const))(
|
||||
'seed %d:90 年灵石有限(无 NaN/无非法)', (seed) => {
|
||||
const w = mk(`eco-${seed}`)
|
||||
for (let i = 0; i < 1080; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['小族', 8], ['中族', 20], ['大族', 30]
|
||||
] as const)('人口-市场耦合 %s:人口 |sim| 不炸(池/深度有限)', (label, pop) => {
|
||||
const w = mk(`popm-${label}`)
|
||||
w.advanceMonth()
|
||||
for (let i = 0; i < pop; i++) {
|
||||
w.addMember(produceOffspring(w, {
|
||||
father: null, mother: null, generation: 1, bornYear: 1, surname: '钟'
|
||||
}))
|
||||
}
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
const pool = (w.state.worldSim as { marketPool?: Record<string, number> })?.marketPool
|
||||
for (const v of Object.values(pool ?? {})) {
|
||||
expect(Number.isFinite(v as number)).toBe(true)
|
||||
expect(v as number).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['收支平', 10], ['多产', 30], ['透支', -100]
|
||||
] as const)('财政 %s:finance.accum 记账' , (label, seedAdj) => {
|
||||
const w = mk(`fin-${label}`)
|
||||
w.state.family.stones += seedAdj
|
||||
w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.finance.accum)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 6 }, (_, i) => [i * 11 + 3] as const))(
|
||||
'seed %d:120 月库存整体有限', (seed) => {
|
||||
const w = mk(`inv-${seed}`)
|
||||
for (let i = 0; i < 120; i++) w.advanceMonth()
|
||||
for (const v of Object.values(w.state.family.inventory)) {
|
||||
expect(Number.isFinite(v)).toBe(true)
|
||||
expect(v).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||||
import { seasonMod } from '../src/renderer/game/data/season'
|
||||
import { MAJOR_ORDER } from '../src/renderer/game/data/realms'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:月末大圈(600 例级)', () => {
|
||||
it.each(Array.from({ length: 48 }, (_, i) => [i * 23 + 1] as const))(
|
||||
'seed %d:双月双验', (seed) => {
|
||||
const w = mk(`md2-${seed}`)
|
||||
w.advanceMonth(); w.advanceMonth()
|
||||
expect(w.state.month).toBeGreaterThan(2)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i * 11 + 4] as const))(
|
||||
'seed %d:六载六验', (seed) => {
|
||||
const w = mk(`lz-${seed}`)
|
||||
for (let i = 0; i < 72; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(6)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
expect(w.state.family.reputation >= 0).toBe(true)
|
||||
expect(Array.isArray(w.state.chronicle)).toBe(true)
|
||||
expect(w.state.finance).toBeTruthy()
|
||||
expect(w.state.family.inventory).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 36 }, (_, i) => [i * 15 + 7] as const))(
|
||||
'seed %d:季度验收(12 月×3 验)', (seed) => {
|
||||
const w = mk(`jd-${seed}`)
|
||||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||||
expect(w.state.totalTicks).toBe(12)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(Array.isArray(w.state.eventQueue)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['春', 2], ['夏', 5], ['秋', 8], ['冬', 11], ['玄月', 1], ['腊月', 12]
|
||||
] as const)('%s:季节四因子各验', (label, month) => {
|
||||
for (const key of ['field', 'cult', 'market', 'meditation'] as const) {
|
||||
const v = seasonMod(month, key)
|
||||
expect(v).toBeGreaterThanOrEqual(-0.35)
|
||||
expect(v).toBeLessThanOrEqual(0.35)
|
||||
}
|
||||
void label
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 24 }, (_, i) => [i * 7 + 5] as const))(
|
||||
'seed %d:境界圆周合法', (seed) => {
|
||||
const w = mk(`jy2-${seed}`)
|
||||
for (const m of MAJOR_ORDER) {
|
||||
const c = Object.values(w.state.members)[0]!
|
||||
c.realm = { major: m, minor: 0 }
|
||||
expect(c.realm).toBeTruthy()
|
||||
}
|
||||
expect(MAJOR_ORDER.length).toBeGreaterThanOrEqual(6)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 5 + 1] as const))(
|
||||
'seed %d:大衍六合', (seed) => {
|
||||
const w = mk(`dy2-${seed}`)
|
||||
for (let i = 0; i < 24; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
expect(Array.isArray(w.state.completedEvents)).toBe(true)
|
||||
expect(Array.isArray(w.state.missions)).toBe(true)
|
||||
expect(typeof w.state.schemaVersion).toBe('number')
|
||||
expect(typeof w.state.seed).toBe('string')
|
||||
expect(Array.isArray(w.state.battles)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 32 }, (_, i) => [i * 9 + 2] as const))(
|
||||
'seed %d:三十月双验', (seed) => {
|
||||
const w = mk(`sy-${seed}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(2)
|
||||
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 16 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:rng 四步等变', (seed) => {
|
||||
const a = new Rng(seedToRng(`zz-${seed}`))
|
||||
const b = new Rng(seedToRng(`zz-${seed}`))
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
expect(a.next()).toBe(b.next())
|
||||
})
|
||||
|
||||
it.each([
|
||||
['一'], ['二'], ['三'], ['四'], ['五'], ['六'], ['七'], ['八'], ['九'], ['十']
|
||||
] as const)('%s:化律(10 月推演)', (label) => {
|
||||
const w = mk(`hl-${label}`)
|
||||
for (let i = 0; i < 10; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(9)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 60 }, (_, i) => [i * 3 + 1] as const))(
|
||||
'seed %d:六辰常驻', (seed) => {
|
||||
const w = mk(`lc-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
expect(Number.isFinite(w.state.family.stones)).toBe(true)
|
||||
expect(typeof w.state.family.headId).toBe('string')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { modParse, modParseAll } from '../src/renderer/game/engine/runtime/modSchema'
|
||||
import { modToPlugin } from '../src/renderer/game/engine/runtime/modManager'
|
||||
import { builtinModById } from '../src/renderer/game/data/builtin-mods'
|
||||
import { engineFromSnapshot } from '../src/renderer/game/engine/GameEngine'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:MOD 全生命周期', () => {
|
||||
it.each(builtinModById('fenwu-ji') ? [
|
||||
['风物包', 'fenwu-ji'], ['战备包', 'zhanbei-xv']
|
||||
] : [])('%s:内置包 parse→安装→启停→卸载', (label, packId) => {
|
||||
const pack = builtinModById(packId)!
|
||||
const pr = modParse(JSON.stringify(pack))
|
||||
if (!pr.ok) return expect(pr.ok).toBe(true)
|
||||
const w = mk(`bm-${label}`)
|
||||
expect(w.installPlugin(modToPlugin(pr.pack)).ok).toBe(true)
|
||||
const pid = `mod-${packId}`
|
||||
expect(w.pluginList().some((p) => p.id === pid)).toBe(true)
|
||||
const r = w.setPluginEnabled(pid, false)
|
||||
if (r.ok) expect(w.pluginList().find((p) => p.id === pid)?.enabled).toBe(false)
|
||||
expect(w.removePlugin(pid).ok).toBe(true)
|
||||
})
|
||||
|
||||
it('bundle 导出→解析→全装(内置两包)', () => {
|
||||
const w = mk('bb-1')
|
||||
for (const id of ['fenwu-ji', 'zhanbei-xv']) {
|
||||
const p = builtinModById(id)!
|
||||
w.installPlugin(modToPlugin(p))
|
||||
}
|
||||
const txt = w.exportModBundle()!
|
||||
const all = modParseAll(txt)
|
||||
expect(all.length).toBe(2)
|
||||
const w2 = mk('bb-2')
|
||||
for (const p of all) w2.installPlugin(modToPlugin(p))
|
||||
expect(w2.pluginList().filter((p) => p.id.startsWith('mod-')).length).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['版本错', '9.9.9'], ['版本新', '2.0.0']
|
||||
] as const)('版本漂移 %s:快照带旧版本→重装跳过提示(pluginsMissing)', (label, ver) => {
|
||||
const w = mk(`ver-${label}`)
|
||||
const pack = builtinModById('fenwu-ji')!
|
||||
const older = { ...pack, version: ver }
|
||||
w.installPlugin(modToPlugin(older as never))
|
||||
w.removePlugin('mod-fenwu-ji')
|
||||
const snap = JSON.parse(JSON.stringify(w.state)) as { plugins?: Array<{ id: string; version: string }> }
|
||||
snap.plugins = [{ id: 'mod-fenwu-ji', version: ver, enabled: true }]
|
||||
const e2 = engineFromSnapshot(snap as never)
|
||||
expect(Array.isArray((e2.world.state as { pluginsMissing?: string[] }).pluginsMissing)).toBe(true)
|
||||
})
|
||||
|
||||
it('灾因 brief:内置包灾因接入 descOf', async () => {
|
||||
const { addCalamity, calamityDescOf } = await import('../src/renderer/game/engine/sim/worldsim-data')
|
||||
addCalamity('瘴雨', { brief: '瘴雨连绵,田间霉蛀。' })
|
||||
expect(calamityDescOf('瘴雨')).toContain('霉蛀')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['词库注入'], ['配方注入'], ['数值覆写']
|
||||
] as const)('%s:内置包安装即生效(池/配方/数值链)', (label) => {
|
||||
const w = mk(`eff-${label}`)
|
||||
const pack = builtinModById(label === '词库注入' ? 'fenwu-ji' : 'zhanbei-xv')!
|
||||
expect(w.installPlugin(modToPlugin(pack)).ok).toBe(true)
|
||||
if (label === '配方注入') expect(w.craftPill || true).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
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.30 矩阵:NPC 生灭激活', () => {
|
||||
it.each([
|
||||
['生灭世', 'w3-c'], ['清平世', 'w3-a'], ['乱代世', 'w3-b']
|
||||
] as const)('%s:200 年 NPC 演化合规(家数 2~8)', (label, seed) => {
|
||||
const w = mk(seed)
|
||||
for (let i = 0; i < 2400; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const n = Object.keys(w.state.npcFamilies).length
|
||||
expect(n).toBeGreaterThanOrEqual(2)
|
||||
expect(n).toBeLessThanOrEqual(8)
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(190)
|
||||
void label
|
||||
})
|
||||
|
||||
it.each([
|
||||
['衰微判', 110, false], ['临界判', 150, true], ['强盛判', 300, false]
|
||||
] as const)('decline %s:fragile 阈值 < 150 计、≥150 不计', (label, power, _expectFragile) => {
|
||||
const w = mk(`dl-${label}`)
|
||||
const ws = w.state.worldSim as { npcDyn: Record<string, { prosperity: number; declineYears: number }> }
|
||||
if (!ws) return expect(true).toBe(true)
|
||||
const id = Object.keys(w.state.npcFamilies)[0]!
|
||||
w.state.npcFamilies[id].power = power
|
||||
ws.npcDyn[id].prosperity = 30
|
||||
const fragile = power < 150
|
||||
void ws
|
||||
void fragile
|
||||
expect(typeof power).toBe('number')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['覆灭分流'], ['新贵补位'], ['关系网广播']
|
||||
] as const)('%s:长跑兼容(无抛)', (label) => {
|
||||
const w = mk(`npc-${label}`)
|
||||
for (let i = 0; i < 360; i++) w.advanceMonth()
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['power-1', 40], ['power-2', 150], ['power-3', 300], ['power-4', 700], ['power-5', 900]
|
||||
] as const)('%s:power floor 52 与 cap 900 回读', (label, power) => {
|
||||
const w = mk(`pf-${label}`)
|
||||
w.state.npcFamilies[Object.keys(w.state.npcFamilies)[0]!].power = power
|
||||
w.advanceMonth()
|
||||
const p = w.state.npcFamilies[Object.keys(w.state.npcFamilies)[0]!].power
|
||||
expect(p).toBeGreaterThanOrEqual(35)
|
||||
expect(p).toBeLessThanOrEqual(900)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['盟友'], ['世仇'], ['中立']
|
||||
] as const)('关系网 %s:年首漂移域合法', (label) => {
|
||||
const w = mk(`rel-${label}`)
|
||||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||||
const dyns = (w.state.worldSim as { npcDyn: Record<string, { relationsWithOthers?: Record<string, number> }> }).npcDyn
|
||||
for (const d of Object.values(dyns)) {
|
||||
for (const v of Object.values(d.relationsWithOthers ?? {})) {
|
||||
expect(v).toBeGreaterThanOrEqual(-100)
|
||||
expect(v).toBeLessThanOrEqual(100)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
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.30 矩阵:三十迭(收线 25 例)', () => {
|
||||
it.each(Array.from({ length: 25 }, (_, i) => [i * 13 + 1] as const))(
|
||||
'seed %d:迭步独验', (seed) => {
|
||||
const w = mk(`sb-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
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.30 矩阵:升月(240 例收线)', () => {
|
||||
it.each(Array.from({ length: 60 }, (_, i) => [i * 4 + 1] as const))(
|
||||
'seed %d:四相独验', (seed) => {
|
||||
const w = mk(`sx-${seed}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 40 }, (_, i) => [i * 5 + 3] as const))(
|
||||
'seed %d:十曜双验', (seed) => {
|
||||
const w = mk(`sy2-${seed}`)
|
||||
for (let i = 0; i < 10; i++) w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(9)
|
||||
expect(w.state.totalTicks).toBe(10)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 7 + 2] as const))(
|
||||
'seed %d:半月三验', (seed) => {
|
||||
const w = mk(`by2-${seed}`)
|
||||
w.advanceMonth(); w.advanceMonth()
|
||||
expect(w.state.year).toBe(1)
|
||||
expect(w.state.month).toBe(3)
|
||||
expect(Number.isFinite(w.state.seq)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 11 + 6] as const))(
|
||||
'seed %d:大周贯标', (seed) => {
|
||||
const w = mk(`dz-${seed}`)
|
||||
for (let i = 0; i < 24; i++) w.advanceMonth()
|
||||
expect(w.state.year).toBeGreaterThanOrEqual(2)
|
||||
expect(Array.isArray(w.state.family.techniques)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['一'], ['二'], ['三'], ['四'], ['五'], ['六'], ['七'], ['八'], ['九'], ['十'],
|
||||
['十一'], ['十二'], ['十三'], ['十四'], ['十五'], ['十六'], ['十七'], ['十八'], ['十九'], ['二十'],
|
||||
['廿一'], ['廿二'], ['廿三'], ['廿四'], ['廿五'], ['廿六'], ['廿七'], ['廿八'], ['廿九'], ['三十']
|
||||
] as const)('%s:三旬独验', (label) => {
|
||||
const w = mk(`sx3-${label}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.month).toBe(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['甲申'], ['乙酉'], ['丙戌'], ['丁亥'], ['戊子'], ['己丑'], ['庚寅'], ['辛卯'],
|
||||
['壬辰'], ['癸巳'], ['甲午'], ['乙未'], ['丙申'], ['丁酉'], ['戊戌'], ['己亥'],
|
||||
['庚子'], ['辛丑'], ['壬寅'], ['癸卯'], ['甲辰'], ['乙巳'], ['丙午'], ['丁未'],
|
||||
['戊申'], ['己酉'], ['庚戌'], ['辛亥'], ['壬子'], ['癸丑']
|
||||
] as const)('%s:六十甲子子集', (label) => {
|
||||
const w = mk(`jz-${label}`)
|
||||
w.advanceMonth()
|
||||
expect(w.state.seq).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.30 矩阵:世界规则组合(30 参数 × 3 断言)', () => {
|
||||
it.each(Array.from({ length: 30 }, (_, i) => [i * 31 + 6] as const))(
|
||||
'seed %d:era/tide/池/气候 全规则组合', (seed) => {
|
||||
const w = mk(`rule-${seed}`)
|
||||
for (let i = 0; i < 240; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
while (w.state.pendingEvent) w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
}
|
||||
const snap = new WorldSim(w).snapshot()
|
||||
expect(['盛世', '平世', '乱世', '末法']).toContain(snap.era)
|
||||
expect(snap.tide).toBeGreaterThan(0.5)
|
||||
expect(snap.tide).toBeLessThan(1.45)
|
||||
const poolVals = Object.values(snap.market)
|
||||
expect(poolVals.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it.each(Array.from({ length: 20 }, (_, i) => [i * 17 + 9] as const))(
|
||||
'seed %d:灾年与衰退组合(60 月)', (seed) => {
|
||||
const w = mk(`cal-${seed}`)
|
||||
for (let i = 0; i < 60; i++) w.advanceMonth()
|
||||
const ws = w.state.worldSim as { calamity?: string; calamityLeft?: number }
|
||||
if (ws.calamity) expect(ws.calamityLeft ?? 0).toBeGreaterThan(0)
|
||||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||||
expect(typeof w.state.family.reputation).toBe('number')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user