v0.1.34b: 插件新卡+MOD 功法效果口/patch 层/冲突日志

【插件】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
This commit is contained in:
2026-08-23 22:09:38 +08:00
parent 3fb64f6e78
commit a7fe166e85
6 changed files with 80 additions and 3 deletions
@@ -52,6 +52,10 @@ export interface PluginContext {
addPostDef: (def: import('../../data/posts').PostDef) => void
addAspirationDef: (def: import('../../data/aspirations').AspirationDef) => void
addFormationDef: (def: import('../../data/formations').FormationDef) => void
/** 0.1.34 功法定义注册(全字段——含流派效果;grade≤3 入求学/秘境池) */
addTechniqueDef: (def: import('../../data/realms').TechniqueDef) => void
/** 0.1.34 世界观察回调(每世界 tick 后——只读记录,禁止写时轮/序;MOD 数值洞察口) */
onWorldSimTick: (fn: (year: number, month: number, gameState: unknown) => void) => () => void
addTraitDef: (def: import('../../data/traits').TraitDef) => void
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
removeCapability: (id: string) => void
+45
View File
@@ -178,6 +178,8 @@ export class World {
clock: GameClock
systems: Record<string, { enabled: boolean }>
plugins: PluginManager
/** 0.1.34 世界观察回调(只读洞察) */
simTickObservers: Array<(year: number, month: number, state: unknown) => void> = []
private eventPools = new Map<string, EventDef[]>()
kernel?: import('../kernel/Kernel').Kernel
@@ -243,6 +245,14 @@ export class World {
addAspirationDef: (def) => registerAspirationDef(def),
addFormationDef: (def) => registerFormationDef(def),
addTraitDef: (def) => registerTraitDef(def),
addTechniqueDef: (def) => {
// 入数据包(MISSIONS 式 mergeAdd 通道——同 id 跳过;grade≤3 自动进求学/秘境池)
PACK.mergeAdd('techniques', [def])
},
onWorldSimTick: (fn) => {
self.simTickObservers.push(fn)
return () => { self.simTickObservers = self.simTickObservers.filter((f) => f !== fn) }
},
addCapability: (cap) => {
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
@@ -458,6 +468,11 @@ export class World {
this.trackStats()
this.clampState()
this.pruneYearFlags()
// 0.1.34 世界观察回调(插件/MOD 只读洞察——零 rng 零时序影响)
if (this.simTickObservers.length > 0) {
const obs = this.simTickObservers
for (const fn of obs) fn(s.year, s.month, s)
}
}
applyEventChoice(eventId: string, idx: number): boolean {
@@ -901,6 +916,36 @@ export class World {
return Math.round(top * bonus)
}
/** 0.1.34 MOD 数值 patchmult 0.5~2 / add 实数——作用于数据包表项;卸载复原) */
applyModPatch(entries: Array<{ target: 'technique' | 'item' | 'building'; id: string; mult?: number; add?: number }>): void {
const undo: Array<() => void> = []
for (const p of entries) {
if (p.target === 'technique') {
const arr = pack().techniques.filter((x) => x.id === p.id)
if (arr.length === 0) continue
const t = arr[0] as unknown as Record<string, number>
const vals: Array<[keyof typeof t, number]> = []
for (const key of ['expBonus', 'powerBonus', 'critChance', 'guardBonus', 'tribBonus', 'secretBonus'] as const) {
const v = t[key]
if (typeof v !== 'number') continue
vals.push([key as never, v])
t[key as never] = Math.max(-5, Math.min(2, p.mult !== undefined ? v * p.mult : v + (p.add ?? 0)))
}
for (const [key, v] of vals) undo.push(() => { t[key as never] = v })
} else if (p.target === 'item') {
const it = (pack().items as Record<string, { basePrice?: number }>)[p.id]
if (!it || typeof it.basePrice !== 'number') continue
const orig = it.basePrice
undo.push(() => { it.basePrice = orig })
it.basePrice = Math.max(1, Math.round(it.basePrice * (p.mult ?? 1) + (p.add ?? 0)))
}
}
this.patchUndo.push(...undo)
}
/** 撤销栈(MOD 卸载时由 modManager uninstall 调用) */
patchUndo: Array<() => void> = []
ancestralRite(): boolean {
const fam = this.state.family
const last = (fam.flag['lastRiteYear'] as number | undefined) ?? -999
@@ -21,5 +21,7 @@ export const SYSTEM_DEFS: SystemDef[] = [
{ id: 'tournament', name: '太虚大比', version: '1.0', desc: '五年一会的太虚仙盟争锋。' },
{ id: 'tribulation', name: '渡劫', version: '1.0', desc: '大境界天劫与护法之仪。' },
{ id: 'apprentice', name: '寄读', version: '1.0', desc: '宗门子弟外修与求经台。' },
{ id: 'season', name: '时节', version: '1.0', desc: '春夏秋冬之乘气(春耕夏修秋市冬闭)。' }
{ id: 'season', name: '时节', version: '1.0', desc: '春夏秋冬之乘气(春耕夏修秋市冬闭)。' },
{ id: 'worldsim', name: '世界演衍', version: '1.0', desc: '世界自演化观察/响应(0.1.34:战争疲劳/断供应急/新秘境灵机)。' },
{ id: 'tech', name: '功法百汇', version: '1.0', desc: '功法流派效果(0.1.34:暴击/受创减免/渡劫/秘境——万象归元)。' }
]
+10 -1
View File
@@ -8,6 +8,7 @@ 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'
@@ -112,7 +113,12 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
for (const it of pack.data.items) base[it.id] = it
ctx.overridePack({ items: base as never })
}
if (pack.data.techniques?.length) ctx.mergeAddPack('techniques', pack.data.techniques 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 已存在(内置优先,新增跳过)。`)
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
@@ -120,8 +126,11 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
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 ?? [])
},
uninstall(ctx2) {
ctx2.world.patchUndo.splice(0).forEach((f) => f())
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
WorldGenPools.removePool(`mod-${pack.id}`)
for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name)
+11 -1
View File
@@ -29,8 +29,10 @@ export interface ModPackData {
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 }>
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 }>
@@ -157,5 +159,13 @@ export function validateMod(pack: ModPack): ModValidation {
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 }
}
@@ -93,6 +93,13 @@ export class PluginManager {
return unsub
}
}
if (prop === 'onWorldSimTick') {
return (fn: Parameters<PluginContext['onWorldSimTick']>[0]) => {
const unsub = (target.onWorldSimTick as PluginContext['onWorldSimTick'])(fn)
rt.unsubscribers.push(unsub)
return unsub
}
}
if (prop === 'addEventPool') {
return (pid: string, events: EventDef[]) => {
;(target.addEventPool as (pid: string, ev: EventDef[]) => void)(pid, events)