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 { aspirationById as aspirationOf } from '../data/aspirations' import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy' import { createWorldState, findInheritor } from './creation' import { SYSTEM_DEFS, SystemDef } from './capabilities' import { emptyClock } from './clocks' import { CotycPlugin, PluginContext, PluginStatus } from '../core/plugin' import { PluginManager } from './pluginManager' import { EventDef } from '../data/events' import { pack, DEFAULT_PACK, PACK } from '../data/registry' import { CORE_PLUGINS } from './plugin-bootstrap' import { GameClock } from '../core/clock' import { SystemHook } from '../core/clock' import { resolveBreakthrough } from './systems/cultivation' import { applyEventChoice } from './systems/events' 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 onSystemChange?(id: string, enabled: boolean): void onPluginChange?(id: string, action: string): 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 = [] if (!state.missions) state.missions = [] if (!state.flags) state.flags = {} if (!state.npcFamilies) state.npcFamilies = {} if (!state.family.flag) state.family.flag = {} if (!state.family.inventory) state.family.inventory = {} if (!state.family.buildings) state.family.buildings = {} if (!state.family.techniques) state.family.techniques = [] if (!state.family.missionIds) state.family.missionIds = [] if (state.family.headId && !state.members[state.family.headId]) { const firstAlive = Object.values(state.members).find((c) => c.alive) if (firstAlive) state.family.headId = firstAlive.id } 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[] clock: GameClock systems: Record plugins: PluginManager private eventPools = new Map() constructor(state: GameState, out: WorldEventBus[] = []) { normalizeGameState(state) this.state = state this.rng = new Rng(state.rng) this.out = out this.clock = emptyClock() this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }])) this.plugins = new PluginManager(this.buildPluginContext()) this.installCorePlugins() } /** 事件池聚合(含动态事件回看) */ buildPluginContext(): PluginContext { const self = this return { world: self, clock: self.clock, register: (phase, fn: SystemHook) => self.clock.register(phase, fn), onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn), addCapability: (cap) => { if (!SYSTEM_DEFS.find((d) => d.id === cap.id)) { SYSTEM_DEFS.push({ id: cap.id, name: cap.name, version: cap.version, desc: cap.desc }) } self.systems[cap.id] = { enabled: true } }, removeCapability: (id) => { delete self.systems[id] }, enableCapability: (id, enabled) => { if (self.systems[id]) self.systems[id].enabled = enabled }, overridePack: (partial) => { void pack self.packOverride(partial) }, resetPack: () => { self.packReset() }, addEventPool: (id, events) => { self.eventPools.set(id, events) }, removeEventPool: (id) => { self.eventPools.delete(id) } } } private packOverride(partial: Partial): void { PACK.override(partial) } private packReset(): void { PACK.reset() } installCorePlugins(): void { // 内置三插件:系统/数据/事件(受保护常驻,统一走管线) for (const p of CORE_PLUGINS) { this.plugins.install(p) } } pluginList(): PluginStatus[] { return this.plugins.list() } pluginChanges(): { id: string; action: string }[] { return this.plugins.listChanges() } installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } { const r = this.plugins.install(p) if (r.ok) this.out.forEach((o) => o.onPluginChange?.(p.id, 'install')) return r } removePlugin(id: string): { ok: boolean; reason?: string } { const r = this.plugins.remove(id) if (r.ok) { this.rebuildEventPoolsAfterRemoval(id) this.out.forEach((o) => o.onPluginChange?.(id, 'remove')) } return r } setPluginEnabled(id: string, enabled: boolean): { ok: boolean; reason?: string } { const r = this.plugins.setEnabled(id, enabled) if (r.ok) this.out.forEach((o) => o.onPluginChange?.(id, enabled ? 'enable' : 'disable')) return r } allEvents(): EventDef[] { const list: EventDef[] = [] for (const pool of this.eventPools.values()) { for (const e of pool) list.push(e) } return list } eventPoolIds(): string[] { return [...this.eventPools.keys()] } private rebuildEventPoolsAfterRemoval(id: string): void { void id // 事件池卸载暂由插件 uninstall 自行处理;此处在 remove 后重置 core 保证可用 if (!this.eventPools.has('core')) this.eventPools.set('core', []) } sysEnabled(id: string): boolean { return this.systems[id]?.enabled ?? true } toggleSystem(id: string): boolean { const s = this.systems[id] if (!s) return false s.enabled = !s.enabled this.out.forEach((o) => o.onSystemChange?.(id, s.enabled)) return s.enabled } systemList(): { id: string; name: string; version: string; desc: string; enabled: boolean }[] { return SYSTEM_DEFS.map((d) => ({ id: d.id, name: d.name, version: d.version, desc: d.desc, enabled: this.sysEnabled(d.id) })) } 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)) } gameOver___placeholder(): void { void 0 } 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.clock.fireYearStart(this) } s.totalTicks++ this.clock.stepMonthly(this) const stonesEnd = s.family.stones s.finance.accum += stonesEnd - stonesStart this.trackStats() this.clampState() this.pruneYearFlags() } /** 年度清理:剔除 3 年以前的年份前缀 flag 键(防长线膨胀) */ private pruneYearFlags(): void { const fam = this.state.family const cutoff = this.state.year - 3 for (const key of Object.keys(fam.flag)) { const m = /^(auction-|prayerDone-|echoDone-|recruitDone-)(\d+)$/.exec(key) if (m && Number(m[2]) < cutoff) delete fam.flag[key] } } /** 月底统一数值钳制:修为/气血/库存/金钱永不越界 */ private clampState(): void { for (const c of Object.values(this.state.members)) { if (c.realmProgress < 0) c.realmProgress = 0 if (c.realmProgress > 100) c.realmProgress = 100 if (c.health < 0) c.health = 0 if (c.health > 100) c.health = 100 } if (this.state.family.stones < 0) this.state.family.stones = 0 for (const [k, v] of Object.entries(this.state.family.inventory)) { if (typeof v === 'number' && v < 0) this.state.family.inventory[k] = 0 } } 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 } publishYearReport(): void { 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)) } epilogueTick(): void { if (this.state.gameOver) return this.checkHead() } 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 = pack().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 arrangeWeddingBridge(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 arrangeWeddingBridge(w: World, aId: Id, bId: Id): boolean { const a = w.state.members[aId] const b = w.state.members[bId] if (!a || !b || !a.alive || !b.alive) return false if (w.ageOf(a) < 16 || w.ageOf(b) < 16) 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 && 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) }