import { BattleLog, Character, ChronicleEntry, GameState, Id, LogItem, Realm, YearlyReport } from '../../types/domain' import { BUILDINGS, bonusOf, buildingById } from '../../data/buildings' import { itemById as defById, pillRecipeByOutput, forgeRecipeByOutput } from '../../data/items' import { POSTS } from '../../data/posts' import { aspirationById as aspirationOf } from '../../data/aspirations' import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy' import { needsTribulation, tribulationEventId } from './Systems/tribulation' import { generateWorld } from '../sim/worldgen' import { registerNpcDef, npcById, NpcFamilyDef } from '../../data/npcs' import { registerBuildingDef } from '../../data/buildings' import { registerPostDef } from '../../data/posts' import { registerAspirationDef } from '../../data/aspirations' import { registerFormationDef } from '../../data/formations' import { registerTraitDef } from '../../data/traits' import { addPillRecipe, addForgeRecipe } from '../../data/items' import { WorldGenPools } from '../sim/worldgen' import { addCalamity as addCalamityImpl } from '../sim/worldsim-data' import { fire } from './Systems/events' import { createWorldState, findInheritor } from './creation' import { SYSTEM_DEFS, SystemDef } from './capabilities' import { emptyClock } from './clocks' import { Rng } from '../kernel/rng' import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/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 '../kernel/clock' import { SystemHook } from '../kernel/clock' import { resolveBreakthrough } from './Systems/cultivation' import { applyEventChoice } from './Systems/events' import { combatPowerOf } from './Systems/combat' export type LogKind = LogItem['kind'] /** 特效出口(引擎语义发射;参数仅为语义锚点,随机散布归渲染层) */ export type FxEmit = { kind: 'spark' | 'blade' | 'pulse' | 'ripple'; source?: string } 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 onFx?(em: FxEmit): void } import { WorldSim } from '../sim/WorldSim' /** 插件代码注册表(0.1.23 持久化重装;未来加载器扩展点) */ export const PLUGIN_REGISTRY = new Map import('../kernel/plugin').CotycPlugin>() export function registerPluginFactory(id: string, factory: () => import('../kernel/plugin').CotycPlugin): void { PLUGIN_REGISTRY.set(id, factory) } export function worldSimOf(w: World): WorldSim { return new WorldSim(w) } export function normalizeGameState(state: GameState): GameState { // 0.1.24 世界种子:旧档缺 worldGen → 按 seed 重放(同一世界)并注册动态 def // P0:世界实例 NPC 定义重注册(幂等——存档世界永稳,MOD 卸载亦完整) if (state.worldGen?.npcs) { for (const n of state.worldGen.npcs) { registerNpcDef(n as never) } } // P0-2 双保险:npcFamilies 中缺 def 的 id → 占位 def(防极端旧档僵尸) for (const id of Object.keys(state.npcFamilies ?? {})) { if (!npcById(id)) { const n = state.npcFamilies[id] registerNpcDef({ id, name: n.name, region: n.region, style: '遗族', desc: '世族遗脉,家道中落而名犹在册。', leaderRealm: 'foundation', initialPower: n.power, powerGrowth: [2, 6], sells: [], buys: ['lingcao'] }) } } if (!state.worldGen && state.seed) { const wg = generateWorld(state.seed) state.worldGen = { relations: wg.relations, eras: wg.eras, marketOffset: wg.marketOffset, regionFlavor: wg.regionFlavor, alliances: wg.alliances, feuds: wg.feuds, npcCount: wg.npcs.length, // P1-9 重放同步 npcs 摘要(与 creation 一致——该档自此自捕获,不再每次重放) npcs: wg.npcs.map((n) => ({ id: n.id, name: n.name, region: n.region, style: n.style, desc: n.desc, leaderRealm: n.leaderRealm, initialPower: n.initialPower, powerGrowth: n.powerGrowth, sells: n.sells, buys: n.buys })) } } // 老版本存档(<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.stats.tourneyHistory) state.stats.tourneyHistory = [] if (state.stats.tourneyBest === undefined) state.stats.tourneyBest = undefined if (state.worldSim) { state.worldSim.marketPool = state.worldSim.marketPool ?? {} state.worldSim.npcDyn = state.worldSim.npcDyn ?? {} state.worldSim.secretQi = state.worldSim.secretQi ?? {} state.worldSim.newsFeed = state.worldSim.newsFeed ?? [] if (typeof state.worldSim.tide !== 'number') state.worldSim.tide = 0.5 if (typeof state.worldSim.tideTicks !== 'number') state.worldSim.tideTicks = 0 if (typeof state.worldSim.npcSuccessions !== 'number') state.worldSim.npcSuccessions = 0 if (typeof state.worldSim.calamityLeft !== 'number') state.worldSim.calamityLeft = 0 if (state.worldSim.era !== 'shengshi' && state.worldSim.era !== 'pingshi' && state.worldSim.era !== 'luanshi' && state.worldSim.era !== 'mofa') { state.worldSim.era = 'pingshi' state.worldSim.eraStartYear = 1 } else if (typeof state.worldSim.eraStartYear !== 'number') { state.worldSim.eraStartYear = state.year ?? 1 } if (typeof state.worldSim.overfluxYear !== 'number') state.worldSim.overfluxYear = -99 if (!Array.isArray(state.worldSim.worldAnnals)) state.worldSim.worldAnnals = [] if (state.worldSim.distress && typeof state.worldSim.distress !== 'object') state.worldSim.distress = undefined for (const dyn of Object.values(state.worldSim.npcDyn)) { if (dyn && typeof (dyn as { prosperity?: unknown }).prosperity !== 'number') { (dyn as { prosperity: number }).prosperity = 50 } } } 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() kernel?: import('../kernel/Kernel').Kernel constructor(state: GameState, out: WorldEventBus[] = [], kernel?: import('../kernel/Kernel').Kernel) { normalizeGameState(state) this.state = state this.out = out if (kernel) { this.clock = kernel.clock this.kernel = kernel this.rng = kernel.rng } else { this.clock = emptyClock() this.rng = new Rng(state.rng) } this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }])) this.plugins = new PluginManager(this.buildPluginContext()) this.installCorePlugins() // 0.1.23 持久化插件重装:读档时按注册表重建非核心插件 const missing: string[] = [] for (const item of state.plugins ?? []) { const mk = PLUGIN_REGISTRY.get(item.id) if (!mk || mk().version !== item.version) { missing.push(item.id) continue } const r = this.installPlugin(mk()) if (r.ok && !item.enabled) this.setPluginEnabled(item.id, false) else if (!r.ok) missing.push(item.id) } if (missing.length > 0) { // P0-4 读档缺失提示(README/MOD 生态承诺对齐) this.state.pluginsMissing = missing } } /** 事件池聚合(含动态事件回看) */ 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), beforePhase: (phase, fn) => self.clock.beforePhase(phase, fn), afterPhase: (phase, fn) => self.clock.afterPhase(phase, fn), addNpcTemplate: (def) => { registerNpcDef(def) if (self.state.worldGen?.relations && !self.state.worldGen.relations[def.id]) { self.state.worldGen.relations[def.id] = {} } }, addPillRecipe: (r) => addPillRecipe(r), addForgeRecipe: (r) => addForgeRecipe(r), addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'), addCalamity: (name, opts) => addCalamityImpl(name, opts), packExtend: (key, additions) => PACK.extend(key as never, additions), mergeAddPack: (key, items) => PACK.mergeAdd(key as never, items), addBuildingDef: (def) => registerBuildingDef(def), addPostDef: (def) => registerPostDef(def), addAspirationDef: (def) => registerAspirationDef(def), addFormationDef: (def) => registerFormationDef(def), addTraitDef: (def) => registerTraitDef(def), addCapability: (cap) => { // 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响 if (!self.systems[cap.id]) 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() } /** 导出当前 MOD 集为一枚 .cotymod 合并包(仅 mod- 前缀内容插件;0.1.27) */ exportModBundle(): string | null { const packs: import('./modSchema').ModPack[] = [] for (const m of this.plugins.modPlugins()) { const src = m.source as import('./modSchema').ModPack if (src) packs.push(src) } if (packs.length === 0) return null return JSON.stringify({ app: 'cotymod', bundle: packs }, null, 2) } 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')) this.syncPersistedPlugins() } 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')) this.syncPersistedPlugins() } 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')) this.syncPersistedPlugins() } return r } allEvents(): EventDef[] { const list: EventDef[] = [] // P0-M:禁用插件的池不参与抽样(tracked 归属:禁用则跳过) const disabledPools = this.plugins.disabledPools() for (const [pid, pool] of this.eventPools) { if (disabledPools.has(pid)) continue for (const e of pool) list.push(e) } return list } eventPoolSnapshot(): Array<{ id: string; events: EventDef[] }> { return [...this.eventPools.entries()].map(([id, ev]) => ({ id, events: ev })) } eventPoolIds(): string[] { return [...this.eventPools.keys()] } /** 插件状态落盘(id/version/enabled)——读档时按注册表重装 */ private syncPersistedPlugins(): void { this.state.plugins = this.plugins .list() .filter((p) => !p.protected) .map((p) => ({ id: p.id, version: p.version, enabled: p.enabled })) } 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) if (this.state.chronicle.length > 520) this.state.chronicle.splice(0, this.state.chronicle.length - 520) this.out.forEach((o) => o.onChronicle(entry, important)) } battle(log: BattleLog): void { this.state.battles.push(log) if (this.state.battles.length > 220) this.state.battles.splice(0, this.state.battles.length - 220) 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.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() } applyEventChoice(eventId: string, idx: number): boolean { const pid = this.state.pendingEvent if (!pid) return false applyEventChoice(this, pid, idx) return true } requeueEvent(eventId: string): void { const s = this.state if (!s.eventQueue.includes(eventId)) s.eventQueue.push(eventId) s.pendingEvent = undefined } /** 年度清理:剔除 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-|legacyDone-|tournamentYear-)(\d+)$/.exec(key) if (m && Number(m[2]) < cutoff) delete fam.flag[key] // tauntCD- 非年份键:随目标家族消亡删除(防长线膨胀) if (key.startsWith('tauntCD-') && !this.state.npcFamilies[key.slice('tauntCD-'.length)]) 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 ==================== /** 灾年救济:耗 50 灵石赈济——目标=当前受灾/景气最低之家(直改 state 的 UI 乌龙道修复) */ reliefCalamity(cost = 50): boolean { const fam = this.state.family if (fam.stones < cost) return false const ws = this.state.worldSim as { calamity?: string; npcDyn?: Record } | undefined const npcs = Object.values(this.state.npcFamilies) if (npcs.length === 0) return false // 选目标:灾年时优先受灾(no 直指字段——以景气最低替代),平时景气最低 let target = npcs[0]! let best = Infinity for (const n of npcs) { const pr = (ws?.npcDyn?.[n.id]?.prosperity ?? 50) if (pr < best) { best = pr; target = n } } fam.stones -= cost target.relation = Math.min(100, target.relation + 8) this.log('info', `开仓济世(灵石${cost}),${target.name} 感念恩义,关系+8。`) void ws return true } /** 盟友求援响应:捐灵石助其重整武备(power+15、关系+8) */ assistAlly(npcId: string, cost = 200): boolean { const fam = this.state.family const ws = this.state.worldSim as { distress?: { id: string } } | undefined if (!ws?.distress || ws.distress.id !== npcId) return false const npc = this.state.npcFamilies[npcId] if (!npc) return false if (fam.stones < cost) return false fam.stones -= cost npc.power = Math.min(900, npc.power + 15) npc.relation = Math.min(100, npc.relation + 8) fam.reputation += 3 ws.distress = undefined this.log('good', `驰援${npc.name}——敌锋既退,盟谊愈坚(声望+3)。`) this.chronicle('diplomacy', `助${npc.name}重整武备,同盟益固。`, undefined, true) return true } /** 调停战端:耗灵石五十,令两家罢兵三年(关系回暖至可容忍线) */ mediateNpcs(npcA: string, npcB: string, cost = 50): boolean { const fam = this.state.family const dyns = this.state.worldSim?.npcDyn as Record }> | undefined if (!dyns?.[npcA]?.relationsWithOthers || dyns[npcA].relationsWithOthers[npcB] === undefined) return false if (fam.stones < cost) return false fam.stones -= cost for (const [a, b] of [[npcA, npcB], [npcB, npcA]] as const) { const rel = dyns[a].relationsWithOthers[b] if (rel < -30) dyns[a].relationsWithOthers[b] = -25 } fam.reputation += 2 this.log('info', `调停${this.state.npcFamilies[npcA]?.name ?? npcA}与${this.state.npcFamilies[npcB]?.name ?? npcB}——干戈化玉帛,声望+2。`) return true } setAlliance(npcId: string, on: boolean): boolean { const npc = this.state.npcFamilies[npcId] if (!npc) return false if (on) { const stA = (this.state.worldSim?.npcDyn?.[npcId] as { stance?: string } | undefined)?.stance const need = stA === 'ally' ? 30 : 40 if (npc.relation < need) return false npc.allied = true npc.alliedSinceYear = this.state.year npc.power = Math.min(900, npc.power + 10) // 1-3 扰动:盟约惹眼——邻家对此家略生嫌隙 for (const dyn of Object.values(this.state.worldSim?.npcDyn ?? {})) { if (dyn && dyn.relationsWithOthers && npcId in dyn.relationsWithOthers) { dyn.relationsWithOthers[npcId] = Math.max(-100, (dyn.relationsWithOthers[npcId] ?? 0) - 3) } } // 0.1.19 盟约连坐:其世仇恼我结盟,两邦关系走冷 const dyn = (this.state.worldSim?.npcDyn as Record }> | undefined)?.[npcId] if (dyn) { for (const [oid, rel] of Object.entries(dyn.relationsWithOthers)) { if (rel < -50) { const foeNpc = this.state.npcFamilies[oid] if (foeNpc) { foeNpc.relation = Math.max(-100, foeNpc.relation - 20) this.log('info', `${foeNpc.name} 见我已与${npc.name}结盟,心怀芥蒂,关系-20。`) } } } } this.log('good', `与${npc.name}结成同盟——盟誓既立,互不犯边。`) this.chronicle('diplomacy', `本族与${npc.name}缔结同盟。`, undefined, true) return true } if (npc.allied) { npc.allied = false npc.relation = Math.max(-100, npc.relation - 20) this.log('bad', `与${npc.name}的盟约破裂——关系转恶。`) this.chronicle('diplomacy', `与${npc.name}结盟破裂。`, undefined, true) } return true } 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 { if (!defById(artifact)) return 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) { if (needsTribulation(c) && this.sysEnabled('tribulation')) { // C13:大境界渡劫不可跳过——丹药转为天劫助益 c.tribBoost = (c.tribBoost ?? 0) + 0.12 c.realmProgress = 100 fire(this, tribulationEventId(c), 1) this.log('info', `${c.name} 服下破境丹,丹力浑厚——雷云受感而聚,九霄震动。`) } else { 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 = buildingById(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 = buildingById(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' | 'pojing' | 'heling' | 'xudan'): boolean { const fam = this.state.family const lvl = fam.buildings['danfang'] const r = pillRecipeByOutput(`pill-${kind}`) if (!lvl || !r) return false if (lvl < r.danfangLevel) { this.log('info', `丹房不足(需 ${r.danfangLevel} 级方可炼${r.name})。`) return false } const inv = fam.inventory if ((inv['lingcao'] ?? 0) < r.lingcao || (inv['beastcore'] ?? 0) < r.beastcore || fam.stones < r.stones) return false inv['lingcao'] -= r.lingcao inv['beastcore'] -= r.beastcore fam.stones -= r.stones const chance = bonusOf('danfang', 'craftChance', lvl) if (this.rng.chance(chance)) { inv[`pill-${kind}`] = (inv[`pill-${kind}`] ?? 0) + 1 this.log('good', `丹房炼成一枚${r.name}。`) return true } inv['lingcao'] += Math.ceil(r.lingcao / 2) inv['beastcore'] += Math.ceil(r.beastcore / 2) fam.stones += Math.ceil(r.stones / 2) this.log('bad', `${r.name}炼废了——丹火失控,药材折半。`) return false } forgeArtifact(kind: 'weapon-qi' | 'weapon-ling' | 'weapon-fa' | 'weapon-yu'): boolean { const fam = this.state.family const r = forgeRecipeByOutput(kind) if (!r) return false const inv = fam.inventory if ((inv['lingkuang'] ?? 0) < r.lingkuang || (inv['beastcore'] ?? 0) < r.beastcore || fam.stones < r.stones) return false inv['lingkuang'] -= r.lingkuang inv['beastcore'] -= r.beastcore fam.stones -= r.stones inv[kind] = (inv[kind] ?? 0) + 1 this.log('good', `炉火淬炼,得一${r.name}。`) 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 } /** 语义特效发射(零随机消耗——渲染自由派生出随机分布) */ emitFx(kind: 'spark' | 'blade' | 'pulse' | 'ripple', source?: string): void { const em: FxEmit = { kind, source } this.out.forEach((o) => o.onFx?.(em)) } 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 + bonusOf('yanwu', 'powerBonus', fam.buildings['yanwu'] ?? 0) + bonusOf('lingshou', 'powerBonus', fam.buildings['lingshou'] ?? 0) + 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) }