Files
echo-nexus/src/components/game/AchievementsPanel.tsx
T

108 lines
4.4 KiB
TypeScript
Executable File

"use client";
// 回响星核 / Echo Nexus — 成就系统面板
import { useGameStore } from "@/store/gameStore";
import { ACHIEVEMENTS } from "@/lib/game/achievements";
import { achievementBonuses } from "@/lib/game/achievements";
import { Trophy, Lock } from "lucide-react";
export function AchievementsPanel() {
const achievements = useGameStore((s) => s.achievements);
const unlockedCount = Object.values(achievements).filter(Boolean).length;
const bonus = achievementBonuses(achievements);
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">
<Trophy className="h-4 w-4 text-amber-400" />
成就
</h3>
<span className="text-[10px] text-muted-foreground font-mono">
{unlockedCount} / {ACHIEVEMENTS.length}
</span>
</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]">
<span className="text-amber-300/80">永久加成</span>
{bonus.crystalsPerSecPct > 0 && (
<span className="text-emerald-300">产能 +{bonus.crystalsPerSecPct}%</span>
)}
{bonus.insightPct > 0 && (
<span className="text-fuchsia-300">洞见 +{bonus.insightPct}%</span>
)}
{bonus.crystalsPerSecPct === 0 && bonus.insightPct === 0 && (
<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) => {
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 ${
unlocked
? "border-white/15 bg-black/30"
: "border-white/5 bg-black/15 opacity-60"
}`}
style={
unlocked
? { boxShadow: `inset 0 0 16px ${a.color}11` }
: undefined
}
>
{unlocked && (
<div
className="absolute -top-6 -right-6 h-16 w-16 rounded-full blur-2xl opacity-40"
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
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>
{unlocked && (
<div className="text-[9px] mt-1 font-mono" style={{ color: a.color + "cc" }}>
{a.rewardText}
</div>
)}
</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; }
`}</style>
</div>
);
}