【素材生成(零外部资源)】 - scripts/gen-art.mjs 程序化产出 5 枚 SVG 资产:金碧山水全景(山脊/金乌/雁阵/水纹)、 祥云纹带、回纹角饰、仙鹤、星辰层 → src/renderer/assets/gen/,npm run gen:art 可重跑 【Canvas 动效系统(单 RAF)】 - ui/fx-canvas.ts:灵雾粒子流(常驻随季色)/金砂喷发(spark)/刀光(blade)/涟漪(ripple), 单实例防重入、防膨胀 cap、GPU 友好、visibility 自停 - ui/fx.ts + core/fxqueue.ts:FxGate 统一发射闸——档位过滤(柔/标/全)、 reduced-motion 降级、单帧批处理防竞态、deltas 纯函数(漂字差分) 【舞美】 - 金碧背景层 bg-scene(全页面,随季节 hue-rotate 色温渐变) - 顶栏时辰司:季节印(春·熏风/夏·蝉鸣/秋·霜叶/冬·雪静)+年月进度环(SVG 弧段)+节气名 - 成员卡:闭关 aura 三点浮动 + 修为流光 shimmer - 消息流:good 事件(突破/获宝/落槌)→金砂喷发,战事→刀光扫过战报 - Boot 卷轴展开过渡;drift 漂字层(独立节点队列不覆盖,8 条裁剪) - 设置页动效三档(柔和/标准/全开) 【竞态规避】 - RAF 单循环 guard、particles cap 裁剪、FxGate draining 互斥、漂字节点附加不覆盖、 visibility 自动降级、advance in-flight 闸沿用 【测试】937→967(timesense 矩阵 15 + FxGate 竞态/档位/deltas 15) 全量 35 套件通过;金钟罩零漂移(纯 UI/素材层);typecheck/build 通过
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import { useGameStore } from '../store'
|
|
import { SaveMeta } from '../../game/types/domain'
|
|
import mountainUrl from '../../assets/mountain.svg'
|
|
|
|
function slotLabel(meta: SaveMeta | null): string {
|
|
if (!meta) return '虚位以待'
|
|
return `${meta.name} · ${meta.year}年${meta.month}月 · ${meta.members}人 · 声望${meta.reputation}`
|
|
}
|
|
|
|
export default function Boot() {
|
|
const slots = useGameStore((s) => s.slots)
|
|
const go = useGameStore((s) => s.go)
|
|
const continueGame = useGameStore((s) => s.continueGame)
|
|
const deleteSave = useGameStore((s) => s.deleteSave)
|
|
const toast = useGameStore((s) => s.toast)
|
|
|
|
return (
|
|
<div className="boot boot-frame">
|
|
<div className="boot-bg">
|
|
<img src={mountainUrl} alt="" />
|
|
</div>
|
|
<div className="boot-inner">
|
|
<div className="boot-title">仙途家族志</div>
|
|
<div className="boot-tagline">一 族 兴 衰 · 千 秋 烟 云</div>
|
|
<div className="boot-divider" />
|
|
<div className="boot-en">CHRONICLE OF THE IMMORTAL CLAN</div>
|
|
<div className="boot-actions" style={{ marginTop: 26 }}>
|
|
<button className="boot-newgame" onClick={() => go('newgame')}>
|
|
开 创 家 族
|
|
</button>
|
|
{slots.map((meta, i) => (
|
|
<div key={i} className="boot-slot">
|
|
{meta && <div className="slot-no">{i + 1}</div>}
|
|
{meta ? (
|
|
<>
|
|
<div style={{ flex: 1, cursor: 'pointer' }} onClick={() => void continueGame(i + 1)}>
|
|
<div className="dim2" style={{ fontSize: '0.72rem', color: '#6d5d40' }}>第{i + 1}档</div>
|
|
<div style={{ color: '#dcc898', letterSpacing: '1px' }}>{slotLabel(meta)}</div>
|
|
</div>
|
|
<button
|
|
className="btn btn-sm btn-danger"
|
|
title="删除本档"
|
|
onClick={() => {
|
|
if (window.confirm(`确定删除第 ${i + 1} 档存档?此操作不可撤销。`)) {
|
|
void deleteSave(i + 1)
|
|
}
|
|
}}
|
|
>
|
|
删
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className="boot-slot-empty" style={{ flex: 1 }} onClick={() => go('newgame')}>
|
|
<span className="slot-no">{i + 1}</span>
|
|
<span style={{ color: '#6d5d40' }}>虚位以待</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 18 }}>
|
|
<span className="seal" style={{ fontSize: '0.62rem' }}>癸未年</span>
|
|
<span style={{ color: '#6d5d40', fontSize: '0.78rem', letterSpacing: '2px' }}>
|
|
燃灯续香火 · 卷中写春秋
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{toast && <div className="toast">{toast}</div>}
|
|
</div>
|
|
)
|
|
}
|