1→"use client"; 2→// 回响星核 / Echo Nexus — Zustand 游戏状态管理 3→import { create } from "zustand"; 4→import { persist, createJSONStorage } from "zustand/middleware"; 5→import type { 6→ GameState, 7→ Crystal, 8→ CrystalTier, 9→ DecodePuzzle, 10→ ExpeditionResult, 11→} from "@/lib/game/types"; 12→import { 13→ INITIAL_STATE, 14→ TECH_TREE, 15→ CRYSTAL_VALUE, 16→ CONTACT, 17→ CRYSTAL_SPAWN, 18→ FRAGMENTS, 19→ PRESTIGE, 20→} from "@/lib/game/config"; 21→import { 22→ createInitialState, 23→ recomputeStats, 24→ decodeRewards, 25→ rollCrystalTierWithBonus, 26→ computeNewBlueprints, 27→ performPrestige, 28→} from "@/lib/game/engine"; 29→import { 30→ generatePuzzle, 31→ tryClickNode, 32→ isSolvable, 33→ resetPuzzle as resetPuz, 34→} from "@/lib/game/decode"; 35→import { 36→ generateExpedition, 37→ resolveNode, 38→ advanceExpedition, 39→ computeExpeditionPower, 40→ computeExpeditionHp, 41→ computeEnergyRegen, 42→ computeEnergyRegenInterval, 43→ EXPEDITION_CONFIG, 44→} from "@/lib/game/expedition"; 45→import { ACHIEVEMENTS, type Achievement } from "@/lib/game/achievements"; 46→import { 47→ TIDE_CONFIG, 48→ rollTide, 49→ getTideModifiers, 50→ computeSilenceCompensation, 51→ type StarTide, 52→ type TideType, 53→} from "@/lib/game/starTide"; 54→import { 55→ getPerk, 56→ constellationBonuses, 57→ rollPerkChoices, 58→} from "@/lib/game/constellation"; 59→import { 60→ migrateChronicleFields, 61→ withPerks, 62→} from "@/lib/game/chronicle"; 63→import { 64→ generateDailyChallenge, 65→ generateWeeklyChallenge, 66→ generateTimedChallenge, 67→ loadDailyProgress, 68→ loadWeeklyProgress, 69→ loadTimedProgress, 70→ loadChainState, 71→ addBeaconProgress, 72→ addWeeklyProgress, 73→ addTimedProgress, 74→ recordChainCompletion, 75→ claimWeeklyReward, 76→ claimTimedReward, 77→ claimChainMilestone, 78→ submitCloudScore, 79→ getTodayKey, 80→ type BeaconDailyChallenge, 81→ type BeaconDailyProgress, 82→ type BeaconWeeklyChallenge, 83→ type BeaconWeeklyProgress, 84→ type BeaconTimedChallenge, 85→ type BeaconTimedProgress, 86→} from "@/lib/game/beacon"; 87→import { setPendingOfflineReport } from "@/lib/game/offlineReport"; 88→import { 89→ ATTRIBUTE_HARD_CAP, 90→ migrateAttributes, 91→ levelUpCheck, 92→ getAllBonuses, 93→ createInitialAttributes, 94→ createInitialAttributeProgress, 95→ type AttributeKey, 96→ type CharacterAttributes, 97→ type AttributeProgress, 98→} from "@/lib/game/attributes"; 99→ 100→interface GameActions { 101→ // 生命周期 102→ init: () => void; 103→ loadOnline: () => void; 104→ hardReset: () => void; 105→ 106→ // 主循环 107→ tick: (now: number) => void; 108→ pulse: () => { gain: number; combo: number } | null; 109→ 110→ // 星潮 111→ tickTide: (now: number) => TideEvent | null; 112→ consumeTideEvents: () => TideEvent[]; 113→ /** P3 socket 多人同步:强制触发一道全球星潮(覆盖本地冷却,持续 60s) */ 114→ triggerGlobalTide: (type: TideType) => void; 115→ 116→ // 解码 117→ startDecode: (crystalId: string) => void; 118→ clickNode: (nodeId: number) => { ok: boolean; finished: boolean; solvable?: boolean; failReason?: string }; 119→ undoStep: () => void; 120→ retryPuzzle: () => void; 121→ abandonPuzzle: () => void; 122→ /** 自动解码 T1(技术解锁后由 tick 调用) */ 123→ autoDecodeTick: () => void; 124→ 125→ // 探险 126→ startExpedition: () => { ok: boolean; reason?: string }; 127→ resolveCurrentNode: () => ExpeditionResult | null; 128→ advanceNode: () => void; 129→ abortExpedition: () => void; 130→ 131→ // 技术 132→ buyTech: (techId: string) => boolean; 133→ 134→ // 飞升 135→ doPrestige: () => { newBp: number } | null; 136→ 137→ // 星图天文台 138→ chooseConstellationPerk: (perkId: string) => boolean; 139→ rerollPerkChoices: () => void; 140→ 141→ // 成就 142→ checkAchievements: () => Achievement[]; 143→ consumeAchievementQueue: () => Achievement[]; 144→ 145→ // 设置 146→ toggleTheme: () => void; 147→ toggleSound: () => void; 148→ 149→ // 深空信标奖励发放(v0.5) 150→ grantBeaconReward: (insights: number, contact: number) => void; 151→ 152→ // 深空信标 · 周挑战领取 + 信标链里程碑领取 + 限时挑战领取(v0.8 / v0.8.2) 153→ claimWeeklyBeacon: () => { 154→ rewardInsight: number; 155→ rewardContact: number; 156→ score: number; 157→ }; 158→ claimTimedBeacon: () => { 159→ rewardInsight: number; 160→ rewardContact: number; 161→ score: number; 162→ }; 163→ claimChainReward: (milestone: number) => { 164→ rewardInsight: number; 165→ rewardContact: number; 166→ label: string; 167→ ok: boolean; 168→ }; 169→ 170→ // 深空巡航奖励发放(v0.6 — 实时 Canvas 玩法) 171→ grantCruiseReward: (rewards: { crystals?: number; insights?: number; contact?: number }) => void; 172→ 173→ // 角色属性(v0.7 P1) 174→ allocateAttribute: (attr: AttributeKey, points?: number) => { ok: boolean; leveledUp?: number }; 175→ gainAttributeExp: (attr: AttributeKey, amount: number) => { leveledUp: number; newLevel: number }; 176→ 177→ // 派生 178→ canPrestige: () => boolean; 179→} 180→ 181→type Store = GameState & GameActions & { 182→ _lastAutoDecode: number; 183→ _lastSpawn: number; 184→ _combo: number; 185→ _lastPulse: number; 186→ _achievementQueue: Achievement[]; 187→ _tideEvents: TideEvent[]; 188→ /** P3 全球星潮状态(ephemeral,不持久化) */ 189→ globalTide: GlobalTideState | null; 190→}; 191→ 192→/** 星潮事件(UI 消费用) */ 193→type TideEvent = { 194→ started?: TideType; 195→ ended?: TideType; 196→ silenceCompensation?: number; 197→ /** 是否来自全球星潮(P3 socket 同步) */ 198→ isGlobal?: boolean; 199→}; 200→ 201→/** 全球星潮状态(与 StarTide 类似,但用于区分本地 vs 全球) */ 202→type GlobalTideState = { 203→ type: TideType; 204→ startedAt: number; 205→ endsAt: number; 206→ id: string; 207→}; 208→ 209→/** 全球星潮持续毫秒数(与 mini-service DURATION_SEC * 1000 一致) */ 210→const GLOBAL_TIDE_DURATION_MS = 60_000; 211→ 212→/** 计算并写回产能字段 */ 213→function syncStats(state: Partial) { 214→ const s = recomputeStats(state); 215→ return { 216→ crystalsPerSec: s.crystalsPerSec, 217→ crystalCap: s.crystalCap, 218→ pulsePower: s.pulsePower, 219→ offlineEff: s.offlineEff, 220→ insightMult: s.insightMult, 221→ contactRateMult: s.contactRateMult, 222→ autoDecode: s.autoDecode, 223→ decodeStepsBonus: s.decodeStepsBonus, 224→ }; 225→} 226→ 227→/** 228→ * 深空信标进度追踪(v0.5 → v0.8 → v0.8.2 扩展)。 229→ * 在游戏关键动作(脉冲/解码/探险/BOSS/洞见)后调用,同时更新: 230→ * 1. 日挑战进度(按今日挑战类型增量) 231→ * 2. 限时挑战进度(按当前 4 小时时段挑战类型增量) 232→ * 3. 周挑战进度(按本周挑战类型增量) 233→ * 4. 信标链:日挑战刚完成时记录一次完成(含 grace 续命逻辑) 234→ * 进度独立存储于 localStorage,不污染 GameState。 235→ * @returns 四类状态变更供 UI 触发通知 236→ */ 237→function trackBeacon( 238→ type: "pulse" | "decode" | "expedition" | "boss" | "insight", 239→ delta: number 240→): { 241→ dailyJustCompleted: boolean; 242→ timedJustCompleted: boolean; 243→ weeklyJustCompleted: boolean; 244→ newChainMilestones: number[]; 245→} { 246→ const result = { 247→ dailyJustCompleted: false, 248→ timedJustCompleted: false, 249→ weeklyJustCompleted: false, 250→ newChainMilestones: [] as number[], 251→ }; 252→ if (typeof window === "undefined") return result; 253→ try { 254→ // ---- 日挑战 ---- 255→ const challenge: BeaconDailyChallenge = generateDailyChallenge(); 256→ if (challenge.type === type) { 257→ const current: BeaconDailyProgress = loadDailyProgress(); 258→ if (current.completedAt === null) { 259→ const { justCompleted } = addBeaconProgress(current, challenge, delta); 260→ result.dailyJustCompleted = justCompleted; 261→ // 日挑战刚完成 → 更新信标链 262→ if (justCompleted) { 263→ const { newMilestones } = recordChainCompletion(getTodayKey()); 264→ result.newChainMilestones = newMilestones; 265→ } 266→ } 267→ } 268→ 269→ // ---- 限时挑战(v0.8.2)---- 270→ const tChallenge: BeaconTimedChallenge = generateTimedChallenge(); 271→ if (tChallenge.type === type) { 272→ const tCurrent: BeaconTimedProgress = loadTimedProgress(); 273→ if (tCurrent.completedAt === null) { 274→ const { justCompleted } = addTimedProgress( 275→ tCurrent, 276→ tChallenge, 277→ delta 278→ ); 279→ result.timedJustCompleted = justCompleted; 280→ } 281→ } 282→ 283→ // ---- 周挑战 ---- 284→ const wChallenge: BeaconWeeklyChallenge = generateWeeklyChallenge(); 285→ if (wChallenge.type === type) { 286→ const wCurrent: BeaconWeeklyProgress = loadWeeklyProgress(); 287→ if (wCurrent.completedAt === null) { 288→ const { justCompleted } = addWeeklyProgress( 289→ wCurrent, 290→ wChallenge, 291→ delta 292→ ); 293→ result.weeklyJustCompleted = justCompleted; 294→ } 295→ } 296→ 297→ return result; 298→ } catch { 299→ return result; 300→ } 301→} 302→ 303→/** 检查并解锁叙事碎片 */ 304→function checkFragments(state: GameState): string[] { 305→ const unlocked: string[] = []; 306→ for (const f of FRAGMENTS) { 307→ if (!state.fragments[f.id] && state.totalDecoded >= f.threshold) { 308→ state.fragments[f.id] = true; 309→ unlocked.push(f.id); 310→ } 311→ } 312→ return unlocked; 313→} 314→ 315→export const useGameStore = create()( 316→ persist( 317→ (set, get) => ({ 318→ ...createInitialState(), 319→ _lastAutoDecode: Date.now(), 320→ _lastSpawn: Date.now(), 321→ _combo: 0, 322→ _lastPulse: 0, 323→ _achievementQueue: [], 324→ _tideEvents: [], 325→ globalTide: null, 326→ 327→ init: () => { 328→ const s = get(); 329→ const now = Date.now(); 330→ // 兼容旧存档:若存在不可解的活跃谜题(旧算法生成),直接清掉 331→ let activePuzzle = s.activePuzzle; 332→ if (activePuzzle && !isSolvable(activePuzzle)) { 333→ // 把晶体放回队列,避免玩家卡死 334→ const crystal: Crystal = { 335→ id: `c_${now}_rec`, 336→ tier: activePuzzle.tier, 337→ value: CRYSTAL_VALUE[activePuzzle.tier].crystals, 338→ createdAt: now, 339→ }; 340→ activePuzzle = null; 341→ set({ 342→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), 343→ }); 344→ } 345→ // 兼容旧存档:补全 achievements / 星潮 / 星图 字段 346→ const achievements = s.achievements ?? {}; 347→ const activeTide = s.activeTide ?? null; 348→ const constellation = s.constellation ?? []; 349→ const pendingPerkChoices = s.pendingPerkChoices ?? null; 350→ // v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered 351→ const migrated = migrateChronicleFields(s); 352→ // v0.7 角色属性兼容:补全 attributes / attributeProgress / pendingAttrPoints 353→ const attrMigrated = migrateAttributes(s); 354→ // 星图「能量共振」天赋 +1 能量上限 355→ const cm = constellationBonuses(constellation); 356→ const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus; 357→ // lastTideEnd:0 表示从未触发过星潮(首次游玩),保留 0 让 firstDelay 生效 358→ const lastTideEndRaw = s.lastTideEnd ?? 0; 359→ // 若旧存档有已过期的星潮,清掉 360→ const tide = activeTide && activeTide.endsAt > now ? activeTide : null; 361→ // 首次进入:补发离线收益 362→ const elapsed = Math.max(0, (now - s.lastTick) / 1000); 363→ if (elapsed > 5) { 364→ const cap = 8 * 3600; 365→ const secs = Math.min(elapsed, cap); 366→ const gain = s.crystalsPerSec * secs * s.offlineEff; 367→ const crystalsBefore = s.crystals; 368→ const crystalsAfter = Math.min(s.crystalCap, s.crystals + gain); 369→ // 设置离线报告(v0.5.2):通知 UI 弹出"欢迎回来"对话框 370→ setPendingOfflineReport({ 371→ elapsedSec: secs, 372→ rawElapsedSec: elapsed, 373→ gain: crystalsAfter - crystalsBefore, 374→ rate: s.crystalsPerSec, 375→ eff: s.offlineEff, 376→ capped: elapsed > cap, 377→ crystalsBefore, 378→ crystalsAfter, 379→ crystalCap: s.crystalCap, 380→ }); 381→ set({ 382→ crystals: crystalsAfter, 383→ lastTick: now, 384→ activePuzzle, 385→ achievements, 386→ activeTide: tide, 387→ lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, 388→ constellation, 389→ pendingPerkChoices, 390→ energyMax, 391→ chronicle: migrated.chronicle, 392→ runStart: migrated.runStart, 393→ bossKills: migrated.bossKills, 394→ starTidesEncountered: migrated.starTidesEncountered, 395→ attributes: attrMigrated.attributes, 396→ attributeProgress: attrMigrated.attributeProgress, 397→ pendingAttrPoints: attrMigrated.pendingAttrPoints, 398→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation, attributes: attrMigrated.attributes }), 399→ }); 400→ } else { 401→ 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 }) }); 402→ } 403→ }, 404→ 405→ loadOnline: () => { 406→ const s = get(); 407→ set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }) }); 408→ }, 409→ 410→ hardReset: () => { 411→ set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [], globalTide: null }); 412→ }, 413→ 414→ tickTide: (now) => { 415→ const s = get(); 416→ // === P3 全球星潮优先:若全球星潮进行中,本地不触发新的星潮 === 417→ if (s.globalTide) { 418→ // 1a) 检查全球星潮是否结束 419→ if (now >= s.globalTide.endsAt) { 420→ const endedType = s.globalTide.type; 421→ let silenceCompensation = 0; 422→ if (endedType === "silence") { 423→ silenceCompensation = computeSilenceCompensation({ 424→ type: endedType, 425→ startedAt: s.globalTide.startedAt, 426→ endsAt: s.globalTide.endsAt, 427→ id: s.globalTide.id, 428→ }); 429→ } 430→ const event: TideEvent = { ended: endedType, isGlobal: true }; 431→ if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; 432→ set({ 433→ globalTide: null, 434→ activeTide: null, 435→ lastTideEnd: now, 436→ insights: s.insights + silenceCompensation, 437→ // 星潮结束后重算 stats 438→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }), 439→ _tideEvents: [...s._tideEvents, event], 440→ }); 441→ return event; 442→ } 443→ // 1b) 全球星潮仍在进行 — 不做本地处理(避免冲突) 444→ return null; 445→ } 446→ 447→ const tide = s.activeTide; 448→ // 星图「星潮引导」减少间隙 449→ const cm = constellationBonuses(s.constellation ?? []); 450→ // v0.7 灵感:星潮触发概率 +X%(缩短间隙) 451→ const am = getAllBonuses(s.attributes ?? {}); 452→ const tideGapReduction = Math.min(0.3, am.tideTriggerBonus); 453→ const gap = Math.max(15000, (TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000) * (1 - tideGapReduction)); 454→ // 2) 检查当前本地星潮是否结束 455→ if (tide && now >= tide.endsAt) { 456→ const endedType = tide.type; 457→ // 寂静期补偿洞见 458→ let silenceCompensation = 0; 459→ if (tide.type === "silence") { 460→ silenceCompensation = computeSilenceCompensation(tide); 461→ } 462→ const newInsights = s.insights + silenceCompensation; 463→ const event: TideEvent = { ended: endedType }; 464→ if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; 465→ set({ 466→ activeTide: null, 467→ lastTideEnd: now, 468→ insights: newInsights, 469→ // 星潮结束后重算 stats(移除 contactRate/insight 修饰) 470→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }), 471→ _tideEvents: [...s._tideEvents, event], 472→ }); 473→ return event; 474→ } 475→ // 3) 检查是否该触发新星潮(间隙已过) 476→ if (!tide) { 477→ const since = now - s.lastTideEnd; 478→ // 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap 479→ const firstStart = s.lastTideEnd === 0 || s.lastTideEnd <= s.createdAt + 1000; 480→ const need = firstStart ? TIDE_CONFIG.firstDelay : gap; 481→ if (since >= need) { 482→ const type = rollTide(); 483→ const newTide: StarTide = { 484→ id: `tide_${now}_${Math.random().toString(36).slice(2, 7)}`, 485→ type, 486→ startedAt: now, 487→ endsAt: now + TIDE_CONFIG.duration, 488→ }; 489→ const event: TideEvent = { started: type }; 490→ // v0.4 编年史:累计遇到的星潮 ID(去重) 491→ const tidesAll = s.starTidesEncountered ?? []; 492→ const tideId = `tide_${type}`; 493→ const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; 494→ set({ 495→ activeTide: newTide, 496→ starTidesEncountered: newTidesAll, 497→ // 星潮开始后重算 stats(应用 contactRate/insight 修饰) 498→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }), 499→ _tideEvents: [...s._tideEvents, event], 500→ }); 501→ return event; 502→ } 503→ } 504→ return null; 505→ }, 506→ 507→ consumeTideEvents: () => { 508→ const s = get(); 509→ if (s._tideEvents.length === 0) return []; 510→ const items = s._tideEvents; 511→ set({ _tideEvents: [] }); 512→ return items; 513→ }, 514→ 515→ // P3 socket 多人同步:服务端广播全球星潮时调用,强制覆盖本地冷却 516→ triggerGlobalTide: (type) => { 517→ const s = get(); 518→ const now = Date.now(); 519→ const id = `global_tide_${now}_${Math.random().toString(36).slice(2, 7)}`; 520→ const newTide: StarTide = { 521→ id, 522→ type, 523→ startedAt: now, 524→ endsAt: now + GLOBAL_TIDE_DURATION_MS, 525→ }; 526→ const newGlobalTide: GlobalTideState = { 527→ type, 528→ startedAt: now, 529→ endsAt: now + GLOBAL_TIDE_DURATION_MS, 530→ id, 531→ }; 532→ // 编年史:累计遇到的星潮 ID(去重) 533→ const tidesAll = s.starTidesEncountered ?? []; 534→ const tideId = `tide_${type}`; 535→ const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; 536→ const event: TideEvent = { started: type, isGlobal: true }; 537→ set({ 538→ globalTide: newGlobalTide, 539→ activeTide: newTide, // 覆盖本地 activeTide(即使本地有进行中的星潮也会被替换) 540→ starTidesEncountered: newTidesAll, 541→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }), 542→ _tideEvents: [...s._tideEvents, event], 543→ }); 544→ }, 545→ 546→ tick: (now) => { 547→ const s = get(); 548→ const dt = Math.max(0, (now - s.lastTick) / 1000); 549→ if (dt <= 0) return; 550→ 551→ // 星潮产能修饰(即时乘) 552→ const tideMod = getTideModifiers(s.activeTide); 553→ const effCps = s.crystalsPerSec * tideMod.crystalsPerSecMult; 554→ // 产能累加(仅闲置产能受仓库上限;探险奖励可超出) 555→ const newCrystals = 556→ s.crystals >= s.crystalCap 557→ ? s.crystals // 已达/超上限,不再自动产出 558→ : Math.min(s.crystalCap, s.crystals + effCps * dt); 559→ 560→ // 自动产出待解码晶体(每 baseInterval 秒一颗,受技术/飞升略提速) 561→ const bpBoost = 1 + s.blueprints.length * 0.03; 562→ const spawnInterval = (CRYSTAL_SPAWN.baseIntervalSec / bpBoost) * 1000; 563→ let pending = s.pendingCrystals; 564→ let lastSpawn = s._lastSpawn; 565→ if ( 566→ now - lastSpawn > spawnInterval && 567→ pending.length < CRYSTAL_SPAWN.maxPending 568→ ) { 569→ // 星图「晶体富集」提升 T2/T3 概率 570→ const cm = constellationBonuses(s.constellation ?? []); 571→ const tier = rollCrystalTierWithBonus(cm.t2t3BonusRate); 572→ const crystal: Crystal = { 573→ id: `c_${now}_${Math.random().toString(36).slice(2, 7)}`, 574→ tier, 575→ value: CRYSTAL_VALUE[tier].crystals, 576→ createdAt: now, 577→ }; 578→ pending = [...pending, crystal]; 579→ lastSpawn = now; 580→ } 581→ 582→ // 能量恢复(探险系统,v0.8.1 动态间隔) 583→ let energy = s.energy; 584→ let lastEnergyTick = s.lastEnergyTick; 585→ if (energy < s.energyMax) { 586→ const intervalSec = computeEnergyRegenInterval(s); 587→ const regen = computeEnergyRegen(lastEnergyTick, now, energy, s.energyMax, intervalSec); 588→ energy = regen.energy; 589→ lastEnergyTick = regen.lastTick; 590→ } else { 591→ lastEnergyTick = now; 592→ } 593→ 594→ set({ 595→ crystals: newCrystals, 596→ lastTick: now, 597→ pendingCrystals: pending, 598→ _lastSpawn: lastSpawn, 599→ energy, 600→ lastEnergyTick, 601→ }); 602→ }, 603→ 604→ pulse: () => { 605→ const s = get(); 606→ const now = Date.now(); 607→ // 连击 608→ let combo = 1; 609→ if (now - s._lastPulse < 1500) { 610→ combo = Math.min(10, s._combo + 1); 611→ } 612→ const mult = 1 + (combo - 1) * 0.15; 613→ // 星潮脉冲威力修饰 614→ const tideMod = getTideModifiers(s.activeTide); 615→ // v0.7 灵感:脉冲连击加成 +X% 616→ const am = getAllBonuses(s.attributes ?? {}); 617→ const comboBonusMult = 1 + am.pulseComboBonus * Math.max(0, combo - 1); 618→ const gain = s.pulsePower * mult * tideMod.pulsePowerMult * comboBonusMult; 619→ set({ 620→ crystals: Math.min(s.crystalCap, s.crystals + gain), 621→ _combo: combo, 622→ _lastPulse: now, 623→ }); 624→ // 深空信标:脉冲任务进度 +1 625→ trackBeacon("pulse", 1); 626→ // v0.7 角色属性:连击 ≥3 给灵感经验 627→ if (combo >= 3) { 628→ const expGain = 1 + Math.floor(combo / 2); // 3 连击=2, 5 连击=3, 10 连击=6 629→ // 内联经验获取(避免递归调用 set) 630→ const prog = s.attributeProgress?.inspiration ?? { exp: 0, level: s.attributes?.inspiration ?? 0 }; 631→ const nextExp = prog.exp + expGain; 632→ const lvlResult = levelUpCheck( 633→ { exp: nextExp, level: s.attributes?.inspiration ?? 0 }, 634→ ATTRIBUTE_HARD_CAP 635→ ); 636→ const newAttributes: CharacterAttributes = { 637→ ...(s.attributes ?? createInitialAttributes()), 638→ inspiration: lvlResult.newProgress.level, 639→ }; 640→ const newProgress: AttributeProgress = { 641→ ...(s.attributeProgress ?? createInitialAttributeProgress()), 642→ inspiration: lvlResult.newProgress, 643→ }; 644→ set({ 645→ attributes: newAttributes, 646→ attributeProgress: newProgress, 647→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 648→ }); 649→ } 650→ return { gain, combo }; 651→ }, 652→ 653→ startDecode: (crystalId) => { 654→ const s = get(); 655→ const crystal = s.pendingCrystals.find((c) => c.id === crystalId); 656→ if (!crystal) return; 657→ // v0.8.2 涌动星潮:解码目标序列 +2(更长谜题) 658→ const tideMod = getTideModifiers(s.activeTide); 659→ const targetLenBonus = tideMod.targetLenBonus ?? 0; 660→ const puzzle = generatePuzzle(crystal.tier, undefined, targetLenBonus); 661→ set({ 662→ activePuzzle: puzzle, 663→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystalId), 664→ }); 665→ }, 666→ 667→ clickNode: (nodeId) => { 668→ const s = get(); 669→ if (!s.activePuzzle) return { ok: false, finished: false, failReason: "无活跃谜题" }; 670→ // 深拷贝谜题 671→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); 672→ const res = tryClickNode(puzzle, nodeId); 673→ if (res.ok) { 674→ if (res.finished) { 675→ // 结算奖励(星潮解码奖励修饰 + 星图「二周目经验」) 676→ const tideMod = getTideModifiers(s.activeTide); 677→ const cm = constellationBonuses(s.constellation ?? []); 678→ const base = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult); 679→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; 680→ const rewards = { 681→ crystals: Math.round(base.crystals * finalMult), 682→ insights: Math.round(base.insights * finalMult), 683→ contact: +(base.contact * finalMult).toFixed(2), 684→ }; 685→ const newTotal = s.totalDecoded + 1; 686→ const newContact = Math.min(100, s.contact + rewards.contact); 687→ const newInsights = s.insights + rewards.insights; 688→ const newCrystals = s.crystals + rewards.crystals; 689→ // 解锁碎片 690→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; 691→ const unlocked = checkFragments(tentative); 692→ // v0.7 角色属性:完成解码给智慧经验(tier 越高经验越多) 693→ const wisdomExpGain = puzzle.tier * 2; 694→ const curAttrs = s.attributes ?? createInitialAttributes(); 695→ const curProg = s.attributeProgress ?? createInitialAttributeProgress(); 696→ const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom }; 697→ const wisdomLvl = levelUpCheck( 698→ { exp: progEntry.exp + wisdomExpGain, level: curAttrs.wisdom }, 699→ ATTRIBUTE_HARD_CAP 700→ ); 701→ const newAttributes: CharacterAttributes = { 702→ ...curAttrs, 703→ wisdom: wisdomLvl.newProgress.level, 704→ }; 705→ const newProgress: AttributeProgress = { 706→ ...curProg, 707→ wisdom: wisdomLvl.newProgress, 708→ }; 709→ set({ 710→ activePuzzle: null, 711→ crystals: newCrystals, 712→ insights: newInsights, 713→ contact: newContact, 714→ totalDecoded: newTotal, 715→ fragments: tentative.fragments, 716→ attributes: newAttributes, 717→ attributeProgress: newProgress, 718→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 719→ }); 720→ // 深空信标:解码 +1,洞见累计 721→ trackBeacon("decode", 1); 722→ trackBeacon("insight", rewards.insights); 723→ return { ok: true, finished: true, failReason: unlocked.join(",") || undefined }; 724→ } 725→ // 点击成功但未完成:检测当前局面是否仍可解 726→ const solvable = isSolvable(puzzle); 727→ set({ activePuzzle: puzzle }); 728→ return { ok: true, finished: false, solvable }; 729→ } 730→ return res; 731→ }, 732→ 733→ undoStep: () => { 734→ const s = get(); 735→ if (!s.activePuzzle || s.activePuzzle.path.length === 0) return; 736→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); 737→ const lastId = puzzle.path.pop(); 738→ if (lastId !== undefined) { 739→ const node = puzzle.grid.find((n) => n.id === lastId); 740→ if (node) node.used = false; 741→ } 742→ set({ activePuzzle: puzzle }); 743→ }, 744→ 745→ retryPuzzle: () => { 746→ const s = get(); 747→ if (!s.activePuzzle) return; 748→ set({ activePuzzle: resetPuz(s.activePuzzle) }); 749→ }, 750→ 751→ abandonPuzzle: () => { 752→ const s = get(); 753→ if (!s.activePuzzle) return; 754→ // 晶体放回队列末尾 755→ const crystal: Crystal = { 756→ id: `c_${Date.now()}_ret`, 757→ tier: s.activePuzzle.tier, 758→ value: CRYSTAL_VALUE[s.activePuzzle.tier].crystals, 759→ createdAt: Date.now(), 760→ }; 761→ set({ 762→ activePuzzle: null, 763→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), 764→ }); 765→ }, 766→ 767→ autoDecodeTick: () => { 768→ const s = get(); 769→ if (!s.autoDecode) return; 770→ const now = Date.now(); 771→ // 星图「自动校准」减少自动解码周期 772→ const cm = constellationBonuses(s.constellation ?? []); 773→ // v0.7 智慧:自动解码周期 -X% 774→ const am = getAllBonuses(s.attributes ?? {}); 775→ const baseInterval = 12000 + cm.autoDecodeIntervalDeltaSec * 1000; 776→ // v0.8.2 棱镜星潮:自动解码周期 ×0.7(-30%) 777→ const tideModEarly = getTideModifiers(s.activeTide); 778→ const tideIntervalMult = tideModEarly.autoDecodeIntervalMult ?? 1; 779→ const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult * tideIntervalMult); 780→ if (now - s._lastAutoDecode < interval) return; 781→ // 找一颗 T1 晶体自动解码 782→ const idx = s.pendingCrystals.findIndex((c) => c.tier === 1); 783→ if (idx < 0) return; 784→ const crystal = s.pendingCrystals[idx]; 785→ const tideMod = getTideModifiers(s.activeTide); 786→ const base = decodeRewards(1, s.insightMult, s.contactRateMult); 787→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; 788→ const rewards = { 789→ crystals: Math.round(base.crystals * finalMult), 790→ insights: Math.round(base.insights * finalMult), 791→ contact: +(base.contact * finalMult).toFixed(2), 792→ }; 793→ const newTotal = s.totalDecoded + 1; 794→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; 795→ checkFragments(tentative); 796→ // v0.7 角色属性:自动解码给智慧经验(少量) 797→ const curAttrs = s.attributes ?? createInitialAttributes(); 798→ const curProg = s.attributeProgress ?? createInitialAttributeProgress(); 799→ const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom }; 800→ const wisdomLvl = levelUpCheck( 801→ { exp: progEntry.exp + 1, level: curAttrs.wisdom }, 802→ ATTRIBUTE_HARD_CAP 803→ ); 804→ const newAttributes: CharacterAttributes = { 805→ ...curAttrs, 806→ wisdom: wisdomLvl.newProgress.level, 807→ }; 808→ const newProgress: AttributeProgress = { 809→ ...curProg, 810→ wisdom: wisdomLvl.newProgress, 811→ }; 812→ set({ 813→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystal.id), 814→ crystals: s.crystals + rewards.crystals, 815→ insights: s.insights + rewards.insights, 816→ contact: Math.min(100, s.contact + rewards.contact), 817→ totalDecoded: newTotal, 818→ fragments: tentative.fragments, 819→ _lastAutoDecode: now, 820→ attributes: newAttributes, 821→ attributeProgress: newProgress, 822→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 823→ }); 824→ // 深空信标:自动解码也算进度 825→ trackBeacon("decode", 1); 826→ trackBeacon("insight", rewards.insights); 827→ }, 828→ 829→ buyTech: (techId) => { 830→ const s = get(); 831→ const node = TECH_TREE.find((t) => t.id === techId); 832→ if (!node) return false; 833→ const cur = s.tech[techId] ?? 0; 834→ if (cur >= 1) return false; // v0.1 每节点 1 级 835→ if (s.insights < node.cost) return false; 836→ const newTech = { ...s.tech, [techId]: 1 }; 837→ set({ 838→ insights: s.insights - node.cost, 839→ tech: newTech, 840→ ...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }), 841→ }); 842→ return true; 843→ }, 844→ 845→ // ============ 探险系统 ============ 846→ startExpedition: () => { 847→ const s = get(); 848→ if (s.activeExpedition && !s.activeExpedition.finished) { 849→ return { ok: false, reason: "已有进行中的探险" }; 850→ } 851→ if (s.energy < EXPEDITION_CONFIG.energyCost) { 852→ return { ok: false, reason: "能量不足" }; 853→ } 854→ const tideMod = getTideModifiers(s.activeTide); 855→ // v0.7 角色属性:探索力 +X% 探险力,勇气 +X 探险生命 856→ const am = getAllBonuses(s.attributes ?? {}); 857→ const basePower = computeExpeditionPower(s) + tideMod.expeditionPowerBonus; 858→ const baseHp = computeExpeditionHp(s) + tideMod.expeditionHpBonus; 859→ const power = Math.round(basePower * am.expeditionPowerMult); 860→ const hp = baseHp + am.expeditionHpBonus; 861→ const seed = Math.floor(Math.random() * 1e9); 862→ const expedition = generateExpedition(seed, power, hp); 863→ set({ 864→ activeExpedition: expedition, 865→ energy: s.energy - EXPEDITION_CONFIG.energyCost, 866→ totalExpeditions: s.totalExpeditions + 1, 867→ }); 868→ return { ok: true }; 869→ }, 870→ 871→ resolveCurrentNode: () => { 872→ const s = get(); 873→ if (!s.activeExpedition || s.activeExpedition.finished) return null; 874→ // 深拷贝 875→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); 876→ // v0.7 勇气:BOSS 战胜率 +X%(动态提高 RNG 阈值) 877→ // v0.8.2 蚀相星潮:BOSS 胜率 +20% 878→ const nodeBefore = exp.nodes[exp.currentNode]; 879→ const isBossNode = nodeBefore?.type === "boss"; 880→ const am = getAllBonuses(s.attributes ?? {}); 881→ const tideMod = getTideModifiers(s.activeTide); 882→ const tideBossBonus = tideMod.bossWinRateBonus ?? 0; 883→ const result = isBossNode 884→ ? resolveNode(exp, () => { 885→ // 单次 rng() 调用:B% 概率返回 0(必胜),其余情况返回 r-B(保持均匀分布) 886→ const r = Math.random(); 887→ const b = Math.min(0.95, am.bossWinRateBonus + tideBossBonus); 888→ return r < b ? 0 : Math.min(1, r - b); 889→ }) 890→ : resolveNode(exp); 891→ // 累计奖励 892→ if (result.crystals) exp.rewards.crystals += result.crystals; 893→ if (result.insights) exp.rewards.insights += result.insights; 894→ if (result.contact) exp.rewards.contact += result.contact; 895→ if (result.fragments) exp.rewards.fragments.push(...result.fragments); 896→ if (result.hpDelta) exp.hp = Math.max(0, Math.min(exp.maxHp, exp.hp + result.hpDelta)); 897→ // 实时入账(玩家立即获得) 898→ const newCrystals = s.crystals + (result.crystals || 0); 899→ const newInsights = s.insights + (result.insights || 0); 900→ const newContact = Math.min(100, s.contact + (result.contact || 0)); 901→ // 碎片解锁 902→ const newFragments = { ...s.fragments }; 903→ if (result.fragments) { 904→ for (const fid of result.fragments) newFragments[fid] = true; 905→ } 906→ // 日志 907→ const logEntry = { 908→ expeditionId: exp.id, 909→ nodeType: nodeBefore?.type || "combat", 910→ result: result.log, 911→ rewards: [ 912→ result.crystals ? `+${result.crystals}晶体` : "", 913→ result.insights ? `+${result.insights}洞见` : "", 914→ result.contact ? `+${result.contact.toFixed(1)}接触` : "", 915→ result.hpDelta ? `${result.hpDelta > 0 ? "+" : ""}${result.hpDelta}生命` : "", 916→ ].filter(Boolean).join(" "), 917→ timestamp: Date.now(), 918→ }; 919→ const newLog = [logEntry, ...s.expeditionLog].slice(0, 30); 920→ 921→ if (result.ended) { 922→ // 探险结束(胜利或失败) 923→ exp.finished = true; 924→ } 925→ 926→ // v0.4 编年史:击破 BOSS 时累计计数 927→ let bossKills = s.bossKills ?? 0; 928→ let bossKilledThisNode = false; 929→ if ( 930→ result.ended && 931→ result.endReason === "victory" && 932→ nodeBefore?.type === "boss" 933→ ) { 934→ bossKills = bossKills + 1; 935→ bossKilledThisNode = true; 936→ } 937→ 938→ // v0.7 角色属性:战斗胜利给勇气+探索力经验;BOSS 额外奖励 939→ let newAttributes = s.attributes ?? createInitialAttributes(); 940→ let newProgress = s.attributeProgress ?? createInitialAttributeProgress(); 941→ let statsNeedResync = false; 942→ // 战斗类节点(combat/boss)且胜利:勇气 + 探索力经验 943→ const isCombatVictory = 944→ (nodeBefore?.type === "combat" || nodeBefore?.type === "boss") && 945→ !result.ended; // 中途战斗胜利(未结束探险) 946→ const isExpeditionVictory = 947→ result.ended && result.endReason === "victory"; 948→ if (isCombatVictory || bossKilledThisNode || isExpeditionVictory) { 949→ const courageGain = bossKilledThisNode ? 8 : 2; 950→ const explorationGain = bossKilledThisNode ? 6 : isExpeditionVictory ? 4 : 1; 951→ const courageLvl = levelUpCheck( 952→ { exp: newProgress.courage.exp + courageGain, level: newAttributes.courage }, 953→ ATTRIBUTE_HARD_CAP 954→ ); 955→ const explLvl = levelUpCheck( 956→ { exp: newProgress.exploration.exp + explorationGain, level: newAttributes.exploration }, 957→ ATTRIBUTE_HARD_CAP 958→ ); 959→ newAttributes = { 960→ ...newAttributes, 961→ courage: courageLvl.newProgress.level, 962→ exploration: explLvl.newProgress.level, 963→ }; 964→ newProgress = { 965→ ...newProgress, 966→ courage: courageLvl.newProgress, 967→ exploration: explLvl.newProgress, 968→ }; 969→ statsNeedResync = true; 970→ } 971→ 972→ set({ 973→ activeExpedition: exp, 974→ crystals: newCrystals, 975→ insights: newInsights, 976→ contact: newContact, 977→ fragments: newFragments, 978→ expeditionLog: newLog, 979→ bossKills, 980→ attributes: newAttributes, 981→ attributeProgress: newProgress, 982→ ...(statsNeedResync 983→ ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }) 984→ : {}), 985→ }); 986→ // 深空信标:探险完成(胜负都计)+ 洞见累计 + BOSS 击破 987→ if (result.ended) { 988→ trackBeacon("expedition", 1); 989→ if (bossKilledThisNode) { 990→ trackBeacon("boss", 1); 991→ } 992→ } 993→ if (result.insights) trackBeacon("insight", result.insights); 994→ return result; 995→ }, 996→ 997→ advanceNode: () => { 998→ const s = get(); 999→ if (!s.activeExpedition || s.activeExpedition.finished) return; 1000→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); 1001→ const node = exp.nodes[exp.currentNode]; 1002→ if (!node || !node.cleared) return; // 当前节点未结算不能前进 1003→ if (exp.currentNode >= exp.nodes.length - 1) return; 1004→ exp.currentNode++; 1005→ set({ activeExpedition: exp }); 1006→ }, 1007→ 1008→ abortExpedition: () => { 1009→ const s = get(); 1010→ if (!s.activeExpedition) return; 1011→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); 1012→ exp.finished = true; 1013→ const logEntry = { 1014→ expeditionId: exp.id, 1015→ nodeType: "rest" as const, 1016→ result: "探险队主动撤退,保留已获奖励。", 1017→ rewards: "", 1018→ timestamp: Date.now(), 1019→ }; 1020→ set({ 1021→ activeExpedition: exp, 1022→ expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30), 1023→ }); 1024→ }, 1025→ 1026→ doPrestige: () => { 1027→ const s = get(); 1028→ if (s.contact < CONTACT.prestigeMin) return null; 1029→ const newBp = computeNewBlueprints(s); 1030→ const next = performPrestige(s); 1031→ // 星图「能量共振」提升上限 1032→ const cm = constellationBonuses(next.constellation ?? []); 1033→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; 1034→ set({ 1035→ ...next, 1036→ energyMax, 1037→ ...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation, attributes: next.attributes }), 1038→ _lastAutoDecode: Date.now(), 1039→ _lastSpawn: Date.now(), 1040→ _combo: 0, 1041→ _lastPulse: 0, 1042→ _tideEvents: [], 1043→ }); 1044→ return { newBp }; 1045→ }, 1046→ 1047→ chooseConstellationPerk: (perkId) => { 1048→ const s = get(); 1049→ if (!s.pendingPerkChoices || !s.pendingPerkChoices.includes(perkId)) return false; 1050→ const perk = getPerk(perkId); 1051→ if (!perk) return false; 1052→ if (s.constellation?.includes(perkId)) return false; 1053→ const newConstellation = [...(s.constellation ?? []), perkId]; 1054→ const cm = constellationBonuses(newConstellation); 1055→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; 1056→ // v0.4 编年史:回填最近一条 entry 的 perksThisAscension 1057→ const chronicle = s.chronicle ?? []; 1058→ let newChronicle = chronicle; 1059→ if (chronicle.length > 0) { 1060→ const lastEntry = chronicle[chronicle.length - 1]; 1061→ const updatedLast = withPerks(lastEntry, [perkId]); 1062→ newChronicle = [...chronicle.slice(0, -1), updatedLast]; 1063→ } 1064→ set({ 1065→ constellation: newConstellation, 1066→ pendingPerkChoices: null, 1067→ energyMax, 1068→ chronicle: newChronicle, 1069→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation, attributes: s.attributes }), 1070→ }); 1071→ return true; 1072→ }, 1073→ 1074→ rerollPerkChoices: () => { 1075→ const s = get(); 1076→ if (!s.pendingPerkChoices) return; 1077→ const choices = rollPerkChoices(s.constellation ?? []); 1078→ if (choices.length > 0) set({ pendingPerkChoices: choices }); 1079→ }, 1080→ 1081→ checkAchievements: () => { 1082→ const s = get(); 1083→ const newlyUnlocked: Achievement[] = []; 1084→ const updated = { ...s.achievements }; 1085→ let crystals = s.crystals; 1086→ let insights = s.insights; 1087→ let contact = s.contact; 1088→ let statsDirty = false; 1089→ for (const a of ACHIEVEMENTS) { 1090→ if (updated[a.id]) continue; 1091→ if (a.check(s)) { 1092→ updated[a.id] = true; 1093→ newlyUnlocked.push(a); 1094→ // 发放即时奖励 1095→ if (a.reward.crystals) crystals += a.reward.crystals; 1096→ if (a.reward.insights) insights += a.reward.insights; 1097→ if (a.reward.contact) contact = Math.min(100, contact + a.reward.contact); 1098→ if (a.reward.crystalsPerSecPct || a.reward.insightPct) statsDirty = true; 1099→ } 1100→ } 1101→ if (newlyUnlocked.length === 0) return []; 1102→ set({ 1103→ achievements: updated, 1104→ crystals, 1105→ insights, 1106→ contact, 1107→ ...(statsDirty 1108→ ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }) 1109→ : {}), 1110→ _achievementQueue: [...s._achievementQueue, ...newlyUnlocked], 1111→ }); 1112→ return newlyUnlocked; 1113→ }, 1114→ 1115→ consumeAchievementQueue: () => { 1116→ const s = get(); 1117→ if (s._achievementQueue.length === 0) return []; 1118→ const items = s._achievementQueue; 1119→ set({ _achievementQueue: [] }); 1120→ return items; 1121→ }, 1122→ 1123→ canPrestige: () => get().contact >= CONTACT.prestigeMin, 1124→ 1125→ toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }), 1126→ toggleSound: () => set({ soundOn: !get().soundOn }), 1127→ 1128→ // 深空信标:发放每日挑战奖励(v0.5) 1129→ grantBeaconReward: (insights, contact) => { 1130→ const s = get(); 1131→ set({ 1132→ insights: s.insights + Math.round(insights), 1133→ contact: Math.min(100, s.contact + contact), 1134→ }); 1135→ }, 1136→ 1137→ // 深空信标:领取周挑战奖励(v0.8) 1138→ claimWeeklyBeacon: () => { 1139→ const challenge = generateWeeklyChallenge(); 1140→ const progress = loadWeeklyProgress(); 1141→ const res = claimWeeklyReward(challenge, progress); 1142→ if (res.rewardInsight > 0 || res.rewardContact > 0) { 1143→ const s = get(); 1144→ set({ 1145→ insights: s.insights + Math.round(res.rewardInsight), 1146→ contact: Math.min(100, s.contact + res.rewardContact), 1147→ }); 1148→ // 异步提交到云排行榜(失败静默,不影响本地) 1149→ if (res.entry) { 1150→ void submitCloudScore(res.entry); 1151→ } 1152→ } 1153→ return { 1154→ rewardInsight: res.rewardInsight, 1155→ rewardContact: res.rewardContact, 1156→ score: res.score, 1157→ }; 1158→ }, 1159→ 1160→ // 深空信标:领取限时挑战奖励(v0.8.2) 1161→ claimTimedBeacon: () => { 1162→ const challenge = generateTimedChallenge(); 1163→ const progress = loadTimedProgress(); 1164→ const res = claimTimedReward(challenge, progress); 1165→ if (res.rewardInsight > 0 || res.rewardContact > 0) { 1166→ const s = get(); 1167→ set({ 1168→ insights: s.insights + Math.round(res.rewardInsight), 1169→ contact: Math.min(100, s.contact + res.rewardContact), 1170→ }); 1171→ // 异步提交到云排行榜(失败静默,不影响本地) 1172→ if (res.entry) { 1173→ void submitCloudScore(res.entry); 1174→ } 1175→ } 1176→ return { 1177→ rewardInsight: res.rewardInsight, 1178→ rewardContact: res.rewardContact, 1179→ score: res.score, 1180→ }; 1181→ }, 1182→ 1183→ // 深空信标:领取信标链里程碑奖励(v0.8) 1184→ claimChainReward: (milestone) => { 1185→ // loadChainState 仅用于前置校验,真正的状态修改由 claimChainMilestone 完成 1186→ const pre = loadChainState(); 1187→ if ( 1188→ pre.currentStreak < milestone || 1189→ pre.milestonesClaimed.includes(milestone) 1190→ ) { 1191→ return { 1192→ rewardInsight: 0, 1193→ rewardContact: 0, 1194→ label: "", 1195→ ok: false, 1196→ }; 1197→ } 1198→ const res = claimChainMilestone(milestone); 1199→ if (res.rewardInsight > 0 || res.rewardContact > 0) { 1200→ const s = get(); 1201→ set({ 1202→ insights: s.insights + Math.round(res.rewardInsight), 1203→ contact: Math.min(100, s.contact + res.rewardContact), 1204→ }); 1205→ } 1206→ return { 1207→ rewardInsight: res.rewardInsight, 1208→ rewardContact: res.rewardContact, 1209→ label: res.label, 1210→ ok: res.rewardInsight > 0 || res.rewardContact > 0, 1211→ }; 1212→ }, 1213→ 1214→ // 深空巡航:发放实时玩法奖励(v0.6) 1215→ grantCruiseReward: (rewards) => { 1216→ const s = get(); 1217→ const addCrystals = rewards.crystals ?? 0; 1218→ const addInsights = rewards.insights ?? 0; 1219→ const addContact = rewards.contact ?? 0; 1220→ // v0.7 角色属性:巡航通关给探索力+勇气经验(按晶体奖励量缩放) 1221→ const totalReward = addCrystals + addInsights * 10 + addContact * 10; 1222→ const expBase = Math.max(2, Math.floor(totalReward / 30)); 1223→ const curAttrs = s.attributes ?? createInitialAttributes(); 1224→ const curProg = s.attributeProgress ?? createInitialAttributeProgress(); 1225→ const explLvl = levelUpCheck( 1226→ { exp: curProg.exploration.exp + expBase, level: curAttrs.exploration }, 1227→ ATTRIBUTE_HARD_CAP 1228→ ); 1229→ const courageLvl = levelUpCheck( 1230→ { exp: curProg.courage.exp + Math.floor(expBase * 0.6), level: curAttrs.courage }, 1231→ ATTRIBUTE_HARD_CAP 1232→ ); 1233→ const newAttributes: CharacterAttributes = { 1234→ ...curAttrs, 1235→ exploration: explLvl.newProgress.level, 1236→ courage: courageLvl.newProgress.level, 1237→ }; 1238→ const newProgress: AttributeProgress = { 1239→ ...curProg, 1240→ exploration: explLvl.newProgress, 1241→ courage: courageLvl.newProgress, 1242→ }; 1243→ set({ 1244→ crystals: Math.min(s.crystalCap, s.crystals + addCrystals), 1245→ insights: s.insights + Math.round(addInsights), 1246→ contact: Math.min(100, s.contact + addContact), 1247→ attributes: newAttributes, 1248→ attributeProgress: newProgress, 1249→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 1250→ }); 1251→ }, 1252→ 1253→ // ============ 角色属性系统(v0.7 P1) ============ 1254→ allocateAttribute: (attr, points = 1) => { 1255→ const s = get(); 1256→ const cur = s.attributes ?? createInitialAttributes(); 1257→ const curVal = cur[attr] ?? 0; 1258→ if (curVal >= ATTRIBUTE_HARD_CAP) { 1259→ return { ok: false, leveledUp: 0 }; 1260→ } 1261→ if ((s.pendingAttrPoints ?? 0) < points) { 1262→ return { ok: false, leveledUp: 0 }; 1263→ } 1264→ const alloc = Math.min(points, ATTRIBUTE_HARD_CAP - curVal, s.pendingAttrPoints); 1265→ const newVal = curVal + alloc; 1266→ const newAttributes: CharacterAttributes = { ...cur, [attr]: newVal }; 1267→ // 同步经验进度 level 字段(保持一致) 1268→ const curProg = s.attributeProgress ?? createInitialAttributeProgress(); 1269→ const oldProg = curProg[attr] ?? { exp: 0, level: curVal }; 1270→ const newProgress: AttributeProgress = { 1271→ ...curProg, 1272→ [attr]: { exp: oldProg.exp, level: newVal }, 1273→ }; 1274→ set({ 1275→ attributes: newAttributes, 1276→ attributeProgress: newProgress, 1277→ pendingAttrPoints: s.pendingAttrPoints - alloc, 1278→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 1279→ }); 1280→ return { ok: true, leveledUp: alloc }; 1281→ }, 1282→ 1283→ gainAttributeExp: (attr, amount) => { 1284→ const s = get(); 1285→ const cur = s.attributes ?? createInitialAttributes(); 1286→ const curProg = s.attributeProgress ?? createInitialAttributeProgress(); 1287→ const progEntry = curProg[attr] ?? { exp: 0, level: cur[attr] }; 1288→ const result = levelUpCheck( 1289→ { exp: progEntry.exp + amount, level: cur[attr] }, 1290→ ATTRIBUTE_HARD_CAP 1291→ ); 1292→ const newAttributes: CharacterAttributes = { ...cur, [attr]: result.newProgress.level }; 1293→ const newProgress: AttributeProgress = { 1294→ ...curProg, 1295→ [attr]: result.newProgress, 1296→ }; 1297→ set({ 1298→ attributes: newAttributes, 1299→ attributeProgress: newProgress, 1300→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }), 1301→ }); 1302→ return { leveledUp: result.levelsGained, newLevel: result.newProgress.level }; 1303→ }, 1304→ }), 1305→ { 1306→ name: "echo-nexus-save-v1", 1307→ storage: createJSONStorage(() => localStorage), 1308→ // 不持久化临时字段 1309→ partialize: (s) => { 1310→ const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, globalTide, ...rest } = s; 1311→ void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents; void globalTide; 1312→ return rest as GameState; 1313→ }, 1314→ } 1315→ ) 1316→); 1317→ 1318→/** 选择器:未解锁碎片中下一个门槛 */ 1319→export function nextFragmentThreshold(totalDecoded: number): number | null { 1320→ for (const f of FRAGMENTS) { 1321→ if (totalDecoded < f.threshold) return f.threshold; 1322→ } 1323→ return null; 1324→} 1325→ 1326→export { FRAGMENTS, PRESTIGE, TECH_TREE }; 1327→ 1328→// 开发期调试:暴露 store 到 window,便于 QA 测试 1329→if (typeof window !== "undefined" && process.env.NODE_ENV !== "production") { 1330→ (window as unknown as { __gameStore?: typeof useGameStore }).__gameStore = useGameStore; 1331→} 1332→