Files
ChronicleOfTheImmortalClan/tests/core-extra.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

126 lines
4.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { applyEventChoice, applyEffect } from '../src/renderer/game/engine/runtime/Systems/events'
import { EffectDef } from '../src/renderer/game/data/events'
import { Character } from '../src/renderer/game/types/domain'
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
}
function applyEff(w: World, eff: EffectDef): void {
applyEffect(w, eff)
}
describe('memberBy 效果全矩阵(核心系统)', () => {
it('exp/wound/heal 对全目标执行', () => {
const w = baseWorld('mb-a')
const before = Object.values(w.state.members).map((c) => c.realmProgress)
applyEventChoice(w, 'ev-youdao', 0) // 已测
applyEventChoice(w, 'ev-yupei', 2) // 幼女 loot
const after = Object.values(w.state.members).map((c) => c.realmProgress)
void before
void after
expect(w.state.members['x5'].fortune).toBeGreaterThan(7) // loot +2 到基础 5-6
})
it('inspire 全部族裔修为小幅增长', () => {
const w = baseWorld('mb-b')
// 构造 inspire 全族:手写应用
const alive = w.aliveMembers()
const before = alive.map((c) => c.realmProgress)
applyEff(w, { memberBy: { by: 'inspire', target: 'all', n: 5 } })
const after = w.aliveMembers().map((c) => c.realmProgress)
expect(after.filter((v, i) => v !== before[i]).length).toBeGreaterThan(0)
expect(w.state.members['x3'].realmProgress).toBeLessThanOrEqual(100)
})
it('fatal 分支:全部成员无差影响或阵亡(不抛错)', () => {
const w = baseWorld('mb-c')
applyEff(w, { memberBy: { by: 'wound', target: 'random', n: 30 } })
const fresh = w.aliveMembers()
expect(fresh.length).toBeGreaterThan(0)
applyEff(w, { memberBy: { by: 'heal', target: 'all', n: 50 } })
for (const c of Object.values(w.state.members)) {
expect(c.health).toBeGreaterThanOrEqual(0)
expect(c.health).toBeLessThanOrEqual(100)
}
})
it('highestPerception/highestPower/highestFortune 目标稳定命中', () => {
const w = baseWorld('mb-d')
const p0 = w.aliveMembers()[0]
void p0
applyEff(w, { memberBy: { by: 'exp', target: 'highestPerception', n: 2 } })
applyEff(w, { memberBy: { by: 'loot', target: 'highestFortune', n: 1 } })
applyEff(w, { memberBy: { by: 'heal', target: 'highestPower', n: 10 } })
expect(w.state.members['x4'].health).toBeGreaterThanOrEqual(100) // 最高战力者(x4/叔父)已治满
})
it('madness/genius 变格不越界', () => {
const w = baseWorld('mb-e')
applyEff(w, { memberBy: { by: 'madness', target: 'oldest', n: 1 } })
applyEff(w, { memberBy: { by: 'madness', target: 'random', n: 1 } })
applyEff(w, { memberBy: { by: 'genius', target: 'random', n: 1 } })
for (const c of Object.values(w.state.members)) {
expect(c.perception).toBeLessThanOrEqual(10)
expect(c.mind).toBeGreaterThanOrEqual(1)
}
})
})
describe('advance 死局与时钟守卫', () => {
it('全员死亡后 clock 仍安全步进(生产与收尾仍跑)', () => {
const w = baseWorld('dead-a')
for (const c of Object.values(w.state.members)) c.alive = false
w.advanceMonth()
expect(w.state.gameOver).toBeTruthy()
w.advanceMonth()
expect(w.state.year).toBeGreaterThanOrEqual(1)
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
})
it('头衔无人可继时 gameOver 语义完整', () => {
const w = baseWorld('dead-b')
for (const c of Object.values(w.state.members)) c.alive = false
w.advanceMonth()
expect(w.state.gameOver?.reason).toContain('香火')
expect(w.state.gameOver?.year).toBeGreaterThanOrEqual(1)
})
it('继承人仅存女性也可继位(家谱连续性)', () => {
const w = baseWorld('dead-c')
const x1 = w.state.members['x1']
x1.alive = false
x1.deathYear = w.state.year
const x3 = w.state.members['x3']
x3.alive = false
x3.deathYear = w.state.year
w.advanceMonth()
expect(['x2', 'x5']).toContain(w.state.family.headId)
})
})
describe('事件效果残留防御', () => {
it('关系 clamp 至 ±100', () => {
const w = baseWorld('rm-a')
applyEventChoice(w, `ev-raid-${Object.keys(w.state.npcFamilies)[0]}`, 0) // 关系打向正 25? 取决于胜负
applyEventChoice(w, 'ev-zhusu', 2) // -20
for (const n of Object.values(w.state.npcFamilies)) {
expect(n.relation).toBeGreaterThanOrEqual(-100)
expect(n.relation).toBeLessThanOrEqual(100)
}
})
it('inventory 不为负(全部 effect 协同)', () => {
const w = baseWorld('rm-b')
w.state.family.inventory['lingcao'] = 6
applyEventChoice(w, 'ev-youdao', 0) // -5
expect(w.state.family.inventory['lingcao']).toBeGreaterThanOrEqual(0)
})
})