v0.1.8: 万物皆是插件 + 全应用审计修复(60+ 缺陷歼灭)
插件协议:
- 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;金钟罩复验(防御批零漂移)
This commit is contained in:
@@ -10,15 +10,19 @@ import {
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { pack } from '../data/registry'
|
||||
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 { buildClock } from './clocks'
|
||||
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'
|
||||
@@ -33,6 +37,7 @@ export interface WorldEventBus {
|
||||
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 {
|
||||
@@ -55,6 +60,18 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
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
|
||||
@@ -69,14 +86,118 @@ export class World {
|
||||
out: WorldEventBus[]
|
||||
clock: GameClock
|
||||
systems: Record<string, { enabled: boolean }>
|
||||
plugins: PluginManager
|
||||
private eventPools = new Map<string, EventDef[]>()
|
||||
|
||||
constructor(state: GameState, out: WorldEventBus[] = []) {
|
||||
normalizeGameState(state)
|
||||
this.state = state
|
||||
this.rng = new Rng(state.rng)
|
||||
this.out = out
|
||||
this.clock = buildClock()
|
||||
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<import('../data/registry').DataPack>): 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 {
|
||||
@@ -525,7 +646,7 @@ export class World {
|
||||
}
|
||||
|
||||
marryTo(aId: Id, bId: Id): boolean {
|
||||
return arrangeWeddingPublic(this, aId, bId)
|
||||
return arrangeWeddingBridge(this, aId, bId)
|
||||
}
|
||||
|
||||
static create(opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }): World {
|
||||
@@ -542,18 +663,13 @@ 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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user