"use client"; // 回响星核 / Echo Nexus — 记忆图谱(碎片叙事 + v0.15 多层文本) import { useState, useMemo } from "react"; import { useShallow } from "zustand/react/shallow"; import { useGameStore, FRAGMENTS, nextFragmentThreshold } from "@/store/gameStore"; import { isLoreLayerUnlocked } from "@/lib/game/config"; import { Card } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { BookOpen, Lock, Sparkles } from "lucide-react"; import type { Fragment, FragmentLoreLayer } from "@/lib/game/types"; const ERA_NAMES = ["", "寂灭前夜", "谐振纪", "飞升潮", "回响纪", "接触纪"]; /** 锁定层用的乱码占位文本(视觉上像被加密的全息碎片) */ const LOCKED_PLACEHOLDER = "▓░▒░ ▓▒░▒▓ ░▒▓░ ▒▓░▒░ ▓▒░ ▓░▒░▒ ░▒▓░▒ ▓░▒ ░▒▓░ ▒▓░▒▓░ ▓▒░░▒▓ ░▒▓ ▓░▒▒░ ▓▒░▓░▒ ░▒▓░ ▒▓░▒░ ▓▒░ ░▒▓░"; /** 渲染单层叙事文本(已解锁 / 未解锁两态)。L1=amber / L2=fuchsia */ function LoreLayerBlock({ layer, unlocked, }: { layer: FragmentLoreLayer; unlocked: boolean; }) { const isL1 = layer.layer === 1; const accentText = isL1 ? "text-amber-300" : "text-fuchsia-300"; const accentBorder = isL1 ? "border-amber-400/30" : "border-fuchsia-400/30"; const accentBg = isL1 ? "bg-amber-950/20" : "bg-fuchsia-950/20"; const accentBody = isL1 ? "text-amber-50/90" : "text-fuchsia-50/90"; const accentChipBg = isL1 ? "bg-amber-500/15" : "bg-fuchsia-500/15"; const accentChipText = isL1 ? "text-amber-300" : "text-fuchsia-300"; const layerTag = isL1 ? "L1" : "L2"; return (
{layerTag} {unlocked ? layer.title : "???"}
{!unlocked && ( {layer.unlockHint} )}
{unlocked ? (

「{layer.text}」

) : ( )} {!unlocked && (
🔒 {layer.unlockHint}
)}
); } export function Codex() { // useShallow 一次性订阅所有判定所需的 state 切片 const { fragments, totalDecoded, ascensions, contact, bossKills } = useGameStore( useShallow((s) => ({ fragments: s.fragments, totalDecoded: s.totalDecoded, ascensions: s.ascensions, contact: s.contact, bossKills: s.bossKills, })), ); const nextThreshold = nextFragmentThreshold(totalDecoded); const [selected, setSelected] = useState(null); // v0.1 只展示第一纪元 const era1 = useMemo(() => FRAGMENTS.filter((f) => f.era === 1), []); const unlockedCount = era1.filter((f) => fragments[f.id]).length; // 用于判定解锁的快照对象(结构匹配 isLoreLayerUnlocked 的 Pick) const stateSnapshot = { ascensions, totalDecoded, contact, fragments, bossKills, }; // 深层叙事统计:所有 loreLayer 中已解锁数 / 总数 const loreStats = useMemo(() => { let total = 0; let unlocked = 0; for (const f of era1) { if (!f.loreLayers) continue; for (const layer of f.loreLayers) { total += 1; if (isLoreLayerUnlocked(layer.unlock, stateSnapshot)) unlocked += 1; } } return { total, unlocked }; }, [era1, ascensions, totalDecoded, contact, fragments, bossKills]); return (

记忆图谱

{unlockedCount}/{era1.length} · 第一纪元
{/* v0.15 深层叙事统计 */}
深层叙事 {loreStats.unlocked} / {loreStats.total} 已解锁
{nextThreshold && (
再解码 {nextThreshold - totalDecoded} 颗 → 下一个碎片
)}
{era1.map((f, i) => { const unlocked = !!fragments[f.id]; // 计算此碎片 L1 / L2 解锁状态(用于标题旁标记) const l1Layer = f.loreLayers?.find((l) => l.layer === 1); const l2Layer = f.loreLayers?.find((l) => l.layer === 2); const l1Unlocked = l1Layer ? isLoreLayerUnlocked(l1Layer.unlock, stateSnapshot) : false; const l2Unlocked = l2Layer ? isLoreLayerUnlocked(l2Layer.unlock, stateSnapshot) : false; const hasDeepUnlocked = l1Unlocked || l2Unlocked; return ( ); })}
{/* 详情弹层 — v0.15 分层渲染 */} {selected && (
setSelected(null)} > e.stopPropagation()} style={{ boxShadow: "0 0 40px rgba(232,121,249,0.25)" }} >
第 {selected.era} 纪元 · {ERA_NAMES[selected.era] || ""}

{selected.title}

{/* L0 表层回响(永远显示,emerald 主题) */}
L0 表层回响

「{selected.echo}」

{/* L1 / L2 深层叙事(条件渲染,按 layer 升序) */} {selected.loreLayers ?.slice() .sort((a, b) => a.layer - b.layer) .map((layer) => { const unlocked = isLoreLayerUnlocked(layer.unlock, stateSnapshot); return (
); })} {/* 若该碎片没有 loreLayers */} {!selected.loreLayers?.length && (
此碎片暂无更深层叙事。
)}
)}
); }