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

142 lines
5.2 KiB
TypeScript

"use client";
// 回响星核 / Echo Nexus — 技术树
import { useGameStore } from "@/store/gameStore";
import { TECH_TREE, TECH_BRANCH_META, formatNum } from "@/lib/game/config";
import { sfx } from "@/hooks/useAudio";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Pickaxe,
ScanLine,
Rocket,
BookOpen,
Check,
Lock,
} from "lucide-react";
import type { TechBranch } from "@/lib/game/types";
const ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
Pickaxe,
ScanLine,
Rocket,
BookOpen,
};
const BRANCH_COLOR: Record<string, string> = {
mining: "#34d399",
decoding: "#fb7185",
expedition: "#fbbf24",
narrative: "#e879f9",
};
export function TechTree() {
const tech = useGameStore((s) => s.tech);
const insights = useGameStore((s) => s.insights);
const buyTech = useGameStore((s) => s.buyTech);
const branches: TechBranch[] = ["mining", "decoding", "expedition", "narrative"];
return (
<div className="flex flex-col gap-3 h-full">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold">技术树</h3>
<span className="text-xs text-muted-foreground">
洞见 <span className="text-amber-300 font-mono">{formatNum(insights)}</span>
</span>
</div>
<div className="flex-1 overflow-y-auto echo-scroll pr-1 space-y-3">
{branches.map((b) => {
const meta = TECH_BRANCH_META[b];
const Icon = ICONS[meta.icon];
const color = BRANCH_COLOR[b];
const nodes = TECH_TREE.filter((t) => t.branch === b);
return (
<div key={b}>
<div className="flex items-center gap-2 mb-1.5 px-1">
<Icon className="h-3.5 w-3.5" />
<span className="text-xs font-medium" style={{ color }}>
{meta.name}
</span>
<span className="text-[10px] text-muted-foreground/70">
{meta.desc}
</span>
</div>
<div className="grid gap-1.5">
{nodes.map((node) => {
const owned = (tech[node.id] ?? 0) >= 1;
const affordable = insights >= node.cost;
return (
<Card
key={node.id}
className={`p-2.5 transition-all duration-200 ${
owned
? "bg-white/5 border-white/10"
: affordable
? "bg-black/30 border-white/15 hover:border-white/30 cursor-pointer"
: "bg-black/20 border-white/5 opacity-60"
}`}
style={
owned
? { boxShadow: `inset 0 0 0 1px ${color}44, 0 0 12px ${color}22` }
: undefined
}
>
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span
className="text-xs font-semibold truncate"
style={{ color: owned ? color : undefined }}
>
{node.name}
</span>
{owned && (
<Check className="h-3 w-3" style={{ color }} />
)}
</div>
<p className="text-[10px] text-muted-foreground/80 truncate">
{node.desc}
</p>
</div>
{!owned && (
<Button
size="sm"
variant={affordable ? "default" : "ghost"}
disabled={!affordable}
onClick={() => {
if (buyTech(node.id)) sfx("techBuy");
}}
className="h-7 px-2 text-[11px] shrink-0"
style={
affordable
? {
background: `${color}22`,
border: `1px solid ${color}66`,
color,
}
: undefined
}
>
<Lock className="h-2.5 w-2.5 mr-1" />
{node.cost}
</Button>
)}
</div>
</Card>
);
})}
</div>
</div>
);
})}
</div>
<style jsx global>{`
.echo-scroll::-webkit-scrollbar { width: 5px; }
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
`}</style>
</div>
);
}