3cf83ed6-dca1-44be-bf7a-a27d19337739
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
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→
|
||||
Reference in New Issue
Block a user