"use client"; // 回响星核 / Echo Nexus — 星图天文台面板(飞升元进程可视化) // 6 个星座(六边形布局)× 3 颗星 = 18 个天赋节点,已解锁的星点亮 + 连线 import { useEffect, useRef, useState, useMemo } from "react"; import { useGameStore } from "@/store/gameStore"; import { CONSTELLATION_PERKS, CONSTELLATION_CATEGORY_META, getPerk, constellationBonuses, getConstellationLayout, getStarsInCategory, countUnlockedInCategory, constellationProgress, type ConstellationCategory, } from "@/lib/game/constellation"; import type { ConstellationPerk } from "@/lib/game/constellation"; interface StarPoint { perk: ConstellationPerk; x: number; y: number; } interface HoverState { perk: ConstellationPerk; x: number; y: number; unlocked: boolean; } export function ConstellationPanel() { const constellation = useGameStore((s) => s.constellation ?? []); const canvasRef = useRef(null); const containerRef = useRef(null); const [size, setSize] = useState({ w: 360, h: 360 }); const [hover, setHover] = useState(null); const ownedSet = useMemo(() => new Set(constellation), [constellation]); // 自适应尺寸 useEffect(() => { const el = containerRef.current; if (!el) return; const ro = new ResizeObserver(() => { const r = el.getBoundingClientRect(); setSize({ w: Math.max(280, r.width), h: Math.max(280, Math.min(420, r.width)) }); }); ro.observe(el); return () => ro.disconnect(); }, []); // 计算布局(中心点 + 半径) const layout = useMemo(() => { const cx = size.w / 2; const cy = size.h / 2; const radius = Math.min(size.w, size.h) * 0.32; return { cx, cy, radius }; }, [size]); // 收集所有星点 + 每个星座的星点列表 const clusters = useMemo(() => { const centers = getConstellationLayout(layout.cx, layout.cy, layout.radius); return centers.map((c) => { const stars = getStarsInCategory(c.category, c.cx, c.cy, 1) as StarPoint[]; return { ...c, stars }; }); }, [layout]); // 绘制 useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const dpr = window.devicePixelRatio || 1; canvas.width = size.w * dpr; canvas.height = size.h * dpr; canvas.style.width = `${size.w}px`; canvas.style.height = `${size.h}px`; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); let rafId = 0; const start = performance.now(); const draw = (now: number) => { const t = (now - start) / 1000; ctx.clearRect(0, 0, size.w, size.h); // 背景径向暗光 const bg = ctx.createRadialGradient(layout.cx, layout.cy, 0, layout.cx, layout.cy, layout.radius * 1.6); bg.addColorStop(0, "rgba(40,30,80,0.18)"); bg.addColorStop(1, "rgba(5,4,16,0)"); ctx.fillStyle = bg; ctx.fillRect(0, 0, size.w, size.h); // 中心装饰:星核 ctx.save(); ctx.translate(layout.cx, layout.cy); const coreR = 6 + Math.sin(t * 1.5) * 1.5; const coreGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, coreR * 3); coreGrad.addColorStop(0, "rgba(232,121,249,0.9)"); coreGrad.addColorStop(0.4, "rgba(232,121,249,0.3)"); coreGrad.addColorStop(1, "rgba(232,121,249,0)"); ctx.fillStyle = coreGrad; ctx.beginPath(); ctx.arc(0, 0, coreR * 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#fce7ff"; ctx.beginPath(); ctx.arc(0, 0, coreR * 0.6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // 连接星座到中心的虚线(已解锁 >=1 时) for (const c of clusters) { const cnt = countUnlockedInCategory(constellation, c.category); if (cnt > 0) { ctx.save(); ctx.strokeStyle = c.meta.glow; ctx.lineWidth = 1; ctx.setLineDash([2, 4]); ctx.globalAlpha = 0.4 + Math.sin(t * 2 + c.cx) * 0.1; ctx.beginPath(); ctx.moveTo(layout.cx, layout.cy); ctx.lineTo(c.cx, c.cy); ctx.stroke(); ctx.restore(); } } // 绘制每个星座 for (const c of clusters) { const cnt = countUnlockedInCategory(constellation, c.category); // 星座中心标签 ctx.save(); ctx.font = "10px ui-monospace, monospace"; ctx.textAlign = "center"; ctx.fillStyle = cnt > 0 ? c.meta.hex : "rgba(148,163,184,0.5)"; ctx.globalAlpha = 0.85; ctx.fillText(c.meta.name, c.cx, c.cy + 38); ctx.restore(); // 同星座内已解锁星点之间的连线 const unlockedStars = c.stars.filter((s) => ownedSet.has(s.perk.id)); if (unlockedStars.length >= 2) { ctx.save(); ctx.strokeStyle = c.meta.glow; ctx.lineWidth = 1.5; ctx.globalAlpha = 0.6; ctx.beginPath(); unlockedStars.forEach((s, i) => { if (i === 0) ctx.moveTo(s.x, s.y); else ctx.lineTo(s.x, s.y); }); ctx.stroke(); ctx.restore(); } // 绘制星点 for (const star of c.stars) { const unlocked = ownedSet.has(star.perk.id); ctx.save(); ctx.translate(star.x, star.y); // 闪烁系数 const twinkle = unlocked ? 0.85 + Math.sin(t * 3 + star.x * 0.1) * 0.15 : 0.3; // 外光晕 const haloR = unlocked ? 10 : 5; const haloGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, haloR); haloGrad.addColorStop(0, unlocked ? c.meta.glow : "rgba(100,116,139,0.2)"); haloGrad.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = haloGrad; ctx.globalAlpha = twinkle; ctx.beginPath(); ctx.arc(0, 0, haloR, 0, Math.PI * 2); ctx.fill(); // 核心 ctx.globalAlpha = unlocked ? 1 : 0.4; ctx.fillStyle = unlocked ? c.meta.hex : "#475569"; ctx.beginPath(); ctx.arc(0, 0, unlocked ? 3.5 : 2, 0, Math.PI * 2); ctx.fill(); // 解锁星点:4 道十字光线 if (unlocked) { ctx.strokeStyle = c.meta.hex; ctx.globalAlpha = 0.7 * twinkle; ctx.lineWidth = 0.8; const ray = 7 + Math.sin(t * 4 + star.y * 0.1) * 1.5; ctx.beginPath(); ctx.moveTo(-ray, 0); ctx.lineTo(ray, 0); ctx.moveTo(0, -ray); ctx.lineTo(0, ray); ctx.stroke(); } ctx.restore(); } } rafId = requestAnimationFrame(draw); }; rafId = requestAnimationFrame(draw); return () => cancelAnimationFrame(rafId); }, [clusters, layout, size, constellation, ownedSet]); // 鼠标交互:检测 hover const handleMove = (e: React.MouseEvent) => { const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; let best: HoverState | null = null; let bestDist = 14; for (const c of clusters) { for (const star of c.stars) { const d = Math.hypot(star.x - mx, star.y - my); if (d < bestDist) { bestDist = d; best = { perk: star.perk, x: star.x, y: star.y, unlocked: ownedSet.has(star.perk.id), }; } } } setHover(best); }; const handleLeave = () => setHover(null); const progress = constellationProgress(constellation); return (
{/* 顶部进度 */}
星图天赋 {progress.unlocked}/{progress.total}
每次飞升可解锁 1 个
{/* Canvas 星图 */}
{/* Hover tooltip */} {hover && (
{hover.unlocked ? "★ " : "☆ "} {hover.perk.name}
{hover.perk.desc}
{CONSTELLATION_CATEGORY_META[hover.perk.category].name} · 序{hover.perk.order}
)}
{/* 类别图例 + 当前总修饰 */}
{(Object.keys(CONSTELLATION_CATEGORY_META) as ConstellationCategory[]).map((cat) => { const meta = CONSTELLATION_CATEGORY_META[cat]; const cnt = countUnlockedInCategory(constellation, cat); const total = CONSTELLATION_PERKS.filter((p) => p.category === cat).length; return (
0 ? { borderColor: `${meta.hex}40` } : undefined} > 0 ? `0 0 4px ${meta.hex}` : "none" }} /> {meta.name} 0 ? meta.hex : "rgba(148,163,184,0.5)" }}> {cnt}/{total}
); })}
{/* 已解锁天赋列表 */}
{constellation.length === 0 ? (
尚未解锁任何天赋。
累积接触进度至 100%,发起「飞升」即可在 3 个随机天赋中选 1。
) : (
{constellation.map((id) => { const perk = getPerk(id); if (!perk) return null; const meta = CONSTELLATION_CATEGORY_META[perk.category]; return (
{perk.name}
{perk.desc}
{meta.name}
); })}
)}
); }