diff --git a/src/renderer/game/engine/kernel/plugin.ts b/src/renderer/game/engine/kernel/plugin.ts index 03b4a01..7650553 100644 --- a/src/renderer/game/engine/kernel/plugin.ts +++ b/src/renderer/game/engine/kernel/plugin.ts @@ -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 diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index 08e8b26..d6fd798 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -178,6 +178,8 @@ export class World { clock: GameClock systems: Record plugins: PluginManager + /** 0.1.34 世界观察回调(只读洞察) */ + simTickObservers: Array<(year: number, month: number, state: unknown) => void> = [] private eventPools = new Map() 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 数值 patch(mult 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 + 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)[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 diff --git a/src/renderer/game/engine/runtime/capabilities.ts b/src/renderer/game/engine/runtime/capabilities.ts index ef13405..8ef1475 100644 --- a/src/renderer/game/engine/runtime/capabilities.ts +++ b/src/renderer/game/engine/runtime/capabilities.ts @@ -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:暴击/受创减免/渡劫/秘境——万象归元)。' } ] diff --git a/src/renderer/game/engine/runtime/modManager.ts b/src/renderer/game/engine/runtime/modManager.ts index 1896c24..aa86bb9 100644 --- a/src/renderer/game/engine/runtime/modManager.ts +++ b/src/renderer/game/engine/runtime/modManager.ts @@ -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) diff --git a/src/renderer/game/engine/runtime/modSchema.ts b/src/renderer/game/engine/runtime/modSchema.ts index 6c5e969..13e0b6e 100644 --- a/src/renderer/game/engine/runtime/modSchema.ts +++ b/src/renderer/game/engine/runtime/modSchema.ts @@ -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 } } diff --git a/src/renderer/game/engine/runtime/pluginManager.ts b/src/renderer/game/engine/runtime/pluginManager.ts index 18c16f4..9dd7a6f 100644 --- a/src/renderer/game/engine/runtime/pluginManager.ts +++ b/src/renderer/game/engine/runtime/pluginManager.ts @@ -93,6 +93,13 @@ export class PluginManager { return unsub } } + if (prop === 'onWorldSimTick') { + return (fn: Parameters[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)