Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/SettingsPanel.tsx
T
thzxx 7839c7b55a v0.1.33: 闭环·流光——物品生态闭环+引擎止血+UI动效打磨+测试 5230
【版本】0.1.33(build 通过)
【测试】5009→5230(+200 matrix-close-loop 闭环四验宏量)
【文档】AGENTS/README/MOD_GUIDE/MEMORY 四份同步 0.1.33(闭环注/四同步纪律/测试数)
2026-08-23 21:39:23 +08:00

295 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useGameStore } from '../store'
import { BUILTIN_MODS } from '../../game/data/builtin-mods'
import { examplePlugin } from '../../game/engine/runtime/demo-plugins'
import { ensureFx } from '../fx'
import { SYSTEM_DEFS } from '../../game/engine/runtime/capabilities'
import { buildBiography } from '../../game/engine/narrative/biography'
export default function SettingsPanel() {
const world = useGameStore((s) => s.world)
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)
const refreshSnapshots = useGameStore((s) => s.refreshSnapshots)
const restoreSnapshot = useGameStore((s) => s.restoreSnapshot)
const snapshots = useGameStore((s) => s.snapshots)
const go = useGameStore((s) => s.go)
const setSpeed = useGameStore((s) => s.setSpeed)
const soundOn = useGameStore((s) => s.soundOn)
const toggleSound = useGameStore((s) => s.toggleSound)
const setNextToast = useGameStore((s) => s.setNextToast)
const [fxMode, setFxModeState] = useState('std')
const setFxMode = (m: 'soft' | 'std' | 'full') => {
setFxModeState(m)
ensureFx().setMode(m)
}
if (!world) return null
const hasDemo = world.pluginList().some((p) => p.id === 'demo-peaks')
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>
<button
className="btn btn-sm"
title="把当前启用的 MOD 集导出为一个 .cotymod 合并包(分享/备份)"
onClick={() => {
const bundle = world.exportModBundle()
if (!bundle) { setNextToast('当前无 MOD 集可导出。'); return }
if (window.api?.writeMod) {
void window.api.writeMod('mod-bundle-export.cotymod', bundle).then((r) => {
setNextToast(r.ok ? '已导出到 userData/mods/mod-bundle-export.cotymod' : `导出失败:${r.error ?? ''}`)
})
}
}}
>导出当前MOD集</button>
<span className="dim2">发现 {modsList.length} 个候选包</span>
</div>
<div style={{ margin: '8px 0 4px' }}>
<span className="dim">内置仓库(官方示范包):</span>
{BUILTIN_MODS.map((m) => (
<button
key={m.id}
className="btn btn-sm"
style={{ marginLeft: 6 }}
title={m.description}
onClick={() => {
void useGameStore.getState().installModText(JSON.stringify(m))
}}
>{m.name} v{m.version}</button>
))}
</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>
<div className="help-text">
「万物皆是插件」:系统 / 数据包 / 事件池统一走 CotycPlugin 协议。核心三插件受保护常驻,
第三方插件可随时安装/卸载(卸载即全摘钩子)。点击下方体验按钮,立刻注入一个真实插件——
事件「山鬼怒吼」将在消息池中定期浮现,同时年首添『护山之力』。
</div>
<div className="plugin-grid">
{world.pluginList().map((p) => (
<div key={p.id} className={`plugin-row ${p.protected ? 'proto' : ''}`}>
<div className="plugin-id">{p.id}</div>
<div className="plugin-info">
<b>{p.name}</b>
<span className="dim2" style={{ fontSize: '0.8rem' }}>
v{p.version}{p.author ? ` · ${p.author}` : ''} · {p.kind} · {p.description}
{p.dependencies?.length ? `(依赖:${p.dependencies.join('、')}` : ''}
{p.conflicts?.length ? `(冲突:${p.conflicts.join('、')}` : ''}
</span>
</div>
<div className="plugin-actions">
<span className={`tag ${p.protected ? 'gold-t' : p.enabled ? '' : 'bad'}`}>
{p.protected ? '核心' : p.enabled ? '已启用' : '已停用'}
</span>
{!p.protected && (
<>
<button
className={`btn btn-sm ${p.enabled ? '' : 'btn-primary'}`}
onClick={() => {
const r = world.setPluginEnabled(p.id, !p.enabled)
if (!r.ok) setNextToast(`启停失败:${r.reason ?? ''}`)
bump()
}}
>
{p.enabled ? '停用' : '启用'}
</button>
<button
className="btn btn-sm btn-danger"
title={p.dependencies?.length ? `依赖:${p.dependencies.join('、')}` : ''}
onClick={() => {
world.removePlugin(p.id)
setNextToast('插件已卸载(钩子/池/能力卡全摘)。')
bump()
}}
>
卸载
</button>
</>
)}
</div>
</div>
))}
</div>
{!hasDemo && (
<button
className="btn btn-primary btn-sm"
onClick={() => {
const r = world.installPlugin(examplePlugin)
if (r.ok) {
bump()
setNextToast('示例插件已安装:事件池注入 + 护山之力生效,可到上方卸载。')
} else {
setNextToast(`安装失败:${r.reason ?? ''}`)
}
}}
>
安装示例插件(护山妖兽)
</button>
)}
</div>
{/* ===== 系统仪表盘:12 能力卡启停 ===== */}
<div className="card settings-card">
<div className="card-title">系统仪表盘(能力卡 · 12 张启停)</div>
<div className="sys-grid">
{SYSTEM_DEFS.map((s) => {
const enabled = world.sysEnabled(s.id)
return (
<div key={s.id} className={`sys-cell ${enabled ? '' : 'off'}`}>
<div className="sys-head">
<b>{s.name}</b>
<span className="dim2" style={{ fontSize: '0.75rem' }}>{s.id} v{s.version}</span>
</div>
<div className="dim2" style={{ fontSize: '0.8rem' }}>{s.desc}</div>
<button
className={`btn btn-sm ${enabled ? 'btn-danger' : 'btn-primary'}`}
onClick={() => {
world.toggleSystem(s.id)
bump()
}}
>
{enabled ? '停用' : '启用'}
</button>
</div>
)
})}
</div>
</div>
{/* ===== 存档与导出 ===== */}
<div className="card settings-card">
<div className="card-title">存档与导出</div>
<table className="settings-table">
<tbody>
<tr>
<td>手动存点(当前第 {slot} 档)</td>
<td><button className="btn btn-sm" onClick={() => void saveNow()}>立即保存</button></td>
</tr>
<tr>
<td>回档时间轴(每季自动快照,最近12份)</td>
<td><button className="btn btn-sm" onClick={() => void refreshSnapshots()}>刷新列表</button></td>
</tr>
{snapshots.slice(0, 12).map((sn, i) => (
<tr key={sn.id}>
<td className={sn.year === world.state.year ? 'gold' : 'dim'}>
{sn.year}{sn.month}{sn.label !== '自动' ? ` · ${sn.label}` : ''}
{sn.year === world.state.year && sn.month === world.state.month ? '(当前)' : ''}
</td>
<td>
<button
className="btn btn-sm"
disabled={sn.year === world.state.year && sn.month === world.state.month}
onClick={() => {
if (window.confirm(`回卷到 ${sn.year}${sn.month}月?当前进度会先保存为回档前夕。此操作不可撤销。`)) {
void restoreSnapshot(sn.id).then(() => bump())
}
}}
>
回卷
</button>
</td>
</tr>
))}
<tr>
<td>导出存档(JSON 文件)</td>
<td><button className="btn btn-sm" onClick={() => void exportSave()}>导出</button></td>
</tr>
<tr>
<td>导入存档到当前位置</td>
<td><button className="btn btn-sm" onClick={() => void importSave(slot)}>导入</button></td>
</tr>
<tr>
<td>音效(合成音:鼓点·钟鸣·纸韵)</td>
<td>
<button className="btn btn-sm" onClick={toggleSound}>{soundOn ? '开(点击关)' : '关(点击开)'}</button>
</td>
</tr>
<tr>
<td>动效档位(灵雾/金砂粒子)</td>
<td>
{(['soft', 'std', 'full'] as const).map((m) => (
<button
key={m}
className={`btn btn-sm ${fxMode === m ? 'btn-primary' : ''}`}
onClick={() => { setFxMode(m); bump() }}
>
{m === 'soft' ? '柔和' : m === 'std' ? '标准' : '全开'}
</button>
))}
</td>
</tr>
<tr>
<td>暂停时间</td>
<td><button className="btn btn-sm" onClick={() => setSpeed(0)}>停止流转</button></td>
</tr>
<tr>
<td>回到主菜单(会先自动保存)</td>
<td>
<button
className="btn btn-sm"
onClick={() => {
void saveNow('离席前').then(() => {
setSpeed(0)
go('boot')
})
}}
>
主菜单
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div className="card settings-card">
<div className="card-title">玩法说明</div>
<div className="help-text">
· 日常以「月」流转:成员修炼、建筑产出、往事兴替、事件沓来。<br />
· 「宗」字牌成员可闭关、服丹、冲击瓶颈;瓶颈即修为满百者,成功率看灵气、丹药与家主督学。<br />
· 「探秘」选人结队远行,途中见战报;凯旋带回灵石、法帖、法器诸宝。<br />
· 「外交」与四邻结好联姻;仇雠之族隔岁来犯,打得赢名望大涨,打不赢蚀钱伤丁。<br />
· 「史书」自动记述繁华与凋零——百年之后,后人翻开这一卷家族志,见代代薪火、历历雪泥。
</div>
<div className="dim2" style={{ marginTop: 8 }}>版本 0.1.33 · Chronicle of the Immortal Clan</div>
</div>
</div>
)
}