From f38669fa4a3dfa141869f9fdacd17ac0d5f601a1 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Sun, 23 Aug 2026 08:34:13 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.3:=20=E6=B7=B1=E5=BA=A6=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=88=E5=85=BC=E5=AE=B9/=E8=A1=80?= =?UTF-8?q?=E7=BC=98/=E8=81=94=E5=A7=BB/=E8=BF=9C=E5=BE=81/=E6=95=B0?= =?UTF-8?q?=E5=80=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 旧档兼容:<0.1.1 存档缺 finance/yearStats/yearlyReports 字段加载归一化(防运行期崩溃) - 血缘防护:同母异父兄妹也禁止婚配(指婚/媒人/联姻三路齐封) - 联姻修正:一家一姻亲(allied 锁),再娶再嫁不再无限刷;男儿娶亲也子孙来归 - 和约语义:议和强推关系至 +30(真停战),不再打完一场还是仇雠 - 远征收队:任务完成/召回自动释放队员闲居;重伤者当月离队疗伤、不足定员提前撤队(修复人员永久困远征/卡状态) - 数值:婴儿夭折率 2%→0.8%;劫掠队强度 0.9→0.78;事件负资源钳制不为负灵石 - UI:丹药按钮显示持有并禁点;点将模式支持取消重选;设置页移除摆设项 - 新增 audit 套件:8 项(含 600 月长跑耐力)总测试 38→46 全绿 --- package.json | 2 +- src/main/index.ts | 10 -- src/renderer/game/engine/creation.ts | 2 +- src/renderer/game/engine/systems/combat.ts | 8 +- src/renderer/game/engine/systems/diplomacy.ts | 9 +- src/renderer/game/engine/systems/events.ts | 2 +- src/renderer/game/engine/systems/lifecycle.ts | 2 +- src/renderer/game/engine/systems/marriage.ts | 14 ++- src/renderer/game/engine/systems/missions.ts | 47 ++++++- src/renderer/game/engine/world.ts | 21 ++++ src/renderer/ui/components/EventModal.tsx | 36 ++++-- src/renderer/ui/components/MemberModal.tsx | 18 ++- src/renderer/ui/panels/SettingsPanel.tsx | 5 - tests/audit.test.ts | 118 ++++++++++++++++++ 14 files changed, 242 insertions(+), 52 deletions(-) create mode 100644 tests/audit.test.ts diff --git a/package.json b/package.json index 57ed141..88ba801 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/index.ts b/src/main/index.ts index 7dc65c9..951222b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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}`) diff --git a/src/renderer/game/engine/creation.ts b/src/renderer/game/engine/creation.ts index 5f52ae1..1ca9aba 100644 --- a/src/renderer/game/engine/creation.ts +++ b/src/renderer/game/engine/creation.ts @@ -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' diff --git a/src/renderer/game/engine/systems/combat.ts b/src/renderer/game/engine/systems/combat.ts index 23e5636..eb794fa 100644 --- a/src/renderer/game/engine/systems/combat.ts +++ b/src/renderer/game/engine/systems/combat.ts @@ -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 } diff --git a/src/renderer/game/engine/systems/diplomacy.ts b/src/renderer/game/engine/systems/diplomacy.ts index e72c242..8925182 100644 --- a/src/renderer/game/engine/systems/diplomacy.ts +++ b/src/renderer/game/engine/systems/diplomacy.ts @@ -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) diff --git a/src/renderer/game/engine/systems/events.ts b/src/renderer/game/engine/systems/events.ts index e41ef4b..31ea4be 100644 --- a/src/renderer/game/engine/systems/events.ts +++ b/src/renderer/game/engine/systems/events.ts @@ -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) } } diff --git a/src/renderer/game/engine/systems/lifecycle.ts b/src/renderer/game/engine/systems/lifecycle.ts index 0d05164..63ca7f3 100644 --- a/src/renderer/game/engine/systems/lifecycle.ts +++ b/src/renderer/game/engine/systems/lifecycle.ts @@ -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 diff --git a/src/renderer/game/engine/systems/marriage.ts b/src/renderer/game/engine/systems/marriage.ts index d7d3f73..030387e 100644 --- a/src/renderer/game/engine/systems/marriage.ts +++ b/src/renderer/game/engine/systems/marriage.ts @@ -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) diff --git a/src/renderer/game/engine/systems/missions.ts b/src/renderer/game/engine/systems/missions.ts index 05100ad..eba1526 100644 --- a/src/renderer/game/engine/systems/missions.ts +++ b/src/renderer/game/engine/systems/missions.ts @@ -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}人手不足,队伍提前收队。`) + } +} diff --git a/src/renderer/game/engine/world.ts b/src/renderer/game/engine/world.ts index 47acf65..35e17ba 100644 --- a/src/renderer/game/engine/world.ts +++ b/src/renderer/game/engine/world.ts @@ -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) diff --git a/src/renderer/ui/components/EventModal.tsx b/src/renderer/ui/components/EventModal.tsx index 8036e76..51132d6 100644 --- a/src/renderer/ui/components/EventModal.tsx +++ b/src/renderer/ui/components/EventModal.tsx @@ -79,16 +79,34 @@ export function EventModal() { )}