结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import { buildReport, verdictLine } from '../src/renderer/game/engine/narrative/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/runtime/Systems/combat'
|
|
import { ENEMIES } from '../src/renderer/game/data/secrets'
|
|
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
|
import { sendMission } from '../src/renderer/game/engine/runtime/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()
|
|
}
|
|
})
|
|
})
|