import { describe, expect, it } from 'vitest' import { World } from '../src/renderer/game/engine/world' import { buildReport, verdictLine } from '../src/renderer/game/core/legacyreport' import { migrateIfNeeded, exportEnvelope, parseImportEnvelope, CURRENT_SCHEMA, APP_ID } from '../src/renderer/game/storage/migrate' import { FORMATIONS, formationById, FormationId } from '../src/renderer/game/data/formations' import { resolveEncounter } from '../src/renderer/game/engine/systems/combat' import { ENEMIES } from '../src/renderer/game/data/secrets' import { applyEventChoice } from '../src/renderer/game/engine/systems/events' import { sendMission } from '../src/renderer/game/engine/systems/missions' 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('legacyreport 百年报告', () => { it('报告含完整序列与死因榜', () => { const w = baseWorld('rep-a') for (let i = 0; i < 30; i++) w.advanceMonth() const r = buildReport(w.state) expect(r.years.length).toBeGreaterThan(0) expect(r.population.length).toBe(r.years.length) expect(r.reputation.length).toBe(r.years.length) expect(r.totalYears).toBe(w.state.year) }) it('短家族得到诚实结语', () => { const w = baseWorld('rep-b') const r = buildReport(w.state) expect(verdictLine(r)).toContain('晨雾') }) it('声望峰值触发盛世措辞', () => { const w = baseWorld('rep-c') w.state.year = 60 w.state.stats.repPeak = 75 for (let y = 1; y <= 12; y++) { w.state.yearlyReports.push({ year: y, nets: 10, births: 1, deaths: 0, rep: 5 + (y >= 8 ? 70 : y), power: 100 }) } const r = buildReport(w.state) // 峰值 75 → 描写盛世 expect(verdictLine(r)).toMatch(/四方来贺/) }) it('太虚榜首写入结语', () => { const w = baseWorld('rep-d') w.state.stats.tourneyHistory = [{ year: 40, rank: 1 }] for (let y = 1; y <= 8; y++) { w.state.yearlyReports.push({ year: y, nets: 0, births: 1, deaths: 0, rep: 5, power: 50 }) } const r = buildReport(w.state) expect(verdictLine(r)).toContain('第1名') }) }) describe('存档迁移管线', () => { it('CURRENT_SCHEMA=2 且 v1→v2 迁移补字段', () => { expect(CURRENT_SCHEMA).toBe(2) const w = baseWorld('mig-a') const asV1 = JSON.parse(JSON.stringify(w.state)) asV1.schemaVersion = 1 delete asV1.stats const res = migrateIfNeeded(asV1) expect(res.ok).toBe(true) const s = (res as never as { state: { stats: unknown; schemaVersion: number } }).state expect(s.stats).toBeTruthy() expect(s.schemaVersion).toBe(2) }) it('未来版本拒绝加载(防误用)', () => { const w = baseWorld('mig-b') const s = JSON.parse(JSON.stringify(w.state)) s.schemaVersion = 99 const res = migrateIfNeeded(s) expect(res.ok).toBe(false) expect((res as never as { error: { code: string } }).error.code).toBe('TOO_NEW') }) it('导出信封带 appId 与版本;导入旧版可迁移', () => { const w = baseWorld('mig-c') w.state.schemaVersion = CURRENT_SCHEMA const env = JSON.parse(exportEnvelope(w.state)) expect(env.app).toBe(APP_ID) expect(env.schemaVersion).toBe(2) const parsed = parseImportEnvelope(JSON.stringify(env)) expect(parsed.ok).toBe(true) }) it('异 app 拒绝导入', () => { const w = baseWorld('mig-d') const env = JSON.parse(exportEnvelope(w.state)) env.app = 'other-game' const parsed = parseImportEnvelope(JSON.stringify(env)) expect(parsed.ok).toBe(false) }) it('损坏 JSON 拒绝导入', () => { const res = parseImportEnvelope('not-json-wrapper') expect(res.ok).toBe(false) }) }) describe('战斗阵型', () => { it.each(Object.keys(FORMATIONS).map((k) => [k as FormationId] as const))('%s 阵型定义合法', (id) => { const f = FORMATIONS[id] expect(f.atk).toBeGreaterThan(0.9) expect(f.atk).toBeLessThanOrEqual(1.1) expect(f.name.length).toBeGreaterThan(0) }) it('未知阵型回退锋阵', () => { expect(formationById('xxx').id).toBe('vanguard') expect(formationById(undefined).id).toBe('vanguard') }) it('锋阵强攻:胜率期望更高(多次模拟占比)', () => { let vanguardWins = 0 let serpentWins = 0 const enemy = ENEMIES.find((e) => e.id === 'e-muche')! for (let trial = 0; trial < 40; trial++) { const w = baseWorld(`form-a-${trial}`) const c = w.state.members['x4'] c.realm = { major: 'foundation', minor: 1 } const r1 = resolveEncounter(w, { title: 't', enemy, risk: 0.78, team: [c], kind: 'scout', year: 1, month: 1, formation: 'vanguard' }) if (r1.win) vanguardWins++ const w2 = baseWorld(`form-b-${trial}`) const c2 = w2.state.members['x4'] c2.realm = { major: 'foundation', minor: 1 } const r2 = resolveEncounter(w2, { title: 't', enemy, risk: 0.78, team: [c2], kind: 'scout', year: 1, month: 1, formation: 'serpent' }) if (r2.win) serpentWins++ } expect(vanguardWins).toBeGreaterThan(serpentWins) }) it('战报首行包含阵名', () => { const w = baseWorld('form-c') const c = w.state.members['x4'] c.realm = { major: 'core', minor: 0 } const res = resolveEncounter(w, { title: '阅兵', enemy: ENEMIES.find((e) => e.id === 'e-huiyuan')!, risk: 0.1, team: [c], kind: 'scout', year: 1, month: 1, formation: 'echelon' }) expect(res.lines.join(' ')).toContain('雁阵') }) it('squad formation 传递到 mission 记录', () => { const w = baseWorld('form-d') const ok = sendMission(w, 'm-anmoku', ['x1'], 'serpent') expect(ok).toBe(true) expect(w.state.missions[0].formation).toBe('serpent') }) }) describe('冬至岁祷', () => { it('11 月触发岁祷(60% 概率下抽样命中或封缄)', () => { const w = baseWorld('win-a') w.state.year = 3 w.state.month = 10 let fired = false for (let i = 0; i < 12; i++) { w.advanceMonth() if (w.state.pendingEvent === 'ev-winterprayer') { fired = true break } w.state.pendingEvent = undefined } expect(fired).toBe(true) // 0.6 概率在 12 次尝试下几乎必中(月每月 11 一次,12次意味着 years 3-4) }) it('观星:声望+3', () => { const w = baseWorld('win-b') applyEventChoice(w, 'ev-winterprayer', 0) expect(w.state.family.reputation).toBeGreaterThan(5) }) it('祈福:全族修为+2', () => { const w = baseWorld('win-c') const c = w.state.members['x5'] c.realm = { major: 'qi', minor: 1 } const p0 = c.realmProgress c.realmProgress = 20 applyEventChoice(w, 'ev-winterprayer', 1) expect(c.realmProgress).toBeGreaterThanOrEqual(p0) }) it('问卜:吉凶均有确定性分支(连测无越界)', () => { for (let i = 0; i < 10; i++) { const w = baseWorld(`win-d-${i}`) applyEventChoice(w, 'ev-winterprayer', 2) expect(w.state.family.stones).toBeGreaterThanOrEqual(760) // 800±60 → ≥740? 800-40=760 expect(w.state.family.stones).toBeLessThanOrEqual(860) expect(w.state.family.flag['prayerAsk']).toBeUndefined() } }) })