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
+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()
})
})