Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/runtime/modManager.ts
T
thzxx b420fa0c67 v0.1.35a: 缺陷歼灭——wonder 崩链/boss 卡队/patch 泄漏/拒单门/衰减/数值平衡 17 条
【高】wonder 全链修复(sendMission/settle/missionById wonders 回退+ExpeditionPanel 合并列表);
boss 战败 releaseSquad(整队不再永困 expedition);patchUndo 按插件归属 Map+rollbackPatch(跨 MOD/跨世界不再泄漏)
【中】buyItem 拒单线>池底(0.35>0.25——断供真断供);warFatigue 年首×0.9 衰减(长局不永久冷降);
MOD 功法注入 trackedTech 卸载摘除;patch 缺失键 warn;guard>1 钳制 Math.max(0,1-guard);
transition 12 处逗号简写→每属性独立时长(0.1.33 动画实际恢复);t-shanling/t-zidian 越级下调;土系恒应(×1.02)
【低】断供计数封顶12/播报 month 修正/era 权重下界 0.05/新贵 id 碰撞 7 位/新字段 normalize 补/
go() 停时器/saveNow 并发锁
【金钟罩】0.1.35 基线重算(warFatigue衰减/池底分离/功法平衡受控变更) 61 例绿
2026-08-23 22:30:02 +08:00

151 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MOD 管理器:ModPack → CotycPlugin 构造(MOD=插件一等子类)。
* 全量复用 PluginManager 管线:持久化/启停/双闸/自动回滚。
*/
import { CotycPlugin } from '../kernel/plugin'
import { ModPack, validateMod } from './modSchema'
import { EventDef } from '../../data/events'
import { findEvent } from './Systems/events'
import { World } from './World'
import { WorldGenPools } from '../sim/worldgen'
import { pack as dataPack } from '../../data/registry'
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
import { registerBuildingDef } from '../../data/buildings'
import { registerPostDef } from '../../data/posts'
import { registerAspirationDef } from '../../data/aspirations'
import { registerFormationDef } from '../../data/formations'
import { registerTraitDef } from '../../data/traits'
const DEFAULT_ITEMS = { lingcao: { id: 'lingcao', name: '灵草', kind: 'resource', basePrice: 8, desc: '最常见的修炼辅材,亦是炼丹基础。', icon: '草' },
lingkuang: { id: 'lingkuang', name: '灵矿', kind: 'resource', basePrice: 12, desc: '铸器与布阵常用矿物。', icon: '矿' },
beastcore: { id: 'beastcore', name: '兽核', kind: 'resource', basePrice: 30, desc: '妖兽一身的精华,可炼丹炼器。', icon: '核' },
'pill-qiyuan': { id: 'pill-qiyuan', name: '聚气丹', kind: 'pill', basePrice: 45, desc: '炼气期修士服之,一月修为大增。', icon: '气' },
'pill-ningyuan': { id: 'pill-ningyuan', name: '凝元丹', kind: 'pill', basePrice: 150, desc: '筑基以上可效,修为增长显著。', icon: '元' },
'pill-pojing': { id: 'pill-pojing', name: '破境丹', kind: 'pill', basePrice: 480, desc: '冲击瓶颈时的辅助神物,提升突破成功率。', icon: '破' },
'weapon-fan': { id: 'weapon-fan', name: '凡器', kind: 'artifact', basePrice: 120, desc: '普通铁器,聊胜于无。', icon: '凡' },
'weapon-qi': { id: 'weapon-qi', name: '法器', kind: 'artifact', basePrice: 450, desc: '蕴灵之器,可引动灵力。', icon: '法' },
'weapon-ling': { id: 'weapon-ling', name: '灵器', kind: 'artifact', basePrice: 1400, desc: '有灵之器,锋芒隐露。', icon: '灵' },
'weapon-fa': { id: 'weapon-fa', name: '法宝', kind: 'artifact', basePrice: 3600, desc: '罕世法宝,非金丹不能驾驭。', icon: '宝' } }
function itemsBasePack(): Record<string, unknown> {
return { ...DEFAULT_ITEMS }
}
function toBuildingDef(b: { id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string | Record<string, string>; upgradeStones?: number; upgradeMineral?: number }) {
return {
id: b.id,
name: b.name,
icon: b.icon,
desc: b.desc,
kind: b.kind,
maxLevel: Math.min(6, Math.max(1, b.maxLevel ?? 3)),
produceTable: b.produceExpr ? booleanExprTable(b.produceExpr) : undefined,
upgradeCost: (L: number) => ({ stones: (b.upgradeStones ?? 80) * Math.pow(1.6, L - 1), lingkuang: Math.round((b.upgradeMineral ?? 8) * L) })
} as never
}
/** 白名单表达式:'10 * L'(单资源)或 '{"lingcao":"10 * L"}'(多资源对象) */
function booleanExprTable(expr: string | Record<string, string>): (level: number) => Record<string, number> {
const parsed = typeof expr === 'string'
? expr.trim().startsWith('{') ? JSON.parse(expr) : { lingcao: expr }
: expr
const entries = Object.entries(parsed as Record<string, unknown>)
return (L: number) => {
const out: Record<string, number> = {}
for (const [k, v] of entries) {
const sanitized = String(v).replace(/L/g, String(L))
if (!/^[0-9.\s+\-*/()^]+$/.test(sanitized)) continue
try {
const val = Function(`"use strict";return (${sanitized.replace(/\^/g, '**')})`)() as number
if (Number.isFinite(val)) out[k] = Math.round(val)
} catch { /* 非法表达式跳过 */ }
}
return out
}
}
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
export function modPreflight(w: World, pack: ModPack): { ok: boolean; reason?: string; warnings: string[] } {
const v = validateMod(pack)
// 权威冲突检测:findEvent(池+动态分支)+ 动态前缀黑名单(防御构造缺口)
for (const e of pack.data.events ?? []) {
if (findEvent(e.id, w) || DYNAMIC_PREFIXES.some((p) => e.id.startsWith(p))) {
return { ok: false, reason: `事件 id 冲突:${e.id}`, warnings: v.warnings }
}
}
return { ok: true, warnings: v.warnings }
}
export function modToPlugin(pack: ModPack): CotycPlugin {
const plugin: CotycPlugin = {
id: `mod-${pack.id}`,
name: `MOD·${pack.name}`,
version: pack.version,
author: pack.author ?? 'mod',
description: pack.description ?? '',
kind: 'content',
dependencies: (pack.depends ?? []).map((d) => `mod-${d}`),
conflicts: (pack.conflicts ?? []).map((c) => `mod-${c}`),
install(ctx) {
// 事件池(与核心事件同池聚合;findEvent 查池)
if (pack.data.events?.length) ctx.addEventPool(`mod-${pack.id}`, pack.data.events)
// 世界模板(worldgen 池候选——新档参与生成)
for (const def of pack.data.npcs ?? []) ctx.addNpcTemplate(def)
// 词库注入(styles/regions/fams/suffixes;带 source 便于精确回滚)
if (pack.data.worldgen) ctx.addWorldGenPool(pack.data.worldgen, `mod-${pack.id}`)
// 配方补充
for (const r of pack.data.pills ?? []) ctx.addPillRecipe(r)
for (const r of pack.data.forges ?? []) ctx.addForgeRecipe(r)
// 灾因补充(0.1.26:MOD 灾因进入世界循环)
for (const c of pack.data.calamities ?? []) {
ctx.addCalamity(c.name, { brief: c.brief, family: c.family, effect: c.effect })
}
// P1-11 worldNum 白名单覆写(0.1.28 接线:仅 WORLDSIM 内键生效,卸载复位)
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
overrideWorldNum(k, v)
}
// M1-M40.1.31)内容面总攻:物品/功法进 Pack(键级合入);建筑/职事/志向/阵型/性格走双源注册
if (pack.data.items?.length) {
const base = itemsBasePack()
for (const it of pack.data.items) base[it.id] = it
ctx.overridePack({ items: base as never })
}
if (pack.data.techniques?.length) {
// 语义冲突日志:同 id 功法(mergeAdd 跳过内置——提示玩家谁改了什么)
const seenT = new Set((dataPack().techniques as Array<{ id: string }>).map((x) => x.id))
for (const t of pack.data.techniques) if (seenT.has(t.id)) ctx.world.log('info', `[MOD:${pack.id}] 功法 ${t.id} 同 id 已存在(内置优先,新增跳过)。`)
// 0.1.35 A-6:注入即追踪(与插件绑拆——卸载按 id 摘除注入行)
const injected = pack.data.techniques.filter((t) => !seenT.has(t.id) && !(pack as unknown as { trackedTech?: Array<string> }).trackedTech?.includes(t.id))
;(pack as unknown as { trackedTech: string[] }).trackedTech = [...((pack as unknown as { trackedTech?: string[] }).trackedTech ?? []), ...injected.map((t) => t.id)]
ctx.mergeAddPack('techniques', pack.data.techniques as never)
}
for (const b of pack.data.buildings ?? []) ctx.addBuildingDef(toBuildingDef(b))
for (const po of pack.data.posts ?? []) ctx.addPostDef({
id: po.id, name: po.id, effect: { type: po.effectType, value: po.value }, max: po.max, desc: po.desc
} as never)
for (const a of pack.data.aspirations ?? []) ctx.addAspirationDef(a as never)
for (const f of pack.data.formations ?? []) ctx.addFormationDef({ ...f, retreatWound: f.retreatWound ?? 1 } as never)
for (const t of pack.data.traits ?? []) ctx.addTraitDef({ ...t, danger: t.bonus?.danger ?? 0 } as never)
// 0.1.34 MOD 数值 patch 层(仅新档生效;卸载复原——mult 0.5~2 / add 实数)
ctx.world.applyModPatch(pack.data.patch ?? [], `mod-${pack.id}`)
},
uninstall(ctx2) {
ctx2.world.rollbackPatch(`mod-${pack.id}`)
const td = (pack as unknown as { trackedTech?: string[] }).trackedTech ?? []
if (td.length) ctx2.world.removeInjectedTechniques(td)
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
WorldGenPools.removePool(`mod-${pack.id}`)
for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name)
for (const r of pack.data.pills ?? []) removePillRecipe(r.output)
for (const r of pack.data.forges ?? []) removeForgeRecipe(r.output)
if (pack.data.worldNum) for (const k of Object.keys(pack.data.worldNum)) resetWorldNum(k)
},
// M1:挂 pack 源——导出/显示用(插件实例可溯源到原始 MOD 包)
source: pack
}
return plugin
}