719 lines
22 KiB
TypeScript
719 lines
22 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import type { QuestNode } from "../types/quest";
|
|
|
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
const S = `
|
|
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=Nunito:wght@800;900&display=swap');
|
|
|
|
/* ══ FULL SCREEN OVERLAY ══ */
|
|
.com-overlay {
|
|
position:fixed; inset:0; z-index:80;
|
|
display:flex; flex-direction:column;
|
|
align-items:center; justify-content:center;
|
|
overflow:hidden;
|
|
}
|
|
|
|
/* ── Sky/sea background that animates in ── */
|
|
.com-bg {
|
|
position:absolute; inset:0;
|
|
background:
|
|
radial-gradient(ellipse 80% 60% at 50% 80%, rgba(0,60,120,0.9) 0%, transparent 70%),
|
|
radial-gradient(ellipse 60% 40% at 50% 20%, rgba(80,0,160,0.7) 0%, transparent 60%),
|
|
linear-gradient(180deg, #050010 0%, #0a0520 40%, #020818 100%);
|
|
animation: comBgIn 0.5s ease both;
|
|
}
|
|
@keyframes comBgIn {
|
|
from{ opacity:0; } to{ opacity:1; }
|
|
}
|
|
|
|
/* ── Stars in background ── */
|
|
.com-star {
|
|
position:absolute; border-radius:50%;
|
|
background:white; pointer-events:none;
|
|
animation:comStarTwinkle var(--sdur) ease-in-out infinite;
|
|
animation-delay:var(--sdelay);
|
|
}
|
|
@keyframes comStarTwinkle {
|
|
0%,100%{ opacity:0.3; transform:scale(1); }
|
|
50% { opacity:1; transform:scale(1.4); }
|
|
}
|
|
|
|
/* ── Gold radial burst (appears on open) ── */
|
|
.com-burst {
|
|
position:absolute; inset:0;
|
|
display:flex; align-items:center; justify-content:center;
|
|
pointer-events:none; z-index:2;
|
|
}
|
|
.com-burst-ring {
|
|
position:absolute; border-radius:50%;
|
|
border:3px solid rgba(251,191,36,0.6);
|
|
animation: comBurstRing var(--brdur) ease-out forwards;
|
|
animation-delay: var(--brdelay);
|
|
opacity:0;
|
|
}
|
|
@keyframes comBurstRing {
|
|
0% { opacity:0.9; transform:scale(0.1); }
|
|
100%{ opacity:0; transform:scale(var(--brs)); }
|
|
}
|
|
|
|
/* ── Ray beams (crepuscular rays) ── */
|
|
.com-rays {
|
|
position:absolute; inset:0;
|
|
display:flex; align-items:center; justify-content:center;
|
|
pointer-events:none; z-index:1;
|
|
}
|
|
.com-ray {
|
|
position:absolute;
|
|
width:3px;
|
|
height:55vh;
|
|
border-radius:100px;
|
|
background:linear-gradient(180deg,rgba(251,191,36,0.5) 0%,transparent 100%);
|
|
transform-origin:50% 100%;
|
|
bottom:50%;
|
|
left:calc(50% - 1.5px);
|
|
transform:rotate(var(--angle)) scaleY(0);
|
|
animation:comRayIn 0.6s ease-out forwards;
|
|
animation-delay:var(--raydelay);
|
|
}
|
|
@keyframes comRayIn {
|
|
0% { transform:rotate(var(--angle)) scaleY(0); opacity:0; }
|
|
40% { opacity:0.8; }
|
|
100%{ transform:rotate(var(--angle)) scaleY(1); opacity:0.15; }
|
|
}
|
|
|
|
/* ── Particle explosion ── */
|
|
.com-particle {
|
|
position:absolute; border-radius:50%;
|
|
pointer-events:none; z-index:4;
|
|
animation:comParticleOut var(--pdur) cubic-bezier(0.25,0.8,0.35,1) forwards;
|
|
animation-delay:var(--pdelay);
|
|
opacity:0;
|
|
}
|
|
@keyframes comParticleOut {
|
|
0% { opacity:1; transform:translate(0,0) scale(1) rotate(0deg); }
|
|
80% { opacity:0.7; }
|
|
100%{ opacity:0; transform:translate(var(--ptx),var(--pty)) scale(0.2) rotate(var(--prot)); }
|
|
}
|
|
|
|
/* ── Coin emojis bursting ── */
|
|
.com-coin {
|
|
position:absolute;
|
|
font-size:var(--csize);
|
|
pointer-events:none; z-index:4;
|
|
animation:comCoinOut var(--cdur) cubic-bezier(0.2,0.9,0.3,1) forwards;
|
|
animation-delay:var(--cdelay);
|
|
opacity:0;
|
|
}
|
|
@keyframes comCoinOut {
|
|
0% { opacity:0; transform:translate(0,0) rotate(0deg) scale(0.3); }
|
|
12% { opacity:1; }
|
|
100%{ opacity:0; transform:translate(var(--ctx),var(--cty)) rotate(var(--crot)) scale(1.1); }
|
|
}
|
|
|
|
/* ── Floating sparkles (stay on screen) ── */
|
|
.com-sparkle {
|
|
position:absolute; pointer-events:none; z-index:3;
|
|
font-size:var(--spsize);
|
|
animation:comSparkleFloat var(--spdur) ease-in-out infinite;
|
|
animation-delay:var(--spdelay);
|
|
opacity:0.7;
|
|
}
|
|
@keyframes comSparkleFloat {
|
|
0%,100%{ transform:translateY(0) rotate(0deg) scale(1); opacity:0.6; }
|
|
50% { transform:translateY(-18px) rotate(180deg) scale(1.2); opacity:1; }
|
|
}
|
|
|
|
/* ── XP number that flies up from chest ── */
|
|
.com-xp-blast {
|
|
position:absolute; pointer-events:none; z-index:5;
|
|
top:50%; left:50%;
|
|
font-family:'Cinzel',serif;
|
|
font-size:2.6rem; font-weight:900;
|
|
color:#fbbf24;
|
|
text-shadow:0 0 30px rgba(251,191,36,1),0 0 60px rgba(251,191,36,0.7),0 0 100px rgba(251,191,36,0.3);
|
|
white-space:nowrap;
|
|
animation:comXPBlast 2s cubic-bezier(0.2,0.8,0.3,1) forwards;
|
|
}
|
|
@keyframes comXPBlast {
|
|
0% { opacity:0; transform:translate(-50%,-40%) scale(0.4); filter:blur(4px); }
|
|
15% { opacity:1; transform:translate(-50%,-60%) scale(1.3); filter:blur(0); }
|
|
60% { opacity:1; transform:translate(-50%,-90%) scale(1); }
|
|
100%{ opacity:0; transform:translate(-50%,-130%) scale(0.8); }
|
|
}
|
|
|
|
/* ── Main card ── */
|
|
.com-card {
|
|
position:relative; z-index:6;
|
|
width:calc(100% - 2.5rem); max-width:340px;
|
|
border-radius:28px; overflow:hidden;
|
|
display:flex; flex-direction:column; align-items:center;
|
|
padding:0;
|
|
box-shadow:0 0 80px rgba(251,191,36,0.2), 0 24px 64px rgba(0,0,0,0.7);
|
|
animation:comCardIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
|
animation-delay:0.1s;
|
|
}
|
|
@keyframes comCardIn {
|
|
from{ opacity:0; transform:scale(0.8) translateY(24px); }
|
|
to { opacity:1; transform:scale(1) translateY(0); }
|
|
}
|
|
|
|
/* Gold shimmer top border */
|
|
.com-card::before {
|
|
content:''; position:absolute; top:0; left:0; right:0; height:2px; z-index:1;
|
|
background:linear-gradient(90deg,transparent 0%,#f59e0b 30%,#fbbf24 50%,#f59e0b 70%,transparent 100%);
|
|
background-size:200% 100%;
|
|
animation:comShimmer 2s linear infinite;
|
|
}
|
|
@keyframes comShimmer {
|
|
0% { background-position:200% 0; }
|
|
100%{ background-position:-200% 0; }
|
|
}
|
|
|
|
/* Card inner bg */
|
|
.com-card-inner {
|
|
width:100%; padding:1.75rem 1.6rem 1.6rem;
|
|
background:linear-gradient(160deg,#12083a 0%,#0c0525 60%,#090320 100%);
|
|
border:1.5px solid rgba(251,191,36,0.25);
|
|
border-radius:28px;
|
|
display:flex; flex-direction:column; align-items:center; gap:0;
|
|
}
|
|
|
|
/* ── Phase label ── */
|
|
.com-label {
|
|
font-family:'Cinzel',serif;
|
|
font-size:0.62rem; font-weight:700; letter-spacing:0.2em;
|
|
text-transform:uppercase; color:rgba(251,191,36,0.55);
|
|
margin-bottom:1.2rem; text-align:center;
|
|
}
|
|
|
|
/* ── Chest area ── */
|
|
.com-chest-area {
|
|
position:relative; width:140px; height:140px;
|
|
display:flex; align-items:center; justify-content:center;
|
|
margin-bottom:1.25rem; cursor:pointer;
|
|
}
|
|
|
|
/* Glow platform beneath chest */
|
|
.com-glow-pad {
|
|
position:absolute; bottom:6px; left:50%;
|
|
transform:translateX(-50%);
|
|
width:100px; height:24px; border-radius:50%;
|
|
background:radial-gradient(ellipse at center,rgba(251,191,36,0.45) 0%,transparent 70%);
|
|
animation:comGlowPad 1.8s ease-in-out infinite;
|
|
filter:blur(4px);
|
|
}
|
|
@keyframes comGlowPad {
|
|
0%,100%{ transform:translateX(-50%) scaleX(1); opacity:0.7; }
|
|
50% { transform:translateX(-50%) scaleX(1.2); opacity:1; }
|
|
}
|
|
|
|
/* Orbit ring */
|
|
.com-orbit {
|
|
position:absolute; inset:8px; border-radius:50%;
|
|
border:1.5px dashed rgba(251,191,36,0.2);
|
|
animation:comOrbit 8s linear infinite;
|
|
}
|
|
@keyframes comOrbit { from{transform:rotate(0deg);} to{transform:rotate(360deg);} }
|
|
.com-orbit-dot {
|
|
position:absolute; top:-5px; left:50%; transform:translateX(-50%);
|
|
width:8px; height:8px; border-radius:50%;
|
|
background:#fbbf24; box-shadow:0 0 10px #fbbf24;
|
|
}
|
|
|
|
/* The chest emoji */
|
|
.com-chest {
|
|
font-size:5.5rem; position:relative; z-index:2;
|
|
filter:drop-shadow(0 8px 20px rgba(251,191,36,0.45));
|
|
transition:filter 0.2s;
|
|
}
|
|
.com-chest.idle {
|
|
animation:comChestIdle 3s ease-in-out infinite;
|
|
}
|
|
@keyframes comChestIdle {
|
|
0%,100%{ transform:translateY(0) rotate(-2deg); }
|
|
50% { transform:translateY(-6px) rotate(2deg); }
|
|
}
|
|
.com-chest.shake {
|
|
animation:comChestShake 0.55s cubic-bezier(0.36,0.07,0.19,0.97) both;
|
|
}
|
|
@keyframes comChestShake {
|
|
0%,100%{ transform:rotate(0deg) scale(1); }
|
|
10% { transform:rotate(-14deg) scale(1.06); }
|
|
25% { transform:rotate(14deg) scale(1.1); }
|
|
40% { transform:rotate(-10deg) scale(1.07); }
|
|
55% { transform:rotate(10deg) scale(1.12); }
|
|
70% { transform:rotate(-6deg) scale(1.06); }
|
|
85% { transform:rotate(6deg) scale(1.04); }
|
|
}
|
|
.com-chest.opening {
|
|
animation:comChestOpen 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
|
}
|
|
@keyframes comChestOpen {
|
|
0% { transform:scale(0.7); filter:brightness(0.4) drop-shadow(0 8px 20px rgba(251,191,36,0.3)); }
|
|
50% { transform:scale(1.25); filter:brightness(1.8) drop-shadow(0 0 50px rgba(251,191,36,1)); }
|
|
100%{ transform:scale(1); filter:brightness(1) drop-shadow(0 8px 30px rgba(251,191,36,0.6)); }
|
|
}
|
|
|
|
/* ── Tap prompt ── */
|
|
.com-tap-title {
|
|
font-family:'Cinzel',serif;
|
|
font-size:1.2rem; font-weight:900; color:white;
|
|
text-align:center; margin-bottom:0.3rem;
|
|
animation:comPulse 1.8s ease-in-out infinite;
|
|
}
|
|
@keyframes comPulse {
|
|
0%,100%{ opacity:1; transform:scale(1); }
|
|
50% { opacity:0.65; transform:scale(0.97); }
|
|
}
|
|
.com-tap-sub {
|
|
font-family:'Nunito',sans-serif;
|
|
font-size:0.75rem; font-weight:800;
|
|
color:rgba(255,255,255,0.35); text-align:center; margin-bottom:1.5rem;
|
|
letter-spacing:0.06em;
|
|
}
|
|
|
|
/* ── Shaking text ── */
|
|
.com-shake-text {
|
|
font-family:'Cinzel',serif;
|
|
font-size:1.1rem; font-weight:900; color:#fbbf24;
|
|
text-align:center; margin-bottom:0.3rem;
|
|
animation:comShakeText 0.3s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes comShakeText {
|
|
from{ transform:translateX(-3px); }
|
|
to { transform:translateX(3px); }
|
|
}
|
|
.com-shake-dots {
|
|
font-size:1.4rem; text-align:center; margin-bottom:1.5rem;
|
|
animation:comShakeText 0.25s ease-in-out infinite alternate;
|
|
}
|
|
|
|
/* ── Reward rows ── */
|
|
.com-rewards-title {
|
|
font-family:'Cinzel',serif;
|
|
font-size:0.65rem; font-weight:700; letter-spacing:0.18em;
|
|
text-transform:uppercase; color:rgba(251,191,36,0.5);
|
|
text-align:center; margin-bottom:0.85rem;
|
|
}
|
|
.com-rewards { display:flex; flex-direction:column; gap:0.55rem; width:100%; margin-bottom:1.1rem; }
|
|
.com-reward-row {
|
|
display:flex; align-items:center; gap:0.85rem;
|
|
padding:0.8rem 1rem;
|
|
background:rgba(255,255,255,0.04);
|
|
border:1px solid rgba(251,191,36,0.18);
|
|
border-radius:16px;
|
|
animation:comRowIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
|
}
|
|
@keyframes comRowIn {
|
|
from{ opacity:0; transform:translateY(18px) scale(0.88); }
|
|
to { opacity:1; transform:translateY(0) scale(1); }
|
|
}
|
|
.com-reward-icon { font-size:1.5rem; flex-shrink:0; filter:drop-shadow(0 2px 8px rgba(251,191,36,0.5)); }
|
|
.com-reward-lbl {
|
|
font-family:'Cinzel',serif;
|
|
font-size:0.65rem; font-weight:700; letter-spacing:0.1em; text-transform:uppercase;
|
|
color:rgba(255,255,255,0.4); margin-bottom:0.12rem;
|
|
}
|
|
.com-reward-val {
|
|
font-family:'Nunito',sans-serif;
|
|
font-size:1.05rem; font-weight:900;
|
|
color:#fbbf24;
|
|
text-shadow:0 0 16px rgba(251,191,36,0.6);
|
|
}
|
|
|
|
/* Special XP row highlight */
|
|
.com-reward-row.xp-row {
|
|
border-color:rgba(251,191,36,0.35);
|
|
background:rgba(251,191,36,0.06);
|
|
}
|
|
|
|
/* ── CTA button ── */
|
|
.com-cta {
|
|
width:100%; padding:1rem;
|
|
background:linear-gradient(135deg,#fbbf24,#f59e0b);
|
|
border:none; border-radius:16px; cursor:pointer;
|
|
font-family:'Cinzel',serif;
|
|
font-size:1rem; font-weight:900; color:#1a0800;
|
|
letter-spacing:0.05em;
|
|
box-shadow:0 5px 0 #b45309, 0 8px 24px rgba(251,191,36,0.4);
|
|
transition:all 0.12s ease;
|
|
animation:comRowIn 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
|
}
|
|
.com-cta:hover { transform:translateY(-3px); box-shadow:0 8px 0 #b45309, 0 14px 32px rgba(251,191,36,0.5); }
|
|
.com-cta:active { transform:translateY(2px); box-shadow:0 3px 0 #b45309; }
|
|
|
|
/* ── Skip hint ── */
|
|
.com-skip {
|
|
position:absolute; bottom:1.5rem;
|
|
font-family:'Nunito',sans-serif;
|
|
font-size:0.65rem; font-weight:700;
|
|
color:rgba(255,255,255,0.2); letter-spacing:0.1em;
|
|
text-transform:uppercase; cursor:pointer; z-index:7;
|
|
transition:color 0.2s;
|
|
}
|
|
.com-skip:hover { color:rgba(255,255,255,0.5); }
|
|
`;
|
|
|
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
const PARTICLE_COLORS = [
|
|
"#fbbf24",
|
|
"#f59e0b",
|
|
"#ef4444",
|
|
"#ec4899",
|
|
"#a855f7",
|
|
"#6366f1",
|
|
"#22d3ee",
|
|
"#4ade80",
|
|
"#fb923c",
|
|
];
|
|
const COIN_EMOJIS = ["🪙", "💰", "✨", "⭐", "💎", "🌟", "💫", "🏅"];
|
|
const SPARKLE_EMOJIS = ["✨", "⭐", "💫", "🌟"];
|
|
|
|
// Rays at evenly spaced angles
|
|
const RAYS = Array.from({ length: 12 }, (_, i) => ({
|
|
id: i,
|
|
angle: `${(i / 12) * 360}deg`,
|
|
delay: `${i * 0.04}s`,
|
|
}));
|
|
|
|
// Burst rings
|
|
const BURST_RINGS = [
|
|
{ id: 0, size: "3", dur: "0.7s", delay: "0s" },
|
|
{ id: 1, size: "5", dur: "0.9s", delay: "0.1s" },
|
|
{ id: 2, size: "8", dur: "1.1s", delay: "0.2s" },
|
|
{ id: 3, size: "12", dur: "1.4s", delay: "0.3s" },
|
|
];
|
|
|
|
// Stars in background — stable between renders
|
|
const STARS = Array.from({ length: 40 }, (_, i) => ({
|
|
id: i,
|
|
w: 1 + ((i * 7) % 3),
|
|
top: `${(i * 17 + 3) % 95}%`,
|
|
left: `${(i * 23 + 11) % 97}%`,
|
|
dur: `${2 + ((i * 3) % 4)}s`,
|
|
delay: `${(i * 7) % 3}s`,
|
|
}));
|
|
|
|
// Sparkles floating around the revealed card
|
|
const SPARKLES = Array.from({ length: 8 }, (_, i) => ({
|
|
id: i,
|
|
emoji: SPARKLE_EMOJIS[i % 4],
|
|
size: `${0.9 + (i % 3) * 0.35}rem`,
|
|
top: `${10 + ((i * 12) % 75)}%`,
|
|
left: `${5 + ((i * 14) % 85)}%`,
|
|
dur: `${2 + (i % 3) * 1.2}s`,
|
|
delay: `${i * 0.3}s`,
|
|
}));
|
|
|
|
type Phase = "idle" | "shaking" | "opening" | "revealed";
|
|
|
|
interface Props {
|
|
node: QuestNode;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const ChestOpenModal = ({ node, onClose }: Props) => {
|
|
const [phase, setPhase] = useState<Phase>("idle");
|
|
const [showXP, setShowXP] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
// Stable particle arrays computed once per mount
|
|
const particles = useRef(
|
|
Array.from({ length: 55 }, (_, i) => ({
|
|
id: i,
|
|
color: PARTICLE_COLORS[i % PARTICLE_COLORS.length],
|
|
w: 3 + (i % 3) * 4,
|
|
tx: ((i % 2 === 0 ? 1 : -1) * (40 + i * 7)) % 200,
|
|
ty: -(30 + ((i * 11) % 190)),
|
|
rot: ((i * 23) % 720) - 360,
|
|
dur: `${0.7 + ((i * 7) % 10) / 10}s`,
|
|
delay: `${((i * 3) % 8) / 30}s`,
|
|
})),
|
|
).current;
|
|
|
|
const coins = useRef(
|
|
Array.from({ length: 18 }, (_, i) => ({
|
|
id: i,
|
|
emoji: COIN_EMOJIS[i % COIN_EMOJIS.length],
|
|
size: `${1 + (i % 3) * 0.45}rem`,
|
|
tx: (i % 2 === 0 ? 1 : -1) * (30 + ((i * 9) % 180)),
|
|
ty: -(40 + ((i * 13) % 200)),
|
|
rot: ((i * 31) % 540) - 270,
|
|
dur: `${0.75 + ((i * 7) % 8) / 10}s`,
|
|
delay: `${((i * 5) % 10) / 30}s`,
|
|
})),
|
|
).current;
|
|
|
|
const tap = () => {
|
|
if (phase !== "idle") return;
|
|
setPhase("shaking");
|
|
timerRef.current = setTimeout(() => {
|
|
setPhase("opening");
|
|
setShowXP(true);
|
|
timerRef.current = setTimeout(() => {
|
|
setShowXP(false);
|
|
setPhase("revealed");
|
|
}, 1800);
|
|
}, 650);
|
|
};
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const rewards = [
|
|
{
|
|
key: "xp",
|
|
cls: "xp-row",
|
|
icon: "⚡",
|
|
lbl: "XP Gained",
|
|
val: `+${node.reward.xp} XP`,
|
|
delay: "0.05s",
|
|
},
|
|
...(node.reward.title
|
|
? [
|
|
{
|
|
key: "title",
|
|
cls: "",
|
|
icon: "🏴☠️",
|
|
lbl: "Crew Title",
|
|
val: node.reward.title,
|
|
delay: "0.15s",
|
|
},
|
|
]
|
|
: []),
|
|
...(node.reward.itemLabel
|
|
? [
|
|
{
|
|
key: "item",
|
|
cls: "",
|
|
icon: "🎁",
|
|
lbl: "Item",
|
|
val: node.reward.itemLabel,
|
|
delay: "0.25s",
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
|
|
const chestClass =
|
|
phase === "idle"
|
|
? "idle"
|
|
: phase === "shaking"
|
|
? "shake"
|
|
: phase === "opening"
|
|
? "opening"
|
|
: "";
|
|
const chestEmoji = phase === "opening" || phase === "revealed" ? "📬" : "📦";
|
|
|
|
return (
|
|
<div className="com-overlay" onClick={phase === "idle" ? tap : undefined}>
|
|
<style>{S}</style>
|
|
|
|
{/* Background */}
|
|
<div className="com-bg" />
|
|
|
|
{/* Stars */}
|
|
{STARS.map((s) => (
|
|
<div
|
|
key={s.id}
|
|
className="com-star"
|
|
style={
|
|
{
|
|
width: s.w,
|
|
height: s.w,
|
|
top: s.top,
|
|
left: s.left,
|
|
"--sdur": s.dur,
|
|
"--sdelay": s.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
))}
|
|
|
|
{/* Crepuscular rays (appear on open) */}
|
|
{(phase === "opening" || phase === "revealed") && (
|
|
<div className="com-rays">
|
|
{RAYS.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="com-ray"
|
|
style={
|
|
{
|
|
"--angle": r.angle,
|
|
"--raydelay": r.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Burst rings */}
|
|
{(phase === "opening" || phase === "revealed") && (
|
|
<div className="com-burst">
|
|
{BURST_RINGS.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="com-burst-ring"
|
|
style={
|
|
{
|
|
width: "100px",
|
|
height: "100px",
|
|
"--brs": r.size,
|
|
"--brdur": r.dur,
|
|
"--brdelay": r.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Particle explosion */}
|
|
{(phase === "opening" || phase === "revealed") && (
|
|
<>
|
|
{particles.map((p) => (
|
|
<div
|
|
key={p.id}
|
|
className="com-particle"
|
|
style={
|
|
{
|
|
width: p.w,
|
|
height: p.w,
|
|
background: p.color,
|
|
top: "50%",
|
|
left: "50%",
|
|
"--ptx": `${p.tx}px`,
|
|
"--pty": `${p.ty}px`,
|
|
"--prot": `${p.rot}deg`,
|
|
"--pdur": p.dur,
|
|
"--pdelay": p.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
))}
|
|
{coins.map((c) => (
|
|
<div
|
|
key={c.id}
|
|
className="com-coin"
|
|
style={
|
|
{
|
|
top: "50%",
|
|
left: "50%",
|
|
"--csize": c.size,
|
|
"--ctx": `${c.tx}px`,
|
|
"--cty": `${c.ty}px`,
|
|
"--crot": `${c.rot}deg`,
|
|
"--cdur": c.dur,
|
|
"--cdelay": c.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
{c.emoji}
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Floating sparkles in revealed state */}
|
|
{phase === "revealed" &&
|
|
SPARKLES.map((sp) => (
|
|
<div
|
|
key={sp.id}
|
|
className="com-sparkle"
|
|
style={
|
|
{
|
|
top: sp.top,
|
|
left: sp.left,
|
|
"--spsize": sp.size,
|
|
"--spdur": sp.dur,
|
|
"--spdelay": sp.delay,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
{sp.emoji}
|
|
</div>
|
|
))}
|
|
|
|
{/* XP blast */}
|
|
{showXP && <div className="com-xp-blast">+{node.reward.xp} XP</div>}
|
|
|
|
{/* Card */}
|
|
<div className="com-card" onClick={(e) => e.stopPropagation()}>
|
|
<div className="com-card-inner">
|
|
<p className="com-label">
|
|
{phase === "revealed" ? "⚓ Treasure Claimed" : "📦 Treasure Chest"}
|
|
</p>
|
|
|
|
{/* Chest */}
|
|
<div
|
|
className="com-chest-area"
|
|
onClick={phase === "idle" ? tap : undefined}
|
|
style={{ cursor: phase === "idle" ? "pointer" : "default" }}
|
|
>
|
|
{phase !== "revealed" && <div className="com-glow-pad" />}
|
|
{phase !== "revealed" && (
|
|
<div className="com-orbit">
|
|
<div className="com-orbit-dot" />
|
|
</div>
|
|
)}
|
|
<span className={`com-chest ${chestClass}`}>{chestEmoji}</span>
|
|
</div>
|
|
|
|
{/* Phase content */}
|
|
{phase === "idle" && (
|
|
<>
|
|
<p className="com-tap-title">Tap to Open!</p>
|
|
<p className="com-tap-sub">YOUR HARD WORK HAS PAID OFF, PIRATE</p>
|
|
</>
|
|
)}
|
|
{phase === "shaking" && (
|
|
<>
|
|
<p className="com-shake-text">The chest stirs...</p>
|
|
<p className="com-shake-dots">⚡ ⚡ ⚡</p>
|
|
</>
|
|
)}
|
|
{phase === "revealed" && (
|
|
<>
|
|
<p className="com-rewards-title">⚓ Spoils of Victory</p>
|
|
<div className="com-rewards">
|
|
{rewards.map((r) => (
|
|
<div
|
|
key={r.key}
|
|
className={`com-reward-row ${r.cls}`}
|
|
style={{ animationDelay: r.delay }}
|
|
>
|
|
<span className="com-reward-icon">{r.icon}</span>
|
|
<div>
|
|
<p className="com-reward-lbl">{r.lbl}</p>
|
|
<p className="com-reward-val">{r.val}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<button
|
|
className="com-cta"
|
|
style={{ animationDelay: rewards.length * 0.1 + "s" }}
|
|
onClick={onClose}
|
|
>
|
|
⚓ Set Sail
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Skip link for impatient pirates */}
|
|
{phase === "revealed" && (
|
|
<p className="com-skip" onClick={onClose}>
|
|
tap anywhere to continue
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|