v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)

- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理
- 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈
- 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查
- 功德碑:宗主辞世即勒石,宗祠页碑录永存
- 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫)
- 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关
- 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
2026-08-23 08:28:49 +08:00
parent 6b15a97d0c
commit fefbe1f1d7
22 changed files with 864 additions and 8 deletions
+113 -1
View File
@@ -2,8 +2,11 @@ import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/market'
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
import { resolveBreakthrough, monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
import { matchesCondPub } from './events.helpers'
import { resetSaveBus, attachLogSink } from './world.helpers'
describe('Combat power', () => {
it('scales with realm', () => {
@@ -153,3 +156,112 @@ describe('Yearly report', () => {
expect(r.power).toBeGreaterThan(0)
})
})
describe('Posts (职事)', () => {
it('assigns up to max and rejects over-cap', () => {
const w = World.create({ seed: 'posts', surname: '徐', familyName: '徐家', motto: 'm', difficulty: 'normal' })
expect(w.assignPost('x3', 'elder')).toBe(true)
expect(w.assignPost('x5', 'elder')).toBe(true)
expect(w.assignPost('x4', 'elder')).toBe(false)
expect(w.postCount('elder')).toBe(2)
})
it('elder boosts cultivation rate', () => {
const w = World.create({ seed: 'posts2', surname: '许', familyName: '许家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x5']
w.state.members['x3'].state = 'meditation'
c.realm = { major: 'qi', minor: 1 }
const r0 = monthlyRate(w, c)
w.assignPost('x3', 'elder')
w.assignPost('x4', 'elder')
const r1 = monthlyRate(w, c)
expect(r1).toBeGreaterThan(r0)
})
it('steward boosts market income', () => {
const w = World.create({ seed: 'posts3', surname: '邓', familyName: '邓家', motto: 'm', difficulty: 'normal' })
w.state.family.buildings['fangshi'] = 2
w.assignPost('x3', 'steward')
const stones0 = w.state.family.stones
w.advanceMonth()
expect(w.state.family.stones).toBeGreaterThan(stones0 + 110)
})
})
describe('Technique wudao (悟道)', () => {
it('accumulates progress and ranks up talents', () => {
const w = World.create({ seed: 'wudao', surname: '康', familyName: '康家', motto: 'm', difficulty: 'easy' })
const c = w.state.members['x4']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = 0
c.techniqueProgress = 95
c.techniqueRank = 0
c.techniqueId = 't-houtu'
c.perception = 9
for (let i = 0; i < 30; i++) w.advanceMonth()
expect(c.techniqueRank ?? 0).toBeGreaterThanOrEqual(1)
})
it('carries wudao to a disciple on major jump', () => {
const w = World.create({ seed: 'wd2', surname: '封', familyName: '封家', motto: 'm', difficulty: 'easy' })
const c = w.state.members['x1']
c.realm = { major: 'qi', minor: 9 }
c.realmProgress = 100
c.mind = 9
c.perception = 9
const child = w.state.members['x3']
child.techniqueId = 't-houtu'
child.techniqueProgress = 10
child.realm = { major: 'qi', minor: 2 }
resolveBreakthrough(w, c, 0.5)
expect(c.realm.major).toBe('foundation')
expect(child.techniqueProgress).toBeGreaterThanOrEqual(60)
})
})
describe('Monument & milestones', () => {
it('writes monument on head death, inherits reign', () => {
const w = World.create({ seed: 'mon', surname: '侯', familyName: '侯家', motto: 'm', difficulty: 'normal' })
const head = w.state.members['x1']
head.alive = false
head.deathYear = w.state.year
w.advanceMonth()
const monument = w.state.chronicle.find((e) => e.text.startsWith('功德碑'))
expect(monument).toBeTruthy()
expect(w.state.family.headId).toBe('x3')
expect(w.state.family.flag['reignStart']).toBe(w.state.year)
})
it('triggers centennial once at year 100', () => {
const w = World.create({ seed: 'cen', surname: '欧阳', familyName: '欧阳家', motto: 'm', difficulty: 'normal' })
resetSaveBus()
attachLogSink(w)
w.state.year = 99
w.state.month = 12
for (let i = 0; i < 3; i++) w.advanceMonth()
expect(w.state.pendingEvent).toBe('ev-centennial')
})
it('feisheng effect grants trail or ascends', () => {
const w = World.create({ seed: 'fs', surname: '皇甫', familyName: '皇甫家', motto: 'm', difficulty: 'normal' })
const c = w.state.members['x4']
c.realm = { major: 'spirit', minor: 1 }
applyEventChoice(w, 'ev-feisheng', 0)
expect(c.traits).toContain('fengxian')
})
})
describe('Genealogy', () => {
it('builds rows per generation with couple units', () => {
const w = World.create({ seed: 'gene', surname: '惠', familyName: '惠家', motto: 'm', difficulty: 'normal' })
const rows = computeGenealogy(w)
expect(rows.length).toBeGreaterThanOrEqual(2)
const second = rows.find((r) => r.gen === 2)
expect(second).toBeTruthy()
const units = second!.units
// x1/x2 couple should appear in gen1
const g1 = rows.find((r) => r.gen === 1)!
expect(g1.units.some((u) => u.parents[0]?.id === 'x1' || u.parents[1]?.id === 'x1')).toBe(true)
expect(units).toBeTruthy()
})
})