Files
ChronicleOfTheImmortalClan/tests/events.test.ts
T
thzxx ec25bf92fc v0.1.24: 寰宇初构——世界种子生成器 + 插件时轮锚点(1034 全绿)
【世界种子生成器(NPC 数量不再定死)】
- sim/worldgen.ts:generateWorld(seed) 确定性塑造天下——NPC 4~8 家随机
  (老牌世家模板 + 词库组合新贵,名字去重)、初始关系网(1-2世仇+1盟友)、
  开局 era 三态、区域风味、市场偏移 ±15%
- 独立派生 rng(seed::worldgen)——World.rng 主序列零消耗:同 seed 世界可重放、
  玩法随机序列不受生成器移动(金钟罩玩法序保护)
- state.worldGen 落档(normalize 旧档按 seed 重放=同世界,存档兼容)
- creation 初始化 npcFamilies/initSim 关系网/era/池偏置全走 worldgen——开局即恩怨
- 局内补位目标 = 开局家数(4~8),生灭闭环持续

【插件深化(0.1.24 第二组钩子)】
- PluginContext.beforePhase/afterPhase(时轮锚点:phase 前后挂勾,卸载全摘)
- PluginContext.addNpcTemplate(世界模板注入——词库插件化)
- plugin-public 文档同步;示例 Plugin 契约升级

【测试适配(世界名单随机化)】
- 静态 npc id 假设全面改动态取家(anyNpc helper 共享);
- 难度梯度用例改「同 seed 不同难度」比较;worldgen 确定性/范围/去重/关系对称 7 例

【测试】1034 全绿(45 套件);金钟罩 0.1.24 基线固化(worldgen 随机世界)+ worldAnnals 等;
build 通过
2026-08-23 16:15:22 +08:00

147 lines
6.0 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, anyNpc } 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 npcId = anyNpc(w, ['n-nulei'])
const npc = w.state.npcFamilies[npcId]
npc.relation = -60
expect(matchesCond(w, { relation: { npcId, lt: -50 } })).toBe(true)
expect(matchesCond(w, { relation: { npcId, 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[anyNpc(w, ['n-nulei'])]
npc.relation = -80
const b0 = w.state.battles.length
applyEventChoice(w, `ev-raid-${anyNpc(w, ['n-nulei'])}`, 0)
expect(w.state.battles.length).toBeGreaterThan(b0)
expect(w.state.family.flag[`raidCD-${anyNpc(w, ['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) // 有但不多
})
})