Files
ChronicleOfTheImmortalClan/tests/aspiration-trib-bio.test.ts
thzxx de46808188 feat(0.1.16-P1): 资源闭环引擎(修为耗草/炼丹概率化/铸器/灵脉收益/NPC战力/灾变落效)+ C13C14
【A 经济闭环】
- 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警
- 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料
- 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石)
- 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤
- NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8)
- 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子

【C 治理】
- C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选
- C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级

【UI 半批】
- 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮
- 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览
- 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
2026-08-23 14:10:42 +08:00

226 lines
7.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { monthlyRate, resolveBreakthrough } from '../src/renderer/game/engine/runtime/Systems/cultivation'
import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat'
import { isFit } from '../src/renderer/game/data/aspirations'
import { needsTribulation, resolveTribulation, tribulationEventId } from '../src/renderer/game/engine/runtime/Systems/tribulation'
import { buildBiography } from '../src/renderer/game/engine/narrative/biography'
import { applyEventChoice } from '../src/renderer/game/engine/runtime/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 }
for (const c of Object.values(w.state.members)) c.realm = { major: 'mortal', minor: 0 }
w.state.worldSim = undefined as never
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
// 春季(月210 ×(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
}