feat(v0.3.1): 星图天文台元进程天赋系统 — 18 天赋 / 3 选 1 draft / Canvas 动态星图
- 新增 src/lib/game/constellation.ts:6 类别 × 3 天赋 = 18 个永久天赋, constellationBonuses() 聚合 19 项修饰器,rollPerkChoices() 保证不同类别 - 状态扩展:GameState.constellation / pendingPerkChoices - engine.ts: recomputeStats 聚合三层加成(技术+蓝图+成就+星图), performPrestige 触发天赋选择 + 飞升礼包补偿,新增 rollCrystalTierWithBonus - expedition.ts: 探险力/生命应用星座修饰器 - gameStore.ts: 新增 chooseConstellationPerk + rerollPerkChoices 动作, 全链路接入(tickTide/autoDecodeTick/clickNode 等用点) - 新增 ConstellationPanel.tsx: Canvas 动态星图(六边形 6 星座 × 3 星点, 闪烁/光晕/十字光线/连接线/中心星核呼吸)+ Hover 提示 + 类别图例 - 新增 ConstellationDialog.tsx: 飞升后自动弹出,3 卡片 draft + 重新抽取 - PrestigeDialog: 增加「星图觉醒预告」卡片 - page.tsx: 6 列标签栏 + 顶部「觉醒」按钮 + 统计面板加星图天赋 - audio.ts: 新增 constellation SFX(上升琶音 + 高频闪光) - achievements.ts: 新增「星图初绘」「六分星辉」2 项成就 - QA: agent-browser + VLM 全流程通过;lint 零错误;编译 < 200ms - 二次回应 Issue #1:从「连连看」扩展为 6 大玩法层 + 元进程 draft
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 星图天赋选择对话框(飞升后弹出,3 选 1)
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import {
|
||||
getPerk,
|
||||
CONSTELLATION_CATEGORY_META,
|
||||
constellationProgress,
|
||||
} from "@/lib/game/constellation";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
import { Sparkles, RefreshCw } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function ConstellationDialog({ open, onOpenChange }: Props) {
|
||||
const pendingChoices = useGameStore((s) => s.pendingPerkChoices);
|
||||
const constellation = useGameStore((s) => s.constellation ?? []);
|
||||
const choosePerk = useGameStore((s) => s.chooseConstellationPerk);
|
||||
const reroll = useGameStore((s) => s.rerollPerkChoices);
|
||||
const [picked, setPicked] = useState<string | null>(null);
|
||||
|
||||
// 关闭时清理本地状态
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (!open) setPicked(null);
|
||||
}, [open]);
|
||||
|
||||
// 自动 open:当 pendingPerkChoices 存在且当前未 open 时
|
||||
useEffect(() => {
|
||||
if (pendingChoices && pendingChoices.length > 0 && !open) {
|
||||
onOpenChange(true);
|
||||
}
|
||||
if ((!pendingChoices || pendingChoices.length === 0) && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
}, [pendingChoices, open, onOpenChange]);
|
||||
|
||||
const handlePick = (perkId: string) => {
|
||||
setPicked(perkId);
|
||||
const ok = choosePerk(perkId);
|
||||
if (ok) {
|
||||
sfx("constellation");
|
||||
// 延迟关闭,让选中动画播放
|
||||
setTimeout(() => onOpenChange(false), 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReroll = () => {
|
||||
reroll();
|
||||
sfx("uiClick");
|
||||
};
|
||||
|
||||
if (!pendingChoices || pendingChoices.length === 0) return null;
|
||||
|
||||
const progress = constellationProgress(constellation);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md border-fuchsia-400/30 bg-gradient-to-br from-[#0a0820]/95 to-[#1a0f30]/95 backdrop-blur-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-fuchsia-200">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
星图觉醒
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-xs">
|
||||
飞升带来的回响在星图上凝聚。从 3 道星辉中选择 1 道,永久获得其加成。
|
||||
<br />
|
||||
<span className="text-fuchsia-300/80">当前已觉醒 {progress.unlocked}/{progress.total}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 mt-1">
|
||||
{pendingChoices.map((perkId) => {
|
||||
const perk = getPerk(perkId);
|
||||
if (!perk) return null;
|
||||
const meta = CONSTELLATION_CATEGORY_META[perk.category];
|
||||
const isPicked = picked === perkId;
|
||||
return (
|
||||
<button
|
||||
key={perkId}
|
||||
onClick={() => handlePick(perkId)}
|
||||
disabled={!!picked}
|
||||
className={`group relative w-full text-left rounded-lg border px-3 py-2.5 transition-all overflow-hidden ${
|
||||
isPicked
|
||||
? "border-fuchsia-400/80 bg-fuchsia-500/10 scale-[1.02]"
|
||||
: picked
|
||||
? "border-white/5 opacity-40"
|
||||
: "border-white/10 hover:border-white/30 hover:bg-white/5"
|
||||
}`}
|
||||
style={{
|
||||
boxShadow: isPicked ? `0 0 20px ${meta.glow}` : undefined,
|
||||
}}
|
||||
>
|
||||
{/* 类别色条 */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1"
|
||||
style={{ background: meta.hex, boxShadow: `0 0 8px ${meta.hex}` }}
|
||||
/>
|
||||
<div className="pl-2 flex items-start gap-3">
|
||||
{/* 类别图标 */}
|
||||
<div
|
||||
className="flex-shrink-0 mt-0.5 h-8 w-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background: `${meta.hex}20`,
|
||||
color: meta.hex,
|
||||
border: `1px solid ${meta.hex}40`,
|
||||
}}
|
||||
>
|
||||
{meta.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className="text-sm font-bold text-foreground">{perk.name}</span>
|
||||
<span
|
||||
className="text-[9px] px-1 py-0.5 rounded font-mono"
|
||||
style={{
|
||||
background: `${meta.hex}20`,
|
||||
color: meta.hex,
|
||||
}}
|
||||
>
|
||||
{meta.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">{perk.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 选中粒子动画 */}
|
||||
{isPicked && (
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="absolute h-1 w-1 rounded-full animate-ping"
|
||||
style={{
|
||||
background: meta.hex,
|
||||
left: `${20 + i * 8}%`,
|
||||
top: `${30 + (i % 3) * 20}%`,
|
||||
animationDelay: `${i * 50}ms`,
|
||||
animationDuration: "800ms",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-3 pt-2 border-t border-white/5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReroll}
|
||||
disabled={!!picked}
|
||||
className="text-muted-foreground hover:text-foreground h-7 text-[11px]"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
重新抽取
|
||||
</Button>
|
||||
<span className="text-[10px] text-muted-foreground/60">
|
||||
✦ 选择后永久保留,跨周目生效
|
||||
</span>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
"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<HTMLCanvasElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [size, setSize] = useState({ w: 360, h: 360 });
|
||||
const [hover, setHover] = useState<HoverState | null>(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<HTMLCanvasElement>) => {
|
||||
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 (
|
||||
<div ref={containerRef} className="flex flex-col gap-2 min-h-0 h-full">
|
||||
{/* 顶部进度 */}
|
||||
<div className="flex items-center justify-between text-[11px] px-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-fuchsia-300">✦</span>
|
||||
<span className="text-muted-foreground">星图天赋</span>
|
||||
<span className="font-mono text-foreground">
|
||||
{progress.unlocked}/{progress.total}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground/60 text-[10px]">每次飞升可解锁 1 个</span>
|
||||
</div>
|
||||
|
||||
{/* Canvas 星图 */}
|
||||
<div className="relative rounded-xl bg-black/30 border border-white/5 overflow-hidden flex-shrink-0">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onMouseMove={handleMove}
|
||||
onMouseLeave={handleLeave}
|
||||
className="block cursor-pointer"
|
||||
/>
|
||||
{/* Hover tooltip */}
|
||||
{hover && (
|
||||
<div
|
||||
className="pointer-events-none absolute z-10 max-w-[200px] rounded-md bg-black/85 border border-white/10 px-2 py-1.5 text-[10px] leading-tight shadow-lg backdrop-blur-sm"
|
||||
style={{
|
||||
left: Math.min(hover.x + 12, size.w - 200),
|
||||
top: Math.max(0, hover.y - 40),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="font-bold mb-0.5"
|
||||
style={{ color: CONSTELLATION_CATEGORY_META[hover.perk.category].hex }}
|
||||
>
|
||||
{hover.unlocked ? "★ " : "☆ "}
|
||||
{hover.perk.name}
|
||||
</div>
|
||||
<div className="text-muted-foreground">{hover.perk.desc}</div>
|
||||
<div className="text-muted-foreground/60 mt-0.5">
|
||||
{CONSTELLATION_CATEGORY_META[hover.perk.category].name} · 序{hover.perk.order}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 类别图例 + 当前总修饰 */}
|
||||
<div className="grid grid-cols-3 gap-1 text-[10px]">
|
||||
{(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 (
|
||||
<div
|
||||
key={cat}
|
||||
className="rounded-md bg-black/25 border border-white/5 px-1.5 py-1 flex items-center gap-1"
|
||||
style={cnt > 0 ? { borderColor: `${meta.hex}40` } : undefined}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-1.5 w-1.5 rounded-full flex-shrink-0"
|
||||
style={{ background: meta.hex, boxShadow: cnt > 0 ? `0 0 4px ${meta.hex}` : "none" }}
|
||||
/>
|
||||
<span className="text-muted-foreground truncate">{meta.name}</span>
|
||||
<span className="ml-auto font-mono" style={{ color: cnt > 0 ? meta.hex : "rgba(148,163,184,0.5)" }}>
|
||||
{cnt}/{total}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 已解锁天赋列表 */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1 echo-scrollbar">
|
||||
{constellation.length === 0 ? (
|
||||
<div className="rounded-md bg-black/25 border border-dashed border-white/10 px-3 py-3 text-center text-[11px] text-muted-foreground/70">
|
||||
<div className="mb-1 text-fuchsia-300/80">✦</div>
|
||||
尚未解锁任何天赋。<br />
|
||||
累积接触进度至 100%,发起「飞升」即可在 3 个随机天赋中选 1。
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{constellation.map((id) => {
|
||||
const perk = getPerk(id);
|
||||
if (!perk) return null;
|
||||
const meta = CONSTELLATION_CATEGORY_META[perk.category];
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="rounded-md bg-black/25 border px-2 py-1.5 flex items-center gap-2 text-[11px]"
|
||||
style={{ borderColor: `${meta.hex}30` }}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full flex-shrink-0"
|
||||
style={{ background: meta.hex, boxShadow: `0 0 6px ${meta.hex}` }}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-foreground truncate">{perk.name}</div>
|
||||
<div className="text-muted-foreground/70 text-[10px] truncate">{perk.desc}</div>
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground/50 font-mono">{meta.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RotateCcw, Sparkles } from "lucide-react";
|
||||
import { RotateCcw, Sparkles, Star } from "lucide-react";
|
||||
import { PRESTIGE } from "@/lib/game/config";
|
||||
import { computeNewBlueprints, computePrestigeBonus } from "@/lib/game/engine";
|
||||
import { constellationProgress } from "@/lib/game/constellation";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
@@ -32,6 +33,7 @@ export function PrestigeDialog({
|
||||
const newBp = computeNewBlueprints(state);
|
||||
const bonus = computePrestigeBonus(state);
|
||||
const totalBpAfter = Math.min(PRESTIGE.maxBlueprints, state.blueprints.length + newBp);
|
||||
const cProgress = constellationProgress(state.constellation ?? []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -83,13 +85,32 @@ export function PrestigeDialog({
|
||||
<div className="text-[11px] text-muted-foreground space-y-1 pt-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Sparkles className="h-3 w-3 text-fuchsia-400" />
|
||||
保留:记忆图谱、累计解码数、飞升次数、蓝图
|
||||
保留:记忆图谱、累计解码数、飞升次数、蓝图、星图天赋
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RotateCcw className="h-3 w-3 text-rose-400" />
|
||||
重置:晶体、洞见、技术树、接触进度、待解码晶体
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 星图觉醒预告 */}
|
||||
<div className="rounded-lg border border-fuchsia-400/30 bg-gradient-to-r from-fuchsia-950/40 to-purple-950/40 p-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 text-fuchsia-300" />
|
||||
<div className="flex-1">
|
||||
<div className="text-[12px] text-fuchsia-200 font-bold">星图觉醒预告</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
飞升后将从 3 道随机天赋中选 1,永久获得加成
|
||||
{cProgress.unlocked < cProgress.total && (
|
||||
<span className="text-fuchsia-300/80"> · 当前 {cProgress.unlocked}/{cProgress.total}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-fuchsia-300/70 font-mono">
|
||||
{cProgress.unlocked < cProgress.total ? "✦ 可觉醒" : "✧ 已圆满"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user