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→
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→
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→