import { BattleLog, Character, ChronicleEntry, GameState, Id, LogItem, Realm, YearlyReport } from '../types/domain' import { Rng } from '../core/rng' import { BUILDINGS } from '../data/buildings' import { POSTS } from '../data/posts' import { TECHNIQUES } from '../data/techniques' import { aspirationById as aspirationOf } from '../data/aspirations' import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy' import { productionTick } from './systems/production' import { deathTick, woundHealTick } from './systems/lifecycle' import { cultivationTick, resolveBreakthrough } from './systems/cultivation' import { missionTick } from './systems/missions' import { eventRoll, applyEventChoice } from './systems/events' import { diplomacyTick } from './systems/diplomacy' import { yearStartMarriage } from './systems/marriage' import { createWorldState, findInheritor } from './creation' import { combatPowerOf } from './systems/combat' export type LogKind = LogItem['kind'] export interface WorldEventBus { onLog(kind: LogKind, text: string): void onChronicle(entry: ChronicleEntry, important: boolean): void onBattle(log: BattleLog): void onPendingEvent(id: string): void onGameOver(reason: string, year: number): void 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 (!state.stats) { state.stats = { repPeak: state.family?.reputation ?? 0, popPeak: Object.values(state.members).filter((c) => c.alive).length, maxRealmIdx: peakRealmIndex(state.members ?? {}), techniqueGrand: grandTechniqueCount(state.members ?? {}), tourneyHistory: [], feishengCount: 0 } } 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 } seq(): Id { this.state.seq++ return `x${this.state.seq.toString(36)}` } syncRng(): void { this.state.rng = this.rng.getState() } log(kind: LogKind, text: string): void { this.out.forEach((o) => o.onLog(kind, text)) } chronicle(cat: ChronicleEntry['category'], text: string, memberId?: Id, important = false): void { const entry: ChronicleEntry = { id: this.seq(), year: this.state.year, month: this.state.month, category: cat, text, memberId, important } this.state.chronicle.push(entry) this.out.forEach((o) => o.onChronicle(entry, important)) } battle(log: BattleLog): void { this.state.battles.push(log) this.out.forEach((o) => o.onBattle(log)) } pendingEvent(id: string): void { this.out.forEach((o) => o.onPendingEvent(id)) } gameOver(reason: string, year: number): void { this.state.gameOver = { year, reason } this.out.forEach((o) => o.onGameOver(reason, year)) } memberById(id: Id): Character { const c = this.state.members[id] if (!c) throw new Error(`member not found ${id}`) return c } aliveMembers(): Character[] { return Object.values(this.state.members).filter((c) => c.alive) } ageOf(c: Character): number { return this.state.year - c.bornYear } head(): Character { return this.memberById(this.state.family.headId) } advanceMonth(): void { const s = this.state const stonesStart = s.family.stones s.month++ if (s.month > 12) { s.month = 1 s.year++ this.yearStart() } s.totalTicks++ productionTick(this) deathTick(this) woundHealTick(this) if (this.aliveMembers().length > 0) { cultivationTick(this) missionTick(this) eventRoll(this) diplomacyTick(this) } this.checkHead() const stonesEnd = s.family.stones s.finance.accum += stonesEnd - stonesStart this.trackStats() } private trackStats(): void { const s = this.state const st = s.stats if (s.family.reputation > st.repPeak) st.repPeak = s.family.reputation const pop = this.aliveMembers().length if (pop > st.popPeak) st.popPeak = pop const idx = peakRealmIndex(s.members) if (idx > st.maxRealmIdx) st.maxRealmIdx = idx const g = grandTechniqueCount(s.members) if (g > st.techniqueGrand) st.techniqueGrand = g } legacyPreview() { return computeLegacy(this.state) } resolveLegacyNow(): LegacyArch { const arch = resolveLegacy(this.state) this.state.stats.resolvedYear = this.state.year this.state.stats.resolveTitle = arch.title this.state.family.flag['resolved'] = true this.chronicle('event', `望气观澜,本族百年气数终有定论——「${arch.title}」。开卷盖印,史入青册。`, undefined, true) this.log('good', `定鼎:${arch.title}。`) return arch } private yearStart(): void { yearStartMarriage(this) this.state.family.reputation += this.postBonus('familyRep') const zhenCount = this.aliveMembers().filter((c) => aspirationOf(c.aspiration)?.effect.type === 'rep').length this.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100 const rep = this.state.family.reputation const power = this.familyPower() const report: YearlyReport = { year: this.state.year - 1, nets: this.state.finance.accum, births: this.state.yearStats.births, deaths: this.state.yearStats.deaths, rep, power } this.state.yearlyReports.push(report) if (this.state.yearlyReports.length > 80) { this.state.yearlyReports.shift() } this.state.finance.accum = 0 this.state.yearStats = { births: 0, deaths: 0 } this.out.forEach((o) => o.onYearPaper?.(report)) } reputationDrift(): void { const cur = this.state.family.reputation const drift = cur > 0 ? -1.5 : cur < 0 ? 1.2 : 0 if (drift !== 0) this.state.family.reputation = Math.round(cur + drift) } totalFamilyReputation(): number { return this.state.family.reputation } private checkHead(): void { const s = this.state if (s.gameOver) return const headId = s.family.headId if (!headId) return const head = this.memberById(headId) if (head.alive) return // 功德碑:宗主薨,勒石纪功 const reignStart = (s.family.flag['reignStart'] as number | undefined) ?? 1 const reignYears = Math.max(1, s.year - reignStart) const peak = s.family.reputation const top = this.aliveMembers().length this.chronicle( 'misc', `功德碑:先主${head.name}承宗${reignYears}载,宗族声望达「${peak}」、丁口${top}。族人勒石铭功,立于宗祠。`, head.id, true ) const heir = findInheritor(this) if (heir) { this.assignHead(heir.id, true) s.family.flag['reignStart'] = s.year } else if (this.aliveMembers().length === 0) { this.gameOver('满门凋零,香火断绝', s.year) } } // ==================== player actions ==================== assignHead(id: Id, silent = false): void { const c = this.memberById(id) if (!c.alive) return if (this.state.family.headId && !silent) { const old = this.memberById(this.state.family.headId) old.isHead = false } else { const oldId = this.state.family.headId if (oldId && this.state.members[oldId]) this.state.members[oldId].isHead = false } c.isHead = true this.state.family.headId = id this.state.family.flag['reignStart'] = this.state.year if (!silent) { this.chronicle('misc', `${c.name} 继任为家主。`, c.id, true) this.log('info', `${c.name} 继任为家主。`) } } setMeditation(id: Id, on: boolean): void { const c = this.memberById(id) if (!c.alive || c.state === 'expedition') return c.state = on ? 'meditation' : 'idle' } giveTechnique(memberId: Id, techId: string): void { const c = this.memberById(memberId) c.techniqueId = techId } teachTechnique(techId: string, cost: number): boolean { const fam = this.state.family if (fam.techniques.includes(techId)) return false if (fam.stones < cost) return false fam.stones -= cost fam.techniques.push(techId) this.log('info', `藏书阁续得《${techId}》,译作一名。`) return true } equip(memberId: Id, artifact: string): void { const c = this.memberById(memberId) if (!c.alive) return c.equipment = artifact } takePill(memberId: Id, pill: string): void { const c = this.memberById(memberId) const inv = this.state.family.inventory if (!c.alive || (inv[pill] ?? 0) <= 0) return inv[pill] = inv[pill]! - 1 if (pill === 'pill-pojing') { if (c.realmProgress >= 100) { this.resolveBottleneck(c, 0.22) } else { this.memberById(memberId).realmProgress = Math.min(100, c.realmProgress + 20) this.log('info', `${c.name} 服下破境丹,灵力充盈。`) } } else { const pct = pill === 'pill-qiyuan' ? 18 : 30 c.realmProgress = Math.min(100, c.realmProgress + pct) this.log('info', `${c.name} 服下丹药,修为精进。`) } } assistedBreakthrough(id: Id): void { const c = this.memberById(id) if (!c.alive || c.realmProgress < 100) return this.resolveBottleneck(c, 0.06 + this.head().mind * 0.005) } private resolveBottleneck(c: Character, boost: number): void { resolveBreakthrough(this, c, boost) } build(id: string): boolean { const fam = this.state.family const def = BUILDINGS[id] if (!def) return false if (fam.buildings[id]) return false const cost = def.upgradeCost(1) if (fam.stones < cost.stones) return false fam.stones -= cost.stones fam.buildings[id] = 1 this.chronicle('building', `建成「${def.name}」。`, undefined, true) this.log('info', `建成「${def.name}」。`) return true } upgrade(id: string): boolean { const fam = this.state.family const def = BUILDINGS[id] const lvl = fam.buildings[id] if (!def || !lvl || lvl >= def.maxLevel) return false const cost = def.upgradeCost(lvl + 1) if (fam.stones < cost.stones || (fam.inventory['lingkuang'] ?? 0) < cost.lingkuang) return false fam.stones -= cost.stones fam.inventory['lingkuang'] -= cost.lingkuang fam.buildings[id] = lvl + 1 this.log('info', `「${def.name}」升至 ${lvl + 1} 级。`) return true } craftPill(kind: 'qiyuan' | 'ningyuan'): boolean { const fam = this.state.family const lvl = fam.buildings['danfang'] if (!lvl) return false const cost = kind === 'qiyuan' ? { lingcao: 15, beastcore: 0, stones: 20 } : { lingcao: 25, beastcore: 4, stones: 60 } if ((fam.inventory['lingcao'] ?? 0) < cost.lingcao) return false if ((fam.inventory['beastcore'] ?? 0) < cost.beastcore) return false if (fam.stones < cost.stones) return false fam.inventory['lingcao'] -= cost.lingcao fam.inventory['beastcore'] -= cost.beastcore fam.stones -= cost.stones fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] = (fam.inventory[kind === 'qiyuan' ? 'pill-qiyuan' : 'pill-ningyuan'] ?? 0) + 1 this.log('info', `丹房炼成一枚${kind === 'qiyuan' ? '聚气丹' : '凝元丹'}。`) return true } addMember(c: Character, father?: Character, mother?: Character): void { const s = this.state if (father || mother) { if (father) { c.fatherId = father.id father.children.push(c.id) } if (mother) mother.children.push(c.id) } c.id = c.id || this.seq() s.members[c.id] = c } postCount(def: string): number { return this.aliveMembers().filter((c) => c.post === def && c.state !== 'apprentice').length } assignPost(memberId: Id, postId: string | undefined): boolean { const c = this.memberById(memberId) if (!c.alive || c.state === 'apprentice') return false if (postId === undefined || postId === '') { c.post = undefined return true } const def = POSTS[postId] if (!def || def.id === 'head') return false if (this.postCount(postId) >= def.max) return false c.post = postId return true } postBonus(type: string): number { let sum = 0 for (const c of this.aliveMembers()) { const def = POSTS[c.post ?? ''] if (def && def.effect.type === type) sum += def.effect.value } return sum } familyPower(): number { const fam = this.state.family const bonus = 1 + (fam.buildings['yanwu'] ?? 0) * 0.04 + (fam.buildings['lingshou'] ?? 0) * 0.05 + this.postBonus('battlePower') const top = this.aliveMembers() .map((c) => combatPowerOf(this, c)) .sort((a, b) => b - a) .slice(0, 4) .reduce((a, b) => a + b, 0) return Math.round(top * bonus) } ancestralRite(): boolean { const fam = this.state.family const last = (fam.flag['lastRiteYear'] as number | undefined) ?? -999 if (this.state.year - last < 2) return false if (fam.stones < 150) return false fam.stones -= 150 fam.flag['lastRiteYear'] = this.state.year fam.reputation += 6 for (const c of this.aliveMembers()) { c.realmProgress = Math.min(100, c.realmProgress + 3) c.health = Math.min(100, c.health + 5) } const headName = this.head()?.name ?? '家主' this.chronicle('event', `${headName} 斋戒三日后开祠祭祖,先祖显灵赐福。`, undefined, true) this.log('good', `祭祖!族中众人灵力温润,族人受益。`) return true } seekSutra(): boolean { const fam = this.state.family if ((fam.buildings['cangshu'] ?? 0) < 3) return false const last = (fam.flag['sutraCD'] as number | undefined) ?? -999 if (this.state.year - last < 2) return false if (fam.stones < 150) return false fam.stones -= 150 fam.flag['sutraCD'] = this.state.year const pool = TECHNIQUES.filter((t) => t.grade >= 2 && !fam.techniques.includes(t.id)) if (pool.length === 0) { this.log('info', '求经访道:天下典籍已入庶几,无可再得。') this.chronicle('event', '求经台广搜天下,经卷已穷。', undefined, false) return true } const t = this.rng.pick(pool) fam.techniques.push(t.id) this.chronicle('event', `遣人下江南求经,携回《${t.name}》。`, undefined, true) this.log('good', `求经台访得《${t.name}》!`) return true } tauntNpc(npcId: string): boolean { const fam = this.state.family const npc = this.state.npcFamilies[npcId] if (!npc) return false const last = (fam.flag[`tauntCD-${npcId}`] as number | undefined) ?? 0 if (this.state.year - last < 1) return false fam.flag[`tauntCD-${npcId}`] = this.state.year npc.relation = Math.max(-100, npc.relation - 20) this.log('bad', `指桑骂槐,${npc.name}记恨于心。`) return true } 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 } marriageCandidatesOf(id: Id): Character[] { const me = this.memberById(id) const meG = me.gender return this.aliveMembers() .filter((c) => c.gender !== meG && c.state !== 'expedition' && c.state !== 'apprentice') .filter((c) => w2age(this, c) >= 16 && w2age(this, c) <= 46) .filter((c) => !c.spouseId || this.isWidowed(c)) .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 ) } canMarry(memberId: Id): boolean { const me = this.memberById(memberId) if (!me.alive) return false return !me.spouseId || this.isWidowed(me) } marryTo(aId: Id, bId: Id): boolean { return arrangeWeddingPublic(this, aId, bId) } static create(opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }): World { const state = createWorldState(opts) return new World(state) } } export function makeWorldFromSave(state: GameState): World { return new World(state, []) } 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) 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 a.spouseId = b.id b.spouseId = a.id const aAge = w.ageOf(a) const bAge = w.ageOf(b) w.chronicle('marriage', `${a.name}(${aAge})与${b.name}(${bAge})拜堂成亲。`, a.id, true) w.log('good', `${a.name} 与 ${b.name} 结为连理。`) return true } export function applyChoice(world: World, eventId: string, optionIdx: number): void { applyEventChoice(world, eventId, optionIdx) }