import { describe, expect, it } from 'vitest' import { World } from '../src/renderer/game/engine/runtime/World' import { combatPowerOf } from '../src/renderer/game/engine/runtime/Systems/combat' import { marketPrice, buyItem, sellItem, buyTechnique } from '../src/renderer/game/engine/sim/Market' import { resolveBreakthrough, monthlyRate } from '../src/renderer/game/engine/runtime/Systems/cultivation' import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events' import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy' import { matchesCondPub } from './events.helpers' import { resetSaveBus, attachLogSink } from './world.helpers' describe('Combat power', () => { it('scales with realm', () => { const w = World.create({ seed: 'p', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' }) const head = w.head() const power = combatPowerOf(w, head) expect(power).toBeGreaterThan(3) power > 0 }) it('higher realm beats lower realm', () => { const w = World.create({ seed: 'p2', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' }) const low = w.state.members['x5'] const high = w.state.members['x4'] expect(combatPowerOf(w, high)).toBeGreaterThan(combatPowerOf(w, low)) }) }) describe('Market', () => { it('buy/sell roundtrip keeps stones consistent', () => { const w = World.create({ seed: 'mk', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' }) const fam = w.state.family const price = marketPrice(w, 'lingcao') const stones0 = fam.stones const buyOk = buyItem(w, 'lingcao', 11) expect(buyOk).toBe(stones0 >= price * 11) if (buyOk) { expect(fam.inventory['lingcao']).toBe(60 + 11) expect(fam.stones).toBe(stones0 - price * 11) sellItem(w, 'lingcao', 11) expect(fam.stones).toBe(stones0) } }) it('cannot buy beyond treasury', () => { const w = World.create({ seed: 'mk2', surname: '王', familyName: '王家', motto: 'm', difficulty: 'hard' }) w.state.family.stones = 10 expect(buyItem(w, 'lingcao', 999)).toBe(false) }) it('buy technique adds to library once', () => { const w = World.create({ seed: 'mk3', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' }) w.state.family.stones = 5000 expect(buyTechnique(w, 't-zhenyu', 700)).toBe(true) expect(w.state.family.techniques.includes('t-zhenyu')).toBe(true) expect(buyTechnique(w, 't-zhenyu', 700)).toBe(false) }) }) describe('Breakthrough', () => { it('advances realm on success', () => { const w = World.create({ seed: 'bt', surname: '李', familyName: '李家', motto: 'm', difficulty: 'easy' }) const c = w.state.members['x5'] c.realm = { major: 'mortal', minor: 0 } c.realmProgress = 100 c.perception = 9 c.mind = 9 resolveBreakthrough(w, c, 0.3) expect(c.realm.major).toBe('qi') }) it('failed breakthrough costs progress but not death', () => { const w = World.create({ seed: 'bt2', surname: '李', familyName: '李家', motto: 'm', difficulty: 'normal' }) const c = w.state.members['x5'] c.realm = { major: 'mortal', minor: 0 } c.realmProgress = 100 resolveBreakthrough(w, c, -0.5) // chance was min-clamped; both outcomes acceptable, but check invariants if (c.realm.major === 'mortal') { expect(c.realmProgress).toBeLessThan(100) } else { expect(c.realmProgress).toBe(0) } }) }) describe('Event conditions', () => { it('leading event ids exist', () => { const w = World.create({ seed: 'evc', surname: '刘', familyName: '刘家', motto: 'm', difficulty: 'normal' }) expect(matchesCondPub(w, { minBuilding: { id: 'lingtian', level: 1 } })).toBe(true) expect(matchesCondPub(w, { minBuilding: { id: 'danfang', level: 1 } })).toBe(false) expect(matchesCondPub(w, { minAdult: 3 })).toBe(true) expect(matchesCondPub(w, { maxAdult: 2 })).toBe(false) }) }) describe('Marriage & equipment & rites', () => { it('rejects siblings, accepts strangers', () => { const w = World.create({ seed: 'marry', surname: '王', familyName: '王家', motto: 'm', difficulty: 'normal' }) const bro = w.state.members['x3'] const sis = w.state.members['x5'] expect(w.canMarry(bro.id)).toBe(true) expect(w.canMarry(sis.id)).toBe(true) expect(w.marriageCandidatesOf(bro.id).map((c) => c.id)).not.toContain(sis.id) }) it('widow can remarry via candidates', () => { const w = World.create({ seed: 'remarry', surname: '赵', familyName: '赵家', motto: 'm', difficulty: 'normal' }) const wife = w.state.members['x2'] wife.spouseId = 'x1' w.state.members['x1'].alive = false expect(w.canMarry(wife.id)).toBe(true) expect(w.marriageCandidatesOf(wife.id).length).toBeGreaterThan(0) }) it('equipping artifact raises combat power', () => { const w = World.create({ seed: 'equip', surname: '钱', familyName: '钱家', motto: 'm', difficulty: 'normal' }) const c = w.state.members['x4'] const power0 = combatPowerOf(w, c) w.state.family.inventory['weapon-qi'] = 1 w.equip(c.id, 'weapon-qi') expect(combatPowerOf(w, c)).toBeGreaterThan(power0) }) it('ancestral rite costs, respects cooldown, grants boon', () => { const w = World.create({ seed: 'rite', surname: '孙', familyName: '孙家', motto: 'm', difficulty: 'easy' }) const fam = w.state.family fam.stones = 1000 const rep0 = fam.reputation const head = w.state.members['x1'] head.realmProgress = 50 expect(w.ancestralRite()).toBe(true) expect(fam.stones).toBe(850) expect(fam.reputation).toBe(rep0 + 6) expect(head.realmProgress).toBe(53) expect(w.ancestralRite()).toBe(false) }) it('taunt has 1-year cooldown', () => { const w = World.create({ seed: 'taunt', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' }) const npcId = Object.keys(w.state.npcFamilies)[0]! const before = w.state.npcFamilies[npcId].relation expect(w.tauntNpc(npcId)).toBe(true) expect(w.state.npcFamilies[npcId].relation).toBe(before - 20) expect(w.tauntNpc(npcId)).toBe(false) }) }) describe('Yearly report', () => { it('generates a report per full year', () => { const w = World.create({ seed: 'rep', surname: '吴', familyName: '吴家', motto: 'm', difficulty: 'normal' }) for (let i = 0; i < 14; i++) w.advanceMonth() expect(w.state.yearlyReports.length).toBe(1) const r = w.state.yearlyReports[0] expect(r.year).toBe(1) expect(r.nets).toBeGreaterThanOrEqual(0) 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() }) })