3cf83ed6-dca1-44be-bf7a-a27d19337739

This commit is contained in:
2026-06-24 01:05:43 +00:00
parent 7cfbc8aba0
commit 23fb451a0b
10 changed files with 2157 additions and 93 deletions
@@ -0,0 +1,609 @@
1→"use client";
2→// 回响星核 / Echo Nexus — 游戏主入口
3→import { useState, useEffect } from "react";
4→import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
5→import { ResourceBar } from "@/components/game/ResourceBar";
6→import { CrystalOrb } from "@/components/game/CrystalOrb";
7→import { DecodePanel } from "@/components/game/DecodeArray";
8→import { TechTree } from "@/components/game/TechTree";
9→import { Codex } from "@/components/game/Codex";
10→import { PrestigeDialog } from "@/components/game/PrestigeDialog";
11→import { SettingsDialog } from "@/components/game/SettingsDialog";
12→import { ExpeditionPanel } from "@/components/game/ExpeditionPanel";
13→import { AchievementsPanel } from "@/components/game/AchievementsPanel";
14→import { AchievementNotifier } from "@/components/game/AchievementNotifier";
15→import { ConstellationPanel } from "@/components/game/ConstellationPanel";
16→import { ConstellationDialog } from "@/components/game/ConstellationDialog";
17→import { ChronicleDialog } from "@/components/game/ChronicleDialog";
18→import { BeaconPanel } from "@/components/game/BeaconPanel";
19→import { TutorialOverlay } from "@/components/game/TutorialOverlay";
20→import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
21→import { CruiseMode } from "@/components/game/CruiseMode";
22→import { AttributesPanel } from "@/components/game/AttributesPanel";
23→import {
24→ StarTideNotifier,
25→ StarTideIndicator,
26→ StarTideOverlay,
27→} from "@/components/game/StarTide";
28→import { useGameLoop } from "@/hooks/useGameLoop";
29→import { useAudioSync } from "@/hooks/useAudio";
30→import { useGlobalTide } from "@/hooks/useGlobalTide";
31→import { useGameStore } from "@/store/gameStore";
32→import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
33→import { Button } from "@/components/ui/button";
34→import {
35→ Settings,
36→ RotateCcw,
37→ Sparkles,
38→ Cpu,
39→ BookOpen,
40→ BarChart3,
41→ Rocket,
42→ Github,
43→ Trophy,
44→ Star,
45→ Radio,
46→ Navigation,
47→ User,
48→ Gem,
49→ Zap,
50→ Flame,
51→ Database,
52→} from "lucide-react";
53→import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
54→import { ACHIEVEMENTS } from "@/lib/game/achievements";
55→import { TIDE_EVENTS } from "@/lib/game/starTide";
56→import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
57→import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon";
58→
59→export default function Page() {
60→ useGameLoop();
61→ useAudioSync();
62→ useGlobalTide();
63→ const [prestigeOpen, setPrestigeOpen] = useState(false);
64→ const [settingsOpen, setSettingsOpen] = useState(false);
65→ const [constellationOpen, setConstellationOpen] = useState(false);
66→ const [chronicleOpen, setChronicleOpen] = useState(false);
67→ const [cruiseOpen, setCruiseOpen] = useState(false);
68→ const [mounted, setMounted] = useState(false);
69→
70→ const contact = useGameStore((s) => s.contact);
71→ const totalDecoded = useGameStore((s) => s.totalDecoded);
72→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
73→ const ascensions = useGameStore((s) => s.ascensions);
74→ const ownedTech = useGameStore((s) => s.tech);
75→ const ownedFragments = useGameStore((s) => s.fragments);
76→ const ownedAchievements = useGameStore((s) => s.achievements);
77→ const ownedConstellation = useGameStore((s) => s.constellation ?? []);
78→ const pendingPerkChoices = useGameStore((s) => s.pendingPerkChoices);
79→ const crystals = useGameStore((s) => s.crystals);
80→ const crystalCap = useGameStore((s) => s.crystalCap);
81→ const activeTide = useGameStore((s) => s.activeTide);
82→ const globalTide = useGameStore((s) => s.globalTide);
83→ const energy = useGameStore((s) => s.energy);
84→ const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
85→ const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
86→ const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
87→
88→ // 深空信标:检测是否有可领取的奖励(独立 localStorage
89→ const [beaconClaimable, setBeaconClaimable] = useState(false);
90→ useEffect(() => {
91→ let cancelled = false;
92→ const check = () => {
93→ try {
94→ const c = generateDailyChallenge();
95→ const p = loadDailyProgress();
96→ if (!cancelled) {
97→ setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey);
98→ }
99→ } catch {
100→ if (!cancelled) setBeaconClaimable(false);
101→ }
102→ };
103→ check();
104→ const id = setInterval(check, 2000);
105→ return () => {
106→ cancelled = true;
107→ clearInterval(id);
108→ };
109→ }, []);
110→
111→ // 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
112→ useEffect(() => {
113→ // eslint-disable-next-line react-hooks/set-state-in-effect
114→ setMounted(true);
115→ }, []);
116→
117→ // 防止 SSR/CSR 不一致
118→ if (!mounted) {
119→ return (
120→ <div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
121→ <div className="animate-pulse">唤醒回响中…</div>
122→ </div>
123→ );
124→ }
125→
126→ const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length;
127→ const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length;
128→ const ownedAchCount = Object.values(ownedAchievements).filter(Boolean).length;
129→ const canPrestige = contact >= 100;
130→ const hasPendingPerk = pendingPerkChoices && pendingPerkChoices.length > 0;
131→
132→ // 仓库满仓警告
133→ const warehouseFull = crystals >= crystalCap * 0.98;
134→
135→ // 目标提示
136→ let goal = "点击中央晶体发起脉冲,累积记忆晶体";
137→ if (totalDecoded === 0 && crystals >= 5) {
138→ goal = "右侧「待解码晶体」点击一颗晶体,开始解码";
139→ } else if (totalDecoded > 0 && ownedTechCount === 0) {
140→ goal = "用洞见解锁技术树,提升产能";
141→ } else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) {
142→ goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length}`;
143→ }
144→ if (energy >= 1 && totalDecoded >= 3) {
145→ goal = "「探险」标签可深入遗迹,获取丰厚奖励";
146→ }
147→ if (ascensions >= 1 && ownedConstellation.length > 0) {
148→ goal = `星图天赋已觉醒 ${ownedConstellation.length}/18,继续飞升获取更多`;
149→ }
150→ if (warehouseFull) {
151→ goal = "⚠ 仓库已满,产能浪费中!请解码晶体或升级仓库";
152→ }
153→ if (hasPendingPerk) {
154→ goal = "✦ 星图觉醒!点击顶部「星图」按钮选择一道天赋";
155→ }
156→ if (activeTide) {
157→ const tm = TIDE_EVENTS[activeTide.type];
158→ const prefix = globalTide ? "🌐 全球星潮 · " : "";
159→ goal = `${prefix}${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
160→ }
161→ if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
162→
163→ return (
164→ <div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
165→ {/* 星空背景 */}
166→ <StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
167→ {/* 星潮背景叠层 */}
168→ <StarTideOverlay />
169→
170→ {/* 顶部 Header */}
171→ <header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
172→ <div className="flex items-center gap-3 mb-2.5">
173→ <div className="flex items-center gap-2.5">
174→ <div className="relative h-9 w-9">
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→ <div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
177→ <Sparkles className="h-4 w-4 text-fuchsia-300" />
178→ </div>
179→ </div>
180→ <div className="leading-tight">
181→ <h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
182→ <p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
183→ ECHO NEXUS · v0.8
184→ </p>
185→ </div>
186→ </div>
187→ <div className="ml-auto flex items-center gap-1.5">
188→ <StarTideIndicator />
189→ <Button
190→ size="sm"
191→ variant="outline"
192→ onClick={() => setCruiseOpen(true)}
193→ className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
194→ aria-label="深空巡航"
195→ title="深空巡航 · 实时玩法"
196→ >
197→ <Navigation className="h-3.5 w-3.5 mr-1" />
198→ 巡航
199→ </Button>
200→ <Button
201→ size="icon"
202→ variant="ghost"
203→ onClick={() => setChronicleOpen(true)}
204→ className="h-8 w-8 relative group"
205→ aria-label="编年史"
206→ title="回响编年史"
207→ >
208→ <BookOpen className="h-4 w-4 group-hover:text-fuchsia-300 transition-colors" />
209→ {chronicleCount > 0 && (
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→ {chronicleCount}
212→ </span>
213→ )}
214→ </Button>
215→ {hasPendingPerk && (
216→ <Button
217→ size="sm"
218→ variant="outline"
219→ onClick={() => setConstellationOpen(true)}
220→ className="border-fuchsia-400/60 text-fuchsia-200 hover:bg-fuchsia-500/15 h-8 px-2.5 animate-pulse"
221→ >
222→ <Star className="h-3.5 w-3.5 mr-1" />
223→ 觉醒
224→ </Button>
225→ )}
226→ {canPrestige && (
227→ <Button
228→ size="sm"
229→ variant="outline"
230→ onClick={() => setPrestigeOpen(true)}
231→ data-tut="prestige-btn"
232→ className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
233→ >
234→ <RotateCcw className="h-3.5 w-3.5 mr-1" />
235→ 飞升
236→ </Button>
237→ )}
238→ <Button
239→ size="icon"
240→ variant="ghost"
241→ onClick={() => setSettingsOpen(true)}
242→ className="h-8 w-8"
243→ aria-label="设置"
244→ >
245→ <Settings className="h-4 w-4" />
246→ </Button>
247→ </div>
248→ </div>
249→ <ResourceBar onPrestige={() => setPrestigeOpen(true)} />
250→ </header>
251→
252→ {/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
253→ <main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
254→ <div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
255→ {/* 左侧:脉冲晶体 */}
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→ {/* 装饰光圈 */}
258→ <div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
259→ <div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
260→ {/* v0.8.2 装饰全息环(填充留白,提升视觉层次) */}
261→ <div className="pointer-events-none absolute inset-6 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
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→ <div className="pointer-events-none absolute inset-16 rounded-full border border-rose-400/8" />
264→ {/* 四角全息标记 */}
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→ <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→ <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→ <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→ {/* 顶部状态条 */}
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→ <span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
272→ <span>CRYSTAL CORE · ONLINE</span>
273→ <span className="h-1 w-1 rounded-full bg-fuchsia-400 animate-pulse" />
274→ </div>
275→ {/* 底部铭文 */}
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→ echo · nexus · archaeology
278→ </div>
279→ <div data-tut="crystal-orb" className="contents">
280→ <CrystalOrb />
281→ </div>
282→ {/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
283→ <CrystalYieldPanel />
284→ </section>
285→
286→ {/* 右侧:解码 + 标签面板 */}
287→ <section className="flex flex-col gap-3 min-h-0">
288→ {/* 解码面板 */}
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→ <DecodePanel />
291→ </div>
292→ {/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
293→ {/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
294→ <div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
295→ <Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
296→ {/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
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→ <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→ <Rocket className="h-3.5 w-3.5" />
300→ <span className="leading-none">探险</span>
301→ {energy >= 1 && !hasActiveExpedition && (
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→ )}
304→ {hasActiveExpedition && (
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→ )}
307→ </TabsTrigger>
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→ <Cpu className="h-3.5 w-3.5" />
310→ <span className="leading-none">技术</span>
311→ </TabsTrigger>
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→ <Star className="h-3.5 w-3.5" />
314→ <span className="leading-none">星图</span>
315→ {hasPendingPerk && (
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→ )}
318→ </TabsTrigger>
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→ <BookOpen className="h-3.5 w-3.5" />
321→ <span className="leading-none">图谱</span>
322→ </TabsTrigger>
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→ <Trophy className="h-3.5 w-3.5" />
325→ <span className="leading-none">成就</span>
326→ {ownedAchCount < ACHIEVEMENTS.length && (
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→ )}
329→ </TabsTrigger>
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→ <Radio className="h-3.5 w-3.5" />
332→ <span className="leading-none">信标</span>
333→ {beaconClaimable && (
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→ )}
336→ </TabsTrigger>
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→ <BarChart3 className="h-3.5 w-3.5" />
339→ <span className="leading-none">统计</span>
340→ </TabsTrigger>
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→ <User className="h-3.5 w-3.5" />
343→ <span className="leading-none">角色</span>
344→ {pendingAttrPoints > 0 && (
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→ )}
347→ </TabsTrigger>
348→ </TabsList>
349→ <TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
350→ <ExpeditionPanel />
351→ </TabsContent>
352→ <TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
353→ <TechTree />
354→ </TabsContent>
355→ <TabsContent value="constellation" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
356→ <ConstellationPanel />
357→ </TabsContent>
358→ <TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
359→ <Codex />
360→ </TabsContent>
361→ <TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
362→ <AchievementsPanel />
363→ </TabsContent>
364→ <TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
365→ <BeaconPanel />
366→ </TabsContent>
367→ <TabsContent value="stats" className="flex-1 mt-2 min-h-0">
368→ <StatsPanel />
369→ </TabsContent>
370→ <TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
371→ <AttributesPanel />
372→ </TabsContent>
373→ </Tabs>
374→ </div>
375→ </section>
376→ </div>
377→ </main>
378→
379→ {/* 底部 Footer */}
380→ <footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
381→ <div
382→ className={`glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs ${
383→ warehouseFull ? "border-amber-400/40 animate-pulse" : ""
384→ } ${activeTide ? "border-white/20" : ""}`}
385→ style={
386→ activeTide
387→ ? { boxShadow: `inset 0 0 16px ${TIDE_EVENTS[activeTide.type].glow}` }
388→ : undefined
389→ }
390→ >
391→ <span
392→ className={
393→ warehouseFull
394→ ? "text-amber-400"
395→ : activeTide
396→ ? ""
397→ : "text-fuchsia-300"
398→ }
399→ style={activeTide ? { color: TIDE_EVENTS[activeTide.type].color } : undefined}
400→ >
401→ ▶
402→ </span>
403→ <span
404→ className={`flex-1 truncate ${
405→ warehouseFull ? "text-amber-200" : "text-muted-foreground"
406→ }`}
407→ >
408→ {goal}
409→ </span>
410→ <span className="hidden sm:inline text-muted-foreground/60">|</span>
411→ <span className="hidden sm:inline text-muted-foreground/70">
412→ 产能 {formatNum(crystalsPerSec)}/s
413→ </span>
414→ <a
415→ href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
416→ target="_blank"
417→ rel="noreferrer"
418→ className="text-muted-foreground/60 hover:text-foreground transition ml-1"
419→ aria-label="仓库"
420→ >
421→ <Github className="h-3.5 w-3.5" />
422→ </a>
423→ </div>
424→ </footer>
425→
426→ <PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
427→ <ChronicleDialog open={chronicleOpen} onOpenChange={setChronicleOpen} />
428→ <ConstellationDialog open={constellationOpen} onOpenChange={setConstellationOpen} />
429→ <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
430→ <AchievementNotifier />
431→ <StarTideNotifier />
432→ <TutorialOverlay />
433→ <OfflineReportDialog />
434→ {cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
435→ </div>
436→ );
437→}
438→
439→/**
440→ * v0.9 工单 #2:全息收益信息面板
441→ * 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库),
442→ * 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。
443→ * 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。
444→ */
445→function CrystalYieldPanel() {
446→ const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
447→ const pulsePower = useGameStore((s) => s.pulsePower);
448→ const combo = useGameStore((s) => s._combo);
449→ const crystals = useGameStore((s) => s.crystals);
450→ const crystalCap = useGameStore((s) => s.crystalCap);
451→
452→ // 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效)
453→ const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1;
454→ const comboHot = combo >= 3; // 连击≥3 高亮闪烁
455→
456→ // 仓库容量百分比
457→ const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0;
458→ const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse
459→
460→ return (
461→ <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
462→ {/* 晶体产能 — emerald */}
463→ <div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
464→ <Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
465→ <div className="min-w-0 leading-tight">
466→ <div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
467→ 晶体产能
468→ </div>
469→ <div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
470→ {formatNum(crystalsPerSec)}
471→ <span className="text-[9px] text-emerald-300/60">/s</span>
472→ </div>
473→ </div>
474→ </div>
475→
476→ {/* 主动脉冲 — rose */}
477→ <div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
478→ <Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
479→ <div className="min-w-0 leading-tight">
480→ <div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
481→ 主动脉冲
482→ </div>
483→ <div className="text-sm font-mono tabular-nums text-rose-200 truncate">
484→ {formatNum(pulsePower)}
485→ {combo >= 1 && (
486→ <span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
487→ )}
488→ </div>
489→ </div>
490→ </div>
491→
492→ {/* 当前连击 — amber,≥3 时高亮闪烁 */}
493→ <div
494→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
495→ comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
496→ }`}
497→ >
498→ <Flame
499→ className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
500→ comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
501→ }`}
502→ />
503→ <div className="min-w-0 leading-tight">
504→ <div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
505→ 当前连击
506→ </div>
507→ <div
508→ className={`text-sm font-mono tabular-nums truncate ${
509→ comboHot ? "text-amber-200" : "text-amber-300/80"
510→ }`}
511→ >
512→ {combo}
513→ <span className="text-[9px] text-amber-300/60">/10</span>
514→ </div>
515→ </div>
516→ </div>
517→
518→ {/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
519→ <div
520→ className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
521→ warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
522→ }`}
523→ >
524→ <Database
525→ className={`h-3.5 w-3.5 shrink-0 ${
526→ warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
527→ }`}
528→ />
529→ <div className="min-w-0 leading-tight">
530→ <div
531→ className={`text-[9px] uppercase tracking-wider truncate ${
532→ warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
533→ }`}
534→ >
535→ 仓库容量
536→ </div>
537→ <div
538→ className={`text-sm font-mono tabular-nums truncate ${
539→ warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
540→ }`}
541→ >
542→ {formatNum(crystals)}
543→ <span
544→ className={`text-[9px] ${
545→ warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
546→ }`}
547→ >
548→ /{formatNum(crystalCap)}
549→ </span>
550→ </div>
551→ </div>
552→ </div>
553→ </div>
554→ );
555→}
556→
557→function StatsPanel() {
558→ const s = useGameStore();
559→ const achCount = Object.values(s.achievements).filter(Boolean).length;
560→ // 深空信标本地排行榜最高分(独立 localStorage
561→ const [beaconBest, setBeaconBest] = useState<number | null>(null);
562→ useEffect(() => {
563→ try {
564→ const lb = loadLeaderboard();
565→ // eslint-disable-next-line react-hooks/set-state-in-effect
566→ setBeaconBest(lb.length > 0 ? lb[0].score : null);
567→ } catch {
568→ setBeaconBest(null);
569→ }
570→ }, []);
571→ const rows = [
572→ { label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
573→ { label: "飞升周目", value: `${s.ascensions}` },
574→ { label: "持有蓝图", value: `${s.blueprints.length} / 6` },
575→ { label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
576→ { label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
577→ { label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` },
578→ { label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` },
579→ { label: "接触进度", value: `${s.contact.toFixed(1)}%` },
580→ { label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
581→ { label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
582→ { label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
583→ { label: "累计探险", value: `${s.totalExpeditions} 次` },
584→ { label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` },
585→ { label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 9` },
586→ { label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` },
587→ { label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` },
588→ { label: "信标最高分", value: beaconBest !== null ? formatNum(beaconBest) : "—" },
589→ { label: "探索力", value: `${s.attributes?.exploration ?? 0} / 100` },
590→ { label: "智慧", value: `${s.attributes?.wisdom ?? 0} / 100` },
591→ { label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` },
592→ { label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` },
593→ { label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` },
594→ ];
595→ return (
596→ <div className="grid grid-cols-2 gap-1.5 text-xs">
597→ {rows.map((r) => (
598→ <div
599→ key={r.label}
600→ className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
601→ >
602→ <span className="text-muted-foreground">{r.label}</span>
603→ <span className="font-mono text-foreground">{r.value}</span>
604→ </div>
605→ ))}
606→ </div>
607→ );
608→}
609→