v0.1.3: 深度审计修复(兼容/血缘/联姻/远征/数值)
- 旧档兼容:<0.1.1 存档缺 finance/yearStats/yearlyReports 字段加载归一化(防运行期崩溃) - 血缘防护:同母异父兄妹也禁止婚配(指婚/媒人/联姻三路齐封) - 联姻修正:一家一姻亲(allied 锁),再娶再嫁不再无限刷;男儿娶亲也子孙来归 - 和约语义:议和强推关系至 +30(真停战),不再打完一场还是仇雠 - 远征收队:任务完成/召回自动释放队员闲居;重伤者当月离队疗伤、不足定员提前撤队(修复人员永久困远征/卡状态) - 数值:婴儿夭折率 2%→0.8%;劫掠队强度 0.9→0.78;事件负资源钳制不为负灵石 - UI:丹药按钮显示持有并禁点;点将模式支持取消重选;设置页移除摆设项 - 新增 audit 套件:8 项(含 600 月长跑耐力)总测试 38→46 全绿
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chronicle-of-the-immortal-clan",
|
||||
"productName": "仙途家族志",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "MetonaTeam",
|
||||
|
||||
@@ -54,16 +54,6 @@ function createWindow(): void {
|
||||
wc.once('did-finish-load', () => {
|
||||
clearTimeout(smokeWatchdog)
|
||||
})
|
||||
void smokeWatchdog
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const url = wc.getURL()
|
||||
const title = await wc.executeJavaScript('document.title').catch((e: Error) => 'ERR:' + e.message)
|
||||
console.log(`[PROBE] url=${url} title=${title}`)
|
||||
} catch (e) {
|
||||
console.log('[PROBE] failed', String(e))
|
||||
}
|
||||
}, 8000)
|
||||
const shotsDir = process.env.SMOKE_SHOTS_DIR
|
||||
wc.on('console-message', (_e, level, message) => {
|
||||
console.log(`[renderer:${level}] ${message}`)
|
||||
|
||||
@@ -153,7 +153,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
uncle.techniqueId = 't-houtu'
|
||||
uncle.traits = ['xinheng', 'shensui']
|
||||
uncle.id = 'x4'
|
||||
uncle.spouseHouse = 'sihai王氏'
|
||||
uncle.spouseHouse = '四海王氏'
|
||||
uncle.children = []
|
||||
|
||||
head.id = 'x1'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { World } from '../world'
|
||||
import { Character, BattleLog, NpcFamilyState } from '../../types/domain'
|
||||
import { Character, BattleLog } from '../../types/domain'
|
||||
import { basePower, describeRealm } from '../../data/realms'
|
||||
import { ARTIFACT_POWER } from '../../data/items'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
@@ -29,10 +29,6 @@ export function enemyPowerOf(enemy: EnemyDef, risk: number): number {
|
||||
return Math.round(base * enemy.strength * (1.05 + risk * 0.55))
|
||||
}
|
||||
|
||||
export function npcPowerOf(npc: NpcFamilyState): number {
|
||||
return Math.round(npc.power)
|
||||
}
|
||||
|
||||
export interface EncounterResult {
|
||||
win: boolean
|
||||
draw: boolean
|
||||
@@ -163,7 +159,7 @@ export function resolveRaid(
|
||||
id: npcId,
|
||||
name: `${npc.name}的劫掠队`,
|
||||
realm: def.leaderRealm,
|
||||
strength: 0.9,
|
||||
strength: 0.78,
|
||||
icon: '袭',
|
||||
desc: def.desc
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export function makePeace(w: World, npcId: string): boolean {
|
||||
const npc = w.state.npcFamilies[npcId]
|
||||
if (fam.stones < 200) return false
|
||||
fam.stones -= 200
|
||||
npc.relation = Math.max(npc.relation + 35, 0)
|
||||
npc.relation = Math.max(npc.relation + 35, 30)
|
||||
w.chronicle('diplomacy', `${npc.name}立下和约,两家罢兵互市。`, undefined, true)
|
||||
w.log('good', `与${npc.name}言和。`)
|
||||
return true
|
||||
@@ -58,16 +58,18 @@ export function marryNpcFamily(w: World, npcId: string): boolean {
|
||||
const s = w.state
|
||||
const fam = s.family
|
||||
const npc = s.npcFamilies[npcId]
|
||||
if (!npc || npc.relation < 25) return false
|
||||
if (!npc || npc.relation < 25 || npc.allied) return false
|
||||
const eligible = w
|
||||
.aliveMembers()
|
||||
.filter((c) => w.ageOf(c) >= 18 && w.ageOf(c) <= 42 && c.state !== 'expedition')
|
||||
.filter((c) => w.ageOf(c) >= 16 && w.ageOf(c) <= 46 && c.state !== 'expedition')
|
||||
.filter((c) => !c.spouseId)
|
||||
if (eligible.length === 0) return false
|
||||
const npcDef = npcById(npcId)
|
||||
const candidate = w.rng.pick(eligible)
|
||||
candidate.spouseHouse = npc.name
|
||||
npc.relation += 20
|
||||
npc.allied = true
|
||||
npc.alliedSinceYear = s.year
|
||||
fam.reputation += 4
|
||||
w.chronicle('marriage', `${candidate.name}与${npc.name}联姻,两家绸缪通好。`, candidate.id, true)
|
||||
w.log('good', `${candidate.name} 与${npc.name}联姻成功!每年或降麟儿。`)
|
||||
@@ -80,6 +82,7 @@ export function arrangeWedding(w: World, aId: string, bId: string): boolean {
|
||||
if (!a.alive || !b.alive || a.spouseId || b.spouseId) 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
|
||||
a.spouseId = b.id
|
||||
b.spouseId = a.id
|
||||
const aAge = w.ageOf(a)
|
||||
|
||||
@@ -210,7 +210,7 @@ function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
|
||||
if (eff.res) {
|
||||
for (const [k, v] of Object.entries(eff.res)) {
|
||||
if (k === 'stones') fam.stones += v
|
||||
if (k === 'stones') fam.stones = Math.max(0, fam.stones + v)
|
||||
else fam.inventory[k] = Math.max(0, (fam.inventory[k] ?? 0) + v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function deathTick(w: World): void {
|
||||
p = Math.min(0.3, Math.pow(t, 5) * 0.9)
|
||||
}
|
||||
if (c.health < 30) p += 0.18
|
||||
if (age < 2) p = Math.max(p, 0.02)
|
||||
if (age < 2) p = Math.max(p, 0.008)
|
||||
if (p > 0 && w.rng.chance(p)) {
|
||||
c.alive = false
|
||||
c.deathYear = w.state.year
|
||||
|
||||
@@ -38,19 +38,18 @@ export function yearStartMarriage(w: World): void {
|
||||
w.log('good', `${fam.surname}家诞下新丁:${first.name}。`)
|
||||
}
|
||||
|
||||
// 联姻外孙来投
|
||||
// 联姻外孙来投(男方娶亲:妻室携子来归;或女儿携子回门)
|
||||
for (const member of Object.values(s.members)) {
|
||||
if (!member.alive || !member.spouseHouse) continue
|
||||
if (member.gender !== 'female') continue
|
||||
const age = w.ageOf(member)
|
||||
if (age < 18 || age > 44) continue
|
||||
if (!w.rng.chance(0.16)) continue
|
||||
if (!w.rng.chance(0.15)) continue
|
||||
const gen = member.generation + 1
|
||||
const house = member.spouseHouse
|
||||
const child = produceOffspring(w, { father: null, mother: null, generation: gen, bornYear: s.year, surname: fam.surname })
|
||||
child.spouseHouse = house
|
||||
w.addMember(child)
|
||||
w.chronicle('birth', `${member.name}自${house}携幼子归来,名唤${child.name}。`, child.id, true)
|
||||
w.chronicle('birth', `${member.name}自${house}携幼子归庄,名唤${child.name}。`, child.id, true)
|
||||
w.state.yearStats.births += 1
|
||||
w.log('info', `${member.name} 领着小辈回门投亲。`)
|
||||
}
|
||||
@@ -78,7 +77,12 @@ export function yearStartMarriage(w: World): void {
|
||||
for (const m of men) {
|
||||
if (w.rng.chance(0.28) && women.length > 0) {
|
||||
const candidate = women.filter(
|
||||
(x) => !(x.fatherId && x.fatherId === m.fatherId) && x !== m && !x.children.includes(m.id) && !m.children.includes(x.id)
|
||||
(x) =>
|
||||
!(x.fatherId && x.fatherId === m.fatherId) &&
|
||||
!(x.motherId && x.motherId === m.motherId) &&
|
||||
x !== m &&
|
||||
!x.children.includes(m.id) &&
|
||||
!m.children.includes(x.id)
|
||||
)
|
||||
if (candidate.length === 0) continue
|
||||
const bride = w.rng.pick(candidate)
|
||||
|
||||
@@ -6,8 +6,10 @@ import { techniqueById } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
|
||||
export function missionTick(w: World): void {
|
||||
const alive = w.state.missions.filter((m) => !m.done)
|
||||
for (const m of alive) {
|
||||
const active = w.state.missions.filter((m) => !m.done)
|
||||
for (const m of active) {
|
||||
settleSquad(w, m)
|
||||
if (m.done) continue
|
||||
m.stageMonth++
|
||||
|
||||
if (m.stageMonth < 2) continue
|
||||
@@ -63,6 +65,7 @@ export function missionTick(w: World): void {
|
||||
if (m.stage >= def.stages.length && !m.done) {
|
||||
m.done = true
|
||||
m.result = 'success'
|
||||
releaseSquad(w, m)
|
||||
const total = rollWarbooty(w, def.completionLoot)
|
||||
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
||||
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
||||
@@ -143,9 +146,41 @@ export function recallAll(w: World, missionId: string): void {
|
||||
if (!m || m.done) return
|
||||
m.done = true
|
||||
m.result = 'recall'
|
||||
m.memberIds.forEach((id) => {
|
||||
const c = w.memberById(id)
|
||||
if (c.alive) c.state = 'idle'
|
||||
})
|
||||
releaseSquad(w, m)
|
||||
w.log('info', '探索队伍奉命返家。')
|
||||
}
|
||||
|
||||
/** 队伍成员状态归位:伤者保持养伤,其余恢复闲居 */
|
||||
function releaseSquad(w: World, m: MissionState): void {
|
||||
for (const id of m.memberIds) {
|
||||
const c = w.memberById(id)
|
||||
if (!c.alive) continue
|
||||
if (c.state === 'wounded') continue
|
||||
c.state = 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
/** 月度巡查:亡者除名、重伤者离队疗伤;人数不足则提前收队 */
|
||||
function settleSquad(w: World, m: MissionState): void {
|
||||
const def = missionById(m.defId)
|
||||
let removed = 0
|
||||
for (let i = m.memberIds.length - 1; i >= 0; i--) {
|
||||
const c = w.memberById(m.memberIds[i])
|
||||
if (!c.alive) {
|
||||
m.memberIds.splice(i, 1)
|
||||
removed++
|
||||
} else if (c.state === 'wounded' || c.health < 30) {
|
||||
c.state = 'wounded'
|
||||
m.memberIds.splice(i, 1)
|
||||
removed++
|
||||
m.log.push(`${c.name} 身负重伤,离队回庄疗养。`)
|
||||
}
|
||||
}
|
||||
if (removed > 0) w.log('info', `${def.name}队中有人离队。`)
|
||||
if (m.memberIds.length < def.minMembers) {
|
||||
m.done = true
|
||||
m.result = 'disband'
|
||||
releaseSquad(w, m)
|
||||
w.log('bad', `${def.name}人手不足,队伍提前收队。`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,31 @@ export interface WorldEventBus {
|
||||
onYearPaper?(entry: YearlyReport): void
|
||||
}
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||
if (!state.finance) state.finance = { accum: 0 }
|
||||
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
|
||||
if (!state.yearlyReports) state.yearlyReports = []
|
||||
if (typeof state.totalTicks !== 'number') state.totalTicks = 0
|
||||
if (typeof state.seq !== 'number') state.seq = 10
|
||||
if (!state.battles) state.battles = []
|
||||
if (!state.eventQueue) state.eventQueue = []
|
||||
if (!state.completedEvents) state.completedEvents = []
|
||||
for (const c of Object.values(state.members)) {
|
||||
if (typeof c.techniqueRank !== 'number') c.techniqueRank = 0
|
||||
if (typeof c.techniqueProgress !== 'number') c.techniqueProgress = 0
|
||||
if (typeof c.health !== 'number') c.health = 100
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export class World {
|
||||
state: GameState
|
||||
rng: Rng
|
||||
out: WorldEventBus[]
|
||||
|
||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
||||
normalizeGameState(state)
|
||||
this.state = state
|
||||
this.rng = new Rng(state.rng)
|
||||
this.out = out
|
||||
@@ -411,6 +430,7 @@ export class World {
|
||||
.filter(
|
||||
(c) =>
|
||||
!(me.fatherId && me.fatherId === c.fatherId) &&
|
||||
!(me.motherId && me.motherId === c.motherId) &&
|
||||
!me.children.includes(c.id) &&
|
||||
!c.children.includes(me.id) &&
|
||||
me.id !== c.id
|
||||
@@ -447,6 +467,7 @@ function arrangeWeddingPublic(w: World, aId: Id, bId: Id): boolean {
|
||||
if (!a.alive || !b.alive || a.spouseId || b.spouseId) 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
|
||||
a.spouseId = b.id
|
||||
b.spouseId = a.id
|
||||
const aAge = w.ageOf(a)
|
||||
|
||||
@@ -79,16 +79,34 @@ export function EventModal() {
|
||||
)}
|
||||
|
||||
<div className="modal-opts">
|
||||
{pendingEventDef.options.map((o, i) => {
|
||||
{squadPicking ? (
|
||||
<>
|
||||
<button className="opt-btn" onClick={() => choose(raidIdx)}>
|
||||
出战!(点将已毕,共{squadSel.length}人)
|
||||
<span className="hint">依所选人手迎击来犯之敌</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setSquadPicking(false)
|
||||
setSquadSel([])
|
||||
}}
|
||||
>
|
||||
返回重选
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
pendingEventDef.options.map((o, i) => {
|
||||
const isRaid = raidIdx === i
|
||||
return (
|
||||
<button key={i} className="opt-btn" onClick={() => choose(i)}>
|
||||
{isRaid && squadPicking ? '出战!(点将已毕)' : o.label}
|
||||
{isRaid && !squadPicking && <span className="hint">点将迎战 · 可自选阵容</span>}
|
||||
{o.label}
|
||||
{isRaid && <span className="hint">点将迎战 · 可自选阵容</span>}
|
||||
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,10 @@ import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/systems/lifecycle'
|
||||
import { POSTS, POST_ORDER } from '../../game/data/posts'
|
||||
|
||||
function pillCount(s: GameState, id: string): number {
|
||||
return s.family.inventory[id] ?? 0
|
||||
}
|
||||
|
||||
function equipableItems(member: Character, s: GameState): string[] {
|
||||
const inv = Object.keys(ARTIFACT_POWER).filter((ai) => (s.family.inventory[ai] ?? 0) > 0)
|
||||
if (member.equipment && !inv.includes(member.equipment)) inv.push(member.equipment)
|
||||
@@ -104,33 +108,39 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={pillCount(s, 'pill-qiyuan') <= 0}
|
||||
title={`聚气丹×${pillCount(s, 'pill-qiyuan')}`}
|
||||
onClick={() => {
|
||||
w.takePill(member.id, 'pill-qiyuan')
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
服聚气丹
|
||||
服聚气丹({pillCount(s, 'pill-qiyuan')})
|
||||
</button>
|
||||
{member.realm.major !== 'mortal' && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={pillCount(s, 'pill-ningyuan') <= 0}
|
||||
title={`凝元丹×${pillCount(s, 'pill-ningyuan')}`}
|
||||
onClick={() => {
|
||||
w.takePill(member.id, 'pill-ningyuan')
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
服凝元丹
|
||||
服凝元丹({pillCount(s, 'pill-ningyuan')})
|
||||
</button>
|
||||
)}
|
||||
{member.realmProgress >= 100 && member.realm.major !== 'mortal' && (
|
||||
{member.realmProgress >= 70 && member.realm.major !== 'mortal' && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={pillCount(s, 'pill-pojing') <= 0}
|
||||
title={`破境丹×${pillCount(s, 'pill-pojing')}`}
|
||||
onClick={() => {
|
||||
w.takePill(member.id, 'pill-pojing')
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
服破境丹
|
||||
服破境丹({pillCount(s, 'pill-pojing')})
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -89,11 +89,6 @@ export default function SettingsPanel() {
|
||||
主菜单
|
||||
</button>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<span>立即推送修复(保存完成提示)</span>
|
||||
<button className="btn btn-sm" onClick={bump}>重绘</button>
|
||||
</div>
|
||||
|
||||
<div className="card-title" style={{ marginTop: 20 }}>玩法说明</div>
|
||||
<div className="help-text">
|
||||
· 日常以「月」流转:成员修炼、建筑产出、往事兴替、事件沓来。<br />
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World, normalizeGameState } from '../src/renderer/game/engine/world'
|
||||
import {
|
||||
marryNpcFamily,
|
||||
makePeace
|
||||
} from '../src/renderer/game/engine/systems/diplomacy'
|
||||
import { sendMission, recallAll } from '../src/renderer/game/engine/systems/missions'
|
||||
import { applyEventChoice } from '../src/renderer/game/engine/systems/events'
|
||||
import { GameState } from '../src/renderer/game/types/domain'
|
||||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||||
|
||||
describe('0.1.3 audit regression', () => {
|
||||
it('loads a 0.1.0-era save (missing finance/yearStats fields)', () => {
|
||||
const w = World.create({ seed: 'legacy', surname: '崔', familyName: '崔家', motto: 'm', difficulty: 'normal' })
|
||||
const legacy = JSON.parse(JSON.stringify(w.state)) as GameState
|
||||
delete (legacy as Record<string, unknown>)['finance']
|
||||
delete (legacy as Record<string, unknown>)['yearStats']
|
||||
delete (legacy as Record<string, unknown>)['yearlyReports']
|
||||
const w2 = new World(legacy)
|
||||
for (let i = 0; i < 40; i++) w2.advanceMonth()
|
||||
expect(w2.state.yearlyReports.length).toBeGreaterThan(0)
|
||||
expect(Number.isNaN(w2.state.family.stones)).toBe(false)
|
||||
})
|
||||
|
||||
it('marries a npc family once only (allied lock)', () => {
|
||||
const w = World.create({ seed: 'hy', surname: '卫', familyName: '卫家', motto: 'm', difficulty: 'normal' })
|
||||
const npc = w.state.npcFamilies['n-danxin']
|
||||
npc.relation = 60
|
||||
// 姑且让长子成年(18岁可婚)
|
||||
w.state.members['x3'].bornYear = 1 - 18
|
||||
expect(marryNpcFamily(w, 'n-danxin')).toBe(true)
|
||||
expect(npc.allied).toBe(true)
|
||||
expect(marryNpcFamily(w, 'n-danxin')).toBe(false)
|
||||
})
|
||||
|
||||
it('or same-mother siblings cannot marry (candidates + direct)', () => {
|
||||
const w = World.create({ seed: 'kin', surname: '陆', familyName: '陆家', motto: 'm', difficulty: 'normal' })
|
||||
const a = w.state.members['x3']
|
||||
const b = w.state.members['x5']
|
||||
// 人为制造同母异父关系
|
||||
a.motherId = 'x2'
|
||||
b.motherId = 'x2'
|
||||
a.fatherId = undefined
|
||||
b.fatherId = undefined
|
||||
expect(w.marriageCandidatesOf(a.id).map((c) => c.id)).not.toContain(b.id)
|
||||
expect(w.marryTo(a.id, b.id)).toBe(false)
|
||||
})
|
||||
|
||||
it('peace cements relation to a floor above zero', () => {
|
||||
const w = World.create({ seed: 'peace', surname: '华', familyName: '华家', motto: 'm', difficulty: 'normal' })
|
||||
const npc = w.state.npcFamilies['n-nulei']
|
||||
npc.relation = -80
|
||||
w.state.family.stones = 900
|
||||
expect(makePeace(w, 'n-nulei')).toBe(true)
|
||||
expect(npc.relation).toBeGreaterThanOrEqual(30)
|
||||
})
|
||||
|
||||
it('event negative resource cannot sink stones below zero', () => {
|
||||
const w = World.create({ seed: 'clamp', surname: '曾', familyName: '曾家', motto: 'm', difficulty: 'normal' })
|
||||
w.state.family.stones = 30
|
||||
applyEventChoice(w, 'ev-raid-n-nulei', 1) // 割地求和 -250
|
||||
expect(w.state.family.stones).toBe(0)
|
||||
})
|
||||
|
||||
it('expedition squad releases members and drops wounded', () => {
|
||||
const w = World.create({ seed: 'exp', surname: '纪', familyName: '纪家', motto: 'm', difficulty: 'normal' })
|
||||
resetSaveBus()
|
||||
attachLogSink(w)
|
||||
const ok = sendMission(w, 'm-anmoku', ['x1', 'x3'])
|
||||
expect(ok).toBe(true)
|
||||
const m = w.state.missions[0]
|
||||
expect(m.memberIds.length).toBe(2)
|
||||
// 制造重伤:让一人带伤退出
|
||||
const c = w.state.members['x3']
|
||||
c.health = 10
|
||||
c.state = 'wounded'
|
||||
w.advanceMonth()
|
||||
expect(m.memberIds).not.toContain('x3')
|
||||
expect(w.state.members['x3'].state).toBe('wounded')
|
||||
// 剩余的 x1 一人 ≥ minMembers=1 可以继续;test recall releases
|
||||
recallAll(w, m.id)
|
||||
expect(w.state.members['x1'].state).toBe('idle')
|
||||
})
|
||||
|
||||
it('squad disband when members run short', () => {
|
||||
const w = World.create({ seed: 'exp2', surname: '窦', familyName: '窦家', motto: 'm', difficulty: 'normal' })
|
||||
resetSaveBus()
|
||||
attachLogSink(w)
|
||||
const ok = sendMission(w, 'm-xuangu', ['x1', 'x3']) // minMembers 1
|
||||
expect(ok).toBe(true)
|
||||
const m = w.state.missions[0]
|
||||
// 两人同时重伤离队
|
||||
w.state.members['x1'].health = 10
|
||||
w.state.members['x1'].state = 'wounded'
|
||||
w.state.members['x3'].health = 10
|
||||
w.state.members['x3'].state = 'wounded'
|
||||
w.advanceMonth()
|
||||
expect(m.done).toBe(true)
|
||||
expect(m.result).toBe('disband')
|
||||
})
|
||||
|
||||
it('long run 600 months stays sane', () => {
|
||||
const w = World.create({ seed: 'long', surname: '丁', familyName: '丁家', motto: 'm', difficulty: 'normal' })
|
||||
resetSaveBus()
|
||||
attachLogSink(w)
|
||||
for (let i = 0; i < 600; i++) {
|
||||
if (w.state.gameOver) break
|
||||
w.advanceMonth()
|
||||
}
|
||||
const s = w.state
|
||||
expect(Number.isNaN(s.family.stones)).toBe(false)
|
||||
expect(s.year).toBeGreaterThan(30)
|
||||
for (const c of Object.values(s.members)) {
|
||||
expect(Number.isNaN(c.realmProgress)).toBe(false)
|
||||
expect(Number.isNaN(c.health)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user