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("idle"); const [showXP, setShowXP] = useState(false); const timerRef = useRef | 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 (
{/* Background */}
{/* Stars */} {STARS.map((s) => (
))} {/* Crepuscular rays (appear on open) */} {(phase === "opening" || phase === "revealed") && (
{RAYS.map((r) => (
))}
)} {/* Burst rings */} {(phase === "opening" || phase === "revealed") && (
{BURST_RINGS.map((r) => (
))}
)} {/* Particle explosion */} {(phase === "opening" || phase === "revealed") && ( <> {particles.map((p) => (
))} {coins.map((c) => (
{c.emoji}
))} )} {/* Floating sparkles in revealed state */} {phase === "revealed" && SPARKLES.map((sp) => (
{sp.emoji}
))} {/* XP blast */} {showXP &&
+{node.reward.xp} XP
} {/* Card */}
e.stopPropagation()}>

{phase === "revealed" ? "⚓ Treasure Claimed" : "📦 Treasure Chest"}

{/* Chest */}
{phase !== "revealed" &&
} {phase !== "revealed" && (
)} {chestEmoji}
{/* Phase content */} {phase === "idle" && ( <>

Tap to Open!

YOUR HARD WORK HAS PAID OFF, PIRATE

)} {phase === "shaking" && ( <>

The chest stirs...

⚡ ⚡ ⚡

)} {phase === "revealed" && ( <>

⚓ Spoils of Victory

{rewards.map((r) => (
{r.icon}

{r.lbl}

{r.val}

))}
)}
{/* Skip link for impatient pirates */} {phase === "revealed" && (

tap anywhere to continue

)}
); };