Files
ChronicleOfTheImmortalClan/tests/combat-missions.test.ts
T
thzxx 09cbad6449 test: 测试矩阵扩至 161 项(深度审计 + 5 处缺陷修复)
新增测试矩阵(12 文件 / 161 cases):
- rng 12 / data 18 / pcgen+cultivation 20 / economy 12 / combat+missions 24
- marriage+diplomacy 13 / events 12 / world+lifecycle 8(含镜像一致)/ save 4+5 / audit 8

审计带出并修复的真实缺陷:
1. Rng.pick 空数组未报错(防空白数组进而崩溃难查)
2. combatPowerOf 未应用功法悟道品阶战力加成(0.1.2 设计缺口)
3. 重伤者即使不代表伤状态仍按满速修炼(health 进修炼公式)
4. 寡妇/鳏夫无法再婚:候选列表放着人执行层却拒婚(spouseId 死旧契约)
5. isWidowed 参数契约陷阱:传 id 静默 false(升级接受 Character|Id 双签名)
2026-08-23 08:47:21 +08:00

202 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { combatPowerOf, resolveEncounter, resolveRaid, rollWarbooty } from '../src/renderer/game/engine/systems/combat'
import { ENEMIES } from '../src/renderer/game/data/secrets'
import { sendMission, recallAll } from '../src/renderer/game/engine/systems/missions'
import { missionById } from '../src/renderer/game/data/secrets'
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('combat 战斗结算', () => {
it('战力复数化:高装备>低装备,闭关者加成累计', () => {
const w = baseWorld('cb-a')
const c = w.state.members['x4']
const p0 = combatPowerOf(w, c)
w.state.family.inventory['weapon-qi'] = 3
w.equip(c.id, 'weapon-qi')
const p1 = combatPowerOf(w, c)
expect(p1).toBeGreaterThan(p0)
c.techniqueRank = 2
expect(combatPowerOf(w, c)).toBeGreaterThan(p1)
})
it('重伤员战力折减(health 因子)', () => {
const w = baseWorld('cb-b')
const c = w.state.members['x3']
const p0 = combatPowerOf(w, c)
c.health = 20
const p1 = combatPowerOf(w, c)
expect(p1).toBeLessThan(p0)
expect(p1).toBeGreaterThan(p0 * 0.3)
})
it('家族战力随供奉职事提升', () => {
const w = baseWorld('cb-c')
const before = w.familyPower()
w.assignPost('x3', 'guardian')
w.assignPost('x4', 'guardian')
expect(w.familyPower()).toBeGreaterThan(before)
})
it('遭遇战必胜路径产出战利并写入战报', () => {
const w = baseWorld('cb-d')
const c = w.state.members['x4']
c.realm = { major: 'foundation', minor: 0 }
c.health = 100
const res = resolveEncounter(w, {
title: '模拟遭遇',
enemy: ENEMIES.find((e) => e.id === 'e-huiyuan')!,
risk: 0.2,
team: [c],
kind: 'scout',
year: w.state.year,
month: w.state.month
})
expect(res.win).toBe(true)
expect(res.loot).toBeTruthy()
expect(w.state.battles.length).toBe(1)
expect(w.state.battles[0].lines.length).toBeGreaterThan(2)
})
it('遭遇战必败路径全体重伤/损失且不崩塌', () => {
const w = baseWorld('cb-e')
const c = w.state.members['x3']
c.realm = { major: 'mortal', minor: 0 }
c.health = 50
const res = resolveEncounter(w, {
title: '必败模拟',
enemy: ENEMIES.find((e) => e.id === 'e-kuangzun')!,
risk: 1,
team: [c],
kind: 'scout',
year: w.state.year,
month: w.state.month
})
expect(res.win).toBe(false)
expect(c.health).toBeLessThanOrEqual(50)
expect(w.state.battles[w.state.battles.length - 1].winner).toBe('enemy')
})
it('raid 后关系变化方向正确', () => {
const w = baseWorld('cb-f')
const npc = w.state.npcFamilies['n-nulei']
npc.relation = -60
const pre = npc.power
const team = [w.state.members['x4']]
team[0].realm = { major: 'core', minor: 0 }
const res = resolveRaid(w, 'n-nulei', team)
if (res.win) {
expect(npc.relation).toBeGreaterThan(-60)
} else {
expect(npc.relation).toBeLessThan(-60)
expect(w.state.family.stones).toBeLessThanOrEqual(1000)
}
void pre
})
it('rollWarbooty 概率性产物写入库存或功法阁', () => {
const w = baseWorld('cb-g')
const loot = rollWarbooty(w, {
resources: { lingcao: [5, 5] },
artifactChance: 1,
techniqueChance: 1
})
expect(loot.lingcao).toBe(5)
const arti = Object.keys(loot).find((k) => k.startsWith('weapon'))
expect(arti).toBeTruthy()
expect(w.state.family.techniques.length).toBeGreaterThan(2)
})
it('战斗人数攻击力线性叠加(阵型抖动内保持单调)', () => {
const w = baseWorld('cb-h')
const a = w.state.members['x3']
const b = w.state.members['x4']
const solo = combatPowerOf(w, a)
const duo = combatPowerOf(w, a) + combatPowerOf(w, b)
expect(duo).toBeGreaterThan(solo)
})
})
describe('missions 探索任务', () => {
it('派遣属性校验:人数不足/重复派遣/并发上限', () => {
const w = baseWorld('ms-a')
expect(sendMission(w, 'm-guzhan', ['x1'])).toBe(false) // 不足 min 2
expect(sendMission(w, 'm-guzhan', ['x3', 'x4'])).toBe(true)
expect(sendMission(w, 'm-guzhan', ['x4', 'x5'])).toBe(false) // x4 已在途
expect(sendMission(w, 'm-anmoku', ['x4'])).toBe(false) // 在职者二次征召被拒
})
it('队伍出发后成员状态为出探,无法再被派', () => {
const w = baseWorld('ms-b')
sendMission(w, 'm-anmoku', ['x3'])
expect(w.state.members['x3'].state).toBe('expedition')
expect(sendMission(w, 'm-anmoku', ['x3', 'x5'])).toBe(false)
})
it('任务按期推进并最终完成,战利入库且状态归位', () => {
const w = baseWorld('ms-c')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 5 } // 保证能打
const squad = [c, w.state.members['x3']]
sendMission(w, 'm-anmoku', squad.map((x) => x.id))
const m = w.state.missions[0]
let guard = 0
while (!m.done && guard < 40) {
if (w.state.gameOver) break
w.advanceMonth()
guard++
}
expect(m.done).toBe(true)
expect(['success', 'retreat', 'disband']).toContain(m.result)
// 若成功则状态归位闲居
if (m.result === 'success') {
expect(w.state.members['x3'].state).not.toBe('expedition')
expect(w.state.members['x5'].state).not.toBe('expedition')
}
})
it('召回立刻释放队员', () => {
const w = baseWorld('ms-d')
sendMission(w, 'm-anmoku', ['x3'])
const m = w.state.missions[0]
recallAll(w, m.id)
expect(m.done).toBe(true)
expect(w.state.members['x3'].state).toBe('idle')
})
it('全员告急:队员尽亡任务仍完成不挂死(保护)', () => {
const w = baseWorld('ms-e')
sendMission(w, 'm-anmoku', ['x3'])
const m = w.state.missions[0]
w.state.members['x3'].alive = false
w.state.members['x3'].deathYear = w.state.year
w.advanceMonth()
expect(m.done).toBe(true)
})
it('秘境阶段索引不会越界', () => {
const w = baseWorld('ms-f')
const def = missionById('m-anmoku')
const c = w.state.members['x5']
c.realm = { major: 'foundation', minor: 0 }
sendMission(w, 'm-anmoku', [c.id])
const m = w.state.missions[0]
let guard = 0
while (!m.done && guard < 60) {
w.advanceMonth()
guard++
if (m.stage >= def.stages.length + 1 && !m.done) {
// 不允许无限加厚
break
}
}
expect(m.stage).toBeLessThanOrEqual(def.stages.length)
})
})