"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 (
);
}
function OverviewStat({
icon,
label,
value,
color,
}: {
icon: React.ReactNode;
label: string;
value: number;
color: string;
}) {
return (
);
}
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 (
{/* 时间轴节点 */}
{/* 卡片主体 */}
{/* 卡片头部 */}
#{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}探险
);
}