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() { )}