【素材生成(零外部资源)】 - 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 通过
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { useGameStore } from './ui/store'
|
|
import Boot from './ui/screens/Boot'
|
|
import NewGame from './ui/screens/NewGame'
|
|
import GameScreen from './ui/screens/GameScreen'
|
|
import { EventModal } from './ui/components/EventModal'
|
|
import { BattleModal } from './ui/components/BattleModal'
|
|
import { PaperModal } from './ui/components/PaperModal'
|
|
import { sClick } from './ui/sound'
|
|
import { ensureFx, fxVisibilityAutopause, applySeason } from './ui/fx'
|
|
import { startFxLayer, spawnBurst, stopFxLayer } from './ui/fx-canvas'
|
|
import { DriftLayer } from './ui/components/DriftLayer'
|
|
import { seasonOf } from './game/data/season'
|
|
|
|
export default function App() {
|
|
const screen = useGameStore((s) => s.screen)
|
|
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
|
const battleView = useGameStore((s) => s.battleView)
|
|
|
|
useEffect(() => {
|
|
useGameStore.getState().init()
|
|
const clicker = () => sClick()
|
|
document.addEventListener('click', clicker, true)
|
|
const fx = ensureFx()
|
|
const stopVis = fxVisibilityAutopause()
|
|
const burst = (e: Event) => {
|
|
const kind = e.type.replace('fx:', '')
|
|
if (kind === 'spark' || kind === 'blade') spawnBurst('spark', 'center')
|
|
if (kind === 'ripple') spawnBurst('ripple')
|
|
if (kind === 'pulse') spawnBurst('ripple')
|
|
}
|
|
for (const k of ['fx:spark', 'fx:blade', 'fx:ripple', 'fx:pulse']) window.addEventListener(k, burst)
|
|
const seasonInit = () => applySeason(seasonOf(useGameStore.getState().world?.state.month ?? 1))
|
|
seasonInit()
|
|
startFxLayer(fx.mode === 'soft' ? 18 : fx.mode === 'full' ? 70 : 36)
|
|
return () => {
|
|
document.removeEventListener('click', clicker, true)
|
|
stopVis()
|
|
for (const k of ['fx:spark', 'fx:blade', 'fx:ripple', 'fx:pulse']) window.removeEventListener(k, burst)
|
|
stopFxLayer()
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<div className="app">
|
|
{screen === 'boot' && <Boot />}
|
|
{screen === 'newgame' && <NewGame />}
|
|
{screen === 'game' && <GameScreen />}
|
|
{pendingEventId && <EventModal />}
|
|
{battleView && <BattleModal />}
|
|
<PaperModal />
|
|
<DriftLayer />
|
|
</div>
|
|
)
|
|
}
|