结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
126 lines
4.9 KiB
TypeScript
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-n-nulei', 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)
|
|
})
|
|
})
|