"use client"; // 回响星核 / Echo Nexus — 回响编年史对话框(v0.4 跨周目叙事时间轴) import { useEffect, useState, useMemo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { useGameStore } from "@/store/gameStore"; import { PERK_NAME_MAP, getCategoryColor, getCategoryName, getPerkCategoryBreakdown, regenerateLoreFromEntry, } from "@/lib/game/chronicle"; import { getPerk } from "@/lib/game/constellation"; import { TIDE_EVENTS } from "@/lib/game/starTide"; import { sfx } from "@/hooks/useAudio"; import type { ChronicleEntry } from "@/lib/game/types"; import { BookOpen, Clock, Swords, Trophy, Sparkles, Star, Waves, Cpu, Zap, ChevronLeft, ChevronRight } from "lucide-react"; import { Button } from "@/components/ui/button"; interface Props { open: boolean; onOpenChange: (v: boolean) => void; } /** tide ID (tide_xxx) → 展示信息(从 TIDE_EVENTS 复用) */ function getTideDisplay(tideId: string): { name: string; color: string } { // tideId 形如 "tide_crystal",剥离前缀后查 TIDE_EVENTS const type = tideId.startsWith("tide_") ? tideId.slice(5) : tideId; const ev = TIDE_EVENTS[type as keyof typeof TIDE_EVENTS]; return ev ? { name: ev.name, color: ev.color } : { name: tideId, color: "#94a3b8" }; } function formatDuration(sec: number): string { if (sec < 60) return `${sec}秒`; const m = Math.floor(sec / 60); const s = sec % 60; if (m < 60) return s > 0 ? `${m}分${s}秒` : `${m}分钟`; const h = Math.floor(m / 60); return `${h}小时${m % 60}分`; } function formatDate(ts: number): string { const d = new Date(ts); const mm = String(d.getMonth() + 1).padStart(2, "0"); const dd = String(d.getDate()).padStart(2, "0"); const hh = String(d.getHours()).padStart(2, "0"); const mi = String(d.getMinutes()).padStart(2, "0"); return `${mm}-${dd} ${hh}:${mi}`; } export function ChronicleDialog({ open, onOpenChange }: Props) { const chronicle = useGameStore((s) => s.chronicle ?? []); const ascensions = useGameStore((s) => s.ascensions ?? 0); const bossKills = useGameStore((s) => s.bossKills ?? 0); const tidesAll = useGameStore((s) => s.starTidesEncountered ?? []); // 倒序展示(最新的在前) const entries: ChronicleEntry[] = useMemo(() => [...chronicle].reverse(), [chronicle]); // v0.8.2 分页(工单 #8):编年史上限 200 条,每页 10 条 const PAGE_SIZE = 10; const totalPages = Math.max(1, Math.ceil(entries.length / PAGE_SIZE)); const [page, setPage] = useState(1); // 当 entries 变化(新飞升)且当前页超出范围时,回到第 1 页(最新) useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect if (page > totalPages) setPage(1); }, [page, totalPages]); // 打开时默认回到第 1 页(最新) useEffect(() => { if (open) { sfx("chronicleOpen"); // eslint-disable-next-line react-hooks/set-state-in-effect setPage(1); } }, [open]); const pagedEntries = entries.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); return ( {/* 顶部装饰条 */}
回响编年史 Chronicle of Echoes 每一次飞升都在深空留下回响。这里铭刻着跨越维度的全部纪元。 {/* 总览统计条 */}
} label="飞升次数" value={ascensions} color="#e879f9" /> } label="BOSS 击破" value={bossKills} color="#fbbf24" /> } label="星潮亲历" value={tidesAll.length} color="#34d399" />
{/* 时间轴滚动区 */}
{entries.length === 0 ? ( ) : (
{/* 中央时间轴线 */}
{pagedEntries.map((entry, idx) => ( ))}
)}
{/* v0.8.2 分页控制栏(工单 #8) */} {entries.length > PAGE_SIZE && (
第 {page} / {totalPages} 页 · 共 {entries.length} 条飞升记录
)}
); } function OverviewStat({ icon, label, value, color, }: { icon: React.ReactNode; label: string; value: number; color: string; }) { return (
{icon}
{label}
{value}
); } function EmptyState({ ascensions }: { ascensions: number }) { const isPreV04 = ascensions > 0; return (
{isPreV04 ? ( <>
已飞升 {ascensions} 次,但编年史尚无记录
编年史于 v0.4 启用。下一次飞升将自动铭刻新纪元,开启永恒叙事时间轴。
) : ( <>
尚未飞升,等待第一次跨越维度
当接触进度满溢时,发起飞升将开启新周目,并在编年史中铭刻一段永恒回响。
)}
); } function ChronicleCard({ entry, isLatest, index, }: { entry: ChronicleEntry; isLatest: boolean; index: number; }) { // 5 纪元循环颜色 const epochColors = ["#34d399", "#fb7185", "#fbbf24", "#e879f9", "#fcd34d"]; const epochColor = epochColors[(entry.ascensionNumber - 1) % 5]; return (
{/* 时间轴节点 */}
{isLatest && (
)}
{/* 卡片主体 */}
{/* 卡片头部 */}
#{String(entry.ascensionNumber).padStart(2, "0")}

{entry.epochName}

{formatDate(entry.timestamp)} · {formatDuration(entry.durationSec)} {isLatest && ( <> · 最新 )}
{/* 叙事文本(显示时重新生成,修复历史条目中 tide_ruins 等原始键名) */}
{regenerateLoreFromEntry(entry)}
{/* 统计芯片 */}
} label="解码" value={entry.summary.crystalsDecodedThisRun} color="#fb7185" /> } label="技术" value={entry.summary.techsUnlocked} color="#34d399" /> } label="探险" value={entry.summary.expeditionsCompleted} color="#fbbf24" /> {entry.summary.bossKills > 0 && ( } label="BOSS" value={entry.summary.bossKills} color="#e879f9" /> )} } label="星潮" value={entry.tidesThisRun.length} color="#94a3b8" /> } label="星图" value={entry.summary.constellationsTotal} color="#fcd34d" />
{/* 星潮列表 */} {entry.tidesThisRun.length > 0 && (
{entry.tidesThisRun.map((tide) => { const td = getTideDisplay(tide); return ( ✦ {td.name} ); })}
)} {/* 天赋徽章 */} {entry.perksThisAscension.length > 0 && (
{entry.perksThisAscension.map((perkId) => { const perk = getPerk(perkId); const cat = perk?.category || "cosmic"; const color = getCategoryColor(cat); const catName = getCategoryName(cat); const perkName = PERK_NAME_MAP[perkId] || perkId; return ( {perkName} ); })}
)} {/* 里程碑 */} {entry.milestones.length > 0 && (
{entry.milestones.map((m, i) => ( {m} ))}
)}
); } function StatChip({ icon, label, value, color, }: { icon: React.ReactNode; label: string; value: number; color: string; }) { return ( {icon} {label} {value} ); } /** 当前周目进度预览(在编年史对话框中以小卡片显示,提示玩家本周目已有数据) */ export function ChronicleCurrentRunBadge() { const runStart = useGameStore((s) => s.runStart); const totalExpeditions = useGameStore((s) => s.totalExpeditions); const totalDecoded = useGameStore((s) => s.totalDecoded); const techCount = useGameStore((s) => Object.keys(s.tech || {}).length); const exps = Math.max(0, totalExpeditions - (runStart?.expeditionsCompleted || 0)); const decoded = Math.max(0, totalDecoded - (runStart?.crystalsDecoded || 0)); const techs = Math.max(0, techCount - (runStart?.techsUnlocked || 0)); if (exps === 0 && decoded === 0 && techs === 0) return null; return (
本周目: {decoded}解码 · {techs}技术 · {exps}探险
); }