插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓
全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项
测试 239→256;金钟罩复验(防御批零漂移)
160 lines
7.1 KiB
TypeScript
160 lines
7.1 KiB
TypeScript
import type { World } from './world'
|
|
import { WorldEventBus } from './world'
|
|
import { YearlyReport, BattleLog, ChronicleEntry, SaveMeta } from '../types/domain'
|
|
import { marketPrice, buyItem, sellItem, buyTechnique } from './market'
|
|
import { sendMission, recallAll } from './systems/missions'
|
|
import { giftNpc, makePeace, marryNpcFamily } from './systems/diplomacy'
|
|
import { combatPowerOf } from './systems/combat'
|
|
import type { LogKind } from './world'
|
|
import { computeLegacy, resolveLegacy, LegacyArch } from '../core/legacy'
|
|
import { yearAxis, AxisCell } from '../core/yearaxis'
|
|
import { PACK } from '../data/registry'
|
|
|
|
export type ActName =
|
|
| 'head.set'
|
|
| 'member.meditate'
|
|
| 'member.technique'
|
|
| 'member.equip'
|
|
| 'member.pill'
|
|
| 'member.advance'
|
|
| 'member.marry'
|
|
| 'member.post'
|
|
| 'estate.build'
|
|
| 'estate.upgrade'
|
|
| 'estate.rite'
|
|
| 'estate.sutra'
|
|
| 'market.buy'
|
|
| 'market.sell'
|
|
| 'market.tech'
|
|
| 'craft.pill'
|
|
| 'diplomacy.gift'
|
|
| 'diplomacy.peace'
|
|
| 'diplomacy.taunt'
|
|
| 'diplomacy.marry'
|
|
| 'expedition.send'
|
|
| 'expedition.recall'
|
|
| 'legacy.resolve'
|
|
|
|
export interface ActPayload {
|
|
memberId?: string
|
|
targetId?: string
|
|
post?: string
|
|
item?: string
|
|
pill?: string
|
|
tech?: string
|
|
building?: string
|
|
npcId?: string
|
|
stones?: number
|
|
mission?: string
|
|
squad?: string[]
|
|
count?: number
|
|
}
|
|
|
|
export const ACT_CATALOG: Record<ActName, { desc: string; fn: (w: World, p: ActPayload) => boolean }> = {
|
|
'head.set': { desc: '册立家主', fn: (w, p) => (p.memberId ? (w.assignHead(p.memberId), true) : false) },
|
|
'member.meditate': { desc: '闭关/出关', fn: (w, p) => (p.memberId ? (w.setMeditation(p.memberId, !!p.count), true) : false) },
|
|
'member.technique': { desc: '传功', fn: (w, p) => (p.memberId && p.tech ? (w.giveTechnique(p.memberId, p.tech), true) : false) },
|
|
'member.equip': { desc: '装备法宝', fn: (w, p) => (p.memberId && p.item ? (w.equip(p.memberId, p.item), true) : false) },
|
|
'member.pill': { desc: '服丹', fn: (w, p) => (p.memberId && p.pill ? (w.takePill(p.memberId, p.pill), true) : false) },
|
|
'member.advance': { desc: '冲关', fn: (w, p) => (p.memberId ? (w.assistedBreakthrough(p.memberId), true) : false) },
|
|
'member.marry': { desc: '指婚', fn: (w, p) => (p.memberId && p.targetId ? w.marryTo(p.memberId, p.targetId) : false) },
|
|
'member.post': { desc: '任职/卸任', fn: (w, p) => (p.memberId ? w.assignPost(p.memberId, p.post) : false) },
|
|
'estate.build': { desc: '营建', fn: (w, p) => (p.building ? w.build(p.building) : false) },
|
|
'estate.upgrade': { desc: '升级', fn: (w, p) => (p.building ? w.upgrade(p.building) : false) },
|
|
'estate.rite': { desc: '祭祖', fn: (w) => w.ancestralRite() },
|
|
'estate.sutra': { desc: '求经', fn: (w) => w.seekSutra() },
|
|
'market.buy': { desc: '购货', fn: (w, p) => (p.item ? buyItem(w, p.item, p.count ?? 1) : false) },
|
|
'market.sell': { desc: '售货', fn: (w, p) => (p.item ? sellItem(w, p.item, p.count ?? 1) : false) },
|
|
'market.tech': { desc: '购法帖', fn: (w, p) => (p.tech ? buyTechnique(w, p.tech, p.stones ?? 0) : false) },
|
|
'craft.pill': { desc: '炼丹', fn: (w, p) => (p.item === 'ningyuan' ? w.craftPill('ningyuan') : Boolean(p.item === 'qiyuan') && w.craftPill('qiyuan')) },
|
|
'diplomacy.gift': { desc: '赠礼', fn: (w, p) => (p.npcId ? giftNpc(w, p.npcId, p.stones ?? 0) : false) },
|
|
'diplomacy.peace': { desc: '议和', fn: (w, p) => (p.npcId ? makePeace(w, p.npcId) : false) },
|
|
'diplomacy.taunt': { desc: '寻衅', fn: (w, p) => (p.npcId ? w.tauntNpc(p.npcId) : false) },
|
|
'diplomacy.marry': { desc: '联姻', fn: (w, p) => (p.npcId ? marryNpcFamily(w, p.npcId) : false) },
|
|
'expedition.send': { desc: '遣队出征', fn: (w, p) => (p.mission ? sendMission(w, p.mission, p.squad ?? []) : false) },
|
|
'expedition.recall': { desc: '召回', fn: (w, p) => (p.memberId ? (recallAll(w, p.memberId), true) : false) },
|
|
'legacy.resolve': { desc: '落印定鼎', fn: (w) => (w.resolveLegacyNow(), true) }
|
|
}
|
|
|
|
export interface QueryResult {
|
|
[k: string]: unknown
|
|
}
|
|
|
|
export class GameFacade {
|
|
constructor(
|
|
public readonly world: World,
|
|
public readonly slot: number,
|
|
private bus?: WorldEventBus
|
|
) {}
|
|
|
|
act(name: ActName, payload: ActPayload = {}): boolean {
|
|
const c = ACT_CATALOG[name]
|
|
if (!c) return false
|
|
return c.fn(this.world, payload)
|
|
}
|
|
|
|
query(ref: string): QueryResult {
|
|
const w = this.world
|
|
switch (ref) {
|
|
case 'family':
|
|
return { name: w.state.family.name, estate: w.state.family.estate, year: w.state.year, month: w.state.month, reputation: w.state.family.reputation, generation: w.state.family.generation }
|
|
case 'members':
|
|
return { list: w.aliveMembers().map((c) => ({ id: c.id, name: c.name, realm: `${c.realm.major}/${c.realm.minor}`, post: c.post ?? '', state: c.state, power: Math.round(combatPowerOf(w, c)) })) }
|
|
case 'legacy':
|
|
return resolveLegacy(w.state) as unknown as QueryResult
|
|
case 'yearAxis':
|
|
return { cells: yearAxis(w.state) as unknown as AxisCell[] }
|
|
case 'systems':
|
|
return { list: w.systemList() }
|
|
case 'finance':
|
|
return { stones: w.state.family.stones, accum: w.state.finance.accum, yearStats: w.state.yearStats }
|
|
case 'plugins':
|
|
return { list: w.pluginList() }
|
|
default:
|
|
return {}
|
|
}
|
|
}
|
|
|
|
subscribe(on: (e: FacadeEvent) => void): () => void {
|
|
const bus: WorldEventBus = {
|
|
onLog: (kind: LogKind, text: string) => on({ type: 'log', kind, text, year: this.world.state.year, month: this.world.state.month }),
|
|
onChronicle: (entry: ChronicleEntry, important: boolean) => on({ type: 'chronicle', entry, important }),
|
|
onBattle: (log: BattleLog) => on({ type: 'battle', log }),
|
|
onPendingEvent: (id: string) => on({ type: 'pending', id }),
|
|
onGameOver: (reason: string) => on({ type: 'gameover', reason }),
|
|
onYearPaper: (report: YearlyReport) => on({ type: 'paper', report }),
|
|
onSystemChange: (id: string, enabled: boolean) => on({ type: 'sysChanged', id, enabled }),
|
|
onPluginChange: (id: string, action: string) => on({ type: 'plugin', id, action })
|
|
}
|
|
this.world.out.push(bus)
|
|
return () => {
|
|
const i = this.world.out.indexOf(bus)
|
|
if (i >= 0) this.world.out.splice(i, 1)
|
|
}
|
|
}
|
|
|
|
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
|
return {
|
|
title: '仙途家族志',
|
|
version: '0.1.8',
|
|
modules: this.world.systemList().length,
|
|
systems: this.world.systemList().filter((s) => s.enabled).length,
|
|
plugins: this.world.pluginList().length,
|
|
packFingerprint: PACK.fingerprint()
|
|
}
|
|
}
|
|
}
|
|
|
|
export type FacadeEvent =
|
|
| { type: 'log'; kind: LogKind; text: string; year: number; month: number }
|
|
| { type: 'chronicle'; entry: ChronicleEntry; important: boolean }
|
|
| { type: 'battle'; log: BattleLog }
|
|
| { type: 'pending'; id: string }
|
|
| { type: 'gameover'; reason: string }
|
|
| { type: 'paper'; report: YearlyReport }
|
|
| { type: 'sysChanged'; id: string; enabled: boolean }
|
|
| { type: 'plugin'; id: string; action: string }
|
|
|
|
export { marketPrice, computeLegacy }
|
|
export type { SaveMeta, LegacyArch }
|