- 统一时轮 GameClock:七 phase 注册制(production/aging/cultivation/missions/ events/diplomacy/epilogue) + 年首三钩子(婚配→岁贡→族簿);advanceMonth 化作薄壳; 新增系统只注册不改本体;alive 守卫、随机序、族簿时序均与重构前逐一等价 - 金钟罩:3 枚 seed × 560 月指纹常驻(重构前后已验证同哈希;时节属有意变更后重固化) - 统一随机 RngHub:开局播种(crypto UV)、音效白噪独立流、引擎 rng 带 nextCount 审计; 全工程拔除逻辑侧 Math.random - 时节系统:春夏秋冬乘子(春田+10%/夏修+5%/秋市+5%/冬闭+8%)入生产与修炼 - 崩溃保护:advance 异常自动存『出险前』档 + 停速提示回档 - 性能预算:dev-only 单 tick>60ms 报最慢 phase - 横轴年表:十年一格大事/生殇聚合 + 春秋原年轴视图 - 测试 197→225(时钟/收集器/四季/年轴/效果全矩阵/死局链/残留防御)
224 lines
7.3 KiB
TypeScript
224 lines
7.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { World } from '../src/renderer/game/engine/world'
|
||
import { monthlyRate, resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
|
||
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
|
||
import { isFit } from '../src/renderer/game/data/aspirations'
|
||
import { needsTribulation, resolveTribulation, tribulationEventId } from '../src/renderer/game/engine/systems/tribulation'
|
||
import { buildBiography } from '../src/renderer/game/core/biography'
|
||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||
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
|
||
}
|
||
|
||
describe('aspirations 志向', () => {
|
||
it('成年礼自动定志(16-19 岁补充)', () => {
|
||
const w = baseWorld('asp-a')
|
||
const c = w.state.members['x3'] // 16 岁
|
||
w.advanceMonth()
|
||
expect(c.aspiration).toBeTruthy()
|
||
})
|
||
|
||
it('求道志向修炼加快,云游稍缓', () => {
|
||
const w = baseWorld('asp-b')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.aspiration = 'dao'
|
||
const dao = monthlyRate(w, c)
|
||
c.aspiration = 'yun'
|
||
const yun = monthlyRate(w, c)
|
||
expect(dao).toBeGreaterThan(yun)
|
||
c.aspiration = undefined
|
||
const none = monthlyRate(w, c)
|
||
expect(dao).toBeGreaterThan(none)
|
||
})
|
||
|
||
it('兵略志向提升战力,商略不影响战力', () => {
|
||
const w = baseWorld('asp-c')
|
||
const c = w.state.members['x4']
|
||
c.aspiration = 'bing'
|
||
const pb = combatPowerOf(w, c)
|
||
c.aspiration = 'shang'
|
||
const ps = combatPowerOf(w, c)
|
||
expect(pb).toBeGreaterThan(ps)
|
||
})
|
||
|
||
it('耕读志向提升灵田产出', () => {
|
||
const w = baseWorld('asp-d')
|
||
w.state.family.buildings = { lingtian: 1 }
|
||
w.state.members['x3'].aspiration = 'geng'
|
||
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
||
w.advanceMonth()
|
||
const gained = (w.state.family.inventory['lingcao'] ?? 0) - c0
|
||
// 春季(月2)10 ×(1+耕读5%+春10%) ≈ 11~12
|
||
expect(gained).toBeGreaterThanOrEqual(11)
|
||
})
|
||
|
||
it('承宗志向提升家族添丁率', () => {
|
||
const w = baseWorld('asp-e')
|
||
let births = 0
|
||
for (const c of Object.values(w.state.members)) {
|
||
c.aspiration = 'cheng'
|
||
if (c.gender === 'male' && c.id !== 'x3') c.state = 'idle'
|
||
}
|
||
for (let i = 0; i < 120; i++) w.advanceMonth()
|
||
for (const c of Object.values(w.state.members)) {
|
||
if (c.bornYear > 1) births++
|
||
}
|
||
expect(births).toBeGreaterThanOrEqual(1)
|
||
})
|
||
})
|
||
|
||
describe('灵根契合', () => {
|
||
it('本命判定按主属性', () => {
|
||
expect(isFit('火', '火')).toBe(true)
|
||
expect(isFit('水', '火')).toBe(false)
|
||
})
|
||
|
||
it('契合者修炼更快', () => {
|
||
const w = baseWorld('fit-a')
|
||
const c = w.state.members['x5']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
c.techniqueId = 't-liehuo' // 火
|
||
c.roots = { grade: 3, primary: '火', secondary: [] }
|
||
const fit = monthlyRate(w, c)
|
||
c.roots = { grade: 3, primary: '水', secondary: [] }
|
||
const nofit = monthlyRate(w, c)
|
||
expect(fit).toBeGreaterThan(nofit)
|
||
})
|
||
|
||
it('契合者战力更高', () => {
|
||
const w = baseWorld('fit-b')
|
||
const c = w.state.members['x4']
|
||
c.techniqueId = 't-liehuo'
|
||
c.roots = { grade: 3, primary: '火', secondary: [] }
|
||
const fit = combatPowerOf(w, c)
|
||
c.roots = { grade: 3, primary: '水', secondary: [] }
|
||
const nofit = combatPowerOf(w, c)
|
||
expect(fit).toBeGreaterThan(nofit)
|
||
})
|
||
})
|
||
|
||
describe('渡劫', () => {
|
||
it('仅大境界晋升触发', () => {
|
||
const w = baseWorld('trb-a')
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'qi', minor: 1 }
|
||
expect(needsTribulation(c)).toBe(false)
|
||
c.realm = { major: 'qi', minor: 8 }
|
||
expect(needsTribulation(c)).toBe(true)
|
||
c.realm = { major: 'foundation', minor: 2 }
|
||
expect(needsTribulation(c)).toBe(true)
|
||
c.realm = { major: 'spirit', minor: 2 }
|
||
expect(needsTribulation(c)).toBe(false)
|
||
})
|
||
|
||
it('触发后事件 id 唯一且可解析', () => {
|
||
const w = baseWorld('trb-b')
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'foundation', minor: 2 }
|
||
c.realmProgress = 100
|
||
w.advanceMonth()
|
||
// 冷却 6 月保护下每月 chance 尝试——长跑 40 月要么事件出现要么仍在等待
|
||
let found = false
|
||
for (let i = 0; i < 40 && !found; i++) {
|
||
w.advanceMonth()
|
||
if (w.state.pendingEvent?.startsWith('ev-trib-')) found = true
|
||
}
|
||
expect(found).toBe(true)
|
||
})
|
||
|
||
it('硬渡成功晋升大境界', () => {
|
||
const w = baseWorld('trb-c')
|
||
let success = false
|
||
for (let attempt = 0; attempt < 30 && !success; attempt++) {
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'qi', minor: 9 }
|
||
c.realmProgress = 100
|
||
c.mind = 9
|
||
c.health = 100
|
||
const r = resolveTribulation(w, c, 'rash')
|
||
if (r === 'success') {
|
||
expect(c.realm.major).toBe('foundation')
|
||
success = true
|
||
} else {
|
||
// 失败后被折断,复置后重试(保持独立样本)
|
||
c.realmProgress = 100
|
||
c.health = 100
|
||
}
|
||
}
|
||
expect(success).toBe(true)
|
||
})
|
||
|
||
it('压制一年延迟后恢复尝试通道', () => {
|
||
const w = baseWorld('trb-d')
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'qi', minor: 9 }
|
||
c.realmProgress = 100
|
||
resolveTribulation(w, c, 'delay')
|
||
expect(c.tribDelayYear).toBe(w.state.year + 1)
|
||
// 未到年份时 cultivationTick 锁定 => 不会立动
|
||
w.advanceMonth()
|
||
expect(c.realmProgress).toBe(100)
|
||
})
|
||
|
||
it('护法失败时护法可能受伤但不会陨落', () => {
|
||
const w = baseWorld('trb-e')
|
||
const c = w.state.members['x3']
|
||
c.realm = { major: 'qi', minor: 9 }
|
||
c.realmProgress = 100
|
||
c.mind = 1 // 低心跳高失败
|
||
c.health = 100
|
||
const guardian = w.state.members['x4']
|
||
guardian.health = 100
|
||
const r = resolveTribulation(w, c, 'guard')
|
||
if (r === 'fail') {
|
||
expect(guardian.alive).toBe(true)
|
||
}
|
||
expect(w.state.family.stones).toBeLessThanOrEqual(800)
|
||
})
|
||
})
|
||
|
||
describe('biography 列传', () => {
|
||
it('活在生人:生平/修行脉络/婚育齐全', () => {
|
||
const w = baseWorld('bio-a')
|
||
const lines = buildBiography(w.state, 'x1')
|
||
const text = lines.map((l) => l.text).join('')
|
||
expect(text).toContain('第1代')
|
||
expect(text).toContain('生于-34年')
|
||
expect(lines.length).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('逝者带墓志铭且包含死因', () => {
|
||
const w = baseWorld('bio-b')
|
||
const c = w.state.members['x5']
|
||
c.alive = false
|
||
c.deathYear = 40
|
||
c.deathCause = '渡劫陨落'
|
||
const lines = buildBiography(w.state, x5id(w))
|
||
const last = lines[lines.length - 1]
|
||
expect(last.label).toBe('墓志')
|
||
expect(last.text).toContain('雷霆') // 渡劫陨落专属纹样
|
||
})
|
||
|
||
it('突破脉络随史书记录增长', () => {
|
||
const w = baseWorld('bio-c')
|
||
const c = w.state.members['x4']
|
||
c.realm = { major: 'qi', minor: 8 }
|
||
c.realmProgress = 100
|
||
c.mind = 9
|
||
resolveBreakthrough(w, c, 0.9)
|
||
const lines = buildBiography(w.state, c.id)
|
||
expect(lines.some((l) => l.label === '修行')).toBe(true)
|
||
})
|
||
})
|
||
|
||
function x5id(w: World): string {
|
||
const c = Object.values(w.state.members).find((m) => m.id === 'x5')!
|
||
return c.id
|
||
}
|