import { useEffect, useState } from "react"; import { X, Lock } from "lucide-react"; import type { QuestNode } from "../types/quest"; // ─── Styles ─────────────────────────────────────────────────────────────────── const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700;900&family=Sorts+Mill+Goudy:ital@0;1&family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); /* ══ OVERLAY ══ */ .qnm-overlay { position: fixed; inset: 0; z-index: 40; background: rgba(4,8,20,0.72); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: flex-end; justify-content: center; animation: qnmFade 0.22s ease both; } @keyframes qnmFade { from{opacity:0} to{opacity:1} } /* ══ SHEET ══ */ .qnm-sheet { width: 100%; max-width: 520px; background: #06101f; border-radius: 28px 28px 0 0; border-top: 1.5px solid rgba(251,191,36,0.2); box-shadow: 0 -12px 60px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06); overflow: hidden; display: flex; flex-direction: column; max-height: 92vh; animation: qnmUp 0.38s cubic-bezier(0.34,1.56,0.64,1) both; } @keyframes qnmUp { from{transform:translateY(100%);opacity:0} to{transform:translateY(0);opacity:1} } /* Handle */ .qnm-handle-row { display:flex; justify-content:center; padding:0.8rem 0 0.3rem; flex-shrink:0; } .qnm-handle { width:38px; height:4px; border-radius:100px; background:rgba(255,255,255,0.12); } /* Close btn */ .qnm-close { position:absolute; top:0.9rem; right:1.1rem; z-index:10; width:30px; height:30px; border-radius:50%; border:1.5px solid rgba(255,255,255,0.12); background:rgba(255,255,255,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; transition: all 0.15s ease; } .qnm-close:hover { border-color:rgba(251,191,36,0.5); background:rgba(251,191,36,0.1); } /* ══ 3D ISLAND STAGE ══ */ .qnm-stage { position: relative; flex-shrink: 0; height: 200px; overflow: hidden; background: linear-gradient(180deg, var(--sky-top) 0%, var(--sky-bot) 55%, var(--sea-col) 100%); } /* Sea waves */ .qnm-sea { position:absolute; bottom:0; left:0; right:0; height:52px; background: var(--sea-col); overflow:hidden; } .qnm-wave { position:absolute; bottom:0; left:-100%; width:300%; height:30px; border-radius:50% 50% 0 0; background: rgba(255,255,255,0.07); animation: qnmWave var(--wdur,5s) ease-in-out infinite; } .qnm-wave:nth-child(2){ animation-delay:-2s; opacity:0.5; } .qnm-wave:nth-child(3){ animation-delay:-4s; opacity:0.3; height:20px; } @keyframes qnmWave { 0% { transform: translateX(0) scaleY(1); } 50% { transform: translateX(15%) scaleY(1.08);} 100%{ transform: translateX(0) scaleY(1); } } /* Floating clouds */ .qnm-cloud { position:absolute; border-radius:50px; background: rgba(255,255,255,0.18); filter: blur(4px); animation: qnmDrift var(--cdur,18s) linear infinite; } @keyframes qnmDrift { 0% { transform: translateX(-120px); opacity:0; } 10% { opacity:1; } 90% { opacity:1; } 100%{ transform: translateX(calc(100vw + 120px)); opacity:0; } } /* ── The 3D island container ── */ .qnm-island-3d-wrap { position: absolute; left: 50%; bottom: 40px; transform: translateX(-50%); perspective: 420px; width: 220px; height: 140px; } .qnm-island-3d { width: 100%; height: 100%; transform-style: preserve-3d; animation: qnmIslandSpin 18s linear infinite; position: relative; } @keyframes qnmIslandSpin { 0% { transform: rotateX(22deg) rotateY(0deg); } 100% { transform: rotateX(22deg) rotateY(360deg); } } /* Island layers — stacked in 3D */ .qnm-il { /* island layer base class */ position: absolute; left: 50%; bottom: 0; transform-origin: bottom center; border-radius: 50%; transform-style: preserve-3d; } /* Water base disc */ .qnm-il-water { width: 200px; height: 44px; margin-left: -100px; background: radial-gradient(ellipse 80% 100% at 50% 40%, var(--sea-hi), var(--sea-col)); border-radius: 50%; transform: translateZ(-4px); box-shadow: 0 0 40px var(--sea-col); animation: qnmWaterShimmer 3s ease-in-out infinite; } @keyframes qnmWaterShimmer { 0%,100%{ opacity:1; } 50% { opacity:0.82; } } /* Ripple rings on water */ .qnm-ripple { position:absolute; left:50%; top:50%; border-radius:50%; border:1.5px solid rgba(255,255,255,0.25); animation: qnmRipple 2.8s ease-out infinite; } .qnm-ripple:nth-child(2){ animation-delay:-1.4s; } @keyframes qnmRipple { 0% { width:60px; height:20px; margin-left:-30px; margin-top:-10px; opacity:0.7; } 100%{ width:180px; height:60px; margin-left:-90px; margin-top:-30px; opacity:0; } } /* Island ground */ .qnm-il-ground { width: 160px; height: 36px; margin-left: -80px; background: radial-gradient(ellipse at 40% 30%, var(--terr-hi), var(--terr-mid) 55%, var(--terr-lo)); border-radius: 50%; transform: translateZ(14px); box-shadow: 0 8px 24px rgba(0,0,0,0.55), inset 0 -4px 8px rgba(0,0,0,0.25); } /* Island side face — gives the 3D depth illusion */ .qnm-il-side { width: 158px; height: 22px; margin-left: -79px; bottom: -12px; background: linear-gradient(180deg, var(--terr-lo), rgba(0,0,0,0.6)); clip-path: ellipse(79px 100% at 50% 0%); transform: translateZ(8px) rotateX(-8deg); } /* Peak */ .qnm-il-peak { width: 80px; height: 60px; margin-left: -40px; bottom: 26px; background: radial-gradient(ellipse at 42% 25%, var(--peak-hi), var(--peak-mid) 60%, var(--peak-lo)); clip-path: var(--peak-shape, polygon(50% 0%, 80% 55%, 100% 100%, 0% 100%, 20% 55%)); transform: translateZ(26px); filter: drop-shadow(0 6px 12px rgba(0,0,0,0.5)); animation: qnmPeakBob 4s ease-in-out infinite; } @keyframes qnmPeakBob { 0%,100%{ transform: translateZ(26px) translateY(0); } 50% { transform: translateZ(26px) translateY(-4px); } } /* Floating decoration layer (trees, cactus, cloud orb, etc.) */ .qnm-il-deco { position: absolute; bottom: 56px; left: 50%; transform: translateZ(42px); animation: qnmDecoFloat 3s ease-in-out infinite; } @keyframes qnmDecoFloat { 0%,100%{ transform: translateZ(42px) translateY(0) rotate(0deg); } 50% { transform: translateZ(42px) translateY(-7px) rotate(3deg); } } .qnm-deco-emoji { font-size:1.4rem; filter:drop-shadow(0 4px 8px rgba(0,0,0,0.5)); } /* Flag pole on active */ .qnm-il-flag { position:absolute; bottom:56px; left:50%; transform: translateZ(50px) translateX(12px); } .qnm-flag-pole { width:2px; height:26px; background:#7c4a1e; border-radius:2px; } .qnm-flag-cloth { position:absolute; top:2px; left:2px; width:16px; height:11px; background:#ef4444; clip-path:polygon(0%0%,100%25%,0%100%); animation: qnmFlagWave 1.2s ease-in-out infinite; transform-origin:left center; } @keyframes qnmFlagWave { 0%,100%{ transform:skewY(0deg); } 50% { transform:skewY(-10deg); } } /* Stars / sparkles above completed island */ .qnm-star { position:absolute; font-size:1rem; animation: qnmStarPop var(--sdur,2s) ease-in-out infinite; animation-delay: var(--sdel,0s); } @keyframes qnmStarPop { 0%,100%{ transform:scale(1) translateY(0); opacity:0.8; } 50% { transform:scale(1.4) translateY(-8px); opacity:1; } } /* ══ CONTENT BELOW THE STAGE ══ */ .qnm-body { flex:1; overflow-y:auto; scrollbar-width:none; display:flex; flex-direction:column; gap:0.85rem; padding:1.1rem 1.25rem 0.5rem; } .qnm-body::-webkit-scrollbar { display:none; } /* Title block */ .qnm-title-block { position:relative; } .qnm-arc-tag { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.58rem; font-weight:800; letter-spacing:0.14em; text-transform:uppercase; color:var(--ac); background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.08); border-radius:100px; padding:0.18rem 0.6rem; margin-bottom:0.45rem; } .qnm-quest-title { font-family:'Cinzel',serif; font-size:1.22rem; font-weight:700; color:white; letter-spacing:0.02em; line-height:1.2; margin-bottom:0.18rem; } .qnm-island-name { font-family:'Nunito Sans',sans-serif; font-size:0.72rem; font-weight:700; color:rgba(255,255,255,0.38); } /* Flavour quote */ .qnm-flavour { background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.07); border-left:3px solid var(--ac); border-radius:0 14px 14px 0; padding:0.8rem 1rem; } .qnm-flavour-text { font-family:'Sorts Mill Goudy',serif; font-size:0.82rem; color:rgba(255,255,255,0.55); font-style:italic; line-height:1.6; } /* Objective card */ .qnm-obj-card { background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:18px; padding:0.9rem 1rem; } .qnm-obj-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.65rem; } .qnm-obj-label { font-size:0.58rem; font-weight:800; letter-spacing:0.14em; text-transform:uppercase; color:rgba(255,255,255,0.3); } .qnm-obj-pct { font-family:'Nunito',sans-serif; font-size:0.78rem; font-weight:900; color:var(--ac); } .qnm-obj-row { display:flex; align-items:center; gap:0.65rem; margin-bottom:0.7rem; } .qnm-obj-icon { width:38px; height:38px; border-radius:12px; flex-shrink:0; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.08); display:flex; align-items:center; justify-content:center; font-size:1.1rem; } .qnm-obj-text { font-family:'Nunito',sans-serif; font-size:0.88rem; font-weight:900; color:white; } .qnm-obj-sub { font-family:'Nunito Sans',sans-serif; font-size:0.68rem; font-weight:600; color:rgba(255,255,255,0.35); margin-top:0.05rem; } /* Progress bar */ .qnm-bar-track { height:9px; background:rgba(255,255,255,0.07); border-radius:100px; overflow:hidden; margin-bottom:0.3rem; } .qnm-bar-fill { height:100%; border-radius:100px; background:linear-gradient(90deg, var(--ac), color-mix(in srgb, var(--ac) 65%, white)); box-shadow:0 0 10px color-mix(in srgb, var(--ac) 55%, transparent); transition:width 0.8s cubic-bezier(0.34,1.56,0.64,1); } .qnm-bar-nums { display:flex; justify-content:space-between; font-family:'Nunito',sans-serif; font-size:0.65rem; font-weight:800; color:rgba(255,255,255,0.28); } .qnm-bar-nums span:first-child { color:var(--ac); } /* ── HOW TO COMPLETE section ── */ .qnm-howto-label { font-size:0.58rem; font-weight:800; letter-spacing:0.14em; text-transform:uppercase; color:rgba(255,255,255,0.3); margin-bottom:0.55rem; margin-top:0.3rem; } .qnm-howto-badges { display:flex; flex-wrap:wrap; gap:0.4rem; } .qnm-howto-badge { display:flex; align-items:center; gap:0.3rem; padding:0.38rem 0.75rem; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1); border-radius:100px; font-family:'Nunito',sans-serif; font-size:0.72rem; font-weight:800; color:rgba(255,255,255,0.7); transition: all 0.15s ease; animation: qnmBadgeIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both; animation-delay: var(--bdel, 0s); } @keyframes qnmBadgeIn { from{ opacity:0; transform:scale(0.8) translateY(6px); } to { opacity:1; transform:scale(1) translateY(0); } } .qnm-howto-badge:hover { background:rgba(255,255,255,0.1); border-color:rgba(255,255,255,0.2); color:white; transform:translateY(-1px); } /* Highlight badge = accent coloured */ .qnm-howto-badge.hi { background:color-mix(in srgb, var(--ac) 18%, transparent); border-color:color-mix(in srgb, var(--ac) 45%, transparent); color:var(--ac); } /* Locked banner */ .qnm-locked-banner { display:flex; align-items:center; gap:0.7rem; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.07); border-radius:16px; padding:0.9rem 1rem; } .qnm-locked-icon { width:38px; height:38px; border-radius:12px; flex-shrink:0; background:rgba(255,255,255,0.05); display:flex; align-items:center; justify-content:center; } .qnm-locked-text { font-family:'Nunito',sans-serif; font-size:0.82rem; font-weight:800; color:rgba(255,255,255,0.4); } .qnm-locked-sub { font-family:'Nunito Sans',sans-serif; font-size:0.68rem; font-weight:600; color:rgba(255,255,255,0.22); margin-top:0.1rem; } /* Reward card */ .qnm-reward-card { background:rgba(251,191,36,0.07); border:1px solid rgba(251,191,36,0.22); border-radius:18px; padding:0.9rem 1rem; } .qnm-reward-label { font-size:0.58rem; font-weight:800; letter-spacing:0.14em; text-transform:uppercase; color:rgba(251,191,36,0.6); margin-bottom:0.6rem; } .qnm-reward-row { display:flex; flex-wrap:wrap; gap:0.4rem; } .qnm-reward-pill { display:flex; align-items:center; gap:0.28rem; padding:0.35rem 0.8rem; background:rgba(251,191,36,0.1); border:1.5px solid rgba(251,191,36,0.3); border-radius:100px; font-family:'Nunito',sans-serif; font-size:0.75rem; font-weight:900; color:#fbbf24; box-shadow:0 2px 8px rgba(251,191,36,0.1); } /* ══ FOOTER CTA ══ */ .qnm-footer { padding:1rem 1.25rem calc(1rem + env(safe-area-inset-bottom)); flex-shrink:0; border-top:1px solid rgba(255,255,255,0.07); background:#06101f; } .qnm-claim-btn { width:100%; padding:0.95rem; background:linear-gradient(135deg,#fbbf24,#f59e0b); border:none; border-radius:16px; cursor:pointer; font-family:'Cinzel',serif; font-size:1rem; font-weight:700; color:#1a0800; letter-spacing:0.04em; box-shadow:0 5px 0 #d97706, 0 8px 24px rgba(251,191,36,0.35); transition:all 0.12s ease; } .qnm-claim-btn:hover { transform:translateY(-2px); box-shadow:0 7px 0 #d97706; } .qnm-claim-btn:active { transform:translateY(2px); box-shadow:0 3px 0 #d97706; } .qnm-note { text-align:center; font-family:'Nunito Sans',sans-serif; font-size:0.72rem; font-weight:700; color:rgba(255,255,255,0.28); padding:0.4rem 0; } `; // ─── Per-arc terrain themes ─────────────────────────────────────────────────── interface Terrain { skyTop: string; skyBot: string; seaCol: string; seaHi: string; terrHi: string; terrMid: string; terrLo: string; peakHi: string; peakMid: string; peakLo: string; decos: string[]; } const TERRAIN: Record = { east_blue: { skyTop: "#0a1628", skyBot: "#0d2240", seaCol: "#0a3d5c", seaHi: "#1a6a8a", terrHi: "#5eead4", terrMid: "#0d9488", terrLo: "#0f5c55", peakHi: "#a7f3d0", peakMid: "#34d399", peakLo: "#065f46", decos: ["🌴", "🌿"], }, alabasta: { skyTop: "#1c0a00", skyBot: "#3d1a00", seaCol: "#7c3a00", seaHi: "#c26010", terrHi: "#fde68a", terrMid: "#d97706", terrLo: "#78350f", peakHi: "#fef3c7", peakMid: "#fbbf24", peakLo: "#92400e", decos: ["🌵", "🏺"], }, skypiea: { skyTop: "#1a0033", skyBot: "#2e0050", seaCol: "#4c1d95", seaHi: "#7c3aed", terrHi: "#e9d5ff", terrMid: "#a855f7", terrLo: "#581c87", peakHi: "#f5d0fe", peakMid: "#d946ef", peakLo: "#701a75", decos: ["☁️", "✨"], }, }; const DEFAULT_TERRAIN = TERRAIN.east_blue; // ─── Per-requirement how-to badges ─────────────────────────────────────────── interface Badge { emoji: string; label: string; highlight?: boolean; } const HOW_TO: Record = { questions: { title: "How to complete this", badges: [ { emoji: "📝", label: "Take a Practice Sheet", highlight: true }, { emoji: "⚡", label: "Try Drills Mode" }, { emoji: "🎯", label: "Aim for 10+ per session" }, { emoji: "🔄", label: "Retry completed sheets" }, ], }, accuracy: { title: "How to complete this", badges: [ { emoji: "🐢", label: "Slow down, read carefully", highlight: true }, { emoji: "📖", label: "Review wrong answers" }, { emoji: "🎯", label: "Do targeted practice", highlight: true }, { emoji: "💡", label: "Use process of elimination" }, ], }, streak: { title: "How to complete this", badges: [ { emoji: "📅", label: "Practice every day", highlight: true }, { emoji: "⏰", label: "Set a daily reminder" }, { emoji: "🔥", label: "Even 5 mins counts" }, { emoji: "🛡️", label: "Use a Streak Shield" }, ], }, sessions: { title: "How to complete this", badges: [ { emoji: "📝", label: "Start a Practice Sheet", highlight: true }, { emoji: "⚡", label: "Complete Drills" }, { emoji: "🏃", label: "Short sessions count too" }, { emoji: "📚", label: "Try different modules" }, ], }, topics: { title: "How to complete this", badges: [ { emoji: "🗺️", label: "Explore new modules", highlight: true }, { emoji: "📊", label: "Check your weak topics" }, { emoji: "🔍", label: "Use Search to find topics" }, { emoji: "⚡", label: "Drill each topic once" }, ], }, xp: { title: "How to complete this", badges: [ { emoji: "🎯", label: "High accuracy = bonus XP", highlight: true }, { emoji: "🔥", label: "Maintain your streak" }, { emoji: "📝", label: "Complete full sheets", highlight: true }, { emoji: "⚡", label: "Use XP Boosts" }, ], }, leaderboard: { title: "How to complete this", badges: [ { emoji: "📈", label: "Aim for 80%+ accuracy", highlight: true }, { emoji: "🔥", label: "Keep your streak alive" }, { emoji: "📝", label: "Do sessions daily" }, { emoji: "🏆", label: "Check the leaderboard" }, ], }, }; // ─── Island shape configs (mirrors the 6 clip-path shapes in QuestMap) ──────── // groundClip = clip-path for the flat top disc of the island // peakClip = clip-path for the hill/feature rising above it // groundW/H = pixel size of the ground layer // peakW/H = pixel size of the peak layer // sideClip = clip-path for the side-face depth layer interface ShapeConfig { groundClip: string; peakClip: string; sideClip: string; groundW: number; groundH: number; peakW: number; peakH: number; peakBottom: number; // translateZ bottom offset in px } // These correspond 1-to-1 with SHAPES[0..5] in QuestMap.tsx const ISLAND_SHAPES: ShapeConfig[] = [ // 0: fat round atoll { groundClip: "ellipse(50% 50% at 50% 50%)", peakClip: "ellipse(50% 50% at 50% 55%)", sideClip: "ellipse(50% 100% at 50% 0%)", groundW: 160, groundH: 38, peakW: 88, peakH: 38, peakBottom: 26, }, // 1: tall mountain — narrow diamond ground, sharp triangular peak { groundClip: "polygon(50% 5%, 92% 50%, 50% 95%, 8% 50%)", peakClip: "polygon(50% 0%, 82% 52%, 100% 100%, 0% 100%, 18% 52%)", sideClip: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)", groundW: 148, groundH: 36, peakW: 72, peakH: 72, peakBottom: 24, }, // 2: wide flat shoal — extra-wide squashed ellipse, low dome { groundClip: "ellipse(50% 40% at 50% 58%)", peakClip: "ellipse(50% 38% at 50% 60%)", sideClip: "ellipse(50% 100% at 50% 0%)", groundW: 178, groundH: 30, peakW: 114, peakH: 28, peakBottom: 22, }, // 3: jagged rocky reef — star-burst polygon { groundClip: "polygon(50% 2%, 63% 35%, 98% 35%, 71% 56%, 80% 92%, 50% 72%, 20% 92%, 29% 56%, 2% 35%, 37% 35%)", peakClip: "polygon(50% 0%, 63% 32%, 98% 32%, 71% 54%, 80% 90%, 50% 70%, 20% 90%, 29% 54%, 2% 32%, 37% 32%)", sideClip: "ellipse(50% 100% at 50% 0%)", groundW: 152, groundH: 38, peakW: 80, peakH: 66, peakBottom: 24, }, // 4: crescent — lopsided asymmetric bean { groundClip: "path('M 80 10 C 120 5, 150 30, 145 55 C 140 78, 110 88, 80 85 C 55 82, 38 70, 42 55 C 46 42, 62 40, 68 50 C 74 60, 65 70, 55 68 C 38 62, 30 42, 42 28 C 55 12, 70 12, 80 10 Z')", peakClip: "ellipse(38% 55% at 38% 50%)", sideClip: "ellipse(50% 100% at 50% 0%)", groundW: 160, groundH: 36, peakW: 80, peakH: 58, peakBottom: 22, }, // 5: teardrop/pear — narrow top, wide rounded base { groundClip: "path('M 50 4 C 72 4, 95 28, 95 55 C 95 78, 76 94, 50 94 C 24 94, 5 78, 5 55 C 5 28, 28 4, 50 4 Z')", peakClip: "polygon(50% 0%, 73% 27%, 88% 62%, 68% 98%, 32% 98%, 12% 62%, 27% 27%)", sideClip: "ellipse(50% 100% at 50% 0%)", groundW: 144, groundH: 38, peakW: 76, peakH: 66, peakBottom: 24, }, ]; // ─── Helpers ────────────────────────────────────────────────────────────────── const reqIcon = (type: string): string => ({ questions: "❓", accuracy: "🎯", streak: "🔥", sessions: "📚", topics: "🗺️", xp: "⚡", leaderboard: "🏆", })[type] ?? "⭐"; // ─── 3D Island Stage ────────────────────────────────────────────────────────── const IslandStage = ({ arcId, status, nodeIndex, }: { arcId: string; status: QuestNode["status"]; nodeIndex: number; }) => { const t = TERRAIN[arcId] ?? DEFAULT_TERRAIN; const shp = ISLAND_SHAPES[nodeIndex % ISLAND_SHAPES.length]; const isCompleted = status === "completed"; const isClaimable = status === "claimable"; const isActive = status === "active"; const isLocked = status === "locked"; const vars = { "--sky-top": t.skyTop, "--sky-bot": t.skyBot, "--sea-col": t.seaCol, "--sea-hi": t.seaHi, "--terr-hi": t.terrHi, "--terr-mid": t.terrMid, "--terr-lo": t.terrLo, "--peak-hi": t.peakHi, "--peak-mid": t.peakMid, "--peak-lo": t.peakLo, } as React.CSSProperties; return (
{/* Clouds */} {[ { w: 70, h: 22, top: 14, delay: 0, dur: 16 }, { w: 50, h: 16, top: 28, delay: -7, dur: 22 }, { w: 40, h: 14, top: 10, delay: -3, dur: 28 }, ].map((c, i) => (
))} {/* Sea + waves */}
{/* Ripple rings on water surface */}
{/* 3D island */}
{/* Water base */}
{/* Island side face */}
{/* Island ground — shaped to match QuestMap */}
{/* Peak / hill — shaped to match QuestMap */} {!isLocked && (
)} {/* Decorations */} {!isLocked && t.decos.map((deco, di) => (
{deco}
))} {/* Pirate flag on active */} {isActive && (
)} {/* Chest bouncing on claimable */} {isClaimable && (
📦
)} {/* Lock icon on locked */} {isLocked && (
🔒
)}
{/* Sparkles for completed */} {isCompleted && ( <> {[ { left: "30%", top: "18%", sdur: "2s", sdel: "0s" }, { left: "62%", top: "12%", sdur: "2.4s", sdel: "0.6s" }, { left: "20%", top: "38%", sdur: "1.8s", sdel: "1.1s" }, { left: "74%", top: "32%", sdur: "2.2s", sdel: "0.3s" }, ].map((s, i) => ( ))} )} {/* Lock overlay tint */} {isLocked && (
🔒
)}
); }; // ─── Main component ─────────────────────────────────────────────────────────── interface Props { node: QuestNode; arcAccent: string; arcDark: string; arcId?: string; nodeIndex?: number; onClose: () => void; onClaim: () => void; } export const QuestNodeModal = ({ node, arcAccent, arcDark, arcId = "east_blue", nodeIndex = 0, onClose, onClaim, }: Props) => { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const progress = Math.min( 100, Math.round((node.progress / node.requirement.target) * 100), ); const isClaimable = node.status === "claimable"; const isLocked = node.status === "locked"; const isCompleted = node.status === "completed"; const isActive = node.status === "active"; const howTo = HOW_TO[node.requirement.type]; return (
e.stopPropagation()}> {/* Handle + close */}
{/* 3D island stage */} {/* Scrollable content */}
{/* Title */}
{reqIcon(node.requirement.type)} Quest

{node.title}

📍 {node.islandName}

{/* Flavour */}

{node.flavourText}

{/* Objective */}
⚓ Objective {!isLocked && ( {isCompleted ? "✅ Done" : `${progress}%`} )}
{reqIcon(node.requirement.type)}

{node.requirement.target} {node.requirement.label}

{isCompleted ? "✅ Completed — treasure claimed!" : isLocked ? "🔒 Complete previous quests first" : `${node.progress} / ${node.requirement.target} done`}

{/* Progress bar */} {!isLocked && ( <>
{node.progress} {node.requirement.target}
)} {/* How-to badges — show when active or claimable */} {(isActive || isClaimable) && howTo && ( <>

🧭 {howTo.title}

{howTo.badges.map((b, bi) => (
{b.emoji} {b.label}
))}
)}
{/* Locked banner */} {isLocked && (

Quest Locked

Complete the previous island to sail here

)} {/* Reward */}

📦 Treasure Chest

⚡ +{node.reward.xp} XP
{node.reward.title && (
🏴‍☠️ {node.reward.title}
)} {node.reward.itemLabel && (
🎁 {node.reward.itemLabel}
)}
{/* Footer CTA */}
{isClaimable ? ( ) : isCompleted ? (

✅ Completed — treasure claimed!

) : isLocked ? (

🔒 Locked — keep sailing

) : (

{progress}% complete · {node.requirement.target - node.progress}{" "} {node.requirement.label} remaining

)}
); };