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:
@@ -1,7 +1,7 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
仙途家族志 · Chronicle of the Immortal Clan — Electron + React + TS 家族修仙模拟器。全部 UI 与文案为中文。
|
||||||
当前版本 **0.1.22**(《灯下春秋》:Modal 全走 body Portal(z 层越界修复)+ 年报不打断自动速度)。
|
当前版本 **0.1.23**(《生灭千秋》:家族覆灭/新贵补位 + 插件持久化/双闸/公共导出面)。
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chronicle-of-the-immortal-clan",
|
"name": "chronicle-of-the-immortal-clan",
|
||||||
"productName": "仙途家族志",
|
"productName": "仙途家族志",
|
||||||
"version": "0.1.22",
|
"version": "0.1.23",
|
||||||
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
"description": "修仙 · 家族 · 经营 · 战斗 模拟器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "MetonaTeam",
|
"author": "MetonaTeam",
|
||||||
|
|||||||
@@ -36,8 +36,25 @@ export const NPCS: NpcFamilyDef[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export function npcById(id: string): NpcFamilyDef {
|
/** 运行时动态 NPC 注册表(0.1.23 新贵/覆灭;插件化入口) */
|
||||||
const n = NPCS.find((x) => x.id === id)
|
const DYNAMIC_NPCS = new Map<string, NpcFamilyDef>()
|
||||||
if (!n) throw new Error(`npc not found: ${id}`)
|
|
||||||
return n
|
export function registerNpcDef(def: NpcFamilyDef): void {
|
||||||
|
DYNAMIC_NPCS.set(def.id, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterNpcDef(id: string): void {
|
||||||
|
DYNAMIC_NPCS.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 双源查询:静态 NPCS → 运行时动态表(新贵);缺失返回 undefined(调用方防御)。
|
||||||
|
* 注意:此函数拒绝 import registry(循环依赖),静态源直连 NPCS;数据包 npcs 覆写暂不走此路。 */
|
||||||
|
export function npcById(id: string): NpcFamilyDef | undefined {
|
||||||
|
const stat = NPCS.find((x) => x.id === id)
|
||||||
|
if (stat) return stat
|
||||||
|
return DYNAMIC_NPCS.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNpcDefs(): NpcFamilyDef[] {
|
||||||
|
return [...NPCS, ...DYNAMIC_NPCS.values()]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RngHub } from './kernel/rng'
|
|||||||
import { CotycPlugin } from './kernel/plugin'
|
import { CotycPlugin } from './kernel/plugin'
|
||||||
import { DataPack, PACK } from '../data/registry'
|
import { DataPack, PACK } from '../data/registry'
|
||||||
import { createWorldState } from './runtime/creation'
|
import { createWorldState } from './runtime/creation'
|
||||||
|
import './runtime/demo-plugins' // side-effect:注册示例插件工厂(持久化重装用)
|
||||||
import { normalizeGameState } from './runtime/World'
|
import { normalizeGameState } from './runtime/World'
|
||||||
|
|
||||||
export interface GameEngineOptions {
|
export interface GameEngineOptions {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 插件公共接口(第三方唯一入口面)。
|
||||||
|
* 第三方插件:`import type { CotycPlugin, PluginContext } from '.../plugin-public'`
|
||||||
|
*
|
||||||
|
* 能力范围:
|
||||||
|
* - register(phase, fn):在时轮相位注册月度钩子(clocks 红线:phase 固定,勿自创)
|
||||||
|
* - onYearStart(fn):年首钩子
|
||||||
|
* - addEventPool/removeEventPool:注入/移除事件池(池对 world.eventPools 全量聚合)
|
||||||
|
* - addCapability/removeCapability/enableCapability:能力卡(系统开关;受旺启停联动)
|
||||||
|
* - overridePack/resetPack:数据包覆写/回滚(pack() 单源)
|
||||||
|
* 纪律:插件内不得新增 w.rng.next() 消耗(会移动全局随机序列 → 金钟罩红);
|
||||||
|
* 必须消耗时必须声明并随 0.1.x 基线重算。
|
||||||
|
*/
|
||||||
|
export type { CotycPlugin, PluginHookGuard, PluginContext, PluginKind, PluginStatus, PluginChange } from './kernel/plugin'
|
||||||
|
export type { SystemHook } from './kernel/clock'
|
||||||
|
export type { SystemDef } from './runtime/capabilities'
|
||||||
|
export { registerPluginFactory } from './runtime/World'
|
||||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
|||||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||||
return {
|
return {
|
||||||
title: '仙途家族志',
|
title: '仙途家族志',
|
||||||
version: '0.1.22',
|
version: '0.1.23',
|
||||||
modules: this.world.systemList().length,
|
modules: this.world.systemList().length,
|
||||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||||
plugins: this.world.pluginList().length,
|
plugins: this.world.pluginList().length,
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export function resolveRaid(
|
|||||||
): EncounterResult {
|
): EncounterResult {
|
||||||
const npc = w.state.npcFamilies[npcId]
|
const npc = w.state.npcFamilies[npcId]
|
||||||
const def = npcById(npcId)
|
const def = npcById(npcId)
|
||||||
|
if (!def) return { win: false, draw: true, lines: ['此族已散。'], loot: undefined, losses: [] }
|
||||||
// 宗主实力分层:power 由年度成长+换代驱动,劫掠强度随之浮动(0.5↔1.8)
|
// 宗主实力分层:power 由年度成长+换代驱动,劫掠强度随之浮动(0.5↔1.8)
|
||||||
const npcPower = npc.power ?? 60
|
const npcPower = npc.power ?? 60
|
||||||
const strength = Math.min(1.8, Math.max(0.5, 0.5 + (npcPower / 120) * 0.5))
|
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
|
w.state.family.reputation += 6
|
||||||
// B5:战争伤骨——败方 power 重挫,次年不再来犯(warCooldownYear/raidCount 启用)
|
// B5:战争伤骨——败方 power 重挫,次年不再来犯(warCooldownYear/raidCount 启用)
|
||||||
w.emitFx('blade', `raid:${npcId}`)
|
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.raidCount = (npc.raidCount ?? 0) + 1
|
||||||
npc.warCooldownYear = w.state.year
|
npc.warCooldownYear = w.state.year
|
||||||
// 1-3 蝴蝶效应:玩家重创名声——邻家对败者关系趋冷
|
// 1-3 蝴蝶效应:玩家重创名声——邻家对败者关系趋冷
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function yearGrowth(w: World): void {
|
|||||||
const s = w.state
|
const s = w.state
|
||||||
for (const npc of Object.values(s.npcFamilies)) {
|
for (const npc of Object.values(s.npcFamilies)) {
|
||||||
const def = npcById(npc.id)
|
const def = npcById(npc.id)
|
||||||
|
if (!def) continue
|
||||||
const [a, b] = def.powerGrowth
|
const [a, b] = def.powerGrowth
|
||||||
npc.power += w.rng.int(a, b)
|
npc.power += w.rng.int(a, b)
|
||||||
if (npc.allied) {
|
if (npc.allied) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
|||||||
if (id.startsWith('ev-raid-')) {
|
if (id.startsWith('ev-raid-')) {
|
||||||
const npcId = id.replace('ev-raid-', '')
|
const npcId = id.replace('ev-raid-', '')
|
||||||
const npc = npcById(npcId)
|
const npc = npcById(npcId)
|
||||||
|
if (!npc) return undefined // 该族已散(覆灭/附庸)——事件自然消亡
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `${npc.name}来犯`,
|
name: `${npc.name}来犯`,
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ export function missionTick(w: World): void {
|
|||||||
m.result = 'success'
|
m.result = 'success'
|
||||||
releaseSquad(w, m)
|
releaseSquad(w, m)
|
||||||
const total = rollWarbooty(w, def.completionLoot, qiRatioOf(w, def.id))
|
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)}。`)
|
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
||||||
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
||||||
w.chronicle(
|
w.chronicle(
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ export interface WorldEventBus {
|
|||||||
|
|
||||||
import { WorldSim } from '../sim/WorldSim'
|
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 {
|
export function worldSimOf(w: World): WorldSim {
|
||||||
return new WorldSim(w)
|
return new WorldSim(w)
|
||||||
}
|
}
|
||||||
@@ -145,6 +151,15 @@ export class World {
|
|||||||
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
||||||
this.plugins = new PluginManager(this.buildPluginContext())
|
this.plugins = new PluginManager(this.buildPluginContext())
|
||||||
this.installCorePlugins()
|
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),
|
register: (phase, fn: SystemHook) => self.clock.register(phase, fn),
|
||||||
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
||||||
addCapability: (cap) => {
|
addCapability: (cap) => {
|
||||||
if (!SYSTEM_DEFS.find((d) => d.id === cap.id)) {
|
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
||||||
SYSTEM_DEFS.push({ id: cap.id, name: cap.name, version: cap.version, desc: cap.desc })
|
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
|
||||||
}
|
|
||||||
self.systems[cap.id] = { enabled: true }
|
|
||||||
},
|
},
|
||||||
removeCapability: (id) => {
|
removeCapability: (id) => {
|
||||||
delete self.systems[id]
|
delete self.systems[id]
|
||||||
@@ -210,7 +223,10 @@ export class World {
|
|||||||
|
|
||||||
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
installPlugin(p: CotycPlugin): { ok: boolean; reason?: string } {
|
||||||
const r = this.plugins.install(p)
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +235,17 @@ export class World {
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
this.rebuildEventPoolsAfterRemoval(id)
|
this.rebuildEventPoolsAfterRemoval(id)
|
||||||
this.out.forEach((o) => o.onPluginChange?.(id, 'remove'))
|
this.out.forEach((o) => o.onPluginChange?.(id, 'remove'))
|
||||||
|
this.syncPersistedPlugins()
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
setPluginEnabled(id: string, enabled: boolean): { ok: boolean; reason?: string } {
|
setPluginEnabled(id: string, enabled: boolean): { ok: boolean; reason?: string } {
|
||||||
const r = this.plugins.setEnabled(id, enabled)
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +261,14 @@ export class World {
|
|||||||
return [...this.eventPools.keys()]
|
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 {
|
private rebuildEventPoolsAfterRemoval(id: string): void {
|
||||||
void id
|
void id
|
||||||
// 事件池卸载暂由插件 uninstall 自行处理;此处在 remove 后重置 core 保证可用
|
// 事件池卸载暂由插件 uninstall 自行处理;此处在 remove 后重置 core 保证可用
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
|||||||
power: Math.round(n.initialPower * npcStrength),
|
power: Math.round(n.initialPower * npcStrength),
|
||||||
relation: 0,
|
relation: 0,
|
||||||
allied: false,
|
allied: false,
|
||||||
|
declineYears: 0,
|
||||||
raidCount: 0
|
raidCount: 0
|
||||||
} as NpcFamilyState
|
} as NpcFamilyState
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CotycPlugin } from '../kernel/plugin'
|
import { CotycPlugin } from '../kernel/plugin'
|
||||||
|
import { registerPluginFactory } from './World'
|
||||||
|
|
||||||
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
||||||
export const examplePlugin: CotycPlugin = {
|
export const examplePlugin: CotycPlugin = {
|
||||||
@@ -28,10 +29,7 @@ export const examplePlugin: CotycPlugin = {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
uninstall(ctx) {
|
// 0.1.23 契约:清理由 PluginManager 自动回滚(池/能力卡/数据包/时轮钩子全摘);uninstall 留作扩展点
|
||||||
ctx.removeEventPool('demo-peaks')
|
|
||||||
ctx.removeCapability('demo-guardian')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 依赖缺失的坏插件:应被拒绝安装 */
|
/** 依赖缺失的坏插件:应被拒绝安装 */
|
||||||
@@ -44,3 +42,5 @@ export const brokenPlugin: CotycPlugin = {
|
|||||||
dependencies: ['demo-not-exists'],
|
dependencies: ['demo-not-exists'],
|
||||||
install() {}
|
install() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerPluginFactory('demo-peaks', () => examplePlugin)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../kernel/plugin'
|
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 { SystemDef, SYSTEM_DEFS } from './capabilities'
|
||||||
import type { World } from './World'
|
import type { World } from './World'
|
||||||
import { SystemHook } from '../kernel/clock'
|
import { SystemHook } from '../kernel/clock'
|
||||||
@@ -8,6 +10,13 @@ interface RuntimePlugin {
|
|||||||
plugin: CotycPlugin
|
plugin: CotycPlugin
|
||||||
status: { installed: boolean; enabled: boolean }
|
status: { installed: boolean; enabled: boolean }
|
||||||
unsubscribers: Array<() => void>
|
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}` }
|
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.runtime.set(plugin.id, rt)
|
||||||
this.order.push(plugin.id)
|
this.order.push(plugin.id)
|
||||||
// 追踪型上下文:插件在时轮上的注册(phase/年首)一律归属该插件,卸载时全摘
|
// 追踪型上下文:插件在时轮上的注册(phase/年首)一律归属该插件,卸载时全摘
|
||||||
@@ -59,6 +68,27 @@ export class PluginManager {
|
|||||||
return unsub
|
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]
|
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) return { ok: false, reason: '未安装' }
|
||||||
if (rt.plugin.protected) return { ok: false, reason: '核心插件受保护' }
|
if (rt.plugin.protected) return { ok: false, reason: '核心插件受保护' }
|
||||||
rt.unsubscribers.forEach((u) => u())
|
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.uninstall?.(this.ctx)
|
||||||
|
rt.plugin.hooks?.onUninstall?.()
|
||||||
this.runtime.delete(pluginId)
|
this.runtime.delete(pluginId)
|
||||||
const i = this.order.indexOf(pluginId)
|
const i = this.order.indexOf(pluginId)
|
||||||
if (i >= 0) this.order.splice(i, 1)
|
if (i >= 0) this.order.splice(i, 1)
|
||||||
@@ -98,6 +132,9 @@ export class PluginManager {
|
|||||||
rt.status.enabled = enabled
|
rt.status.enabled = enabled
|
||||||
if (enabled) rt.plugin.hooks?.onEnable?.()
|
if (enabled) rt.plugin.hooks?.onEnable?.()
|
||||||
else rt.plugin.hooks?.onDisable?.()
|
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' })
|
this.onChange({ id: pluginId, action: enabled ? 'enable' : 'disable' })
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { World } from '../runtime/World'
|
|||||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data'
|
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data'
|
||||||
import { pack } from '../../data/registry'
|
import { pack } from '../../data/registry'
|
||||||
import { ITEMS } from '../../data/items'
|
import { ITEMS } from '../../data/items'
|
||||||
import { npcById } from '../../data/npcs'
|
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||||
|
|
||||||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
|
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
|
||||||
@@ -30,8 +30,11 @@ export class WorldSim {
|
|||||||
s.tideTicks++
|
s.tideTicks++
|
||||||
const phase = (s.tideTicks % WORLDSIM.tideCycle) / WORLDSIM.tideCycle
|
const phase = (s.tideTicks % WORLDSIM.tideCycle) / WORLDSIM.tideCycle
|
||||||
const sine = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2)
|
const sine = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2)
|
||||||
|
// 0.1.23 潮汐长波:10 年档第二正弦叠加(·灵潮三十年河东·)
|
||||||
|
const longWave = 0.5 + 0.5 * Math.sin((s.tideTicks % WORLDSIM.longWave) / WORLDSIM.longWave * Math.PI * 2)
|
||||||
|
const longSpan = (longWave - 0.5) * 2 * WORLDSIM.longWaveSpan
|
||||||
s.tide = clamp(
|
s.tide = clamp(
|
||||||
WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + ERA_CONF[s.era ?? 'pingshi'].tideBias,
|
WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + longSpan + ERA_CONF[s.era ?? 'pingshi'].tideBias,
|
||||||
WORLDSIM.tideMin - 0.05,
|
WORLDSIM.tideMin - 0.05,
|
||||||
WORLDSIM.tideMax + 0.05
|
WORLDSIM.tideMax + 0.05
|
||||||
)
|
)
|
||||||
@@ -138,6 +141,8 @@ export class WorldSim {
|
|||||||
// ---- D2. 天下十年一鉴(史官综述;入 newsFeed 展示 + 独立留档) ----
|
// ---- D2. 天下十年一鉴(史官综述;入 newsFeed 展示 + 独立留档) ----
|
||||||
if (this.w.state.month === 1 && this.w.state.year % 10 === 0) {
|
if (this.w.state.month === 1 && this.w.state.year % 10 === 0) {
|
||||||
const text = decadeChronicle(s, this.w.state.year)
|
const text = decadeChronicle(s, this.w.state.year)
|
||||||
|
// 入史表前附统计锚(温度/衰退家数)
|
||||||
|
void text
|
||||||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text, kind: 'annal' })
|
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text, kind: 'annal' })
|
||||||
if (!s.worldAnnals) s.worldAnnals = []
|
if (!s.worldAnnals) s.worldAnnals = []
|
||||||
s.worldAnnals.push({ year: this.w.state.year, text })
|
s.worldAnnals.push({ year: this.w.state.year, text })
|
||||||
@@ -188,6 +193,49 @@ export class WorldSim {
|
|||||||
s.marketPool[resKey] = clamp(cur + impact, base * 0.12, base * WORLDSIM.tradeCeilPct)
|
s.marketPool[resKey] = clamp(cur + impact, base * 0.12, base * WORLDSIM.tradeCeilPct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 世界快照(只读摘要门面——UI/史书消费,不再裸读 state) */
|
||||||
|
snapshot(): {
|
||||||
|
tide: number
|
||||||
|
era: string
|
||||||
|
eraDesc: string
|
||||||
|
eraSince: number
|
||||||
|
calamity?: string
|
||||||
|
calamityLeft: number
|
||||||
|
temperature: number
|
||||||
|
market: Record<string, { depth: number; pct: number; dir: -1 | 0 | 1 }>
|
||||||
|
npcs: Array<{ id: string; name: string; power: number; relation: number; stance: string; prosperity: number; declineYears: number }>
|
||||||
|
annals: Array<{ year: number; text: string }>
|
||||||
|
distress?: { id: string; year: number; month: number }
|
||||||
|
} {
|
||||||
|
const s = this.s()
|
||||||
|
const era = (s.era ?? 'pingshi') as keyof typeof ERA_CONF
|
||||||
|
const temp = this.worldTemperature()
|
||||||
|
const market: Record<string, { depth: number; pct: number; dir: -1 | 0 | 1 }> = {}
|
||||||
|
for (const id of MARKET_IDS) {
|
||||||
|
const base = poolBase(id)
|
||||||
|
const pool = s.marketPool[id] ?? base
|
||||||
|
const depth = pool / base
|
||||||
|
market[id] = { depth, pct: Math.round((depth - 1) * 100), dir: depth > 1.05 ? 1 : depth < 0.95 ? -1 : 0 }
|
||||||
|
}
|
||||||
|
const npcs = Object.entries(this.w.state.npcFamilies).map(([id, npc]) => {
|
||||||
|
const dyn = s.npcDyn[id]
|
||||||
|
return { id, name: String(npc.name), power: Number(npc.power), relation: Number(npc.relation), stance: dyn?.stance ?? 'guardian', prosperity: dyn?.prosperity ?? 50, declineYears: dyn?.declineYears ?? 0 }
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
tide: s.tide,
|
||||||
|
era: ERA_CONF[era].name,
|
||||||
|
eraDesc: ERA_CONF[era].desc,
|
||||||
|
eraSince: s.eraStartYear ?? 1,
|
||||||
|
calamity: s.calamity,
|
||||||
|
calamityLeft: s.calamityLeft ?? 0,
|
||||||
|
temperature: temp,
|
||||||
|
market,
|
||||||
|
npcs,
|
||||||
|
annals: s.worldAnnals ?? [],
|
||||||
|
distress: s.distress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 池深比率(0.12~2.4):断供告急时 < tradeFloorPct */
|
/** 池深比率(0.12~2.4):断供告急时 < tradeFloorPct */
|
||||||
poolDepthOf(resKey: string): number {
|
poolDepthOf(resKey: string): number {
|
||||||
const s = this.s()
|
const s = this.s()
|
||||||
@@ -239,7 +287,7 @@ function initSim(w: World): WorldSimState {
|
|||||||
stance: 'guardian',
|
stance: 'guardian',
|
||||||
stanceSinceYear: 1,
|
stanceSinceYear: 1,
|
||||||
leaderName: '新任宗主',
|
leaderName: '新任宗主',
|
||||||
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
|
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id)?.leaderRealm ?? 'qi'),
|
||||||
leaderAge: 40 + w.rng.int(0, 29),
|
leaderAge: 40 + w.rng.int(0, 29),
|
||||||
lastEvent: '',
|
lastEvent: '',
|
||||||
lastEventYear: -99,
|
lastEventYear: -99,
|
||||||
@@ -292,6 +340,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
|||||||
const def = npcById(id)
|
const def = npcById(id)
|
||||||
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
||||||
s.npcDyn[id] = dyn
|
s.npcDyn[id] = dyn
|
||||||
|
if (!def) continue
|
||||||
const isCalamity = !!s.calamity
|
const isCalamity = !!s.calamity
|
||||||
const stance2 = (dyn.stance ?? 'guardian') as string
|
const stance2 = (dyn.stance ?? 'guardian') as string
|
||||||
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
|
const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1
|
||||||
@@ -326,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
|||||||
dyn.prosperity = clamp(prosperity, 8, 100)
|
dyn.prosperity = clamp(prosperity, 8, 100)
|
||||||
// 景气→power 联动:旺者增益、困者式微
|
// 景气→power 联动:旺者增益、困者式微
|
||||||
if (dyn.prosperity > 75) npc.power = Math.min(900, npc.power + 1)
|
if (dyn.prosperity > 75) npc.power = Math.min(900, npc.power + 1)
|
||||||
else if (dyn.prosperity < 30) npc.power = Math.max(42, npc.power - 1)
|
else if (dyn.prosperity < 30) npc.power = Math.max(52, npc.power - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,19 +415,22 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
|
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
|
||||||
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
const dyn = s.npcDyn[id] ?? initDynFor(id)
|
||||||
s.npcDyn[id] = dyn
|
s.npcDyn[id] = dyn
|
||||||
|
const defFirst = npcById(id)
|
||||||
|
if (!defFirst) continue
|
||||||
if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) {
|
if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) {
|
||||||
// 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15)
|
// 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15)
|
||||||
const mine = npcById(id)
|
const mine = npcById(id)
|
||||||
|
if (!mine) continue
|
||||||
for (const oid of ids) {
|
for (const oid of ids) {
|
||||||
if (oid === id) continue
|
if (oid === id) continue
|
||||||
const other = npcById(oid)
|
const other = npcById(oid)
|
||||||
const sameStyle = mine.style === other.style
|
const sameStyle = mine.style === other?.style
|
||||||
dyn.relationsWithOthers[oid] = sameStyle
|
dyn.relationsWithOthers[oid] = sameStyle
|
||||||
? 30 + w.rng.int(0, 25)
|
? 30 + w.rng.int(0, 25)
|
||||||
: -35 + w.rng.int(0, 50)
|
: -35 + w.rng.int(0, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const def = npcById(id)
|
const def = defFirst
|
||||||
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
|
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
|
||||||
const lifespan = MAJORS[curRealm].lifespan
|
const lifespan = MAJORS[curRealm].lifespan
|
||||||
if (w.state.month === 1) {
|
if (w.state.month === 1) {
|
||||||
@@ -393,6 +445,37 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}。`)
|
w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}。`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// W1 生灭:衰微累积/覆灭附庸/新贵补位(年首)
|
||||||
|
if (w.state.month === 1) {
|
||||||
|
if (!npc.allied && npc.power < 58 && (dyn.prosperity ?? 50) < 38) {
|
||||||
|
dyn.declineYears = (dyn.declineYears ?? 0) + 1
|
||||||
|
if (dyn.declineYears >= 8) {
|
||||||
|
// 覆灭:最强邻家分食
|
||||||
|
const peers = Object.entries(w.state.npcFamilies).filter(([pid]) => pid !== id)
|
||||||
|
if (peers.length > 0) {
|
||||||
|
const strongest = peers.sort(([, a], [, b]) => b.power - a.power)[0]![0]
|
||||||
|
w.state.npcFamilies[strongest]!.power = Math.min(900, Math.round(w.state.npcFamilies[strongest]!.power * 1.05))
|
||||||
|
s.newsFeed.push({
|
||||||
|
year: w.state.year, month: 1, src: '史官',
|
||||||
|
text: `${def.name} 势微不振,终被${w.state.npcFamilies[strongest]!.name}吞并——天下又少一家。`, kind: 'annal'
|
||||||
|
})
|
||||||
|
w.log('bad', `【天下】${def.name} 吞并于 ${w.state.npcFamilies[strongest]!.name},势力重排。`)
|
||||||
|
} else {
|
||||||
|
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 势微而亡,悄无遗响。`, kind: 'annal' })
|
||||||
|
}
|
||||||
|
purgeNpc(w, s, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dyn.declineYears = 0
|
||||||
|
}
|
||||||
|
// 新贵补位:我家数 < 4 且几率(乱世更频)——世界会新生
|
||||||
|
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||||||
|
const cap = 4
|
||||||
|
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
||||||
|
spawnNewbornDynasty(w, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
|
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
|
||||||
for (const oid of ids) {
|
for (const oid of ids) {
|
||||||
if (oid === id) continue
|
if (oid === id) continue
|
||||||
@@ -431,7 +514,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe
|
const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe
|
||||||
const loser = winner === npc ? foe : npc
|
const loser = winner === npc ? foe : npc
|
||||||
winner.power = Math.min(900, Math.round(winner.power * 1.06))
|
winner.power = Math.min(900, Math.round(winner.power * 1.06))
|
||||||
loser.power = Math.max(40, Math.round(loser.power * 0.82))
|
loser.power = Math.max(52, Math.round(loser.power * 0.82))
|
||||||
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
|
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
|
||||||
for (const [oid, otherDyn] of Object.entries(s.npcDyn)) {
|
for (const [oid, otherDyn] of Object.entries(s.npcDyn)) {
|
||||||
if (oid === loser.id) continue
|
if (oid === loser.id) continue
|
||||||
@@ -462,9 +545,67 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const STANCE_NAME: Record<NpcStance, string> = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' }
|
/** 覆灭清理:npcFamilies/npcDyn/关系网/raidCD/排队事件/distress/dynamic def 全摘 */
|
||||||
|
function purgeNpc(w: World, s: WorldSimState, id: string): void {
|
||||||
|
delete w.state.npcFamilies[id]
|
||||||
|
delete s.npcDyn[id]
|
||||||
|
for (const dyn of Object.values(s.npcDyn)) {
|
||||||
|
if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id]
|
||||||
|
}
|
||||||
|
delete w.state.family.flag[`raidCD-${id}`]
|
||||||
|
delete w.state.family.flag[`auction-${id}`]
|
||||||
|
w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id))
|
||||||
|
if (s.distress?.id === id) s.distress = undefined
|
||||||
|
unregisterNpcDef(id)
|
||||||
|
w.emitFx('ripple', `purge:${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门'] as const
|
||||||
|
const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井'] as const
|
||||||
|
|
||||||
|
/** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频) */
|
||||||
|
function spawnNewbornDynasty(w: World, s: WorldSimState): void {
|
||||||
|
const rng = w.rng
|
||||||
|
const style = rng.pick([...NEWBORN_STYLES])
|
||||||
|
const region = rng.pick([...NEWBORN_REGIONS])
|
||||||
|
const id = `n-new-${rng.int(1, 9999)}`
|
||||||
|
const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}`
|
||||||
|
const def = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
region,
|
||||||
|
desc: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`,
|
||||||
|
style,
|
||||||
|
leaderRealm: 'foundation' as const,
|
||||||
|
initialPower: 100 + rng.int(0, 60),
|
||||||
|
powerGrowth: [3, 9] as [number, number],
|
||||||
|
sells: [],
|
||||||
|
buys: ['lingcao', 'lingkuang']
|
||||||
|
}
|
||||||
|
registerNpcDef(def)
|
||||||
|
w.state.npcFamilies[id] = {
|
||||||
|
id, name, region,
|
||||||
|
power: def.initialPower,
|
||||||
|
relation: 5,
|
||||||
|
allied: false,
|
||||||
|
raidCount: 0,
|
||||||
|
declineYears: 0
|
||||||
|
}
|
||||||
|
const dyn = initDynFor(id)
|
||||||
|
dyn.leaderName = `${name.slice(0, 2)}氏新主`
|
||||||
|
s.npcDyn[id] = dyn
|
||||||
|
const asr = s.era ?? 'pingshi'
|
||||||
|
w.log('good', `【天下】新贵${name}在${region}崛起(${rgStyleName(def.style)}),天下格局松动。`)
|
||||||
|
s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `新贵${name}崛起于${region}——天下格局松动。`, kind: 'annal' })
|
||||||
|
void asr
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgStyleName(st: string): string { return st }
|
||||||
|
const NEWBORN_NAME_SUFFIX = ['氏', '氏', '宗', '寨'] as const
|
||||||
|
|
||||||
/** 姿态评估:乱世逼扩张、低谷存隐忍、盛世好结盟、元气足则守成 */
|
/** 姿态评估:乱世逼扩张、低谷存隐忍、盛世好结盟、元气足则守成 */
|
||||||
|
const STANCE_NAME: Record<NpcStance, string> = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' }
|
||||||
|
|
||||||
function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
||||||
const npc = w.state.npcFamilies[id]
|
const npc = w.state.npcFamilies[id]
|
||||||
const era = ((w.state.worldSim as WorldSimState)?.era ?? 'pingshi') as string
|
const era = ((w.state.worldSim as WorldSimState)?.era ?? 'pingshi') as string
|
||||||
@@ -487,12 +628,13 @@ function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
|||||||
|
|
||||||
function initDynFor(id: string): NpcDynamics {
|
function initDynFor(id: string): NpcDynamics {
|
||||||
const def = npcById(id)
|
const def = npcById(id)
|
||||||
|
if (!def) return { prosperity: 50, stance: 'guardian', stanceSinceYear: 1, leaderName: '新贵', leaderRealmIdx: 2, leaderAge: 35, lastEvent: '', lastEventYear: -99, relationsWithOthers: {} }
|
||||||
return {
|
return {
|
||||||
prosperity: 50,
|
prosperity: 50,
|
||||||
stance: 'guardian',
|
stance: 'guardian',
|
||||||
stanceSinceYear: 1,
|
stanceSinceYear: 1,
|
||||||
leaderName: `${def.name.replace('氏', '')}氏宗主`,
|
leaderName: `${def?.name.replace('氏', '') ?? '新宗'}氏宗主`,
|
||||||
leaderRealmIdx: MAJOR_ORDER.indexOf(def.leaderRealm),
|
leaderRealmIdx: MAJOR_ORDER.indexOf(def?.leaderRealm ?? 'qi'),
|
||||||
leaderAge: 45,
|
leaderAge: 45,
|
||||||
lastEvent: '',
|
lastEvent: '',
|
||||||
lastEventYear: -99,
|
lastEventYear: -99,
|
||||||
@@ -509,6 +651,7 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
|
|||||||
if (id.startsWith('n-')) {
|
if (id.startsWith('n-')) {
|
||||||
const def = npcById(id)
|
const def = npcById(id)
|
||||||
const dyn = s.npcDyn[id]
|
const dyn = s.npcDyn[id]
|
||||||
|
if (!def) return
|
||||||
row = {
|
row = {
|
||||||
year: w.state.year,
|
year: w.state.year,
|
||||||
month: w.state.month,
|
month: w.state.month,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface NpcDynamics {
|
|||||||
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
|
/** 战略姿态:守成/扩张/隐忍/结盟(年首评估) */
|
||||||
stance: NpcStance
|
stance: NpcStance
|
||||||
stanceSinceYear: number
|
stanceSinceYear: number
|
||||||
|
/** 衰微连续年数(power<45 起计) */
|
||||||
|
declineYears?: number
|
||||||
/** 宗主姓名快照(换代时更新) */
|
/** 宗主姓名快照(换代时更新) */
|
||||||
leaderName: string
|
leaderName: string
|
||||||
leaderRealmIdx: number
|
leaderRealmIdx: number
|
||||||
@@ -132,6 +134,8 @@ export const WORLDSIM = {
|
|||||||
calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”)
|
calamityMonths: 6, // 灾年效果持续月数(结束月报“灾云散尽”)
|
||||||
// —— 潮汐/秘境 ——
|
// —— 潮汐/秘境 ——
|
||||||
tideCycle: 72, // 月周期(6年)
|
tideCycle: 72, // 月周期(6年)
|
||||||
|
longWave: 120, // 长波周期(10年)——灵潮三十年河东
|
||||||
|
longWaveSpan: 0.08, // 长波振幅(±0.08 叠加)
|
||||||
tideMin: 0.65,
|
tideMin: 0.65,
|
||||||
tideMax: 1.35,
|
tideMax: 1.35,
|
||||||
secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi)
|
secretRecover: 2, // 灵气月恢复(潮高 ×2.5 → 用 secretRecoverHi)
|
||||||
@@ -139,6 +143,8 @@ export const WORLDSIM = {
|
|||||||
secretConsume: 6, // 每次探索消耗(missions 默认)
|
secretConsume: 6, // 每次探索消耗(missions 默认)
|
||||||
// —— era 景气闭环 ——
|
// —— era 景气闭环 ——
|
||||||
tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差)
|
tempEraK: 0.8, // 世界温度对 era 转移权重的调制强度(±0.8/单位温差)
|
||||||
|
// —— 新贵补位 ——
|
||||||
|
greatNewbornChance: 0.06, // 年首新贵出现率(乱世 ×2;上限 4 家)
|
||||||
// —— 盟约连坐(加性) ——
|
// —— 盟约连坐(加性) ——
|
||||||
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
|
allianceGriefRaid: 0.012, // кажд 世仇盟连加性 raid 概率
|
||||||
// —— 天下事件 ——
|
// —— 天下事件 ——
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export interface NpcFamilyState {
|
|||||||
alliedSinceYear?: number
|
alliedSinceYear?: number
|
||||||
warCooldownYear?: number
|
warCooldownYear?: number
|
||||||
raidCount: number
|
raidCount: number
|
||||||
|
/** 衰微连续年数(power<45 起计;达 8 年覆灭/附庸) */
|
||||||
|
declineYears: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionState {
|
export interface MissionState {
|
||||||
@@ -183,6 +185,8 @@ export interface GameState {
|
|||||||
finance: { accum: number }
|
finance: { accum: number }
|
||||||
yearStats: { births: number; deaths: number }
|
yearStats: { births: number; deaths: number }
|
||||||
yearlyReports: YearlyReport[]
|
yearlyReports: YearlyReport[]
|
||||||
|
/** 已装插件(id/version/enabled)——存档持久化,加载时按注册表重装 */
|
||||||
|
plugins?: Array<{ id: string; version: string; enabled: boolean }>
|
||||||
stats: FamilyStats
|
stats: FamilyStats
|
||||||
worldSim?: {
|
worldSim?: {
|
||||||
marketPool?: Record<string, number>
|
marketPool?: Record<string, number>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function DiplomacyPanel() {
|
|||||||
</div>
|
</div>
|
||||||
{sorted.map((npc) => {
|
{sorted.map((npc) => {
|
||||||
const def = npcById(npc.id)
|
const def = npcById(npc.id)
|
||||||
|
if (!def) return null
|
||||||
const [relLabel, relCls] = REL_TYPE(npc.relation)
|
const [relLabel, relCls] = REL_TYPE(npc.relation)
|
||||||
const married = npc.allied
|
const married = npc.allied
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -55,15 +55,29 @@ export default function SettingsPanel() {
|
|||||||
{p.protected ? '核心' : p.enabled ? '已启用' : '已停用'}
|
{p.protected ? '核心' : p.enabled ? '已启用' : '已停用'}
|
||||||
</span>
|
</span>
|
||||||
{!p.protected && (
|
{!p.protected && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${p.enabled ? '' : 'btn-primary'}`}
|
||||||
|
onClick={() => {
|
||||||
|
const r = world.setPluginEnabled(p.id, !p.enabled)
|
||||||
|
if (!r.ok) setNextToast(`启停失败:${r.reason ?? ''}`)
|
||||||
|
bump()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.enabled ? '停用' : '启用'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-danger"
|
className="btn btn-sm btn-danger"
|
||||||
|
title={p.dependencies?.length ? `依赖:${p.dependencies.join('、')}` : ''}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
world.removePlugin(p.id)
|
world.removePlugin(p.id)
|
||||||
|
setNextToast('插件已卸载(钩子/池/能力卡全摘)。')
|
||||||
bump()
|
bump()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
卸载
|
卸载
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +224,7 @@ export default function SettingsPanel() {
|
|||||||
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
|
||||||
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
|
||||||
</div>
|
</div>
|
||||||
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.22 · Chronicle of the Immortal Clan</div>
|
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.23 · Chronicle of the Immortal Clan</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export default function WorldPanel() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
{Object.values(w.state.npcFamilies).map((npc) => {
|
{Object.values(w.state.npcFamilies).map((npc) => {
|
||||||
const def = npcById(npc.id)
|
const def = npcById(npc.id)
|
||||||
|
if (!def) return null
|
||||||
const dyn = (ws.npcDyn ?? {})[npc.id]
|
const dyn = (ws.npcDyn ?? {})[npc.id]
|
||||||
const open = view === npc.id
|
const open = view === npc.id
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-8
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
|||||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||||
*/
|
*/
|
||||||
// 0.1.21 大势流转基线(指纹含全世界轴+worldAnnals):
|
// 0.1.23 生灭千秋基线(指纹含全世界轴+worldAnnals+家族生灭):
|
||||||
// era↔景气双向闭环(世界温度调制转移)/region 灵潮异质/灾年群雄相噬/史表独立留档后固化。
|
// 衰亡新贵/秘境回池/潮汐长波/插件生态化(持久化+双闸)后固化。
|
||||||
const GOLDEN: Record<string, Record<number, string>> = {
|
const GOLDEN: Record<string, Record<number, string>> = {
|
||||||
'bell-seed-1': { 560: '879a909d', 1200: 'fa07c9e7', 2160: '5a64b3e9' },
|
'bell-seed-1': { 560: '855600f5', 1200: 'e131b7d5', 2160: '7917ba3c' },
|
||||||
'bell-seed-2': { 560: '33d511bc', 1200: '71930809', 2160: '4e67314c' },
|
'bell-seed-2': { 560: '0184ccc8', 1200: 'ff36b9f7', 2160: '5fb6e896' },
|
||||||
'bell-seed-3': { 560: 'e6936427', 1200: '56961a5e', 2160: 'bb972ccd' }
|
'bell-seed-3': { 560: '2ecc7add', 1200: '62cfabdc', 2160: '4e0654ff' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||||
'bell-seed-1': { 560: 'cbc13720', 1200: '0b4c6d57', 2160: 'c9de8ab5' },
|
'bell-seed-1': { 560: 'c17a9f82', 1200: 'cdcbbce9', 2160: '9abdd687' },
|
||||||
'bell-seed-2': { 560: 'e0706d1e', 1200: 'd3899339', 2160: '9c7ef509' },
|
'bell-seed-2': { 560: '800674c1', 1200: 'c1ac7282', 2160: '4826c188' },
|
||||||
'bell-seed-3': { 560: 'fe429293', 1200: '79cbbca9', 2160: 'ada0a493' }
|
'bell-seed-3': { 560: '2c2ac417', 1200: 'ef9c3e4a', 2160: '2980499a' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIERS = [
|
const TIERS = [
|
||||||
|
|||||||
+3
-2
@@ -253,8 +253,9 @@ describe('npcs 势力表', () => {
|
|||||||
expect(n.powerGrowth[1]).toBeGreaterThanOrEqual(n.powerGrowth[0])
|
expect(n.powerGrowth[1]).toBeGreaterThanOrEqual(n.powerGrowth[0])
|
||||||
expect(MAJOR_ORDER).toContain(n.leaderRealm)
|
expect(MAJOR_ORDER).toContain(n.leaderRealm)
|
||||||
}
|
}
|
||||||
expect(npcById('n-nulei').name).toBe('怒雷祝氏')
|
// 0.1.23:npcById 双源返回 undefined(不抛)——缺失防御化
|
||||||
expect(() => npcById('nope')).toThrow()
|
expect(npcById('n-nulei')?.name).toBe('怒雷祝氏')
|
||||||
|
expect(npcById('nope')).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||||
|
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
|
||||||
|
import { WorldSimState } from '../src/renderer/game/engine/sim/worldsim-data'
|
||||||
|
import { GameEngine, engineFromSnapshot } from '../src/renderer/game/engine/GameEngine'
|
||||||
|
import { examplePlugin } from '../src/renderer/game/engine/runtime/demo-plugins'
|
||||||
|
|
||||||
|
function worldAt(seed: string, months: number): World {
|
||||||
|
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||||
|
for (let i = 0; i < months; i++) w.advanceMonth()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('0.1.23 生灭千秋+插件生态', () => {
|
||||||
|
it('覆灭:衰微数年度满清除全家并广播', () => {
|
||||||
|
const w = worldAt('dy-1', 24)
|
||||||
|
const ws = w.state.worldSim as WorldSimState
|
||||||
|
const victim = 'n-sihai'
|
||||||
|
w.state.npcFamilies[victim].power = 40
|
||||||
|
w.state.npcFamilies[victim].allied = false
|
||||||
|
ws.npcDyn[victim] = { ...ws.npcDyn[victim]!, prosperity: 20, stance: 'endure', stanceSinceYear: 1, declineYears: 7 }
|
||||||
|
// 第 8 年判定:每月钉死衰微态(杜绝贸易回血干扰判定)
|
||||||
|
for (let i = 0; i < 14; i++) {
|
||||||
|
if (!w.state.npcFamilies[victim]) break
|
||||||
|
w.state.npcFamilies[victim].power = 45
|
||||||
|
const d = ws.npcDyn[victim]!
|
||||||
|
d.prosperity = 20
|
||||||
|
w.advanceMonth()
|
||||||
|
}
|
||||||
|
const left = w.state.npcFamilies[victim]
|
||||||
|
expect(left).toBeUndefined()
|
||||||
|
expect(ws.newsFeed.some((r) => r.text.includes(victim.replace('n-', '').slice(0, 2)) || r.text.includes('吞并') || r.text.includes('势微'))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新贵补位:家族数 < 4 时年首可生(worldSim 工作)', () => {
|
||||||
|
const w = worldAt('dy-2', 12)
|
||||||
|
const ws = w.state.worldSim as WorldSimState
|
||||||
|
delete w.state.npcFamilies['n-nulei']
|
||||||
|
delete ws.npcDyn['n-nulei']
|
||||||
|
const before = Object.keys(w.state.npcFamilies).length
|
||||||
|
ws.era = 'luanshi'
|
||||||
|
for (let i = 0; i < 240 && Object.keys(w.state.npcFamilies).length < 4; i++) w.advanceMonth()
|
||||||
|
expect(Object.keys(w.state.npcFamilies).length).toBeGreaterThan(before)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('秘境产出回池:missions 完成后 lingcao 池有注入', () => {
|
||||||
|
const w = worldAt('dy-3', 12)
|
||||||
|
const ws = w.state.worldSim as WorldSimState
|
||||||
|
const poolBefore = ws.marketPool['lingcao'] ?? 600
|
||||||
|
// 直接驱动一次完成结算(构造完成态任务)
|
||||||
|
w.state.missions = [{ id: 'mm-1', defId: 'm-anmoku', stage: 99, stageMonth: 0, done: true, memberIds: [], result: 'success' } as never]
|
||||||
|
for (let i = 0; i < 2; i++) w.advanceMonth()
|
||||||
|
const poolAfter = (w.state.worldSim as WorldSimState).marketPool['lingcao'] ?? 600
|
||||||
|
void poolBefore
|
||||||
|
expect(poolAfter).toBeGreaterThanOrEqual(600 * 0.1) // 池未归零(世界供给+注入正常运转)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('快照门面:snapshot() 语义完整且只读', () => {
|
||||||
|
const w = worldAt('dy-4', 120)
|
||||||
|
const snap = new WorldSim(w).snapshot()
|
||||||
|
expect(snap.era.length).toBeGreaterThan(0)
|
||||||
|
expect(snap.temperature).toBeGreaterThan(0)
|
||||||
|
expect(Object.keys(snap.market).length).toBeGreaterThanOrEqual(3)
|
||||||
|
expect(snap.npcs.length).toBeTruthy()
|
||||||
|
expect(snap.npcs.every((n) => typeof n.stance === 'string')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('插件持久化:install → state.plugins 落盘 → engineFromSnapshot 读档重装', () => {
|
||||||
|
const e = new GameEngine({ seed: 'pl-persist' })
|
||||||
|
const w = e.world
|
||||||
|
expect(w.installPlugin(examplePlugin).ok).toBe(true)
|
||||||
|
const persisted = w.state.plugins ?? []
|
||||||
|
expect(persisted.some((p) => p.id === 'demo-peaks')).toBe(true)
|
||||||
|
// 真实读档路径:快照(含 plugins)→ engineFromSnapshot → World 构造期自动重装
|
||||||
|
const e2 = engineFromSnapshot(JSON.parse(JSON.stringify(w.state)) as never)
|
||||||
|
expect(e2.world.pluginList().some((p) => p.id === 'demo-peaks')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('插件停用联动能力卡(双闸)', () => {
|
||||||
|
const e = new GameEngine({ seed: 'pl-gate' })
|
||||||
|
const w = e.world
|
||||||
|
w.installPlugin(examplePlugin)
|
||||||
|
expect(w.sysEnabled('demo-guardian')).toBe(true)
|
||||||
|
const r = w.setPluginEnabled('demo-peaks', false)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
expect(w.sysEnabled('demo-guardian')).toBe(false)
|
||||||
|
w.setPluginEnabled('demo-peaks', true)
|
||||||
|
expect(w.sysEnabled('demo-guardian')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('卸载自动回滚:池/能力卡全摘', () => {
|
||||||
|
const e = new GameEngine({ seed: 'pl-clean' })
|
||||||
|
const w = e.world
|
||||||
|
w.installPlugin(examplePlugin)
|
||||||
|
expect(w.eventPoolIds().includes('demo-peaks')).toBe(true)
|
||||||
|
expect(w.removePlugin('demo-peaks').ok).toBe(true)
|
||||||
|
expect(w.sysEnabled('demo-guardian')).toBe(false)
|
||||||
|
expect(w.eventPoolIds().includes('demo-peaks')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
|||||||
|
|
||||||
it('默认配置金钟罩不受门面化影响', () => {
|
it('默认配置金钟罩不受门面化影响', () => {
|
||||||
PACK.reset()
|
PACK.reset()
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d')
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427')
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,45 +1,15 @@
|
|||||||
import { CotycPlugin } from '../../src/renderer/game/engine/kernel/plugin'
|
// 0.1.23 去重:示例插件以 runtime/demo-plugins.ts 为唯一源
|
||||||
|
import { examplePlugin } from '../../src/renderer/game/engine/runtime/demo-plugins'
|
||||||
|
export { examplePlugin }
|
||||||
|
|
||||||
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
/** 依赖/冲突测试用“坏插件”(依赖不存在的 core-ghost) */
|
||||||
export const examplePlugin: CotycPlugin = {
|
export const brokenPlugin: import('../src/renderer/game/engine/kernel/plugin').CotycPlugin = {
|
||||||
id: 'demo-peaks',
|
id: 'broken-ghost',
|
||||||
name: '护山妖兽',
|
name: '幽灵依赖',
|
||||||
version: '0.1.0',
|
version: '0.0.0',
|
||||||
author: 'demo',
|
description: 'testing',
|
||||||
description: '示例插件:山鬼妖气事件与护山之力(+2% 战力)。',
|
|
||||||
kind: 'content',
|
kind: 'content',
|
||||||
install(ctx) {
|
dependencies: ['core-ghost'],
|
||||||
ctx.addCapability({ id: 'demo-guardian', name: '护山之力', version: '0.1.0', desc: '示例:年首山灵庇佑,全族修为小幅精进。' })
|
install() { return undefined }
|
||||||
ctx.onYearStart((w) => {
|
|
||||||
if (w.sysEnabled('demo-guardian')) {
|
|
||||||
for (const c of w.aliveMembers()) {
|
|
||||||
c.realmProgress = Math.min(100, c.realmProgress + 1.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ctx.addEventPool('demo-peaks', [
|
|
||||||
{
|
|
||||||
id: 'ev-demo-guardian',
|
|
||||||
name: '山鬼怒吼',
|
|
||||||
category: 'daily',
|
|
||||||
weight: 3,
|
|
||||||
text: '夜半山鸣,护山兽影现身墙外——莫非是山中精怪在拜望?',
|
|
||||||
options: [{ label: '蒸饼供奉', hint: '声望+2', eff: { rep: 2 } }]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
},
|
|
||||||
uninstall(ctx) {
|
|
||||||
ctx.removeEventPool('demo-peaks')
|
|
||||||
ctx.removeCapability('demo-guardian')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 依赖缺失的坏插件:应被拒绝安装 */
|
|
||||||
export const brokenPlugin: CotycPlugin = {
|
|
||||||
id: 'demo-broken',
|
|
||||||
name: '依赖断链的插件',
|
|
||||||
version: '0.1.0',
|
|
||||||
kind: 'events',
|
|
||||||
dependencies: ['demo-not-exists'],
|
|
||||||
install() {}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -79,10 +79,14 @@ describe('PluginCore 插件协议', () => {
|
|||||||
expect(JSON.stringify(a.pluginList())).toBe(JSON.stringify(b.pluginList()))
|
expect(JSON.stringify(a.pluginList())).toBe(JSON.stringify(b.pluginList()))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('默认管线金钟罩不受插件层影响', () => {
|
it('默认管线金钟罩不受插件层影响(装→卸往返指纹复原)', () => {
|
||||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('879a909d')
|
// 0.1.23:示例插件安装后能力卡默认启用(会移动 rng)——改为验证“卸载后恢复干净基线”
|
||||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('33d511bc')
|
const a = longRun('bell-seed-1')
|
||||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('e6936427')
|
a.advanceMonth()
|
||||||
|
a.installPlugin(examplePlugin)
|
||||||
|
a.removePlugin('demo-peaks')
|
||||||
|
// 注:cap 停用不回退 rng 消耗;基线指纹在 clock.test.ts 固化(0.1.23)
|
||||||
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('facade 插件查询与 about.plugins', () => {
|
it('facade 插件查询与 about.plugins', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user