结构(旧 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
262 lines
9.4 KiB
TypeScript
262 lines
9.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||
import { Rng, seedToRng } from '../src/renderer/game/engine/kernel/rng'
|
||
import { newCharacter, rollAttributes, rollRoots, rollPersonality, traitBonuses, calcLifespan } from '../src/renderer/game/engine/runtime/pcgen'
|
||
import { monthlyRate, perAttemptChance } from '../src/renderer/game/engine/runtime/Systems/cultivation'
|
||
import { lifespanOf } from '../src/renderer/game/engine/runtime/Systems/lifecycle'
|
||
|
||
function rng(): Rng {
|
||
return new Rng(seedToRng('pcgen'))
|
||
}
|
||
|
||
describe('pcgen 人物生成', () => {
|
||
it('rollAttributes 限制在 1~10', () => {
|
||
const r = rng()
|
||
for (let i = 0; i < 300; i++) {
|
||
const v = rollAttributes(r, 5, 3)
|
||
expect(v).toBeGreaterThanOrEqual(1)
|
||
expect(v).toBeLessThanOrEqual(10)
|
||
}
|
||
})
|
||
|
||
it('rollRoots 品阶在 0~5,副属性与主属性不重复', () => {
|
||
const r = rng()
|
||
for (let i = 0; i < 50; i++) {
|
||
const roots = rollRoots(r)
|
||
expect(roots.grade).toBeGreaterThanOrEqual(0)
|
||
expect(roots.grade).toBeLessThanOrEqual(5)
|
||
expect(roots.secondary.length).toBeLessThanOrEqual(2)
|
||
expect(roots.secondary).not.toContain(roots.primary)
|
||
expect(new Set(roots.secondary).size).toBe(roots.secondary.length)
|
||
}
|
||
})
|
||
|
||
it('父母品阶子代平均贴近父母', () => {
|
||
const r = rng()
|
||
const dad = newCharacter(rng(), { name: '父', gender: 'male', generation: 1, bornYear: -20, age: 20, realm: { major: 'qi', minor: 1 } })
|
||
dad.roots = { grade: 4, primary: '火', secondary: [] }
|
||
const mom = newCharacter(rng(), { name: '母', gender: 'female', generation: 1, bornYear: -20, age: 20, realm: { major: 'qi', minor: 1 } })
|
||
mom.roots = { grade: 4, primary: '火', secondary: [] }
|
||
let near = 0
|
||
for (let i = 0; i < 40; i++) {
|
||
const child = rollRoots(r, { m: dad, f: mom })
|
||
if (child.grade >= 3) near++
|
||
}
|
||
// 双天品父母子代大概率不劣于玄品
|
||
expect(near).toBeGreaterThan(25)
|
||
})
|
||
|
||
it('rollPersonality 返回 1~2 个合法特质', () => {
|
||
const r = rng()
|
||
for (let i = 0; i < 50; i++) {
|
||
const t = rollPersonality(r)
|
||
expect(t.length).toBeGreaterThanOrEqual(1)
|
||
expect(t.length).toBeLessThanOrEqual(2)
|
||
expect(new Set(t).size).toBe(t.length)
|
||
}
|
||
})
|
||
|
||
it('traitBonuses 聚合回值在合理区间', () => {
|
||
const chara = newCharacter(rng(), { name: '某人', gender: 'male', generation: 1, bornYear: -20, age: 20 })
|
||
chara.traits = ['zisheng', 'jingkan'] // 自律+精研
|
||
const b = traitBonuses(chara)
|
||
expect(b.exp).toBeGreaterThan(1)
|
||
expect(b.breakBonus + b.charmBonus).toBeGreaterThanOrEqual(0)
|
||
chara.traits = ['xinheng'] // 性狠
|
||
expect(traitBonuses(chara).windBonus).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('calcLifespan 随根骨略增长且为正', () => {
|
||
const low = calcLifespan('qi', 1)
|
||
const high = calcLifespan('qi', 9)
|
||
expect(high).toBeGreaterThan(low)
|
||
expect(low).toBeGreaterThan(0)
|
||
})
|
||
})
|
||
|
||
describe('monthlyRate 修炼速率', () => {
|
||
function baseWorld(seed: string): World {
|
||
return World.create({ seed, surname: '尹', familyName: '尹家', motto: 'm', difficulty: 'normal' })
|
||
}
|
||
|
||
it('感知越高修得越快', () => {
|
||
const w = baseWorld('cult-a')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.realmProgress = 0
|
||
c.perception = 3
|
||
const low = monthlyRate(w, c)
|
||
c.perception = 9
|
||
const high = monthlyRate(w, c)
|
||
expect(high).toBeGreaterThan(low * 1.6)
|
||
})
|
||
|
||
it('闭关显著快于闲居', () => {
|
||
const w = baseWorld('cult-b')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.state = 'idle'
|
||
const idle = monthlyRate(w, c)
|
||
c.state = 'meditation'
|
||
const med = monthlyRate(w, c)
|
||
expect(med).toBeGreaterThan(idle * 1.25)
|
||
})
|
||
|
||
it('负伤大幅减速,重伤近乎停滞', () => {
|
||
const w = baseWorld('cult-c')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.health = 80
|
||
const mild = monthlyRate(w, c)
|
||
c.health = 15
|
||
const grave = monthlyRate(w, c)
|
||
expect(grave).toBeLessThan(mild * 0.6)
|
||
})
|
||
|
||
it('童子启蒙与暮年衰退', () => {
|
||
const w = baseWorld('cult-d')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.bornYear = w.state.year // 0 岁
|
||
const kid = monthlyRate(w, c)
|
||
c.bornYear = w.state.year - 70
|
||
const elder = monthlyRate(w, c)
|
||
const adult = (() => {
|
||
const c2 = w.state.members['x4']
|
||
c2.bornYear = w.state.year - 30
|
||
return monthlyRate(w, c2)
|
||
})()
|
||
expect(kid).toBeLessThan(adult)
|
||
expect(elder).toBeLessThan(adult)
|
||
})
|
||
|
||
it('聚灵阵与洞府对闭关加成', () => {
|
||
const w = baseWorld('cult-e')
|
||
w.state.family.buildings['juling'] = 2
|
||
w.state.family.buildings['dongfu'] = 2
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.state = 'meditation'
|
||
const withB = monthlyRate(w, c)
|
||
w.state.family.buildings = { lingtian: 1, zongci: 1 }
|
||
const without = monthlyRate(w, c)
|
||
expect(withB).toBeGreaterThan(without)
|
||
})
|
||
|
||
it('无功法散修明显慢于有功者', () => {
|
||
const w = baseWorld('cult-f')
|
||
const c1 = w.state.members['x5']
|
||
c1.realm = { major: 'qi', minor: 1 }
|
||
c1.techniqueId = 't-qinglian'
|
||
const withT = monthlyRate(w, c1)
|
||
c1.techniqueId = undefined
|
||
const without = monthlyRate(w, c1)
|
||
expect(withT).toBeGreaterThan(without)
|
||
})
|
||
|
||
it('大境界修炼效率递减(元婴比炼气慢)', () => {
|
||
const w = baseWorld('cult-g')
|
||
const c1 = w.state.members['x5']
|
||
c1.realm = { major: 'qi', minor: 1 }
|
||
const qi = monthlyRate(w, c1)
|
||
c1.realm = { major: 'nascent', minor: 0 }
|
||
const nas = monthlyRate(w, c1)
|
||
expect(nas).toBeLessThan(qi)
|
||
})
|
||
|
||
it('perAttemptChance 受心性与性格影响(勇气)', () => {
|
||
const w = baseWorld('cult-h')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 8 }
|
||
c.realmProgress = 100
|
||
c.mind = 3
|
||
c.traits = []
|
||
const low = perAttemptChance(w, c)
|
||
c.mind = 9
|
||
c.traits = ['tiangan']
|
||
const high = perAttemptChance(w, c)
|
||
expect(high).toBeGreaterThan(low)
|
||
expect(low).toBeGreaterThan(0.4)
|
||
})
|
||
|
||
it('瓶颈自动尝试存在 6 个月冷却', () => {
|
||
const w = baseWorld('cult-i')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.realmProgress = 100
|
||
c.lastBreakthroughAttempt = w.state.year * 12 + w.state.month - 3
|
||
const before = c.realmProgress // 应保持 100(冷却中不自动尝试)
|
||
w.advanceMonth()
|
||
expect(c.realmProgress).toBeGreaterThanOrEqual(100) // 仍 ≥100(要么未尝试要么成功从0算)
|
||
// 未尝试则保持 100;尝试要么成功(realm 变化或者 progress=0)
|
||
const plausible =
|
||
c.realmProgress >= 100 || (c.realmProgress < 100 && c.realm.major === 'qi' && c.realm.minor === 1) || c.realm.major !== 'qi'
|
||
expect(plausible).toBe(true)
|
||
void before
|
||
})
|
||
})
|
||
|
||
describe('lifecycle 寿命', () => {
|
||
it('50 岁修士不会自然死亡(远早于寿元)', () => {
|
||
const w = World.create({ seed: 'life-a', surname: '秦', familyName: '秦家', motto: 'm', difficulty: 'normal' })
|
||
const c = w.state.members['x1']
|
||
c.bornYear = w.state.year - 50
|
||
for (let i = 0; i < 24; i++) w.advanceMonth()
|
||
expect(c.alive).toBe(true)
|
||
})
|
||
|
||
it('寿元临界后逐渐死亡(模拟 60 周年内全部辞世)', () => {
|
||
const w = World.create({ seed: 'life-b', surname: '雷', familyName: '雷家', motto: 'm', difficulty: 'easy' })
|
||
const c = w.state.members['x5']
|
||
c.bornYear = w.state.year - 999 // 深过期
|
||
let died = false
|
||
for (let i = 0; i < 120 && !died; i++) {
|
||
w.advanceMonth()
|
||
died = !c.alive
|
||
}
|
||
expect(died).toBe(true)
|
||
expect(c.deathCause === '幼夭' || c.deathCause === '寿元将尽' || c.deathCause === '伤势不治').toBe(true)
|
||
})
|
||
|
||
it('重伤平复机制:伤者每月回复→健康', () => {
|
||
const w = World.create({ seed: 'life-c', surname: '盼', familyName: '盼家', motto: 'm', difficulty: 'normal' })
|
||
const c = w.state.members['x3']
|
||
c.health = 10
|
||
c.state = 'wounded'
|
||
const h0 = c.health
|
||
w.advanceMonth()
|
||
expect(c.health).toBeGreaterThan(h0)
|
||
// 不断推进最终恢复闲居
|
||
for (let i = 0; i < 12; i++) w.advanceMonth()
|
||
expect(c.health).toBeGreaterThanOrEqual(95)
|
||
if (c.alive && c.health >= 95) expect(c.state).toBe('idle')
|
||
})
|
||
|
||
it('重伤过低小幅增加早逝风险(抽样验证无崩溃且部分死亡)', () => {
|
||
const w = World.create({ seed: 'life-d', surname: '荣', familyName: '荣家', motto: 'm', difficulty: 'normal' })
|
||
const c = w.state.members['x4']
|
||
c.bornYear = w.state.year - 999 // 未到寿限前不会因这点死亡; 直接推到长期
|
||
c.health = 1
|
||
c.physique = 1
|
||
let deaths = 0
|
||
for (let i = 0; i < 150; i++) w.advanceMonth()
|
||
deaths = !c.alive ? 1 : 0
|
||
if (deaths) expect(c.deathCause).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
describe('lifespanOf 计算', () => {
|
||
it('随大境界与根骨增长', () => {
|
||
const w = World.create({ seed: 'ls', surname: '魏', familyName: '魏家', motto: 'm', difficulty: 'normal' })
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.physique = 4
|
||
const qi = lifespanOf(w, c)
|
||
c.realm = { major: 'core', minor: 0 }
|
||
const core = lifespanOf(w, c)
|
||
expect(core).toBeGreaterThan(qi)
|
||
c.physique = 8
|
||
expect(lifespanOf(w, c)).toBeGreaterThan(qi)
|
||
})
|
||
})
|