129 lines
5.1 KiB
TypeScript
Executable File
129 lines
5.1 KiB
TypeScript
Executable File
"use client";
|
|
// 回响星核 / Echo Nexus — 记忆图谱(碎片叙事)
|
|
import { useState } from "react";
|
|
import { useGameStore, FRAGMENTS } from "@/store/gameStore";
|
|
import { nextFragmentThreshold } from "@/store/gameStore";
|
|
import { Card } from "@/components/ui/card";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { BookOpen, Lock, Sparkles } from "lucide-react";
|
|
import type { Fragment } from "@/lib/game/types";
|
|
|
|
const ERA_NAMES = ["", "寂灭前夜", "谐振纪", "飞升潮", "回响纪", "接触纪"];
|
|
|
|
export function Codex() {
|
|
const fragments = useGameStore((s) => s.fragments);
|
|
const totalDecoded = useGameStore((s) => s.totalDecoded);
|
|
const nextThreshold = nextFragmentThreshold(totalDecoded);
|
|
const [selected, setSelected] = useState<Fragment | null>(null);
|
|
|
|
// v0.1 只展示第一纪元
|
|
const era1 = FRAGMENTS.filter((f) => f.era === 1);
|
|
const unlockedCount = era1.filter((f) => fragments[f.id]).length;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2 h-full">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
|
<BookOpen className="h-4 w-4 text-fuchsia-400" />
|
|
记忆图谱
|
|
</h3>
|
|
<span className="text-xs text-muted-foreground">
|
|
{unlockedCount}/{era1.length} · 第一纪元
|
|
</span>
|
|
</div>
|
|
|
|
{nextThreshold && (
|
|
<div className="text-[10px] text-muted-foreground/80 px-1">
|
|
再解码 <span className="text-fuchsia-300 font-mono">{nextThreshold - totalDecoded}</span> 颗 → 下一个碎片
|
|
</div>
|
|
)}
|
|
|
|
<ScrollArea className="flex-1 min-h-0">
|
|
<div className="grid grid-cols-2 gap-2 pr-2">
|
|
{era1.map((f, i) => {
|
|
const unlocked = !!fragments[f.id];
|
|
return (
|
|
<button
|
|
key={f.id}
|
|
onClick={() => unlocked && setSelected(f)}
|
|
disabled={!unlocked}
|
|
className={`relative aspect-[4/3] rounded-xl border text-left p-2 transition-all duration-300 overflow-hidden ${
|
|
unlocked
|
|
? "border-fuchsia-400/40 bg-gradient-to-br from-fuchsia-950/40 to-black/40 hover:border-fuchsia-400/70 hover:scale-[1.02] cursor-pointer"
|
|
: "border-white/5 bg-black/30 cursor-not-allowed"
|
|
}`}
|
|
style={
|
|
unlocked
|
|
? { boxShadow: "0 0 16px rgba(232,121,249,0.15)" }
|
|
: undefined
|
|
}
|
|
>
|
|
{/* 编号 */}
|
|
<span
|
|
className={`absolute top-1.5 right-1.5 text-[9px] font-mono ${
|
|
unlocked ? "text-fuchsia-300/70" : "text-white/20"
|
|
}`}
|
|
>
|
|
{String(i + 1).padStart(2, "0")}
|
|
</span>
|
|
{unlocked ? (
|
|
<>
|
|
<div className="absolute -top-3 -right-3 h-12 w-12 rounded-full bg-fuchsia-500/20 blur-xl" />
|
|
<Sparkles className="h-3 w-3 text-fuchsia-300 mb-1" />
|
|
<div className="text-[11px] font-semibold text-fuchsia-100 leading-tight">
|
|
{f.title}
|
|
</div>
|
|
<div className="text-[9px] text-muted-foreground mt-1 line-clamp-2">
|
|
{f.echo.slice(0, 28)}…
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center h-full gap-1">
|
|
<Lock className="h-4 w-4 text-white/20" />
|
|
<span className="text-[9px] text-white/30">
|
|
需解码 {f.threshold} 颗
|
|
</span>
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* 详情弹层 */}
|
|
{selected && (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
|
|
onClick={() => setSelected(null)}
|
|
>
|
|
<Card
|
|
className="max-w-md w-full p-5 bg-gradient-to-br from-fuchsia-950/60 to-black/80 border-fuchsia-400/40"
|
|
onClick={(e) => e.stopPropagation()}
|
|
style={{ boxShadow: "0 0 40px rgba(232,121,249,0.25)" }}
|
|
>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Sparkles className="h-4 w-4 text-fuchsia-300" />
|
|
<span className="text-[10px] text-fuchsia-300/70 font-mono">
|
|
第 {selected.era} 纪元 · {ERA_NAMES[selected.era] || ""}
|
|
</span>
|
|
</div>
|
|
<h4 className="text-lg font-bold text-fuchsia-100 mb-3">
|
|
{selected.title}
|
|
</h4>
|
|
<p className="text-sm text-foreground/90 leading-relaxed italic">
|
|
「{selected.echo}」
|
|
</p>
|
|
<button
|
|
onClick={() => setSelected(null)}
|
|
className="mt-4 text-xs text-muted-foreground hover:text-foreground transition"
|
|
>
|
|
关闭
|
|
</button>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|