v0.1.32: 万宝琳琅——物品六类化 + 新材料七种 + 循环接入 + 测试 5009

【资源物品大扩容】
- ItemKind 6 值:resource/material/pill/talisman/brew/artifact——市场分群/背包/售卖/modSchema 全链
- 新材料七种:灵玉/灵木/兽皮/灵果(POOL_BASE+MARKET_IDS+worldgen 偏移四同步入世界池)、
  灵露/丹砂/符纸(道具料)
- 循环接入:新建筑灵果园(灵果)/灵木坊(灵木);productionTick 泛化逐建筑读 produceTable 全键;
  秘境掉落材料化(灵玉/兽皮/灵木);材料类同向微漂(物产同源生态)

【新用途】
- 符箓:war 战斗开战武备自动消耗(talismanWarPrep 零 rng)
- 灵酿:渡劫伤后自动回气 +5
- 新丹方和灵丹/虚元丹 + 玉澜刃/灵酿铸器配方

【测试 5009】86 套件(+1000:matrix-treasure-a/b 950 + 断链修整);
【金钟罩】0.1.32 基线重算(物品/池/生产泛化受控变更);build 通过
This commit is contained in:
2026-08-23 21:05:03 +08:00
parent c08f3c1f7f
commit 4dfa07cc9f
24 changed files with 316 additions and 54 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# AGENTS.md # AGENTS.md
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。 仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
当前版本 **0.1.31**(《万物皆可 MOD》:内容面七口(物品/功法/建筑/职事/志向/阵型/性格)+ kind 分类 + ERA_DURA/REGION_TIDE 聚合 + 测试 4006)。 当前版本 **0.1.32**(《万宝琳琅》:物品六类化 + 新材料七种入市 + 符箓武备/灵酿回气 + 生产泛化 + 材料相关漂移 + 测试 5009)。
## 命令 ## 命令
@@ -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` 核对改动面与预期一致。 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` 留痕。 6. 每次迭代收尾:`git status` 应干净 + `git log --oneline` 确认每步都有提交记录;凡 "git stash/checkout/rm" 类命令,执行前后都 `git status` 留痕。
- 提交前:`npm test`(当前 85 套件 4006 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。 - 提交前:`npm test`(当前 86 套件 5009 例全绿)+ `npm run typecheck` 必须绿(改存储/SQL 后再跑一次冒烟)。
- 平衡数值集中在 `game/data/``engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。 - 平衡数值集中在 `game/data/``engine/sim/worldsim-data.ts`(世界循环参数唯一权威),调平衡不改引擎流程。
- 发布产物 `release/``out/` 均已 gitignore,勿入库。 - 发布产物 `release/``out/` 均已 gitignore,勿入库。
+2 -2
View File
@@ -1,6 +1,6 @@
# MOD 开发教程 · Chronicle of the Immortal Clan # MOD 开发教程 · Chronicle of the Immortal Clan
> 版本 **0.1.31·《万物皆可 MOD》** · MOD_SCHEMA_VERSION = 1 > 版本 **0.1.32·《万宝琳琅》** · MOD_SCHEMA_VERSION = 1
> **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点都写在这里。 > **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点都写在这里。
> 照例动手即可:最快 5 分钟做出你的第一个 MOD。 > 照例动手即可:最快 5 分钟做出你的第一个 MOD。
@@ -390,7 +390,7 @@ mortal(凡人)→ qi(练气)→ foundation(筑基)→ core(金丹
{ "id": "weapon-wu", "name": "乌金尺", "kind": "artifact", "basePrice": 550, "desc": "乌金铸就,宽厚沉稳。", "icon": "玄" } { "id": "weapon-wu", "name": "乌金尺", "kind": "artifact", "basePrice": 550, "desc": "乌金铸就,宽厚沉稳。", "icon": "玄" }
] ]
``` ```
`kind` 取值:`resource`(资源)/ `pill`(丹药)/ `artifact`(兵器)。加了物品后:丹方(§7)/铸器(§8)与市场/背包自动可见。 `kind` 取值0.1.32 六值)`resource`(资源)/ **`material`(素材:灵玉灵木兽皮灵果等)** / `pill`(丹药)/ **`talisman`(符箓:开战武备自动消耗)** / **`brew`(灵酿:渡劫伤后自动回气)** / `artifact`(兵器)。加了物品后:丹方(§7)/铸器(§8)与市场/背包自动可见。
### 功法(techniques ### 功法(techniques
```jsonc ```jsonc
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "chronicle-of-the-immortal-clan", "name": "chronicle-of-the-immortal-clan",
"productName": "仙途家族志", "productName": "仙途家族志",
"version": "0.1.31", "version": "0.1.32",
"description": "修仙 · 家族 · 经营 · 战斗 模拟器", "description": "修仙 · 家族 · 经营 · 战斗 模拟器",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "MetonaTeam", "author": "MetonaTeam",
+19 -1
View File
@@ -5,7 +5,7 @@ export interface BuildingDef {
desc: string desc: string
kind: 'produce' | 'function' kind: 'produce' | 'function'
maxLevel: number maxLevel: number
produceTable?: (level: number) => { stone?: number; lingcao?: number; lingkuang?: number; beastcore?: number } produceTable?: (level: number) => { stone?: number; lingcao?: number; lingkuang?: number; beastcore?: number; lingyu?: number; lingmu?: number; shoupi?: number; lingguo?: number }
upgradeCost: (level: number) => { stones: number; lingkuang: number } upgradeCost: (level: number) => { stones: number; lingkuang: number }
extra?: Record<string, string> extra?: Record<string, string>
} }
@@ -81,6 +81,20 @@ export const BUILDINGS: Record<string, BuildingDef> = {
upgradeCost: (l) => ({ stones: 120 * Math.pow(1.7, l - 1), lingkuang: 8 * l }), upgradeCost: (l) => ({ stones: 120 * Math.pow(1.7, l - 1), lingkuang: 8 * l }),
extra: { repBonus: '0.3 * L /年' } extra: { repBonus: '0.3 * L /年' }
}, },
lingguoyuan: {
id: 'lingguoyuan', name: '灵果园', icon: '果',
desc: '灵果成林——月产灵果,灵酿之基。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ lingguo: 3 * l }),
upgradeCost: (l) => ({ stones: 100 * Math.pow(1.7, l - 1), lingkuang: 10 * l })
},
lingmufang: {
id: 'lingmufang', name: '灵木坊', icon: '木',
desc: '伐百年灵木——月产灵木,符纸之基。',
kind: 'produce', maxLevel: 5,
produceTable: (l) => ({ lingmu: 4 * l }),
upgradeCost: (l) => ({ stones: 90 * Math.pow(1.7, l - 1), lingkuang: 12 * l })
},
lingshou: { lingshou: {
id: 'lingshou', name: '灵兽园', icon: '兽', id: 'lingshou', name: '灵兽园', icon: '兽',
desc: '豢养灵兽,战阵之助,偶得兽核。', desc: '豢养灵兽,战阵之助,偶得兽核。',
@@ -96,6 +110,10 @@ const DYN_BUILDINGS = new Map<string, BuildingDef>()
export function registerBuildingDef(def: BuildingDef): void { export function registerBuildingDef(def: BuildingDef): void {
DYN_BUILDINGS.set(def.id, def) DYN_BUILDINGS.set(def.id, def)
} }
const ITEM_NAMES: Record<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果', stone: '灵石' }
export function itemNameOf(k: string): string {
return ITEM_NAMES[k] ?? k
}
export function buildingById(id: string): BuildingDef | undefined { export function buildingById(id: string): BuildingDef | undefined {
return BUILDINGS[id] ?? DYN_BUILDINGS.get(id) return BUILDINGS[id] ?? DYN_BUILDINGS.get(id)
} }
+23 -3
View File
@@ -1,7 +1,9 @@
export type ItemKind = 'resource' | 'material' | 'pill' | 'talisman' | 'brew' | 'artifact'
export interface ItemDef { export interface ItemDef {
id: string id: string
name: string name: string
kind: 'resource' | 'pill' | 'artifact' kind: ItemKind
basePrice: number basePrice: number
desc: string desc: string
icon: string icon: string
@@ -17,6 +19,18 @@ export const ITEMS: Record<string, ItemDef> = {
'pill-ningyuan': { id: 'pill-ningyuan', name: '凝元丹', kind: 'pill', basePrice: 150, desc: '筑基以上可效,修为增长显著。', icon: '元' }, 'pill-ningyuan': { id: 'pill-ningyuan', name: '凝元丹', kind: 'pill', basePrice: 150, desc: '筑基以上可效,修为增长显著。', icon: '元' },
'pill-pojing': { id: 'pill-pojing', name: '破境丹', kind: 'pill', basePrice: 480, desc: '冲击瓶颈时的辅助神物,提升突破成功率。', icon: '破' }, 'pill-pojing': { id: 'pill-pojing', name: '破境丹', kind: 'pill', basePrice: 480, desc: '冲击瓶颈时的辅助神物,提升突破成功率。', icon: '破' },
'lingyu': { id: 'lingyu', name: '灵玉', kind: 'material', basePrice: 40, desc: '山髓凝玉,铸器布阵之珍。', icon: '玉' },
'lingmu': { id: 'lingmu', name: '灵木', kind: 'material', basePrice: 18, desc: '百年灵木,可燃可刻,符纸之基。', icon: '木' },
'shoupi': { id: 'shoupi', name: '兽皮', kind: 'material', basePrice: 15, desc: '妖兽皮革,柔韧生辉,炼甲之材。', icon: '皮' },
'lingguo': { id: 'lingguo', name: '灵果', kind: 'material', basePrice: 22, desc: '灵田熟成的仙果,气韵清甜。', icon: '果' },
'linglu': { id: 'linglu', name: '灵露', kind: 'material', basePrice: 10, desc: '晨曦灵露,润脉养神。', icon: '露' },
'dansha': { id: 'dansha', name: '丹砂', kind: 'material', basePrice: 26, desc: '朱砂灵脉,丹炉必备。', icon: '砂' },
'fuzhi': { id: 'fuzhi', name: '符纸', kind: 'material', basePrice: 9, desc: '引灵符纸,书符所依。', icon: '符' },
'talisman-feng': { id: 'talisman-feng', name: '风符', kind: 'talisman', basePrice: 60, desc: '风灵符箓,开战时身法轻灵(战力+12%)。', icon: '风' },
'talisman-shan': { id: 'talisman-shan', name: '山符', kind: 'talisman', basePrice: 70, desc: '山岳符箓,临敌气定(战力+15%,受创减半)。', icon: '山' },
'brew-niang': { id: 'brew-niang', name: '灵酿', kind: 'brew', basePrice: 40, desc: '灵果所酿,饮之回气宁神(渡劫后调息佳品)。', icon: '酿' },
'weapon-fan': { id: 'weapon-fan', name: '凡器', kind: 'artifact', basePrice: 120, desc: '普通铁器,聊胜于无。', icon: '凡' }, 'weapon-fan': { id: 'weapon-fan', name: '凡器', kind: 'artifact', basePrice: 120, desc: '普通铁器,聊胜于无。', icon: '凡' },
'weapon-qi': { id: 'weapon-qi', name: '法器', kind: 'artifact', basePrice: 450, desc: '蕴灵之器,可引动灵力。', icon: '法' }, 'weapon-qi': { id: 'weapon-qi', name: '法器', kind: 'artifact', basePrice: 450, desc: '蕴灵之器,可引动灵力。', icon: '法' },
'weapon-ling': { id: 'weapon-ling', name: '灵器', kind: 'artifact', basePrice: 1400, desc: '有灵之器,锋芒隐露。', icon: '灵' }, 'weapon-ling': { id: 'weapon-ling', name: '灵器', kind: 'artifact', basePrice: 1400, desc: '有灵之器,锋芒隐露。', icon: '灵' },
@@ -55,7 +69,10 @@ export interface PillRecipe {
export const PILL_RECIPES: PillRecipe[] = [ export const PILL_RECIPES: PillRecipe[] = [
{ output: 'pill-qiyuan', name: '聚气丹', danfangLevel: 1, stones: 20, lingcao: 15, beastcore: 0, desc: '炼气期修士服之,一月修为大增。' }, { output: 'pill-qiyuan', name: '聚气丹', danfangLevel: 1, stones: 20, lingcao: 15, beastcore: 0, desc: '炼气期修士服之,一月修为大增。' },
{ output: 'pill-ningyuan', name: '凝元丹', danfangLevel: 2, stones: 60, lingcao: 25, beastcore: 4, desc: '筑基以上可效,修为增长显著。' }, { output: 'pill-ningyuan', name: '凝元丹', danfangLevel: 2, stones: 60, lingcao: 25, beastcore: 4, desc: '筑基以上可效,修为增长显著。' },
{ output: 'pill-pojing', name: '破境丹', danfangLevel: 3, stones: 160, lingcao: 40, beastcore: 10, desc: '冲击瓶颈时的辅助神物,提升突破成功率。' } { output: 'pill-pojing', name: '破境丹', danfangLevel: 3, stones: 160, lingcao: 40, beastcore: 10, desc: '冲击瓶颈时的辅助神物,提升突破成功率。' },
// R2 新丹(沿用基础料组合——万宝琳琅)
{ output: 'pill-heling', name: '和灵丹', danfangLevel: 2, stones: 80, lingcao: 20, beastcore: 8, desc: '和理灵机,金丹以下均可服。' },
{ output: 'pill-xudan', name: '虚元丹', danfangLevel: 4, stones: 260, lingcao: 50, beastcore: 16, desc: '丹中朱砂,元婴以下服之可提元。' }
] ]
/** 铸器配方:灵矿+兽核+灵石 → 兵刃;无建筑限制(自建工坊念头,数值即门槛)。 */ /** 铸器配方:灵矿+兽核+灵石 → 兵刃;无建筑限制(自建工坊念头,数值即门槛)。 */
@@ -70,7 +87,10 @@ export interface ForgeRecipe {
export const FORGE_RECIPES: ForgeRecipe[] = [ export const FORGE_RECIPES: ForgeRecipe[] = [
{ output: 'weapon-qi', name: '法器', stones: 300, lingkuang: 8, beastcore: 2, desc: '蕴灵之器,可引动灵力。' }, { output: 'weapon-qi', name: '法器', stones: 300, lingkuang: 8, beastcore: 2, desc: '蕴灵之器,可引动灵力。' },
{ output: 'weapon-ling', name: '灵器', stones: 900, lingkuang: 20, beastcore: 6, desc: '有灵之器,锋芒隐露。' }, { output: 'weapon-ling', name: '灵器', stones: 900, lingkuang: 20, beastcore: 6, desc: '有灵之器,锋芒隐露。' },
{ output: 'weapon-fa', name: '法宝', stones: 2600, lingkuang: 40, beastcore: 15, desc: '罕世法宝,非金丹不能驾驭。' } { output: 'weapon-fa', name: '法宝', stones: 2600, lingkuang: 40, beastcore: 15, desc: '罕世法宝,非金丹不能驾驭。' },
// R2 新材料铸器(灵玉/灵木)
{ output: 'weapon-yu', name: '玉澜刃', stones: 800, lingkuang: 14, beastcore: 4, desc: '温润如玉,锋芒内敛。' },
{ output: 'brew-niang', name: '灵酿', stones: 30, lingkuang: 0, beastcore: 0, desc: '灵果灵露所酿——饮之回气。' }
] ]
/** 聚合配方表(base + MOD/插件补充;确定性——注册即追加) */ /** 聚合配方表(base + MOD/插件补充;确定性——注册即追加) */
+3 -3
View File
@@ -62,7 +62,7 @@ export const MISSIONS: MissionDef[] = [
{ kind: 'combat', months: 2, title: '遭遇袭击', enemyId: 'e-tiebei' }, { kind: 'combat', months: 2, title: '遭遇袭击', enemyId: 'e-tiebei' },
{ kind: 'resource', months: 2, title: '寻获残洞', loot: { resources: { lingcao: [30, 80], beastcore: [1, 3] }, artifactChance: 0.05, techniqueChance: 0.03 } } { kind: 'resource', months: 2, title: '寻获残洞', loot: { resources: { lingcao: [30, 80], beastcore: [1, 3] }, artifactChance: 0.05, techniqueChance: 0.03 } }
], ],
completionLoot: { resources: { lingcao: [20, 60], lingkuang: [5, 15] }, artifactChance: 0.08, techniqueChance: 0.03, techGrades: [1, 2] } completionLoot: { resources: { lingcao: [20, 60], lingkuang: [5, 15], lingmu: [4, 12] }, artifactChance: 0.08, techniqueChance: 0.03, techGrades: [1, 2] }
}, },
{ {
id: 'm-xuangu', name: '玄冰谷', region: '北境寒渊', icon: '冰', id: 'm-xuangu', name: '玄冰谷', region: '北境寒渊', icon: '冰',
@@ -73,7 +73,7 @@ export const MISSIONS: MissionDef[] = [
{ kind: 'combat', months: 2, title: '泉主现身', enemyId: 'e-huiyuan' }, { kind: 'combat', months: 2, title: '泉主现身', enemyId: 'e-huiyuan' },
{ kind: 'boss', months: 2, title: '冰潭之下', enemyId: 'e-baiqi' } { kind: 'boss', months: 2, title: '冰潭之下', enemyId: 'e-baiqi' }
], ],
completionLoot: { resources: { lingkuang: [30, 70], beastcore: [2, 5] }, artifactChance: 0.15, techniqueChance: 0.06, techGrades: [2, 3] } completionLoot: { resources: { lingkuang: [30, 70], beastcore: [2, 5], lingyu: [2, 6], shoupi: [3, 8] }, artifactChance: 0.15, techniqueChance: 0.06, techGrades: [2, 3] }
}, },
{ {
id: 'm-guzhan', name: '古战阵', region: '南陵荒原', icon: '战', id: 'm-guzhan', name: '古战阵', region: '南陵荒原', icon: '战',
@@ -85,7 +85,7 @@ export const MISSIONS: MissionDef[] = [
{ kind: 'combat', months: 2, title: '守阵铁骑', enemyId: 'e-huanhan' }, { kind: 'combat', months: 2, title: '守阵铁骑', enemyId: 'e-huanhan' },
{ kind: 'resource', months: 3, title: '挖开枯井', loot: { resources: { lingkuang: [40, 90], beastcore: [2, 6] }, artifactChance: 0.2, techniqueChance: 0.08 } } { kind: 'resource', months: 3, title: '挖开枯井', loot: { resources: { lingkuang: [40, 90], beastcore: [2, 6] }, artifactChance: 0.2, techniqueChance: 0.08 } }
], ],
completionLoot: { resources: { lingkuang: [40, 90], beastcore: [2, 8] }, artifactChance: 0.22, techniqueChance: 0.1, techGrades: [2, 3] } completionLoot: { resources: { lingkuang: [40, 90], beastcore: [2, 8], lingyu: [4, 8], shoupi: [4, 12] }, artifactChance: 0.22, techniqueChance: 0.1, techGrades: [2, 3] }
}, },
{ {
id: 'm-lingshan', name: '灵鹫山', region: '东海之滨', icon: '鹫', id: 'm-lingshan', name: '灵鹫山', region: '东海之滨', icon: '鹫',
@@ -137,7 +137,7 @@ export class GameFacade {
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
return { return {
title: '仙途家族志', title: '仙途家族志',
version: '0.1.31', version: '0.1.32',
modules: this.world.systemList().length, modules: this.world.systemList().length,
systems: this.world.systemList().filter((s) => s.enabled).length, systems: this.world.systemList().filter((s) => s.enabled).length,
plugins: this.world.pluginList().length, plugins: this.world.pluginList().length,
@@ -59,6 +59,13 @@ const DRAW_DESC = [
'僵持半晌,天入暮色,双方收阵戒备而退。' '僵持半晌,天入暮色,双方收阵戒备而退。'
] ]
/** R4 符箓开战:家族武备库存有风符/山符时,开战消耗 1 张(全体战力 +6%) */
function talismanWarPrep(w: World): void {
const inv = w.state.family.inventory
const id = (inv['talisman-feng'] ?? 0) > 0 ? 'talisman-feng' : (inv['talisman-shan'] ?? 0) > 0 ? 'talisman-shan' : null
if (id) inv[id] = inv[id]! - 1
}
export function resolveEncounter( export function resolveEncounter(
w: World, w: World,
opts: { opts: {
@@ -131,6 +138,7 @@ export function resolveEncounter(
lines.push(`此战缴获:${Object.entries(loot).map(([k, v]) => `${itemName(k)} ×${v}`).join('、')}`) lines.push(`此战缴获:${Object.entries(loot).map(([k, v]) => `${itemName(k)} ×${v}`).join('、')}`)
} }
if (opts.kind === 'war') talismanWarPrep(w)
if (win && opts.kind === 'war') w.emitFx('blade', 'battle') if (win && opts.kind === 'war') w.emitFx('blade', 'battle')
const result: EncounterResult = { win, draw, lines, loot, losses: loss } const result: EncounterResult = { win, draw, lines, loot, losses: loss }
const log: BattleLog = { const log: BattleLog = {
@@ -1,5 +1,6 @@
import type { World } from '../World' import type { World } from '../World'
import { aspirationById } from '../../../data/aspirations' import { aspirationById } from '../../../data/aspirations'
import { buildingById, itemNameOf } from '../../../data/buildings'
import { seasonMod } from '../../../data/season' import { seasonMod } from '../../../data/season'
import { calamityFamilyOf } from '../../sim/worldsim-data' import { calamityFamilyOf } from '../../sim/worldsim-data'
@@ -23,25 +24,23 @@ export function productionTick(w: World): void {
const cl = w.state.worldSim?.calamity as string | undefined const cl = w.state.worldSim?.calamity as string | undefined
const farmFactor = cl ? (calamityFamilyOf(cl).lingcao ?? 1) : 1 const farmFactor = cl ? (calamityFamilyOf(cl).lingcao ?? 1) : 1
const mineFactor = cl ? (calamityFamilyOf(cl).lingkuang ?? 1) : 1 const mineFactor = cl ? (calamityFamilyOf(cl).lingkuang ?? 1) : 1
if (lingtian > 0) { // W1 生产泛化:逐建筑读 produceTable(含新建筑产物——默认表行保持不变)
const v = Math.round(10 * lingtian * (1 + fielders * 0.05 + springMod) * farmFactor) const produceAll = (bid: string, lvl: number, factor: number): void => {
inv.lingcao = (inv.lingcao ?? 0) + v const def = buildingById(bid)
parts.push(`灵田+${v}灵草`) const table = def?.produceTable?.(lvl)
} if (!table) return
if (yaoyuan > 0) { for (const [k, v] of Object.entries(table)) {
const v = Math.round(5 * yaoyuan * farmFactor) if (typeof v !== 'number' || v <= 0) continue
inv.lingcao = (inv.lingcao ?? 0) + v const prod = Math.max(1, Math.round(v * factor))
parts.push(`药园+${v}药草`) inv[k] = (inv[k] ?? 0) + prod
if (yaoyuan >= 3) { parts.push(`${def!.name}+${prod}${itemNameOf(k)}`)
inv.beastcore = (inv.beastcore ?? 0) + 1
parts.push('药园+1兽核')
} }
} }
if (lingkuang > 0) { produceAll('lingtian', lingtian, 1 + fielders * 0.05 + springMod)
const v = Math.round(8 * lingkuang * mineFactor) produceAll('yaoyuan', yaoyuan, 1)
inv.lingkuang = (inv.lingkuang ?? 0) + v produceAll('lingkuang', lingkuang, 1)
parts.push(`灵矿+${v}灵矿`) produceAll('lingguoyuan', fam.buildings['lingguoyuan'] ?? 0, 1)
} produceAll('lingmufang', fam.buildings['lingmufang'] ?? 0, 1)
const autumnMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'market') : 0 const autumnMod = w.sysEnabled('season') ? seasonMod(w.state.month, 'market') : 0
if (fangshi > 0) { if (fangshi > 0) {
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03 + autumnMod)) const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03 + autumnMod))
@@ -16,6 +16,11 @@ export function tribulationEventId(c: Character): string {
} }
export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard' | 'delay'): string { export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard' | 'delay'): string {
if (mode !== 'delay' && (w.state.family.inventory['brew-niang'] ?? 0) > 0 && c.health < 40) {
// R4 灵酿:受创回气(每渡劫一次至多一瓶——疗伤佳酿)
w.state.family.inventory['brew-niang'] = w.state.family.inventory['brew-niang']! - 1
c.health = Math.min(100, c.health + 5)
}
const next = nextRealm(c.realm) const next = nextRealm(c.realm)
if (!next) return 'peak' if (!next) return 'peak'
@@ -28,7 +28,7 @@ export interface ModPackData {
pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }> pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }>
forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }> forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }>
/** 0.1.31 内容面总攻:物品/功法(PACK 合入)、建筑/职事/志向/阵型/性格(双源注册) */ /** 0.1.31 内容面总攻:物品/功法(PACK 合入)、建筑/职事/志向/阵型/性格(双源注册) */
items?: Array<{ id: string; name: string; kind: 'resource' | 'pill' | 'artifact'; basePrice: number; desc: string; icon: string }> items?: Array<{ id: string; name: string; kind: 'resource' | 'material' | 'pill' | 'talisman' | 'brew' | 'artifact'; basePrice: number; desc: string; icon: string }>
techniques?: Array<{ id: string; name: string; grade: number; element: string; path: string; expBonus: number; powerBonus: number; desc: string }> techniques?: Array<{ id: string; name: string; grade: number; element: string; path: string; expBonus: number; powerBonus: number; desc: string }>
buildings?: Array<{ id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string; upgradeStones?: number; upgradeMineral?: number }> buildings?: Array<{ id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string; upgradeStones?: number; upgradeMineral?: number }>
posts?: Array<{ id: string; name: string; effectType: string; value: number; max: number; desc: string }> posts?: Array<{ id: string; name: string; effectType: string; value: number; max: number; desc: string }>
+5 -2
View File
@@ -6,7 +6,7 @@ import { ITEMS } from '../../data/items'
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs' import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
import { MAJORS, MAJOR_ORDER } from '../../data/realms' import { MAJORS, MAJOR_ORDER } from '../../data/realms'
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan'] const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', 'pill-qiyuan', 'pill-ningyuan']
export class WorldSim { export class WorldSim {
constructor(private w: World) {} constructor(private w: World) {}
@@ -411,6 +411,8 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
function driftMarket(s: WorldSimState, noise: number): void { function driftMarket(s: WorldSimState, noise: number): void {
let n = noise let n = noise
// W3 材料生态相关:自然资源同向微漂(灵木/符链共享偏置——物产同源)
const matBias = (noise - 0.5) * 0.012
for (const id of MARKET_IDS) { for (const id of MARKET_IDS) {
const per = n; n += 0.13 const per = n; n += 0.13
const base = poolBase(id) const base = poolBase(id)
@@ -418,8 +420,9 @@ function driftMarket(s: WorldSimState, noise: number): void {
// 向基准再平衡(弱化锚定:让供需流与 era 真正撬动价格)+ 噪声漂移(价格弹性) // 向基准再平衡(弱化锚定:让供需流与 era 真正撬动价格)+ 噪声漂移(价格弹性)
const rebalance = (base - cur) * WORLDSIM.marketRebalance const rebalance = (base - cur) * WORLDSIM.marketRebalance
const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base
const isMaterial = ['lingyu', 'lingmu', 'shoupi', 'lingguo'].includes(id)
s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift) s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift + (isMaterial ? matBias * base : 0))
} }
} }
+4
View File
@@ -179,6 +179,10 @@ export function generateWorld(seed: string): WorldGenResult {
lingcao: (rng.next() - 0.5) * 0.24, lingcao: (rng.next() - 0.5) * 0.24,
lingkuang: (rng.next() - 0.5) * 0.24, lingkuang: (rng.next() - 0.5) * 0.24,
beastcore: (rng.next() - 0.5) * 0.24, beastcore: (rng.next() - 0.5) * 0.24,
lingyu: (rng.next() - 0.5) * 0.24,
lingmu: (rng.next() - 0.5) * 0.24,
shoupi: (rng.next() - 0.5) * 0.24,
lingguo: (rng.next() - 0.5) * 0.24,
'pill-qiyuan': (rng.next() - 0.5) * 0.18, 'pill-qiyuan': (rng.next() - 0.5) * 0.18,
'pill-ningyuan': (rng.next() - 0.5) * 0.18 'pill-ningyuan': (rng.next() - 0.5) * 0.18
} }
@@ -82,6 +82,10 @@ export const POOL_BASE: Record<string, number> = {
lingcao: 600, lingcao: 600,
lingkuang: 300, lingkuang: 300,
beastcore: 80, beastcore: 80,
lingyu: 120,
lingmu: 220,
shoupi: 160,
lingguo: 260,
'pill-qiyuan': 90, 'pill-qiyuan': 90,
'pill-ningyuan': 40 'pill-ningyuan': 40
} }
+7 -4
View File
@@ -18,7 +18,10 @@ export default function MarketPanel() {
const fam = w.state.family const fam = w.state.family
const inv = fam.inventory const inv = fam.inventory
const goods = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'resource' || i.kind === 'pill'), []) const goods = useMemo(
() => Object.values(ITEMS).filter((i) => ['resource', 'material', 'pill', 'talisman', 'brew', 'artifact'].includes(i.kind)),
[]
)
const artifacts = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'artifact'), []) const artifacts = useMemo(() => Object.values(ITEMS).filter((i) => i.kind === 'artifact'), [])
const marketTech = TECHNIQUES.filter((t) => t.grade <= 3) const marketTech = TECHNIQUES.filter((t) => t.grade <= 3)
const cangshuLv = fam.buildings['cangshu'] ?? 0 const cangshuLv = fam.buildings['cangshu'] ?? 0
@@ -50,10 +53,10 @@ export default function MarketPanel() {
</thead> </thead>
<tbody> <tbody>
{[...goods, ...artifacts].map((item) => { {[...goods, ...artifacts].map((item) => {
const have = item.id === 'lingcao' || item.id === 'lingkuang' || item.id === 'beastcore' || item.kind === 'pill' || item.kind === 'artifact' const have = ['resource', 'material', 'pill', 'talisman', 'brew', 'artifact'].includes(item.kind)
? inv[item.id] ?? 0 ? inv[item.id] ?? 0
: 0 : 0
const stock = item.kind === 'artifact' ? 3 : item.kind === 'pill' ? 5 : 9999 const stock = item.kind === 'artifact' ? 3 : ['pill', 'talisman', 'brew'].includes(item.kind) ? 5 : 9999
const count = item.kind === 'artifact' ? 1 : 5 const count = item.kind === 'artifact' ? 1 : 5
return ( return (
<tr key={item.id}> <tr key={item.id}>
@@ -75,7 +78,7 @@ export default function MarketPanel() {
> >
{count} {count}
</button>{' '} </button>{' '}
{((item.kind === 'resource' || item.kind === 'pill') && have >= count) && ( {(['resource', 'material', 'pill'].includes(item.kind) && have >= count) && (
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => { onClick={() => {
+1 -1
View File
@@ -286,7 +286,7 @@ export default function SettingsPanel() {
· <br /> · <br />
· ·
</div> </div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.31 · Chronicle of the Immortal Clan</div> <div className="dim2" style={{ marginTop: 8 }}> 0.1.32 · Chronicle of the Immortal Clan</div>
</div> </div>
</div> </div>
) )
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
}) })
it('防御性修补后金钟罩不变(行为等价确认)', () => { it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('778d1488')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('00ac5fa4')
}) })
}) })
+8 -8
View File
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/ */
// 0.1.30 兼济基线:人口-世界成本闭环(人多粮少)/世界关注度(匹夫无罪) // 0.1.32 万宝琳琅基线:物品六类化/新材料(灵玉灵木兽皮灵果入池)/
// + 内置 MOD 仓库/冲突矩阵 UI/预览升级后固化(生灭/经济/战斗受控变更) // 生产泛化(produceTable 全键)/武备符箓/灵酿回气/材料相关漂移后固化
const GOLDEN: Record<string, Record<number, string>> = { const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: '5670331a', 1200: 'e0a65c40', 2160: 'c8fe205a' }, 'bell-seed-1': { 560: '778d1488', 1200: 'a098f660', 2160: 'bc511374' },
'bell-seed-2': { 560: '8d0b60ef', 1200: '08e60d00', 2160: 'd13f1b64' }, 'bell-seed-2': { 560: 'dfab016c', 1200: 'dc3d843f', 2160: '7096cf66' },
'bell-seed-3': { 560: 'bb5fd1e2', 1200: '80ddba5a', 2160: '37794cb0' } 'bell-seed-3': { 560: '00ac5fa4', 1200: 'e2278a35', 2160: '173d2770' }
} }
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件; /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = { const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'fc002722', 1200: '7e526f0e', 2160: '3625b13b' }, 'bell-seed-1': { 560: '9c603274', 1200: 'e8ada769', 2160: '3e632c02' },
'bell-seed-2': { 560: '3e9698e7', 1200: '68940d48', 2160: '9036b57b' }, 'bell-seed-2': { 560: '3c952764', 1200: '48e457fd', 2160: '6caa2e3e' },
'bell-seed-3': { 560: 'f8d10c28', 1200: '2c4c86ae', 2160: 'caa382fc' } 'bell-seed-3': { 560: 'de77eb9f', 1200: '2d4a1039', 2160: '41cf5c22' }
} }
const TIERS = [ const TIERS = [
+1 -1
View File
@@ -17,7 +17,7 @@ describe('物品表逐行矩阵', () => {
(_id, name, price, kind) => { (_id, name, price, kind) => {
expect(name.length).toBeGreaterThan(0) expect(name.length).toBeGreaterThan(0)
expect(price).toBeGreaterThan(0) expect(price).toBeGreaterThan(0)
expect(['resource', 'pill', 'artifact']).toContain(kind) expect(['resource', 'material', 'pill', 'talisman', 'brew', 'artifact']).toContain(kind)
} }
) )
+3 -3
View File
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
const f = new GameFacade(w, 1) const f = new GameFacade(w, 1)
const info = f.about() const info = f.about()
expect(info.title).toBe('仙途家族志') expect(info.title).toBe('仙途家族志')
expect(info.version).toContain('0.1.31') expect(info.version).toContain('0.1.32')
expect(info.modules).toBeGreaterThanOrEqual(11) expect(info.modules).toBeGreaterThanOrEqual(11)
expect(info.systems).toBeGreaterThan(0) expect(info.systems).toBeGreaterThan(0)
expect(info.plugins).toBeGreaterThanOrEqual(3) expect(info.plugins).toBeGreaterThanOrEqual(3)
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
it('默认配置金钟罩不受门面化影响', () => { it('默认配置金钟罩不受门面化影响', () => {
PACK.reset() PACK.reset()
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('5670331a') expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('778d1488')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('bb5fd1e2') expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('00ac5fa4')
}) })
}) })
+1 -1
View File
@@ -13,7 +13,7 @@ import { MAJORS } from '../src/renderer/game/data/realms'
describe('矩阵:物品表全量', () => { describe('矩阵:物品表全量', () => {
it.each(Object.values(ITEMS))('%s:三件套字段合法', (item) => { it.each(Object.values(ITEMS))('%s:三件套字段合法', (item) => {
expect(item.id.length).toBeGreaterThan(0) expect(item.id.length).toBeGreaterThan(0)
expect(['resource', 'pill', 'artifact']).toContain(item.kind) expect(['resource', 'material', 'pill', 'talisman', 'brew', 'artifact']).toContain(item.kind)
expect(item.basePrice).toBeGreaterThan(0) expect(item.basePrice).toBeGreaterThan(0)
expect(item.icon.length).toBeGreaterThan(0) expect(item.icon.length).toBeGreaterThan(0)
expect(item.name.length).toBeGreaterThan(0) expect(item.name.length).toBeGreaterThan(0)
+99
View File
@@ -0,0 +1,99 @@
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.32 矩阵:琳琅甲组', () => {
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 3] as const))(
'seed %d:辰1·三验', (seed) => {
const w = mk(`琳琅甲组-0-${seed}`)
for (let m = 0; m < 6; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 4] as const))(
'seed %d:辰2·三验', (seed) => {
const w = mk(`琳琅甲组-1-${seed}`)
for (let m = 0; m < 12; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 5] as const))(
'seed %d:辰3·三验', (seed) => {
const w = mk(`琳琅甲组-2-${seed}`)
for (let m = 0; m < 18; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 6] as const))(
'seed %d:辰4·三验', (seed) => {
const w = mk(`琳琅甲组-3-${seed}`)
for (let m = 0; m < 24; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 7] as const))(
'seed %d:辰5·三验', (seed) => {
const w = mk(`琳琅甲组-4-${seed}`)
for (let m = 0; m < 30; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 8] as const))(
'seed %d:辰6·三验', (seed) => {
const w = mk(`琳琅甲组-5-${seed}`)
for (let m = 0; m < 36; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 9] as const))(
'seed %d:辰7·三验', (seed) => {
const w = mk(`琳琅甲组-6-${seed}`)
for (let m = 0; m < 42; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 10] as const))(
'seed %d:辰8·三验', (seed) => {
const w = mk(`琳琅甲组-7-${seed}`)
for (let m = 0; m < 48; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 11] as const))(
'seed %d:辰9·三验', (seed) => {
const w = mk(`琳琅甲组-8-${seed}`)
for (let m = 0; m < 54; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 12] as const))(
'seed %d:辰10·三验', (seed) => {
const w = mk(`琳琅甲组-9-${seed}`)
for (let m = 0; m < 60; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(Array.isArray(w.state.chronicle)).toBe(true)
expect(Number.isFinite(w.state.family.stones)).toBe(true)
})
})
+99
View File
@@ -0,0 +1,99 @@
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.32 矩阵:琳琅乙组', () => {
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 303] as const))(
'seed %d:辰11·四验', (seed) => {
const w = mk(`琳琅乙组-0-${seed}`)
for (let m = 0; m < 9; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 304] as const))(
'seed %d:辰12·四验', (seed) => {
const w = mk(`琳琅乙组-1-${seed}`)
for (let m = 0; m < 18; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 305] as const))(
'seed %d:辰13·四验', (seed) => {
const w = mk(`琳琅乙组-2-${seed}`)
for (let m = 0; m < 27; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 306] as const))(
'seed %d:辰14·四验', (seed) => {
const w = mk(`琳琅乙组-3-${seed}`)
for (let m = 0; m < 36; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 307] as const))(
'seed %d:辰15·四验', (seed) => {
const w = mk(`琳琅乙组-4-${seed}`)
for (let m = 0; m < 45; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 308] as const))(
'seed %d:辰16·四验', (seed) => {
const w = mk(`琳琅乙组-5-${seed}`)
for (let m = 0; m < 54; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 309] as const))(
'seed %d:辰17·四验', (seed) => {
const w = mk(`琳琅乙组-6-${seed}`)
for (let m = 0; m < 63; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 310] as const))(
'seed %d:辰18·四验', (seed) => {
const w = mk(`琳琅乙组-7-${seed}`)
for (let m = 0; m < 72; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
it.each(Array.from({ length: 50 }, (_, k) => [k * 3 + 311] as const))(
'seed %d:辰19·四验', (seed) => {
const w = mk(`琳琅乙组-8-${seed}`)
for (let m = 0; m < 81; m++) w.advanceMonth()
expect(Number.isFinite(w.state.seq)).toBe(true)
expect(w.state.family.inventory).toBeTruthy()
expect(w.state.stats).toBeTruthy()
expect(Number.isFinite(w.state.totalTicks)).toBe(true)
})
})
+1 -1
View File
@@ -45,7 +45,7 @@ describe('0.1.24 寰宇初构·世界种子', () => {
it('开局时界(era 三态合法)与市场偏移存在', () => { it('开局时界(era 三态合法)与市场偏移存在', () => {
const g = generateWorld('wera') const g = generateWorld('wera')
expect(['shengshi', 'pingshi', 'luanshi']).toContain(g.eras) expect(['shengshi', 'pingshi', 'luanshi']).toContain(g.eras)
expect(Object.keys(g.marketOffset).length).toBe(5) expect(Object.keys(g.marketOffset).length).toBe(9)
}) })
it('世界创建即随机:同 seed 同势力、不同 seed 不同势力', () => { it('世界创建即随机:同 seed 同势力、不同 seed 不同势力', () => {