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) // 有但不多 }) })