【世界种子生成器(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 通过
147 lines
6.0 KiB
TypeScript
147 lines
6.0 KiB
TypeScript
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) // 有但不多
|
||
})
|
||
})
|