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 双签名)
This commit is contained in:
2026-08-23 08:47:21 +08:00
parent f38669fa4a
commit 09cbad6449
14 changed files with 1605 additions and 40 deletions
+2
View File
@@ -52,6 +52,7 @@ export class Rng {
}
pick<T>(arr: T[]): T {
if (arr.length === 0) throw new Error('Rng.pick: 空数组无可选项')
return arr[this.int(0, arr.length - 1)]
}
@@ -61,6 +62,7 @@ export class Rng {
shuffle<T>(arr: T[]): T[] {
const a = [...arr]
if (a.length <= 1) return a
for (let i = a.length - 1; i > 0; i--) {
const j = this.int(0, i)
const t = a[i]
+2 -1
View File
@@ -13,7 +13,8 @@ export function combatPowerOf(w: World, c: Character): number {
const base = basePower(c.realm)
const stat = 1 + (c.perception + c.physique) / 32
const tech = techniqueById(c.techniqueId)
const techBonus = tech ? 1 + tech.powerBonus : 1
const wuRank = (c.techniqueRank ?? 0) > 1 ? 0.2 : (c.techniqueRank ?? 0) === 1 ? 0.08 : 0
const techBonus = tech ? 1 + tech.powerBonus + wuRank : 1
const equip = c.equipment ? 1 + (ARTIFACT_POWER[c.equipment] ?? 0) : 1
const trait = 1 + traitBonuses(c).windBonus
const health = 0.5 + 0.5 * (c.health / 100)
@@ -36,6 +36,11 @@ export function monthlyRate(w: World, c: Character): number {
} else if (c.state === 'wounded') {
rate *= c.health > 40 ? 0.5 : 0.15
}
// 带伤不愈者难以静修(与状态无关的直观削率)
if (c.alive && c.state !== 'wounded') {
if (c.health <= 40) rate *= 0.45
else if (c.health <= 70) rate *= 0.8
}
if (w.ageOf(c) < 8) rate *= 0.4
if (w.ageOf(c) > 55) rate *= 0.7
rate *= masteryRateOfMajor(c.realm.major)
@@ -62,7 +62,7 @@ export function marryNpcFamily(w: World, npcId: string): boolean {
const eligible = w
.aliveMembers()
.filter((c) => w.ageOf(c) >= 16 && w.ageOf(c) <= 46 && c.state !== 'expedition')
.filter((c) => !c.spouseId)
.filter((c) => !c.spouseId || w.isWidowed(c))
if (eligible.length === 0) return false
const npcDef = npcById(npcId)
const candidate = w.rng.pick(eligible)
@@ -79,7 +79,9 @@ export function marryNpcFamily(w: World, npcId: string): boolean {
export function arrangeWedding(w: World, aId: string, bId: string): boolean {
const a = w.memberById(aId)
const b = w.memberById(bId)
if (!a.alive || !b.alive || a.spouseId || b.spouseId) return false
if (!a.alive || !b.alive) return false
if (a.spouseId && !w.isWidowed(a)) return false
if (b.spouseId && !w.isWidowed(b)) return false
if (a.gender === b.gender) return false
if (a.fatherId === b.fatherId && a.fatherId) return false
if (a.motherId === b.motherId && a.motherId) return false
+13 -4
View File
@@ -414,9 +414,10 @@ export class World {
return true
}
isWidowed(member: Character): boolean {
if (!member.spouseId) return false
const sp = this.state.members[member.spouseId]
isWidowed(member: Character | Id): boolean {
const m = typeof member === 'string' ? this.state.members[member] : member
if (!m || !m.spouseId) return false
const sp = this.state.members[m.spouseId]
return !!sp && !sp.alive
}
@@ -461,10 +462,18 @@ function w2age(w: World, c: Character): number {
return w.ageOf(c)
}
function widowedOf(w: World, m: Character): boolean {
if (!m.spouseId) return false
const sp = w.state.members[m.spouseId]
return !!sp && !sp.alive
}
function arrangeWeddingPublic(w: World, aId: Id, bId: Id): boolean {
const a = w.memberById(aId)
const b = w.memberById(bId)
if (!a.alive || !b.alive || a.spouseId || b.spouseId) return false
if (!a.alive || !b.alive) return false
if (a.spouseId && !widowedOf(w, a)) return false
if (b.spouseId && !widowedOf(w, b)) return false
if (a.gender === b.gender) return false
if (a.fatherId && a.fatherId === b.fatherId) return false
if (a.motherId && a.motherId === b.motherId) return false