"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}」
) : (
「{LOCKED_PLACEHOLDER}」
)}
{!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 && (
此碎片暂无更深层叙事。
)}
)}
);
}