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
+39 -29
View File
@@ -20,6 +20,8 @@ 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,
@@ -49,6 +51,7 @@ import {
Zap,
Flame,
Database,
BookMarked,
} from "lucide-react";
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
import { ACHIEVEMENTS } from "@/lib/game/achievements";
@@ -180,7 +183,7 @@ export default function Page() {
<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.8
ECHO NEXUS · v0.10
</p>
</div>
</div>
@@ -249,52 +252,52 @@ export default function Page() {
<ResourceBar onPrestige={() => setPrestigeOpen(true)} />
</header>
{/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
{/* 主体 — 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-[1fr_minmax(360px,420px)] gap-3 h-full">
{/* 左侧:脉冲晶体 */}
<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">
<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-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
<div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
{/* v0.8.2 装饰全息环(填充留白,提升视觉层次 */}
<div className="pointer-events-none absolute inset-6 rounded-full border border-emerald-400/10 animate-[spin_60s_linear_infinite]" />
<div className="pointer-events-none absolute inset-10 rounded-full border border-dashed border-fuchsia-400/10 animate-[spin_90s_linear_infinite_reverse]" />
<div className="pointer-events-none absolute inset-16 rounded-full border border-rose-400/8" />
<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-3 left-3 h-4 w-4 border-l border-t border-emerald-400/30 rounded-tl" />
<div className="pointer-events-none absolute top-3 right-3 h-4 w-4 border-r border-t border-fuchsia-400/30 rounded-tr" />
<div className="pointer-events-none absolute bottom-3 left-3 h-4 w-4 border-l border-b border-amber-400/30 rounded-bl" />
<div className="pointer-events-none absolute bottom-3 right-3 h-4 w-4 border-r border-b border-rose-400/30 rounded-br" />
<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-3 left-1/2 -translate-x-1/2 flex items-center gap-2 text-[9px] font-mono text-muted-foreground/60">
<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 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">
echo · nexus · archaeology
</div>
<div data-tut="crystal-orb" className="contents">
{/* 晶体球区域(紧凑居中) */}
<div data-tut="crystal-orb" className="relative flex justify-center pt-3">
<CrystalOrb />
</div>
{/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
{/* 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-[300px] sm:min-h-[360px] max-h-[560px]">
<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>
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
{/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
<div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 / 遗迹图鉴 */}
<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.9 工单修复:标签页响应式布局,小屏 42 行避免拥挤遮挡 */}
<TabsList className="grid grid-cols-4 sm:grid-cols-8 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
{/* v0.10 标签页响应式布局,小屏 33 行避免拥挤遮挡,大屏 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>
@@ -345,6 +348,10 @@ export default function Page() {
<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 />
@@ -370,6 +377,9 @@ export default function Page() {
<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>
+125 -43
View File
@@ -1,30 +1,64 @@
"use client";
// 回响星核 / Echo Nexus — 成就系统面板
// 回响星核 / Echo Nexus — 成就系统面板(v0.10 工单 #2 重写:单列 + 分类过滤 + 防溢出)
import { useState, useMemo } from "react";
import { useGameStore } from "@/store/gameStore";
import { ACHIEVEMENTS } from "@/lib/game/achievements";
import { achievementBonuses } from "@/lib/game/achievements";
import { Trophy, Lock } from "lucide-react";
import { ACHIEVEMENTS, achievementBonuses } from "@/lib/game/achievements";
import { Trophy, Lock, Sparkles, Filter } from "lucide-react";
type FilterKey = "all" | "unlocked" | "locked";
const FILTER_TABS: { key: FilterKey; label: string; color: string }[] = [
{ key: "all", label: "全部", color: "#a78bfa" },
{ key: "unlocked", label: "已解锁", color: "#34d399" },
{ key: "locked", label: "未解锁", color: "#fb7185" },
];
export function AchievementsPanel() {
const achievements = useGameStore((s) => s.achievements);
const [filter, setFilter] = useState<FilterKey>("all");
const unlockedCount = Object.values(achievements).filter(Boolean).length;
const bonus = achievementBonuses(achievements);
const totalCount = ACHIEVEMENTS.length;
const unlockPct = totalCount > 0 ? (unlockedCount / totalCount) * 100 : 0;
// 按筛选条件过滤成就列表
const filtered = useMemo(() => {
return ACHIEVEMENTS.filter((a) => {
const unlocked = !!achievements[a.id];
if (filter === "unlocked") return unlocked;
if (filter === "locked") return !unlocked;
return true;
});
}, [achievements, filter]);
return (
<div className="flex flex-col gap-2.5 h-full">
{/* 顶部:进度 + 加成总览 */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-1.5">
<div className="flex flex-col gap-2.5 h-full min-h-0">
{/* 顶部:标题 + 进度 */}
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold flex items-center gap-1.5 shrink-0">
<Trophy className="h-4 w-4 text-amber-400" />
</h3>
<span className="text-[10px] text-muted-foreground font-mono">
{unlockedCount} / {ACHIEVEMENTS.length}
</span>
<div className="flex items-center gap-2 text-[10px] font-mono">
<span className="text-amber-300">{unlockedCount}</span>
<span className="text-muted-foreground/50">/</span>
<span className="text-muted-foreground">{totalCount}</span>
<span className="text-muted-foreground/60">({unlockPct.toFixed(0)}%)</span>
</div>
</div>
{/* 进度条 */}
<div className="h-1 rounded-full bg-black/40 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-500 via-rose-400 to-fuchsia-400 transition-all duration-500"
style={{ width: `${unlockPct}%`, boxShadow: "0 0 8px rgba(251,191,36,0.5)" }}
/>
</div>
{/* 永久加成条 */}
<div className="rounded-lg border border-amber-400/20 bg-amber-950/20 px-2.5 py-1.5 flex items-center gap-3 text-[10px]">
<div className="rounded-lg border border-amber-400/20 bg-amber-950/20 px-2.5 py-1.5 flex items-center gap-2 text-[10px] flex-wrap">
<Sparkles className="h-3 w-3 text-amber-400 shrink-0" />
<span className="text-amber-300/80"></span>
{bonus.crystalsPerSecPct > 0 && (
<span className="text-emerald-300"> +{bonus.crystalsPerSecPct}%</span>
@@ -33,64 +67,106 @@ export function AchievementsPanel() {
<span className="text-fuchsia-300"> +{bonus.insightPct}%</span>
)}
{bonus.crystalsPerSecPct === 0 && bonus.insightPct === 0 && (
<span className="text-muted-foreground/60"></span>
<span className="text-muted-foreground/60"></span>
)}
</div>
{/* 成就网格 — 移除固定 max-h,用 flex-1 min-h-0 自适应父容器,避免小屏挤压重叠 */}
<div className="flex-1 min-h-0 grid grid-cols-1 sm:grid-cols-2 gap-1.5 content-start overflow-y-auto echo-scroll pr-1">
{ACHIEVEMENTS.map((a) => {
{/* 分类过滤标签 */}
<div className="flex items-center gap-1 text-[10px]">
<Filter className="h-3 w-3 text-muted-foreground/60 shrink-0" />
{FILTER_TABS.map((t) => {
const active = filter === t.key;
const count =
t.key === "all"
? totalCount
: t.key === "unlocked"
? unlockedCount
: totalCount - unlockedCount;
return (
<button
key={t.key}
onClick={() => setFilter(t.key)}
className={`px-2 py-0.5 rounded-md transition-all ${
active
? "bg-white/10 text-foreground"
: "text-muted-foreground/70 hover:text-foreground hover:bg-white/5"
}`}
style={active ? { color: t.color, boxShadow: `inset 0 0 0 1px ${t.color}55` } : undefined}
>
{t.label}
<span className="ml-1 font-mono opacity-70">{count}</span>
</button>
);
})}
</div>
{/* 成就列表 — 单列展示,每个卡片 min-h 防止内容溢出被裁切 */}
<div className="flex-1 min-h-0 overflow-y-auto echo-scroll pr-1 space-y-1.5">
{filtered.length === 0 && (
<div className="text-center text-[11px] text-muted-foreground/60 py-6">
</div>
)}
{filtered.map((a) => {
const unlocked = !!achievements[a.id];
return (
<div
key={a.id}
className={`relative rounded-lg border px-2.5 py-2 transition-all duration-300 overflow-hidden ${
className={`relative rounded-lg border px-2.5 py-2 transition-all duration-300 min-h-[68px] flex items-center ${
unlocked
? "border-white/15 bg-black/30"
: "border-white/5 bg-black/15 opacity-60"
: "border-white/5 bg-black/15 opacity-70"
}`}
style={
unlocked
? { boxShadow: `inset 0 0 16px ${a.color}11` }
? { boxShadow: `inset 0 0 14px ${a.color}10` }
: undefined
}
>
{unlocked && (
<div
className="absolute -top-6 -right-6 h-16 w-16 rounded-full blur-2xl opacity-40"
className="absolute -top-6 -right-6 h-16 w-16 rounded-full blur-2xl opacity-40 pointer-events-none"
style={{ background: a.color }}
/>
)}
<div className="relative flex items-start gap-2">
{/* 图标徽章 */}
<div
className="shrink-0 h-8 w-8 rounded-lg flex items-center justify-center text-base"
style={{
background: unlocked ? `${a.color}22` : "rgba(255,255,255,0.04)",
border: `1px solid ${unlocked ? a.color + "55" : "rgba(255,255,255,0.08)"}`,
color: unlocked ? a.color : "#666",
boxShadow: unlocked ? `0 0 10px ${a.color}33` : "none",
}}
>
{unlocked ? a.icon : <Lock className="h-3.5 w-3.5" />}
</div>
{/* 文本 */}
<div className="min-w-0 flex-1">
<div
{/* 图标徽章 */}
<div
className="shrink-0 h-10 w-10 rounded-lg flex items-center justify-center text-lg mr-2.5"
style={{
background: unlocked ? `${a.color}22` : "rgba(255,255,255,0.04)",
border: `1px solid ${unlocked ? a.color + "55" : "rgba(255,255,255,0.08)"}`,
color: unlocked ? a.color : "#666",
boxShadow: unlocked ? `0 0 10px ${a.color}33` : "none",
}}
>
{unlocked ? a.icon : <Lock className="h-4 w-4" />}
</div>
{/* 文本 — 使用 flex-1 + min-w-0 防止文本撑爆容器 */}
<div className="min-w-0 flex-1 flex flex-col gap-0.5">
<div className="flex items-center gap-1.5">
<span
className="text-[11px] font-semibold truncate"
style={{ color: unlocked ? a.color : "rgba(255,255,255,0.5)" }}
>
{a.name}
</div>
<div className="text-[10px] text-muted-foreground/80 leading-tight mt-0.5">
{a.desc}
</div>
</span>
{unlocked && (
<div className="text-[9px] mt-1 font-mono" style={{ color: a.color + "cc" }}>
{a.rewardText}
</div>
<span
className="text-[8px] px-1 py-0 rounded shrink-0 font-mono"
style={{ background: `${a.color}22`, color: a.color }}
>
</span>
)}
</div>
<div className="text-[10px] text-muted-foreground/80 leading-tight line-clamp-2">
{a.desc}
</div>
{unlocked && (
<div className="text-[9px] mt-0.5 font-mono truncate" style={{ color: a.color + "cc" }}>
{a.rewardText}
</div>
)}
</div>
</div>
);
@@ -101,6 +177,12 @@ export function AchievementsPanel() {
.echo-scroll::-webkit-scrollbar { width: 5px; }
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 3px; }
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
`}</style>
</div>
);
+2 -17
View File
@@ -49,7 +49,6 @@ export function ExpeditionPanel() {
const resolveCurrentNode = useGameStore((s) => s.resolveCurrentNode);
const advanceNode = useGameStore((s) => s.advanceNode);
const abortExpedition = useGameStore((s) => s.abortExpedition);
const dismissExpedition = useGameStore((s) => s.dismissExpedition);
const { toast } = useToast();
const [lastLog, setLastLog] = useState<string | null>(null);
const [now, setNow] = useState(() => Date.now());
@@ -109,10 +108,8 @@ export function ExpeditionPanel() {
};
// ===== 无活跃探险:展示入口 =====
// v0.10 工单 #2 修复:探险结束自动清空 activeExpedition,无需 justFinished 判断
if (!activeExpedition || activeExpedition.finished) {
// v0.9 工单修复:如果探险刚结束(finished 且有摘要),显示结算面板
const justFinished = activeExpedition?.finished && lastExpeditionSummary &&
Date.now() - lastExpeditionSummary.finishedAt < 60000;
return (
<div className="flex flex-col gap-3 h-full">
<div className="flex items-center justify-between">
@@ -123,7 +120,7 @@ export function ExpeditionPanel() {
<span className="text-[10px] text-muted-foreground"> {totalExpeditions} </span>
</div>
{/* v0.9 工单修复:上次探险结果摘要 */}
{/* v0.9 工单修复:上次探险结果摘要(不限时间,常驻展示) */}
{lastExpeditionSummary && (
<div
className={`rounded-xl border p-2.5 relative overflow-hidden ${
@@ -224,18 +221,6 @@ export function ExpeditionPanel() {
))}
</div>
)}
{/* v0.9 工单修复:刚结束的探险,显示"返回"按钮 */}
{justFinished && (
<Button
onClick={() => dismissExpedition()}
variant="outline"
size="sm"
className="h-7 text-[11px] border-amber-400/30 text-amber-200"
>
</Button>
)}
</div>
);
}
+215
View File
@@ -0,0 +1,215 @@
"use client";
// 回响星核 / Echo Nexus — 探索目标雷达(v0.10 新增)
// 动态展示当前可解锁/可探索的下一目标,让"等待"变"期待"
// 缓解工单 #2 "占地方没用" 反馈 — 晶体球下方提供持续探索动力
import { useGameStore } from "@/store/gameStore";
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
import { ACHIEVEMENTS } from "@/lib/game/achievements";
import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
import { EXPEDITION_CONFIG } from "@/lib/game/expedition";
import {
Target,
Cpu,
Trophy,
BookOpen,
Rocket,
Star,
TrendingDown,
CheckCircle2,
Sparkles,
} from "lucide-react";
interface TargetItem {
id: string;
icon: React.ComponentType<{ className?: string }>;
color: string;
label: string;
detail: string;
progress: number; // 0-100
hint: string;
}
export function ExplorationRadar() {
const s = useGameStore();
// ===== 计算各系统的下一个目标 =====
const targets: TargetItem[] = [];
// 1. 下一个技术节点(按洞见排序)
const nextTech = TECH_TREE.filter((t) => !s.tech[t.id]).sort((a, b) => a.cost - b.cost)[0];
if (nextTech) {
const insightProgress = Math.min(100, (s.insights / nextTech.cost) * 100);
targets.push({
id: "tech_" + nextTech.id,
icon: Cpu,
color: "#34d399",
label: nextTech.name,
detail: `${nextTech.cost} 洞见 · ${nextTech.desc}`,
progress: insightProgress,
hint: s.insights >= nextTech.cost ? "可解锁" : `还差 ${nextTech.cost - s.insights} 洞见`,
});
}
// 2. 下一个成就(按达成难度近似)
const nextAch = ACHIEVEMENTS.find((a) => !s.achievements[a.id]);
if (nextAch) {
targets.push({
id: "ach_" + nextAch.id,
icon: Trophy,
color: "#fbbf24",
label: nextAch.name,
detail: nextAch.desc,
progress: 30, // 成就进度难以精确计算,给固定视觉提示
hint: nextAch.rewardText,
});
}
// 3. 下一个记忆碎片
const nextFrag = FRAGMENTS.find((f) => !s.fragments[f.id]);
if (nextFrag) {
const ownedFrag = FRAGMENTS.filter((f) => s.fragments[f.id]).length;
targets.push({
id: "frag_" + nextFrag.id,
icon: BookOpen,
color: "#e879f9",
label: nextFrag.title,
detail: "解码晶体有概率获取",
progress: (ownedFrag / FRAGMENTS.length) * 100,
hint: `图谱 ${ownedFrag}/${FRAGMENTS.length}`,
});
}
// 4. 探险能量
if (s.energy < EXPEDITION_CONFIG.energyCost) {
targets.push({
id: "expedition_energy",
icon: Rocket,
color: "#fb7185",
label: "探险能量恢复中",
detail: `满能量可深入遗迹`,
progress: (s.energy / EXPEDITION_CONFIG.energyCost) * 100,
hint: `${s.energy}/${EXPEDITION_CONFIG.energyCost}`,
});
}
// 5. 飞升进度(接触<100 时)
if (s.contact < 100) {
targets.push({
id: "ascension",
icon: Star,
color: "#a78bfa",
label: "接触进度",
detail: "满 100 可飞升新周目",
progress: s.contact,
hint: `${s.contact.toFixed(1)}/100`,
});
}
// 6. 星图天赋(飞升后未选择)
const ownedConstellation = s.constellation ?? [];
if (s.pendingPerkChoices && s.pendingPerkChoices.length > 0) {
targets.push({
id: "constellation_pending",
icon: Sparkles,
color: "#e879f9",
label: "星图觉醒待选择",
detail: "3 选 1 永久天赋",
progress: 100,
hint: "立即选择",
});
} else if (ownedConstellation.length < CONSTELLATION_PERKS.length && s.ascensions > 0) {
targets.push({
id: "constellation_next",
icon: Star,
color: "#e879f9",
label: "下一道星图天赋",
detail: `已觉醒 ${ownedConstellation.length}/${CONSTELLATION_PERKS.length}`,
progress: (ownedConstellation.length / CONSTELLATION_PERKS.length) * 100,
hint: "飞升后解锁",
});
}
// 取前 3 个最相关目标展示
const topTargets = targets.slice(0, 3);
if (topTargets.length === 0) {
return (
<div className="rounded-xl border border-emerald-400/20 bg-emerald-950/10 p-3 text-center">
<CheckCircle2 className="h-5 w-5 text-emerald-400 mx-auto mb-1" />
<p className="text-[11px] text-emerald-200/80"></p>
<p className="text-[9px] text-muted-foreground/60 mt-0.5"></p>
</div>
);
}
return (
<div className="rounded-xl border border-white/10 bg-black/30 p-2.5 space-y-2">
{/* 标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Target className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-[11px] font-semibold text-emerald-200"></span>
</div>
<span className="text-[9px] text-muted-foreground/50 font-mono">RADAR</span>
</div>
{/* 目标列表 */}
<div className="space-y-1.5">
{topTargets.map((t) => {
const Icon = t.icon;
const isReady = t.progress >= 100;
return (
<div
key={t.id}
className={`rounded-lg p-1.5 transition-all ${
isReady
? "bg-white/8 border border-white/15"
: "bg-black/20 border border-white/5"
}`}
style={
isReady
? { boxShadow: `0 0 10px ${t.color}33` }
: undefined
}
>
<div className="flex items-center gap-1.5 mb-1">
<Icon
className={`h-3 w-3 shrink-0 ${isReady ? "animate-pulse" : ""}`}
style={{ color: t.color }}
/>
<span
className="text-[10px] font-medium truncate flex-1"
style={{ color: isReady ? t.color : "rgba(255,255,255,0.75)" }}
>
{t.label}
</span>
<span className="text-[8px] font-mono text-muted-foreground/70 shrink-0">
{t.hint}
</span>
</div>
{/* 进度条 */}
<div className="h-0.5 rounded-full bg-black/50 overflow-hidden">
<div
className="h-full transition-all duration-500"
style={{
width: `${Math.min(100, t.progress)}%`,
background: t.color,
boxShadow: isReady ? `0 0 6px ${t.color}` : "none",
}}
/>
</div>
{/* 详情 */}
<p className="text-[8px] text-muted-foreground/60 mt-0.5 truncate">{t.detail}</p>
</div>
);
})}
</div>
{/* 底部提示 */}
<div className="flex items-center gap-1 text-[8px] text-muted-foreground/50 pt-0.5 border-t border-white/5">
<TrendingDown className="h-2.5 w-2.5" />
<span> · </span>
</div>
</div>
);
}
+219
View File
@@ -0,0 +1,219 @@
"use client";
// 回响星核 / Echo Nexus — 文明遗迹图鉴面板(v0.10 新增)
// 展示深空考古发现的远古文明遗迹,增加世界观深度与探索乐趣
import { useState } from "react";
import { useGameStore } from "@/store/gameStore";
import {
RELIC_CODEX,
getUnlockedRelics,
getRelicProgress,
getRelicRewards,
type RelicEntry,
} from "@/lib/game/relicCodex";
import { BookMarked, Lock, Sparkles, Clock, Users, ScrollText } from "lucide-react";
export function RelicCodex() {
const state = useGameStore();
const [selected, setSelected] = useState<string | null>(null);
const unlockedRelics = getUnlockedRelics(state);
const progress = getRelicProgress(state);
const rewards = getRelicRewards(state);
const selectedRelic = selected ? RELIC_CODEX.find((r) => r.id === selected) : null;
return (
<div className="flex flex-col gap-2.5 h-full min-h-0">
{/* 顶部:标题 + 进度 */}
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold flex items-center gap-1.5 shrink-0">
<BookMarked className="h-4 w-4 text-fuchsia-400" />
</h3>
<div className="flex items-center gap-2 text-[10px] font-mono">
<span className="text-fuchsia-300">{unlockedRelics.length}</span>
<span className="text-muted-foreground/50">/</span>
<span className="text-muted-foreground">{RELIC_CODEX.length}</span>
<span className="text-muted-foreground/60">({progress.toFixed(0)}%)</span>
</div>
</div>
{/* 进度条 */}
<div className="h-1 rounded-full bg-black/40 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-fuchsia-500 via-rose-400 to-amber-400 transition-all duration-500"
style={{ width: `${progress}%`, boxShadow: "0 0 8px rgba(232,121,249,0.5)" }}
/>
</div>
{/* 累计奖励总览 */}
<div className="rounded-lg border border-fuchsia-400/20 bg-fuchsia-950/20 px-2.5 py-1.5 flex items-center gap-2 text-[10px] flex-wrap">
<Sparkles className="h-3 w-3 text-fuchsia-400 shrink-0" />
<span className="text-fuchsia-300/80"></span>
{rewards.crystals > 0 && (
<span className="text-emerald-300">+{rewards.crystals} </span>
)}
{rewards.insights > 0 && (
<span className="text-amber-300">+{rewards.insights} </span>
)}
{rewards.contact > 0 && (
<span className="text-fuchsia-300">+{rewards.contact} </span>
)}
{rewards.crystalsPerSecPct > 0 && (
<span className="text-emerald-300"> +{rewards.crystalsPerSecPct}%</span>
)}
{unlockedRelics.length === 0 && (
<span className="text-muted-foreground/60"></span>
)}
</div>
{/* 遗迹网格 — 2 列展示,每个卡片 min-h 防溢出 */}
<div className="flex-1 min-h-0 overflow-y-auto echo-scroll pr-1">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{RELIC_CODEX.map((relic) => {
const unlocked = !!state.achievements?.[`relic_${relic.id}`] || relic.unlock(state);
const isSelected = selected === relic.id;
return (
<button
key={relic.id}
onClick={() => setSelected(isSelected ? null : relic.id)}
className={`relative rounded-lg border px-2.5 py-2 transition-all duration-300 min-h-[72px] text-left ${
unlocked
? isSelected
? "border-white/30 bg-white/8"
: "border-white/15 bg-black/30 hover:bg-black/40"
: "border-white/5 bg-black/15 opacity-70"
}`}
style={
unlocked
? { boxShadow: `inset 0 0 14px ${relic.color}10` }
: undefined
}
>
{unlocked && (
<div
className="absolute -top-6 -right-6 h-14 w-14 rounded-full blur-2xl opacity-30 pointer-events-none"
style={{ background: relic.color }}
/>
)}
<div className="relative flex items-start gap-2">
{/* 图标 */}
<div
className="shrink-0 h-9 w-9 rounded-lg flex items-center justify-center text-lg"
style={{
background: unlocked ? `${relic.color}22` : "rgba(255,255,255,0.04)",
border: `1px solid ${unlocked ? relic.color + "55" : "rgba(255,255,255,0.08)"}`,
color: unlocked ? relic.color : "#555",
boxShadow: unlocked ? `0 0 10px ${relic.color}33` : "none",
}}
>
{unlocked ? relic.icon : <Lock className="h-3.5 w-3.5" />}
</div>
{/* 文本 */}
<div className="min-w-0 flex-1">
<div
className="text-[11px] font-semibold truncate"
style={{ color: unlocked ? relic.color : "rgba(255,255,255,0.5)" }}
>
{unlocked ? relic.name : "??? 未发现"}
</div>
{unlocked ? (
<>
<div className="text-[9px] text-muted-foreground/70 truncate flex items-center gap-1">
<Users className="h-2.5 w-2.5" />
{relic.race}
</div>
<div className="text-[9px] text-muted-foreground/60 truncate flex items-center gap-1">
<Clock className="h-2.5 w-2.5" />
{relic.era}
</div>
</>
) : (
<div className="text-[9px] text-muted-foreground/60 mt-0.5 line-clamp-2">
{relic.unlockHint}
</div>
)}
</div>
</div>
</button>
);
})}
</div>
{/* 选中遗迹的详细故事 */}
{selectedRelic && (
<div
className="mt-2 rounded-lg border p-3 relative overflow-hidden"
style={{
borderColor: `${selectedRelic.color}44`,
background: `linear-gradient(135deg, ${selectedRelic.color}11, rgba(0,0,0,0.4))`,
}}
>
<div
className="absolute -top-8 -right-8 h-24 w-24 rounded-full blur-2xl pointer-events-none"
style={{ background: `${selectedRelic.color}22` }}
/>
<div className="relative space-y-1.5">
<div className="flex items-center gap-2">
<span className="text-base" style={{ color: selectedRelic.color }}>
{selectedRelic.icon}
</span>
<span
className="text-sm font-semibold"
style={{ color: selectedRelic.color }}
>
{selectedRelic.name}
</span>
</div>
<div className="flex items-center gap-3 text-[9px] text-muted-foreground/80">
<span className="flex items-center gap-1">
<Users className="h-2.5 w-2.5" />
{selectedRelic.race}
</span>
<span className="flex items-center gap-1">
<Clock className="h-2.5 w-2.5" />
{selectedRelic.era}
</span>
</div>
<p className="text-[10px] text-muted-foreground/90 italic">
{selectedRelic.brief}
</p>
<div className="flex items-start gap-1.5 pt-1 border-t border-white/5">
<ScrollText
className="h-3 w-3 mt-0.5 shrink-0"
style={{ color: selectedRelic.color }}
/>
<p className="text-[10px] text-foreground/85 leading-relaxed">
{selectedRelic.story}
</p>
</div>
<div className="flex items-center gap-1.5 pt-1">
<Sparkles
className="h-3 w-3 shrink-0"
style={{ color: selectedRelic.color }}
/>
<span
className="text-[10px] font-mono"
style={{ color: selectedRelic.color + "cc" }}
>
{selectedRelic.rewardText}
</span>
</div>
</div>
</div>
)}
</div>
<style jsx global>{`
.echo-scroll::-webkit-scrollbar { width: 5px; }
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.14); border-radius: 3px; }
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
`}</style>
</div>
);
}
+260
View File
@@ -0,0 +1,260 @@
// 回响星核 / Echo Nexus — 文明遗迹图鉴(v0.10 新增)
// 深空考古世界观载体:8 大远古文明遗迹,每个承载独特故事碎片
// 通过游戏进度逐步解锁,增加"探索未知"的乐趣
import type { GameState } from "./types";
export interface RelicEntry {
id: string;
name: string;
/** 文明种族 */
race: string;
/** 距今时代 */
era: string;
/** 图标 emoji */
icon: string;
/** 主题色(emerald/rose/amber/fuchsia 四色全息) */
color: string;
/** 简短描述 */
brief: string;
/** 完整故事碎片(解锁后展示) */
story: string;
/** 解锁条件描述 */
unlockHint: string;
/** 解锁判定 */
unlock: (s: GameState) => boolean;
/** 解锁奖励 */
reward: {
crystals?: number;
insights?: number;
contact?: number;
};
rewardText: string;
}
export const RELIC_CODEX: RelicEntry[] = [
{
id: "relic_first_echo",
name: "初次回响",
race: "无名先驱",
era: "纪元前 · 未知",
icon: "✦",
color: "#34d399",
brief: "深空中第一缕被捕获的智慧信号。",
story:
"当自治无人机首次解码出这缕信号时,整个星核为之震颤。那不是噪声,而是某种古老文明留下的问候——跨越亿万年的回响,证明我们并不孤独。",
unlockHint: "发起第一次脉冲扫描",
unlock: (s) => s.totalDecoded >= 0 || s.crystals > 0,
reward: { crystals: 10 },
rewardText: "+10 晶体",
},
{
id: "relic_crystal_whisper",
name: "晶体低语",
race: "晶语族",
era: "第一纪元 · 晶石时代",
icon: "◈",
color: "#fb7185",
brief: "能够将记忆封存在晶体中的远古种族。",
story:
"晶语族相信,记忆是宇宙的本质。他们建造了巨大的晶体矩阵,将整个文明的历史封存在谐振频率中。当他们的恒星熄灭时,唯有这些晶体在虚空中持续低语。",
unlockHint: "累计解码 10 颗记忆晶体",
unlock: (s) => s.totalDecoded >= 10,
reward: { insights: 20, crystalsPerSecPct: 5 } as any,
rewardText: "+20 洞见 · 产能 +5%",
},
{
id: "relic_void_ark",
name: "虚空方舟",
race: "渡厄者",
era: "第二纪元 · 大迁徙",
icon: "❖",
color: "#fbbf24",
brief: "为逃离超新星爆发而建造的星际方舟。",
story:
"渡厄者的母星在一夜之间步入死亡。他们用三百年建造了十二艘方舟,每一艘都载着十亿灵魂的数字意识。如今,只有这一艘的残骸漂浮在我们的考古坐标上。",
unlockHint: "完成 5 次遗迹探险",
unlock: (s) => s.totalExpeditions >= 5,
reward: { insights: 30 },
rewardText: "+30 洞见",
},
{
id: "relic_silent_court",
name: "寂寂王朝",
race: "静默王族",
era: "第三纪元 · 铁律纪",
icon: "♛",
color: "#e879f9",
brief: "以沉默为律法的神秘王朝。",
story:
"静默王族统治了三百个恒星系,却从未留下任何文字。他们的律法刻在引力波中,他们的诗歌写在脉冲星里。我们只能通过残余的引力涟漪,窥见这个王朝的辉煌与寂灭。",
unlockHint: "击败 3 个探险 BOSS",
unlock: (s) => (s.bossKills ?? 0) >= 3,
reward: { contact: 10 },
rewardText: "+10 接触",
},
{
id: "relic_starforge",
name: "锻星炉",
race: "铸星者",
era: "第四纪元 · 恒星工程",
icon: "✺",
color: "#34d399",
brief: "能够人工点燃恒星的工程文明。",
story:
"铸星者不满足于利用恒星能量,他们要创造恒星。他们建造了锻星炉——一种能在虚空中凝聚物质的巨型结构。然而,每一次点火都会消耗一整个星系的物质,最终他们自己也成为了燃料。",
unlockHint: "解锁 6 项技术",
unlock: (s) => Object.values(s.tech).filter((v) => v > 0).length >= 6,
reward: { insights: 50, crystals: 100 },
rewardText: "+50 洞见 · +100 晶体",
},
{
id: "relic_memory_weaver",
name: "忆织者",
race: "织忆族",
era: "第五纪元 · 意识网络",
icon: "✧",
color: "#fb7185",
brief: "将集体意识编织成网络的心灵文明。",
story:
"织忆族没有个体概念。他们的每一个『我』都是网络的一个节点,死亡只是断开连接。当整个网络陷入沉睡时,他们编织了最后一张记忆之网,等待后人唤醒。我们的解码,正是唤醒。",
unlockHint: "拼凑 4 块记忆碎片",
unlock: (s) => Object.values(s.fragments).filter(Boolean).length >= 4,
reward: { contact: 15 },
rewardText: "+15 接触",
},
{
id: "relic_eclipse_tribe",
name: "蚀相部落",
race: "蚀相族",
era: "第六纪元 · 阴影纪",
icon: "☀",
color: "#fbbf24",
brief: "栖身于食恒星阴影中的猎手文明。",
story:
"蚀相族敬畏阴影。他们在双星系统的食相瞬间狩猎,在光与暗的边界舞蹈。当一颗恒星被吞噬殆尽,他们便迁徙至下一对双星。他们的遗迹,永远隐藏在某个行星的阴影里。",
unlockHint: "亲历 3 种星潮事件",
unlock: (s) => (s.starTidesEncountered ?? []).length >= 3,
reward: { insights: 40 },
rewardText: "+40 洞见",
},
{
id: "relic_ascended",
name: "飞升者遗迹",
race: "前代飞升者",
era: "终末纪元 · 超越",
icon: "✦",
color: "#e879f9",
brief: "曾经的飞升者留下的最后讯息。",
story:
"在我们之前,已有无数文明触碰到飞升的临界点。他们留下了什么?不是技术,不是财富,而是一句跨越时空的低语:「飞升不是终点,而是新的开始。勇敢者,继续探索。」",
unlockHint: "完成第一次飞升",
unlock: (s) => s.ascensions >= 1,
reward: { insights: 100, contact: 25 },
rewardText: "+100 洞见 · +25 接触",
},
{
id: "relic_constellation",
name: "星图密钥",
race: "观星者议会",
era: "永恒 · 星图守护者",
icon: "✧",
color: "#34d399",
brief: "守护星图天赋秘密的远古议会。",
story:
"观星者议会不属于任何时代。他们存在于所有时代的星图中,守护着飞升的真正秘密:每一道星图天赋,都是前代飞升者留下的礼物。集齐所有天赋,便能窥见宇宙的真相。",
unlockHint: "觉醒 6 道星图天赋",
unlock: (s) => (s.constellation ?? []).length >= 6,
reward: { insights: 80 },
rewardText: "+80 洞见",
},
{
id: "relic_chronicle",
name: "编年史之心",
race: "史官族",
era: "全纪元 · 记录者",
icon: "❖",
color: "#fb7185",
brief: "记录所有纪元兴衰的永恒史官。",
story:
"史官族从不干涉,只记录。他们的身体是活体的编年史,每一个细胞都承载着一个纪元的记忆。当最后一个史官死去时,他将所有记忆注入了这颗编年史之心,等待后人继承。",
unlockHint: "积累 5 段编年史",
unlock: (s) => (s.chronicle ?? []).length >= 5,
reward: { insights: 60, contact: 20 },
rewardText: "+60 洞见 · +20 接触",
},
{
id: "relic_attribute",
name: "四维之证",
race: "原型人格",
era: "元始 · 自我觉醒",
icon: "◈",
color: "#fbbf24",
brief: "自我觉醒的四维人格原型。",
story:
"探索力、智慧、勇气、灵感——这四维属性并非凭空而来。它们源自一个远古的原型人格,那个第一次说出「我」的存在。我们的每一次成长,都是对那个原型的回响。",
unlockHint: "任一角色属性达到 30",
unlock: (s) => {
const a = s.attributes;
if (!a) return false;
return (
a.exploration >= 30 ||
a.wisdom >= 30 ||
a.courage >= 30 ||
a.inspiration >= 30
);
},
reward: { insights: 50 },
rewardText: "+50 洞见",
},
{
id: "relic_cruise",
name: "深空航迹",
race: "远航者联盟",
era: "第七纪元 · 星海远航",
icon: "➤",
color: "#e879f9",
brief: "穿越星海寻找新家园的远航者。",
story:
"远航者联盟由十二个种族组成,他们共享一艘巨型母舰,在星海中寻找新的家园。他们留下了航迹图——每一道光痕都是一段旅程,每一个航点都是一个希望。我们的巡航,正是延续他们的航迹。",
unlockHint: "深空巡航累计通关 5 关",
unlock: (s) => {
try {
const data = localStorage.getItem("echo-nexus-cruise-v1");
if (!data) return false;
const parsed = JSON.parse(data);
return (parsed.totalClears ?? 0) >= 5;
} catch {
return false;
}
},
reward: { insights: 70, crystals: 200 },
rewardText: "+70 洞见 · +200 晶体",
},
];
/** 获取已解锁的遗迹列表 */
export function getUnlockedRelics(s: GameState): RelicEntry[] {
return RELIC_CODEX.filter((r) => r.unlock(s));
}
/** 获取图鉴进度(0-100 */
export function getRelicProgress(s: GameState): number {
const unlocked = getUnlockedRelics(s).length;
return (unlocked / RELIC_CODEX.length) * 100;
}
/** 获取已解锁遗迹的奖励总和 */
export function getRelicRewards(s: GameState) {
const unlocked = getUnlockedRelics(s);
let crystals = 0;
let insights = 0;
let contact = 0;
let crystalsPerSecPct = 0;
for (const r of unlocked) {
if (r.reward.crystals) crystals += r.reward.crystals;
if (r.reward.insights) insights += r.reward.insights;
if (r.reward.contact) contact += r.reward.contact;
if ((r.reward as any).crystalsPerSecPct) crystalsPerSecPct += (r.reward as any).crystalsPerSecPct;
}
return { crystals, insights, contact, crystalsPerSecPct };
}
+6 -4
View File
@@ -925,8 +925,9 @@ export const useGameStore = create<Store>()(
};
const newLog = [logEntry, ...s.expeditionLog].slice(0, 30);
// v0.10 工单 #2 修复:探险结束时立即清空 activeExpedition,避免 finished 状态卡死
// 保留 lastExpeditionSummary 用于入口展示「上次结果」
if (result.ended) {
// 探险结束(胜利或失败)
exp.finished = true;
}
@@ -988,7 +989,8 @@ export const useGameStore = create<Store>()(
}
set({
activeExpedition: exp,
// v0.10 工单 #2 修复:探险结束立即清空 activeExpedition,让玩家无障碍再出发
activeExpedition: result.ended ? null : exp,
crystals: newCrystals,
insights: newInsights,
contact: newContact,
@@ -1028,7 +1030,6 @@ export const useGameStore = create<Store>()(
const s = get();
if (!s.activeExpedition) return;
const exp = JSON.parse(JSON.stringify(s.activeExpedition));
exp.finished = true;
const logEntry = {
expeditionId: exp.id,
nodeType: "rest" as const,
@@ -1044,7 +1045,8 @@ export const useGameStore = create<Store>()(
finishedAt: Date.now(),
};
set({
activeExpedition: exp,
// v0.10 工单 #2 修复:撤退立即清空 activeExpedition,避免 finished 卡死
activeExpedition: null,
expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30),
lastExpeditionSummary: summary,
});
@@ -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→
@@ -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→
+73
View File
@@ -856,3 +856,76 @@ Stage Summary:
- 在线地址:https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/
- 定时任务已重建:job_id=228637,每 15 分钟 webDevReview
- 版本:v0.8 → v0.9
---
## v0.10 工单 #2 大重构 — 空间再分配 + 探索深度(2026-01-24
### 触发:Gitea Issue #2 + 用户追加反馈
- **Gitea Issue #2** "现阶段玩法过于简单,没有可玩性"
- BUG:探险只能一次后再也触发不了 / 元素偏移被遮挡 / 成就UI全遮挡 / 提示框碍眼
- 问题:占满空屏的没用按钮 / 世界观敷衍 / 成就系统无用 / 技术树太少
- **用户追加反馈**:80%空间只给一个只能点10次的球 / 长操作UI太小 / 成就框溢出 / 审美疲劳 / 先调研后重构
### 调研结论(放置+深空游戏UI设计)
- 《Cookie Clicker》:左右分栏,左侧点击区+右侧建筑列表
- 《Melvor Idle》:上下分区,顶部导航+主内容区
- 放置游戏原则:核心循环可见 / 进度可见 / 多面板协同 / 空间感
- 深空主题应有:星图导航 / 文明遗迹图鉴 / 时序日志 / 雷达扫描
### 核心改动(v0.10
#### 1. 布局重构 — 空间再分配(page.tsx)
- **旧**`grid-cols-[1fr_minmax(360px,420px)]` 左 60% 右 40%(晶体球占满)
- **新**`grid-cols-[minmax(300px,360px)_1fr]` 左 30% 右 70%(晶体球紧凑,右侧扩展)
- 左栏:晶体球(紧凑居中)+ 收益小卡 + **新增探索目标雷达**
- 右栏:解码面板 + 标签页(9个:探险/技术/星图/图谱/成就/信标/统计/角色/图鉴)
- 标签页响应式:小屏 3列3行,大屏 9列1行
- VLM 验证:左右比例 3:7,晶体球不再占满,无溢出重叠
#### 2. 探险 BUG 修复(gameStore.ts + ExpeditionPanel.tsx
- **根因**:探险结束 `finished=true``activeExpedition` 不清理,60秒后"收起结算"按钮消失但状态卡死
- **修复**`resolveCurrentNode``abortExpedition` 结束时立即 `activeExpedition: null`
- 保留 `lastExpeditionSummary` 用于入口展示"上次结果"(不限时间)
- 移除 `justFinished` 60秒限制和"收起结算"按钮
- **验证**agent-browser 完整跑通 探险→完成→再出发 流程 ✅
#### 3. 成就面板重写(AchievementsPanel.tsx
- **旧问题**2列网格 + overflow-hidden 导致成就框内元素只显示上半部分
- **新方案**:单列展示 + min-h-[68px] + line-clamp-2 + flex-1 min-w-0
- 新增分类过滤标签(全部/已解锁/未解锁,带计数)
- 新增顶部进度条(emerald→rose→fuchsia 渐变)
- 移除 overflow-hidden,改用 line-clamp 控制文本
- **验证**VLM 确认"无溢出或截断" ✅
#### 4. 新增探索目标雷达(ExplorationRadar.tsx
- 动态计算 6 类下一目标:技术节点/成就/记忆碎片/探险能量/飞升进度/星图天赋
- 取前 3 个最相关目标展示,带进度条 + 提示
- 让晶体球区"等待"变"期待",缓解"占地方没用"反馈
#### 5. 新增文明遗迹图鉴系统(relicCodex.ts + RelicCodex.tsx
- **12 个远古文明遗迹**,每个承载独特故事碎片:
1. 初次回响(无名先驱)2. 晶体低语(晶语族)3. 虚空方舟(渡厄者)
4. 寂寂王朝(静默王族)5. 锻星炉(铸星者)6. 忆织者(织忆族)
7. 蚀相部落(蚀相族)8. 飞升者遗迹 9. 星图密钥(观星者议会)
10. 编年史之心(史官族)11. 四维之证(原型人格)12. 深空航迹(远航者联盟)
- 每个遗迹:name/race/era/icon/color/brief/story/unlockHint/unlock/reward
- 解锁条件覆盖所有游戏系统(脉冲/解码/探险/BOSS/技术/碎片/星潮/飞升/星图/编年史/属性/巡航)
- UI:2列网格 + 点击展开详细故事 + 累计奖励总览
- 增加"第九艺术"世界观深度
#### 6. 版本号 v0.8 → v0.10
### QA 验证(agent-browser + VLM
- ✅ 布局重构:左右 3:7,晶体球紧凑,探索雷达可见,无溢出
- ✅ 成就面板:分类过滤可见,无溢出截断,进度条可见
- ✅ 遗迹图鉴:12个遗迹展示,进度 2/12,故事展开正常
- ✅ 探险流程:探索节点→前进→完成→自动清理→再出发(totalExp 2→3)
- ✅ lint 零错误
- ✅ VLM 整体视觉冲击力 8/10
### 未解决/下一步
- 🟡 技术树扩展(用户反馈"太少,分分钟点满")— 待 v0.11
- 🟡 成就系统价值化(成就解锁主动技能)— 待 v0.11
- 🟡 VLM 建议:解码区 hover 效果 / 状态文字对比 / 星云背景
- 🟡 部署 gh-pages