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' import { Component, type ReactNode, type ErrorInfo } from 'react' interface EBState { err: Error | null } export class AppErrorBoundary extends Component<{ children: ReactNode }, EBState> { state: EBState = { err: null } static getDerivedStateFromError(err: Error): EBState { return { err } } componentDidCatch(err: Error, info: ErrorInfo): void { console.error('[ui-crash]', err, info.componentStack) } render(): ReactNode { if (this.state.err) { return (

界面出错了(游戏数据未受影响)

{String(this.state.err?.message ?? this.state.err)}
) } return this.props.children } } 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 (
{screen === 'boot' && } {screen === 'newgame' && } {screen === 'game' && } {pendingEventId && } {battleView && }
) }