1→"use client"; 2→// 回响星核 / Echo Nexus — 深空信标面板 3→// v0.5:每日挑战 + 本地排行榜 4→// v0.8:周挑战 + 信标链(连续完成奖励) 5→import { useState, useEffect, useCallback } from "react"; 6→import { useGameStore } from "@/store/gameStore"; 7→import { useToast } from "@/hooks/use-toast"; 8→import { sfx } from "@/hooks/useAudio"; 9→import { 10→ generateDailyChallenge, 11→ generateWeeklyChallenge, 12→ loadDailyProgress, 13→ loadWeeklyProgress, 14→ loadLeaderboard, 15→ loadChainState, 16→ claimBeaconReward, 17→ BEACON_DIFFICULTY, 18→ BEACON_TYPE_META, 19→ BEACON_CHAIN_MILESTONES, 20→ BEACON_CHAIN_REWARDS, 21→ getWeekKey, 22→ getTodayKey, 23→ getNextMilestone, 24→ getChainProgress, 25→ msUntilNextDay, 26→ msUntilNextWeek, 27→ formatCountdown, 28→ type BeaconDailyChallenge, 29→ type BeaconDailyProgress, 30→ type BeaconWeeklyChallenge, 31→ type BeaconWeeklyProgress, 32→ type BeaconScoreEntry, 33→ type BeaconChallengeType, 34→ type BeaconDifficulty, 35→ type BeaconChainState, 36→} from "@/lib/game/beacon"; 37→import { formatNum } from "@/lib/game/config"; 38→import { Button } from "@/components/ui/button"; 39→import { Progress } from "@/components/ui/progress"; 40→import { 41→ Radio, 42→ Clock, 43→ Trophy, 44→ Sparkles, 45→ Award, 46→ Crown, 47→ Medal, 48→ Link2, 49→ Flame, 50→ Zap, 51→} from "lucide-react"; 52→ 53→const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"]; 54→ 55→function rankBadge(rank: number) { 56→ if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" }; 57→ if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" }; 58→ if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" }; 59→ return null; 60→} 61→ 62→export function BeaconPanel() { 63→ const grantBeaconReward = useGameStore((s) => s.grantBeaconReward); 64→ const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon); 65→ const claimChainReward = useGameStore((s) => s.claimChainReward); 66→ const { toast } = useToast(); 67→ 68→ const [challenge, setChallenge] = useState(null); 69→ const [progress, setProgress] = useState(null); 70→ const [weeklyChallenge, setWeeklyChallenge] = 71→ useState(null); 72→ const [weeklyProgress, setWeeklyProgress] = 73→ useState(null); 74→ const [chainState, setChainState] = useState(null); 75→ const [leaderboard, setLeaderboard] = useState([]); 76→ const [dayCountdown, setDayCountdown] = useState("00:00:00"); 77→ const [weekCountdown, setWeekCountdown] = useState("00:00:00"); 78→ const [now, setNow] = useState(Date.now()); 79→ 80→ // 初始化 + 每秒刷新(进度 + 倒计时) 81→ useEffect(() => { 82→ setChallenge(generateDailyChallenge()); 83→ setProgress(loadDailyProgress()); 84→ setWeeklyChallenge(generateWeeklyChallenge()); 85→ setWeeklyProgress(loadWeeklyProgress()); 86→ setChainState(loadChainState()); 87→ setLeaderboard(loadLeaderboard()); 88→ const id = setInterval(() => { 89→ setNow(Date.now()); 90→ setProgress(loadDailyProgress()); 91→ setWeeklyProgress(loadWeeklyProgress()); 92→ setChainState(loadChainState()); 93→ setChallenge((c) => c ?? generateDailyChallenge()); 94→ setWeeklyChallenge((c) => c ?? generateWeeklyChallenge()); 95→ }, 1000); 96→ return () => clearInterval(id); 97→ }, []); 98→ 99→ useEffect(() => { 100→ setDayCountdown(formatCountdown(msUntilNextDay(new Date(now)))); 101→ setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now)))); 102→ }, [now]); 103→ 104→ const handleClaimDaily = useCallback(() => { 105→ if (!challenge || !progress) return; 106→ if (progress.completedAt === null || progress.claimed) return; 107→ const res = claimBeaconReward(challenge, progress); 108→ setLeaderboard(res.leaderboard); 109→ setProgress(loadDailyProgress()); 110→ // 发放奖励到游戏状态 111→ grantBeaconReward(res.rewardInsight, res.rewardContact); 112→ sfx("achievement"); 113→ toast({ 114→ title: "✦ 每日信标奖励已领取", 115→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed( 116→ 1 117→ )} 接触 · 得分 ${res.score}`, 118→ }); 119→ }, [challenge, progress, toast, grantBeaconReward]); 120→ 121→ const handleClaimWeekly = useCallback(() => { 122→ if (!weeklyChallenge || !weeklyProgress) return; 123→ if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return; 124→ const res = claimWeeklyBeacon(); 125→ setLeaderboard(loadLeaderboard()); 126→ setWeeklyProgress(loadWeeklyProgress()); 127→ if (res.rewardInsight > 0 || res.rewardContact > 0) { 128→ sfx("achievement"); 129→ toast({ 130→ title: "✦ 周挑战奖励已领取", 131→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed( 132→ 1 133→ )} 接触 · 得分 ${res.score}`, 134→ }); 135→ } 136→ }, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]); 137→ 138→ const handleClaimChain = useCallback( 139→ (milestone: number) => { 140→ const reward = BEACON_CHAIN_REWARDS.find( 141→ (r) => r.milestone === milestone 142→ ); 143→ const res = claimChainReward(milestone); 144→ setChainState(loadChainState()); 145→ if (res.ok) { 146→ sfx("achievement"); 147→ toast({ 148→ title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`, 149→ description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed( 150→ 1 151→ )} 接触`, 152→ }); 153→ } 154→ }, 155→ [toast, claimChainReward] 156→ ); 157→ 158→ if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) { 159→ return ( 160→
161→ 正在校准深空信标… 162→
163→ ); 164→ } 165→ 166→ const diffMeta = BEACON_DIFFICULTY[challenge.difficulty]; 167→ const typeMeta = BEACON_TYPE_META[challenge.type]; 168→ const pct = Math.min(100, (progress.progress / challenge.goal) * 100); 169→ const isCompleted = progress.completedAt !== null; 170→ const isClaimed = progress.claimed; 171→ const canClaim = isCompleted && !isClaimed; 172→ 173→ // 周挑战派生量 174→ const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty]; 175→ const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type]; 176→ const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100); 177→ const wCompleted = weeklyProgress.completedAt !== null; 178→ const wClaimed = weeklyProgress.claimed; 179→ const wCanClaim = wCompleted && !wClaimed; 180→ 181→ // 信标链派生量 182→ const chainProgress = getChainProgress(chainState.currentStreak); 183→ const nextMilestone = getNextMilestone(chainState.currentStreak); 184→ const todayKey = getTodayKey(); 185→ const completedToday = chainState.lastCompletedDateKey === todayKey; 186→ 187→ return ( 188→
189→ 225→ 226→ {/* 头部:信标 + 倒计时 */} 227→
228→

229→ 230→ 深空信标 231→

232→
233→ 234→ 次日重置 235→ {dayCountdown} 236→
237→
238→ 239→ {/* 每日挑战卡片 */} 240→
248→ {/* 背景装饰:脉冲环 */} 249→ {!isCompleted && ( 250→ <> 251→
258→
265→ 266→ )} 267→ 268→
269→ {/* 难度 + 类型 标签 */} 270→
271→ 275→ {diffMeta.icon} 276→ {diffMeta.label} 277→ 278→ 279→ {typeMeta.icon} {typeMeta.label} 280→ 281→ {isCompleted && ( 282→ 283→ 已完成 284→ 285→ )} 286→
287→ 288→ {/* 挑战标题 */} 289→

290→ {challenge.title} 291→

292→

293→ {challenge.desc} 294→

295→ 296→ {/* 进度条 */} 297→
298→
299→ 进度 300→ 301→ {Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit} 302→ 303→
304→ 311→
312→ 313→ {/* 奖励 + 领取按钮 */} 314→
315→
316→ 奖励: 317→ {challenge.rewardInsight > 0 && ( 318→ +{formatNum(challenge.rewardInsight)}洞见 319→ )} 320→ +{challenge.rewardContact.toFixed(1)}接触 321→
322→ 336→
337→ 338→ {/* 完成时长 */} 339→ {isCompleted && progress.durationSec > 0 && ( 340→
341→ 完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒 342→
343→ )} 344→
345→
346→ 347→ {/* 周挑战 + 信标链(桌面端并排,移动端单列) */} 348→
349→ {/* ===== 周挑战卡片(fuchsia 主题) ===== */} 350→
358→ {/* 头部:标题 + weekKey + 倒计时 */} 359→
360→
361→ 362→ 363→ 周挑战 · WEEKLY 364→ 365→
366→
367→ 368→ {weeklyChallenge.weekKey} 369→ 370→ 371→ {weekCountdown} 372→
373→
374→ 375→ {/* 难度 + 类型 标签 */} 376→
377→ 381→ {wDiffMeta.icon} 382→ {wDiffMeta.label} 383→ 384→ 385→ {wTypeMeta.icon} {wTypeMeta.label} 386→ 387→ {wCompleted && ( 388→ 389→ 已完成 390→ 391→ )} 392→
393→ 394→ {/* 标题 */} 395→

399→ {weeklyChallenge.title} 400→

401→

402→ {weeklyChallenge.desc} 403→

404→ 405→ {/* 进度条 */} 406→
407→
408→ 进度 409→ 410→ {Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit} 411→ 412→
413→ 420→
421→ 422→ {/* 奖励 + 领取按钮 */} 423→
424→
425→ 奖励: 426→ {weeklyChallenge.rewardInsight > 0 && ( 427→ 428→ +{formatNum(weeklyChallenge.rewardInsight)}洞见 429→ 430→ )} 431→ 432→ +{weeklyChallenge.rewardContact.toFixed(1)}接触 433→ 434→
435→ 457→
458→ 459→ {wCompleted && weeklyProgress.durationSec > 0 && ( 460→
461→ 完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒 462→
463→ )} 464→
465→ 466→ {/* ===== 信标链卡片(amber→rose 渐变) ===== */} 467→
474→ {/* 头部:标题 + 当前连续天数 */} 475→
476→
477→ 478→ 479→ 信标链 · CHAIN 480→ 481→
482→
483→ 484→ 连续 485→ 486→ {chainState.currentStreak} 487→ 488→ 489→
490→
491→ 492→ {/* 今日完成状态 */} 493→
494→ 495→ {completedToday 496→ ? "今日已贡献" 497→ : "今日尚未完成日挑战"} 498→ 499→ 506→ {completedToday ? "✓ 已记录" : "○ 待完成"} 507→ 508→
509→ 510→ {/* 4 个里程碑节点横向排列 */} 511→
512→ {/* 节点之间的连线(背景灰) */} 513→
514→ {/* 已达成部分高亮(基于 prev→next 插值) */} 515→ {(() => { 516→ // 节点圆心水平位置(百分比) 517→ const MILESTONE_POS: Record = { 518→ 0: 12.5, 519→ 3: 12.5, 520→ 7: 37.5, 521→ 14: 62.5, 522→ 30: 87.5, 523→ }; 524→ const prevPos = 525→ MILESTONE_POS[chainProgress.prev] ?? 12.5; 526→ const nextPos = 527→ chainProgress.next !== null 528→ ? MILESTONE_POS[chainProgress.next] ?? 87.5 529→ : 87.5; 530→ const pct = chainProgress.progressPct / 100; 531→ const activeEndPos = prevPos + (nextPos - prevPos) * pct; 532→ const widthPct = Math.max(0, activeEndPos - 12.5); 533→ if (widthPct <= 0) return null; 534→ return ( 535→
545→ ); 546→ })()} 547→ 548→ {BEACON_CHAIN_MILESTONES.map((m) => { 549→ const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m); 550→ const claimed = chainState.milestonesClaimed.includes(m); 551→ const reachable = 552→ chainState.currentStreak >= m && !claimed; 553→ const inProgress = chainState.currentStreak > 0 && nextMilestone === m; 554→ // 节点配色 555→ let nodeBg = "rgba(255,255,255,0.05)"; 556→ let nodeBorder = "rgba(255,255,255,0.15)"; 557→ let textColor = "rgba(255,255,255,0.4)"; 558→ if (claimed) { 559→ nodeBg = "rgba(52,211,153,0.25)"; 560→ nodeBorder = "#34d399"; 561→ textColor = "#34d399"; 562→ } else if (reachable) { 563→ nodeBg = "rgba(251,113,133,0.20)"; 564→ nodeBorder = "#fb7185"; 565→ textColor = "#fb7185"; 566→ } else if (inProgress) { 567→ nodeBg = "rgba(251,191,36,0.20)"; 568→ nodeBorder = "#fbbf24"; 569→ textColor = "#fbbf24"; 570→ } 571→ 572→ return ( 573→
577→
587→ {claimed ? ( 588→ 589→ ) : ( 590→ m 591→ )} 592→
593→ 597→ {reward?.label} 598→ 599→ {claimed ? ( 600→ 601→ 已领 602→ 603→ ) : reachable ? ( 604→ 616→ ) : ( 617→ 618→ +{reward?.rewardInsight}洞 619→ 620→ )} 621→
622→ ); 623→ })} 624→
625→ 626→ {/* 进度条 */} 627→
628→
629→ 630→ {chainProgress.next === null 631→ ? "已通关全部里程碑" 632→ : `下一目标:${chainProgress.next} 天`} 633→ 634→ 635→ {chainProgress.current} 636→ {chainProgress.next !== null && ` / ${chainProgress.next}`} 天 637→ 638→
639→ 646→
647→ 648→ {/* 底部统计 */} 649→
650→
651→ 最长 652→ 653→ {chainState.longestStreak}天 654→ 655→
656→
657→ 累计 658→ 659→ {chainState.totalCompletions}次 660→ 661→
662→
663→ 续命 664→ {chainState.graceUsed >= 1 ? ( 665→ 已用 666→ ) : ( 667→ 可用 668→ )} 669→
670→
671→
672→
673→ 674→ {/* 本地排行榜 */} 675→
676→
677→ 678→ 深空排行榜 679→ 本地 · Top {leaderboard.length || 0} 680→
681→ 682→ {leaderboard.length === 0 ? ( 683→
684→ 685→ 尚无记录。完成今日或本周信标即可登榜。 686→
687→ ) : ( 688→
689→ {leaderboard.map((entry, i) => { 690→ const rb = rankBadge(i); 691→ const eDiff = BEACON_DIFFICULTY[entry.difficulty]; 692→ const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType]; 693→ const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey(); 694→ return ( 695→
705→ {/* 排名 */} 706→ 707→ {rb ? ( 708→ 709→ ) : ( 710→ {i + 1} 711→ )} 712→ 713→ {/* 类型 + 难度 + 周挑战标记 */} 714→ 715→ {eDiff.icon} 716→ {eType.label} 717→ {entry.isWeekly && ( 718→ 719→ WEEK 720→ 721→ )} 722→ {entry.progress >= 1 && } 723→ 724→ {/* 用时 */} 725→ 726→ {Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s 727→ 728→ {/* 分数 */} 729→ 730→ {formatNum(entry.score)} 731→ 732→
733→ ); 734→ })} 735→
736→ )} 737→
738→ 739→ {/* 难度图例 */} 740→
741→ {DIFFICULTY_ORDER.map((d) => { 742→ const m = BEACON_DIFFICULTY[d]; 743→ return ( 744→ 745→ {m.icon} 746→ {m.label} ×{m.mult} 747→ 748→ ); 749→ })} 750→
751→
752→ ); 753→} 754→