"use client"; // 回响星核 / Echo Nexus — 深空信标面板(v0.5 每日挑战 + 本地排行榜) import { useState, useEffect, useCallback } from "react"; import { useGameStore } from "@/store/gameStore"; import { useToast } from "@/hooks/use-toast"; import { sfx } from "@/hooks/useAudio"; import { generateDailyChallenge, loadDailyProgress, loadLeaderboard, claimBeaconReward, msUntilNextDay, formatCountdown, getTodayKey, BEACON_DIFFICULTY, BEACON_TYPE_META, type BeaconDailyChallenge, type BeaconDailyProgress, type BeaconScoreEntry, type BeaconChallengeType, type BeaconDifficulty, } from "@/lib/game/beacon"; import { formatNum } from "@/lib/game/config"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { Radio, Clock, Trophy, Sparkles, Award, Crown, Medal } from "lucide-react"; const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"]; function rankBadge(rank: number) { if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" }; if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" }; if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" }; return null; } export function BeaconPanel() { const insights = useGameStore((s) => s.insights); const grantBeaconReward = useGameStore((s) => s.grantBeaconReward); const { toast } = useToast(); const [challenge, setChallenge] = useState(null); const [progress, setProgress] = useState(null); const [leaderboard, setLeaderboard] = useState([]); const [countdown, setCountdown] = useState("00:00:00"); const [now, setNow] = useState(Date.now()); // 初始化 + 每秒刷新(进度 + 倒计时) useEffect(() => { setChallenge(generateDailyChallenge()); setProgress(loadDailyProgress()); setLeaderboard(loadLeaderboard()); const id = setInterval(() => { setNow(Date.now()); setProgress(loadDailyProgress()); setChallenge((c) => c ?? generateDailyChallenge()); }, 1000); return () => clearInterval(id); }, []); useEffect(() => { setCountdown(formatCountdown(msUntilNextDay(new Date(now)))); }, [now]); const handleClaim = useCallback(() => { if (!challenge || !progress) return; if (progress.completedAt === null || progress.claimed) return; const res = claimBeaconReward(challenge, progress); setLeaderboard(res.leaderboard); setProgress(loadDailyProgress()); // 发放奖励到游戏状态 grantBeaconReward(res.rewardInsight, res.rewardContact); sfx("achievement"); toast({ title: "✦ 信标奖励已领取", description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(1)} 接触 · 得分 ${res.score}`, }); }, [challenge, progress, toast, grantBeaconReward]); if (!challenge || !progress) { return (
正在校准深空信标…
); } const diffMeta = BEACON_DIFFICULTY[challenge.difficulty]; const typeMeta = BEACON_TYPE_META[challenge.type]; const pct = Math.min(100, (progress.progress / challenge.goal) * 100); const isCompleted = progress.completedAt !== null; const isClaimed = progress.claimed; const canClaim = isCompleted && !isClaimed; return (
{/* 头部:信标 + 倒计时 */}

深空信标

次日重置 {countdown}
{/* 每日挑战卡片 */}
{/* 背景装饰:脉冲环 */} {!isCompleted && ( <>
)}
{/* 难度 + 类型 标签 */}
{diffMeta.icon} {diffMeta.label} {typeMeta.icon} {typeMeta.label} {isCompleted && ( 已完成 )}
{/* 挑战标题 */}

{challenge.title}

{challenge.desc}

{/* 进度条 */}
进度 {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
{/* 奖励 + 领取按钮 */}
奖励: {challenge.rewardInsight > 0 && ( +{formatNum(challenge.rewardInsight)}洞见 )} +{challenge.rewardContact.toFixed(1)}接触
{/* 完成时长 */} {isCompleted && progress.durationSec > 0 && (
完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
)}
{/* 本地排行榜 */}
深空排行榜 本地 · Top {leaderboard.length || 0}
{leaderboard.length === 0 ? (
尚无记录。完成今日信标即可登榜。
) : (
{leaderboard.map((entry, i) => { const rb = rankBadge(i); const eDiff = BEACON_DIFFICULTY[entry.difficulty]; const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType]; const isMine = entry.dateKey === getTodayKey(); return (
{/* 排名 */} {rb ? ( ) : ( {i + 1} )} {/* 类型 + 难度 */} {eDiff.icon} {eType.label} {entry.progress >= 1 && } {/* 用时 */} {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s {/* 分数 */} {formatNum(entry.score)}
); })}
)}
{/* 难度图例 */}
{DIFFICULTY_ORDER.map((d) => { const m = BEACON_DIFFICULTY[d]; return ( {m.icon} {m.label} ×{m.mult} ); })}
); }