Files
echo-nexus/tool-results/read_1782262268678_852a10fd13a6.txt
T

609 lines
40 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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→ <div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
121→ 121→ <div className="animate-pulse">唤醒回响中…</div>
122→ 122→ </div>
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→ <div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
165→ 165→ {/* 星空背景 */}
166→ 166→ <StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
167→ 167→ {/* 星潮背景叠层 */}
168→ 168→ <StarTideOverlay />
169→ 169→
170→ 170→ {/* 顶部 Header */}
171→ 171→ <header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
172→ 172→ <div className="flex items-center gap-3 mb-2.5">
173→ 173→ <div className="flex items-center gap-2.5">
174→ 174→ <div className="relative h-9 w-9">
175→ 175→ <div className="absolute inset-0 rounded-full bg-gradient-to-br from-emerald-400 via-fuchsia-500 to-rose-400 blur-md opacity-60" />
176→ 176→ <div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
177→ 177→ <Sparkles className="h-4 w-4 text-fuchsia-300" />
178→ 178→ </div>
179→ 179→ </div>
180→ 180→ <div className="leading-tight">
181→ 181→ <h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
182→ 182→ <p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
183→ 183→ ECHO NEXUS · v0.8
184→ 184→ </p>
185→ 185→ </div>
186→ 186→ </div>
187→ 187→ <div className="ml-auto flex items-center gap-1.5">
188→ 188→ <StarTideIndicator />
189→ 189→ <Button
190→ 190→ size="sm"
191→ 191→ variant="outline"
192→ 192→ onClick={() => setCruiseOpen(true)}
193→ 193→ className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
194→ 194→ aria-label="深空巡航"
195→ 195→ title="深空巡航 · 实时玩法"
196→ 196→ >
197→ 197→ <Navigation className="h-3.5 w-3.5 mr-1" />
198→ 198→ 巡航
199→ 199→ </Button>
200→ 200→ <Button
201→ 201→ size="icon"
202→ 202→ variant="ghost"
203→ 203→ onClick={() => setChronicleOpen(true)}
204→ 204→ className="h-8 w-8 relative group"
205→ 205→ aria-label="编年史"
206→ 206→ title="回响编年史"
207→ 207→ >
208→ 208→ <BookOpen className="h-4 w-4 group-hover:text-fuchsia-300 transition-colors" />
209→ 209→ {chronicleCount > 0 && (
210→ 210→ <span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-1 rounded-full bg-fuchsia-500 text-[9px] font-mono font-bold text-white flex items-center justify-center border border-fuchsia-300/50">
211→ 211→ {chronicleCount}
212→ 212→ </span>
213→ 213→ )}
214→ 214→ </Button>
215→ 215→ {hasPendingPerk && (
216→ 216→ <Button
217→ 217→ size="sm"
218→ 218→ variant="outline"
219→ 219→ onClick={() => setConstellationOpen(true)}
220→ 220→ className="border-fuchsia-400/60 text-fuchsia-200 hover:bg-fuchsia-500/15 h-8 px-2.5 animate-pulse"
221→ 221→ >
222→ 222→ <Star className="h-3.5 w-3.5 mr-1" />
223→ 223→ 觉醒
224→ 224→ </Button>
225→ 225→ )}
226→ 226→ {canPrestige && (
227→ 227→ <Button
228→ 228→ size="sm"
229→ 229→ variant="outline"
230→ 230→ onClick={() => setPrestigeOpen(true)}
231→ 231→ data-tut="prestige-btn"
232→ 232→ className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
233→ 233→ >
234→ 234→ <RotateCcw className="h-3.5 w-3.5 mr-1" />
235→ 235→ 飞升
236→ 236→ </Button>
237→ 237→ )}
238→ 238→ <Button
239→ 239→ size="icon"
240→ 240→ variant="ghost"
241→ 241→ onClick={() => setSettingsOpen(true)}
242→ 242→ className="h-8 w-8"
243→ 243→ aria-label="设置"
244→ 244→ >
245→ 245→ <Settings className="h-4 w-4" />
246→ 246→ </Button>
247→ 247→ </div>
248→ 248→ </div>
249→ 249→ <ResourceBar onPrestige={() => setPrestigeOpen(true)} />
250→ 250→ </header>
251→ 251→
252→ 252→ {/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
253→ 253→ <main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
254→ 254→ <div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
255→ 255→ {/* 左侧:脉冲晶体 */}
256→ 256→ <section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[340px] sm:min-h-[420px] relative overflow-hidden">
257→ 257→ {/* 装饰光圈 */}
258→ 258→ <div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
259→ 259→ <div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
260→ 260→ {/* v0.8.2 装饰全息环(填充留白,提升视觉层次) */}
261→ 261→ <div className="pointer-events-none absolute inset-6 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
262→ 262→ <div className="pointer-events-none absolute inset-10 rounded-full border border-dashed border-fuchsia-400/10 animate-[spin_90s_linear_infinite_reverse]" />
263→ 263→ <div className="pointer-events-none absolute inset-16 rounded-full border border-rose-400/8" />
264→ 264→ {/* 四角全息标记 */}
265→ 265→ <div className="pointer-events-none absolute top-3 left-3 h-4 w-4 border-l border-t border-emerald-400/30 rounded-tl" />
266→ 266→ <div className="pointer-events-none absolute top-3 right-3 h-4 w-4 border-r border-t border-fuchsia-400/30 rounded-tr" />
267→ 267→ <div className="pointer-events-none absolute bottom-3 left-3 h-4 w-4 border-l border-b border-amber-400/30 rounded-bl" />
268→ 268→ <div className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 border-r border-b border-rose-400/30 rounded-br" />
269→ 269→ {/* 顶部状态条 */}
270→ 270→ <div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 flex items-center gap-2 text-[9px] font-mono text-muted-foreground/60">
271→ 271→ <span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
272→ 272→ <span>CRYSTAL CORE · ONLINE</span>
273→ 273→ <span className="h-1 w-1 rounded-full bg-fuchsia-400 animate-pulse" />
274→ 274→ </div>
275→ 275→ {/* 底部铭文 */}
276→ 276→ <div className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 text-[8px] font-mono text-muted-foreground/40 tracking-[0.3em] uppercase">
277→ 277→ echo · nexus · archaeology
278→ 278→ </div>
279→ 279→ <div data-tut="crystal-orb" className="contents">
280→ 280→ <CrystalOrb />
281→ 281→ </div>
282→ 282→ {/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
283→ 283→ <CrystalYieldPanel />
284→ 284→ </section>
285→ 285→
286→ 286→ {/* 右侧:解码 + 标签面板 */}
287→ 287→ <section className="flex flex-col gap-3 min-h-0">
288→ 288→ {/* 解码面板 */}
289→ 289→ <div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[300px] sm:min-h-[360px] max-h-[560px]">
290→ 290→ <DecodePanel />
291→ 291→ </div>
292→ 292→ {/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
293→ 293→ {/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
294→ 294→ <div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
295→ 295→ <Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
296→ 296→ {/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
297→ 297→ <TabsList className="grid grid-cols-4 sm:grid-cols-8 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
298→ 298→ <TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
299→ 299→ <Rocket className="h-3.5 w-3.5" />
300→ 300→ <span className="leading-none">探险</span>
301→ 301→ {energy >= 1 && !hasActiveExpedition && (
302→ 302→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
303→ 303→ )}
304→ 304→ {hasActiveExpedition && (
305→ 305→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
306→ 306→ )}
307→ 307→ </TabsTrigger>
308→ 308→ <TabsTrigger value="tech" data-tut="tab-tech" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
309→ 309→ <Cpu className="h-3.5 w-3.5" />
310→ 310→ <span className="leading-none">技术</span>
311→ 311→ </TabsTrigger>
312→ 312→ <TabsTrigger value="constellation" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-fuchsia-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
313→ 313→ <Star className="h-3.5 w-3.5" />
314→ 314→ <span className="leading-none">星图</span>
315→ 315→ {hasPendingPerk && (
316→ 316→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-fuchsia-400 animate-pulse ring-1 ring-black/50" />
317→ 317→ )}
318→ 318→ </TabsTrigger>
319→ 319→ <TabsTrigger value="codex" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-cyan-500/15 data-[state=active]:shadow-[0_0_12px_rgba(34,211,238,0.3)] transition-all">
320→ 320→ <BookOpen className="h-3.5 w-3.5" />
321→ 321→ <span className="leading-none">图谱</span>
322→ 322→ </TabsTrigger>
323→ 323→ <TabsTrigger value="ach" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
324→ 324→ <Trophy className="h-3.5 w-3.5" />
325→ 325→ <span className="leading-none">成就</span>
326→ 326→ {ownedAchCount < ACHIEVEMENTS.length && (
327→ 327→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
328→ 328→ )}
329→ 329→ </TabsTrigger>
330→ 330→ <TabsTrigger value="beacon" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
331→ 331→ <Radio className="h-3.5 w-3.5" />
332→ 332→ <span className="leading-none">信标</span>
333→ 333→ {beaconClaimable && (
334→ 334→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-emerald-400 animate-pulse ring-1 ring-black/50" />
335→ 335→ )}
336→ 336→ </TabsTrigger>
337→ 337→ <TabsTrigger value="stats" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-slate-500/15 data-[state=active]:shadow-[0_0_12px_rgba(148,163,184,0.3)] transition-all">
338→ 338→ <BarChart3 className="h-3.5 w-3.5" />
339→ 339→ <span className="leading-none">统计</span>
340→ 340→ </TabsTrigger>
341→ 341→ <TabsTrigger value="attributes" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-gradient-to-br data-[state=active]:from-emerald-500/15 data-[state=active]:via-fuchsia-500/15 data-[state=active]:to-rose-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
342→ 342→ <User className="h-3.5 w-3.5" />
343→ 343→ <span className="leading-none">角色</span>
344→ 344→ {pendingAttrPoints > 0 && (
345→ 345→ <span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
346→ 346→ )}
347→ 347→ </TabsTrigger>
348→ 348→ </TabsList>
349→ 349→ <TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
350→ 350→ <ExpeditionPanel />
351→ 351→ </TabsContent>
352→ 352→ <TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
353→ 353→ <TechTree />
354→ 354→ </TabsContent>
355→ 355→ <TabsContent value="constellation" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
356→ 356→ <ConstellationPanel />
357→ 357→ </TabsContent>
358→ 358→ <TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
359→ 359→ <Codex />
360→ 360→ </TabsContent>
361→ 361→ <TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
362→ 362→ <AchievementsPanel />
363→ 363→ </TabsContent>
364→ 364→ <TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
365→ 365→ <BeaconPanel />
366→ 366→ </TabsContent>
367→ 367→ <TabsContent value="stats" className="flex-1 mt-2 min-h-0">
368→ 368→ <StatsPanel />
369→ 369→ </TabsContent>
370→ 370→ <TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
371→ 371→ <AttributesPanel />
372→ 372→ </TabsContent>
373→ 373→ </Tabs>
374→ 374→ </div>
375→ 375→ </section>
376→ 376→ </div>
377→ 377→ </main>
378→ 378→
379→ 379→ {/* 底部 Footer */}
380→ 380→ <footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
381→ 381→ <div
382→ 382→ className={`glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs ${
383→ 383→ warehouseFull ? "border-amber-400/40 animate-pulse" : ""
384→ 384→ } ${activeTide ? "border-white/20" : ""}`}
385→ 385→ style={
386→ 386→ activeTide
387→ 387→ ? { boxShadow: `inset 0 0 16px ${TIDE_EVENTS[activeTide.type].glow}` }
388→ 388→ : undefined
389→ 389→ }
390→ 390→ >
391→ 391→ <span
392→ 392→ className={
393→ 393→ warehouseFull
394→ 394→ ? "text-amber-400"
395→ 395→ : activeTide
396→ 396→ ? ""
397→ 397→ : "text-fuchsia-300"
398→ 398→ }
399→ 399→ style={activeTide ? { color: TIDE_EVENTS[activeTide.type].color } : undefined}
400→ 400→ >
401→ 401→ ▶
402→ 402→ </span>
403→ 403→ <span
404→ 404→ className={`flex-1 truncate ${
405→ 405→ warehouseFull ? "text-amber-200" : "text-muted-foreground"
406→ 406→ }`}
407→ 407→ >
408→ 408→ {goal}
409→ 409→ </span>
410→ 410→ <span className="hidden sm:inline text-muted-foreground/60">|</span>
411→ 411→ <span className="hidden sm:inline text-muted-foreground/70">
412→ 412→ 产能 {formatNum(crystalsPerSec)}/s
413→ 413→ </span>
414→ 414→ <a
415→ 415→ href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
416→ 416→ target="_blank"
417→ 417→ rel="noreferrer"
418→ 418→ className="text-muted-foreground/60 hover:text-foreground transition ml-1"
419→ 419→ aria-label="仓库"
420→ 420→ >
421→ 421→ <Github className="h-3.5 w-3.5" />
422→ 422→ </a>
423→ 423→ </div>
424→ 424→ </footer>
425→ 425→
426→ 426→ <PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
427→ 427→ <ChronicleDialog open={chronicleOpen} onOpenChange={setChronicleOpen} />
428→ 428→ <ConstellationDialog open={constellationOpen} onOpenChange={setConstellationOpen} />
429→ 429→ <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
430→ 430→ <AchievementNotifier />
431→ 431→ <StarTideNotifier />
432→ 432→ <TutorialOverlay />
433→ 433→ <OfflineReportDialog />
434→ 434→ {cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
435→ 435→ </div>
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→ <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
462→ 462→ {/* 晶体产能 — emerald */}
463→ 463→ <div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
464→ 464→ <Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
465→ 465→ <div className="min-w-0 leading-tight">
466→ 466→ <div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
467→ 467→ 晶体产能
468→ 468→ </div>
469→ 469→ <div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
470→ 470→ {formatNum(crystalsPerSec)}
471→ 471→ <span className="text-[9px] text-emerald-300/60">/s</span>
472→ 472→ </div>
473→ 473→ </div>
474→ 474→ </div>
475→ 475→
476→ 476→ {/* 主动脉冲 — rose */}
477→ 477→ <div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
478→ 478→ <Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
479→ 479→ <div className="min-w-0 leading-tight">
480→ 480→ <div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
481→ 481→ 主动脉冲
482→ 482→ </div>
483→ 483→ <div className="text-sm font-mono tabular-nums text-rose-200 truncate">
484→ 484→ {formatNum(pulsePower)}
485→ 485→ {combo >= 1 && (
486→ 486→ <span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
487→ 487→ )}
488→ 488→ </div>
489→ 489→ </div>
490→ 490→ </div>
491→ 491→
492→ 492→ {/* 当前连击 — amber,≥3 时高亮闪烁 */}
493→ 493→ <div
494→ 494→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
495→ 495→ comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
496→ 496→ }`}
497→ 497→ >
498→ 498→ <Flame
499→ 499→ className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
500→ 500→ comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
501→ 501→ }`}
502→ 502→ />
503→ 503→ <div className="min-w-0 leading-tight">
504→ 504→ <div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
505→ 505→ 当前连击
506→ 506→ </div>
507→ 507→ <div
508→ 508→ className={`text-sm font-mono tabular-nums truncate ${
509→ 509→ comboHot ? "text-amber-200" : "text-amber-300/80"
510→ 510→ }`}
511→ 511→ >
512→ 512→ {combo}
513→ 513→ <span className="text-[9px] text-amber-300/60">/10</span>
514→ 514→ </div>
515→ 515→ </div>
516→ 516→ </div>
517→ 517→
518→ 518→ {/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
519→ 519→ <div
520→ 520→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
521→ 521→ warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
522→ 522→ }`}
523→ 523→ >
524→ 524→ <Database
525→ 525→ className={`h-3.5 w-3.5 shrink-0 ${
526→ 526→ warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
527→ 527→ }`}
528→ 528→ />
529→ 529→ <div className="min-w-0 leading-tight">
530→ 530→ <div
531→ 531→ className={`text-[9px] uppercase tracking-wider truncate ${
532→ 532→ warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
533→ 533→ }`}
534→ 534→ >
535→ 535→ 仓库容量
536→ 536→ </div>
537→ 537→ <div
538→ 538→ className={`text-sm font-mono tabular-nums truncate ${
539→ 539→ warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
540→ 540→ }`}
541→ 541→ >
542→ 542→ {formatNum(crystals)}
543→ 543→ <span
544→ 544→ className={`text-[9px] ${
545→ 545→ warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
546→ 546→ }`}
547→ 547→ >
548→ 548→ /{formatNum(crystalCap)}
549→ 549→ </span>
550→ 550→ </div>
551→ 551→ </div>
552→ 552→ </div>
553→ 553→ </div>
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<number | null>(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→ <div className="grid grid-cols-2 gap-1.5 text-xs">
597→ 597→ {rows.map((r) => (
598→ 598→ <div
599→ 599→ key={r.label}
600→ 600→ className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
601→ 601→ >
602→ 602→ <span className="text-muted-foreground">{r.label}</span>
603→ 603→ <span className="font-mono text-foreground">{r.value}</span>
604→ 604→ </div>
605→ 605→ ))}
606→ 606→ </div>
607→ 607→ );
608→ 608→}
609→ 609→