diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid index 3857e0ee9..b10554094 100644 --- a/.zscripts/dev.pid +++ b/.zscripts/dev.pid @@ -1 +1 @@ -959 +3832 diff --git a/tool-results/read_1782247259300_abf0610082e8.txt b/tool-results/read_1782247259300_abf0610082e8.txt new file mode 100644 index 000000000..7b2a76441 --- /dev/null +++ b/tool-results/read_1782247259300_abf0610082e8.txt @@ -0,0 +1,843 @@ + 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→ + 71→interface GameActions { + 72→ // 生命周期 + 73→ init: () => void; + 74→ loadOnline: () => void; + 75→ hardReset: () => void; + 76→ + 77→ // 主循环 + 78→ tick: (now: number) => void; + 79→ pulse: () => { gain: number; combo: number } | null; + 80→ + 81→ // 星潮 + 82→ tickTide: (now: number) => { started?: TideType; ended?: TideType; silenceCompensation?: number } | null; + 83→ consumeTideEvents: () => { started?: TideType; ended?: TideType; silenceCompensation?: number }[]; + 84→ + 85→ // 解码 + 86→ startDecode: (crystalId: string) => void; + 87→ clickNode: (nodeId: number) => { ok: boolean; finished: boolean; solvable?: boolean; failReason?: string }; + 88→ undoStep: () => void; + 89→ retryPuzzle: () => void; + 90→ abandonPuzzle: () => void; + 91→ /** 自动解码 T1(技术解锁后由 tick 调用) */ + 92→ autoDecodeTick: () => void; + 93→ + 94→ // 探险 + 95→ startExpedition: () => { ok: boolean; reason?: string }; + 96→ resolveCurrentNode: () => ExpeditionResult | null; + 97→ advanceNode: () => void; + 98→ abortExpedition: () => void; + 99→ + 100→ // 技术 + 101→ buyTech: (techId: string) => boolean; + 102→ + 103→ // 飞升 + 104→ doPrestige: () => { newBp: number } | null; + 105→ + 106→ // 星图天文台 + 107→ chooseConstellationPerk: (perkId: string) => boolean; + 108→ rerollPerkChoices: () => void; + 109→ + 110→ // 成就 + 111→ checkAchievements: () => Achievement[]; + 112→ consumeAchievementQueue: () => Achievement[]; + 113→ + 114→ // 设置 + 115→ toggleTheme: () => void; + 116→ toggleSound: () => void; + 117→ + 118→ // 深空信标奖励发放(v0.5) + 119→ grantBeaconReward: (insights: number, contact: number) => void; + 120→ + 121→ // 深空巡航奖励发放(v0.6 — 实时 Canvas 玩法) + 122→ grantCruiseReward: (rewards: { crystals?: number; insights?: number; contact?: number }) => void; + 123→ + 124→ // 派生 + 125→ canPrestige: () => boolean; + 126→} + 127→ + 128→type Store = GameState & GameActions & { + 129→ _lastAutoDecode: number; + 130→ _lastSpawn: number; + 131→ _combo: number; + 132→ _lastPulse: number; + 133→ _achievementQueue: Achievement[]; + 134→ _tideEvents: { started?: TideType; ended?: TideType; silenceCompensation?: number }[]; + 135→}; + 136→ + 137→/** 计算并写回产能字段 */ + 138→function syncStats(state: Partial) { + 139→ const s = recomputeStats(state); + 140→ return { + 141→ crystalsPerSec: s.crystalsPerSec, + 142→ crystalCap: s.crystalCap, + 143→ pulsePower: s.pulsePower, + 144→ offlineEff: s.offlineEff, + 145→ insightMult: s.insightMult, + 146→ contactRateMult: s.contactRateMult, + 147→ autoDecode: s.autoDecode, + 148→ decodeStepsBonus: s.decodeStepsBonus, + 149→ }; + 150→} + 151→ + 152→/** + 153→ * 深空信标进度追踪(v0.5)。 + 154→ * 在游戏关键动作(脉冲/解码/探险/BOSS/洞见)后调用,按今日挑战类型增量更新进度。 + 155→ * 进度独立存储于 localStorage(echo-nexus-beacon-prog-v1),不污染 GameState。 + 156→ * @returns 若刚完成则返回 true(供 UI 触发通知) + 157→ */ + 158→function trackBeacon( + 159→ type: "pulse" | "decode" | "expedition" | "boss" | "insight", + 160→ delta: number + 161→): boolean { + 162→ if (typeof window === "undefined") return false; + 163→ try { + 164→ const challenge: BeaconDailyChallenge = generateDailyChallenge(); + 165→ if (challenge.type !== type) return false; + 166→ const current: BeaconDailyProgress = loadDailyProgress(); + 167→ if (current.completedAt !== null) return false; // 已完成不再累加 + 168→ const { justCompleted } = addBeaconProgress(current, challenge, delta); + 169→ return justCompleted; + 170→ } catch { + 171→ return false; + 172→ } + 173→} + 174→ + 175→/** 检查并解锁叙事碎片 */ + 176→function checkFragments(state: GameState): string[] { + 177→ const unlocked: string[] = []; + 178→ for (const f of FRAGMENTS) { + 179→ if (!state.fragments[f.id] && state.totalDecoded >= f.threshold) { + 180→ state.fragments[f.id] = true; + 181→ unlocked.push(f.id); + 182→ } + 183→ } + 184→ return unlocked; + 185→} + 186→ + 187→export const useGameStore = create()( + 188→ persist( + 189→ (set, get) => ({ + 190→ ...createInitialState(), + 191→ _lastAutoDecode: Date.now(), + 192→ _lastSpawn: Date.now(), + 193→ _combo: 0, + 194→ _lastPulse: 0, + 195→ _achievementQueue: [], + 196→ _tideEvents: [], + 197→ + 198→ init: () => { + 199→ const s = get(); + 200→ const now = Date.now(); + 201→ // 兼容旧存档:若存在不可解的活跃谜题(旧算法生成),直接清掉 + 202→ let activePuzzle = s.activePuzzle; + 203→ if (activePuzzle && !isSolvable(activePuzzle)) { + 204→ // 把晶体放回队列,避免玩家卡死 + 205→ const crystal: Crystal = { + 206→ id: `c_${now}_rec`, + 207→ tier: activePuzzle.tier, + 208→ value: CRYSTAL_VALUE[activePuzzle.tier].crystals, + 209→ createdAt: now, + 210→ }; + 211→ activePuzzle = null; + 212→ set({ + 213→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), + 214→ }); + 215→ } + 216→ // 兼容旧存档:补全 achievements / 星潮 / 星图 字段 + 217→ const achievements = s.achievements ?? {}; + 218→ const activeTide = s.activeTide ?? null; + 219→ const constellation = s.constellation ?? []; + 220→ const pendingPerkChoices = s.pendingPerkChoices ?? null; + 221→ // v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered + 222→ const migrated = migrateChronicleFields(s); + 223→ // 星图「能量共振」天赋 +1 能量上限 + 224→ const cm = constellationBonuses(constellation); + 225→ const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus; + 226→ // lastTideEnd:0 表示从未触发过星潮(首次游玩),保留 0 让 firstDelay 生效 + 227→ const lastTideEndRaw = s.lastTideEnd ?? 0; + 228→ // 若旧存档有已过期的星潮,清掉 + 229→ const tide = activeTide && activeTide.endsAt > now ? activeTide : null; + 230→ // 首次进入:补发离线收益 + 231→ const elapsed = Math.max(0, (now - s.lastTick) / 1000); + 232→ if (elapsed > 5) { + 233→ const cap = 8 * 3600; + 234→ const secs = Math.min(elapsed, cap); + 235→ const gain = s.crystalsPerSec * secs * s.offlineEff; + 236→ const crystalsBefore = s.crystals; + 237→ const crystalsAfter = Math.min(s.crystalCap, s.crystals + gain); + 238→ // 设置离线报告(v0.5.2):通知 UI 弹出"欢迎回来"对话框 + 239→ setPendingOfflineReport({ + 240→ elapsedSec: secs, + 241→ rawElapsedSec: elapsed, + 242→ gain: crystalsAfter - crystalsBefore, + 243→ rate: s.crystalsPerSec, + 244→ eff: s.offlineEff, + 245→ capped: elapsed > cap, + 246→ crystalsBefore, + 247→ crystalsAfter, + 248→ crystalCap: s.crystalCap, + 249→ }); + 250→ set({ + 251→ crystals: crystalsAfter, + 252→ lastTick: now, + 253→ activePuzzle, + 254→ achievements, + 255→ activeTide: tide, + 256→ lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, + 257→ constellation, + 258→ pendingPerkChoices, + 259→ energyMax, + 260→ chronicle: migrated.chronicle, + 261→ runStart: migrated.runStart, + 262→ bossKills: migrated.bossKills, + 263→ starTidesEncountered: migrated.starTidesEncountered, + 264→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }), + 265→ }); + 266→ } else { + 267→ 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, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }) }); + 268→ } + 269→ }, + 270→ + 271→ loadOnline: () => { + 272→ const s = get(); + 273→ set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }) }); + 274→ }, + 275→ + 276→ hardReset: () => { + 277→ set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [] }); + 278→ }, + 279→ + 280→ tickTide: (now) => { + 281→ const s = get(); + 282→ const tide = s.activeTide; + 283→ // 星图「星潮引导」减少间隙 + 284→ const cm = constellationBonuses(s.constellation ?? []); + 285→ const gap = Math.max(15000, TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000); + 286→ // 1) 检查当前星潮是否结束 + 287→ if (tide && now >= tide.endsAt) { + 288→ const endedType = tide.type; + 289→ // 寂静期补偿洞见 + 290→ let silenceCompensation = 0; + 291→ if (tide.type === "silence") { + 292→ silenceCompensation = computeSilenceCompensation(tide); + 293→ } + 294→ const newInsights = s.insights + silenceCompensation; + 295→ const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { ended: endedType }; + 296→ if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; + 297→ set({ + 298→ activeTide: null, + 299→ lastTideEnd: now, + 300→ insights: newInsights, + 301→ // 星潮结束后重算 stats(移除 contactRate/insight 修饰) + 302→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation }), + 303→ _tideEvents: [...s._tideEvents, event], + 304→ }); + 305→ return event; + 306→ } + 307→ // 2) 检查是否该触发新星潮(间隙已过) + 308→ if (!tide) { + 309→ const since = now - s.lastTideEnd; + 310→ // 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap + 311→ const firstStart = s.lastTideEnd === 0 || s.lastTideEnd <= s.createdAt + 1000; + 312→ const need = firstStart ? TIDE_CONFIG.firstDelay : gap; + 313→ if (since >= need) { + 314→ const type = rollTide(); + 315→ const newTide: StarTide = { + 316→ id: `tide_${now}_${Math.random().toString(36).slice(2, 7)}`, + 317→ type, + 318→ startedAt: now, + 319→ endsAt: now + TIDE_CONFIG.duration, + 320→ }; + 321→ const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { started: type }; + 322→ // v0.4 编年史:累计遇到的星潮 ID(去重) + 323→ const tidesAll = s.starTidesEncountered ?? []; + 324→ const tideId = `tide_${type}`; + 325→ const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; + 326→ set({ + 327→ activeTide: newTide, + 328→ starTidesEncountered: newTidesAll, + 329→ // 星潮开始后重算 stats(应用 contactRate/insight 修饰) + 330→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation }), + 331→ _tideEvents: [...s._tideEvents, event], + 332→ }); + 333→ return event; + 334→ } + 335→ } + 336→ return null; + 337→ }, + 338→ + 339→ consumeTideEvents: () => { + 340→ const s = get(); + 341→ if (s._tideEvents.length === 0) return []; + 342→ const items = s._tideEvents; + 343→ set({ _tideEvents: [] }); + 344→ return items; + 345→ }, + 346→ + 347→ tick: (now) => { + 348→ const s = get(); + 349→ const dt = Math.max(0, (now - s.lastTick) / 1000); + 350→ if (dt <= 0) return; + 351→ + 352→ // 星潮产能修饰(即时乘) + 353→ const tideMod = getTideModifiers(s.activeTide); + 354→ const effCps = s.crystalsPerSec * tideMod.crystalsPerSecMult; + 355→ // 产能累加(仅闲置产能受仓库上限;探险奖励可超出) + 356→ const newCrystals = + 357→ s.crystals >= s.crystalCap + 358→ ? s.crystals // 已达/超上限,不再自动产出 + 359→ : Math.min(s.crystalCap, s.crystals + effCps * dt); + 360→ + 361→ // 自动产出待解码晶体(每 baseInterval 秒一颗,受技术/飞升略提速) + 362→ const bpBoost = 1 + s.blueprints.length * 0.03; + 363→ const spawnInterval = (CRYSTAL_SPAWN.baseIntervalSec / bpBoost) * 1000; + 364→ let pending = s.pendingCrystals; + 365→ let lastSpawn = s._lastSpawn; + 366→ if ( + 367→ now - lastSpawn > spawnInterval && + 368→ pending.length < CRYSTAL_SPAWN.maxPending + 369→ ) { + 370→ // 星图「晶体富集」提升 T2/T3 概率 + 371→ const cm = constellationBonuses(s.constellation ?? []); + 372→ const tier = rollCrystalTierWithBonus(cm.t2t3BonusRate); + 373→ const crystal: Crystal = { + 374→ id: `c_${now}_${Math.random().toString(36).slice(2, 7)}`, + 375→ tier, + 376→ value: CRYSTAL_VALUE[tier].crystals, + 377→ createdAt: now, + 378→ }; + 379→ pending = [...pending, crystal]; + 380→ lastSpawn = now; + 381→ } + 382→ + 383→ // 能量恢复(探险系统) + 384→ let energy = s.energy; + 385→ let lastEnergyTick = s.lastEnergyTick; + 386→ if (energy < s.energyMax) { + 387→ const regen = computeEnergyRegen(lastEnergyTick, now, energy, s.energyMax); + 388→ energy = regen.energy; + 389→ lastEnergyTick = regen.lastTick; + 390→ } else { + 391→ lastEnergyTick = now; + 392→ } + 393→ + 394→ set({ + 395→ crystals: newCrystals, + 396→ lastTick: now, + 397→ pendingCrystals: pending, + 398→ _lastSpawn: lastSpawn, + 399→ energy, + 400→ lastEnergyTick, + 401→ }); + 402→ }, + 403→ + 404→ pulse: () => { + 405→ const s = get(); + 406→ const now = Date.now(); + 407→ // 连击 + 408→ let combo = 1; + 409→ if (now - s._lastPulse < 1500) { + 410→ combo = Math.min(10, s._combo + 1); + 411→ } + 412→ const mult = 1 + (combo - 1) * 0.15; + 413→ // 星潮脉冲威力修饰 + 414→ const tideMod = getTideModifiers(s.activeTide); + 415→ const gain = s.pulsePower * mult * tideMod.pulsePowerMult; + 416→ set({ + 417→ crystals: Math.min(s.crystalCap, s.crystals + gain), + 418→ _combo: combo, + 419→ _lastPulse: now, + 420→ }); + 421→ // 深空信标:脉冲任务进度 +1 + 422→ trackBeacon("pulse", 1); + 423→ return { gain, combo }; + 424→ }, + 425→ + 426→ startDecode: (crystalId) => { + 427→ const s = get(); + 428→ const crystal = s.pendingCrystals.find((c) => c.id === crystalId); + 429→ if (!crystal) return; + 430→ const puzzle = generatePuzzle(crystal.tier); + 431→ set({ + 432→ activePuzzle: puzzle, + 433→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystalId), + 434→ }); + 435→ }, + 436→ + 437→ clickNode: (nodeId) => { + 438→ const s = get(); + 439→ if (!s.activePuzzle) return { ok: false, finished: false, failReason: "无活跃谜题" }; + 440→ // 深拷贝谜题 + 441→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); + 442→ const res = tryClickNode(puzzle, nodeId); + 443→ if (res.ok) { + 444→ if (res.finished) { + 445→ // 结算奖励(星潮解码奖励修饰 + 星图「二周目经验」) + 446→ const tideMod = getTideModifiers(s.activeTide); + 447→ const cm = constellationBonuses(s.constellation ?? []); + 448→ const base = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult); + 449→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; + 450→ const rewards = { + 451→ crystals: Math.round(base.crystals * finalMult), + 452→ insights: Math.round(base.insights * finalMult), + 453→ contact: +(base.contact * finalMult).toFixed(2), + 454→ }; + 455→ const newTotal = s.totalDecoded + 1; + 456→ const newContact = Math.min(100, s.contact + rewards.contact); + 457→ const newInsights = s.insights + rewards.insights; + 458→ const newCrystals = s.crystals + rewards.crystals; + 459→ // 解锁碎片 + 460→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; + 461→ const unlocked = checkFragments(tentative); + 462→ set({ + 463→ activePuzzle: null, + 464→ crystals: newCrystals, + 465→ insights: newInsights, + 466→ contact: newContact, + 467→ totalDecoded: newTotal, + 468→ fragments: tentative.fragments, + 469→ }); + 470→ // 深空信标:解码 +1,洞见累计 + 471→ trackBeacon("decode", 1); + 472→ trackBeacon("insight", rewards.insights); + 473→ return { ok: true, finished: true, failReason: unlocked.join(",") || undefined }; + 474→ } + 475→ // 点击成功但未完成:检测当前局面是否仍可解 + 476→ const solvable = isSolvable(puzzle); + 477→ set({ activePuzzle: puzzle }); + 478→ return { ok: true, finished: false, solvable }; + 479→ } + 480→ return res; + 481→ }, + 482→ + 483→ undoStep: () => { + 484→ const s = get(); + 485→ if (!s.activePuzzle || s.activePuzzle.path.length === 0) return; + 486→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); + 487→ const lastId = puzzle.path.pop(); + 488→ if (lastId !== undefined) { + 489→ const node = puzzle.grid.find((n) => n.id === lastId); + 490→ if (node) node.used = false; + 491→ } + 492→ set({ activePuzzle: puzzle }); + 493→ }, + 494→ + 495→ retryPuzzle: () => { + 496→ const s = get(); + 497→ if (!s.activePuzzle) return; + 498→ set({ activePuzzle: resetPuz(s.activePuzzle) }); + 499→ }, + 500→ + 501→ abandonPuzzle: () => { + 502→ const s = get(); + 503→ if (!s.activePuzzle) return; + 504→ // 晶体放回队列末尾 + 505→ const crystal: Crystal = { + 506→ id: `c_${Date.now()}_ret`, + 507→ tier: s.activePuzzle.tier, + 508→ value: CRYSTAL_VALUE[s.activePuzzle.tier].crystals, + 509→ createdAt: Date.now(), + 510→ }; + 511→ set({ + 512→ activePuzzle: null, + 513→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), + 514→ }); + 515→ }, + 516→ + 517→ autoDecodeTick: () => { + 518→ const s = get(); + 519→ if (!s.autoDecode) return; + 520→ const now = Date.now(); + 521→ // 星图「自动校准」减少自动解码周期 + 522→ const cm = constellationBonuses(s.constellation ?? []); + 523→ const interval = Math.max(5000, 12000 + cm.autoDecodeIntervalDeltaSec * 1000); + 524→ if (now - s._lastAutoDecode < interval) return; + 525→ // 找一颗 T1 晶体自动解码 + 526→ const idx = s.pendingCrystals.findIndex((c) => c.tier === 1); + 527→ if (idx < 0) return; + 528→ const crystal = s.pendingCrystals[idx]; + 529→ const tideMod = getTideModifiers(s.activeTide); + 530→ const base = decodeRewards(1, s.insightMult, s.contactRateMult); + 531→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; + 532→ const rewards = { + 533→ crystals: Math.round(base.crystals * finalMult), + 534→ insights: Math.round(base.insights * finalMult), + 535→ contact: +(base.contact * finalMult).toFixed(2), + 536→ }; + 537→ const newTotal = s.totalDecoded + 1; + 538→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; + 539→ checkFragments(tentative); + 540→ set({ + 541→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystal.id), + 542→ crystals: s.crystals + rewards.crystals, + 543→ insights: s.insights + rewards.insights, + 544→ contact: Math.min(100, s.contact + rewards.contact), + 545→ totalDecoded: newTotal, + 546→ fragments: tentative.fragments, + 547→ _lastAutoDecode: now, + 548→ }); + 549→ // 深空信标:自动解码也算进度 + 550→ trackBeacon("decode", 1); + 551→ trackBeacon("insight", rewards.insights); + 552→ }, + 553→ + 554→ buyTech: (techId) => { + 555→ const s = get(); + 556→ const node = TECH_TREE.find((t) => t.id === techId); + 557→ if (!node) return false; + 558→ const cur = s.tech[techId] ?? 0; + 559→ if (cur >= 1) return false; // v0.1 每节点 1 级 + 560→ if (s.insights < node.cost) return false; + 561→ const newTech = { ...s.tech, [techId]: 1 }; + 562→ set({ + 563→ insights: s.insights - node.cost, + 564→ tech: newTech, + 565→ ...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }), + 566→ }); + 567→ return true; + 568→ }, + 569→ + 570→ // ============ 探险系统 ============ + 571→ startExpedition: () => { + 572→ const s = get(); + 573→ if (s.activeExpedition && !s.activeExpedition.finished) { + 574→ return { ok: false, reason: "已有进行中的探险" }; + 575→ } + 576→ if (s.energy < EXPEDITION_CONFIG.energyCost) { + 577→ return { ok: false, reason: "能量不足" }; + 578→ } + 579→ const tideMod = getTideModifiers(s.activeTide); + 580→ const power = computeExpeditionPower(s) + tideMod.expeditionPowerBonus; + 581→ const hp = computeExpeditionHp(s) + tideMod.expeditionHpBonus; + 582→ const seed = Math.floor(Math.random() * 1e9); + 583→ const expedition = generateExpedition(seed, power, hp); + 584→ set({ + 585→ activeExpedition: expedition, + 586→ energy: s.energy - EXPEDITION_CONFIG.energyCost, + 587→ totalExpeditions: s.totalExpeditions + 1, + 588→ }); + 589→ return { ok: true }; + 590→ }, + 591→ + 592→ resolveCurrentNode: () => { + 593→ const s = get(); + 594→ if (!s.activeExpedition || s.activeExpedition.finished) return null; + 595→ // 深拷贝 + 596→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 597→ const result = resolveNode(exp); + 598→ // 累计奖励 + 599→ if (result.crystals) exp.rewards.crystals += result.crystals; + 600→ if (result.insights) exp.rewards.insights += result.insights; + 601→ if (result.contact) exp.rewards.contact += result.contact; + 602→ if (result.fragments) exp.rewards.fragments.push(...result.fragments); + 603→ if (result.hpDelta) exp.hp = Math.max(0, Math.min(exp.maxHp, exp.hp + result.hpDelta)); + 604→ // 实时入账(玩家立即获得) + 605→ const newCrystals = s.crystals + (result.crystals || 0); + 606→ const newInsights = s.insights + (result.insights || 0); + 607→ const newContact = Math.min(100, s.contact + (result.contact || 0)); + 608→ // 碎片解锁 + 609→ const newFragments = { ...s.fragments }; + 610→ if (result.fragments) { + 611→ for (const fid of result.fragments) newFragments[fid] = true; + 612→ } + 613→ // 日志 + 614→ const logEntry = { + 615→ expeditionId: exp.id, + 616→ nodeType: exp.nodes[exp.currentNode]?.type || "combat", + 617→ result: result.log, + 618→ rewards: [ + 619→ result.crystals ? `+${result.crystals}晶体` : "", + 620→ result.insights ? `+${result.insights}洞见` : "", + 621→ result.contact ? `+${result.contact.toFixed(1)}接触` : "", + 622→ result.hpDelta ? `${result.hpDelta > 0 ? "+" : ""}${result.hpDelta}生命` : "", + 623→ ].filter(Boolean).join(" "), + 624→ timestamp: Date.now(), + 625→ }; + 626→ const newLog = [logEntry, ...s.expeditionLog].slice(0, 30); + 627→ + 628→ if (result.ended) { + 629→ // 探险结束(胜利或失败) + 630→ exp.finished = true; + 631→ } + 632→ + 633→ // v0.4 编年史:击破 BOSS 时累计计数 + 634→ let bossKills = s.bossKills ?? 0; + 635→ if ( + 636→ result.ended && + 637→ result.endReason === "victory" && + 638→ exp.nodes[exp.currentNode]?.type === "boss" + 639→ ) { + 640→ bossKills = bossKills + 1; + 641→ } + 642→ + 643→ set({ + 644→ activeExpedition: exp, + 645→ crystals: newCrystals, + 646→ insights: newInsights, + 647→ contact: newContact, + 648→ fragments: newFragments, + 649→ expeditionLog: newLog, + 650→ bossKills, + 651→ }); + 652→ // 深空信标:探险完成(胜负都计)+ 洞见累计 + BOSS 击破 + 653→ if (result.ended) { + 654→ trackBeacon("expedition", 1); + 655→ if (result.endReason === "victory" && exp.nodes[exp.currentNode]?.type === "boss") { + 656→ trackBeacon("boss", 1); + 657→ } + 658→ } + 659→ if (result.insights) trackBeacon("insight", result.insights); + 660→ return result; + 661→ }, + 662→ + 663→ advanceNode: () => { + 664→ const s = get(); + 665→ if (!s.activeExpedition || s.activeExpedition.finished) return; + 666→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 667→ const node = exp.nodes[exp.currentNode]; + 668→ if (!node || !node.cleared) return; // 当前节点未结算不能前进 + 669→ if (exp.currentNode >= exp.nodes.length - 1) return; + 670→ exp.currentNode++; + 671→ set({ activeExpedition: exp }); + 672→ }, + 673→ + 674→ abortExpedition: () => { + 675→ const s = get(); + 676→ if (!s.activeExpedition) return; + 677→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 678→ exp.finished = true; + 679→ const logEntry = { + 680→ expeditionId: exp.id, + 681→ nodeType: "rest" as const, + 682→ result: "探险队主动撤退,保留已获奖励。", + 683→ rewards: "", + 684→ timestamp: Date.now(), + 685→ }; + 686→ set({ + 687→ activeExpedition: exp, + 688→ expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30), + 689→ }); + 690→ }, + 691→ + 692→ doPrestige: () => { + 693→ const s = get(); + 694→ if (s.contact < CONTACT.prestigeMin) return null; + 695→ const newBp = computeNewBlueprints(s); + 696→ const next = performPrestige(s); + 697→ // 星图「能量共振」提升上限 + 698→ const cm = constellationBonuses(next.constellation ?? []); + 699→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; + 700→ set({ + 701→ ...next, + 702→ energyMax, + 703→ ...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation }), + 704→ _lastAutoDecode: Date.now(), + 705→ _lastSpawn: Date.now(), + 706→ _combo: 0, + 707→ _lastPulse: 0, + 708→ _tideEvents: [], + 709→ }); + 710→ return { newBp }; + 711→ }, + 712→ + 713→ chooseConstellationPerk: (perkId) => { + 714→ const s = get(); + 715→ if (!s.pendingPerkChoices || !s.pendingPerkChoices.includes(perkId)) return false; + 716→ const perk = getPerk(perkId); + 717→ if (!perk) return false; + 718→ if (s.constellation?.includes(perkId)) return false; + 719→ const newConstellation = [...(s.constellation ?? []), perkId]; + 720→ const cm = constellationBonuses(newConstellation); + 721→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; + 722→ // v0.4 编年史:回填最近一条 entry 的 perksThisAscension + 723→ const chronicle = s.chronicle ?? []; + 724→ let newChronicle = chronicle; + 725→ if (chronicle.length > 0) { + 726→ const lastEntry = chronicle[chronicle.length - 1]; + 727→ const updatedLast = withPerks(lastEntry, [perkId]); + 728→ newChronicle = [...chronicle.slice(0, -1), updatedLast]; + 729→ } + 730→ set({ + 731→ constellation: newConstellation, + 732→ pendingPerkChoices: null, + 733→ energyMax, + 734→ chronicle: newChronicle, + 735→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation }), + 736→ }); + 737→ return true; + 738→ }, + 739→ + 740→ rerollPerkChoices: () => { + 741→ const s = get(); + 742→ if (!s.pendingPerkChoices) return; + 743→ const choices = rollPerkChoices(s.constellation ?? []); + 744→ if (choices.length > 0) set({ pendingPerkChoices: choices }); + 745→ }, + 746→ + 747→ checkAchievements: () => { + 748→ const s = get(); + 749→ const newlyUnlocked: Achievement[] = []; + 750→ const updated = { ...s.achievements }; + 751→ let crystals = s.crystals; + 752→ let insights = s.insights; + 753→ let contact = s.contact; + 754→ let statsDirty = false; + 755→ for (const a of ACHIEVEMENTS) { + 756→ if (updated[a.id]) continue; + 757→ if (a.check(s)) { + 758→ updated[a.id] = true; + 759→ newlyUnlocked.push(a); + 760→ // 发放即时奖励 + 761→ if (a.reward.crystals) crystals += a.reward.crystals; + 762→ if (a.reward.insights) insights += a.reward.insights; + 763→ if (a.reward.contact) contact = Math.min(100, contact + a.reward.contact); + 764→ if (a.reward.crystalsPerSecPct || a.reward.insightPct) statsDirty = true; + 765→ } + 766→ } + 767→ if (newlyUnlocked.length === 0) return []; + 768→ set({ + 769→ achievements: updated, + 770→ crystals, + 771→ insights, + 772→ contact, + 773→ ...(statsDirty + 774→ ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation }) + 775→ : {}), + 776→ _achievementQueue: [...s._achievementQueue, ...newlyUnlocked], + 777→ }); + 778→ return newlyUnlocked; + 779→ }, + 780→ + 781→ consumeAchievementQueue: () => { + 782→ const s = get(); + 783→ if (s._achievementQueue.length === 0) return []; + 784→ const items = s._achievementQueue; + 785→ set({ _achievementQueue: [] }); + 786→ return items; + 787→ }, + 788→ + 789→ canPrestige: () => get().contact >= CONTACT.prestigeMin, + 790→ + 791→ toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }), + 792→ toggleSound: () => set({ soundOn: !get().soundOn }), + 793→ + 794→ // 深空信标:发放每日挑战奖励(v0.5) + 795→ grantBeaconReward: (insights, contact) => { + 796→ const s = get(); + 797→ set({ + 798→ insights: s.insights + Math.round(insights), + 799→ contact: Math.min(100, s.contact + contact), + 800→ }); + 801→ }, + 802→ + 803→ // 深空巡航:发放实时玩法奖励(v0.6) + 804→ grantCruiseReward: (rewards) => { + 805→ const s = get(); + 806→ const addCrystals = rewards.crystals ?? 0; + 807→ const addInsights = rewards.insights ?? 0; + 808→ const addContact = rewards.contact ?? 0; + 809→ set({ + 810→ crystals: Math.min(s.crystalCap, s.crystals + addCrystals), + 811→ insights: s.insights + Math.round(addInsights), + 812→ contact: Math.min(100, s.contact + addContact), + 813→ }); + 814→ }, + 815→ }), + 816→ { + 817→ name: "echo-nexus-save-v1", + 818→ storage: createJSONStorage(() => localStorage), + 819→ // 不持久化临时字段 + 820→ partialize: (s) => { + 821→ const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, ...rest } = s; + 822→ void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents; + 823→ return rest as GameState; + 824→ }, + 825→ } + 826→ ) + 827→); + 828→ + 829→/** 选择器:未解锁碎片中下一个门槛 */ + 830→export function nextFragmentThreshold(totalDecoded: number): number | null { + 831→ for (const f of FRAGMENTS) { + 832→ if (totalDecoded < f.threshold) return f.threshold; + 833→ } + 834→ return null; + 835→} + 836→ + 837→export { FRAGMENTS, PRESTIGE, TECH_TREE }; + 838→ + 839→// 开发期调试:暴露 store 到 window,便于 QA 测试 + 840→if (typeof window !== "undefined" && process.env.NODE_ENV !== "production") { + 841→ (window as unknown as { __gameStore?: typeof useGameStore }).__gameStore = useGameStore; + 842→} + 843→ \ No newline at end of file diff --git a/tool-results/read_1782247263000_ec7aa6aa7dd7.txt b/tool-results/read_1782247263000_ec7aa6aa7dd7.txt new file mode 100644 index 000000000..278298362 --- /dev/null +++ b/tool-results/read_1782247263000_ec7aa6aa7dd7.txt @@ -0,0 +1,843 @@ + 1→ 1→"use client"; + 2→ 2→// 回响星核 / Echo Nexus — Zustand 游戏状态管理 + 3→ 3→import { create } from "zustand"; + 4→ 4→import { persist, createJSONStorage } from "zustand/middleware"; + 5→ 5→import type { + 6→ 6→ GameState, + 7→ 7→ Crystal, + 8→ 8→ CrystalTier, + 9→ 9→ DecodePuzzle, + 10→ 10→ ExpeditionResult, + 11→ 11→} from "@/lib/game/types"; + 12→ 12→import { + 13→ 13→ INITIAL_STATE, + 14→ 14→ TECH_TREE, + 15→ 15→ CRYSTAL_VALUE, + 16→ 16→ CONTACT, + 17→ 17→ CRYSTAL_SPAWN, + 18→ 18→ FRAGMENTS, + 19→ 19→ PRESTIGE, + 20→ 20→} from "@/lib/game/config"; + 21→ 21→import { + 22→ 22→ createInitialState, + 23→ 23→ recomputeStats, + 24→ 24→ decodeRewards, + 25→ 25→ rollCrystalTierWithBonus, + 26→ 26→ computeNewBlueprints, + 27→ 27→ performPrestige, + 28→ 28→} from "@/lib/game/engine"; + 29→ 29→import { + 30→ 30→ generatePuzzle, + 31→ 31→ tryClickNode, + 32→ 32→ isSolvable, + 33→ 33→ resetPuzzle as resetPuz, + 34→ 34→} from "@/lib/game/decode"; + 35→ 35→import { + 36→ 36→ generateExpedition, + 37→ 37→ resolveNode, + 38→ 38→ advanceExpedition, + 39→ 39→ computeExpeditionPower, + 40→ 40→ computeExpeditionHp, + 41→ 41→ computeEnergyRegen, + 42→ 42→ EXPEDITION_CONFIG, + 43→ 43→} from "@/lib/game/expedition"; + 44→ 44→import { ACHIEVEMENTS, type Achievement } from "@/lib/game/achievements"; + 45→ 45→import { + 46→ 46→ TIDE_CONFIG, + 47→ 47→ rollTide, + 48→ 48→ getTideModifiers, + 49→ 49→ computeSilenceCompensation, + 50→ 50→ type StarTide, + 51→ 51→ type TideType, + 52→ 52→} from "@/lib/game/starTide"; + 53→ 53→import { + 54→ 54→ getPerk, + 55→ 55→ constellationBonuses, + 56→ 56→ rollPerkChoices, + 57→ 57→} from "@/lib/game/constellation"; + 58→ 58→import { + 59→ 59→ migrateChronicleFields, + 60→ 60→ withPerks, + 61→ 61→} from "@/lib/game/chronicle"; + 62→ 62→import { + 63→ 63→ generateDailyChallenge, + 64→ 64→ loadDailyProgress, + 65→ 65→ addBeaconProgress, + 66→ 66→ type BeaconDailyChallenge, + 67→ 67→ type BeaconDailyProgress, + 68→ 68→} from "@/lib/game/beacon"; + 69→ 69→import { setPendingOfflineReport } from "@/lib/game/offlineReport"; + 70→ 70→ + 71→ 71→interface GameActions { + 72→ 72→ // 生命周期 + 73→ 73→ init: () => void; + 74→ 74→ loadOnline: () => void; + 75→ 75→ hardReset: () => void; + 76→ 76→ + 77→ 77→ // 主循环 + 78→ 78→ tick: (now: number) => void; + 79→ 79→ pulse: () => { gain: number; combo: number } | null; + 80→ 80→ + 81→ 81→ // 星潮 + 82→ 82→ tickTide: (now: number) => { started?: TideType; ended?: TideType; silenceCompensation?: number } | null; + 83→ 83→ consumeTideEvents: () => { started?: TideType; ended?: TideType; silenceCompensation?: number }[]; + 84→ 84→ + 85→ 85→ // 解码 + 86→ 86→ startDecode: (crystalId: string) => void; + 87→ 87→ clickNode: (nodeId: number) => { ok: boolean; finished: boolean; solvable?: boolean; failReason?: string }; + 88→ 88→ undoStep: () => void; + 89→ 89→ retryPuzzle: () => void; + 90→ 90→ abandonPuzzle: () => void; + 91→ 91→ /** 自动解码 T1(技术解锁后由 tick 调用) */ + 92→ 92→ autoDecodeTick: () => void; + 93→ 93→ + 94→ 94→ // 探险 + 95→ 95→ startExpedition: () => { ok: boolean; reason?: string }; + 96→ 96→ resolveCurrentNode: () => ExpeditionResult | null; + 97→ 97→ advanceNode: () => void; + 98→ 98→ abortExpedition: () => void; + 99→ 99→ + 100→ 100→ // 技术 + 101→ 101→ buyTech: (techId: string) => boolean; + 102→ 102→ + 103→ 103→ // 飞升 + 104→ 104→ doPrestige: () => { newBp: number } | null; + 105→ 105→ + 106→ 106→ // 星图天文台 + 107→ 107→ chooseConstellationPerk: (perkId: string) => boolean; + 108→ 108→ rerollPerkChoices: () => void; + 109→ 109→ + 110→ 110→ // 成就 + 111→ 111→ checkAchievements: () => Achievement[]; + 112→ 112→ consumeAchievementQueue: () => Achievement[]; + 113→ 113→ + 114→ 114→ // 设置 + 115→ 115→ toggleTheme: () => void; + 116→ 116→ toggleSound: () => void; + 117→ 117→ + 118→ 118→ // 深空信标奖励发放(v0.5) + 119→ 119→ grantBeaconReward: (insights: number, contact: number) => void; + 120→ 120→ + 121→ 121→ // 深空巡航奖励发放(v0.6 — 实时 Canvas 玩法) + 122→ 122→ grantCruiseReward: (rewards: { crystals?: number; insights?: number; contact?: number }) => void; + 123→ 123→ + 124→ 124→ // 派生 + 125→ 125→ canPrestige: () => boolean; + 126→ 126→} + 127→ 127→ + 128→ 128→type Store = GameState & GameActions & { + 129→ 129→ _lastAutoDecode: number; + 130→ 130→ _lastSpawn: number; + 131→ 131→ _combo: number; + 132→ 132→ _lastPulse: number; + 133→ 133→ _achievementQueue: Achievement[]; + 134→ 134→ _tideEvents: { started?: TideType; ended?: TideType; silenceCompensation?: number }[]; + 135→ 135→}; + 136→ 136→ + 137→ 137→/** 计算并写回产能字段 */ + 138→ 138→function syncStats(state: Partial) { + 139→ 139→ const s = recomputeStats(state); + 140→ 140→ return { + 141→ 141→ crystalsPerSec: s.crystalsPerSec, + 142→ 142→ crystalCap: s.crystalCap, + 143→ 143→ pulsePower: s.pulsePower, + 144→ 144→ offlineEff: s.offlineEff, + 145→ 145→ insightMult: s.insightMult, + 146→ 146→ contactRateMult: s.contactRateMult, + 147→ 147→ autoDecode: s.autoDecode, + 148→ 148→ decodeStepsBonus: s.decodeStepsBonus, + 149→ 149→ }; + 150→ 150→} + 151→ 151→ + 152→ 152→/** + 153→ 153→ * 深空信标进度追踪(v0.5)。 + 154→ 154→ * 在游戏关键动作(脉冲/解码/探险/BOSS/洞见)后调用,按今日挑战类型增量更新进度。 + 155→ 155→ * 进度独立存储于 localStorage(echo-nexus-beacon-prog-v1),不污染 GameState。 + 156→ 156→ * @returns 若刚完成则返回 true(供 UI 触发通知) + 157→ 157→ */ + 158→ 158→function trackBeacon( + 159→ 159→ type: "pulse" | "decode" | "expedition" | "boss" | "insight", + 160→ 160→ delta: number + 161→ 161→): boolean { + 162→ 162→ if (typeof window === "undefined") return false; + 163→ 163→ try { + 164→ 164→ const challenge: BeaconDailyChallenge = generateDailyChallenge(); + 165→ 165→ if (challenge.type !== type) return false; + 166→ 166→ const current: BeaconDailyProgress = loadDailyProgress(); + 167→ 167→ if (current.completedAt !== null) return false; // 已完成不再累加 + 168→ 168→ const { justCompleted } = addBeaconProgress(current, challenge, delta); + 169→ 169→ return justCompleted; + 170→ 170→ } catch { + 171→ 171→ return false; + 172→ 172→ } + 173→ 173→} + 174→ 174→ + 175→ 175→/** 检查并解锁叙事碎片 */ + 176→ 176→function checkFragments(state: GameState): string[] { + 177→ 177→ const unlocked: string[] = []; + 178→ 178→ for (const f of FRAGMENTS) { + 179→ 179→ if (!state.fragments[f.id] && state.totalDecoded >= f.threshold) { + 180→ 180→ state.fragments[f.id] = true; + 181→ 181→ unlocked.push(f.id); + 182→ 182→ } + 183→ 183→ } + 184→ 184→ return unlocked; + 185→ 185→} + 186→ 186→ + 187→ 187→export const useGameStore = create()( + 188→ 188→ persist( + 189→ 189→ (set, get) => ({ + 190→ 190→ ...createInitialState(), + 191→ 191→ _lastAutoDecode: Date.now(), + 192→ 192→ _lastSpawn: Date.now(), + 193→ 193→ _combo: 0, + 194→ 194→ _lastPulse: 0, + 195→ 195→ _achievementQueue: [], + 196→ 196→ _tideEvents: [], + 197→ 197→ + 198→ 198→ init: () => { + 199→ 199→ const s = get(); + 200→ 200→ const now = Date.now(); + 201→ 201→ // 兼容旧存档:若存在不可解的活跃谜题(旧算法生成),直接清掉 + 202→ 202→ let activePuzzle = s.activePuzzle; + 203→ 203→ if (activePuzzle && !isSolvable(activePuzzle)) { + 204→ 204→ // 把晶体放回队列,避免玩家卡死 + 205→ 205→ const crystal: Crystal = { + 206→ 206→ id: `c_${now}_rec`, + 207→ 207→ tier: activePuzzle.tier, + 208→ 208→ value: CRYSTAL_VALUE[activePuzzle.tier].crystals, + 209→ 209→ createdAt: now, + 210→ 210→ }; + 211→ 211→ activePuzzle = null; + 212→ 212→ set({ + 213→ 213→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), + 214→ 214→ }); + 215→ 215→ } + 216→ 216→ // 兼容旧存档:补全 achievements / 星潮 / 星图 字段 + 217→ 217→ const achievements = s.achievements ?? {}; + 218→ 218→ const activeTide = s.activeTide ?? null; + 219→ 219→ const constellation = s.constellation ?? []; + 220→ 220→ const pendingPerkChoices = s.pendingPerkChoices ?? null; + 221→ 221→ // v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered + 222→ 222→ const migrated = migrateChronicleFields(s); + 223→ 223→ // 星图「能量共振」天赋 +1 能量上限 + 224→ 224→ const cm = constellationBonuses(constellation); + 225→ 225→ const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus; + 226→ 226→ // lastTideEnd:0 表示从未触发过星潮(首次游玩),保留 0 让 firstDelay 生效 + 227→ 227→ const lastTideEndRaw = s.lastTideEnd ?? 0; + 228→ 228→ // 若旧存档有已过期的星潮,清掉 + 229→ 229→ const tide = activeTide && activeTide.endsAt > now ? activeTide : null; + 230→ 230→ // 首次进入:补发离线收益 + 231→ 231→ const elapsed = Math.max(0, (now - s.lastTick) / 1000); + 232→ 232→ if (elapsed > 5) { + 233→ 233→ const cap = 8 * 3600; + 234→ 234→ const secs = Math.min(elapsed, cap); + 235→ 235→ const gain = s.crystalsPerSec * secs * s.offlineEff; + 236→ 236→ const crystalsBefore = s.crystals; + 237→ 237→ const crystalsAfter = Math.min(s.crystalCap, s.crystals + gain); + 238→ 238→ // 设置离线报告(v0.5.2):通知 UI 弹出"欢迎回来"对话框 + 239→ 239→ setPendingOfflineReport({ + 240→ 240→ elapsedSec: secs, + 241→ 241→ rawElapsedSec: elapsed, + 242→ 242→ gain: crystalsAfter - crystalsBefore, + 243→ 243→ rate: s.crystalsPerSec, + 244→ 244→ eff: s.offlineEff, + 245→ 245→ capped: elapsed > cap, + 246→ 246→ crystalsBefore, + 247→ 247→ crystalsAfter, + 248→ 248→ crystalCap: s.crystalCap, + 249→ 249→ }); + 250→ 250→ set({ + 251→ 251→ crystals: crystalsAfter, + 252→ 252→ lastTick: now, + 253→ 253→ activePuzzle, + 254→ 254→ achievements, + 255→ 255→ activeTide: tide, + 256→ 256→ lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, + 257→ 257→ constellation, + 258→ 258→ pendingPerkChoices, + 259→ 259→ energyMax, + 260→ 260→ chronicle: migrated.chronicle, + 261→ 261→ runStart: migrated.runStart, + 262→ 262→ bossKills: migrated.bossKills, + 263→ 263→ starTidesEncountered: migrated.starTidesEncountered, + 264→ 264→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }), + 265→ 265→ }); + 266→ 266→ } else { + 267→ 267→ 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, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }) }); + 268→ 268→ } + 269→ 269→ }, + 270→ 270→ + 271→ 271→ loadOnline: () => { + 272→ 272→ const s = get(); + 273→ 273→ set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }) }); + 274→ 274→ }, + 275→ 275→ + 276→ 276→ hardReset: () => { + 277→ 277→ set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [] }); + 278→ 278→ }, + 279→ 279→ + 280→ 280→ tickTide: (now) => { + 281→ 281→ const s = get(); + 282→ 282→ const tide = s.activeTide; + 283→ 283→ // 星图「星潮引导」减少间隙 + 284→ 284→ const cm = constellationBonuses(s.constellation ?? []); + 285→ 285→ const gap = Math.max(15000, TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000); + 286→ 286→ // 1) 检查当前星潮是否结束 + 287→ 287→ if (tide && now >= tide.endsAt) { + 288→ 288→ const endedType = tide.type; + 289→ 289→ // 寂静期补偿洞见 + 290→ 290→ let silenceCompensation = 0; + 291→ 291→ if (tide.type === "silence") { + 292→ 292→ silenceCompensation = computeSilenceCompensation(tide); + 293→ 293→ } + 294→ 294→ const newInsights = s.insights + silenceCompensation; + 295→ 295→ const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { ended: endedType }; + 296→ 296→ if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation; + 297→ 297→ set({ + 298→ 298→ activeTide: null, + 299→ 299→ lastTideEnd: now, + 300→ 300→ insights: newInsights, + 301→ 301→ // 星潮结束后重算 stats(移除 contactRate/insight 修饰) + 302→ 302→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation }), + 303→ 303→ _tideEvents: [...s._tideEvents, event], + 304→ 304→ }); + 305→ 305→ return event; + 306→ 306→ } + 307→ 307→ // 2) 检查是否该触发新星潮(间隙已过) + 308→ 308→ if (!tide) { + 309→ 309→ const since = now - s.lastTideEnd; + 310→ 310→ // 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap + 311→ 311→ const firstStart = s.lastTideEnd === 0 || s.lastTideEnd <= s.createdAt + 1000; + 312→ 312→ const need = firstStart ? TIDE_CONFIG.firstDelay : gap; + 313→ 313→ if (since >= need) { + 314→ 314→ const type = rollTide(); + 315→ 315→ const newTide: StarTide = { + 316→ 316→ id: `tide_${now}_${Math.random().toString(36).slice(2, 7)}`, + 317→ 317→ type, + 318→ 318→ startedAt: now, + 319→ 319→ endsAt: now + TIDE_CONFIG.duration, + 320→ 320→ }; + 321→ 321→ const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { started: type }; + 322→ 322→ // v0.4 编年史:累计遇到的星潮 ID(去重) + 323→ 323→ const tidesAll = s.starTidesEncountered ?? []; + 324→ 324→ const tideId = `tide_${type}`; + 325→ 325→ const newTidesAll = tidesAll.includes(tideId) ? tidesAll : [...tidesAll, tideId]; + 326→ 326→ set({ + 327→ 327→ activeTide: newTide, + 328→ 328→ starTidesEncountered: newTidesAll, + 329→ 329→ // 星潮开始后重算 stats(应用 contactRate/insight 修饰) + 330→ 330→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation }), + 331→ 331→ _tideEvents: [...s._tideEvents, event], + 332→ 332→ }); + 333→ 333→ return event; + 334→ 334→ } + 335→ 335→ } + 336→ 336→ return null; + 337→ 337→ }, + 338→ 338→ + 339→ 339→ consumeTideEvents: () => { + 340→ 340→ const s = get(); + 341→ 341→ if (s._tideEvents.length === 0) return []; + 342→ 342→ const items = s._tideEvents; + 343→ 343→ set({ _tideEvents: [] }); + 344→ 344→ return items; + 345→ 345→ }, + 346→ 346→ + 347→ 347→ tick: (now) => { + 348→ 348→ const s = get(); + 349→ 349→ const dt = Math.max(0, (now - s.lastTick) / 1000); + 350→ 350→ if (dt <= 0) return; + 351→ 351→ + 352→ 352→ // 星潮产能修饰(即时乘) + 353→ 353→ const tideMod = getTideModifiers(s.activeTide); + 354→ 354→ const effCps = s.crystalsPerSec * tideMod.crystalsPerSecMult; + 355→ 355→ // 产能累加(仅闲置产能受仓库上限;探险奖励可超出) + 356→ 356→ const newCrystals = + 357→ 357→ s.crystals >= s.crystalCap + 358→ 358→ ? s.crystals // 已达/超上限,不再自动产出 + 359→ 359→ : Math.min(s.crystalCap, s.crystals + effCps * dt); + 360→ 360→ + 361→ 361→ // 自动产出待解码晶体(每 baseInterval 秒一颗,受技术/飞升略提速) + 362→ 362→ const bpBoost = 1 + s.blueprints.length * 0.03; + 363→ 363→ const spawnInterval = (CRYSTAL_SPAWN.baseIntervalSec / bpBoost) * 1000; + 364→ 364→ let pending = s.pendingCrystals; + 365→ 365→ let lastSpawn = s._lastSpawn; + 366→ 366→ if ( + 367→ 367→ now - lastSpawn > spawnInterval && + 368→ 368→ pending.length < CRYSTAL_SPAWN.maxPending + 369→ 369→ ) { + 370→ 370→ // 星图「晶体富集」提升 T2/T3 概率 + 371→ 371→ const cm = constellationBonuses(s.constellation ?? []); + 372→ 372→ const tier = rollCrystalTierWithBonus(cm.t2t3BonusRate); + 373→ 373→ const crystal: Crystal = { + 374→ 374→ id: `c_${now}_${Math.random().toString(36).slice(2, 7)}`, + 375→ 375→ tier, + 376→ 376→ value: CRYSTAL_VALUE[tier].crystals, + 377→ 377→ createdAt: now, + 378→ 378→ }; + 379→ 379→ pending = [...pending, crystal]; + 380→ 380→ lastSpawn = now; + 381→ 381→ } + 382→ 382→ + 383→ 383→ // 能量恢复(探险系统) + 384→ 384→ let energy = s.energy; + 385→ 385→ let lastEnergyTick = s.lastEnergyTick; + 386→ 386→ if (energy < s.energyMax) { + 387→ 387→ const regen = computeEnergyRegen(lastEnergyTick, now, energy, s.energyMax); + 388→ 388→ energy = regen.energy; + 389→ 389→ lastEnergyTick = regen.lastTick; + 390→ 390→ } else { + 391→ 391→ lastEnergyTick = now; + 392→ 392→ } + 393→ 393→ + 394→ 394→ set({ + 395→ 395→ crystals: newCrystals, + 396→ 396→ lastTick: now, + 397→ 397→ pendingCrystals: pending, + 398→ 398→ _lastSpawn: lastSpawn, + 399→ 399→ energy, + 400→ 400→ lastEnergyTick, + 401→ 401→ }); + 402→ 402→ }, + 403→ 403→ + 404→ 404→ pulse: () => { + 405→ 405→ const s = get(); + 406→ 406→ const now = Date.now(); + 407→ 407→ // 连击 + 408→ 408→ let combo = 1; + 409→ 409→ if (now - s._lastPulse < 1500) { + 410→ 410→ combo = Math.min(10, s._combo + 1); + 411→ 411→ } + 412→ 412→ const mult = 1 + (combo - 1) * 0.15; + 413→ 413→ // 星潮脉冲威力修饰 + 414→ 414→ const tideMod = getTideModifiers(s.activeTide); + 415→ 415→ const gain = s.pulsePower * mult * tideMod.pulsePowerMult; + 416→ 416→ set({ + 417→ 417→ crystals: Math.min(s.crystalCap, s.crystals + gain), + 418→ 418→ _combo: combo, + 419→ 419→ _lastPulse: now, + 420→ 420→ }); + 421→ 421→ // 深空信标:脉冲任务进度 +1 + 422→ 422→ trackBeacon("pulse", 1); + 423→ 423→ return { gain, combo }; + 424→ 424→ }, + 425→ 425→ + 426→ 426→ startDecode: (crystalId) => { + 427→ 427→ const s = get(); + 428→ 428→ const crystal = s.pendingCrystals.find((c) => c.id === crystalId); + 429→ 429→ if (!crystal) return; + 430→ 430→ const puzzle = generatePuzzle(crystal.tier); + 431→ 431→ set({ + 432→ 432→ activePuzzle: puzzle, + 433→ 433→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystalId), + 434→ 434→ }); + 435→ 435→ }, + 436→ 436→ + 437→ 437→ clickNode: (nodeId) => { + 438→ 438→ const s = get(); + 439→ 439→ if (!s.activePuzzle) return { ok: false, finished: false, failReason: "无活跃谜题" }; + 440→ 440→ // 深拷贝谜题 + 441→ 441→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); + 442→ 442→ const res = tryClickNode(puzzle, nodeId); + 443→ 443→ if (res.ok) { + 444→ 444→ if (res.finished) { + 445→ 445→ // 结算奖励(星潮解码奖励修饰 + 星图「二周目经验」) + 446→ 446→ const tideMod = getTideModifiers(s.activeTide); + 447→ 447→ const cm = constellationBonuses(s.constellation ?? []); + 448→ 448→ const base = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult); + 449→ 449→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; + 450→ 450→ const rewards = { + 451→ 451→ crystals: Math.round(base.crystals * finalMult), + 452→ 452→ insights: Math.round(base.insights * finalMult), + 453→ 453→ contact: +(base.contact * finalMult).toFixed(2), + 454→ 454→ }; + 455→ 455→ const newTotal = s.totalDecoded + 1; + 456→ 456→ const newContact = Math.min(100, s.contact + rewards.contact); + 457→ 457→ const newInsights = s.insights + rewards.insights; + 458→ 458→ const newCrystals = s.crystals + rewards.crystals; + 459→ 459→ // 解锁碎片 + 460→ 460→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; + 461→ 461→ const unlocked = checkFragments(tentative); + 462→ 462→ set({ + 463→ 463→ activePuzzle: null, + 464→ 464→ crystals: newCrystals, + 465→ 465→ insights: newInsights, + 466→ 466→ contact: newContact, + 467→ 467→ totalDecoded: newTotal, + 468→ 468→ fragments: tentative.fragments, + 469→ 469→ }); + 470→ 470→ // 深空信标:解码 +1,洞见累计 + 471→ 471→ trackBeacon("decode", 1); + 472→ 472→ trackBeacon("insight", rewards.insights); + 473→ 473→ return { ok: true, finished: true, failReason: unlocked.join(",") || undefined }; + 474→ 474→ } + 475→ 475→ // 点击成功但未完成:检测当前局面是否仍可解 + 476→ 476→ const solvable = isSolvable(puzzle); + 477→ 477→ set({ activePuzzle: puzzle }); + 478→ 478→ return { ok: true, finished: false, solvable }; + 479→ 479→ } + 480→ 480→ return res; + 481→ 481→ }, + 482→ 482→ + 483→ 483→ undoStep: () => { + 484→ 484→ const s = get(); + 485→ 485→ if (!s.activePuzzle || s.activePuzzle.path.length === 0) return; + 486→ 486→ const puzzle: DecodePuzzle = JSON.parse(JSON.stringify(s.activePuzzle)); + 487→ 487→ const lastId = puzzle.path.pop(); + 488→ 488→ if (lastId !== undefined) { + 489→ 489→ const node = puzzle.grid.find((n) => n.id === lastId); + 490→ 490→ if (node) node.used = false; + 491→ 491→ } + 492→ 492→ set({ activePuzzle: puzzle }); + 493→ 493→ }, + 494→ 494→ + 495→ 495→ retryPuzzle: () => { + 496→ 496→ const s = get(); + 497→ 497→ if (!s.activePuzzle) return; + 498→ 498→ set({ activePuzzle: resetPuz(s.activePuzzle) }); + 499→ 499→ }, + 500→ 500→ + 501→ 501→ abandonPuzzle: () => { + 502→ 502→ const s = get(); + 503→ 503→ if (!s.activePuzzle) return; + 504→ 504→ // 晶体放回队列末尾 + 505→ 505→ const crystal: Crystal = { + 506→ 506→ id: `c_${Date.now()}_ret`, + 507→ 507→ tier: s.activePuzzle.tier, + 508→ 508→ value: CRYSTAL_VALUE[s.activePuzzle.tier].crystals, + 509→ 509→ createdAt: Date.now(), + 510→ 510→ }; + 511→ 511→ set({ + 512→ 512→ activePuzzle: null, + 513→ 513→ pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending), + 514→ 514→ }); + 515→ 515→ }, + 516→ 516→ + 517→ 517→ autoDecodeTick: () => { + 518→ 518→ const s = get(); + 519→ 519→ if (!s.autoDecode) return; + 520→ 520→ const now = Date.now(); + 521→ 521→ // 星图「自动校准」减少自动解码周期 + 522→ 522→ const cm = constellationBonuses(s.constellation ?? []); + 523→ 523→ const interval = Math.max(5000, 12000 + cm.autoDecodeIntervalDeltaSec * 1000); + 524→ 524→ if (now - s._lastAutoDecode < interval) return; + 525→ 525→ // 找一颗 T1 晶体自动解码 + 526→ 526→ const idx = s.pendingCrystals.findIndex((c) => c.tier === 1); + 527→ 527→ if (idx < 0) return; + 528→ 528→ const crystal = s.pendingCrystals[idx]; + 529→ 529→ const tideMod = getTideModifiers(s.activeTide); + 530→ 530→ const base = decodeRewards(1, s.insightMult, s.contactRateMult); + 531→ 531→ const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult; + 532→ 532→ const rewards = { + 533→ 533→ crystals: Math.round(base.crystals * finalMult), + 534→ 534→ insights: Math.round(base.insights * finalMult), + 535→ 535→ contact: +(base.contact * finalMult).toFixed(2), + 536→ 536→ }; + 537→ 537→ const newTotal = s.totalDecoded + 1; + 538→ 538→ const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } }; + 539→ 539→ checkFragments(tentative); + 540→ 540→ set({ + 541→ 541→ pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystal.id), + 542→ 542→ crystals: s.crystals + rewards.crystals, + 543→ 543→ insights: s.insights + rewards.insights, + 544→ 544→ contact: Math.min(100, s.contact + rewards.contact), + 545→ 545→ totalDecoded: newTotal, + 546→ 546→ fragments: tentative.fragments, + 547→ 547→ _lastAutoDecode: now, + 548→ 548→ }); + 549→ 549→ // 深空信标:自动解码也算进度 + 550→ 550→ trackBeacon("decode", 1); + 551→ 551→ trackBeacon("insight", rewards.insights); + 552→ 552→ }, + 553→ 553→ + 554→ 554→ buyTech: (techId) => { + 555→ 555→ const s = get(); + 556→ 556→ const node = TECH_TREE.find((t) => t.id === techId); + 557→ 557→ if (!node) return false; + 558→ 558→ const cur = s.tech[techId] ?? 0; + 559→ 559→ if (cur >= 1) return false; // v0.1 每节点 1 级 + 560→ 560→ if (s.insights < node.cost) return false; + 561→ 561→ const newTech = { ...s.tech, [techId]: 1 }; + 562→ 562→ set({ + 563→ 563→ insights: s.insights - node.cost, + 564→ 564→ tech: newTech, + 565→ 565→ ...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }), + 566→ 566→ }); + 567→ 567→ return true; + 568→ 568→ }, + 569→ 569→ + 570→ 570→ // ============ 探险系统 ============ + 571→ 571→ startExpedition: () => { + 572→ 572→ const s = get(); + 573→ 573→ if (s.activeExpedition && !s.activeExpedition.finished) { + 574→ 574→ return { ok: false, reason: "已有进行中的探险" }; + 575→ 575→ } + 576→ 576→ if (s.energy < EXPEDITION_CONFIG.energyCost) { + 577→ 577→ return { ok: false, reason: "能量不足" }; + 578→ 578→ } + 579→ 579→ const tideMod = getTideModifiers(s.activeTide); + 580→ 580→ const power = computeExpeditionPower(s) + tideMod.expeditionPowerBonus; + 581→ 581→ const hp = computeExpeditionHp(s) + tideMod.expeditionHpBonus; + 582→ 582→ const seed = Math.floor(Math.random() * 1e9); + 583→ 583→ const expedition = generateExpedition(seed, power, hp); + 584→ 584→ set({ + 585→ 585→ activeExpedition: expedition, + 586→ 586→ energy: s.energy - EXPEDITION_CONFIG.energyCost, + 587→ 587→ totalExpeditions: s.totalExpeditions + 1, + 588→ 588→ }); + 589→ 589→ return { ok: true }; + 590→ 590→ }, + 591→ 591→ + 592→ 592→ resolveCurrentNode: () => { + 593→ 593→ const s = get(); + 594→ 594→ if (!s.activeExpedition || s.activeExpedition.finished) return null; + 595→ 595→ // 深拷贝 + 596→ 596→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 597→ 597→ const result = resolveNode(exp); + 598→ 598→ // 累计奖励 + 599→ 599→ if (result.crystals) exp.rewards.crystals += result.crystals; + 600→ 600→ if (result.insights) exp.rewards.insights += result.insights; + 601→ 601→ if (result.contact) exp.rewards.contact += result.contact; + 602→ 602→ if (result.fragments) exp.rewards.fragments.push(...result.fragments); + 603→ 603→ if (result.hpDelta) exp.hp = Math.max(0, Math.min(exp.maxHp, exp.hp + result.hpDelta)); + 604→ 604→ // 实时入账(玩家立即获得) + 605→ 605→ const newCrystals = s.crystals + (result.crystals || 0); + 606→ 606→ const newInsights = s.insights + (result.insights || 0); + 607→ 607→ const newContact = Math.min(100, s.contact + (result.contact || 0)); + 608→ 608→ // 碎片解锁 + 609→ 609→ const newFragments = { ...s.fragments }; + 610→ 610→ if (result.fragments) { + 611→ 611→ for (const fid of result.fragments) newFragments[fid] = true; + 612→ 612→ } + 613→ 613→ // 日志 + 614→ 614→ const logEntry = { + 615→ 615→ expeditionId: exp.id, + 616→ 616→ nodeType: exp.nodes[exp.currentNode]?.type || "combat", + 617→ 617→ result: result.log, + 618→ 618→ rewards: [ + 619→ 619→ result.crystals ? `+${result.crystals}晶体` : "", + 620→ 620→ result.insights ? `+${result.insights}洞见` : "", + 621→ 621→ result.contact ? `+${result.contact.toFixed(1)}接触` : "", + 622→ 622→ result.hpDelta ? `${result.hpDelta > 0 ? "+" : ""}${result.hpDelta}生命` : "", + 623→ 623→ ].filter(Boolean).join(" "), + 624→ 624→ timestamp: Date.now(), + 625→ 625→ }; + 626→ 626→ const newLog = [logEntry, ...s.expeditionLog].slice(0, 30); + 627→ 627→ + 628→ 628→ if (result.ended) { + 629→ 629→ // 探险结束(胜利或失败) + 630→ 630→ exp.finished = true; + 631→ 631→ } + 632→ 632→ + 633→ 633→ // v0.4 编年史:击破 BOSS 时累计计数 + 634→ 634→ let bossKills = s.bossKills ?? 0; + 635→ 635→ if ( + 636→ 636→ result.ended && + 637→ 637→ result.endReason === "victory" && + 638→ 638→ exp.nodes[exp.currentNode]?.type === "boss" + 639→ 639→ ) { + 640→ 640→ bossKills = bossKills + 1; + 641→ 641→ } + 642→ 642→ + 643→ 643→ set({ + 644→ 644→ activeExpedition: exp, + 645→ 645→ crystals: newCrystals, + 646→ 646→ insights: newInsights, + 647→ 647→ contact: newContact, + 648→ 648→ fragments: newFragments, + 649→ 649→ expeditionLog: newLog, + 650→ 650→ bossKills, + 651→ 651→ }); + 652→ 652→ // 深空信标:探险完成(胜负都计)+ 洞见累计 + BOSS 击破 + 653→ 653→ if (result.ended) { + 654→ 654→ trackBeacon("expedition", 1); + 655→ 655→ if (result.endReason === "victory" && exp.nodes[exp.currentNode]?.type === "boss") { + 656→ 656→ trackBeacon("boss", 1); + 657→ 657→ } + 658→ 658→ } + 659→ 659→ if (result.insights) trackBeacon("insight", result.insights); + 660→ 660→ return result; + 661→ 661→ }, + 662→ 662→ + 663→ 663→ advanceNode: () => { + 664→ 664→ const s = get(); + 665→ 665→ if (!s.activeExpedition || s.activeExpedition.finished) return; + 666→ 666→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 667→ 667→ const node = exp.nodes[exp.currentNode]; + 668→ 668→ if (!node || !node.cleared) return; // 当前节点未结算不能前进 + 669→ 669→ if (exp.currentNode >= exp.nodes.length - 1) return; + 670→ 670→ exp.currentNode++; + 671→ 671→ set({ activeExpedition: exp }); + 672→ 672→ }, + 673→ 673→ + 674→ 674→ abortExpedition: () => { + 675→ 675→ const s = get(); + 676→ 676→ if (!s.activeExpedition) return; + 677→ 677→ const exp = JSON.parse(JSON.stringify(s.activeExpedition)); + 678→ 678→ exp.finished = true; + 679→ 679→ const logEntry = { + 680→ 680→ expeditionId: exp.id, + 681→ 681→ nodeType: "rest" as const, + 682→ 682→ result: "探险队主动撤退,保留已获奖励。", + 683→ 683→ rewards: "", + 684→ 684→ timestamp: Date.now(), + 685→ 685→ }; + 686→ 686→ set({ + 687→ 687→ activeExpedition: exp, + 688→ 688→ expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30), + 689→ 689→ }); + 690→ 690→ }, + 691→ 691→ + 692→ 692→ doPrestige: () => { + 693→ 693→ const s = get(); + 694→ 694→ if (s.contact < CONTACT.prestigeMin) return null; + 695→ 695→ const newBp = computeNewBlueprints(s); + 696→ 696→ const next = performPrestige(s); + 697→ 697→ // 星图「能量共振」提升上限 + 698→ 698→ const cm = constellationBonuses(next.constellation ?? []); + 699→ 699→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; + 700→ 700→ set({ + 701→ 701→ ...next, + 702→ 702→ energyMax, + 703→ 703→ ...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation }), + 704→ 704→ _lastAutoDecode: Date.now(), + 705→ 705→ _lastSpawn: Date.now(), + 706→ 706→ _combo: 0, + 707→ 707→ _lastPulse: 0, + 708→ 708→ _tideEvents: [], + 709→ 709→ }); + 710→ 710→ return { newBp }; + 711→ 711→ }, + 712→ 712→ + 713→ 713→ chooseConstellationPerk: (perkId) => { + 714→ 714→ const s = get(); + 715→ 715→ if (!s.pendingPerkChoices || !s.pendingPerkChoices.includes(perkId)) return false; + 716→ 716→ const perk = getPerk(perkId); + 717→ 717→ if (!perk) return false; + 718→ 718→ if (s.constellation?.includes(perkId)) return false; + 719→ 719→ const newConstellation = [...(s.constellation ?? []), perkId]; + 720→ 720→ const cm = constellationBonuses(newConstellation); + 721→ 721→ const energyMax = INITIAL_STATE.energyMax + cm.energyMaxBonus; + 722→ 722→ // v0.4 编年史:回填最近一条 entry 的 perksThisAscension + 723→ 723→ const chronicle = s.chronicle ?? []; + 724→ 724→ let newChronicle = chronicle; + 725→ 725→ if (chronicle.length > 0) { + 726→ 726→ const lastEntry = chronicle[chronicle.length - 1]; + 727→ 727→ const updatedLast = withPerks(lastEntry, [perkId]); + 728→ 728→ newChronicle = [...chronicle.slice(0, -1), updatedLast]; + 729→ 729→ } + 730→ 730→ set({ + 731→ 731→ constellation: newConstellation, + 732→ 732→ pendingPerkChoices: null, + 733→ 733→ energyMax, + 734→ 734→ chronicle: newChronicle, + 735→ 735→ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation }), + 736→ 736→ }); + 737→ 737→ return true; + 738→ 738→ }, + 739→ 739→ + 740→ 740→ rerollPerkChoices: () => { + 741→ 741→ const s = get(); + 742→ 742→ if (!s.pendingPerkChoices) return; + 743→ 743→ const choices = rollPerkChoices(s.constellation ?? []); + 744→ 744→ if (choices.length > 0) set({ pendingPerkChoices: choices }); + 745→ 745→ }, + 746→ 746→ + 747→ 747→ checkAchievements: () => { + 748→ 748→ const s = get(); + 749→ 749→ const newlyUnlocked: Achievement[] = []; + 750→ 750→ const updated = { ...s.achievements }; + 751→ 751→ let crystals = s.crystals; + 752→ 752→ let insights = s.insights; + 753→ 753→ let contact = s.contact; + 754→ 754→ let statsDirty = false; + 755→ 755→ for (const a of ACHIEVEMENTS) { + 756→ 756→ if (updated[a.id]) continue; + 757→ 757→ if (a.check(s)) { + 758→ 758→ updated[a.id] = true; + 759→ 759→ newlyUnlocked.push(a); + 760→ 760→ // 发放即时奖励 + 761→ 761→ if (a.reward.crystals) crystals += a.reward.crystals; + 762→ 762→ if (a.reward.insights) insights += a.reward.insights; + 763→ 763→ if (a.reward.contact) contact = Math.min(100, contact + a.reward.contact); + 764→ 764→ if (a.reward.crystalsPerSecPct || a.reward.insightPct) statsDirty = true; + 765→ 765→ } + 766→ 766→ } + 767→ 767→ if (newlyUnlocked.length === 0) return []; + 768→ 768→ set({ + 769→ 769→ achievements: updated, + 770→ 770→ crystals, + 771→ 771→ insights, + 772→ 772→ contact, + 773→ 773→ ...(statsDirty + 774→ 774→ ? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation }) + 775→ 775→ : {}), + 776→ 776→ _achievementQueue: [...s._achievementQueue, ...newlyUnlocked], + 777→ 777→ }); + 778→ 778→ return newlyUnlocked; + 779→ 779→ }, + 780→ 780→ + 781→ 781→ consumeAchievementQueue: () => { + 782→ 782→ const s = get(); + 783→ 783→ if (s._achievementQueue.length === 0) return []; + 784→ 784→ const items = s._achievementQueue; + 785→ 785→ set({ _achievementQueue: [] }); + 786→ 786→ return items; + 787→ 787→ }, + 788→ 788→ + 789→ 789→ canPrestige: () => get().contact >= CONTACT.prestigeMin, + 790→ 790→ + 791→ 791→ toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }), + 792→ 792→ toggleSound: () => set({ soundOn: !get().soundOn }), + 793→ 793→ + 794→ 794→ // 深空信标:发放每日挑战奖励(v0.5) + 795→ 795→ grantBeaconReward: (insights, contact) => { + 796→ 796→ const s = get(); + 797→ 797→ set({ + 798→ 798→ insights: s.insights + Math.round(insights), + 799→ 799→ contact: Math.min(100, s.contact + contact), + 800→ 800→ }); + 801→ 801→ }, + 802→ 802→ + 803→ 803→ // 深空巡航:发放实时玩法奖励(v0.6) + 804→ 804→ grantCruiseReward: (rewards) => { + 805→ 805→ const s = get(); + 806→ 806→ const addCrystals = rewards.crystals ?? 0; + 807→ 807→ const addInsights = rewards.insights ?? 0; + 808→ 808→ const addContact = rewards.contact ?? 0; + 809→ 809→ set({ + 810→ 810→ crystals: Math.min(s.crystalCap, s.crystals + addCrystals), + 811→ 811→ insights: s.insights + Math.round(addInsights), + 812→ 812→ contact: Math.min(100, s.contact + addContact), + 813→ 813→ }); + 814→ 814→ }, + 815→ 815→ }), + 816→ 816→ { + 817→ 817→ name: "echo-nexus-save-v1", + 818→ 818→ storage: createJSONStorage(() => localStorage), + 819→ 819→ // 不持久化临时字段 + 820→ 820→ partialize: (s) => { + 821→ 821→ const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, ...rest } = s; + 822→ 822→ void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents; + 823→ 823→ return rest as GameState; + 824→ 824→ }, + 825→ 825→ } + 826→ 826→ ) + 827→ 827→); + 828→ 828→ + 829→ 829→/** 选择器:未解锁碎片中下一个门槛 */ + 830→ 830→export function nextFragmentThreshold(totalDecoded: number): number | null { + 831→ 831→ for (const f of FRAGMENTS) { + 832→ 832→ if (totalDecoded < f.threshold) return f.threshold; + 833→ 833→ } + 834→ 834→ return null; + 835→ 835→} + 836→ 836→ + 837→ 837→export { FRAGMENTS, PRESTIGE, TECH_TREE }; + 838→ 838→ + 839→ 839→// 开发期调试:暴露 store 到 window,便于 QA 测试 + 840→ 840→if (typeof window !== "undefined" && process.env.NODE_ENV !== "production") { + 841→ 841→ (window as unknown as { __gameStore?: typeof useGameStore }).__gameStore = useGameStore; + 842→ 842→} + 843→ 843→ \ No newline at end of file