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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user