Files
ChronicleOfTheImmortalClan/tests/events.test.ts
T
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
2026-08-23 13:23:25 +08:00

146 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/runtime/Systems/events'
import { EVENTS } from '../src/renderer/game/data/events'
import { resetSaveBus, attachLogSink } from './world.helpers'
function baseWorld(seed: string): World {
const w = World.create({ seed, surname: '童', familyName: '童家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
return w
}
describe('event 条件矩阵', () => {
it('all/any/not 逻辑嵌套', () => {
const w = baseWorld('ev-m1')
const cond = { all: [{ minAdult: 2 }, { any: [{ minRep: 0 }, { maxRep: -1 }] }], not: { minBuilding: { id: 'danfang', level: 1 } } }
expect(matchesCond(w, cond)).toBe(true)
const bad = { all: [{ minAdult: 2 }], not: { minAdult: 1 } }
expect(matchesCond(w, bad)).toBe(false)
})
it('数值条件边界(>=/<', () => {
const w = baseWorld('ev-m2')
w.state.family.reputation = 30
expect(matchesCond(w, { minRep: 30 })).toBe(true)
expect(matchesCond(w, { minRep: 31 })).toBe(false)
expect(matchesCond(w, { maxRep: 30 })).toBe(true)
expect(matchesCond(w, { maxRep: 29 })).toBe(false)
})
it('资源与建筑条件', () => {
const w = baseWorld('ev-m3')
w.state.family.inventory['lingcao'] = 100
expect(matchesCond(w, { minResource: { id: 'lingcao', n: 50 } })).toBe(true)
expect(matchesCond(w, { minResource: { id: 'lingcao', n: 200 } })).toBe(false)
w.state.family.buildings = { danfang: 2 }
expect(matchesCond(w, { minBuilding: { id: 'danfang', level: 2 } })).toBe(true)
expect(matchesCond(w, { minBuilding: { id: 'danfang', level: 3 } })).toBe(false)
expect(matchesCond(w, { minBuilding: { id: 'lingshou', level: 1 } })).toBe(false)
})
it('关系条件和 flag 条件', () => {
const w = baseWorld('ev-m4')
const npc = w.state.npcFamilies['n-nulei']
npc.relation = -60
expect(matchesCond(w, { relation: { npcId: 'n-nulei', lt: -50 } })).toBe(true)
expect(matchesCond(w, { relation: { npcId: 'n-nulei', gt: 0 } })).toBe(false)
w.state.family.flag['tenants'] = true
expect(matchesCond(w, { flag: { key: 'tenants', eq: true } })).toBe(true)
expect(matchesCond(w, { flag: { key: 'tenants', eq: false } })).toBe(false)
})
})
describe('event 生命周期', () => {
it('once 事件只触发一次', () => {
const w = baseWorld('ev-o1')
const onceEv = EVENTS.find((e) => e.once)!
w.state.completedEvents = [onceEv.id]
const before = w.state.completedEvents.length
applyEventChoice(w, onceEv.id, 0)
// applyEventChoice 会重复检查 completedEvents; actually fire 由 eventRoll 把关;这里验证 once 不重入:
expect(w.state.completedEvents.length).toBe(before) // 已存在不重复 push
})
it('applyChoice 未知事件安全退出并清 pending', () => {
const w = baseWorld('ev-o2')
w.state.pendingEvent = 'ev-not-exist'
applyEventChoice(w, 'ev-not-exist', 0)
expect(w.state.pendingEvent).toBeUndefined()
})
it('选择应用资源/声誉/关系效果并明确记录', () => {
const w = baseWorld('ev-o3')
const ev = EVENTS.find((e) => e.id === 'ev-youdao')! // 灵草-5 聚气丹+1
w.state.family.inventory['lingcao'] = 50
const p0 = w.state.family.inventory['pill-qiyuan'] ?? 0
applyEventChoice(w, ev.id, 0)
expect(w.state.family.inventory['lingcao']).toBe(45)
expect(w.state.family.inventory['pill-qiyuan']).toBe(p0 + 1)
})
it('raid 选项触发实际战斗记录与冷却', () => {
const w = baseWorld('ev-o4')
const npc = w.state.npcFamilies['n-nulei']
npc.relation = -80
const b0 = w.state.battles.length
applyEventChoice(w, 'ev-raid-n-nulei', 0)
expect(w.state.battles.length).toBeGreaterThan(b0)
expect(w.state.family.flag['raidCD-n-nulei']).toBe(w.state.year)
})
it('事件效果不会把成员数削减到非法', () => {
const w = baseWorld('ev-o5')
for (const c of Object.values(w.state.members)) {
if (c.id !== 'x1') c.alive = false
}
const mortal = w.state.members['x1']
mortal.realm = { major: 'spirit', minor: 0 }
applyEventChoice(w, 'ev-feisheng', 0)
// stay 分支:成员仍在世,获得仙风特质
expect(w.state.members['x1'].alive).toBe(true)
expect(w.state.members['x1'].traits).toContain('fengxian')
})
it('飞升离开分支:族内蒙荫与头衔交接', () => {
const w = baseWorld('ev-o6')
const strong = w.state.members['x4']
strong.realm = { major: 'spirit', minor: 0 }
applyEventChoice(w, 'ev-feisheng', 1)
expect(strong.alive).toBe(false)
expect(strong.deathCause).toBe('云游飞升')
expect(w.state.family.flag['fengFeiBless']).toBe(true)
// 若继任者存在
expect(w.state.family.headId).toBeTruthy()
})
it('百年庆典仅在第 100 年起火', () => {
const w = baseWorld('ev-o7')
w.state.year = 99
w.state.month = 12
w.advanceMonth()
expect(w.state.pendingEvent).toBe('ev-centennial')
// 已完成后不再重复(completedEvents 注入)
const w2 = baseWorld('ev-o8')
w2.state.year = 100
w2.state.month = 6
w2.state.completedEvents = ['ev-centennial']
for (let i = 0; i < 8; i++) w2.advanceMonth()
expect(w2.state.pendingEvent).not.toBe('ev-centennial')
})
it('eventRoll 不投出已完成的 once 事件', () => {
const w = baseWorld('ev-o9')
for (const e of EVENTS.filter((x) => x.once)) w.state.completedEvents.push(e.id)
for (let i = 0; i < 40; i++) {
w.advanceMonth()
w.state.pendingEvent = undefined
}
const fired = w.state.completedEvents.length === EVENTS.filter((x) => x.once).length
expect(fired).toBe(true)
// 没有任何 once 事件被重复发起: 通过 completedEvents 长度恒定同初值
expect(w.state.chronicle.length).toBeLessThan(40) // 有但不多
})
})