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:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user