【插件】SYSTEM_DEFS + worldview 观察卡/tech 功法卡(14);PluginContext + addTechniqueDef(全字段入 PACK 数组, grade≤3 自动池) + onWorldSimTick 观察回调(零 rng 零时序——proxy 追踪注销); 【MOD】ModPackData.techniques 补 4 流派效果字段;新增 patch 层(target 数值 mult 0.5~2/add——install 应用 uninstall 复原,validate 规则进预检);同 id 功法冲突 warn 日志 【门面】Runtime 未动;卸载回滚栈 patchUndo
172 lines
8.6 KiB
TypeScript
172 lines
8.6 KiB
TypeScript
/**
|
||
* MOD 包 schema(0.1.25《乾坤一统》)
|
||
* MOD = 可分发的外部内容包(JSON 单文件/文件)——Manifest + 纯数据。
|
||
* MOD 经 ModManager 转换为 CotycPlugin 进标准插件管线(持久化/双闸/回滚免费获得)。
|
||
*/
|
||
import { EventDef } from '../../data/events'
|
||
import { NpcFamilyDef } from '../../data/npcs'
|
||
|
||
export type ModKind = 'content' | 'data' | 'rules'
|
||
|
||
export interface ModPackData {
|
||
/** 追加事件(需与既有 id 无冲突) */
|
||
events?: EventDef[]
|
||
/** 世界模板(worldgen 池候选——新档生成时参与) */
|
||
npcs?: NpcFamilyDef[]
|
||
/** 世界生成词库注入 */
|
||
worldgen?: {
|
||
styles?: string[]
|
||
regions?: string[]
|
||
fams?: string[]
|
||
suffixes?: string[]
|
||
}
|
||
/** 灾因扩展(0.1.26:进入世界灾签循环) */
|
||
calamities?: Array<{ name: string; brief?: string; family?: Record<string, number>; effect?: Record<string, number> }>
|
||
/** 世界数值白名单覆写(0.1.27 声明口径,0.1.28 接线;卸载复位) */
|
||
worldNum?: Record<string, number>
|
||
/** 丹方/铸器配方 */
|
||
pills?: Array<{ output: string; name: string; danfangLevel: number; stones: number; lingcao: number; beastcore: number; desc: string }>
|
||
forges?: Array<{ output: string; name: string; stones: number; lingkuang: number; beastcore: number; desc: string }>
|
||
/** 0.1.31 内容面总攻:物品/功法(PACK 合入)、建筑/职事/志向/阵型/性格(双源注册) */
|
||
items?: Array<{ id: string; name: string; kind: 'resource' | 'material' | 'pill' | 'talisman' | 'brew' | 'artifact'; basePrice: number; desc: string; icon: string }>
|
||
techniques?: Array<{ id: string; name: string; grade: number; element: string; path: string; expBonus: number; powerBonus: number; desc: string; critChance?: number; guardBonus?: number; tribBonus?: number; secretBonus?: number }>
|
||
buildings?: Array<{ id: string; name: string; icon: string; desc: string; kind: 'produce' | 'function'; maxLevel: number; produceExpr?: string; upgradeStones?: number; upgradeMineral?: number }>
|
||
/** 0.1.34 数值 patch 层(mult 0.5~2 / add 实数——仅新开档生效;老档世界形成为档记录) */
|
||
patch?: Array<{ target: 'technique' | 'item' | 'building'; id: string; mult?: number; add?: number }>
|
||
posts?: Array<{ id: string; name: string; effectType: string; value: number; max: number; desc: string }>
|
||
aspirations?: Array<{ id: string; name: string; desc: string; effectType: string; value: number }>
|
||
formations?: Array<{ id: string; name: string; icon: string; desc: string; atk: number; def: number; retreatWound: number }>
|
||
traits?: Array<{ id: string; name: string; desc: string; bonus?: Record<string, number> }>
|
||
}
|
||
|
||
export interface ModPack {
|
||
id: string
|
||
name: string
|
||
/** MOD 类型(0.1.31):content=内容包 / data=实体包 / rules=规则包 */
|
||
kind?: ModKind
|
||
version: string
|
||
gameVersion?: string
|
||
author?: string
|
||
description?: string
|
||
/** 模式范式版本(0.1.26 起建议声明 = 1) */
|
||
schema?: number
|
||
depends?: string[]
|
||
conflicts?: string[]
|
||
data: ModPackData
|
||
}
|
||
|
||
export type ModParseResult = { ok: true; pack: ModPack } | { ok: false; error: string }
|
||
|
||
const ID_RE = /^[a-z0-9][a-z0-9-_]{1,31}$/i
|
||
|
||
/** 纯解析(任何文本 → ModPack 或错误);校验层供 vitest 直测 */
|
||
export function modParse(text: string): ModParseResult {
|
||
let raw: unknown
|
||
try {
|
||
raw = JSON.parse(text)
|
||
} catch {
|
||
return { ok: false, error: 'MOD 文件不是合法 JSON。' }
|
||
}
|
||
// P0-7 bundle 分支:游戏内导出的合并包({app:'cotymod',bundle:[...]})→ 逐包递归(首包解析失败即报)
|
||
const asRec = raw as { app?: string; bundle?: unknown[] }
|
||
if (asRec?.app === 'cotymod' && Array.isArray(asRec.bundle)) {
|
||
if (asRec.bundle.length === 0) return { ok: false, error: 'bundle 为空。' }
|
||
for (const b of asRec.bundle) {
|
||
const r = modParse(JSON.stringify(b))
|
||
if (!r.ok) return { ok: false, error: `bundle 内包非法:${r.error}` }
|
||
}
|
||
return modParse(JSON.stringify(asRec.bundle[0]))
|
||
}
|
||
if (!raw || typeof raw !== 'object') return { ok: false, error: 'MOD 内容为空。' }
|
||
const p = raw as Record<string, unknown>
|
||
if (typeof p.id !== 'string' || !ID_RE.test(p.id)) return { ok: false, error: '缺失/非法的 MOD id。' }
|
||
if (typeof p.name !== 'string' || !p.name.trim()) return { ok: false, error: '缺失 MOD 名称。' }
|
||
if (typeof p.version !== 'string' || !p.version.trim()) return { ok: false, error: '缺失 MOD 版本号。' }
|
||
const data = (p.data ?? {}) as Record<string, unknown>
|
||
if (!data || typeof data !== 'object') return { ok: false, error: 'MOD data 段缺失。' }
|
||
if (Array.isArray(data.events)) {
|
||
for (const e of data.events) {
|
||
if (!e || typeof e !== 'object') return { ok: false, error: 'MOD 事件表含非法行。' }
|
||
const ev = e as Record<string, unknown>
|
||
if (typeof ev.id !== 'string' || typeof ev.name !== 'string' || !Array.isArray(ev.options)) {
|
||
return { ok: false, error: `MOD 事件 ${String(ev.id ?? '?')} 缺 id/name/options。` }
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(data.npcs)) {
|
||
for (const n of data.npcs) {
|
||
const nn = n as Record<string, unknown>
|
||
if (typeof nn.id !== 'string' || typeof nn.name !== 'string' || typeof nn.region !== 'string') {
|
||
return { ok: false, error: 'MOD 模板表含非法行。' }
|
||
}
|
||
}
|
||
}
|
||
return { ok: true, pack: raw as ModPack }
|
||
}
|
||
|
||
/** P1-O 全量解析:bundle → 全部包;单包 → [包];非法 → [] */
|
||
export function modParseAll(text: string): ModPack[] {
|
||
let raw: unknown
|
||
try { raw = JSON.parse(text) } catch { return [] }
|
||
const asRec = raw as { app?: string; bundle?: unknown[] }
|
||
if (asRec?.app === 'cotymod' && Array.isArray(asRec.bundle)) {
|
||
const out: ModPack[] = []
|
||
for (const b of asRec.bundle) {
|
||
const r = modParse(JSON.stringify(b))
|
||
if (r.ok) out.push(r.pack)
|
||
}
|
||
return out
|
||
}
|
||
const r = modParse(text)
|
||
return r.ok ? [r.pack] : []
|
||
}
|
||
|
||
export const MOD_SCHEMA_VERSION = 1
|
||
|
||
export interface ModValidation {
|
||
ok: boolean
|
||
warnings: string[]
|
||
}
|
||
|
||
const CATEGORIES = ['daily', 'major', 'fate'] as const
|
||
const REALMS = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const
|
||
|
||
/** 范式校验(0.1.26 规范):深度字段检查——event 形状/模板境界/配方非负 */
|
||
export function validateMod(pack: ModPack): ModValidation {
|
||
const warnings: string[] = []
|
||
if (pack.schema !== undefined && pack.schema !== MOD_SCHEMA_VERSION) {
|
||
warnings.push(`schema 版本 ${pack.schema} ≠ 当前支持 ${MOD_SCHEMA_VERSION}(提示 MOD 作者更新)。`)
|
||
}
|
||
if (!pack.author) warnings.push('缺 author(建议填,供玩家辨识)。')
|
||
for (const e of pack.data.events ?? []) {
|
||
if (!CATEGORIES.includes(e.category as never)) warnings.push(`事件 ${e.id} category 非法(daily|major|fate)。`)
|
||
if (!(e.weight >= 0 && e.weight <= 100)) warnings.push(`事件 ${e.id} weight 超界(0~100)。`)
|
||
if (!e.text || !e.text.trim()) warnings.push(`事件 ${e.id} 缺正文。`)
|
||
if (!e.options || e.options.length === 0) warnings.push(`事件 ${e.id} 缺选项。`)
|
||
for (const o of e.options ?? []) {
|
||
if (!o.label) warnings.push(`事件 ${e.id} 某选项缺 label。`)
|
||
if (!o.eff || typeof o.eff !== 'object') warnings.push(`事件 ${e.id} 某选项缺 eff(可空对象)。`)
|
||
}
|
||
}
|
||
for (const n of pack.data.npcs ?? []) {
|
||
if (!REALMS.includes(n.leaderRealm)) warnings.push(`模板 ${n.id} leaderRealm 非法(${REALMS.join('|')})。`)
|
||
if (!(n.initialPower >= 60 && n.initialPower <= 400)) warnings.push(`模板 ${n.id} initialPower 建议 60~400。`)
|
||
}
|
||
for (const r of pack.data.pills ?? []) {
|
||
if ([r.stones, r.lingcao, r.beastcore].some((v) => v < 0)) warnings.push(`丹方 ${r.output} 含负成本。`)
|
||
if (!(r.danfangLevel >= 1 && r.danfangLevel <= 5)) warnings.push(`丹方 ${r.output} danfangLevel 建议 1~5。`)
|
||
}
|
||
for (const r of pack.data.forges ?? []) {
|
||
if ([r.stones, r.lingkuang, r.beastcore].some((v) => v < 0)) warnings.push(`配方 ${r.output} 含负成本。`)
|
||
}
|
||
for (const t of pack.data.techniques ?? []) {
|
||
if (!t.grade || t.grade < 1 || t.grade > 4) warnings.push(`功法 ${t.id} grade 建议 1~4。`)
|
||
if (Math.abs(t.expBonus) > 0.5 || Math.abs(t.powerBonus) > 1) warnings.push(`功法 ${t.id} 数值强度提醒(expBonus≤0.5/powerBonus≤1)。`)
|
||
}
|
||
for (const p of pack.data.patch ?? []) {
|
||
if (!(p.mult === undefined || (p.mult >= 0.5 && p.mult <= 2))) warnings.push(`patch ${p.target}:${p.id} mult 限 0.5~2。`)
|
||
if (p.mult !== undefined && p.add !== undefined) warnings.push(`patch ${p.target}:${p.id} mult/add 二选一(勿同给)。`)
|
||
}
|
||
return { ok: warnings.length === 0, warnings }
|
||
}
|