【主线一:世界自演进深化《八方风雨》】 - 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/超卖/连坐/调停/求援)
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { seasonOf } from '../src/renderer/game/data/season'
|
|
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)(
|
|
'月%i → %s',
|
|
(m, s) => {
|
|
expect(seasonOf(m)).toBe(s)
|
|
}
|
|
)
|
|
|
|
it.each([[1, '立春'], [2, '雨水'], [4, '清明'], [12, '冬至'], [12, '小寒']])('节气 24 枚全在', () => {
|
|
expect(TIDES.length).toBe(24)
|
|
})
|
|
|
|
it.each([1, 6, 12].map((m) => [m] as const))('年环 %i 月在 [0,1]', (m) => {
|
|
const r = yearRing(m)
|
|
expect(r).toBeGreaterThanOrEqual(0)
|
|
expect(r).toBeLessThan(1)
|
|
})
|
|
|
|
it('季相色均出自金碧墨色系(accent/tint 非空且不为霓虹)', () => {
|
|
for (const s of ['spring', 'summer', 'autumn', 'winter'] as const) {
|
|
const t = seasonTint(s)
|
|
expect(t.accent).toMatch(/^#[0-9a-f]{6}$/i)
|
|
expect(t.glow).toMatch(/^\d+, \d+, \d+$/)
|
|
expect(t.name.length).toBeGreaterThan(0)
|
|
const vars = seasonCssVars(s)
|
|
expect(vars['--season-accent']).toBe(t.accent)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('FxGate 竞态安全', () => {
|
|
function freshGate() {
|
|
const emitted: FxKind[] = []
|
|
const gate = new FxGate({ emit: (e) => emitted.push(e.kind) })
|
|
return { gate, emitted }
|
|
}
|
|
|
|
it('std 模式:spark/mist/ripple/pulse 允许,blade 需 full', () => {
|
|
const { gate, emitted } = freshGate()
|
|
gate.emit('spark')
|
|
gate.emit('mist')
|
|
gate.emit('ripple')
|
|
gate.emit('pulse')
|
|
gate.emit('blade')
|
|
expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse'])
|
|
expect(emitted).not.toContain('blade')
|
|
})
|
|
|
|
it('soft 模式:仅 ripple/pulse/drift', () => {
|
|
const { gate, emitted } = freshGate()
|
|
gate.setMode('soft')
|
|
gate.emit('spark')
|
|
gate.emit('ripple')
|
|
gate.emit('drift')
|
|
expect(emitted).toEqual(['ripple', 'drift'])
|
|
})
|
|
|
|
it('prefers-reduced 时全发抑制(降级)', () => {
|
|
const { gate, emitted } = freshGate()
|
|
gate.setReduced(true)
|
|
gate.emit('spark')
|
|
gate.emit('pulse')
|
|
expect(emitted).toEqual([])
|
|
})
|
|
|
|
it('重入安全:队列 drain 期间再 emit 不丢不叠(单帧批处理语义)', () => {
|
|
const { gate, emitted } = freshGate()
|
|
gate.emit('spark')
|
|
gate.emit('spark')
|
|
gate.emit('spark')
|
|
expect(emitted.length).toBe(3)
|
|
expect(gate.pending()).toBe(0)
|
|
})
|
|
|
|
it('deltas:差异、净零、NaN 滤除', () => {
|
|
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 原值被过滤
|
|
expect(d.d).toBe(3)
|
|
})
|
|
|
|
it('deltas 空对象不产生输出', () => {
|
|
expect(Object.keys(deltas({}, {}))).toHaveLength(0)
|
|
})
|
|
})
|