【高】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 例绿
151 lines
8.9 KiB
TypeScript
151 lines
8.9 KiB
TypeScript
/**
|
||
* 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-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<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
|
||
}
|