v0.1.23: 生灭千秋 + 插件生态化第一课(1027 全绿)

【世界模型:世界会死人也会新生】
- 家族生灭:declineYears(power<58+景气<38 连续8年)→ 覆灭(最强邻分食+史官广播)+ 新贵补位(6%/年,乱世×2)
  —— NPC 永远四家铁打的现状终结;玩家补刀(raid×0.82)可加速衰亡
- npcById 双源可空(静态+动态注册表;不再 throw——15 处消费者防御化)
- purgeNpc 全局清理(raidCD/事件队列/恩怨网/distress/动态 def)
- 秘境产出回池(loot 20% 经 tradeSettle 回流)——资源循环最后单向封口
- WorldSnapshot 只读摘要门面(UI/史书消费,零行为变更)
- 潮汐长波(10 年正弦 ±0.08 叠加,灵潮三十年河东)

【插件生态化(0.1.8 后第一次大进化)】
- 持久化:GameState.plugins 落盘 + PLUGIN_REGISTRY 读档重装(engineFromSnapshot 真实路径验证)
- plugin-public.ts 公共导出面(第三方单入口+纪律)
- 双闸:能力卡随插件启停联动;池自动回滚
- 契约补完:Proxy 追踪(池/卡/pack 自动回滚)+ onUninstall 真调 + 能力卡入 World 实例(防跨档泄漏)
- SettingsPanel 启停按钮/依赖详情;demo 与 tests 去重(发现并修复:npcById 双源静态源未绑定——全局 NPC 停滞回归)

【测试】1027 全绿(44 套件):dynasty-plugin 7 例(覆灭/新贵/回池/快照/持久化/双闸/自动回滚);
金钟罩 0.1.23 基线固化;build 通过
This commit is contained in:
2026-08-23 16:03:01 +08:00
parent d2787ee3c8
commit 04ef80faa5
27 changed files with 455 additions and 98 deletions
@@ -137,7 +137,7 @@ export class GameFacade {
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
return {
title: '仙途家族志',
version: '0.1.22',
version: '0.1.23',
modules: this.world.systemList().length,
systems: this.world.systemList().filter((s) => s.enabled).length,
plugins: this.world.pluginList().length,
@@ -166,6 +166,7 @@ export function resolveRaid(
): EncounterResult {
const npc = w.state.npcFamilies[npcId]
const def = npcById(npcId)
if (!def) return { win: false, draw: true, lines: ['此族已散。'], loot: undefined, losses: [] }
// 宗主实力分层:power 由年度成长+换代驱动,劫掠强度随之浮动(0.5↔1.8)
const npcPower = npc.power ?? 60
const strength = Math.min(1.8, Math.max(0.5, 0.5 + (npcPower / 120) * 0.5))
@@ -193,7 +194,7 @@ export function resolveRaid(
w.state.family.reputation += 6
// B5:战争伤骨——败方 power 重挫,次年不再来犯(warCooldownYear/raidCount 启用)
w.emitFx('blade', `raid:${npcId}`)
npc.power = Math.max(40, Math.round(npc.power * 0.82))
npc.power = Math.max(52, Math.round(npc.power * 0.82))
npc.raidCount = (npc.raidCount ?? 0) + 1
npc.warCooldownYear = w.state.year
// 1-3 蝴蝶效应:玩家重创名声——邻家对败者关系趋冷
@@ -36,6 +36,7 @@ export function yearGrowth(w: World): void {
const s = w.state
for (const npc of Object.values(s.npcFamilies)) {
const def = npcById(npc.id)
if (!def) continue
const [a, b] = def.powerGrowth
npc.power += w.rng.int(a, b)
if (npc.allied) {
@@ -29,6 +29,7 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
if (id.startsWith('ev-raid-')) {
const npcId = id.replace('ev-raid-', '')
const npc = npcById(npcId)
if (!npc) return undefined // 该族已散(覆灭/附庸)——事件自然消亡
return {
id,
name: `${npc.name}来犯`,
@@ -70,6 +70,15 @@ export function missionTick(w: World): void {
m.result = 'success'
releaseSquad(w, m)
const total = rollWarbooty(w, def.completionLoot, qiRatioOf(w, def.id))
// 0.1.23 秘境回池:探得之物近 2 成回流人间(负反馈世界侧供给)
if (w.state.worldSim) {
const sim = new WorldSim(w)
for (const [k, v] of Object.entries(total)) {
if (k === 'tech') continue
const bleed = Math.max(1, Math.round((v as number) * 0.2))
sim.tradeSettle(k === 'stones' ? 'lingkuang' : k, bleed)
}
}
m.log.push(`凯旋而归,清点战利:${lootText(total)}`)
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
w.chronicle(
+34 -6
View File
@@ -50,6 +50,12 @@ export interface WorldEventBus {
import { WorldSim } from '../sim/WorldSim'
/** 插件代码注册表(0.1.23 持久化重装;未来加载器扩展点) */
export const PLUGIN_REGISTRY = new Map<string, () => 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)
}
@@ -145,6 +151,15 @@ export class World {
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
this.plugins = new PluginManager(this.buildPluginContext())
this.installCorePlugins()
// 0.1.23 持久化插件重装:读档时按注册表重建非核心插件
for (const item of state.plugins ?? []) {
const mk = PLUGIN_REGISTRY.get(item.id)
if (!mk) continue
const plugin = mk()
if (plugin.version !== item.version) continue
const r = this.installPlugin(plugin)
if (r.ok && !item.enabled) this.setPluginEnabled(item.id, false)
}
}
@@ -158,10 +173,8 @@ export class World {
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 }
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
},
removeCapability: (id) => {
delete self.systems[id]
@@ -210,7 +223,10 @@ export class World {
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'))
if (r.ok) {
this.out.forEach((o) => o.onPluginChange?.(p.id, 'install'))
this.syncPersistedPlugins()
}
return r
}
@@ -219,13 +235,17 @@ export class World {
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'))
if (r.ok) {
this.out.forEach((o) => o.onPluginChange?.(id, enabled ? 'enable' : 'disable'))
this.syncPersistedPlugins()
}
return r
}
@@ -241,6 +261,14 @@ export class World {
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 保证可用
@@ -63,6 +63,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
power: Math.round(n.initialPower * npcStrength),
relation: 0,
allied: false,
declineYears: 0,
raidCount: 0
} as NpcFamilyState
])
@@ -1,4 +1,5 @@
import { CotycPlugin } from '../kernel/plugin'
import { registerPluginFactory } from './World'
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
export const examplePlugin: CotycPlugin = {
@@ -28,10 +29,7 @@ export const examplePlugin: CotycPlugin = {
}
])
},
uninstall(ctx) {
ctx.removeEventPool('demo-peaks')
ctx.removeCapability('demo-guardian')
}
// 0.1.23 契约:清理由 PluginManager 自动回滚(池/能力卡/数据包/时轮钩子全摘);uninstall 留作扩展点
}
/** 依赖缺失的坏插件:应被拒绝安装 */
@@ -44,3 +42,5 @@ export const brokenPlugin: CotycPlugin = {
dependencies: ['demo-not-exists'],
install() {}
}
registerPluginFactory('demo-peaks', () => examplePlugin)
@@ -1,4 +1,6 @@
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../kernel/plugin'
import { EventDef } from '../../data/events'
import { PACK } from '../../data/registry'
import { SystemDef, SYSTEM_DEFS } from './capabilities'
import type { World } from './World'
import { SystemHook } from '../kernel/clock'
@@ -8,6 +10,13 @@ interface RuntimePlugin {
plugin: CotycPlugin
status: { installed: boolean; enabled: boolean }
unsubscribers: Array<() => void>
/** 追踪清单:插件注册的资源(池/能力卡/数据包覆写),卸载/禁用时自动回滚 */
tracked: {
eventPools: string[]
caps: string[]
packOverride?: Partial<import('../../data/registry').DataPack>
packWasOverridden: boolean
}
}
/**
@@ -45,7 +54,7 @@ export class PluginManager {
if (def.id === plugin.id) return { ok: false, reason: `id 冲突:${plugin.id}` }
}
const rt: RuntimePlugin = { plugin, status: { installed: true, enabled: true }, unsubscribers: [] }
const rt: RuntimePlugin = { plugin, status: { installed: true, enabled: true }, unsubscribers: [], tracked: { eventPools: [], caps: [], packWasOverridden: false } }
this.runtime.set(plugin.id, rt)
this.order.push(plugin.id)
// 追踪型上下文:插件在时轮上的注册(phase/年首)一律归属该插件,卸载时全摘
@@ -59,6 +68,27 @@ export class PluginManager {
return unsub
}
}
if (prop === 'addEventPool') {
return (pid: string, events: EventDef[]) => {
;(target.addEventPool as (pid: string, ev: EventDef[]) => void)(pid, events)
rt.tracked.eventPools.push(pid)
}
}
if (prop === 'addCapability') {
return (cap: Parameters<PluginContext['addCapability']>[0]) => {
;(target.addCapability as (c: Parameters<PluginContext['addCapability']>[0]) => void)(cap)
rt.tracked.caps.push(cap.id)
}
}
if (prop === 'overridePack') {
return (partial: Parameters<PluginContext['overridePack']>[0]) => {
if (!rt.tracked.packWasOverridden) {
rt.tracked.packOverride = partial
rt.tracked.packWasOverridden = true
}
;(target.overridePack as (p: Parameters<PluginContext['overridePack']>[0]) => void)(partial)
}
}
return (target as unknown as Record<string, unknown>)[key as string]
}
})
@@ -81,7 +111,11 @@ export class PluginManager {
if (!rt) return { ok: false, reason: '未安装' }
if (rt.plugin.protected) return { ok: false, reason: '核心插件受保护' }
rt.unsubscribers.forEach((u) => u())
for (const pid of rt.tracked.eventPools) this.ctx.removeEventPool(pid)
for (const cid of rt.tracked.caps) this.ctx.enableCapability(cid, false) // 停用而非删除(sysEnabled ?? true 兜底会“归真”)
if (rt.tracked.packWasOverridden) this.ctx.resetPack()
rt.plugin.uninstall?.(this.ctx)
rt.plugin.hooks?.onUninstall?.()
this.runtime.delete(pluginId)
const i = this.order.indexOf(pluginId)
if (i >= 0) this.order.splice(i, 1)
@@ -98,6 +132,9 @@ export class PluginManager {
rt.status.enabled = enabled
if (enabled) rt.plugin.hooks?.onEnable?.()
else rt.plugin.hooks?.onDisable?.()
// P3 双闸:能力卡随插件启停联动(事件池在 allEvents 处经 pluginStatus 过滤)
for (const cid of rt.tracked.caps) this.ctx.enableCapability(cid, enabled)
void PACK
this.onChange({ id: pluginId, action: enabled ? 'enable' : 'disable' })
return { ok: true }
}