v0.1.25: 乾坤一统——MOD 系统首版(1040 全绿,金钟罩不漂)

【P0 修复(0.1.24 遗留)】worldGen.npcs defs 落档 + normalize 幂等重注册——读档后
动态家族(n-g-*)npcById 永命中;MOD 卸载后存档世界仍完整(存档世界永稳)

【MOD 系统四层】
- M2 内容层:modSchema.ts(ModPack 全 JSON:manifest+data{events/npcs/worldgen/pills/forges};
  modParse 纯解析校验 id/name/version/事件模板合法性)
- M3 管理器:modToPlugin(MOD→CotycPlugin 一等子类——插件持久化/双闸/启停/回滚全复用)
- M1 传输层:main IPC mods:scan/read(userData/mods/*.json|*.cotymod)+ preload + env.d.ts
  + store refreshMods/installModByName/installModText
- M4 UI:SettingsPanel「MOD 层」卡(扫描目录/候选列表/安装);内置示例包通路

【世界/插件协同】
- WorldGenPools 聚合词库池(默认不变→基准世界不变;reset() 供隔离);MOD 于新档参与生成
- items.ts 配方聚合注册表(PILL/FORGE base+扩展);PluginContext addPillRecipe/addForgeRecipe/addWorldGenPool

【边界(明注)】灾因/世界数值表注入归平衡版;脚本型 MOD 不做;MOD 只影响新档世界(老档不变形)

【测试】1040 全绿(46 套件):worldGen 存档 1 例 + mod-system 5 例(解析/转换/模板/依赖/基准指纹);
金钟罩 0.1.24 基线未漂(世界生成默认池不变);build 通过
This commit is contained in:
2026-08-23 16:29:29 +08:00
parent ec25bf92fc
commit 5d9c333c76
18 changed files with 364 additions and 21 deletions
+33 -1
View File
@@ -10,6 +10,7 @@ export default function SettingsPanel() {
const bump = useGameStore((s) => s.bump)
const slot = useGameStore((s) => s.slot)
const saveNow = useGameStore((s) => s.saveNow)
const modsList = useGameStore((s) => s.modsList)
const exportSave = useGameStore((s) => s.exportSave)
const importSave = useGameStore((s) => s.importSave)
const refreshSlots = useGameStore((s) => s.refreshSlots)
@@ -32,6 +33,37 @@ export default function SettingsPanel() {
return (
<div className="settings-wrap">
{/* ===== MOD 层:外部内容包(userData/mods/*.cotymod|json===== */}
<div className="card settings-card">
<div className="card-title">MOD </div>
<div className="help-text">
MOD <code>userData/mods/</code>*.cotymod / *.json
///线
</div>
<div style={{ display: 'flex', gap: 6, marginBottom: 6, flexWrap: 'wrap' }}>
<button className="btn btn-sm" onClick={() => { void useGameStore.getState().refreshMods(); setNextToast('已扫描 mods 目录。') }}> </button>
<span className="dim2"> {modsList.length} </span>
</div>
{modsList.length > 0 && (
<div className="plugin-grid">
{modsList.map((name) => (
<div key={name} className="plugin-row">
<div className="plugin-id">{name}</div>
<div className="plugin-info">
<span className="dim2" style={{ fontSize: '0.8rem' }}></span>
</div>
<div className="plugin-actions">
<button
className="btn btn-sm"
onClick={() => { void useGameStore.getState().installModByName(name) }}
></button>
</div>
</div>
))}
</div>
)}
</div>
{/* ===== 插件层:万物皆是插件的实证窗口 ===== */}
<div className="card settings-card">
<div className="card-title">manifest </div>
@@ -224,7 +256,7 @@ export default function SettingsPanel() {
· <br />
·
</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.24 · Chronicle of the Immortal Clan</div>
<div className="dim2" style={{ marginTop: 8 }}> 0.1.25 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
+33
View File
@@ -67,6 +67,10 @@ export interface GameStore {
engine?: GameEngine
advancing: boolean
setNextToast: (msg: string) => void
modsList: string[]
refreshMods: () => Promise<void>
installModByName: (name: string) => Promise<void>
installModText: (text: string) => Promise<void>
act: (name: ActName, payload: import('../game/engine/runtime/ApiFacade').ActPayload) => boolean
}
@@ -94,6 +98,35 @@ export const useGameStore = create<GameStore>((set, get) => ({
engine: undefined as GameEngine | undefined,
advancing: false,
setNextToast: (msg: string) => setNextToast(msg),
modsList: [],
refreshMods: async () => {
const u = useGameStore.getState()
if (!window.api?.scanMods) return
const r = await window.api.scanMods()
set({ modsList: r.ok ? (r.files ?? []) : [] })
void u
},
installModByName: async (name) => {
if (!window.api?.readMod) return
const r = await window.api.readMod(name)
if (!r.ok || !r.text) { setNextToast(`读取 MOD 失败:${r.error ?? ''}`); return }
await useGameStore.getState().installModText(r.text)
},
installModText: async (text) => {
const { modParse } = await import('../game/engine/runtime/modSchema')
const parsed = modParse(text)
if (!parsed.ok) { setNextToast(`MOD 解析失败:${parsed.error}`); return }
const { modToPlugin } = await import('../game/engine/runtime/modManager')
const st = useGameStore.getState()
if (!st.world) { setNextToast('请先进入一局再装 MOD。'); return }
const res = st.world.installPlugin(modToPlugin(parsed.pack))
if (res.ok) {
setNextToast(`MOD「${parsed.pack.name}」已装——新开档的世界将受其影响。`)
set((s) => ({ revision: s.revision + 1 }))
} else {
setNextToast(`MOD 安装失败:${res.reason ?? ''}`)
}
},
screen: 'boot',
world: null,
slot: 1,