Files
echo-nexus/src/app/page.tsx
T

619 lines
30 KiB
TypeScript
Executable File
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.
"use client";
// 回响星核 / Echo Nexus — 游戏主入口
import { useState, useEffect } from "react";
import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
import { ResourceBar } from "@/components/game/ResourceBar";
import { CrystalOrb } from "@/components/game/CrystalOrb";
import { DecodePanel } from "@/components/game/DecodeArray";
import { TechTree } from "@/components/game/TechTree";
import { Codex } from "@/components/game/Codex";
import { PrestigeDialog } from "@/components/game/PrestigeDialog";
import { SettingsDialog } from "@/components/game/SettingsDialog";
import { ExpeditionPanel } from "@/components/game/ExpeditionPanel";
import { AchievementsPanel } from "@/components/game/AchievementsPanel";
import { AchievementNotifier } from "@/components/game/AchievementNotifier";
import { ConstellationPanel } from "@/components/game/ConstellationPanel";
import { ConstellationDialog } from "@/components/game/ConstellationDialog";
import { ChronicleDialog } from "@/components/game/ChronicleDialog";
import { BeaconPanel } from "@/components/game/BeaconPanel";
import { TutorialOverlay } from "@/components/game/TutorialOverlay";
import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
import { CruiseMode } from "@/components/game/CruiseMode";
import { AttributesPanel } from "@/components/game/AttributesPanel";
import { ExplorationRadar } from "@/components/game/ExplorationRadar";
import { RelicCodex } from "@/components/game/RelicCodex";
import {
StarTideNotifier,
StarTideIndicator,
StarTideOverlay,
} from "@/components/game/StarTide";
import { useGameLoop } from "@/hooks/useGameLoop";
import { useAudioSync } from "@/hooks/useAudio";
import { useGlobalTide } from "@/hooks/useGlobalTide";
import { useGameStore } from "@/store/gameStore";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
Settings,
RotateCcw,
Sparkles,
Cpu,
BookOpen,
BarChart3,
Rocket,
Github,
Trophy,
Star,
Radio,
Navigation,
User,
Gem,
Zap,
Flame,
Database,
BookMarked,
} from "lucide-react";
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
import { ACHIEVEMENTS } from "@/lib/game/achievements";
import { TIDE_EVENTS } from "@/lib/game/starTide";
import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
import { generateDailyChallenge, loadDailyProgress, loadLeaderboard } from "@/lib/game/beacon";
export default function Page() {
useGameLoop();
useAudioSync();
useGlobalTide();
const [prestigeOpen, setPrestigeOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [constellationOpen, setConstellationOpen] = useState(false);
const [chronicleOpen, setChronicleOpen] = useState(false);
const [cruiseOpen, setCruiseOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const contact = useGameStore((s) => s.contact);
const totalDecoded = useGameStore((s) => s.totalDecoded);
const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
const ascensions = useGameStore((s) => s.ascensions);
const ownedTech = useGameStore((s) => s.tech);
const ownedFragments = useGameStore((s) => s.fragments);
const ownedAchievements = useGameStore((s) => s.achievements);
const ownedConstellation = useGameStore((s) => s.constellation ?? []);
const pendingPerkChoices = useGameStore((s) => s.pendingPerkChoices);
const crystals = useGameStore((s) => s.crystals);
const crystalCap = useGameStore((s) => s.crystalCap);
const activeTide = useGameStore((s) => s.activeTide);
const globalTide = useGameStore((s) => s.globalTide);
const energy = useGameStore((s) => s.energy);
const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
// 深空信标:检测是否有可领取的奖励(独立 localStorage
const [beaconClaimable, setBeaconClaimable] = useState(false);
useEffect(() => {
let cancelled = false;
const check = () => {
try {
const c = generateDailyChallenge();
const p = loadDailyProgress();
if (!cancelled) {
setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey);
}
} catch {
if (!cancelled) setBeaconClaimable(false);
}
};
check();
const id = setInterval(check, 2000);
return () => {
cancelled = true;
clearInterval(id);
};
}, []);
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
// 防止 SSR/CSR 不一致
if (!mounted) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#050410] text-muted-foreground">
<div className="animate-pulse">唤醒回响中…</div>
</div>
);
}
const ownedTechCount = Object.values(ownedTech).filter((v) => v > 0).length;
const ownedFragCount = Object.values(ownedFragments).filter(Boolean).length;
const ownedAchCount = Object.values(ownedAchievements).filter(Boolean).length;
const canPrestige = contact >= 100;
const hasPendingPerk = pendingPerkChoices && pendingPerkChoices.length > 0;
// 仓库满仓警告
const warehouseFull = crystals >= crystalCap * 0.98;
// 目标提示
let goal = "点击中央晶体发起脉冲,累积记忆晶体";
if (totalDecoded === 0 && crystals >= 5) {
goal = "右侧「待解码晶体」点击一颗晶体,开始解码";
} else if (totalDecoded > 0 && ownedTechCount === 0) {
goal = "用洞见解锁技术树,提升产能";
} else if (totalDecoded > 0 && ownedFragCount < FRAGMENTS.length) {
goal = `继续解码,拼凑记忆图谱(${ownedFragCount}/${FRAGMENTS.length}`;
}
if (energy >= 1 && totalDecoded >= 3) {
goal = "「探险」标签可深入遗迹,获取丰厚奖励";
}
if (ascensions >= 1 && ownedConstellation.length > 0) {
goal = `星图天赋已觉醒 ${ownedConstellation.length}/18,继续飞升获取更多`;
}
if (warehouseFull) {
goal = "⚠ 仓库已满,产能浪费中!请解码晶体或升级仓库";
}
if (hasPendingPerk) {
goal = "✦ 星图觉醒!点击顶部「星图」按钮选择一道天赋";
}
if (activeTide) {
const tm = TIDE_EVENTS[activeTide.type];
const prefix = globalTide ? "🌐 全球星潮 · " : "";
goal = `${prefix}${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
}
if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
return (
<div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
{/* 星空背景 */}
<StarfieldCanvas className="fixed inset-0 w-full h-full -z-10" />
{/* 星潮背景叠层 */}
<StarTideOverlay />
{/* 顶部 Header */}
<header className="sticky top-0 z-30 px-3 sm:px-5 pt-3 pb-2">
<div className="flex items-center gap-3 mb-2.5">
<div className="flex items-center gap-2.5">
<div className="relative h-9 w-9">
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-emerald-400 via-fuchsia-500 to-rose-400 blur-md opacity-60" />
<div className="absolute inset-1 rounded-full bg-[#050410] flex items-center justify-center">
<Sparkles className="h-4 w-4 text-fuchsia-300" />
</div>
</div>
<div className="leading-tight">
<h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
<p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
ECHO NEXUS · v0.10
</p>
</div>
</div>
<div className="ml-auto flex items-center gap-1.5">
<StarTideIndicator />
<Button
size="sm"
variant="outline"
onClick={() => setCruiseOpen(true)}
className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
aria-label="深空巡航"
title="深空巡航 · 实时玩法"
>
<Navigation className="h-3.5 w-3.5 mr-1" />
巡航
</Button>
<Button
size="icon"
variant="ghost"
onClick={() => setChronicleOpen(true)}
className="h-8 w-8 relative group"
aria-label="编年史"
title="回响编年史"
>
<BookOpen className="h-4 w-4 group-hover:text-fuchsia-300 transition-colors" />
{chronicleCount > 0 && (
<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">
{chronicleCount}
</span>
)}
</Button>
{hasPendingPerk && (
<Button
size="sm"
variant="outline"
onClick={() => setConstellationOpen(true)}
className="border-fuchsia-400/60 text-fuchsia-200 hover:bg-fuchsia-500/15 h-8 px-2.5 animate-pulse"
>
<Star className="h-3.5 w-3.5 mr-1" />
觉醒
</Button>
)}
{canPrestige && (
<Button
size="sm"
variant="outline"
onClick={() => setPrestigeOpen(true)}
data-tut="prestige-btn"
className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
>
<RotateCcw className="h-3.5 w-3.5 mr-1" />
飞升
</Button>
)}
<Button
size="icon"
variant="ghost"
onClick={() => setSettingsOpen(true)}
className="h-8 w-8"
aria-label="设置"
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
<ResourceBar onPrestige={() => setPrestigeOpen(true)} />
</header>
{/* 主体 — v0.10 工单 #2 布局重构:反转左右比例,左栏紧凑晶体球,右栏扩展核心玩法 */}
<main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
<div className="grid grid-cols-1 lg:grid-cols-[minmax(300px,360px)_1fr] gap-3 h-full">
{/* 左侧:脉冲晶体(紧凑模式) + 收益 + 探索雷达 */}
<section className="glass rounded-2xl p-3 sm:p-4 flex flex-col gap-3 min-h-[340px] sm:min-h-[420px] relative overflow-hidden">
{/* 装饰光圈 */}
<div className="pointer-events-none absolute -top-16 -left-16 h-40 w-40 rounded-full bg-emerald-500/10 blur-3xl" />
<div className="pointer-events-none absolute -bottom-16 -right-16 h-40 w-40 rounded-full bg-fuchsia-500/10 blur-3xl" />
{/* v0.10 装饰全息环(缩小适应紧凑布局) */}
<div className="pointer-events-none absolute inset-4 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
<div className="pointer-events-none absolute inset-8 rounded-full border border-dashed border-fuchsia-400/10 animate-[spin_90s_linear_infinite_reverse]" />
{/* 四角全息标记 */}
<div className="pointer-events-none absolute top-2 left-2 h-3 w-3 border-l border-t border-emerald-400/30 rounded-tl" />
<div className="pointer-events-none absolute top-2 right-2 h-3 w-3 border-r border-t border-fuchsia-400/30 rounded-tr" />
<div className="pointer-events-none absolute bottom-2 left-2 h-3 w-3 border-l border-b border-amber-400/30 rounded-bl" />
<div className="pointer-events-none absolute bottom-2 right-2 h-3 w-3 border-r border-b border-rose-400/30 rounded-br" />
{/* 顶部状态条 */}
<div className="pointer-events-none absolute top-2 left-1/2 -translate-x-1/2 flex items-center gap-1.5 text-[8px] font-mono text-muted-foreground/60 z-10">
<span className="h-1 w-1 rounded-full bg-emerald-400 animate-pulse" />
<span>CRYSTAL CORE · ONLINE</span>
<span className="h-1 w-1 rounded-full bg-fuchsia-400 animate-pulse" />
</div>
{/* 晶体球区域(紧凑居中) */}
<div data-tut="crystal-orb" className="relative flex justify-center pt-3">
<CrystalOrb />
</div>
{/* v0.9 全息收益信息面板 */}
<CrystalYieldPanel />
{/* v0.10 探索目标雷达(新增)— 填充留白,让晶体球区有价值 */}
<ExplorationRadar />
</section>
{/* 右侧:解码 + 标签面板(v0.10 扩展,从 420px 扩到 1fr */}
<section className="flex flex-col gap-3 min-h-0">
{/* 解码面板 */}
<div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[280px] sm:min-h-[320px] max-h-[480px]">
<DecodePanel />
</div>
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 / 遗迹图鉴 */}
<div className="glass rounded-2xl p-3 min-h-[320px] sm:min-h-[380px] max-h-[560px] flex flex-col">
<Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
{/* v0.10 标签页响应式布局,小屏 3 列 3 行避免拥挤遮挡,大屏 9 列 */}
<TabsList className="grid grid-cols-3 sm:grid-cols-9 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
<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">
<Rocket className="h-3.5 w-3.5" />
<span className="leading-none">探险</span>
{energy >= 1 && !hasActiveExpedition && (
<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" />
)}
{hasActiveExpedition && (
<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" />
)}
</TabsTrigger>
<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">
<Cpu className="h-3.5 w-3.5" />
<span className="leading-none">技术</span>
</TabsTrigger>
<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">
<Star className="h-3.5 w-3.5" />
<span className="leading-none">星图</span>
{hasPendingPerk && (
<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" />
)}
</TabsTrigger>
<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">
<BookOpen className="h-3.5 w-3.5" />
<span className="leading-none">图谱</span>
</TabsTrigger>
<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">
<Trophy className="h-3.5 w-3.5" />
<span className="leading-none">成就</span>
{ownedAchCount < ACHIEVEMENTS.length && (
<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" />
)}
</TabsTrigger>
<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">
<Radio className="h-3.5 w-3.5" />
<span className="leading-none">信标</span>
{beaconClaimable && (
<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" />
)}
</TabsTrigger>
<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">
<BarChart3 className="h-3.5 w-3.5" />
<span className="leading-none">统计</span>
</TabsTrigger>
<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">
<User className="h-3.5 w-3.5" />
<span className="leading-none">角色</span>
{pendingAttrPoints > 0 && (
<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" />
)}
</TabsTrigger>
<TabsTrigger value="relic" 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">
<BookMarked className="h-3.5 w-3.5" />
<span className="leading-none">图鉴</span>
</TabsTrigger>
</TabsList>
<TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<ExpeditionPanel />
</TabsContent>
<TabsContent value="tech" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<TechTree />
</TabsContent>
<TabsContent value="constellation" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<ConstellationPanel />
</TabsContent>
<TabsContent value="codex" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<Codex />
</TabsContent>
<TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<AchievementsPanel />
</TabsContent>
<TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<BeaconPanel />
</TabsContent>
<TabsContent value="stats" className="flex-1 mt-2 min-h-0">
<StatsPanel />
</TabsContent>
<TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<AttributesPanel />
</TabsContent>
<TabsContent value="relic" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
<RelicCodex />
</TabsContent>
</Tabs>
</div>
</section>
</div>
</main>
{/* 底部 Footer */}
<footer className="sticky bottom-0 z-20 mt-auto px-3 sm:px-5 pb-2 pt-1">
<div
className={`glass rounded-xl px-3 py-2 flex items-center gap-2 text-xs ${
warehouseFull ? "border-amber-400/40 animate-pulse" : ""
} ${activeTide ? "border-white/20" : ""}`}
style={
activeTide
? { boxShadow: `inset 0 0 16px ${TIDE_EVENTS[activeTide.type].glow}` }
: undefined
}
>
<span
className={
warehouseFull
? "text-amber-400"
: activeTide
? ""
: "text-fuchsia-300"
}
style={activeTide ? { color: TIDE_EVENTS[activeTide.type].color } : undefined}
>
</span>
<span
className={`flex-1 truncate ${
warehouseFull ? "text-amber-200" : "text-muted-foreground"
}`}
>
{goal}
</span>
<span className="hidden sm:inline text-muted-foreground/60">|</span>
<span className="hidden sm:inline text-muted-foreground/70">
产能 {formatNum(crystalsPerSec)}/s
</span>
<a
href="https://git.atdunbg.xyz/Super_Z/echo-nexus"
target="_blank"
rel="noreferrer"
className="text-muted-foreground/60 hover:text-foreground transition ml-1"
aria-label="仓库"
>
<Github className="h-3.5 w-3.5" />
</a>
</div>
</footer>
<PrestigeDialog open={prestigeOpen} onOpenChange={setPrestigeOpen} />
<ChronicleDialog open={chronicleOpen} onOpenChange={setChronicleOpen} />
<ConstellationDialog open={constellationOpen} onOpenChange={setConstellationOpen} />
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
<AchievementNotifier />
<StarTideNotifier />
<TutorialOverlay />
<OfflineReportDialog />
{cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
</div>
);
}
/**
* v0.9 工单 #2:全息收益信息面板
* 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库),
* 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。
* 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。
*/
function CrystalYieldPanel() {
const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
const pulsePower = useGameStore((s) => s.pulsePower);
const combo = useGameStore((s) => s._combo);
const crystals = useGameStore((s) => s.crystals);
const crystalCap = useGameStore((s) => s.crystalCap);
// 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效)
const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1;
const comboHot = combo >= 3; // 连击≥3 高亮闪烁
// 仓库容量百分比
const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0;
const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse
return (
<div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
{/* 晶体产能 — emerald */}
<div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
<Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
<div className="min-w-0 leading-tight">
<div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
晶体产能
</div>
<div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
{formatNum(crystalsPerSec)}
<span className="text-[9px] text-emerald-300/60">/s</span>
</div>
</div>
</div>
{/* 主动脉冲 — rose */}
<div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
<Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
<div className="min-w-0 leading-tight">
<div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
主动脉冲
</div>
<div className="text-sm font-mono tabular-nums text-rose-200 truncate">
{formatNum(pulsePower)}
{combo >= 1 && (
<span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
)}
</div>
</div>
</div>
{/* 当前连击 — amber,≥3 时高亮闪烁 */}
<div
className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
}`}
>
<Flame
className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
}`}
/>
<div className="min-w-0 leading-tight">
<div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
当前连击
</div>
<div
className={`text-sm font-mono tabular-nums truncate ${
comboHot ? "text-amber-200" : "text-amber-300/80"
}`}
>
{combo}
<span className="text-[9px] text-amber-300/60">/10</span>
</div>
</div>
</div>
{/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
<div
className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
}`}
>
<Database
className={`h-3.5 w-3.5 shrink-0 ${
warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
}`}
/>
<div className="min-w-0 leading-tight">
<div
className={`text-[9px] uppercase tracking-wider truncate ${
warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
}`}
>
仓库容量
</div>
<div
className={`text-sm font-mono tabular-nums truncate ${
warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
}`}
>
{formatNum(crystals)}
<span
className={`text-[9px] ${
warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
}`}
>
/{formatNum(crystalCap)}
</span>
</div>
</div>
</div>
</div>
);
}
function StatsPanel() {
const s = useGameStore();
const achCount = Object.values(s.achievements).filter(Boolean).length;
// 深空信标本地排行榜最高分(独立 localStorage
const [beaconBest, setBeaconBest] = useState<number | null>(null);
useEffect(() => {
try {
const lb = loadLeaderboard();
// eslint-disable-next-line react-hooks/set-state-in-effect
setBeaconBest(lb.length > 0 ? lb[0].score : null);
} catch {
setBeaconBest(null);
}
}, []);
const rows = [
{ label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
{ label: "飞升周目", value: `${s.ascensions}` },
{ label: "持有蓝图", value: `${s.blueprints.length} / 6` },
{ label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
{ label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
{ label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` },
{ label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` },
{ label: "接触进度", value: `${s.contact.toFixed(1)}%` },
{ label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
{ label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
{ label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
{ label: "累计探险", value: `${s.totalExpeditions} 次` },
{ label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` },
{ label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 9` },
{ label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` },
{ label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` },
{ label: "信标最高分", value: beaconBest !== null ? formatNum(beaconBest) : "—" },
{ label: "探索力", value: `${s.attributes?.exploration ?? 0} / 100` },
{ label: "智慧", value: `${s.attributes?.wisdom ?? 0} / 100` },
{ label: "勇气", value: `${s.attributes?.courage ?? 0} / 100` },
{ label: "灵感", value: `${s.attributes?.inspiration ?? 0} / 100` },
{ label: "待分配属性点", value: `${s.pendingAttrPoints ?? 0}` },
];
return (
<div className="grid grid-cols-2 gap-1.5 text-xs">
{rows.map((r) => (
<div
key={r.label}
className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
>
<span className="text-muted-foreground">{r.label}</span>
<span className="font-mono text-foreground">{r.value}</span>
</div>
))}
</div>
);
}