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