/** * 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 { return { ...DEFAULT_ITEMS } } function toBuildingDef(b: { id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string | Record; 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): (level: number) => Record { const parsed = typeof expr === 'string' ? expr.trim().startsWith('{') ? JSON.parse(expr) : { lingcao: expr } : expr const entries = Object.entries(parsed as Record) return (L: number) => { const out: Record = {} 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-M4(0.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 }).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 }