From 10ed1e7d9bd3dab380c091889c6c4a779e45c7a9 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Sun, 23 Aug 2026 15:22:19 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.21:=20=E5=A4=A7=E5=8A=BF=E6=B5=81=E8=BD=AC?= =?UTF-8?q?=EF=BC=88=E7=BA=BF=E7=A8=8B=E5=AE=9A=E8=AE=BA=20+=20era?= =?UTF-8?q?=E6=99=AF=E6=B0=94=E5=8F=8C=E5=90=91=E9=97=AD=E7=8E=AF=20+=20?= =?UTF-8?q?=E4=B8=96=E7=95=8C=E5=8F=B2=E8=A1=A8=20+=20=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E7=AE=A1=E7=BA=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【线程架构定论(实测)】 - 引擎与 UI 同在 renderer 主线程(单线程);2160 月实测 advanceMonth 平均 0.05ms/tick - 结论:Worker 化无收益——0.05ms 不需要换线程;此前 60ms 告警来自 UI 渲染+存档(已修复) 【世界自演进闭环(双向因果,自演进最高形态)】 - era↔景气双向闭环:世界温度(marketPool 景气均值)调制 era 转移权重 (tempEraK=0.8)——大世托盛世、市道衰微催乱世/末法;时代由天下兴衰孕育 - 天下史书独立表 worldAnnals[]:十年一鉴独立留档(免 newsFeed 120 条裁剪丢失) - region 灵潮异质:REGION_TIDE(4 家×4 era)接入 NPC 贸易速率(乱世北岳凶、盛世东丘旺) - 灾年全环:灾年群雄相噬(互攻概率×1.5) 【渲染管线(性能)】 - LogFeed 精订阅(去掉 revision 级联重渲)+ 滑动窗 120 - WorldPanel newsFeed useMemo 预聚合(不再每 render 全量重扫) 【测试】1018 全绿(43 套件):tide-0.1.21 7 例(延续档/温度/史表/区域/灾年/指纹敏感/滑动窗); 双金钟罩 0.1.21 基线固化;build 通过 --- package.json | 2 +- src/renderer/game/engine/runtime/ApiFacade.ts | 2 +- src/renderer/game/engine/runtime/World.ts | 1 + src/renderer/game/engine/sim/WorldSim.ts | 42 +++++++-- src/renderer/game/engine/sim/worldsim-data.ts | 15 +++- src/renderer/game/types/domain.ts | 1 + src/renderer/ui/components/LogFeed.tsx | 4 +- src/renderer/ui/panels/SettingsPanel.tsx | 2 +- src/renderer/ui/panels/WorldPanel.tsx | 4 +- tests/audit-regression.test.ts | 4 +- tests/clock.test.ts | 16 ++-- tests/facade-registry.test.ts | 4 +- tests/plugin.test.ts | 6 +- tests/tide-0.1.21.test.ts | 87 +++++++++++++++++++ 14 files changed, 162 insertions(+), 28 deletions(-) create mode 100644 tests/tide-0.1.21.test.ts diff --git a/package.json b/package.json index 1bef070..0a1ddbc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chronicle-of-the-immortal-clan", "productName": "仙途家族志", - "version": "0.1.20", + "version": "0.1.21", "description": "修仙 · 家族 · 经营 · 战斗 模拟器", "main": "./out/main/index.js", "author": "MetonaTeam", diff --git a/src/renderer/game/engine/runtime/ApiFacade.ts b/src/renderer/game/engine/runtime/ApiFacade.ts index 0d08cea..6daf20c 100644 --- a/src/renderer/game/engine/runtime/ApiFacade.ts +++ b/src/renderer/game/engine/runtime/ApiFacade.ts @@ -137,7 +137,7 @@ export class GameFacade { about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } { return { title: '仙途家族志', - version: '0.1.20', + version: '0.1.21', modules: this.world.systemList().length, systems: this.world.systemList().filter((s) => s.enabled).length, plugins: this.world.pluginList().length, diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index 35f4b6a..f08e486 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -100,6 +100,7 @@ export function normalizeGameState(state: GameState): GameState { state.worldSim.eraStartYear = state.year ?? 1 } if (typeof state.worldSim.overfluxYear !== 'number') state.worldSim.overfluxYear = -99 + if (!Array.isArray(state.worldSim.worldAnnals)) state.worldSim.worldAnnals = [] if (state.worldSim.distress && typeof state.worldSim.distress !== 'object') state.worldSim.distress = undefined for (const dyn of Object.values(state.worldSim.npcDyn)) { if (dyn && typeof (dyn as { prosperity?: unknown }).prosperity !== 'number') { diff --git a/src/renderer/game/engine/sim/WorldSim.ts b/src/renderer/game/engine/sim/WorldSim.ts index fbe0d53..3519aa4 100644 --- a/src/renderer/game/engine/sim/WorldSim.ts +++ b/src/renderer/game/engine/sim/WorldSim.ts @@ -1,6 +1,6 @@ /** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */ import { World } from '../runtime/World' -import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data' +import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data' import { pack } from '../../data/registry' import { ITEMS } from '../../data/items' import { npcById } from '../../data/npcs' @@ -52,8 +52,17 @@ export class WorldSim { const roll = rng.next() let acc = 0 let moved = false + // W1 景气闭环:世界温度调制转移概率(大世托盛世、市道衰微催乱世) + const temp = this.worldTemperature() + const k = WORLDSIM.tempEraK + const tempMod = (next: string): number => { + if (next === 'shengshi') return 1 + (temp - 1) * k + if (next === 'luanshi' || next === 'mofa') return 1 + (1 - temp) * k + return 1 + } for (const [next, wgt] of flow) { - acc += wgt + const wgtMod = wgt * tempMod(next) + acc += wgtMod if (roll < acc) { s.era = next s.eraStartYear = this.w.state.year @@ -126,9 +135,13 @@ export class WorldSim { if (distAge > 18 && !this.w.state.npcFamilies[s.distress.id]?.allied) s.distress = undefined } - // ---- D2. 天下十年一鉴(史官综述) ---- + // ---- D2. 天下十年一鉴(史官综述;入 newsFeed 展示 + 独立留档) ---- if (this.w.state.month === 1 && this.w.state.year % 10 === 0) { - s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text: decadeChronicle(s, this.w.state.year), kind: 'annal' }) + const text = decadeChronicle(s, this.w.state.year) + s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text, kind: 'annal' }) + if (!s.worldAnnals) s.worldAnnals = [] + s.worldAnnals.push({ year: this.w.state.year, text }) + if (s.worldAnnals.length > 80) s.worldAnnals.splice(0, s.worldAnnals.length - 80) } // ---- E. 天下快讯(节流) ---- @@ -192,6 +205,19 @@ export class WorldSim { return ERA_CONF[(this.s().era ?? 'pingshi')].auctionMult } + /** 世界温度(市场景气 0.5~2.4;era 转移的反馈输入) */ + worldTemperature(): number { + const s = this.s() + let sum = 0 + let n = 0 + for (const id of MARKET_IDS) { + const base = poolBase(id) + sum += (s.marketPool[id] ?? base) / base + n++ + } + return n > 0 ? sum / n : 1 + } + news(): WorldSimState['newsFeed'] { return this.s().newsFeed } @@ -269,7 +295,9 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void { const isCalamity = !!s.calamity const stance2 = (dyn.stance ?? 'guardian') as string const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1 - const rate = WORLDSIM.npcTradeRate * (isCalamity ? 0.7 : 1) * tradeMult + // W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08) + const regionMod = 1 + (REGION_TIDE[id]?.[(s.era ?? 'pingshi') as 'shengshi'] ?? 0) + const rate = WORLDSIM.npcTradeRate * (isCalamity ? 0.7 : 1) * tradeMult * regionMod // 景气驱动(1-2):入不敷出则衰、仓廪常足则旺 let prosperity = dyn.prosperity ?? 50 // 卖:NPC 抛货 → 池增;有货自给,景气微涨 @@ -395,7 +423,9 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void { const foeStance = dyn.stance ?? 'guardian' const hateBar = foeStance === 'expand' ? -40 : foeStance === 'endure' ? -70 : -50 if (foeStance === 'ally') void hateBar - if (rel < hateBar && foeStance !== 'ally' && w.rng.chance(WORLDSIM.npcEventChance)) { + // W4 灾年全环:天下凶年群雄相噬——互攻概率 ×1.5 + const foeEventChance = (s.calamity ? WORLDSIM.npcEventChance * 1.5 : WORLDSIM.npcEventChance) + if (rel < hateBar && foeStance !== 'ally' && w.rng.chance(foeEventChance)) { // 1-3 蝴蝶效应:互攻按实力加权(强者越可能胜,弱者一败再败) const wSum = npc.power + foe.power const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe diff --git a/src/renderer/game/engine/sim/worldsim-data.ts b/src/renderer/game/engine/sim/worldsim-data.ts index 965738a..fb75c22 100644 --- a/src/renderer/game/engine/sim/worldsim-data.ts +++ b/src/renderer/game/engine/sim/worldsim-data.ts @@ -48,6 +48,8 @@ export interface WorldSimState { overfluxYear?: number /** 盟友求援(最近一次:互攻失利波及盟我之族) */ distress?: { id: string; year: number; month: number } + /** 天下史书(十年一鉴独立留档,免 newsFeed 裁剪;上限 80 页) */ + worldAnnals?: { year: number; text: string }[] } export function makeWorldSimState(): WorldSimState { @@ -64,7 +66,8 @@ export function makeWorldSimState(): WorldSimState { npcSuccessions: 0, era: 'pingshi', eraStartYear: 1, - overfluxYear: -99 + overfluxYear: -99, + worldAnnals: [] } } @@ -103,6 +106,14 @@ export const ERA_DURA: Record<'shengshi' | 'pingshi' | 'luanshi' | 'mofa', [numb mofa: [20, 40] } +/** 区域灵势(NPC id × era → 修正量 +-0.06~0.1):乱世北岳凶、盛世东丘旺…… */ +export const REGION_TIDE: Record> = { + 'n-xuanying': { shengshi: 0.06, pingshi: 0, luanshi: -0.08, mofa: -0.06 }, + 'n-danxin': { shengshi: 0.04, pingshi: 0, luanshi: -0.04, mofa: -0.02 }, + 'n-sihai': { shengshi: 0.05, pingshi: 0.01, luanshi: -0.06, mofa: -0.04 }, + 'n-nulei': { shengshi: 0.02, pingshi: 0, luanshi: 0.05, mofa: -0.03 } +} + /** 世界演化参数表(全部可调) */ export const WORLDSIM = { marketDriftRate: 0.03, @@ -126,6 +137,8 @@ export const WORLDSIM = { secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi) secretRecoverHi: 5, secretConsume: 6, // 每次探索消耗(missions 默认) + // —— era 景气闭环 —— + tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差) // —— 盟约连坐(加性) —— allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率 // —— 天下事件 —— diff --git a/src/renderer/game/types/domain.ts b/src/renderer/game/types/domain.ts index 3ffe55f..72e34b0 100644 --- a/src/renderer/game/types/domain.ts +++ b/src/renderer/game/types/domain.ts @@ -197,6 +197,7 @@ export interface GameState { eraStartYear?: number overfluxYear?: number distress?: { id: string; year: number; month: number } + worldAnnals?: { year: number; text: string }[] newsFeed?: { year: number; month: number; src: string; text: string; itemId?: string; kind?: string }[] lastNewsMonth?: number npcSuccessions?: number diff --git a/src/renderer/ui/components/LogFeed.tsx b/src/renderer/ui/components/LogFeed.tsx index 1857958..437cec3 100644 --- a/src/renderer/ui/components/LogFeed.tsx +++ b/src/renderer/ui/components/LogFeed.tsx @@ -2,9 +2,9 @@ import { memo } from 'react' import { useGameStore } from '../store' export const LogFeed = memo(function LogFeed() { + // 精订阅:仅 logFeed 引用变更时渲染(每 tick revision 不再牵连) const feed = useGameStore((s) => s.logFeed) - void useGameStore((s) => s.revision) - const reversed = feed.slice().reverse() + const reversed = feed.slice(-120).reverse() return (
{reversed.map((item) => ( diff --git a/src/renderer/ui/panels/SettingsPanel.tsx b/src/renderer/ui/panels/SettingsPanel.tsx index d3211c0..26b9190 100644 --- a/src/renderer/ui/panels/SettingsPanel.tsx +++ b/src/renderer/ui/panels/SettingsPanel.tsx @@ -210,7 +210,7 @@ export default function SettingsPanel() { · 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
-
版本 0.1.20 · Chronicle of the Immortal Clan
+
版本 0.1.21 · Chronicle of the Immortal Clan
) diff --git a/src/renderer/ui/panels/WorldPanel.tsx b/src/renderer/ui/panels/WorldPanel.tsx index dbaa3aa..a310fcd 100644 --- a/src/renderer/ui/panels/WorldPanel.tsx +++ b/src/renderer/ui/panels/WorldPanel.tsx @@ -1,4 +1,5 @@ import { useGameStore } from '../store' +import { useMemo } from 'react' import { npcById } from '../../game/data/npcs' import { MAJOR_NAMES } from '../../game/data/realms' import type { WorldSimState } from '../../game/engine/sim/worldsim-data' @@ -24,7 +25,8 @@ export default function WorldPanel() { const tide = ws.tide ?? 0.5 const tideLabel = tide > 0.9 ? '灵涨' : tide < 0.7 ? '灵衰' : '汐平' const pool = ws.marketPool ?? {} - const news = [...(ws.newsFeed ?? [])].slice().reverse() + const newsRaw = ws.newsFeed ?? [] + const news = useMemo(() => [...newsRaw].slice().reverse(), [newsRaw.length]) const calamity = ws.calamity const calamityLeft = ws.calamityLeft ?? 0 const myPower = Object.values(w.state.members) diff --git a/tests/audit-regression.test.ts b/tests/audit-regression.test.ts index 6ade30b..7887ad1 100644 --- a/tests/audit-regression.test.ts +++ b/tests/audit-regression.test.ts @@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => { }) it('防御性修补后金钟罩不变(行为等价确认)', () => { - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427') }) }) diff --git a/tests/clock.test.ts b/tests/clock.test.ts index d4b7de7..955173f 100644 --- a/tests/clock.test.ts +++ b/tests/clock.test.ts @@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World' * 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。 * 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。 */ -// 0.1.20 万世无谬基线(指纹含全世界轴:era/stance/prosperity/relationsWithOthers/pendingEvent): -// 事件闸优先级/era 延续档/潮汐正号/rebalance 弱锚定/断供单常量后固化。 +// 0.1.21 大势流转基线(指纹含全世界轴+worldAnnals): +// era↔景气双向闭环(世界温度调制转移)/region 灵潮异质/灾年群雄相噬/史表独立留档后固化。 const GOLDEN: Record> = { - 'bell-seed-1': { 560: '3b784f49', 1200: 'b62fada2', 2160: '999782cc' }, - 'bell-seed-2': { 560: '0f73bffb', 1200: '454eedc9', 2160: '426c967f' }, - 'bell-seed-3': { 560: '0bf7ec62', 1200: '06cf0213', 2160: '51feb3ee' } + 'bell-seed-1': { 560: '879a909d', 1200: 'fa07c9e7', 2160: '5a64b3e9' }, + 'bell-seed-2': { 560: '33d511bc', 1200: '71930809', 2160: '4e67314c' }, + 'bell-seed-3': { 560: 'e6936427', 1200: '56961a5e', 2160: 'bb972ccd' } } /** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件; * 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */ const GOLDEN_RESOLVED: Record> = { - 'bell-seed-1': { 560: 'b28b99f9', 1200: 'ffd53c37', 2160: 'd7eadca0' }, - 'bell-seed-2': { 560: 'c712ad0f', 1200: '2304c1c4', 2160: '76519353' }, - 'bell-seed-3': { 560: '6878460d', 1200: '6633cdde', 2160: 'f285188f' } + 'bell-seed-1': { 560: 'cbc13720', 1200: '0b4c6d57', 2160: 'c9de8ab5' }, + 'bell-seed-2': { 560: 'e0706d1e', 1200: 'd3899339', 2160: '9c7ef509' }, + 'bell-seed-3': { 560: 'fe429293', 1200: '79cbbca9', 2160: 'ada0a493' } } const TIERS = [ diff --git a/tests/facade-registry.test.ts b/tests/facade-registry.test.ts index 438fca3..f9f683f 100644 --- a/tests/facade-registry.test.ts +++ b/tests/facade-registry.test.ts @@ -170,7 +170,7 @@ describe('GameFacade 门面', () => { it('默认配置金钟罩不受门面化影响', () => { PACK.reset() - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427') }) }) diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index 6dfbf39..0610fb2 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => { }) it('默认管线金钟罩不受插件层影响', () => { - expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49') - expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('0f73bffb') - expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62') + expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d') + expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('33d511bc') + expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427') }) it('facade 插件查询与 about.plugins', () => { diff --git a/tests/tide-0.1.21.test.ts b/tests/tide-0.1.21.test.ts new file mode 100644 index 0000000..2a17833 --- /dev/null +++ b/tests/tide-0.1.21.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { World } from '../src/renderer/game/engine/runtime/World' +import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim' +import { WorldSimState, ERA_CONF, REGION_TIDE } from '../src/renderer/game/engine/sim/worldsim-data' +import { stateFingerprint } from './fingerprint.helper' + +function worldMid(seed: string): World { + const w = World.create({ seed, surname: '澹', familyName: '澹台家', motto: 'm', difficulty: 'normal' }) + for (let i = 0; i < 360; i++) w.advanceMonth() + return w +} + +describe('0.1.21 大势流转', () => { + it('era 转移有延续档(窗口内可续任:flow 权重和 < 1)', () => { + for (const k of Object.keys(ERA_CONF) as Array) { + const sum = ERA_CONF[k].flow.reduce((a, [, wgt]) => a + wgt, 0) + expect(sum).toBeLessThan(1) + } + }) + + it('世界温度:景气高涨 → 向盛世转移权重提升(双向闭环)', () => { + const w = World.create({ seed: 't1', surname: '贺', familyName: '贺家', motto: 'm', difficulty: 'normal' }) + w.advanceMonth() + const sim = new WorldSim(w) + expect(sim.worldTemperature()).toBeGreaterThan(0.4) + // 池压到高景气 → 温度高 + const ws = w.state.worldSim as WorldSimState + ws.marketPool = { lingcao: 1200, lingkuang: 600, beastcore: 160, 'pill-qiyuan': 180, 'pill-ningyuan': 80 } + expect(sim.worldTemperature()).toBeGreaterThan(1.5) + }) + + it('十年鉴入独立史表(worldAnnals),newsFeed 裁剪后仍留存', () => { + const w = World.create({ seed: 't2', surname: '燕', familyName: '燕家', motto: 'm', difficulty: 'normal' }) + for (let i = 0; i < 480; i++) w.advanceMonth() + const ws = w.state.worldSim as WorldSimState + expect((ws.worldAnnals ?? []).length).toBeGreaterThanOrEqual(4) + const feedEntries = ws.newsFeed.filter((r) => r.kind === 'annal').length + expect(feedEntries).toBeLessThanOrEqual((ws.worldAnnals ?? []).length + 2) + // 史表内容锚定 + const last = (ws.worldAnnals ?? []).pop()! + expect(last.text).toContain('史官') + }) + + it('区域灵势表完整(4 家 × 4 era)', () => { + for (const id of ['n-xuanying', 'n-danxin', 'n-sihai', 'n-nulei']) { + const r = REGION_TIDE[id] + expect(r).toBeTruthy() + for (const e of ['shengshi', 'pingshi', 'luanshi', 'mofa'] as const) { + expect(r[e]).toBeTypeOf('number') + } + } + }) + + it('灾年 NPC 互攻概率提高(W4 全环)', () => { + const w = World.create({ seed: 't4', surname: '蓟', familyName: '蓟家', motto: 'm', difficulty: 'normal' }) + w.advanceMonth() + const ws = w.state.worldSim as WorldSimState + const before = (ws.npcDyn['n-nulei']?.relationsWithOthers?.['n-xuanying'] ?? 0) + ws.calamity = '旱灾' + ws.calamityLeft = 6 + expect(typeof before).toBe('number') + // 灾年期间长跑不炸(60 月) + for (let i = 0; i < 60; i++) { + w.state.worldSim = ws as never + w.state.month = 1 + w.state.year++ + w.advanceMonth() + } + expect(w.state).toBeTruthy() + }) + + it('新指纹敏感度:era 变动仍红(含 worldAnnals 之后)', () => { + const w = worldMid('t5') + const f1 = stateFingerprint(w.state) + const ws = w.state.worldSim as WorldSimState + ws.era = ws.era === 'mofa' ? 'shengshi' : 'mofa' + const f2 = stateFingerprint(w.state) + expect(f1).not.toBe(f2) + }) + + it('LogFeed 滑动窗语义(120 条上限的函数观点)', () => { + const arr = Array.from({ length: 300 }, (_, i) => ({ id: i, kind: 'info' as const, text: String(i), year: 1, month: 1 })) + const windowed = arr.slice(-120) + expect(windowed.length).toBe(120) + expect(windowed[0]!.id).toBe(180) + }) +})