import { create } from 'zustand' import { World, WorldEventBus } from '../game/engine/world' import { EventDef } from '../game/data/events' import { findEvent, applyEventChoice } from '../game/engine/systems/events' import { BattleLog, ChronicleEntry, GameState, LogItem, SaveMeta } from '../game/types/domain' import { getSlotManager, getSaveSlot } from '../game/storage/db' import { metaFromState, updateSlotMeta } from './storeHelper' export type Screen = 'boot' | 'newgame' | 'game' export type PanelId = 'family' | 'territory' | 'market' | 'diplomacy' | 'expedition' | 'chronicle' | 'settings' export interface GameStore { screen: Screen world: World | null slot: number slots: (SaveMeta | null)[] speed: number timer: ReturnType | null logFeed: LogItem[] pendingEventId?: string pendingEventDef?: EventDef battleView?: BattleLog panel: PanelId selectedMemberId?: string selectedMissionDef?: string revision: number toast?: string gameOverReason?: string init: () => Promise go: (screen: Screen) => void startNewGame: (opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }, slot: number) => Promise continueGame: (slot: number) => Promise loadSnapshot: (slot: number, snapshotId: string) => Promise advance: () => Promise addLog: (item: LogItem) => void onChronicle: (e: ChronicleEntry, important: boolean) => void onBattle: (log: BattleLog) => void onPendingEvent: (id: string) => void onGameOver: (reason: string) => void saveNow: (label?: string) => Promise bump: () => void setSpeed: (v: number) => void setPanel: (p: PanelId) => void setSelected: (id?: string) => void setMissionDef: (id?: string) => void exportSave: () => Promise importSave: (slot: number) => Promise deleteSave: (slot: number) => Promise refreshSlots: () => Promise } const LOG_CAP = 260 let logSeq = 1 export const useGameStore = create((set, get) => ({ screen: 'boot', world: null, slot: 1, slots: [], speed: 0, timer: null, logFeed: [], pendingEventId: undefined, pendingEventDef: undefined, battleView: undefined, panel: 'family', selectedMemberId: undefined, selectedMissionDef: undefined, revision: 0, toast: undefined, gameOverReason: undefined, init: async () => { const manager = getSlotManager() const slots = await manager.listSlotMetas() set({ slots }) if (typeof window !== 'undefined') { ;(window as unknown as Record).__cotycBootReady = true const hack = window as unknown as Record hack.__cotycDebug = { startNewGame: () => get().startNewGame({ seed: 'smoke-' + Date.now(), surname: '林', familyName: '林氏', motto: 'm', difficulty: 'normal' }, 1), advance: () => get().advance(), resolvePending: () => { const st = get() if (st.pendingEventId && st.world) { applyEventChoice(st.world, st.pendingEventId, 0) useGameStore.setState({ pendingEventId: undefined, pendingEventDef: undefined }) st.bump() } }, getYear: () => get().world?.state.year ?? 0, openPanel: (p: string) => { useGameStore.setState({ panel: p as never }) }, closeBattles: () => { useGameStore.setState({ battleView: undefined }) }, getChronicle: () => get().world?.state.chronicle?.slice().reverse() ?? [], getFeed: () => get().logFeed.slice() } } }, go: (screen) => set({ screen }), startNewGame: async (opts, slot) => { const world = World.create(opts) set({ world, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0 }) const st = get() st.world?.out.push(makeBus(st)) await st.saveNow('开局') }, continueGame: async (slot) => { const manager = getSlotManager() const file = await getSaveSlot(slot) await file.open() const state = await file.loadState() if (!state) throw new Error('找不到存档数据') openState(state, slot) await manager.updateSlotMeta(slot, metaFromState(state, slot)) }, loadSnapshot: async (slot, snapshotId) => { const file = await getSaveSlot(slot) await file.open() const state = await file.loadState(snapshotId) if (!state) throw new Error('找不到快照') openState(state, slot) }, advance: async () => { const st = get() const w = st.world if (!w || st.pendingEventId || w.state.gameOver) return try { w.advanceMonth() w.syncRng() set((s) => ({ revision: s.revision + 1 })) await Stash.save(st, '自动') } catch (e) { console.error(e) } }, addLog: (item) => { set((s) => { const next = (s.logFeed.length >= LOG_CAP ? s.logFeed.slice(s.logFeed.length - LOG_CAP + 1) : s.logFeed).concat(item) return { logFeed: next } }) }, onChronicle: (e, important) => { if (important) { set((s) => ({ revision: s.revision + 1 })) } }, onBattle: (log) => set({ battleView: log, revision: get().revision + 1 }), onPendingEvent: (id) => { const def = findEvent(id) set({ pendingEventId: id, pendingEventDef: def }) }, onGameOver: (reason) => set({ gameOverReason: reason, speed: 0, revision: get().revision + 1 }), saveNow: async (label = '手动') => { await Stash.save(get(), label) }, bump: () => set((s) => ({ revision: s.revision + 1 })), setSpeed: (v) => { const st = get() if (st.timer) clearInterval(st.timer) if (v <= 0) { set({ speed: v, timer: null }) return } const ms = v === 1 ? 3200 : v === 2 ? 1800 : 900 if (v >= 3) v = 3 const timer = setInterval(() => { const cur = get() if (!cur.pendingEventId && cur.world && !cur.world.state.gameOver) { void cur.advance() } }, ms) set({ speed: v, timer }) }, setPanel: (p) => set({ panel: p }), setSelected: (id) => set({ selectedMemberId: id }), setMissionDef: (id) => set({ selectedMissionDef: id }), exportSave: async () => { const st = get() if (!st.world || !window.api) return const file = await getSaveSlot(st.slot) const meta0 = metaFromState(st.world.state, st.slot) st.world.syncRng() await file.saveState(st.world.state, '导出前') const json = await file.exportAll() const meta = metaFromState(st.world.state, st.slot) const r = await window.api.exportSave(json, `${meta.name}-年${meta.year}年`) if (r.ok) set({ toast: '存档已导出。' }) else set({ toast: `导出失败:${r.error ?? ''}` }) }, importSave: async (slot) => { if (!window.api) return const r = await window.api.importSave() if (!r.ok || !r.text) { set({ toast: `导入失败:${r.error ?? ''}` }) return } const file = await getSaveSlot(slot) await file.open() const ok = await file.importAll(r.text) if (ok) { set({ toast: '导入成功,重新读取存档槽。' }) await get().refreshSlots() } else { set({ toast: '导入失败:文件格式不正确。' }) } }, deleteSave: async (slot) => { const file = await getSaveSlot(slot) await file.open() await file.wipe() const manager = getSlotManager() await manager.removeSlotMeta(slot) await get().refreshSlots() }, refreshSlots: async () => { const manager = getSlotManager() const slots = await manager.listSlotMetas() set({ slots }) } })) function openState(state: GameState, slot: number): void { const world = new World(state) const st = useGameStore.getState() world.out.push(makeBus(st)) useGameStore.setState({ world, slot, screen: 'game', panel: 'family', logFeed: [], battleView: undefined, pendingEventId: undefined, pendingEventDef: undefined, revision: 1, speed: 0, gameOverReason: state.gameOver?.reason }) } function makeBus(st: GameStore): WorldEventBus { return { onLog: (kind, text) => { const w = st.world st.addLog({ id: logSeq++, kind, text, year: w?.state.year ?? 0, month: w?.state.month ?? 0 }) }, onChronicle: (e, important) => { st.addLog({ id: logSeq++, kind: 'chronicle', text: `${e.year}年${e.month}月 ${e.text}`, year: e.year, month: e.month }) if (important) st.onChronicle(e, important) }, onBattle: (log) => st.onBattle(log), onPendingEvent: (id) => st.onPendingEvent(id), onGameOver: (reason) => st.onGameOver(reason) } } const Stash = { async save(st: GameStore, label: string): Promise { if (!st.world) return const file = await getSaveSlot(st.slot) await file.open() st.world.syncRng() await file.saveState(st.world.state, label) await file.saveChronicle(st.world.state.chronicle) const meta = metaFromState(st.world.state, st.slot) await file.setMeta(meta) await updateSlotMeta(st.slot, meta) } }