v0.1.19: 八方风雨 + 孤立系统归并(双主线,1002 全绿)

【主线一:世界自演进深化《八方风雨》】
- NPC 战略姿态 stance:守成/扩张/隐忍/结盟四态,年首评估(era>景气>power>relation)
  全行为调制:raid 概率×2/×0.4/×0、互攻阈值 -40/-70/不攻、贸量 ×1.3/×0.7、
  赠礼收益 ally×1.3、结盟门槛 ally 放宽至30——NPC 从统计物理到行为战略
- 超卖潮(谷贱伤农):池>1.8×base 世界产能回调×0.7 + 年度播报(防玩家玩崩市场)
- 盟约连坐:结盟令世仇(rel<-50)对玩家-20;盟者世仇 raid 概率×2
- 盟友求援:互攻失利盟友告急(distress 旗标)——WorldPanel 响应(出资200/见死不救)
- 干预天下:调停(50灵石令世仇-60→-25、声望+2)/ 资助盟友;恩怨网情报视图(各家关系展开+调停钮)

【主线二:孤立系统归并引擎】
- P0 兜底修复:empty() 收敛 makeWorldSimState 单源 + normalize 补 tideTicks/calamityLeft/
  era/eraStartYear/prosperity/distress/overfluxYear(旧档 NaN 级联防死)
- FxGate 从 engine/kernel/fxqueue.ts 并入 ui/fx.ts(kernel 清纯引擎;deltas 顶层导出,
  3 处引用 10 行改动);engine 侧零 UI 依赖再下一城
- 音效语义补充:onFx→blade/pulse/spark→sWar/sGong/sBell;onLog kind 兜底保留(不静音)
- store.advance drift/season 段左移 fx.ts(fxSeason 复活接管 season 检测)
- 死码清除:timesense.tideOf/tintLabel、tideDir 全链(类型/初始/写入/domain)
- domain.ts worldSim 局部类型补全 0.1.18/0.1.19 全部新字段(断言类型安全)

【测试】1002 全绿(41 套件):stance-world 7 例(兜底/姿态/raid/超卖/连坐/调停/求援)
This commit is contained in:
2026-08-23 14:53:46 +08:00
parent 2f62eecb08
commit e853fbdb71
20 changed files with 420 additions and 139 deletions
+2 -2
View File
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
})
it('防御性修补后金钟罩不变(行为等价确认)', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d07ebe85')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ecdc62da')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
})
})
+5 -5
View File
@@ -7,12 +7,12 @@ import { World } from '../src/renderer/game/engine/runtime/World'
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
*/
// 0.1.18 时代绘卷基线:世纪弧 era 状态机(四态/调制)+ NPC 景气 prosperity
// + 蝴蝶效应(互攻加权/打残广播/玩家行动写关系网)+ 十年一鉴后固化。
// 0.1.19 八方风雨基线:NPC 战略姿态(stance 四态+年首重估+全行为调制)
// + 超卖潮(谷贱伤农产能回调)+ 盟约连坐/求援 + 调停/资助干预后固化。
const GOLDEN: Record<string, Record<number, string>> = {
'bell-seed-1': { 560: 'd07ebe85', 1200: '32d9267f', 2160: '046967c9' },
'bell-seed-2': { 560: '3fcec396', 1200: '3c40f605', 2160: 'b3ec6229' },
'bell-seed-3': { 560: 'ecdc62da', 1200: '562bd778', 2160: '6ebb02a3' }
'bell-seed-1': { 560: 'aeed3b65', 1200: '0cbd4079', 2160: '7d1efba9' },
'bell-seed-2': { 560: '6bb6ff89', 1200: 'a73b5655', 2160: 'f3537f46' },
'bell-seed-3': { 560: '39892725', 1200: '4b69c491', 2160: '950e8410' }
}
const TIERS = [
+3 -3
View File
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
const f = new GameFacade(w, 1)
const info = f.about()
expect(info.title).toBe('仙途家族志')
expect(info.version).toContain('0.1.17')
expect(info.version).toContain('0.1.19')
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('d07ebe85')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ecdc62da')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
})
})
+4 -9
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { seasonOf } from '../src/renderer/game/data/season'
import { tideOf, yearRing, seasonTint, seasonCssVars, TIDES } from '../src/renderer/game/engine/kernel/timesense'
import { FxGate, FxKind } from '../src/renderer/game/engine/kernel/fxqueue'
import { yearRing, seasonTint, seasonCssVars, TIDES } from '../src/renderer/game/engine/kernel/timesense'
import { FxGate, FxKind, deltas } from '../src/renderer/ui/fx'
describe('timesense 时间感知矩阵', () => {
it.each([[1, 'spring'], [3, 'spring'], [4, 'summer'], [6, 'summer'], [7, 'autumn'], [9, 'autumn'], [10, 'winter'], [12, 'winter']] as const)(
@@ -15,11 +15,6 @@ describe('timesense 时间感知矩阵', () => {
expect(TIDES.length).toBe(24)
})
it.each([1, 2, 4, 6, 9, 11, 12].map((m) => [m] as const))('月%i 两个节气均有效', (m) => {
expect(TIDES).toContain(tideOf(m, 0))
expect(TIDES).toContain(tideOf(m, 1))
})
it.each([1, 6, 12].map((m) => [m] as const))('年环 %i 月在 [0,1]', (m) => {
const r = yearRing(m)
expect(r).toBeGreaterThanOrEqual(0)
@@ -83,7 +78,7 @@ describe('FxGate 竞态安全', () => {
})
it('deltas:差异、净零、NaN 滤除', () => {
const d = FxGate.deltas({ a: 10, b: 5, c: NaN }, { a: 14, b: 5, d: 3 })
const d = deltas({ a: 10, b: 5, c: NaN }, { a: 14, b: 5, d: 3 })
expect(d.a).toBe(4)
expect(d.b).toBeUndefined()
expect(d.c).toBeUndefined() // NaN 原值被过滤
@@ -91,6 +86,6 @@ describe('FxGate 竞态安全', () => {
})
it('deltas 空对象不产生输出', () => {
expect(Object.keys(FxGate.deltas({}, {}))).toHaveLength(0)
expect(Object.keys(deltas({}, {}))).toHaveLength(0)
})
})
+3 -3
View File
@@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => {
})
it('默认管线金钟罩不受插件层影响', () => {
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('d07ebe85')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('3fcec396')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('ecdc62da')
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('6bb6ff89')
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
})
it('facade 插件查询与 about.plugins', () => {
+109
View File
@@ -0,0 +1,109 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { normalizeGameState } from '../src/renderer/game/engine/runtime/World'
import { WorldSimState } from '../src/renderer/game/engine/sim/worldsim-data'
describe('0.1.19 八方风雨', () => {
it('兜底:空 worldSim 的旧档 normalize 后 tideTicks 有值(P0 修复)', () => {
const w = World.create({ seed: 'p0-1', surname: '甯', familyName: '甯家', motto: 'm', difficulty: 'normal' })
const state = w.state as never
const fixed = normalizeGameState({
...(state as Record<string, unknown>),
worldSim: { marketPool: {}, npcDyn: {}, secretQi: {}, newsFeed: [] }
} as never) as { worldSim: { tideTicks: number; era: string; calamityLeft: number } }
expect(fixed.worldSim.tideTicks).toBe(0)
expect(fixed.worldSim.era).toBe('pingshi')
expect(fixed.worldSim.calamityLeft).toBe(0)
})
it('stance:四姿态值域 + 年首(约8年)可再评估', () => {
const w = World.create({ seed: 'st-1', surname: '冀', familyName: '冀家', motto: 'm', difficulty: 'normal' })
const seen = new Set<string>()
for (let i = 0; i < 1600; i++) {
if (w.state.gameOver) break
w.advanceMonth()
const dyns = (w.state.worldSim as WorldSimState).npcDyn
for (const d of Object.values(dyns)) {
expect(['guardian', 'expand', 'endure', 'ally']).toContain(d.stance ?? 'guardian')
if (d.stance) seen.add(d.stance)
}
}
expect(seen.size).toBeGreaterThanOrEqual(2)
})
it('姿态影响 raid 概率标尺(expand 双倍、endure 打折)', () => {
const w = World.create({ seed: 'st-2', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei']!, stance: 'expand', stanceSinceYear: 1 }
// 关系拉到仇深 + 伪造 raidCD 前一年,跑 240 月计数
const npc = w.state.npcFamilies['n-nulei']
npc.relation = -70
npc.allied = false
let evcount = 0
for (let i = 0; i < 600; i++) {
npc.relation = -70 // 钉死(diplomacy drift 会拉回)
dyns.npcDyn['n-nulei'] = { ...dyns.npcDyn['n-nulei'], stance: 'expand', stanceSinceYear: w.state.year }
w.advanceMonth()
// 任何 pending 顶住事件闸则先清场(否则后续 raid 无法 fire)
while (w.state.pendingEvent) {
if (w.state.pendingEvent.startsWith('ev-raid-')) evcount++
w.applyEventChoice(w.state.pendingEvent, 0)
}
}
// 0.06×12月×10年 ≈ 7 次期待,≥1 稳
expect(evcount).toBeGreaterThanOrEqual(1)
})
it('超卖潮:库存过剩时世界产能乘0.7回调且播报一次/年', () => {
const w = World.create({ seed: 'st-3', surname: '盛', familyName: '盛家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const ws = w.state.worldSim as WorldSimState
ws.marketPool['lingcao'] = 2000 // 远超 600*1.8
const before = ws.marketPool['lingcao']
w.advanceMonth()
const after = ws.marketPool['lingcao']
expect(after).toBeLessThan(before)
expect(ws.newsFeed.some((r) => r.text.includes('谷贱伤农'))).toBe(true)
})
it('结盟连坐:世仇得闻而暗恨(玩家关系-20)', () => {
const w = World.create({ seed: 'st-4', surname: '方', familyName: '方家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-xuanying']!.relationsWithOthers['n-nulei'] = -70
w.state.npcFamilies['n-xuanying'].relation = 60
const before = w.state.npcFamilies['n-nulei'].relation
expect(w.setAlliance('n-xuanying', true)).toBe(true)
expect(w.state.npcFamilies['n-nulei'].relation).toBeLessThan(before)
})
it('调停:花50灵石令世仇回暖、声望+2', () => {
const w = World.create({ seed: 'st-5', surname: '凌', familyName: '凌家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const dyns = w.state.worldSim as WorldSimState
dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai'] = -60
w.state.family.stones = 500
const r0 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
expect(w.mediateNpcs('n-danxin', 'n-sihai')).toBe(true)
const r1 = dyns.npcDyn['n-danxin']!.relationsWithOthers['n-sihai']
expect(r1).toBeGreaterThan(r0)
expect(w.state.family.reputation).toBeGreaterThanOrEqual(2)
})
it('盟友求援:互攻失利盟友告急 → assistAlly 可应', () => {
const w = World.create({ seed: 'st-6', surname: '古', familyName: '古家', motto: 'm', difficulty: 'normal' })
w.advanceMonth()
const ws = w.state.worldSim as WorldSimState
ws.npcDyn['n-xuanying']!.relationsWithOthers['n-nulei'] = -80
ws.npcDyn['n-nulei']!.relationsWithOthers['n-xuanying'] = -80
w.state.npcFamilies['n-xuanying'].allied = true
w.state.family.stones = 1000
// 手动挂求援(互攻触发具随机性,此处测响应链)
ws.distress = { id: 'n-xuanying', year: w.state.year, month: w.state.month }
const p0 = w.state.npcFamilies['n-xuanying'].power
expect(w.assistAlly('n-xuanying')).toBe(true)
expect(w.state.npcFamilies['n-xuanying'].power).toBeGreaterThan(p0)
expect(ws.distress).toBeUndefined()
})
})