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:
@@ -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(
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user