"use client"; // 回响星核 / Echo Nexus — Zustand 游戏状态管理 import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import type { GameState, Crystal, CrystalTier, DecodePuzzle, ExpeditionResult, } from "@/lib/game/types"; import { INITIAL_STATE, TECH_TREE, CRYSTAL_VALUE, CONTACT, CRYSTAL_SPAWN, FRAGMENTS, PRESTIGE, } from "@/lib/game/config"; import { createInitialState, recomputeStats, decodeRewards, rollCrystalTierWithBonus, computeNewBlueprints, performPrestige, } from "@/lib/game/engine"; import { generatePuzzle, tryClickNode, isSolvable, resetPuzzle as resetPuz, } from "@/lib/game/decode"; import { generateExpedition, resolveNode, advanceExpedition, computeExpeditionPower, computeExpeditionHp, computeEnergyRegen, computeEnergyRegenInterval, EXPEDITION_CONFIG, } from "@/lib/game/expedition"; import { ACHIEVEMENTS, type Achievement } from "@/lib/game/achievements"; import { TIDE_CONFIG, rollTide, getTideModifiers, computeSilenceCompensation, type StarTide, type TideType, } from "@/lib/game/starTide"; import { getPerk, constellationBonuses, rollPerkChoices, } from "@/lib/game/constellation"; import { migrateChronicleFields, withPerks, } from "@/lib/game/chronicle"; import { generateDailyChallenge, generateWeeklyChallenge, generateTimedChallenge, loadDailyProgress, loadWeeklyProgress, loadTimedProgress, loadChainState, addBeaconProgress, addWeeklyProgress, addTimedProgress, recordChainCompletion, claimWeeklyReward, claimTimedReward, claimChainMilestone, submitCloudScore, getTodayKey, type BeaconDailyChallenge, type BeaconDailyProgress, type BeaconWeeklyChallenge, type BeaconWeeklyProgress, type BeaconTimedChallenge, type BeaconTimedProgress, } from "@/lib/game/beacon"; import { setPendingOfflineReport } from "@/lib/game/offlineReport"; import { ATTRIBUTE_HARD_CAP, migrateAttributes, levelUpCheck, getAllBonuses, createInitialAttributes, createInitialAttributeProgress, type AttributeKey, type CharacterAttributes, type AttributeProgress, } from "@/lib/game/attributes"; interface GameActions { // 生命周期 init: () => void; loadOnline: () => void; hardReset: () => void; // 主循环 tick: (now: number) => void; pulse: () => { gain: number; combo: number } | null; // 星潮 tickTide: (now: number) => TideEvent | null; consumeTideEvents: () => TideEvent[]; /** P3 socket 多人同步:强制触发一道全球星潮(覆盖本地冷却,持续 60s) */ triggerGlobalTide: (type: TideType) => void; // 解码 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; // 探险 startExpedition: () => { ok: boolean; reason?: string }; resolveCurrentNode: () => ExpeditionResult | null; advanceNode: () => void; abortExpedition: () => void; /** v0.9 工单修复:手动关闭已结束的探险,回到入口 */ dismissExpedition: () => void; // 技术 buyTech: (techId: string) => boolean; // 飞升 doPrestige: () => { newBp: number } | null; // 星图天文台 chooseConstellationPerk: (perkId: string) => boolean; rerollPerkChoices: () => void; // 成就 checkAchievements: () => Achievement[]; consumeAchievementQueue: () => Achievement[]; // 设置 toggleTheme: () => void; toggleSound: () => void; // 深空信标奖励发放(v0.5) grantBeaconReward: (insights: number, contact: number) => void; // 深空信标 · 周挑战领取 + 信标链里程碑领取 + 限时挑战领取(v0.8 / v0.8.2) claimWeeklyBeacon: () => { rewardInsight: number; rewardContact: number; score: number; }; claimTimedBeacon: () => { rewardInsight: number; rewardContact: number; score: number; }; claimChainReward: (milestone: number) => { rewardInsight: number; rewardContact: number; label: string; ok: boolean; }; // 深空巡航奖励发放(v0.6 — 实时 Canvas 玩法) grantCruiseReward: (rewards: { crystals?: number; insights?: number; contact?: number }) => void; // 角色属性(v0.7 P1) allocateAttribute: (attr: AttributeKey, points?: number) => { ok: boolean; leveledUp?: number }; gainAttributeExp: (attr: AttributeKey, amount: number) => { leveledUp: number; newLevel: number }; // 派生 canPrestige: () => boolean; } type Store = GameState & GameActions & { _lastAutoDecode: number; _lastSpawn: number; _combo: number; _lastPulse: number; _achievementQueue: Achievement[]; _tideEvents: TideEvent[]; /** P3 全球星潮状态(ephemeral,不持久化) */ globalTide: GlobalTideState | null; }; /** 星潮事件(UI 消费用) */ type TideEvent = { started?: TideType; ended?: TideType; silenceCompensation?: number; /** 是否来自全球星潮(P3 socket 同步) */ isGlobal?: boolean; }; /** 全球星潮状态(与 StarTide 类似,但用于区分本地 vs 全球) */ type GlobalTideState = { type: TideType; startedAt: number; endsAt: number; id: string; }; /** 全球星潮持续毫秒数(与 mini-service DURATION_SEC * 1000 一致) */ const GLOBAL_TIDE_DURATION_MS = 60_000; /** 计算并写回产能字段 */ 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, decodeStepsBonus: s.decodeStepsBonus, }; } /** * 深空信标进度追踪(v0.5 → v0.8 → v0.8.2 扩展)。 * 在游戏关键动作(脉冲/解码/探险/BOSS/洞见)后调用,同时更新: * 1. 日挑战进度(按今日挑战类型增量) * 2. 限时挑战进度(按当前 4 小时时段挑战类型增量) * 3. 周挑战进度(按本周挑战类型增量) * 4. 信标链:日挑战刚完成时记录一次完成(含 grace 续命逻辑) * 进度独立存储于 localStorage,不污染 GameState。 * @returns 四类状态变更供 UI 触发通知 */ function trackBeacon( type: "pulse" | "decode" | "expedition" | "boss" | "insight", delta: number ): { dailyJustCompleted: boolean; timedJustCompleted: boolean; weeklyJustCompleted: boolean; newChainMilestones: number[]; } { const result = { dailyJustCompleted: false, timedJustCompleted: false, weeklyJustCompleted: false, newChainMilestones: [] as number[], }; if (typeof window === "undefined") return result; try { // ---- 日挑战 ---- const challenge: BeaconDailyChallenge = generateDailyChallenge(); if (challenge.type === type) { const current: BeaconDailyProgress = loadDailyProgress(); if (current.completedAt === null) { const { justCompleted } = addBeaconProgress(current, challenge, delta); result.dailyJustCompleted = justCompleted; // 日挑战刚完成 → 更新信标链 if (justCompleted) { const { newMilestones } = recordChainCompletion(getTodayKey()); result.newChainMilestones = newMilestones; } } } // ---- 限时挑战(v0.8.2)---- const tChallenge: BeaconTimedChallenge = generateTimedChallenge(); if (tChallenge.type === type) { const tCurrent: BeaconTimedProgress = loadTimedProgress(); if (tCurrent.completedAt === null) { const { justCompleted } = addTimedProgress( tCurrent, tChallenge, delta ); result.timedJustCompleted = justCompleted; } } // ---- 周挑战 ---- const wChallenge: BeaconWeeklyChallenge = generateWeeklyChallenge(); if (wChallenge.type === type) { const wCurrent: BeaconWeeklyProgress = loadWeeklyProgress(); if (wCurrent.completedAt === null) { const { justCompleted } = addWeeklyProgress( wCurrent, wChallenge, delta ); result.weeklyJustCompleted = justCompleted; } } return result; } catch { return result; } } /** 检查并解锁叙事碎片 */ 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, _achievementQueue: [], _tideEvents: [], globalTide: null, 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), }); } // 兼容旧存档:补全 achievements / 星潮 / 星图 字段 const achievements = s.achievements ?? {}; const activeTide = s.activeTide ?? null; const constellation = s.constellation ?? []; const pendingPerkChoices = s.pendingPerkChoices ?? null; // v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered const migrated = migrateChronicleFields(s); // v0.7 角色属性兼容:补全 attributes / attributeProgress / pendingAttrPoints const attrMigrated = migrateAttributes(s); // 星图「能量共振」天赋 +1 能量上限 const cm = constellationBonuses(constellation); const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus; // lastTideEnd:0 表示从未触发过星潮(首次游玩),保留 0 让 firstDelay 生效 const lastTideEndRaw = s.lastTideEnd ?? 0; // 若旧存档有已过期的星潮,清掉 const tide = activeTide && activeTide.endsAt > now ? activeTide : null; // 首次进入:补发离线收益 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; const crystalsBefore = s.crystals; const crystalsAfter = Math.min(s.crystalCap, s.crystals + gain); // 设置离线报告(v0.5.2):通知 UI 弹出"欢迎回来"对话框 setPendingOfflineReport({ elapsedSec: secs, rawElapsedSec: elapsed, gain: crystalsAfter - crystalsBefore, rate: s.crystalsPerSec, eff: s.offlineEff, capped: elapsed > cap, crystalsBefore, crystalsAfter, crystalCap: s.crystalCap, }); set({ crystals: crystalsAfter, lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, chronicle: migrated.chronicle, runStart: migrated.runStart, bossKills: migrated.bossKills, starTidesEncountered: migrated.starTidesEncountered, attributes: attrMigrated.attributes, attributeProgress: attrMigrated.attributeProgress, pendingAttrPoints: attrMigrated.pendingAttrPoints, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation, attributes: attrMigrated.attributes }), }); } else { set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, chronicle: migrated.chronicle, runStart: migrated.runStart, bossKills: migrated.bossKills, starTidesEncountered: migrated.starTidesEncountered, attributes: attrMigrated.attributes, attributeProgress: attrMigrated.attributeProgress, pendingAttrPoints: attrMigrated.pendingAttrPoints, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation, attributes: attrMigrated.attributes }) }); } }, loadOnline: () => { const s = get(); set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }) }); }, hardReset: () => { set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [], globalTide: null }); }, tickTide: (now) => { const s = get(); // === P3 全球星潮优先:若全球星潮进行中,本地不触发新的星潮 === if (s.globalTide) { // 1a) 检查全球星潮是否结束 if (now >= s.globalTide.endsAt) { const endedType = s.globalTide.type; let silenceCompensation = 0; if (endedType === "silence") { silenceCompensation = computeSilenceCompensation({ type: endedType, startedAt: s.globalTide.startedAt, endsAt: s.globalTide.endsAt, id: s.globalTide.id, }); } const event: TideEvent = { ended: endedType, isGlobal: true }; if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; set({ globalTide: null, activeTide: null, lastTideEnd: now, insights: s.insights + silenceCompensation, // 星潮结束后重算 stats ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }), _tideEvents: [...s._tideEvents, event], }); return event; } // 1b) 全球星潮仍在进行 — 不做本地处理(避免冲突) return null; } const tide = s.activeTide; // 星图「星潮引导」减少间隙 const cm = constellationBonuses(s.constellation ?? []); // v0.7 灵感:星潮触发概率 +X%(缩短间隙) const am = getAllBonuses(s.attributes ?? {}); const tideGapReduction = Math.min(0.3, am.tideTriggerBonus); const gap = Math.max(15000, (TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000) * (1 - tideGapReduction)); // 2) 检查当前本地星潮是否结束 if (tide && now >= tide.endsAt) { const endedType = tide.type; // 寂静期补偿洞见 let silenceCompensation = 0; if (tide.type === "silence") { silenceCompensation = computeSilenceCompensation(tide); } const newInsights = s.insights + silenceCompensation; const event: TideEvent = { ended: endedType }; if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; set({ activeTide: null, lastTideEnd: now, insights: newInsights, // 星潮结束后重算 stats(移除 contactRate/insight 修饰) ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }), _tideEvents: [...s._tideEvents, event], }); return event; } // 3) 检查是否该触发新星潮(间隙已过) if (!tide) { const since = now - s.lastTideEnd; // 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap const firstStart = s.lastTideEnd === 0 || s.lastTideEnd <= s.createdAt + 1000; const need = firstStart ? TIDE_CONFIG.firstDelay : gap; if (since >= need) { const type = rollTide(); const newTide: StarTide = { id: `tide_${now}_${Math.random().toString(36).slice(2, 7)}`, type, startedAt: now, endsAt: now + TIDE_CONFIG.duration, }; const event: TideEvent = { started: type }; // v0.4 编年史:累计遇到的星潮 ID(去重) const tidesAll = s.starTidesEncountered ?? []; const tideId = `tide_${type}`; const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; set({ activeTide: newTide, starTidesEncountered: newTidesAll, // 星潮开始后重算 stats(应用 contactRate/insight 修饰) ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }), _tideEvents: [...s._tideEvents, event], }); return event; } } return null; }, consumeTideEvents: () => { const s = get(); if (s._tideEvents.length === 0) return []; const items = s._tideEvents; set({ _tideEvents: [] }); return items; }, // P3 socket 多人同步:服务端广播全球星潮时调用,强制覆盖本地冷却 triggerGlobalTide: (type) => { const s = get(); const now = Date.now(); const id = `global_tide_${now}_${Math.random().toString(36).slice(2, 7)}`; const newTide: StarTide = { id, type, startedAt: now, endsAt: now + GLOBAL_TIDE_DURATION_MS, }; const newGlobalTide: GlobalTideState = { type, startedAt: now, endsAt: now + GLOBAL_TIDE_DURATION_MS, id, }; // 编年史:累计遇到的星潮 ID(去重) const tidesAll = s.starTidesEncountered ?? []; const tideId = `tide_${type}`; const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; const event: TideEvent = { started: type, isGlobal: true }; set({ globalTide: newGlobalTide, activeTide: newTide, // 覆盖本地 activeTide(即使本地有进行中的星潮也会被替换) starTidesEncountered: newTidesAll, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }), _tideEvents: [...s._tideEvents, event], }); }, tick: (now) => { const s = get(); const dt = Math.max(0, (now - s.lastTick) / 1000); if (dt <= 0) return; // 星潮产能修饰(即时乘) const tideMod = getTideModifiers(s.activeTide); const effCps = s.crystalsPerSec * tideMod.crystalsPerSecMult; // 产能累加(仅闲置产能受仓库上限;探险奖励可超出) const newCrystals = s.crystals >= s.crystalCap ? s.crystals // 已达/超上限,不再自动产出 : Math.min(s.crystalCap, s.crystals + effCps * 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 ) { // 星图「晶体富集」提升 T2/T3 概率 const cm = constellationBonuses(s.constellation ?? []); const tier = rollCrystalTierWithBonus(cm.t2t3BonusRate); 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; } // 能量恢复(探险系统,v0.8.1 动态间隔) let energy = s.energy; let lastEnergyTick = s.lastEnergyTick; if (energy < s.energyMax) { const intervalSec = computeEnergyRegenInterval(s); const regen = computeEnergyRegen(lastEnergyTick, now, energy, s.energyMax, intervalSec); energy = regen.energy; lastEnergyTick = regen.lastTick; } else { lastEnergyTick = now; } set({ crystals: newCrystals, lastTick: now, pendingCrystals: pending, _lastSpawn: lastSpawn, energy, lastEnergyTick, }); }, 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 tideMod = getTideModifiers(s.activeTide); // v0.7 灵感:脉冲连击加成 +X% const am = getAllBonuses(s.attributes ?? {}); const comboBonusMult = 1 + am.pulseComboBonus * Math.max(0, combo - 1); const gain = s.pulsePower * mult * tideMod.pulsePowerMult * comboBonusMult; set({ crystals: Math.min(s.crystalCap, s.crystals + gain), _combo: combo, _lastPulse: now, }); // 深空信标:脉冲任务进度 +1 trackBeacon("pulse", 1); // v0.7 角色属性:连击 ≥3 给灵感经验 if (combo >= 3) { const expGain = 1 + Math.floor(combo / 2); // 3 连击=2, 5 连击=3, 10 连击=6 // 内联经验获取(避免递归调用 set) const prog = s.attributeProgress?.inspiration ?? { exp: 0, level: s.attributes?.inspiration ?? 0 }; const nextExp = prog.exp + expGain; const lvlResult = levelUpCheck( { exp: nextExp, level: s.attributes?.inspiration ?? 0 }, ATTRIBUTE_HARD_CAP ); const newAttributes: CharacterAttributes = { ...(s.attributes ?? createInitialAttributes()), inspiration: lvlResult.newProgress.level, }; const newProgress: AttributeProgress = { ...(s.attributeProgress ?? createInitialAttributeProgress()), inspiration: lvlResult.newProgress, }; set({ attributes: newAttributes, attributeProgress: newProgress, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); } return { gain, combo }; }, startDecode: (crystalId) => { const s = get(); const crystal = s.pendingCrystals.find((c) => c.id === crystalId); if (!crystal) return; // v0.8.2 涌动星潮:解码目标序列 +2(更长谜题) const tideMod = getTideModifiers(s.activeTide); const targetLenBonus = tideMod.targetLenBonus ?? 0; const puzzle = generatePuzzle(crystal.tier, undefined, targetLenBonus); 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 tideMod = getTideModifiers(s.activeTide); const cm = constellationBonuses(s.constellation ?? []); const base = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult); const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; const rewards = { crystals: Math.round(base.crystals * finalMult), insights: Math.round(base.insights * finalMult), contact: +(base.contact * finalMult).toFixed(2), }; 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); // v0.7 角色属性:完成解码给智慧经验(tier 越高经验越多) const wisdomExpGain = puzzle.tier * 2; const curAttrs = s.attributes ?? createInitialAttributes(); const curProg = s.attributeProgress ?? createInitialAttributeProgress(); const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom }; const wisdomLvl = levelUpCheck( { exp: progEntry.exp + wisdomExpGain, level: curAttrs.wisdom }, ATTRIBUTE_HARD_CAP ); const newAttributes: CharacterAttributes = { ...curAttrs, wisdom: wisdomLvl.newProgress.level, }; const newProgress: AttributeProgress = { ...curProg, wisdom: wisdomLvl.newProgress, }; set({ activePuzzle: null, crystals: newCrystals, insights: newInsights, contact: newContact, totalDecoded: newTotal, fragments: tentative.fragments, attributes: newAttributes, attributeProgress: newProgress, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); // 深空信标:解码 +1,洞见累计 trackBeacon("decode", 1); trackBeacon("insight", rewards.insights); 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(); // 星图「自动校准」减少自动解码周期 const cm = constellationBonuses(s.constellation ?? []); // v0.7 智慧:自动解码周期 -X% const am = getAllBonuses(s.attributes ?? {}); const baseInterval = 12000 + cm.autoDecodeIntervalDeltaSec * 1000; // v0.8.2 棱镜星潮:自动解码周期 ×0.7(-30%) const tideModEarly = getTideModifiers(s.activeTide); const tideIntervalMult = tideModEarly.autoDecodeIntervalMult ?? 1; const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult * tideIntervalMult); if (now - s._lastAutoDecode < interval) return; // 找一颗 T1 晶体自动解码;v0.9 dec_5「全息解码核心」解锁后可自动解码 T2 let autoTier: CrystalTier = 1; let idx = s.pendingCrystals.findIndex((c) => c.tier === 1); if (idx < 0 && s.tech?.dec_5) { idx = s.pendingCrystals.findIndex((c) => c.tier === 2); autoTier = 2; } if (idx < 0) return; const crystal = s.pendingCrystals[idx]; const tideMod = getTideModifiers(s.activeTide); const base = decodeRewards(autoTier, s.insightMult, s.contactRateMult); const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; const rewards = { crystals: Math.round(base.crystals * finalMult), insights: Math.round(base.insights * finalMult), contact: +(base.contact * finalMult).toFixed(2), }; const newTotal = s.totalDecoded + 1; const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; checkFragments(tentative); // v0.7 角色属性:自动解码给智慧经验(少量) const curAttrs = s.attributes ?? createInitialAttributes(); const curProg = s.attributeProgress ?? createInitialAttributeProgress(); const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom }; const wisdomLvl = levelUpCheck( { exp: progEntry.exp + 1, level: curAttrs.wisdom }, ATTRIBUTE_HARD_CAP ); const newAttributes: CharacterAttributes = { ...curAttrs, wisdom: wisdomLvl.newProgress.level, }; const newProgress: AttributeProgress = { ...curProg, wisdom: wisdomLvl.newProgress, }; 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, attributes: newAttributes, attributeProgress: newProgress, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); // 深空信标:自动解码也算进度 trackBeacon("decode", 1); trackBeacon("insight", rewards.insights); }, 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, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }), }); return true; }, // ============ 探险系统 ============ startExpedition: () => { const s = get(); if (s.activeExpedition && !s.activeExpedition.finished) { return { ok: false, reason: "已有进行中的探险" }; } if (s.energy < EXPEDITION_CONFIG.energyCost) { return { ok: false, reason: "能量不足" }; } const tideMod = getTideModifiers(s.activeTide); // v0.7 角色属性:探索力 +X% 探险力,勇气 +X 探险生命 const am = getAllBonuses(s.attributes ?? {}); const basePower = computeExpeditionPower(s) + tideMod.expeditionPowerBonus; const baseHp = computeExpeditionHp(s) + tideMod.expeditionHpBonus; const power = Math.round(basePower * am.expeditionPowerMult); const hp = baseHp + am.expeditionHpBonus; const seed = Math.floor(Math.random() * 1e9); const expedition = generateExpedition(seed, power, hp); set({ activeExpedition: expedition, energy: s.energy - EXPEDITION_CONFIG.energyCost, totalExpeditions: s.totalExpeditions + 1, }); return { ok: true }; }, resolveCurrentNode: () => { const s = get(); if (!s.activeExpedition || s.activeExpedition.finished) return null; // 深拷贝 const exp = JSON.parse(JSON.stringify(s.activeExpedition)); // v0.7 勇气:BOSS 战胜率 +X%(动态提高 RNG 阈值) // v0.8.2 蚀相星潮:BOSS 胜率 +20% const nodeBefore = exp.nodes[exp.currentNode]; const isBossNode = nodeBefore?.type === "boss"; const am = getAllBonuses(s.attributes ?? {}); const tideMod = getTideModifiers(s.activeTide); const tideBossBonus = tideMod.bossWinRateBonus ?? 0; const result = isBossNode ? resolveNode(exp, () => { // 单次 rng() 调用:B% 概率返回 0(必胜),其余情况返回 r-B(保持均匀分布) const r = Math.random(); const b = Math.min(0.95, am.bossWinRateBonus + tideBossBonus); return r < b ? 0 : Math.min(1, r - b); }) : resolveNode(exp); // 累计奖励 if (result.crystals) exp.rewards.crystals += result.crystals; if (result.insights) exp.rewards.insights += result.insights; if (result.contact) exp.rewards.contact += result.contact; if (result.fragments) exp.rewards.fragments.push(...result.fragments); if (result.hpDelta) exp.hp = Math.max(0, Math.min(exp.maxHp, exp.hp + result.hpDelta)); // 实时入账(玩家立即获得) const newCrystals = s.crystals + (result.crystals || 0); const newInsights = s.insights + (result.insights || 0); const newContact = Math.min(100, s.contact + (result.contact || 0)); // 碎片解锁 const newFragments = { ...s.fragments }; if (result.fragments) { for (const fid of result.fragments) newFragments[fid] = true; } // 日志 const logEntry = { expeditionId: exp.id, nodeType: nodeBefore?.type || "combat", result: result.log, rewards: [ result.crystals ? `+${result.crystals}晶体` : "", result.insights ? `+${result.insights}洞见` : "", result.contact ? `+${result.contact.toFixed(1)}接触` : "", result.hpDelta ? `${result.hpDelta > 0 ? "+" : ""}${result.hpDelta}生命` : "", ].filter(Boolean).join(" "), timestamp: Date.now(), }; const newLog = [logEntry, ...s.expeditionLog].slice(0, 30); // v0.10 工单 #2 修复:探险结束时立即清空 activeExpedition,避免 finished 状态卡死 // 保留 lastExpeditionSummary 用于入口展示「上次结果」 if (result.ended) { exp.finished = true; } // v0.9 工单修复:探险结束时记录摘要,用于入口界面展示 let lastExpeditionSummary = s.lastExpeditionSummary ?? null; if (result.ended && (result.endReason === "victory" || result.endReason === "defeat")) { lastExpeditionSummary = { result: result.endReason, rewards: { ...exp.rewards }, nodeCount: exp.nodes.length, finishedAt: Date.now(), }; } // v0.4 编年史:击破 BOSS 时累计计数 let bossKills = s.bossKills ?? 0; let bossKilledThisNode = false; if ( result.ended && result.endReason === "victory" && nodeBefore?.type === "boss" ) { bossKills = bossKills + 1; bossKilledThisNode = true; } // v0.7 角色属性:战斗胜利给勇气+探索力经验;BOSS 额外奖励 let newAttributes = s.attributes ?? createInitialAttributes(); let newProgress = s.attributeProgress ?? createInitialAttributeProgress(); let statsNeedResync = false; // 战斗类节点(combat/boss)且胜利:勇气 + 探索力经验 const isCombatVictory = (nodeBefore?.type === "combat" || nodeBefore?.type === "boss") && !result.ended; // 中途战斗胜利(未结束探险) const isExpeditionVictory = result.ended && result.endReason === "victory"; if (isCombatVictory || bossKilledThisNode || isExpeditionVictory) { const courageGain = bossKilledThisNode ? 8 : 2; const explorationGain = bossKilledThisNode ? 6 : isExpeditionVictory ? 4 : 1; const courageLvl = levelUpCheck( { exp: newProgress.courage.exp + courageGain, level: newAttributes.courage }, ATTRIBUTE_HARD_CAP ); const explLvl = levelUpCheck( { exp: newProgress.exploration.exp + explorationGain, level: newAttributes.exploration }, ATTRIBUTE_HARD_CAP ); newAttributes = { ...newAttributes, courage: courageLvl.newProgress.level, exploration: explLvl.newProgress.level, }; newProgress = { ...newProgress, courage: courageLvl.newProgress, exploration: explLvl.newProgress, }; statsNeedResync = true; } set({ // v0.10 工单 #2 修复:探险结束立即清空 activeExpedition,让玩家无障碍再出发 activeExpedition: result.ended ? null : exp, crystals: newCrystals, insights: newInsights, contact: newContact, fragments: newFragments, expeditionLog: newLog, bossKills, attributes: newAttributes, attributeProgress: newProgress, lastExpeditionSummary, ...(statsNeedResync ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }) : {}), }); // 深空信标:探险完成(胜负都计)+ 洞见累计 + BOSS 击破 if (result.ended) { trackBeacon("expedition", 1); if (bossKilledThisNode) { trackBeacon("boss", 1); } } if (result.insights) trackBeacon("insight", result.insights); return result; }, advanceNode: () => { const s = get(); if (!s.activeExpedition || s.activeExpedition.finished) return; const exp = JSON.parse(JSON.stringify(s.activeExpedition)); const node = exp.nodes[exp.currentNode]; if (!node || !node.cleared) return; // 当前节点未结算不能前进 if (exp.currentNode >= exp.nodes.length - 1) return; exp.currentNode++; set({ activeExpedition: exp }); }, abortExpedition: () => { const s = get(); if (!s.activeExpedition) return; const exp = JSON.parse(JSON.stringify(s.activeExpedition)); const logEntry = { expeditionId: exp.id, nodeType: "rest" as const, result: "探险队主动撤退,保留已获奖励。", rewards: "", timestamp: Date.now(), }; // v0.9 工单修复:记录上次探险摘要,用于入口展示 const summary = { result: "abort" as const, rewards: { ...exp.rewards }, nodeCount: exp.nodes.length, finishedAt: Date.now(), }; set({ // v0.10 工单 #2 修复:撤退立即清空 activeExpedition,避免 finished 卡死 activeExpedition: null, expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30), lastExpeditionSummary: summary, }); }, // v0.9 工单修复:手动关闭已结束探险,回到入口 dismissExpedition: () => { const s = get(); if (!s.activeExpedition || !s.activeExpedition.finished) return; set({ activeExpedition: null }); }, doPrestige: () => { const s = get(); if (s.contact < CONTACT.prestigeMin) return null; const newBp = computeNewBlueprints(s); const next = performPrestige(s); // 星图「能量共振」提升上限 const cm = constellationBonuses(next.constellation ?? []); const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; set({ ...next, energyMax, ...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation, attributes: next.attributes }), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _tideEvents: [], }); return { newBp }; }, chooseConstellationPerk: (perkId) => { const s = get(); if (!s.pendingPerkChoices || !s.pendingPerkChoices.includes(perkId)) return false; const perk = getPerk(perkId); if (!perk) return false; if (s.constellation?.includes(perkId)) return false; const newConstellation = [...(s.constellation ?? []), perkId]; const cm = constellationBonuses(newConstellation); const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; // v0.4 编年史:回填最近一条 entry 的 perksThisAscension const chronicle = s.chronicle ?? []; let newChronicle = chronicle; if (chronicle.length > 0) { const lastEntry = chronicle[chronicle.length - 1]; const updatedLast = withPerks(lastEntry, [perkId]); newChronicle = [...chronicle.slice(0, -1), updatedLast]; } set({ constellation: newConstellation, pendingPerkChoices: null, energyMax, chronicle: newChronicle, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation, attributes: s.attributes }), }); return true; }, rerollPerkChoices: () => { const s = get(); if (!s.pendingPerkChoices) return; const choices = rollPerkChoices(s.constellation ?? []); if (choices.length > 0) set({ pendingPerkChoices: choices }); }, checkAchievements: () => { const s = get(); const newlyUnlocked: Achievement[] = []; const updated = { ...s.achievements }; let crystals = s.crystals; let insights = s.insights; let contact = s.contact; let statsDirty = false; for (const a of ACHIEVEMENTS) { if (updated[a.id]) continue; if (a.check(s)) { updated[a.id] = true; newlyUnlocked.push(a); // 发放即时奖励 if (a.reward.crystals) crystals += a.reward.crystals; if (a.reward.insights) insights += a.reward.insights; if (a.reward.contact) contact = Math.min(100, contact + a.reward.contact); if (a.reward.crystalsPerSecPct || a.reward.insightPct) statsDirty = true; } } if (newlyUnlocked.length === 0) return []; set({ achievements: updated, crystals, insights, contact, ...(statsDirty ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }) : {}), _achievementQueue: [...s._achievementQueue, ...newlyUnlocked], }); return newlyUnlocked; }, consumeAchievementQueue: () => { const s = get(); if (s._achievementQueue.length === 0) return []; const items = s._achievementQueue; set({ _achievementQueue: [] }); return items; }, canPrestige: () => get().contact >= CONTACT.prestigeMin, toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }), toggleSound: () => set({ soundOn: !get().soundOn }), // 深空信标:发放每日挑战奖励(v0.5) grantBeaconReward: (insights, contact) => { const s = get(); set({ insights: s.insights + Math.round(insights), contact: Math.min(100, s.contact + contact), }); }, // 深空信标:领取周挑战奖励(v0.8) claimWeeklyBeacon: () => { const challenge = generateWeeklyChallenge(); const progress = loadWeeklyProgress(); const res = claimWeeklyReward(challenge, progress); if (res.rewardInsight > 0 || res.rewardContact > 0) { const s = get(); set({ insights: s.insights + Math.round(res.rewardInsight), contact: Math.min(100, s.contact + res.rewardContact), }); // 异步提交到云排行榜(失败静默,不影响本地) if (res.entry) { void submitCloudScore(res.entry); } } return { rewardInsight: res.rewardInsight, rewardContact: res.rewardContact, score: res.score, }; }, // 深空信标:领取限时挑战奖励(v0.8.2) claimTimedBeacon: () => { const challenge = generateTimedChallenge(); const progress = loadTimedProgress(); const res = claimTimedReward(challenge, progress); if (res.rewardInsight > 0 || res.rewardContact > 0) { const s = get(); set({ insights: s.insights + Math.round(res.rewardInsight), contact: Math.min(100, s.contact + res.rewardContact), }); // 异步提交到云排行榜(失败静默,不影响本地) if (res.entry) { void submitCloudScore(res.entry); } } return { rewardInsight: res.rewardInsight, rewardContact: res.rewardContact, score: res.score, }; }, // 深空信标:领取信标链里程碑奖励(v0.8) claimChainReward: (milestone) => { // loadChainState 仅用于前置校验,真正的状态修改由 claimChainMilestone 完成 const pre = loadChainState(); if ( pre.currentStreak < milestone || pre.milestonesClaimed.includes(milestone) ) { return { rewardInsight: 0, rewardContact: 0, label: "", ok: false, }; } const res = claimChainMilestone(milestone); if (res.rewardInsight > 0 || res.rewardContact > 0) { const s = get(); set({ insights: s.insights + Math.round(res.rewardInsight), contact: Math.min(100, s.contact + res.rewardContact), }); } return { rewardInsight: res.rewardInsight, rewardContact: res.rewardContact, label: res.label, ok: res.rewardInsight > 0 || res.rewardContact > 0, }; }, // 深空巡航:发放实时玩法奖励(v0.6) grantCruiseReward: (rewards) => { const s = get(); const addCrystals = rewards.crystals ?? 0; const addInsights = rewards.insights ?? 0; const addContact = rewards.contact ?? 0; // v0.7 角色属性:巡航通关给探索力+勇气经验(按晶体奖励量缩放) const totalReward = addCrystals + addInsights * 10 + addContact * 10; const expBase = Math.max(2, Math.floor(totalReward / 30)); const curAttrs = s.attributes ?? createInitialAttributes(); const curProg = s.attributeProgress ?? createInitialAttributeProgress(); const explLvl = levelUpCheck( { exp: curProg.exploration.exp + expBase, level: curAttrs.exploration }, ATTRIBUTE_HARD_CAP ); const courageLvl = levelUpCheck( { exp: curProg.courage.exp + Math.floor(expBase * 0.6), level: curAttrs.courage }, ATTRIBUTE_HARD_CAP ); const newAttributes: CharacterAttributes = { ...curAttrs, exploration: explLvl.newProgress.level, courage: courageLvl.newProgress.level, }; const newProgress: AttributeProgress = { ...curProg, exploration: explLvl.newProgress, courage: courageLvl.newProgress, }; set({ crystals: Math.min(s.crystalCap, s.crystals + addCrystals), insights: s.insights + Math.round(addInsights), contact: Math.min(100, s.contact + addContact), attributes: newAttributes, attributeProgress: newProgress, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); }, // ============ 角色属性系统(v0.7 P1) ============ allocateAttribute: (attr, points = 1) => { const s = get(); const cur = s.attributes ?? createInitialAttributes(); const curVal = cur[attr] ?? 0; if (curVal >= ATTRIBUTE_HARD_CAP) { return { ok: false, leveledUp: 0 }; } if ((s.pendingAttrPoints ?? 0) < points) { return { ok: false, leveledUp: 0 }; } const alloc = Math.min(points, ATTRIBUTE_HARD_CAP - curVal, s.pendingAttrPoints); const newVal = curVal + alloc; const newAttributes: CharacterAttributes = { ...cur, [attr]: newVal }; // 同步经验进度 level 字段(保持一致) const curProg = s.attributeProgress ?? createInitialAttributeProgress(); const oldProg = curProg[attr] ?? { exp: 0, level: curVal }; const newProgress: AttributeProgress = { ...curProg, [attr]: { exp: oldProg.exp, level: newVal }, }; set({ attributes: newAttributes, attributeProgress: newProgress, pendingAttrPoints: s.pendingAttrPoints - alloc, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); return { ok: true, leveledUp: alloc }; }, gainAttributeExp: (attr, amount) => { const s = get(); const cur = s.attributes ?? createInitialAttributes(); const curProg = s.attributeProgress ?? createInitialAttributeProgress(); const progEntry = curProg[attr] ?? { exp: 0, level: cur[attr] }; const result = levelUpCheck( { exp: progEntry.exp + amount, level: cur[attr] }, ATTRIBUTE_HARD_CAP ); const newAttributes: CharacterAttributes = { ...cur, [attr]: result.newProgress.level }; const newProgress: AttributeProgress = { ...curProg, [attr]: result.newProgress, }; set({ attributes: newAttributes, attributeProgress: newProgress, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), }); return { leveledUp: result.levelsGained, newLevel: result.newProgress.level }; }, }), { name: "echo-nexus-save-v1", version: 2, storage: createJSONStorage(() => localStorage), // v0.12 存档迁移:旧存档(v0/v1,key=echo-nexus-save-v1)补全新增字段,防崩溃 migrate: (persistedState: unknown, version: number) => { void version; // v0/v1 统一走合并迁移,无需分支 const s = (persistedState || {}) as Record; const base = createInitialState(); // 合并:以当前 INITIAL_STATE 为基底,旧存档字段覆盖,缺失字段用默认值 const migrated = { ...base, ...s, // v0.8+ 新增字段兜底 bossKills: (s.bossKills as number) ?? 0, starTidesEncountered: (s.starTidesEncountered as string[]) ?? [], chronicle: (s.chronicle as unknown[]) ?? [], runStart: (s.runStart as number) ?? Date.now(), // v0.8.2 attributes 系统 attributes: (s.attributes as Record) ?? base.attributes, attributeProgress: (s.attributeProgress as unknown) ?? base.attributeProgress, pendingAttrPoints: (s.pendingAttrPoints as number) ?? 0, // v0.10 探险系统字段兜底 activeExpedition: (s.activeExpedition as unknown) ?? null, totalExpeditions: (s.totalExpeditions as number) ?? 0, lastExpeditionSummary: (s.lastExpeditionSummary as unknown) ?? null, // 星图兜底 constellation: (s.constellation as string[]) ?? [], pendingPerkChoices: (s.pendingPerkChoices as unknown[]) ?? [], // 技术树/碎片/成就兜底(防旧存档为 undefined) tech: (s.tech as Record) ?? {}, fragments: (s.fragments as Record) ?? {}, achievements: (s.achievements as Record) ?? {}, } as GameState; // 丢弃不可解的活跃谜题(旧算法生成) if (migrated.activePuzzle && typeof migrated.activePuzzle === "object") { const p = migrated.activePuzzle as { nodes?: unknown[] }; if (!p.nodes || !Array.isArray(p.nodes) || p.nodes.length === 0) { migrated.activePuzzle = null; } } return migrated; }, // 不持久化临时字段 partialize: (s) => { const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, globalTide, ...rest } = s; void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents; void globalTide; 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 }; // 开发期调试:暴露 store 到 window,便于 QA 测试 if (typeof window !== "undefined" && process.env.NODE_ENV !== "production") { (window as unknown as { __gameStore?: typeof useGameStore }).__gameStore = useGameStore; }