"use client"; // 回响星核 / Echo Nexus — Zustand 游戏状态管理 import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import type { GameState, Crystal, CrystalTier, DecodePuzzle, } from "@/lib/game/types"; import { INITIAL_STATE, TECH_TREE, CRYSTAL_VALUE, CONTACT, CRYSTAL_SPAWN, FRAGMENTS, PRESTIGE, } from "@/lib/game/config"; import { createInitialState, recomputeStats, decodeRewards, rollCrystalTier, computeNewBlueprints, performPrestige, } from "@/lib/game/engine"; import { generatePuzzle, tryClickNode, isSolvable, resetPuzzle as resetPuz, } from "@/lib/game/decode"; interface GameActions { // 生命周期 init: () => void; loadOnline: () => void; hardReset: () => void; // 主循环 tick: (now: number) => void; pulse: () => { gain: number; combo: number } | null; // 解码 startDecode: (crystalId: string) => void; clickNode: (nodeId: number) => { ok: boolean; finished: boolean; solvable?: boolean; failReason?: string }; undoStep: () => void; retryPuzzle: () => void; abandonPuzzle: () => void; /** 自动解码 T1(技术解锁后由 tick 调用) */ autoDecodeTick: () => void; // 技术 buyTech: (techId: string) => boolean; // 飞升 doPrestige: () => { newBp: number } | null; // 设置 toggleTheme: () => void; toggleSound: () => void; // 派生 canPrestige: () => boolean; } type Store = GameState & GameActions & { _lastAutoDecode: number; _lastSpawn: number; _combo: number; _lastPulse: number; }; /** 计算并写回产能字段 */ function syncStats(state: Partial) { const s = recomputeStats(state); return { crystalsPerSec: s.crystalsPerSec, crystalCap: s.crystalCap, pulsePower: s.pulsePower, offlineEff: s.offlineEff, insightMult: s.insightMult, contactRateMult: s.contactRateMult, autoDecode: s.autoDecode, }; } /** 检查并解锁叙事碎片 */ function checkFragments(state: GameState): string[] { const unlocked: string[] = []; for (const f of FRAGMENTS) { if (!state.fragments[f.id] && state.totalDecoded >= f.threshold) { state.fragments[f.id] = true; unlocked.push(f.id); } } return unlocked; } export const useGameStore = create()( persist( (set, get) => ({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, init: () => { const s = get(); const now = Date.now(); // 兼容旧存档:若存在不可解的活跃谜题(旧算法生成),直接清掉 let activePuzzle = s.activePuzzle; if (activePuzzle && !isSolvable(activePuzzle)) { // 把晶体放回队列,避免玩家卡死 const crystal: Crystal = { id: `c_${now}_rec`, tier: activePuzzle.tier, value: CRYSTAL_VALUE[activePuzzle.tier].crystals, createdAt: now, }; activePuzzle = null; set({ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), }); } // 首次进入:补发离线收益 const elapsed = Math.max(0, (now - s.lastTick) / 1000); if (elapsed > 5) { const cap = 8 * 3600; const secs = Math.min(elapsed, cap); const gain = s.crystalsPerSec * secs * s.offlineEff; set({ crystals: Math.min(s.crystalCap, s.crystals + gain), lastTick: now, activePuzzle, ...syncStats({ tech: s.tech, blueprints: s.blueprints }), }); } else { set({ lastTick: now, activePuzzle, ...syncStats({ tech: s.tech, blueprints: s.blueprints }) }); } }, loadOnline: () => { const s = get(); set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints }) }); }, hardReset: () => { set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0 }); }, tick: (now) => { const s = get(); const dt = Math.max(0, (now - s.lastTick) / 1000); if (dt <= 0) return; // 产能累加(受仓库上限) const newCrystals = Math.min( s.crystalCap, s.crystals + s.crystalsPerSec * dt ); // 自动产出待解码晶体(每 baseInterval 秒一颗,受技术/飞升略提速) const bpBoost = 1 + s.blueprints.length * 0.03; const spawnInterval = (CRYSTAL_SPAWN.baseIntervalSec / bpBoost) * 1000; let pending = s.pendingCrystals; let lastSpawn = s._lastSpawn; if ( now - lastSpawn > spawnInterval && pending.length < CRYSTAL_SPAWN.maxPending ) { const tier = rollCrystalTier(); const crystal: Crystal = { id: `c_${now}_${Math.random().toString(36).slice(2, 7)}`, tier, value: CRYSTAL_VALUE[tier].crystals, createdAt: now, }; pending = [...pending, crystal]; lastSpawn = now; } set({ crystals: newCrystals, lastTick: now, pendingCrystals: pending, _lastSpawn: lastSpawn, }); }, pulse: () => { const s = get(); const now = Date.now(); // 连击 let combo = 1; if (now - s._lastPulse < 1500) { combo = Math.min(10, s._combo + 1); } const mult = 1 + (combo - 1) * 0.15; const gain = s.pulsePower * mult; set({ crystals: Math.min(s.crystalCap, s.crystals + gain), _combo: combo, _lastPulse: now, }); return { gain, combo }; }, startDecode: (crystalId) => { const s = get(); const crystal = s.pendingCrystals.find((c) => c.id === crystalId); if (!crystal) return; const puzzle = generatePuzzle(crystal.tier); set({ activePuzzle: puzzle, pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystalId), }); }, clickNode: (nodeId) => { const s = get(); if (!s.activePuzzle) return { ok: false, finished: false, failReason: "无活跃谜题" }; // 深拷贝谜题 const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); const res = tryClickNode(puzzle, nodeId); if (res.ok) { if (res.finished) { // 结算奖励 const rewards = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult); const newTotal = s.totalDecoded + 1; const newContact = Math.min(100, s.contact + rewards.contact); const newInsights = s.insights + rewards.insights; const newCrystals = s.crystals + rewards.crystals; // 解锁碎片 const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; const unlocked = checkFragments(tentative); set({ activePuzzle: null, crystals: newCrystals, insights: newInsights, contact: newContact, totalDecoded: newTotal, fragments: tentative.fragments, }); return { ok: true, finished: true, failReason: unlocked.join(",") || undefined }; } // 点击成功但未完成:检测当前局面是否仍可解 const solvable = isSolvable(puzzle); set({ activePuzzle: puzzle }); return { ok: true, finished: false, solvable }; } return res; }, undoStep: () => { const s = get(); if (!s.activePuzzle || s.activePuzzle.path.length === 0) return; const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); const lastId = puzzle.path.pop(); if (lastId !== undefined) { const node = puzzle.grid.find((n) => n.id === lastId); if (node) node.used = false; } set({ activePuzzle: puzzle }); }, retryPuzzle: () => { const s = get(); if (!s.activePuzzle) return; set({ activePuzzle: resetPuz(s.activePuzzle) }); }, abandonPuzzle: () => { const s = get(); if (!s.activePuzzle) return; // 晶体放回队列末尾 const crystal: Crystal = { id: `c_${Date.now()}_ret`, tier: s.activePuzzle.tier, value: CRYSTAL_VALUE[s.activePuzzle.tier].crystals, createdAt: Date.now(), }; set({ activePuzzle: null, pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), }); }, autoDecodeTick: () => { const s = get(); if (!s.autoDecode) return; const now = Date.now(); if (now - s._lastAutoDecode < 12000) return; // 找一颗 T1 晶体自动解码 const idx = s.pendingCrystals.findIndex((c) => c.tier === 1); if (idx < 0) return; const crystal = s.pendingCrystals[idx]; const rewards = decodeRewards(1, s.insightMult, s.contactRateMult); const newTotal = s.totalDecoded + 1; const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; checkFragments(tentative); set({ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystal.id), crystals: s.crystals + rewards.crystals, insights: s.insights + rewards.insights, contact: Math.min(100, s.contact + rewards.contact), totalDecoded: newTotal, fragments: tentative.fragments, _lastAutoDecode: now, }); }, buyTech: (techId) => { const s = get(); const node = TECH_TREE.find((t) => t.id === techId); if (!node) return false; const cur = s.tech[techId] ?? 0; if (cur >= 1) return false; // v0.1 每节点 1 级 if (s.insights < node.cost) return false; const newTech = { ...s.tech, [techId]: 1 }; set({ insights: s.insights - node.cost, tech: newTech, ...syncStats({ tech: newTech, blueprints: s.blueprints }), }); return true; }, doPrestige: () => { const s = get(); if (s.contact < CONTACT.prestigeMin) return null; const newBp = computeNewBlueprints(s); const next = performPrestige(s); set({ ...next, ...syncStats({ tech: next.tech, blueprints: next.blueprints }), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, }); return { newBp }; }, canPrestige: () => get().contact >= CONTACT.prestigeMin, toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }), toggleSound: () => set({ soundOn: !get().soundOn }), }), { name: "echo-nexus-save-v1", storage: createJSONStorage(() => localStorage), // 不持久化临时字段 partialize: (s) => { const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, ...rest } = s; void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; return rest as GameState; }, } ) ); /** 选择器:未解锁碎片中下一个门槛 */ export function nextFragmentThreshold(totalDecoded: number): number | null { for (const f of FRAGMENTS) { if (totalDecoded < f.threshold) return f.threshold; } return null; } export { FRAGMENTS, PRESTIGE, TECH_TREE };