Files
ChronicleOfTheImmortalClan/tests/events.test.ts
T
thzxx 09cbad6449 test: 测试矩阵扩至 161 项(深度审计 + 5 处缺陷修复)
新增测试矩阵(12 文件 / 161 cases):
- rng 12 / data 18 / pcgen+cultivation 20 / economy 12 / combat+missions 24
- marriage+diplomacy 13 / events 12 / world+lifecycle 8(含镜像一致)/ save 4+5 / audit 8

审计带出并修复的真实缺陷:
1. Rng.pick 空数组未报错(防空白数组进而崩溃难查)
2. combatPowerOf 未应用功法悟道品阶战力加成(0.1.2 设计缺口)
3. 重伤者即使不代表伤状态仍按满速修炼(health 进修炼公式)
4. 寡妇/鳏夫无法再婚:候选列表放着人执行层却拒婚(spouseId 死旧契约)
5. isWidowed 参数契约陷阱:传 id 静默 false(升级接受 Character|Id 双签名)
2026-08-23 08:47:21 +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/world'
import { matchesCond, applyEventChoice, findEvent, eventRoll } from '../src/renderer/game/engine/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) // 有但不多
})
})