1→ 1→"use client"; 2→ 2→// 回响星核 / Echo Nexus — 游戏主入口 3→ 3→import { useState, useEffect } from "react"; 4→ 4→import { StarfieldCanvas } from "@/components/game/StarfieldCanvas"; 5→ 5→import { ResourceBar } from "@/components/game/ResourceBar"; 6→ 6→import { CrystalOrb } from "@/components/game/CrystalOrb"; 7→ 7→import { DecodePanel } from "@/components/game/DecodeArray"; 8→ 8→import { TechTree } from "@/components/game/TechTree"; 9→ 9→import { Codex } from "@/components/game/Codex"; 10→ 10→import { PrestigeDialog } from "@/components/game/PrestigeDialog"; 11→ 11→import { SettingsDialog } from "@/components/game/SettingsDialog"; 12→ 12→import { ExpeditionPanel } from "@/components/game/ExpeditionPanel"; 13→ 13→import { AchievementsPanel } from "@/components/game/AchievementsPanel"; 14→ 14→import { AchievementNotifier } from "@/components/game/AchievementNotifier"; 15→ 15→import { ConstellationPanel } from "@/components/game/ConstellationPanel"; 16→ 16→import { ConstellationDialog } from "@/components/game/ConstellationDialog"; 17→ 17→import { ChronicleDialog } from "@/components/game/ChronicleDialog"; 18→ 18→import { BeaconPanel } from "@/components/game/BeaconPanel"; 19→ 19→import { TutorialOverlay } from "@/components/game/TutorialOverlay"; 20→ 20→import { OfflineReportDialog } from "@/components/game/OfflineReportDialog"; 21→ 21→import { CruiseMode } from "@/components/game/CruiseMode"; 22→ 22→import { AttributesPanel } from "@/components/game/AttributesPanel"; 23→ 23→import { 24→ 24→ StarTideNotifier, 25→ 25→ StarTideIndicator, 26→ 26→ StarTideOverlay, 27→ 27→} from "@/components/game/StarTide"; 28→ 28→import { useGameLoop } from "@/hooks/useGameLoop"; 29→ 29→import { useAudioSync } from "@/hooks/useAudio"; 30→ 30→import { useGlobalTide } from "@/hooks/useGlobalTide"; 31→ 31→import { useGameStore } from "@/store/gameStore"; 32→ 32→import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 33→ 33→import { Button } from "@/components/ui/button"; 34→ 34→import { 35→ 35→ Settings, 36→ 36→ RotateCcw, 37→ 37→ Sparkles, 38→ 38→ Cpu, 39→ 39→ BookOpen, 40→ 40→ BarChart3, 41→ 41→ Rocket, 42→ 42→ Github, 43→ 43→ Trophy, 44→ 44→ Star, 45→ 45→ Radio, 46→ 46→ Navigation, 47→ 47→ User, 48→ 48→ Gem, 49→ 49→ Zap, 50→ 50→ Flame, 51→ 51→ Database, 52→ 52→} from "lucide-react"; 53→ 53→import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config"; 54→ 54→import { ACHIEVEMENTS } from "@/lib/game/achievements"; 55→ 55→import { TIDE_EVENTS } from "@/lib/game/starTide"; 56→ 56→import { CONSTELLATION_PERKS } from "@/lib/game/constellation"; 57→ 57→import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon"; 58→ 58→ 59→ 59→export default function Page() { 60→ 60→ useGameLoop(); 61→ 61→ useAudioSync(); 62→ 62→ useGlobalTide(); 63→ 63→ const [prestigeOpen, setPrestigeOpen] = useState(false); 64→ 64→ const [settingsOpen, setSettingsOpen] = useState(false); 65→ 65→ const [constellationOpen, setConstellationOpen] = useState(false); 66→ 66→ const [chronicleOpen, setChronicleOpen] = useState(false); 67→ 67→ const [cruiseOpen, setCruiseOpen] = useState(false); 68→ 68→ const [mounted, setMounted] = useState(false); 69→ 69→ 70→ 70→ const contact = useGameStore((s) => s.contact); 71→ 71→ const totalDecoded = useGameStore((s) => s.totalDecoded); 72→ 72→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec); 73→ 73→ const ascensions = useGameStore((s) => s.ascensions); 74→ 74→ const ownedTech = useGameStore((s) => s.tech); 75→ 75→ const ownedFragments = useGameStore((s) => s.fragments); 76→ 76→ const ownedAchievements = useGameStore((s) => s.achievements); 77→ 77→ const ownedConstellation = useGameStore((s) => s.constellation ?? []); 78→ 78→ const pendingPerkChoices = useGameStore((s) => s.pendingPerkChoices); 79→ 79→ const crystals = useGameStore((s) => s.crystals); 80→ 80→ const crystalCap = useGameStore((s) => s.crystalCap); 81→ 81→ const activeTide = useGameStore((s) => s.activeTide); 82→ 82→ const globalTide = useGameStore((s) => s.globalTide); 83→ 83→ const energy = useGameStore((s) => s.energy); 84→ 84→ const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished); 85→ 85→ const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0); 86→ 86→ const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0); 87→ 87→ 88→ 88→ // 深空信标:检测是否有可领取的奖励(独立 localStorage) 89→ 89→ const [beaconClaimable, setBeaconClaimable] = useState(false); 90→ 90→ useEffect(() => { 91→ 91→ let cancelled = false; 92→ 92→ const check = () => { 93→ 93→ try { 94→ 94→ const c = generateDailyChallenge(); 95→ 95→ const p = loadDailyProgress(); 96→ 96→ if (!cancelled) { 97→ 97→ setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey); 98→ 98→ } 99→ 99→ } catch { 100→ 100→ if (!cancelled) setBeaconClaimable(false); 101→ 101→ } 102→ 102→ }; 103→ 103→ check(); 104→ 104→ const id = setInterval(check, 2000); 105→ 105→ return () => { 106→ 106→ cancelled = true; 107→ 107→ clearInterval(id); 108→ 108→ }; 109→ 109→ }, []); 110→ 110→ 111→ 111→ // 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致 112→ 112→ useEffect(() => { 113→ 113→ // eslint-disable-next-line react-hooks/set-state-in-effect 114→ 114→ setMounted(true); 115→ 115→ }, []); 116→ 116→ 117→ 117→ // 防止 SSR/CSR 不一致 118→ 118→ if (!mounted) { 119→ 119→ return ( 120→ 120→
121→ 121→
唤醒回响中…
122→ 122→
123→ 123→ ); 124→ 124→ } 125→ 125→ 126→ 126→ const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length; 127→ 127→ const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length; 128→ 128→ const ownedAchCount = Object.values(ownedAchievements).filter(Boolean).length; 129→ 129→ const canPrestige = contact >= 100; 130→ 130→ const hasPendingPerk = pendingPerkChoices && pendingPerkChoices.length > 0; 131→ 131→ 132→ 132→ // 仓库满仓警告 133→ 133→ const warehouseFull = crystals >= crystalCap * 0.98; 134→ 134→ 135→ 135→ // 目标提示 136→ 136→ let goal = "点击中央晶体发起脉冲,累积记忆晶体"; 137→ 137→ if (totalDecoded === 0 && crystals >= 5) { 138→ 138→ goal = "右侧「待解码晶体」点击一颗晶体,开始解码"; 139→ 139→ } else if (totalDecoded > 0 && ownedTechCount === 0) { 140→ 140→ goal = "用洞见解锁技术树,提升产能"; 141→ 141→ } else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) { 142→ 142→ goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length})`; 143→ 143→ } 144→ 144→ if (energy >= 1 && totalDecoded >= 3) { 145→ 145→ goal = "「探险」标签可深入遗迹,获取丰厚奖励"; 146→ 146→ } 147→ 147→ if (ascensions >= 1 && ownedConstellation.length > 0) { 148→ 148→ goal = `星图天赋已觉醒 ${ownedConstellation.length}/18,继续飞升获取更多`; 149→ 149→ } 150→ 150→ if (warehouseFull) { 151→ 151→ goal = "⚠ 仓库已满,产能浪费中!请解码晶体或升级仓库"; 152→ 152→ } 153→ 153→ if (hasPendingPerk) { 154→ 154→ goal = "✦ 星图觉醒!点击顶部「星图」按钮选择一道天赋"; 155→ 155→ } 156→ 156→ if (activeTide) { 157→ 157→ const tm = TIDE_EVENTS[activeTide.type]; 158→ 158→ const prefix = globalTide ? "🌐 全球星潮 · " : ""; 159→ 159→ goal = `${prefix}${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`; 160→ 160→ } 161→ 161→ if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目"; 162→ 162→ 163→ 163→ return ( 164→ 164→
165→ 165→ {/* 星空背景 */} 166→ 166→ 167→ 167→ {/* 星潮背景叠层 */} 168→ 168→ 169→ 169→ 170→ 170→ {/* 顶部 Header */} 171→ 171→
172→ 172→
173→ 173→
174→ 174→
175→ 175→
176→ 176→
177→ 177→ 178→ 178→
179→ 179→
180→ 180→
181→ 181→

回响星核

182→ 182→

183→ 183→ ECHO NEXUS · v0.8 184→ 184→

185→ 185→
186→ 186→
187→ 187→
188→ 188→ 189→ 189→ 200→ 200→ 215→ 215→ {hasPendingPerk && ( 216→ 216→ 225→ 225→ )} 226→ 226→ {canPrestige && ( 227→ 227→ 237→ 237→ )} 238→ 238→ 247→ 247→
248→ 248→
249→ 249→ setPrestigeOpen(true)} /> 250→ 250→
251→ 251→ 252→ 252→ {/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */} 253→ 253→
254→ 254→
255→ 255→ {/* 左侧:脉冲晶体 */} 256→ 256→
257→ 257→ {/* 装饰光圈 */} 258→ 258→
259→ 259→
260→ 260→ {/* v0.8.2 装饰全息环(填充留白,提升视觉层次) */} 261→ 261→
262→ 262→
263→ 263→
264→ 264→ {/* 四角全息标记 */} 265→ 265→
266→ 266→
267→ 267→
268→ 268→
269→ 269→ {/* 顶部状态条 */} 270→ 270→
271→ 271→ 272→ 272→ CRYSTAL CORE · ONLINE 273→ 273→ 274→ 274→
275→ 275→ {/* 底部铭文 */} 276→ 276→
277→ 277→ echo · nexus · archaeology 278→ 278→
279→ 279→
280→ 280→ 281→ 281→
282→ 282→ {/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */} 283→ 283→ 284→ 284→
285→ 285→ 286→ 286→ {/* 右侧:解码 + 标签面板 */} 287→ 287→
288→ 288→ {/* 解码面板 */} 289→ 289→
290→ 290→ 291→ 291→
292→ 292→ {/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */} 293→ 293→ {/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */} 294→ 294→
295→ 295→ 296→ 296→ {/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */} 297→ 297→ 298→ 298→ 299→ 299→ 300→ 300→ 探险 301→ 301→ {energy >= 1 && !hasActiveExpedition && ( 302→ 302→ 303→ 303→ )} 304→ 304→ {hasActiveExpedition && ( 305→ 305→ 306→ 306→ )} 307→ 307→ 308→ 308→ 309→ 309→ 310→ 310→ 技术 311→ 311→ 312→ 312→ 313→ 313→ 314→ 314→ 星图 315→ 315→ {hasPendingPerk && ( 316→ 316→ 317→ 317→ )} 318→ 318→ 319→ 319→ 320→ 320→ 321→ 321→ 图谱 322→ 322→ 323→ 323→ 324→ 324→ 325→ 325→ 成就 326→ 326→ {ownedAchCount < ACHIEVEMENTS.length && ( 327→ 327→ 328→ 328→ )} 329→ 329→ 330→ 330→ 331→ 331→ 332→ 332→ 信标 333→ 333→ {beaconClaimable && ( 334→ 334→ 335→ 335→ )} 336→ 336→ 337→ 337→ 338→ 338→ 339→ 339→ 统计 340→ 340→ 341→ 341→ 342→ 342→ 343→ 343→ 角色 344→ 344→ {pendingAttrPoints > 0 && ( 345→ 345→ 346→ 346→ )} 347→ 347→ 348→ 348→ 349→ 349→ 350→ 350→ 351→ 351→ 352→ 352→ 353→ 353→ 354→ 354→ 355→ 355→ 356→ 356→ 357→ 357→ 358→ 358→ 359→ 359→ 360→ 360→ 361→ 361→ 362→ 362→ 363→ 363→ 364→ 364→ 365→ 365→ 366→ 366→ 367→ 367→ 368→ 368→ 369→ 369→ 370→ 370→ 371→ 371→ 372→ 372→ 373→ 373→ 374→ 374→
375→ 375→
376→ 376→
377→ 377→
378→ 378→ 379→ 379→ {/* 底部 Footer */} 380→ 380→
381→ 381→
391→ 391→ 401→ 401→ ▶ 402→ 402→ 403→ 403→ 408→ 408→ {goal} 409→ 409→ 410→ 410→ | 411→ 411→ 412→ 412→ 产能 {formatNum(crystalsPerSec)}/s 413→ 413→ 414→ 414→ 421→ 421→ 422→ 422→ 423→ 423→
424→ 424→
425→ 425→ 426→ 426→ 427→ 427→ 428→ 428→ 429→ 429→ 430→ 430→ 431→ 431→ 432→ 432→ 433→ 433→ 434→ 434→ {cruiseOpen && setCruiseOpen(false)} />} 435→ 435→
436→ 436→ ); 437→ 437→} 438→ 438→ 439→ 439→/** 440→ 440→ * v0.9 工单 #2:全息收益信息面板 441→ 441→ * 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库), 442→ 442→ * 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。 443→ 443→ * 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。 444→ 444→ */ 445→ 445→function CrystalYieldPanel() { 446→ 446→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec); 447→ 447→ const pulsePower = useGameStore((s) => s.pulsePower); 448→ 448→ const combo = useGameStore((s) => s._combo); 449→ 449→ const crystals = useGameStore((s) => s.crystals); 450→ 450→ const crystalCap = useGameStore((s) => s.crystalCap); 451→ 451→ 452→ 452→ // 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效) 453→ 453→ const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1; 454→ 454→ const comboHot = combo >= 3; // 连击≥3 高亮闪烁 455→ 455→ 456→ 456→ // 仓库容量百分比 457→ 457→ const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0; 458→ 458→ const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse 459→ 459→ 460→ 460→ return ( 461→ 461→
462→ 462→ {/* 晶体产能 — emerald */} 463→ 463→
464→ 464→ 465→ 465→
466→ 466→
467→ 467→ 晶体产能 468→ 468→
469→ 469→
470→ 470→ {formatNum(crystalsPerSec)} 471→ 471→ /s 472→ 472→
473→ 473→
474→ 474→
475→ 475→ 476→ 476→ {/* 主动脉冲 — rose */} 477→ 477→
478→ 478→ 479→ 479→
480→ 480→
481→ 481→ 主动脉冲 482→ 482→
483→ 483→
484→ 484→ {formatNum(pulsePower)} 485→ 485→ {combo >= 1 && ( 486→ 486→ ×{comboMult.toFixed(2)} 487→ 487→ )} 488→ 488→
489→ 489→
490→ 490→
491→ 491→ 492→ 492→ {/* 当前连击 — amber,≥3 时高亮闪烁 */} 493→ 493→
498→ 498→ 503→ 503→
504→ 504→
505→ 505→ 当前连击 506→ 506→
507→ 507→
512→ 512→ {combo} 513→ 513→ /10 514→ 514→
515→ 515→
516→ 516→
517→ 517→ 518→ 518→ {/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */} 519→ 519→
524→ 524→ 529→ 529→
530→ 530→
535→ 535→ 仓库容量 536→ 536→
537→ 537→
542→ 542→ {formatNum(crystals)} 543→ 543→ 548→ 548→ /{formatNum(crystalCap)} 549→ 549→ 550→ 550→
551→ 551→
552→ 552→
553→ 553→
554→ 554→ ); 555→ 555→} 556→ 556→ 557→ 557→function StatsPanel() { 558→ 558→ const s = useGameStore(); 559→ 559→ const achCount = Object.values(s.achievements).filter(Boolean).length; 560→ 560→ // 深空信标本地排行榜最高分(独立 localStorage) 561→ 561→ const [beaconBest, setBeaconBest] = useState(null); 562→ 562→ useEffect(() => { 563→ 563→ try { 564→ 564→ const lb = loadLeaderboard(); 565→ 565→ // eslint-disable-next-line react-hooks/set-state-in-effect 566→ 566→ setBeaconBest(lb.length > 0 ? lb[0].score : null); 567→ 567→ } catch { 568→ 568→ setBeaconBest(null); 569→ 569→ } 570→ 570→ }, []); 571→ 571→ const rows = [ 572→ 572→ { label: "累计解码晶体", value: `${s.totalDecoded} 颗` }, 573→ 573→ { label: "飞升周目", value: `${s.ascensions}` }, 574→ 574→ { label: "持有蓝图", value: `${s.blueprints.length} / 6` }, 575→ 575→ { label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` }, 576→ 576→ { label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` }, 577→ 577→ { label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` }, 578→ 578→ { label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` }, 579→ 579→ { label: "接触进度", value: `${s.contact.toFixed(1)}%` }, 580→ 580→ { label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` }, 581→ 581→ { label: "仓库上限", value: `${formatNum(s.crystalCap)}` }, 582→ 582→ { label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` }, 583→ 583→ { label: "累计探险", value: `${s.totalExpeditions} 次` }, 584→ 584→ { label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` }, 585→ 585→ { label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 9` }, 586→ 586→ { label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` }, 587→ 587→ { label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` }, 588→ 588→ { label: "信标最高分", value: beaconBest !== null ? formatNum(beaconBest) : "—" }, 589→ 589→ { label: "探索力", value: `${s.attributes?.exploration ?? 0} / 100` }, 590→ 590→ { label: "智慧", value: `${s.attributes?.wisdom ?? 0} / 100` }, 591→ 591→ { label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` }, 592→ 592→ { label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` }, 593→ 593→ { label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` }, 594→ 594→ ]; 595→ 595→ return ( 596→ 596→
597→ 597→ {rows.map((r) => ( 598→ 598→
602→ 602→ {r.label} 603→ 603→ {r.value} 604→ 604→
605→ 605→ ))} 606→ 606→
607→ 607→ ); 608→ 608→} 609→ 609→