3cf83ed6-dca1-44be-bf7a-a27d19337739
This commit is contained in:
+39
-29
@@ -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 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
|
||||
<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 标签页响应式布局,小屏 3 列 3 行避免拥挤遮挡,大屏 9 列 */}
|
||||
<TabsList className="grid grid-cols-3 sm:grid-cols-9 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
|
||||
<TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
<span className="leading-none">探险</span>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user