1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { u as useTranslation } from "./vendor-i18n-BC7ZMXWQ.js"; 3import { dh as SlidersVertical, aW as X, bg as Music, bX as VolumeX, bU as Volume2, e2 as Palette, fb as Dices, bi as Sparkles, b7 as Check, bt as Tv, aX as ArrowLeft, cI as Disc, cG as Keyboard, cD as Drum, fc as PawPrint, fd as Boxes, ev as Bookmark, b2 as Upload, bE as Link, fe as Radar, c1 as Share2, bR as ExternalLink, b_ as Pause, bV as Play, bF as ChevronRight, co as FileMusic, cM as RefreshCw, bf as Search, cB as ChevronUp, b6 as LoaderCircle, db as Music2, cC as ChevronDown, cg as Activity, ch as Gauge, cz as Trophy, bs as Zap, c4 as RotateCcw, c7 as Shuffle, cv as Award, ff as Smartphone, bT as Mic, cR as MicOff, ck as Layers, cS as Minus, cf as Plus, c$ as Minimize2, d0 as Maximize2, b1 as CircleAlert } from "./vendor-D0P4XgNj.js"; 4import { a as Midi } from "./vendor-audio-D-wTggbx.js"; 5import { a as api } from "./index-w5lXls15.js"; 6import { a as generateTrackUrl, e as isMobileDevice } from "./utils-CEVb7nN1.js"; 7import { P as PianoRollGame } from "./PianoRollGame-BVidPvgJ.js"; 8import { T as TaikoGame } from "./TaikoGame-D89Exv0S.js"; 9import "./synth-Fr-K52Ox.js"; 10import "./vendor-react-three-BCww3P1x.js"; 11import "./wasm-synth-adapter-DpNGIIyM.js"; 12import "./vendor-spessasynth-DH8n6o_V.js"; 13import "./SharedKeyboard-B8ktwnUd.js"; 14import "./useMidiKeyboard-DlyEIN8T.js"; 15import "./GlobeFlag-V1uaxVEK.js"; 16import "./SouthparkInstruments-C-w5kAUh.js"; 17import "./IkMath-D6fi1ppd.js"; 18import "./vendor-framer-B-4FZk1Z.js"; 19import "./SouthparkVariants-DYvo6xjJ.js"; 20const ARCADE_THEMES = { 21 "5thmix": { 22 id: "5thmix", 23 name: "CYBER 5thMIX", 24 subtitle: "DDR 5thMIX / 4thMIX CLASSIC", 25 eraLabel: "ARCADE 2001", 26 primary: "#00f0ff", 27 // Electric Cyan 28 primaryHover: "#38bdf8", 29 secondary: "#facc15", 30 // Sunburst Yellow 31 accent: "#ec4899", 32 // Hot Pink 33 backgroundDark: "#041026", 34 deckBorder: "border-4 border-cyan-400 shadow-[0_0_35px_rgba(0,240,255,0.35)]", 35 // Header 36 selectMusicTitle: "SELECT MUSIC", 37 selectMusicBannerClass: "bg-gradient-to-r from-cyan-400 via-sky-300 to-cyan-400 text-slate-950 border-2 border-white shadow-[0_0_20px_rgba(0,240,255,0.9),0_0_40px_rgba(0,240,255,0.4)] font-black tracking-[0.2em] px-8 py-1.5 rounded-full text-base sm:text-lg italic", 38 timerBoxClass: "bg-gradient-to-b from-yellow-300 via-amber-400 to-yellow-500 text-blue-950 border-2 border-yellow-200 shadow-[0_0_15px_rgba(250,204,21,0.8)] font-black text-lg sm:text-xl px-2.5 py-0.5 rounded-xl", 39 headerArrowColor: "#facc15", 40 marqueeGradient: "from-blue-950 via-cyan-950 to-blue-950", 41 marqueeBorder: "border-b-4 border-cyan-400/90 shadow-[0_0_25px_rgba(0,240,255,0.4)]", 42 creditsBadge: "bg-yellow-400 text-black border-2 border-yellow-300 shadow-[0_0_12px_rgba(250,204,21,0.6)]", 43 // Wheel & Cartridge 44 cartridgeShape: "pill", 45 cartridgeBg: "bg-blue-950/70 border-2 border-cyan-700/50 text-cyan-200 hover:border-cyan-400/70", 46 cartridgeBorder: "border-cyan-500/40", 47 cartridgeActiveBg: "bg-gradient-to-r from-blue-700 via-cyan-600 to-blue-800 text-white", 48 cartridgeActiveBorder: "border-yellow-400 shadow-[0_0_25px_rgba(0,240,255,0.7)]", 49 cartridgeActiveThickBorder: "border-4 border-yellow-300 shadow-[0_0_30px_rgba(0,240,255,0.8),inset_0_0_15px_rgba(250,204,21,0.4)]", 50 cartridgeActiveGlow: "rgba(0,240,255,0.7)", 51 titleColorActive: "text-yellow-300 drop-shadow-[0_2px_6px_rgba(0,0,0,0.9)]", 52 titleColorInactive: "text-amber-400/90 hover:text-yellow-300", 53 artistColorActive: "text-cyan-200 font-bold", 54 artistColorInactive: "text-blue-300/60", 55 wheelHalftoneType: "dots", 56 wheelBgGradient: "bg-gradient-to-b from-blue-950/80 via-slate-950/90 to-blue-950/95", 57 // Information Deck & Banner 58 bannerBorderClass: "border-4 border-cyan-400 bg-gradient-to-r from-blue-950 via-cyan-950 to-blue-900 shadow-[0_0_25px_rgba(0,240,255,0.4)]", 59 bannerBgClass: "bg-gradient-to-b from-blue-900/60 via-slate-900/80 to-blue-950/90", 60 hudStageBoxClass: "bg-blue-950/90 border-2 border-cyan-400/80 text-cyan-300 shadow-[0_0_10px_rgba(0,240,255,0.3)]", 61 badgeStyle: "grade", 62 footprintColor: "#facc15", 63 footprint1PColor: "#facc15", 64 // Yellow 65 footprint2PColor: "#ec4899", 66 // Hot Pink 67 // Radar 68 radarPolygonFill: "rgba(0, 240, 255, 0.45)", 69 radarPolygonStroke: "#00f0ff", 70 radarGridStroke: "rgba(0, 240, 255, 0.3)", 71 radarLabelColor: "#facc15",
72 // Canvas Background 73 bgGradientTop: "#020617", 74 bgGradientHorizon: "#082f49", 75 bgGradientBottom: "#020c1b", 76 horizonGlowColor: "rgba(0, 240, 255, 0.45)", 77 particleColors: ["#00f0ff", "#facc15", "#ec4899", "#38bdf8"], 78 gridLineColor: "rgba(0, 240, 255, 0.28)" 79 }, 80 "max2": { 81 id: "max2", 82 name: "GROOVE LIME MAX2", 83 subtitle: "DDR MAX2 / MATRIX TECH HUD", 84 eraLabel: "ARCADE 2002", 85 primary: "#a3e635", 86 // Acid Cyber Lime 87 primaryHover: "#84cc16", 88 secondary: "#22c55e", 89 // Emerald Green 90 accent: "#eab308", 91 // Tech Amber 92 backgroundDark: "#031405", 93 deckBorder: "border-4 border-lime-400 shadow-[0_0_35px_rgba(163,230,53,0.35)]", 94 // Header 95 selectMusicTitle: "// Select Music", 96 selectMusicBannerClass: "bg-gradient-to-r from-lime-400 via-lime-300 to-lime-400 text-slate-950 border-2 border-white shadow-[0_0_20px_rgba(163,230,53,0.9),0_0_40px_rgba(163,230,53,0.4)] font-black tracking-[0.2em] px-8 py-1.5 rounded-md text-base sm:text-lg italic", 97 timerBoxClass: "bg-gradient-to-b from-lime-400 via-lime-500 to-emerald-600 text-slate-950 border-2 border-lime-200 shadow-[0_0_15px_rgba(163,230,53,0.8)] font-black text-lg sm:text-xl px-2.5 py-0.5 rounded-md", 98 headerArrowColor: "#a3e635", 99 marqueeGradient: "from-emerald-950 via-lime-950 to-green-950", 100 marqueeBorder: "border-b-4 border-lime-400/90 shadow-[0_0_25px_rgba(163,230,53,0.4)]", 101 creditsBadge: "bg-lime-400 text-black border-2 border-lime-300 shadow-[0_0_12px_rgba(163,230,53,0.6)]", 102 // Wheel & Cartridge 103 cartridgeShape: "trapezoid", 104 cartridgeBg: "bg-emerald-950/70 border-2 border-lime-800/40 text-lime-200 hover:border-lime-500/70", 105 cartridgeBorder: "border-lime-500/40", 106 cartridgeActiveBg: "bg-gradient-to-r from-lime-400 via-lime-500 to-emerald-400 text-slate-950 font-black", 107 cartridgeActiveBorder: "border-white shadow-[0_0_25px_rgba(163,230,53,0.8)]", 108 cartridgeActiveThickBorder: "border-4 border-white shadow-[0_0_30px_rgba(163,230,53,0.9),inset_0_0_15px_rgba(255,255,255,0.6)]", 109 cartridgeActiveGlow: "rgba(163,230,53,0.8)", 110 titleColorActive: "text-slate-950 font-black drop-shadow-[0_1px_2px_rgba(255,255,255,0.6)]", 111 titleColorInactive: "text-lime-300/80 hover:text-lime-200", 112 artistColorActive: "text-emerald-950 font-black", 113 artistColorInactive: "text-lime-500/60", 114 wheelHalftoneType: "matrix", 115 wheelBgGradient: "bg-gradient-to-b from-emerald-950/80 via-black/85 to-lime-950/90", 116 // Information Deck & Banner 117 bannerBorderClass: "border-4 border-lime-400 bg-gradient-to-r from-emerald-950 via-lime-950 to-black shadow-[0_0_25px_rgba(163,230,53,0.4)]", 118 bannerBgClass: "bg-gradient-to-b from-emerald-950/70 via-slate-950/80 to-lime-950/90", 119 hudStageBoxClass: "bg-emerald-950/90 border-2 border-lime-400/80 text-lime-300 shadow-[0_0_10px_rgba(163,230,53,0.3)]", 120 badgeStyle: "kanji", 121 footprintColor: "#a3e635", 122 footprint1PColor: "#a3e635", 123 // Acid Lime 124 footprint2PColor: "#f97316", 125 // Orange 126 // Radar 127 radarPolygonFill: "rgba(163, 230, 53, 0.45)", 128 radarPolygonStroke: "#bef264", 129 radarGridStroke: "rgba(163, 230, 53, 0.3)", 130 radarLabelColor: "#a3e635", 131 // Canvas Background 132 bgGradientTop: "#020f04", 133 bgGradientHorizon: "#063810", 134 bgGradientBottom: "#010a03", 135 horizonGlowColor: "rgba(163, 230, 53, 0.4)", 136 particleColors: ["#a3e635", "#22c55e", "#86efac", "#eab308"], 137 gridLineColor: "rgba(163, 230, 53, 0.25)"
138 }, 139 "sunset": { 140 id: "sunset", 141 name: "NEON SUNSET MAX", 142 subtitle: "DDR MAX / CARBON FIRE CHASSIS", 143 eraLabel: "ARCADE 2001", 144 primary: "#f97316", 145 // Molten Neon Orange 146 primaryHover: "#ea580c", 147 secondary: "#ef4444", 148 // Crimson Red 149 accent: "#00f0ff", 150 // Ice Cyan Contrast 151 backgroundDark: "#140602", 152 deckBorder: "border-4 border-orange-500 shadow-[0_0_35px_rgba(249,115,22,0.35)]", 153 // Header 154 selectMusicTitle: "// Select Music", 155 selectMusicBannerClass: "bg-gradient-to-r from-amber-400 via-yellow-300 to-amber-500 text-slate-950 border-2 border-white shadow-[0_0_20px_rgba(245,158,11,0.9),0_0_40px_rgba(245,158,11,0.4)] font-black tracking-[0.2em] px-8 py-1.5 rounded-md text-base sm:text-lg italic", 156 timerBoxClass: "bg-gradient-to-b from-amber-300 via-orange-400 to-amber-500 text-slate-950 border-2 border-yellow-200 shadow-[0_0_15px_rgba(245,158,11,0.9)] font-black text-lg sm:text-xl px-2.5 py-0.5 rounded-md", 157 headerArrowColor: "#00f0ff", 158 marqueeGradient: "from-red-950 via-orange-950 to-amber-950", 159 marqueeBorder: "border-b-4 border-orange-500/90 shadow-[0_0_25px_rgba(249,115,22,0.4)]", 160 creditsBadge: "bg-orange-500 text-black border-2 border-orange-300 shadow-[0_0_12px_rgba(249,115,22,0.6)]", 161 // Wheel & Cartridge 162 cartridgeShape: "trapezoid", 163 cartridgeBg: "bg-zinc-950/75 border-2 border-orange-900/50 text-orange-200 hover:border-orange-500/70", 164 cartridgeBorder: "border-orange-500/40", 165 cartridgeActiveBg: "bg-gradient-to-r from-amber-600 via-orange-600 to-amber-500 text-white font-black", 166 cartridgeActiveBorder: "border-amber-300 shadow-[0_0_25px_rgba(249,115,22,0.8)]", 167 cartridgeActiveThickBorder: "border-4 border-amber-300 shadow-[0_0_35px_rgba(249,115,22,0.9),inset_0_0_15px_rgba(251,191,36,0.5)]", 168 cartridgeActiveGlow: "rgba(249,115,22,0.85)", 169 titleColorActive: "text-cyan-200 font-black drop-shadow-[0_2px_6px_rgba(0,0,0,0.9)]", 170 titleColorInactive: "text-cyan-400/90 hover:text-cyan-300", 171 artistColorActive: "text-amber-200 font-bold", 172 artistColorInactive: "text-cyan-600/70", 173 wheelHalftoneType: "sunset", 174 wheelBgGradient: "bg-gradient-to-b from-amber-950/70 via-red-950/80 to-black/95", 175 // Information Deck & Banner 176 bannerBorderClass: "border-4 border-orange-500 bg-gradient-to-r from-red-950 via-orange-950 to-zinc-950 shadow-[0_0_25px_rgba(249,115,22,0.4)]", 177 bannerBgClass: "bg-gradient-to-b from-red-950/70 via-slate-950/85 to-orange-950/90", 178 hudStageBoxClass: "bg-orange-950/90 border-2 border-orange-500/80 text-orange-300 shadow-[0_0_10px_rgba(249,115,22,0.3)]", 179 badgeStyle: "kanji", 180 footprintColor: "#f97316", 181 footprint1PColor: "#22c55e", 182 // Green 183 footprint2PColor: "#f97316", 184 // Orange 185 // Radar 186 radarPolygonFill: "rgba(249, 115, 22, 0.45)", 187 radarPolygonStroke: "#fdba74", 188 radarGridStroke: "rgba(249, 115, 22, 0.3)", 189 radarLabelColor: "#fb923c", 190 // Canvas Background 191 bgGradientTop: "#0c0201", 192 bgGradientHorizon: "#3b0d05", 193 bgGradientBottom: "#080100", 194 horizonGlowColor: "rgba(249, 115, 22, 0.45)", 195 particleColors: ["#f97316", "#ef4444", "#facc15", "#00f0ff"], 196 gridLineColor: "rgba(249, 115, 22, 0.25)" 197 } 198}; 199const THEME_LIST = ["5thmix", "max2", "sunset"]; 200function computeGrooveRadar(song) { 201 if (!song) { 202 return { stream: 50, voltage: 50, chaos: 50, freeze: 50, air: 50 }; 203 } 204 const bpm = typeof song.bpm === "number" && song.bpm > 0 ? song.bpm : 120; 205 const duration = typeof song.duration === "number" && song.duration > 0 ? song.duration : 180; 206 const stars = typeof song.difficultyStars === "number" ? song.difficultyStars : 3; 207 const title = String(song.title || song.id || "HamieTrack"); 208 let hash = 0; 209 for (let i = 0; i < title.length; i++) { 210 hash = hash * 31 + title.charCodeAt(i) & 4294967295; 211 } 212 const pseudo = (offset) => { 213 const val = Math.sin(hash + offset) * 1e4; 214 return val - Math.floor(val); 215 }; 216 const streamBase = Math.min(100, Math.max(20, Math.round(bpm / 180 * 55 + stars * 9 + pseudo(1) * 15))); 217 const voltageBase = Math.min(100, Math.max(25, Math.round(stars * 16 + (bpm > 140 ? 20 : 5) + pseudo(2) * 20))); 218 const chaosBase = Math.min(100, Math.max(15, Math.round(bpm % 17 * 3 + stars * 12 + pseudo(3) * 25))); 219 const freezeBase = Math.min(100, Math.max(20, Math.round(duration / 240 * 40 + pseudo(4) * 45))); 220 const airBase = Math.min(100, Math.max(20, Math.round(stars * 14 + pseudo(5) * 30))); 221 return { 222 stream: streamBase, 223 voltage: voltageBase, 224 chaos: chaosBase, 225 freeze: freezeBase, 226 air: airBase 227 }; 228} 229function getRandomTheme() { 230 const idx = Math.floor(Math.random() * THEME_LIST.length); 231 return THEME_LIST[idx]; 232}
233const ArcadeBackground = ({ 234 enableScanlines = true, 235 theme = ARCADE_THEMES["5thmix"], 236 isActive = true 237}) => { 238 const canvasRef = reactExports.useRef(null); 239 const themeRef = reactExports.useRef(theme); 240 themeRef.current = theme; 241 const isActiveRef = reactExports.useRef(isActive); 242 isActiveRef.current = isActive; 243 reactExports.useEffect(() => { 244 const canvas = canvasRef.current; 245 if (!canvas) return; 246 const ctx = canvas.getContext("2d", { alpha: false }); 247 if (!ctx) return; 248 let animationFrameId; 249 let width = canvas.width = window.innerWidth; 250 let height = canvas.height = window.innerHeight; 251 const handleResize = () => { 252 if (!canvas) return; 253 width = canvas.width = window.innerWidth; 254 height = canvas.height = window.innerHeight; 255 }; 256 window.addEventListener("resize", handleResize); 257 const particles = []; 258 for (let i = 0; i < 30; i++) { 259 particles.push({ 260 x: Math.random() * width, 261 y: Math.random() * height, 262 size: Math.random() * 2.2 + 1, 263 speedY: -(Math.random() * 0.5 + 0.2), 264 colorIndex: Math.floor(Math.random() * 4), 265 alpha: Math.random() * 0.6 + 0.2 266 }); 267 } 268 let gridOffset = 0; 269 let lastFrameTime = performance.now(); 270 const render = (now) => { 271 if (!isActiveRef.current) { 272 animationFrameId = requestAnimationFrame(render); 273 return; 274 } 275 const delta = now - lastFrameTime; 276 if (delta < 15) { 277 animationFrameId = requestAnimationFrame(render); 278 return; 279 } 280 lastFrameTime = now; 281 const currentTheme = themeRef.current; 282 const bgGrad = ctx.createLinearGradient(0, 0, 0, height); 283 bgGrad.addColorStop(0, currentTheme.bgGradientTop); 284 bgGrad.addColorStop(0.5, currentTheme.bgGradientHorizon); 285 bgGrad.addColorStop(1, currentTheme.bgGradientBottom); 286 ctx.fillStyle = bgGrad; 287 ctx.fillRect(0, 0, width, height); 288 const horizonY = height * 0.52; 289 const horizonGlow = ctx.createRadialGradient(width / 2, horizonY, 10, width / 2, horizonY, width * 0.6); 290 horizonGlow.addColorStop(0, currentTheme.horizonGlowColor); 291 horizonGlow.addColorStop(1, "transparent"); 292 ctx.fillStyle = horizonGlow; 293 ctx.fillRect(0, 0, width, height); 294 gridOffset = (gridOffset + 0.7) % 36; 295 ctx.save(); 296 ctx.beginPath(); 297 ctx.rect(0, horizonY, width, height - horizonY); 298 ctx.clip(); 299 const vpX = width / 2; 300 const vpY = horizonY; 301 ctx.lineWidth = 1; 302 const numRays = Math.min(28, Math.floor(width / 50)); 303 const rayGrad = ctx.createLinearGradient(vpX, vpY, vpX, height); 304 rayGrad.addColorStop(0, `${currentTheme.secondary}15`); 305 rayGrad.addColorStop(0.5, `${currentTheme.primary}40`); 306 rayGrad.addColorStop(1, `${currentTheme.primary}80`); 307 ctx.strokeStyle = rayGrad; 308 ctx.beginPath(); 309 for (let i = -numRays; i <= numRays; i++) { 310 const bottomX = vpX + i * 55 * 2.2; 311 ctx.moveTo(vpX, vpY); 312 ctx.lineTo(bottomX, height); 313 } 314 ctx.stroke(); 315 for (let yStep = 0; yStep < 12; yStep++) { 316 const progress = (yStep * 36 + gridOffset) % 432 / 432; 317 const depthY = horizonY + Math.pow(progress, 2.6) * (height - horizonY); 318 const alpha = Math.min(1, Math.pow(progress, 1.4)); 319 ctx.strokeStyle = currentTheme.gridLineColor.replace("0.25", `${alpha * 0.55}`).replace("0.22", `${alpha * 0.55}`); 320 ctx.lineWidth = 1 + progress * 1.5; 321 ctx.beginPath(); 322 ctx.moveTo(0, depthY); 323 ctx.lineTo(width, depthY); 324 ctx.stroke(); 325 } 326 ctx.restore(); 327 const laserGrad = ctx.createLinearGradient(0, 0, width, 0); 328 laserGrad.addColorStop(0, "transparent"); 329 laserGrad.addColorStop(0.2, currentTheme.primary); 330 laserGrad.addColorStop(0.5, currentTheme.secondary); 331 laserGrad.addColorStop(0.8, currentTheme.primary); 332 laserGrad.addColorStop(1, "transparent"); 333 ctx.strokeStyle = laserGrad; 334 ctx.lineWidth = 2; 335 ctx.beginPath(); 336 ctx.moveTo(0, horizonY); 337 ctx.lineTo(width, horizonY); 338 ctx.stroke(); 339 const pColors = currentTheme.particleColors; 340 for (const p of particles) { 341 p.y += p.speedY; 342 if (p.y < 0) { 343 p.y = height; 344 p.x = Math.random() * width; 345 }
346 ctx.fillStyle = pColors[p.colorIndex % pColors.length]; 347 ctx.globalAlpha = p.alpha; 348 ctx.fillRect(p.x, p.y, p.size, p.size); 349 } 350 ctx.globalAlpha = 1; 351 animationFrameId = requestAnimationFrame(render); 352 }; 353 animationFrameId = requestAnimationFrame(render); 354 return () => { 355 window.removeEventListener("resize", handleResize); 356 cancelAnimationFrame(animationFrameId); 357 }; 358 }, []); 359 if (!isActive) { 360 return null; 361 } 362 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 pointer-events-none z-0 overflow-hidden", children: [ 363 /* @__PURE__ */ jsxRuntimeExports.jsx("canvas", { ref: canvasRef, className: "absolute inset-0 w-full h-full will-change-transform", style: { transform: "translateZ(0)" } }), 364 /* @__PURE__ */ jsxRuntimeExports.jsx( 365 "div", 366 { 367 className: "absolute top-0 right-0 h-full pointer-events-none transition-all duration-300", 368 style: { 369 width: "52%", 370 backgroundImage: theme.wheelHalftoneType === "matrix" ? "linear-gradient(rgba(34, 197, 94, 0.12) 2px, transparent 2px), linear-gradient(90deg, rgba(34, 197, 94, 0.12) 2px, transparent 2px)" : theme.wheelHalftoneType === "sunset" ? "radial-gradient(circle, rgba(15, 5, 5, 0.72) 2.2px, transparent 2.3px)" : "radial-gradient(circle, rgba(0, 240, 255, 0.18) 2px, transparent 2.2px)", 371 backgroundSize: theme.wheelHalftoneType === "matrix" ? "15px 15px" : "14px 14px" 372 } 373 } 374 ), 375 enableScanlines && /* @__PURE__ */ jsxRuntimeExports.jsx( 376 "div", 377 { 378 className: "absolute inset-0 opacity-[0.07] pointer-events-none", 379 style: { 380 backgroundImage: "repeating-linear-gradient(0deg, #000, #000 2px, transparent 2px, transparent 4px)", 381 backgroundSize: "100% 4px" 382 } 383 } 384 ), 385 /* @__PURE__ */ jsxRuntimeExports.jsx( 386 "div", 387 { 388 className: "absolute inset-0 pointer-events-none", 389 style: { 390 background: "radial-gradient(circle at center, transparent 40%, rgba(3, 7, 18, 0.75) 100%)" 391 } 392 } 393 ) 394 ] }); 395}; 396class ArcadeAudioEngine { 397 constructor() { 398 this.ctx = null; 399 this.masterGain = null; 400 this.bgmGain = null; 401 this.sfxGain = null; 402 this.isBgmPlaying = false; 403 this.isBgmPaused = false; 404 this.isBgmAllowed = false; 405 this.bgmInterval = null; 406 this.currentBgmStep = 0; 407 this.bgmVolume = 0.25; 408 this.sfxVolume = 0.6; 409 this.isMuted = false; 410 } 411 enableBgm() { 412 this.isBgmAllowed = true; 413 } 414 initContext() { 415 if (!this.ctx && typeof window !== "undefined") { 416 const AudioContextClass = window.AudioContext || window.webkitAudioContext; 417 if (AudioContextClass) { 418 this.ctx = new AudioContextClass(); 419 this.masterGain = this.ctx.createGain(); 420 this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : 1, this.ctx.currentTime); 421 this.masterGain.connect(this.ctx.destination); 422 this.bgmGain = this.ctx.createGain(); 423 this.bgmGain.gain.setValueAtTime(this.bgmVolume, this.ctx.currentTime); 424 this.bgmGain.connect(this.masterGain); 425 this.sfxGain = this.ctx.createGain(); 426 this.sfxGain.gain.setValueAtTime(this.sfxVolume, this.ctx.currentTime); 427 this.sfxGain.connect(this.masterGain); 428 } 429 } 430 if (this.ctx && this.ctx.state === "suspended") { 431 this.ctx.resume().catch(() => { 432 }); 433 } 434 } 435 getAudioContext() { 436 this.initContext(); 437 return this.ctx; 438 } 439 // ========================================== 440 // SOUND EFFECTS (SFX) 441 // ========================================== 442 /** 443 * Classic Arcade Double-Tone Coin Drop (B5 -> E6) 444 */ 445 playCoin() { 446 try { 447 this.initContext(); 448 if (!this.ctx || !this.sfxGain || this.isMuted) return; 449 const t = this.ctx.currentTime; 450 const osc = this.ctx.createOscillator(); 451 const gain = this.ctx.createGain(); 452 osc.type = "sine"; 453 osc.frequency.setValueAtTime(987.77, t); 454 osc.frequency.setValueAtTime(1318.51, t + 0.08); 455 gain.gain.setValueAtTime(1e-3, t); 456 gain.gain.linearRampToValueAtTime(0.4, t + 0.01); 457 gain.gain.setValueAtTime(0.4, t + 0.08); 458 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.45); 459 osc.connect(gain); 460 gain.connect(this.sfxGain); 461 osc.start(t); 462 osc.stop(t + 0.46); 463 } catch (e) { 464 } 465 } 466 /** 467 * Snappy Arcade Menu Select Blip 468 */ 469 playSelect() { 470 try { 471 this.initContext(); 472 if (!this.ctx || !this.sfxGain || this.isMuted) return; 473 const t = this.ctx.currentTime; 474 const osc = this.ctx.createOscillator(); 475 const gain = this.ctx.createGain(); 476 osc.type = "triangle"; 477 osc.frequency.setValueAtTime(440, t); 478 osc.frequency.exponentialRampToValueAtTime(880, t + 0.06); 479 gain.gain.setValueAtTime(0.3, t); 480 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.08); 481 osc.connect(gain); 482 gain.connect(this.sfxGain); 483 osc.start(t); 484 osc.stop(t + 0.09); 485 } catch (e) { 486 } 487 } 488 /** 489 * Arcade Confirmation / Decide Tone 490 */ 491 playDecide() { 492 this.playCoin(); 493 } 494 /** 495 * Subtle Hover Click 496 */ 497 playHover() { 498 try { 499 this.initContext(); 500 if (!this.ctx || !this.sfxGain || this.isMuted) return; 501 const t = this.ctx.currentTime; 502 const osc = this.ctx.createOscillator(); 503 const gain = this.ctx.createGain(); 504 osc.type = "sine"; 505 osc.frequency.setValueAtTime(620, t); 506 gain.gain.setValueAtTime(0.08, t); 507 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.03); 508 osc.connect(gain); 509 gain.connect(this.sfxGain); 510 osc.start(t); 511 osc.stop(t + 0.04); 512 } catch (e) { 513 } 514 } 515 /** 516 * Song Roulette Wheel Tick with variable pitch 517 */ 518 playRouletteTick(pitchMultiplier = 1) { 519 try { 520 this.initContext(); 521 if (!this.ctx || !this.sfxGain || this.isMuted) return; 522 const t = this.ctx.currentTime; 523 const osc = this.ctx.createOscillator(); 524 const gain = this.ctx.createGain(); 525 osc.type = "square"; 526 const baseFreq = 520 * pitchMultiplier; 527 osc.frequency.setValueAtTime(baseFreq, t); 528 osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.5, t + 0.03); 529 gain.gain.setValueAtTime(0.2, t); 530 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.04); 531 osc.connect(gain); 532 gain.connect(this.sfxGain); 533 osc.start(t); 534 osc.stop(t + 0.05); 535 } catch (e) { 536 } 537 } 538 /** 539 * Rapid Arcade Roulette Spin SFX Sequence 540 */ 541 playRoulette() { 542 try { 543 for (let i = 0; i < 8; i++) { 544 setTimeout(() => { 545 this.playRouletteTick(1 + i * 0.07); 546 }, i * 60); 547 } 548 } catch (e) { 549 } 550 } 551 /** 552 * Power-Up Stage Start Fanfare 553 */ 554 playStageStart() { 555 try { 556 this.initContext(); 557 if (!this.ctx || !this.sfxGain || this.isMuted) return; 558 const t = this.ctx.currentTime; 559 const notes = [261.63, 329.63, 392, 523.25, 659.25, 783.99, 1046.5];
560 notes.forEach((freq, idx) => { 561 const noteTime = t + idx * 0.045; 562 const osc = this.ctx.createOscillator(); 563 const gain = this.ctx.createGain(); 564 osc.type = "sawtooth"; 565 osc.frequency.setValueAtTime(freq, noteTime); 566 gain.gain.setValueAtTime(1e-3, noteTime); 567 gain.gain.linearRampToValueAtTime(0.25, noteTime + 0.01); 568 gain.gain.exponentialRampToValueAtTime(1e-3, noteTime + (idx === notes.length - 1 ? 0.6 : 0.2)); 569 osc.connect(gain); 570 gain.connect(this.sfxGain); 571 osc.start(noteTime); 572 osc.stop(noteTime + (idx === notes.length - 1 ? 0.65 : 0.25)); 573 }); 574 } catch (e) { 575 } 576 } 577 /** 578 * Classic Arcade Error / Invalid Action Low Buzzer 579 */ 580 playBuzzer() { 581 try { 582 this.initContext(); 583 if (!this.ctx || !this.sfxGain || this.isMuted) return; 584 const t = this.ctx.currentTime; 585 const osc = this.ctx.createOscillator(); 586 const gain = this.ctx.createGain(); 587 osc.type = "sawtooth"; 588 osc.frequency.setValueAtTime(140, t); 589 osc.frequency.setValueAtTime(120, t + 0.08); 590 gain.gain.setValueAtTime(0.2, t); 591 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.22); 592 osc.connect(gain); 593 gain.connect(this.sfxGain); 594 osc.start(t); 595 osc.stop(t + 0.23); 596 } catch (e) { 597 } 598 } 599 /** 600 * Victory / Stage Clear Jingle 601 */ 602 playStageClear() { 603 try { 604 this.initContext(); 605 if (!this.ctx || !this.sfxGain || this.isMuted) return; 606 const t = this.ctx.currentTime; 607 const chords = [ 608 { time: 0, freqs: [392, 493.88, 587.33] }, 609 // G 610 { time: 0.18, freqs: [392, 493.88, 587.33] }, 611 { time: 0.36, freqs: [440, 554.37, 659.25] }, 612 // A 613 { time: 0.6, freqs: [523.25, 659.25, 783.99, 1046.5] } 614 // C high 615 ]; 616 chords.forEach((chord) => { 617 const noteTime = t + chord.time; 618 chord.freqs.forEach((freq) => { 619 const osc = this.ctx.createOscillator(); 620 const gain = this.ctx.createGain(); 621 osc.type = "triangle"; 622 osc.frequency.setValueAtTime(freq, noteTime); 623 gain.gain.setValueAtTime(1e-3, noteTime); 624 gain.gain.linearRampToValueAtTime(0.2, noteTime + 0.02); 625 gain.gain.exponentialRampToValueAtTime(1e-3, noteTime + 0.4); 626 osc.connect(gain); 627 gain.connect(this.sfxGain); 628 osc.start(noteTime); 629 osc.stop(noteTime + 0.42); 630 }); 631 }); 632 } catch (e) { 633 } 634 } 635 // ========================================== 636 // PROCEDURAL ARCADE BACKGROUND MUSIC (BGM) 637 // ========================================== 638 /** 639 * Catchy 16-bit Synthwave / Chiptune Arcade Loop (128 BPM) 640 * Melody + Bassline + Percussion synthesize continuously. 641 */ 642 startBgm() { 643 if (!this.isBgmAllowed) return; 644 if (this.isBgmPlaying) return; 645 this.initContext(); 646 if (!this.ctx || !this.bgmGain) return; 647 try { 648 this.bgmGain.gain.cancelScheduledValues(this.ctx.currentTime); 649 this.bgmGain.gain.setValueAtTime(this.isMuted ? 0 : this.bgmVolume, this.ctx.currentTime); 650 } catch (e) { 651 } 652 this.isBgmPlaying = true; 653 this.isBgmPaused = false; 654 this.currentBgmStep = 0; 655 const stepTimeMs = 6e4 / 128 / 4; 656 const bassLine = [ 657 // Bar 1: Am 658 220, 659 0, 660 220, 661 0, 662 220, 663 0, 664 330, 665 0, 666 220, 667 0, 668 220, 669 0, 670 220, 671 330, 672 220, 673 0, 674 // Bar 2: F 675 174.61, 676 0, 677 174.61, 678 0, 679 174.61, 680 0, 681 261.63, 682 0, 683 174.61, 684 0, 685 174.61, 686 0, 687 174.61, 688 261.63, 689 174.61, 690 0, 691 // Bar 3: C 692 261.63, 693 0, 694 261.63, 695 0, 696 261.63, 697 0, 698 392, 699 0, 700 261.63, 701 0, 702 261.63, 703 0, 704 261.63, 705 392, 706 261.63, 707 0, 708 // Bar 4: G / Em 709 196, 710 0, 711 196, 712 0, 713 196, 714 0, 715 293.66, 716 0, 717 196, 718 0, 719 196, 720 0, 721 246.94, 722 293.66, 723 392, 724 0 725 ]; 726 const arpLine = [ 727 // Bar 1: Am arp 728 440, 729 523.25, 730 659.25, 731 880, 732 659.25, 733 523.25, 734 440, 735 523.25, 736 659.25, 737 880, 738 1046.5, 739 880, 740 659.25, 741 523.25, 742 659.25, 743 880, 744 // Bar 2: F arp 745 349.23, 746 440, 747 523.25, 748 698.46, 749 523.25, 750 440, 751 349.23, 752 440, 753 523.25, 754 698.46, 755 880, 756 698.46, 757 523.25, 758 440, 759 523.25, 760 698.46, 761 // Bar 3: C arp 762 523.25, 763 659.25, 764 783.99, 765 1046.5, 766 783.99, 767 659.25, 768 523.25, 769 659.25, 770 783.99, 771 1046.5, 772 1318.5, 773 1046.5, 774 783.99, 775 659.25, 776 783.99, 777 1046.5, 778 // Bar 4: G arp 779 392, 780 493.88, 781 587.33, 782 783.99, 783 587.33, 784 493.88, 785 392, 786 493.88, 787 587.33, 788 783.99, 789 987.77, 790 783.99, 791 587.33, 792 493.88, 793 587.33, 794 783.99 795 ]; 796 this.bgmInterval = setInterval(() => { 797 if (!this.ctx || !this.bgmGain || this.isBgmPaused || this.isMuted) return; 798 const t = this.ctx.currentTime + 0.02; 799 const step = this.currentBgmStep % 64; 800 const arpFreq = arpLine[step]; 801 if (arpFreq) { 802 const osc = this.ctx.createOscillator(); 803 const filter = this.ctx.createBiquadFilter(); 804 const noteGain = this.ctx.createGain(); 805 osc.type = "square"; 806 osc.frequency.setValueAtTime(arpFreq, t); 807 filter.type = "lowpass"; 808 filter.frequency.setValueAtTime(2800, t); 809 filter.frequency.exponentialRampToValueAtTime(800, t + 0.1); 810 noteGain.gain.setValueAtTime(0.07, t); 811 noteGain.gain.exponentialRampToValueAtTime(1e-3, t + 0.1); 812 osc.connect(filter); 813 filter.connect(noteGain); 814 noteGain.connect(this.bgmGain); 815 osc.start(t); 816 osc.stop(t + 0.11); 817 } 818 const bassFreq = bassLine[step]; 819 if (bassFreq) { 820 const bOsc = this.ctx.createOscillator(); 821 const bGain = this.ctx.createGain(); 822 bOsc.type = "sawtooth"; 823 bOsc.frequency.setValueAtTime(bassFreq / 2, t); 824 bGain.gain.setValueAtTime(0.12, t); 825 bGain.gain.exponentialRampToValueAtTime(1e-3, t + 0.12); 826 bOsc.connect(bGain); 827 bGain.connect(this.bgmGain); 828 bOsc.start(t); 829 bOsc.stop(t + 0.13); 830 } 831 if (step % 2 === 0) { 832 this.synthesizeChiptuneNoise(t, 0.03, step % 8 === 4 ? 0.08 : 0.04); 833 } 834 if (step % 8 === 0) { 835 this.synthesizeRetroKick(t); 836 } else if (step % 8 === 4) { 837 this.synthesizeRetroSnare(t); 838 } 839 this.currentBgmStep++; 840 }, stepTimeMs); 841 } 842 synthesizeChiptuneNoise(t, duration, vol) { 843 if (!this.ctx || !this.bgmGain) return; 844 try { 845 const bufferSize = this.ctx.sampleRate * duration; 846 const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); 847 const data = buffer.getChannelData(0); 848 for (let i = 0; i < bufferSize; i++) { 849 data[i] = Math.random() * 2 - 1; 850 } 851 const noise = this.ctx.createBufferSource(); 852 noise.buffer = buffer; 853 const filter = this.ctx.createBiquadFilter(); 854 filter.type = "highpass"; 855 filter.frequency.setValueAtTime(7e3, t); 856 const gain = this.ctx.createGain(); 857 gain.gain.setValueAtTime(vol, t); 858 gain.gain.exponentialRampToValueAtTime(1e-3, t + duration); 859 noise.connect(filter); 860 filter.connect(gain); 861 gain.connect(this.bgmGain); 862 noise.start(t); 863 noise.stop(t + duration); 864 } catch (e) { 865 } 866 } 867 synthesizeRetroKick(t) { 868 if (!this.ctx || !this.bgmGain) return; 869 try { 870 const osc = this.ctx.createOscillator(); 871 const gain = this.ctx.createGain(); 872 osc.frequency.setValueAtTime(140, t); 873 osc.frequency.exponentialRampToValueAtTime(35, t + 0.08); 874 gain.gain.setValueAtTime(0.2, t); 875 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.1); 876 osc.connect(gain); 877 gain.connect(this.bgmGain); 878 osc.start(t); 879 osc.stop(t + 0.11); 880 } catch (e) { 881 } 882 } 883 synthesizeRetroSnare(t) { 884 this.synthesizeChiptuneNoise(t, 0.08, 0.12); 885 } 886 stopBgm() { 887 this.isBgmAllowed = false;
888 if (this.bgmInterval) { 889 clearInterval(this.bgmInterval); 890 this.bgmInterval = null; 891 } 892 this.isBgmPlaying = false; 893 this.isBgmPaused = false; 894 this.currentBgmStep = 0; 895 if (this.bgmGain && this.ctx) { 896 try { 897 this.bgmGain.gain.cancelScheduledValues(this.ctx.currentTime); 898 this.bgmGain.gain.setValueAtTime(0, this.ctx.currentTime); 899 } catch (e) { 900 } 901 } 902 } 903 pauseBgm() { 904 this.isBgmPaused = true; 905 } 906 resumeBgm() { 907 if (!this.isBgmPlaying) { 908 this.startBgm(); 909 } else { 910 this.isBgmPaused = false; 911 } 912 } 913 isPlaying() { 914 return this.isBgmPlaying && !this.isBgmPaused && !this.isMuted; 915 } 916 setBgmVolume(volume) { 917 this.bgmVolume = Math.max(0, Math.min(1, volume)); 918 if (this.bgmGain && this.ctx) { 919 this.bgmGain.gain.setValueAtTime(this.bgmVolume, this.ctx.currentTime); 920 } 921 } 922 getBgmVolume() { 923 return this.bgmVolume; 924 } 925 toggleMute() { 926 this.setMuted(!this.isMuted); 927 return this.isMuted; 928 } 929 setMuted(muted) { 930 this.isMuted = muted; 931 if (this.masterGain && this.ctx) { 932 this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : 1, this.ctx.currentTime); 933 } 934 } 935 /** 936 * Mechanical Ratchet Wheel Tick for carousel and keyboard navigation 937 */ 938 playWheelTick() { 939 try { 940 this.initContext(); 941 if (!this.ctx || !this.sfxGain || this.isMuted) return; 942 const t = this.ctx.currentTime; 943 const osc = this.ctx.createOscillator(); 944 const gain = this.ctx.createGain(); 945 osc.type = "triangle"; 946 osc.frequency.setValueAtTime(360, t); 947 osc.frequency.exponentialRampToValueAtTime(180, t + 0.025); 948 gain.gain.setValueAtTime(0.18, t); 949 gain.gain.exponentialRampToValueAtTime(1e-3, t + 0.03); 950 osc.connect(gain); 951 gain.connect(this.sfxGain); 952 osc.start(t); 953 osc.stop(t + 0.035); 954 } catch (e) { 955 } 956 } 957 duckBGM(ducked) { 958 if (!this.bgmGain || !this.ctx) return; 959 const target = ducked ? Math.min(0.03, this.bgmVolume * 0.12) : this.bgmVolume; 960 const t = this.ctx.currentTime; 961 this.bgmGain.gain.cancelScheduledValues(t); 962 this.bgmGain.gain.setValueAtTime(this.bgmGain.gain.value, t); 963 this.bgmGain.gain.linearRampToValueAtTime(target, t + 0.3); 964 } 965 isMuteActive() { 966 return this.isMuted; 967 } 968} 969const arcadeAudio = new ArcadeAudioEngine(); 970const ArcadeSettingsModal = ({ 971 isOpen, 972 onClose, 973 theme, 974 themeMode, 975 onSelectTheme, 976 enableScanlines, 977 setEnableScanlines, 978 isBgmActive, 979 onToggleBgm, 980 isMuted, 981 onToggleMute, 982 bgmVol, 983 onVolumeChange 984}) => { 985 const { t } = useTranslation(); 986 const modalRef = reactExports.useRef(null); 987 reactExports.useEffect(() => { 988 if (!isOpen) return; 989 const handleOutsideClick = (e) => { 990 if (modalRef.current && !modalRef.current.contains(e.target)) { 991 onClose(); 992 } 993 }; 994 const handleKeyDown = (e) => { 995 if (e.key === "Escape") { 996 e.stopPropagation(); 997 onClose(); 998 } 999 }; 1000 document.addEventListener("mousedown", handleOutsideClick); 1001 window.addEventListener("keydown", handleKeyDown); 1002 return () => { 1003 document.removeEventListener("mousedown", handleOutsideClick); 1004 window.removeEventListener("keydown", handleKeyDown); 1005 }; 1006 }, [isOpen, onClose]); 1007 if (!isOpen) return null; 1008 const THEME_OPTIONS = [ 1009 { id: "random", name: "RANDOM", subtitle: "Auto-Cycle Styles", color: "#ec4899" }, 1010 { id: "5thmix", name: "CYBER 5thMIX", subtitle: "DDR 5thMIX / 4thMIX", color: "#00f0ff" }, 1011 { id: "max2", name: "MATRIX TECH", subtitle: "DDR MAX2 Cyber", color: "#22c55e" }, 1012 { id: "sunset", name: "SUNSET RETRO", subtitle: "Outrun 80s Synthwave", color: "#f97316" } 1013 ]; 1014 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1015 "div", 1016 { 1017 ref: modalRef, 1018 className: "absolute right-0 top-full mt-2 w-80 sm:w-96 rounded-2xl bg-[#090d19]/95 backdrop-blur-xl border-2 shadow-[0_20px_60px_rgba(0,0,0,0.9)] z-50 p-4 sm:p-5 font-mono text-white animate-in fade-in zoom-in-95 duration-150 select-none", 1019 style: { borderColor: `${theme.primary}88`, boxShadow: `0 10px 40px -10px ${theme.primary}33` }, 1020 onClick: (e) => e.stopPropagation(), 1021 children: [
1022 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-3 border-b border-white/10 mb-4", children: [ 1023 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1024 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 15, style: { color: theme.primary } }), 1025 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-extrabold text-xs tracking-wider uppercase text-white", children: t("gameCenter.settings", "ARCADE SETTINGS") }) 1026 ] }), 1027 /* @__PURE__ */ jsxRuntimeExports.jsx( 1028 "button", 1029 { 1030 onClick: () => { 1031 arcadeAudio.playSelect(); 1032 onClose(); 1033 }, 1034 className: "p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 transition-colors cursor-pointer", 1035 title: "Close Settings [ESC]", 1036 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 15 }) 1037 } 1038 ) 1039 ] }),
1040 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 1041 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2.5", children: [ 1042 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] font-black uppercase tracking-wider text-zinc-400 flex items-center gap-1.5", children: [ 1043 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 11, style: { color: theme.primary } }), 1044 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.audioSettings", "AUDIO & BGM") }) 1045 ] }), 1046 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-2.5 rounded-xl bg-zinc-900/60 border border-white/5", children: [ 1047 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 1048 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-200", children: t("gameCenter.arcadeBgm", "Arcade BGM") }),
1049 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500", children: "16-Bit Chiptune Synth" }) 1050 ] }), 1051 /* @__PURE__ */ jsxRuntimeExports.jsx( 1052 "button", 1053 { 1054 type: "button", 1055 onClick: () => { 1056 arcadeAudio.playSelect(); 1057 onToggleBgm(); 1058 }, 1059 className: `px-3 py-1 rounded-lg text-xs font-black transition-all cursor-pointer border ${isBgmActive && !isMuted ? "bg-cyan-500/20 text-cyan-300 border-cyan-400/60 shadow-[0_0_12px_rgba(6,182,212,0.3)]" : "bg-zinc-800 text-zinc-400 border-white/10 hover:text-white"}`, 1060 children: isBgmActive && !isMuted ? "ON" : "OFF" 1061 } 1062 ) 1063 ] }), 1064 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2.5 rounded-xl bg-zinc-900/60 border border-white/5 space-y-2", children: [ 1065 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
1066 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 font-bold", children: t("gameCenter.bgmVolume", "BGM Volume") }), 1067 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-cyan-400 font-black", children: [ 1068 Math.round(bgmVol), 1069 "%" 1070 ] }) 1071 ] }), 1072 /* @__PURE__ */ jsxRuntimeExports.jsx( 1073 "input", 1074 { 1075 type: "range", 1076 min: "0", 1077 max: "100", 1078 value: bgmVol, 1079 onChange: (e) => onVolumeChange(Number(e.target.value)), 1080 className: "w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-cyan-400" 1081 } 1082 ) 1083 ] }), 1084 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-2.5 rounded-xl bg-zinc-900/60 border border-white/5", children: [ 1085 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1086 isMuted ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 14, className: "text-red-400" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 14, className: "text-emerald-400" }),
1087 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 1088 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-200", children: t("gameCenter.masterMute", "Master Sound") }), 1089 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500", children: "SFX & Game Audio" }) 1090 ] }) 1091 ] }), 1092 /* @__PURE__ */ jsxRuntimeExports.jsx( 1093 "button", 1094 { 1095 type: "button", 1096 onClick: () => { 1097 arcadeAudio.playSelect(); 1098 onToggleMute(); 1099 }, 1100 className: `px-3 py-1 rounded-lg text-xs font-black transition-all cursor-pointer border ${isMuted ? "bg-red-950/70 text-red-300 border-red-500/60 shadow-[0_0_12px_rgba(239,68,68,0.3)]" : "bg-emerald-950/60 text-emerald-300 border-emerald-500/50"}`, 1101 children: isMuted ? "MUTED" : "ACTIVE" 1102 } 1103 ) 1104 ] }) 1105 ] }),
1106 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2.5 pt-1 border-t border-white/10", children: [ 1107 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 1108 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] font-black uppercase tracking-wider text-zinc-400 flex items-center gap-1.5", children: [ 1109 /* @__PURE__ */ jsxRuntimeExports.jsx(Palette, { size: 11, style: { color: theme.primary } }), 1110 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.cabinetStyle", "CABINET STYLE") }) 1111 ] }), 1112 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] px-1.5 py-0.5 rounded bg-zinc-800 text-zinc-400 border border-white/10 font-bold", children: "Shortcut: [T]" }) 1113 ] }), 1114 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 gap-1.5", children: THEME_OPTIONS.map((opt) => { 1115 const isSelected = themeMode === opt.id; 1116 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1117 "button", 1118 { 1119 type: "button", 1120 onClick: () => { 1121 arcadeAudio.playWheelTick(); 1122 onSelectTheme(opt.id); 1123 }, 1124 className: `p-2 rounded-xl text-left transition-all border flex flex-col justify-between cursor-pointer ${isSelected ? "bg-zinc-800/90 border-2 shadow-sm" : "bg-zinc-900/50 border-white/10 hover:bg-zinc-800/60 hover:border-white/20"}`, 1125 style: isSelected ? { borderColor: opt.color } : {}, 1126 children: [
1127 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1", children: [ 1128 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 1129 opt.id === "random" ? /* @__PURE__ */ jsxRuntimeExports.jsx(Dices, { size: 12, className: "text-pink-400 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 12, style: { color: opt.color } }), 1130 /* @__PURE__ */ jsxRuntimeExports.jsx( 1131 "span", 1132 { 1133 className: "text-[11px] font-black tracking-tight", 1134 style: isSelected ? { color: opt.color } : { color: "#e4e4e7" }, 1135 children: opt.name 1136 } 1137 ) 1138 ] }), 1139 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12, style: { color: opt.color } }) 1140 ] }),
1141 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] text-zinc-500 font-normal leading-tight", children: opt.subtitle }) 1142 ] 1143 }, 1144 opt.id 1145 ); 1146 }) }) 1147 ] }), 1148 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2 pt-1 border-t border-white/10", children: [ 1149 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] font-black uppercase tracking-wider text-zinc-400 flex items-center gap-1.5", children: [ 1150 /* @__PURE__ */ jsxRuntimeExports.jsx(Tv, { size: 11, style: { color: theme.primary } }), 1151 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.displayFilters", "DISPLAY FILTERS") }) 1152 ] }), 1153 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-2.5 rounded-xl bg-zinc-900/60 border border-white/5", children: [ 1154 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [
1155 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-200", children: t("gameCenter.crtScanlines", "CRT Scanlines") }), 1156 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500", children: "Phosphor Raster Overlay" }) 1157 ] }), 1158 /* @__PURE__ */ jsxRuntimeExports.jsx( 1159 "button", 1160 { 1161 type: "button", 1162 onClick: () => { 1163 arcadeAudio.playSelect(); 1164 setEnableScanlines(!enableScanlines); 1165 }, 1166 className: `px-3 py-1 rounded-lg text-xs font-black transition-all cursor-pointer border ${enableScanlines ? "bg-purple-950/70 text-purple-300 border-purple-500/60 shadow-[0_0_12px_rgba(168,85,247,0.3)]" : "bg-zinc-800 text-zinc-400 border-white/10 hover:text-white"}`, 1167 children: enableScanlines ? "ON" : "OFF" 1168 } 1169 ) 1170 ] }) 1171 ] }) 1172 ] }),
1173 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 pt-2.5 border-t border-white/10 flex items-center justify-between text-[9px] text-zinc-500", children: [ 1174 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "[T] Cycle Style ⢠[M] Mute" }), 1175 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "[ESC] Close" }) 1176 ] }) 1177 ] 1178 } 1179 ); 1180}; 1181const GAME_MODULES = [ 1182 { 1183 id: "piano-roll", 1184 name: "Piano Roll Rush", 1185 japaneseName: "ãã¢ãã»ãã¼ã«ã»ã©ãã·ã¥", 1186 tagline: "High-speed falling note keyboard action", 1187 description: "Test your reflexes with falling notes! Connect your MIDI keyboard or jam on your computer keys. Features accurate timing judgements, combo streaks, and soundfont synthesis.", 1188 badge: "RHYTHM SYNTH", 1189 icon: "Keyboard", 1190 themeColor: { 1191 primary: "#06b6d4", 1192 // cyan-500 1193 accent: "#8b5cf6", 1194 // purple-500 1195 glow: "rgba(6, 182, 212, 0.4)", 1196 gradient: "from-cyan-500 via-indigo-500 to-purple-600", 1197 border: "border-cyan-500/50" 1198 }, 1199 cabinetStyle: { 1200 headerBg: "bg-gradient-to-r from-cyan-950 via-indigo-950 to-purple-950", 1201 marqueeText: "HAMIE KEYS", 1202 bezelColor: "#0891b2" 1203 }, 1204 supportedInputs: ["keyboard", "midi_device", "mouse", "touch"], 1205 minPlayers: 1, 1206 maxPlayers: 1, 1207 enabled: true, 1208 defaultSettings: { 1209 speed: 1.2, 1210 playMelodyTrackOnly: false 1211 } 1212 }, 1213 { 1214 id: "taiko", 1215 name: "Taiko no Hamie", 1216 japaneseName: "太é¼ã®ããã¼", 1217 tagline: "Authentic Japanese festival drumming excitement", 1218 description: "Feel the rhythm of the festival! Hit the center (Don) and the rim (Ka) in sync with the music. Features 1 to 4 local players on the same keyboard, Hamie mascot animations, and authentic drum synthesis.", 1219 badge: "ARCADE DRUM", 1220 icon: "Drum", 1221 themeColor: { 1222 primary: "#f43f5e", 1223 // rose-500 1224 accent: "#f59e0b", 1225 // amber-500 1226 glow: "rgba(244, 63, 94, 0.4)", 1227 gradient: "from-rose-500 via-orange-500 to-amber-500", 1228 border: "border-rose-500/50" 1229 }, 1230 cabinetStyle: { 1231 headerBg: "bg-gradient-to-r from-red-950 via-rose-950 to-amber-950", 1232 marqueeText: "TAIKO NO HAMIE", 1233 bezelColor: "#e11d48" 1234 }, 1235 supportedInputs: ["keyboard", "touch"], 1236 minPlayers: 1, 1237 maxPlayers: 4, 1238 enabled: true, 1239 defaultSettings: { 1240 difficulty: "futsuu" 1241 // kantan, futsuu, muzukashii, oni 1242 } 1243 }, 1244 { 1245 id: "meow-choir", 1246 name: "Meow Choir", 1247 japaneseName: "ã«ãããåå±å£", 1248 tagline: "Kitten stage chorus rhythm adventure", 1249 description: "Lead the kitten chorus around the center stage microphone! Pop musical note bubbles over the piano keys to harmonize sweet meows in perfect tune.", 1250 badge: "CHOIR", 1251 icon: "Cat", 1252 themeColor: { 1253 primary: "#a855f7", 1254 // purple-500 1255 accent: "#f43f5e", 1256 // rose-500 1257 glow: "rgba(168, 85, 247, 0.4)", 1258 gradient: "from-purple-500 via-pink-500 to-rose-400", 1259 border: "border-purple-500/50" 1260 }, 1261 cabinetStyle: { 1262 headerBg: "bg-gradient-to-r from-indigo-950 via-purple-950 to-pink-950", 1263 marqueeText: "MEOW CHOIR", 1264 bezelColor: "#9333ea" 1265 }, 1266 supportedInputs: ["keyboard", "touch", "mouse"], 1267 minPlayers: 1, 1268 maxPlayers: 1, 1269 enabled: true, 1270 defaultSettings: { 1271 difficulty: "playful", 1272 speed: 1.15 1273 } 1274 }, 1275 { 1276 id: "rhythm-test", 1277 name: "Rhythm Test", 1278 japaneseName: "ãªãºã ãã¹ã", 1279 tagline: "Measure beat pattern assessment & training", 1280 description: "Test your rhythm sense with 4x4 measure grids! Tap along to beat patterns with keyboard, fingers, or clap into your microphone with Bongo Cat.", 1281 badge: "RHYTHM TEST", 1282 icon: "Boxes", 1283 themeColor: { 1284 primary: "#9333ea", 1285 // purple-600 1286 accent: "#ea580c", 1287 // orange-600 1288 glow: "rgba(147, 51, 234, 0.4)", 1289 gradient: "from-purple-600 via-pink-600 to-amber-500", 1290 border: "border-purple-500/50" 1291 }, 1292 cabinetStyle: { 1293 headerBg: "bg-gradient-to-r from-purple-950 via-indigo-950 to-stone-950", 1294 marqueeText: "RHYTHM TEST", 1295 bezelColor: "#7e22ce" 1296 }, 1297 supportedInputs: ["keyboard", "touch", "microphone"], 1298 minPlayers: 1, 1299 maxPlayers: 1, 1300 enabled: true, 1301 defaultSettings: { 1302 mode: "play-along", 1303 difficulty: "medium", 1304 soundType: "bongo" 1305 } 1306 } 1307]; 1308const ArcadeMarqueeHeader = ({ 1309 stage, 1310 selectedSong, 1311 selectedModule, 1312 onGoToStage, 1313 onSelectModule, 1314 onExit, 1315 enableScanlines, 1316 setEnableScanlines, 1317 theme, 1318 themeMode, 1319 onCycleTheme, 1320 onSelectTheme = () => { 1321 }, 1322 onSelectMusic 1323}) => { 1324 const { t } = useTranslation(); 1325 const [isBgmActive, setIsBgmActive] = reactExports.useState(false); 1326 const [isMuted, setIsMuted] = reactExports.useState(arcadeAudio.isMuteActive()); 1327 const [bgmVol, setBgmVol] = reactExports.useState(arcadeAudio.getBgmVolume() * 100); 1328 const [showSettings, setShowSettings] = reactExports.useState(false); 1329 reactExports.useEffect(() => { 1330 const handleFirstInteraction = (e) => { 1331 const target = e.target; 1332 if (target && typeof target.closest === "function" && target.closest("[data-exit-arcade]")) { 1333 return; 1334 } 1335 if (!arcadeAudio.isPlaying()) { 1336 arcadeAudio.startBgm(); 1337 setIsBgmActive(true); 1338 } 1339 window.removeEventListener("click", handleFirstInteraction); 1340 window.removeEventListener("keydown", handleFirstInteraction); 1341 }; 1342 window.addEventListener("click", handleFirstInteraction); 1343 window.addEventListener("keydown", handleFirstInteraction); 1344 return () => { 1345 window.removeEventListener("click", handleFirstInteraction); 1346 window.removeEventListener("keydown", handleFirstInteraction); 1347 arcadeAudio.stopBgm(); 1348 }; 1349 }, []); 1350 reactExports.useEffect(() => { 1351 const handleGlobalShortcuts = (e) => { 1352 var _a; 1353 const targetTag = (_a = e.target) == null ? void 0 : _a.tagName; 1354 if (targetTag === "INPUT" || targetTag === "TEXTAREA") return; 1355 if (e.key === "m" || e.key === "M") { 1356 const nextMute = arcadeAudio.toggleMute(); 1357 setIsMuted(nextMute); 1358 } 1359 }; 1360 window.addEventListener("keydown", handleGlobalShortcuts); 1361 return () => window.removeEventListener("keydown", handleGlobalShortcuts); 1362 }, []); 1363 const toggleBgm = () => { 1364 arcadeAudio.playSelect(); 1365 if (arcadeAudio.isPlaying()) { 1366 arcadeAudio.stopBgm(); 1367 setIsBgmActive(false); 1368 } else { 1369 arcadeAudio.startBgm(); 1370 setIsBgmActive(true); 1371 } 1372 }; 1373 const toggleMute = () => { 1374 arcadeAudio.playSelect(); 1375 const nextMute = arcadeAudio.toggleMute(); 1376 setIsMuted(nextMute); 1377 }; 1378 const handleVolumeChange = (val) => { 1379 setBgmVol(val); 1380 arcadeAudio.setBgmVolume(val / 100); 1381 }; 1382 return /* @__PURE__ */ jsxRuntimeExports.jsxs("header", { className: `relative z-30 w-full bg-gradient-to-r ${theme.marqueeGradient} bg-[#070b16] border-b-2 ${theme.marqueeBorder} shadow-lg transition-all duration-200`, children: [
1383 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-1.5 w-full bg-gradient-to-r from-pink-500 via-yellow-400 via-cyan-400 to-pink-500 animate-pulse" }), 1384 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `max-w-7xl mx-auto px-3 sm:px-4 flex items-center justify-between gap-2 sm:gap-4 transition-all ${stage === "ARENA" ? "py-1.5" : "py-2 sm:py-2.5"}`, children: [ 1385 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [ 1386 stage === "SONG" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1387 "button", 1388 { 1389 "data-exit-arcade": "true", 1390 type: "button", 1391 onClick: (e) => { 1392 e.stopPropagation(); 1393 arcadeAudio.stopBgm(); 1394 arcadeAudio.playCoin(); 1395 onExit(); 1396 }, 1397 className: "group flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-1.5 rounded-lg bg-red-950/60 hover:bg-red-900/80 border border-red-500/40 hover:border-red-400 text-red-300 hover:text-white font-mono text-xs font-bold transition-all shadow-[0_0_15px_rgba(239,68,68,0.2)] active:scale-95 cursor-pointer", 1398 title: "Return to standard HamieNET website", 1399 children: [
1400 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 14, className: "group-hover:-translate-x-0.5 transition-transform text-red-400" }), 1401 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tracking-wider uppercase", children: t("gameCenter.exitArcade", "EXIT ARCADE") }) 1402 ] 1403 } 1404 ), 1405 stage === "CABINET" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 1406 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1407 "button", 1408 { 1409 type: "button", 1410 onClick: () => { 1411 arcadeAudio.playSelect(); 1412 if (onGoToStage) onGoToStage("SONG"); 1413 else if (onSelectMusic) onSelectMusic(); 1414 }, 1415 className: "group flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-cyan-950/70 hover:bg-cyan-900/90 border border-cyan-500/40 hover:border-cyan-400 text-cyan-300 hover:text-white font-mono text-xs font-bold transition-all shadow-[0_0_15px_rgba(6,182,212,0.25)] active:scale-95 cursor-pointer", 1416 title: "Return to music selector [ESC]", 1417 children: [
1418 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 14, className: "group-hover:-translate-x-0.5 transition-transform text-cyan-400" }), 1419 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tracking-wider uppercase font-black", children: t("gameCenter.musicSelect", "MUSIC SELECT") }), 1420 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "hidden sm:inline text-[9px] px-1 py-0.5 rounded bg-cyan-950 border border-cyan-800 text-cyan-300 font-mono", children: "ESC" }) 1421 ] 1422 } 1423 ), 1424 /* @__PURE__ */ jsxRuntimeExports.jsx( 1425 "button", 1426 { 1427 "data-exit-arcade": "true", 1428 type: "button", 1429 onClick: (e) => { 1430 e.stopPropagation(); 1431 arcadeAudio.stopBgm(); 1432 arcadeAudio.playCoin(); 1433 onExit(); 1434 }, 1435 className: "p-1.5 rounded-lg bg-slate-900/60 hover:bg-red-950/60 border border-slate-700/60 hover:border-red-500/40 text-slate-400 hover:text-red-300 transition-all cursor-pointer", 1436 title: "Exit Arcade to Main Site", 1437 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 13 }) 1438 } 1439 ) 1440 ] }), 1441 stage === "ARENA" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [
1442 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1443 "button", 1444 { 1445 type: "button", 1446 onClick: () => { 1447 arcadeAudio.playSelect(); 1448 if (onGoToStage) onGoToStage("CABINET"); 1449 }, 1450 className: "group flex items-center gap-1 px-2.5 py-1 rounded-lg bg-amber-950/70 hover:bg-amber-900/90 border border-amber-500/40 hover:border-amber-400 text-amber-300 hover:text-white font-mono text-[11px] font-bold transition-all shadow-[0_0_12px_rgba(245,158,11,0.25)] active:scale-95 cursor-pointer", 1451 title: "Return to Cabinet Selection [ESC]", 1452 children: [ 1453 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 12, className: "group-hover:-translate-x-0.5 transition-transform text-amber-400" }), 1454 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tracking-wider uppercase font-black", children: t("gameCenter.cabinets", "CABINETS") }) 1455 ] 1456 } 1457 ),
1458 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1459 "button", 1460 { 1461 type: "button", 1462 onClick: () => { 1463 arcadeAudio.playSelect(); 1464 if (onGoToStage) onGoToStage("SONG"); 1465 else if (onSelectMusic) onSelectMusic(); 1466 }, 1467 className: "flex items-center gap-1 px-2 py-1 rounded-lg bg-slate-900/80 hover:bg-slate-800 border border-slate-700 hover:border-cyan-400 text-slate-300 hover:text-cyan-300 font-mono text-[11px] font-bold transition-all active:scale-95 cursor-pointer", 1468 title: "Return to Song Selection", 1469 children: [ 1470 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 12, className: "text-cyan-400" }), 1471 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "SONGS" }) 1472 ] 1473 } 1474 ) 1475 ] }) 1476 ] }), 1477 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center select-none flex-1 min-w-0 px-1 sm:px-2", children: [
1478 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 sm:gap-2 text-[10px] font-mono font-bold tracking-wider mb-1", children: [ 1479 /* @__PURE__ */ jsxRuntimeExports.jsx( 1480 "button", 1481 { 1482 type: "button", 1483 onClick: () => { 1484 arcadeAudio.playSelect(); 1485 if (onGoToStage) onGoToStage("SONG"); 1486 else if (onSelectMusic) onSelectMusic(); 1487 }, 1488 className: `px-2 sm:px-2.5 py-0.5 rounded-full flex items-center gap-1 transition-all cursor-pointer ${stage === "SONG" ? "bg-cyan-500 text-slate-950 font-black shadow-[0_0_12px_rgba(6,182,212,0.6)]" : "bg-slate-900/90 text-cyan-300 hover:text-white border border-cyan-800/60 hover:border-cyan-400 active:scale-95"}`, 1489 title: "1. Music Selection [Click to Browse Songs]", 1490 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "1. MUSIC" }) 1491 } 1492 ), 1493 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600 text-[10px]", children: "â¶" }),
1494 /* @__PURE__ */ jsxRuntimeExports.jsx( 1495 "button", 1496 { 1497 type: "button", 1498 onClick: () => { 1499 if (selectedSong && onGoToStage) { 1500 arcadeAudio.playSelect(); 1501 onGoToStage("CABINET"); 1502 } 1503 }, 1504 disabled: !selectedSong, 1505 className: `px-2 sm:px-2.5 py-0.5 rounded-full flex items-center gap-1 transition-all ${stage === "CABINET" ? "bg-amber-400 text-slate-950 font-black shadow-[0_0_12px_rgba(245,158,11,0.6)] cursor-pointer" : selectedSong ? "bg-slate-900/90 text-amber-300 hover:text-white border border-amber-800/60 hover:border-amber-400 cursor-pointer active:scale-95" : "bg-slate-950/40 text-zinc-600 border border-zinc-800/50 cursor-not-allowed opacity-50"}`, 1506 title: selectedSong ? "2. Cabinet Selection [Click to Choose Game Mode]" : "Pick a song first", 1507 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "2. CABINET" }) 1508 } 1509 ),
1510 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600 text-[10px]", children: "â¶" }), 1511 /* @__PURE__ */ jsxRuntimeExports.jsx( 1512 "button", 1513 { 1514 type: "button", 1515 onClick: () => { 1516 if (selectedSong && onGoToStage) { 1517 arcadeAudio.playStageStart(); 1518 onGoToStage("ARENA"); 1519 } 1520 }, 1521 disabled: !selectedSong, 1522 className: `px-2 sm:px-2.5 py-0.5 rounded-full flex items-center gap-1 transition-all ${stage === "ARENA" ? "bg-rose-500 text-white font-black shadow-[0_0_12px_rgba(244,63,94,0.6)] cursor-pointer animate-pulse" : selectedSong ? "bg-slate-900/90 text-rose-300 hover:text-white border border-rose-800/60 hover:border-rose-400 cursor-pointer active:scale-95" : "bg-slate-950/40 text-zinc-600 border border-zinc-800/50 cursor-not-allowed opacity-50"}`, 1523 title: selectedSong ? "3. Game Arena [Enter Stage]" : "Pick a song first", 1524 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "3. ARENA" }) 1525 } 1526 ) 1527 ] }), 1528 stage === "SONG" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1529 "button", 1530 { 1531 type: "button", 1532 onClick: () => { 1533 if (onSelectMusic) { 1534 arcadeAudio.playSelect(); 1535 onSelectMusic(); 1536 } 1537 }, 1538 className: "flex flex-col items-center cursor-pointer group select-none transition-transform active:scale-95 focus:outline-none", 1539 title: "Music Selection", 1540 children: [
1541 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `${theme.selectMusicBannerClass} flex items-center justify-center gap-2 uppercase group-hover:brightness-110 transition-all`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: theme.selectMusicTitle }) }), 1542 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-mono tracking-widest uppercase font-extrabold mt-0.5 group-hover:text-white transition-colors", style: { color: `${theme.primary}cc` }, children: theme.subtitle }) 1543 ] 1544 } 1545 ), 1546 stage === "CABINET" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 1547 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-0.5 rounded-full font-black text-xs tracking-widest uppercase bg-gradient-to-r from-amber-400 via-orange-400 to-amber-500 text-slate-950 shadow-[0_0_15px_rgba(245,158,11,0.5)] border border-yellow-200", children: "// CHOOSE GAME CABINET //" }), 1548 selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1549 "button", 1550 { 1551 type: "button", 1552 onClick: () => { 1553 arcadeAudio.playSelect(); 1554 if (onGoToStage) onGoToStage("SONG"); 1555 else if (onSelectMusic) onSelectMusic(); 1556 }, 1557 className: "flex items-center gap-1.5 mt-1 text-[10px] font-mono text-cyan-300 hover:text-white bg-slate-900/80 px-2.5 py-0.5 rounded-full border border-cyan-500/40 hover:border-cyan-400 transition-all cursor-pointer shadow-sm active:scale-95", 1558 title: "Click to change selected song [ESC]", 1559 children: [
1560 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 11, className: "text-cyan-400 animate-spin" }), 1561 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold max-w-[200px] truncate", children: selectedSong.title }), 1562 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-400", children: [ 1563 "⢠", 1564 selectedSong.bpm, 1565 " BPM" 1566 ] }), 1567 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] text-amber-400 ml-1 font-black", children: "[CHANGE SONG]" }) 1568 ] 1569 } 1570 ) 1571 ] }), 1572 stage === "ARENA" && selectedModule && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1573 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1 p-0.5 rounded-xl bg-slate-950/90 border border-slate-800 shadow-inner", children: GAME_MODULES.map((mod) => { 1574 const isActiveMod = mod.id === selectedModule.id; 1575 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1576 "button", 1577 { 1578 type: "button", 1579 onClick: () => onSelectModule && onSelectModule(mod), 1580 className: `px-2 sm:px-2.5 py-1 rounded-lg text-[10px] font-black tracking-wider uppercase transition-all flex items-center gap-1 cursor-pointer active:scale-95 ${isActiveMod ? mod.id === "piano-roll" ? "bg-cyan-500 text-slate-950 shadow-[0_0_12px_rgba(6,182,212,0.6)] font-extrabold" : mod.id === "taiko" ? "bg-rose-500 text-white shadow-[0_0_12px_rgba(244,63,94,0.6)] font-extrabold" : mod.id === "meow-choir" ? "bg-purple-500 text-slate-950 shadow-[0_0_12px_rgba(168,85,247,0.6)] font-extrabold" : "bg-amber-500 text-slate-950 shadow-[0_0_12px_rgba(245,158,11,0.6)] font-extrabold" : "text-slate-400 hover:text-white hover:bg-slate-800/60"}`, 1581 title: `Quick-switch to ${mod.name}`, 1582 children: [ 1583 mod.id === "piano-roll" && /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 12 }), 1584 mod.id === "taiko" && /* @__PURE__ */ jsxRuntimeExports.jsx(Drum, { size: 12 }), 1585 mod.id === "meow-choir" && /* @__PURE__ */ jsxRuntimeExports.jsx(PawPrint, { size: 12 }), 1586 mod.id === "rhythm-test" && /* @__PURE__ */ jsxRuntimeExports.jsx(Boxes, { size: 12 }),
1587 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden md:inline", children: mod.name }) 1588 ] 1589 }, 1590 mod.id 1591 ); 1592 }) }), 1593 selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden lg:flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-900/90 border border-cyan-500/30 text-[10px] font-mono text-cyan-300", children: [ 1594 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 11, className: "text-cyan-400 animate-spin" }), 1595 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold max-w-[140px] truncate", children: selectedSong.title }), 1596 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500", children: [ 1597 "⢠", 1598 selectedSong.bpm, 1599 " BPM" 1600 ] }) 1601 ] }) 1602 ] }) 1603 ] }), 1604 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center gap-1.5 sm:gap-2 shrink-0", children: [
1605 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1606 "button", 1607 { 1608 type: "button", 1609 onClick: (e) => { 1610 e.stopPropagation(); 1611 toggleBgm(); 1612 }, 1613 className: `flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 sm:py-1.5 rounded-xl border font-mono text-xs font-black transition-all cursor-pointer select-none active:scale-95 ${isBgmActive && !isMuted ? "bg-cyan-950/80 border-cyan-400 text-cyan-300 shadow-[0_0_12px_rgba(6,182,212,0.4)]" : "bg-slate-900/70 hover:bg-slate-800 border-zinc-700/60 text-zinc-400 hover:text-zinc-200"}`, 1614 title: isBgmActive && !isMuted ? "Mute Arcade Background Music [M]" : "Play Arcade Background Music [M]", 1615 children: [ 1616 isBgmActive && !isMuted ? /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 13, className: "text-cyan-400 animate-pulse" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 13, className: "text-zinc-500" }), 1617 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tracking-wider text-[10px] sm:text-[11px] hidden sm:inline", children: isBgmActive && !isMuted ? "BGM ON" : "BGM OFF" }) 1618 ] 1619 } 1620 ),
1621 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1622 "button", 1623 { 1624 type: "button", 1625 onClick: (e) => { 1626 e.stopPropagation(); 1627 arcadeAudio.playSelect(); 1628 setShowSettings(!showSettings); 1629 }, 1630 className: `flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 sm:py-1.5 rounded-xl border font-mono text-xs font-black transition-all cursor-pointer select-none active:scale-95 ${showSettings ? "bg-zinc-800 text-white shadow-lg" : "bg-slate-900/70 hover:bg-slate-800 text-zinc-300 hover:text-white"}`, 1631 style: { borderColor: `${theme.primary}aa`, boxShadow: showSettings ? `0 0 15px ${theme.primary}44` : void 0 }, 1632 title: "Arcade Settings [BGM, Style, Volume]", 1633 children: [ 1634 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 13, style: { color: theme.primary } }), 1635 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "tracking-wider text-[10px] sm:text-[11px] hidden sm:inline", children: t("gameCenter.settings", "SETTINGS") }) 1636 ] 1637 } 1638 ),
1639 /* @__PURE__ */ jsxRuntimeExports.jsx( 1640 ArcadeSettingsModal, 1641 { 1642 isOpen: showSettings, 1643 onClose: () => setShowSettings(false), 1644 theme, 1645 themeMode, 1646 onSelectTheme, 1647 enableScanlines, 1648 setEnableScanlines, 1649 isBgmActive, 1650 onToggleBgm: toggleBgm, 1651 isMuted, 1652 onToggleMute: toggleMute, 1653 bgmVol, 1654 onVolumeChange: handleVolumeChange 1655 } 1656 ) 1657 ] }) 1658 ] }) 1659 ] }); 1660}; 1661const ArcadeGrooveRadar = ({ 1662 stats, 1663 theme, 1664 size = 240, 1665 showLabels = true 1666}) => { 1667 const axes = reactExports.useMemo(() => [ 1668 { key: "stream", ja: "å ¨ä½å¯åº¦", en: "STREAM", value: stats.stream, angle: -Math.PI / 2 }, 1669 { key: "chaos", ja: "å¤å度", en: "CHAOS", value: stats.chaos, angle: -Math.PI / 2 + 2 * Math.PI / 5 }, 1670 { key: "freeze", ja: "è¸ã¿ã£ã±åº¦", en: "FREEZE", value: stats.freeze, angle: -Math.PI / 2 + 4 * Math.PI / 5 }, 1671 { key: "air", ja: "ã¸ã£ã³ã度", en: "AIR", value: stats.air, angle: -Math.PI / 2 + 6 * Math.PI / 5 }, 1672 { key: "voltage", ja: "æå¤§å¯åº¦", en: "VOLTAGE", value: stats.voltage, angle: -Math.PI / 2 + 8 * Math.PI / 5 } 1673 ], [stats]); 1674 const center = size / 2; 1675 const maxRadius = size / 2 * 0.68; 1676 const getPoint = (radius, angle) => { 1677 return { 1678 x: center + radius * Math.cos(angle), 1679 y: center + radius * Math.sin(angle) 1680 }; 1681 }; 1682 const getRingPath = (fraction) => { 1683 const points = axes.map((a) => getPoint(maxRadius * fraction, a.angle)); 1684 return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ") + " Z"; 1685 }; 1686 const polygonPath = reactExports.useMemo(() => { 1687 const points = axes.map((a) => { 1688 const r = Math.max(10, a.value / 100 * maxRadius); 1689 return getPoint(r, a.angle); 1690 }); 1691 return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ") + " Z"; 1692 }, [axes, maxRadius]); 1693 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex flex-col items-center justify-center select-none", style: { width: size, height: size }, children: [ 1694 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1695 "div", 1696 { 1697 className: "absolute inset-1 rounded-full border border-zinc-800/80 bg-zinc-950/70 shadow-inner overflow-hidden pointer-events-none", 1698 style: { 1699 boxShadow: `inset 0 0 25px rgba(0,0,0,0.8), 0 0 15px ${theme.primary}18` 1700 }, 1701 children: [
1702 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 opacity-20 bg-[radial-gradient(#ffffff_1px,transparent_1px)] [background-size:12px_12px]" }), 1703 /* @__PURE__ */ jsxRuntimeExports.jsx( 1704 "div", 1705 { 1706 className: "absolute inset-3 rounded-full border border-dashed opacity-25", 1707 style: { borderColor: theme.radarGridStroke } 1708 } 1709 ) 1710 ] 1711 } 1712 ), 1713 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1714 "svg", 1715 { 1716 width: size, 1717 height: size, 1718 viewBox: `0 0 ${size} ${size}`, 1719 className: "relative z-10 overflow-visible", 1720 children: [ 1721 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [ 1722 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: `radarGlow-${theme.id}`, cx: "50%", cy: "50%", r: "50%", children: [ 1723 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: theme.radarPolygonStroke, stopOpacity: "0.6" }),
1724 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: theme.primary, stopOpacity: "0.15" }) 1725 ] }), 1726 /* @__PURE__ */ jsxRuntimeExports.jsx("filter", { id: `polygonGlow-${theme.id}`, x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsxRuntimeExports.jsx("feDropShadow", { dx: "0", dy: "0", stdDeviation: "4", floodColor: theme.radarPolygonStroke, floodOpacity: "0.7" }) }) 1727 ] }), 1728 [0.25, 0.5, 0.75, 1].map((frac, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 1729 "path", 1730 { 1731 d: getRingPath(frac), 1732 fill: "none", 1733 stroke: theme.radarGridStroke, 1734 strokeWidth: i === 3 ? 1.5 : 0.75, 1735 strokeDasharray: i < 3 ? "2 2" : "none" 1736 }, 1737 i 1738 )), 1739 axes.map((a, i) => { 1740 const p = getPoint(maxRadius, a.angle); 1741 return /* @__PURE__ */ jsxRuntimeExports.jsx( 1742 "line", 1743 { 1744 x1: center, 1745 y1: center, 1746 x2: p.x, 1747 y2: p.y, 1748 stroke: theme.radarGridStroke, 1749 strokeWidth: 0.8 1750 }, 1751 i 1752 ); 1753 }),
1754 /* @__PURE__ */ jsxRuntimeExports.jsx( 1755 "path", 1756 { 1757 d: polygonPath, 1758 fill: `url(#radarGlow-${theme.id})`, 1759 stroke: theme.radarPolygonStroke, 1760 strokeWidth: 2, 1761 filter: `url(#polygonGlow-${theme.id})`, 1762 className: "transition-all duration-500 ease-out" 1763 } 1764 ), 1765 axes.map((a, i) => { 1766 const r = Math.max(10, a.value / 100 * maxRadius); 1767 const p = getPoint(r, a.angle); 1768 return /* @__PURE__ */ jsxRuntimeExports.jsx( 1769 "circle", 1770 { 1771 cx: p.x, 1772 cy: p.y, 1773 r: 3, 1774 fill: theme.secondary, 1775 stroke: "#000", 1776 strokeWidth: 1, 1777 className: "transition-all duration-500 ease-out" 1778 }, 1779 i 1780 ); 1781 }) 1782 ] 1783 } 1784 ), 1785 showLabels && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 pointer-events-none text-[9px] font-mono leading-tight", children: [
1786 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-1 left-1/2 -translate-x-1/2 text-center", children: [ 1787 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold tracking-tight text-white/90 drop-shadow-[0_1px_2px_rgba(0,0,0,1)]", children: "å ¨ä½å¯åº¦" }), 1788 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-black tracking-widest text-[8px]", style: { color: theme.primary }, children: "STREAM" }) 1789 ] }), 1790 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-7 left-1 text-left", children: [ 1791 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold tracking-tight text-white/90 drop-shadow-[0_1px_2px_rgba(0,0,0,1)]", children: "æå¤§å¯åº¦" }), 1792 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-black tracking-widest text-[8px]", style: { color: theme.primary }, children: "VOLTAGE" }) 1793 ] }), 1794 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-7 right-1 text-right", children: [
1795 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold tracking-tight text-white/90 drop-shadow-[0_1px_2px_rgba(0,0,0,1)]", children: "å¤å度" }), 1796 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-black tracking-widest text-[8px]", style: { color: theme.primary }, children: "CHAOS" }) 1797 ] }), 1798 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-2 left-3 text-left", children: [ 1799 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold tracking-tight text-white/90 drop-shadow-[0_1px_2px_rgba(0,0,0,1)]", children: "ã¸ã£ã³ã度" }), 1800 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-black tracking-widest text-[8px]", style: { color: theme.primary }, children: "AIR" }) 1801 ] }), 1802 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-2 right-3 text-right", children: [ 1803 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold tracking-tight text-white/90 drop-shadow-[0_1px_2px_rgba(0,0,0,1)]", children: "è¸ã¿ã£ã±åº¦" }),
1804 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-black tracking-widest text-[8px]", style: { color: theme.primary }, children: "FREEZE" }) 1805 ] }) 1806 ] }) 1807 ] }); 1808}; 1809const HamieFaceIcon = ({ 1810 size = 16, 1811 active = true, 1812 color, 1813 emptyColor = "#3f3f46", 1814 variant = "don", 1815 className = "", 1816 title 1817}) => { 1818 if (!active) { 1819 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1820 "svg", 1821 { 1822 width: size, 1823 height: size, 1824 viewBox: "0 0 32 32", 1825 fill: "none", 1826 xmlns: "http://www.w3.org/2000/svg", 1827 className: `shrink-0 opacity-40 ${className}`, 1828 children: [ 1829 title && /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: title }), 1830 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "8", cy: "8", r: "4.5", fill: emptyColor }), 1831 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "24", cy: "8", r: "4.5", fill: emptyColor }), 1832 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "16", cy: "18", r: "11", fill: emptyColor }) 1833 ] 1834 } 1835 ); 1836 } 1837 const headFill = color || (variant === "don" ? "#ef4444" : variant === "ka" ? "#3b82f6" : variant === "gold" ? "#f59e0b" : "#06b6d4"); 1838 const innerEarColor = variant === "ka" ? "#93c5fd" : "#fca5a5"; 1839 const blushColor = variant === "ka" ? "rgba(56, 189, 248, 0.9)" : "rgba(244, 63, 94, 0.9)"; 1840 const noseColor = variant === "ka" ? "#0284c7" : "#e11d48"; 1841 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1842 "svg", 1843 { 1844 width: size, 1845 height: size, 1846 viewBox: "0 0 32 32", 1847 fill: "none", 1848 xmlns: "http://www.w3.org/2000/svg", 1849 className: `shrink-0 transition-transform drop-shadow-[0_0_5px_rgba(245,158,11,0.5)] ${className}`, 1850 children: [ 1851 title && /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: title }),
1852 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "8", cy: "8", r: "4.5", fill: headFill, stroke: "#ffffff", strokeWidth: "1.2" }), 1853 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "8", cy: "8", r: "2.4", fill: innerEarColor }), 1854 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "24", cy: "8", r: "4.5", fill: headFill, stroke: "#ffffff", strokeWidth: "1.2" }), 1855 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "24", cy: "8", r: "2.4", fill: innerEarColor }), 1856 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "16", cy: "18", r: "11", fill: headFill, stroke: "#ffffff", strokeWidth: "1.5" }), 1857 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "16", cy: "20.5", rx: "7.5", ry: "4.5", fill: "#ffffff" }), 1858 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "10.5", cy: "20", r: "2", fill: blushColor }), 1859 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "21.5", cy: "20", r: "2", fill: blushColor }), 1860 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "16", r: "1.8", fill: "#18181b" }),
1861 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "11.4", cy: "15.4", r: "0.65", fill: "#ffffff" }), 1862 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "20", cy: "16", r: "1.8", fill: "#18181b" }), 1863 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "19.4", cy: "15.4", r: "0.65", fill: "#ffffff" }), 1864 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "16", cy: "19.2", rx: "1.1", ry: "0.8", fill: noseColor }), 1865 /* @__PURE__ */ jsxRuntimeExports.jsx( 1866 "path", 1867 { 1868 d: "M 14 21 Q 15 22.2 16 21.2 Q 17 22.2 18 21", 1869 stroke: "#881337", 1870 strokeWidth: "0.9", 1871 strokeLinecap: "round", 1872 fill: "none" 1873 } 1874 ) 1875 ] 1876 } 1877 ); 1878}; 1879const formatArcadeDuration = (val) => { 1880 if (val === null || val === void 0) return "0:00"; 1881 if (typeof val === "string") { 1882 if (val.includes(":")) return val; 1883 const num = parseFloat(val); 1884 if (!isNaN(num) && isFinite(num) && num >= 0) { 1885 const mins = Math.floor(num / 60); 1886 const secs = Math.floor(num % 60); 1887 return `${mins}:${secs.toString().padStart(2, "0")}`; 1888 } 1889 return "0:00"; 1890 } 1891 if (typeof val === "number") { 1892 if (isNaN(val) || !isFinite(val) || val < 0) return "0:00"; 1893 const mins = Math.floor(val / 60); 1894 const secs = Math.floor(val % 60); 1895 return `${mins}:${secs.toString().padStart(2, "0")}`; 1896 } 1897 return "0:00"; 1898}; 1899const parseDurationSec = (val) => { 1900 if (typeof val === "number") { 1901 return !isNaN(val) && isFinite(val) && val >= 0 ? val : 180; 1902 } 1903 if (typeof val === "string") { 1904 if (val.includes(":")) { 1905 const parts = val.split(":").map(Number); 1906 if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { 1907 return parts[0] * 60 + parts[1]; 1908 } 1909 if (parts.length === 3 && !isNaN(parts[0]) && !isNaN(parts[1]) && !isNaN(parts[2])) { 1910 return parts[0] * 3600 + parts[1] * 60 + parts[2]; 1911 } 1912 } 1913 const num = parseFloat(val); 1914 return !isNaN(num) && isFinite(num) && num >= 0 ? num : 180; 1915 } 1916 return 180; 1917}; 1918const CATEGORIES = [ 1919 { key: "all", label: "ALL SONGS" }, 1920 { key: "anime", label: "ANIME & GAME" }, 1921 { key: "classical", label: "CLASSICAL SHREDS" }, 1922 { key: "pop", label: "POP / ROCK" }, 1923 { key: "fast", label: "HIGH SPEED (140+)" } 1924]; 1925const ArcadeSongSelector = ({ 1926 currentUser, 1927 selectedSong, 1928 onSelectSong, 1929 onProceedToCabinet, 1930 theme = ARCADE_THEMES["5thmix"], 1931 onCycleTheme = () => { 1932 } 1933}) => { 1934 const { t } = useTranslation(); 1935 const [activeTab, setActiveTab] = reactExports.useState("site"); 1936 const [watchLaterIds, setWatchLaterIds] = reactExports.useState(/* @__PURE__ */ new Set()); 1937 const [isCopiedShare, setIsCopiedShare] = reactExports.useState(false); 1938 const [toastMessage, setToastMessage] = reactExports.useState(null); 1939 const toastTimerRef = reactExports.useRef(null); 1940 const showToast = reactExports.useCallback((msg) => { 1941 if (toastTimerRef.current) clearTimeout(toastTimerRef.current); 1942 setToastMessage(msg); 1943 toastTimerRef.current = setTimeout(() => { 1944 setToastMessage(null); 1945 toastTimerRef.current = null; 1946 }, 2600); 1947 }, []); 1948 const [deckView, setDeckView] = reactExports.useState( 1949 theme.id === "5thmix" ? "vinyl" : "radar" 1950 ); 1951 reactExports.useEffect(() => { 1952 setDeckView(theme.id === "5thmix" ? "vinyl" : "radar"); 1953 }, [theme.id]); 1954 const [searchQuery, setSearchQuery] = reactExports.useState(""); 1955 const [selectedCategory, setSelectedCategory] = reactExports.useState("all"); 1956 const [tracks, setTracks] = reactExports.useState([]); 1957 const [isLoadingTracks, setIsLoadingTracks] = reactExports.useState(false); 1958 const [isSpinningRoulette, setIsSpinningRoulette] = reactExports.useState(false); 1959 const [selectedWheelIndex, setSelectedWheelIndex] = reactExports.useState(0); 1960 const tracksRef = reactExports.useRef([]); 1961 tracksRef.current = tracks; 1962 const selectedWheelIndexRef = reactExports.useRef(selectedWheelIndex); 1963 selectedWheelIndexRef.current = selectedWheelIndex; 1964 const selectedSongRef = reactExports.useRef(selectedSong); 1965 selectedSongRef.current = selectedSong; 1966 const selectedCategoryRef = reactExports.useRef(selectedCategory); 1967 selectedCategoryRef.current = selectedCategory; 1968 const onProceedToCabinetRef = reactExports.useRef(onProceedToCabinet); 1969 onProceedToCabinetRef.current = onProceedToCabinet; 1970 const onCycleThemeRef = reactExports.useRef(onCycleTheme); 1971 onCycleThemeRef.current = onCycleTheme; 1972 const isSpinningRouletteRef = reactExports.useRef(isSpinningRoulette); 1973 isSpinningRouletteRef.current = isSpinningRoulette; 1974 const previewDebounceTimerRef = reactExports.useRef(null); 1975 const playChorusPreviewRef = reactExports.useRef(() => { 1976 }); 1977 const onSelectSongRef = reactExports.useRef(onSelectSong); 1978 onSelectSongRef.current = onSelectSong; 1979 const grooveRadarStats = reactExports.useMemo(() => { 1980 return computeGrooveRadar(selectedSong || (tracks.length > 0 ? tracks[selectedWheelIndex] : null)); 1981 }, [selectedSong, tracks, selectedWheelIndex]); 1982 const [isPlayingPreview, setIsPlayingPreview] = reactExports.useState(false); 1983 const [previewProgress, setPreviewProgress] = reactExports.useState(0); 1984 const [previewCurrentTime, setPreviewCurrentTime] = reactExports.useState(0); 1985 const previewAudioRef = reactExports.useRef(null); 1986 const [isParsingFile, setIsParsingFile] = reactExports.useState(false); 1987 const [dragOver, setDragOver] = reactExports.useState(false); 1988 const [urlInput, setUrlInput] = reactExports.useState(""); 1989 const [urlError, setUrlError] = reactExports.useState(null); 1990 const fileInputRef = reactExports.useRef(null); 1991 const wheelContainerRef = reactExports.useRef(null); 1992 const [refreshTrigger, setRefreshTrigger] = reactExports.useState(0); 1993 const targetIndexRef = reactExports.useRef(0); 1994 const currentIndexRef = reactExports.useRef(0); 1995 const [displayPos, setDisplayPos] = reactExports.useState(0); 1996 const animFrameRef = reactExports.useRef(null); 1997 const isDraggingRef = reactExports.useRef(false); 1998 const dragStartYRef = reactExports.useRef(0); 1999 const dragStartTargetRef = reactExports.useRef(0); 2000 const dragMovedRef = reactExports.useRef(false); 2001 const wheelAccumulatorRef = reactExports.useRef(0); 2002 const wheelLastTimeRef = reactExports.useRef(0); 2003 const calculateStars = (track) => { 2004 var _a, _b; 2005 const tempo = ((_a = track.midiMetadata) == null ? void 0 : _a.tempo) || 120; 2006 const notesCount = ((_b = track.midiMetadata) == null ? void 0 : _b.notesCount) || 1e3; 2007 const duration = parseDurationSec(track.durationSec || track.duration); 2008 const notesPerSec = duration > 0 ? notesCount / duration : 5; 2009 if (notesPerSec > 16 || tempo >= 180) return 5; 2010 if (notesPerSec > 10 || tempo >= 150) return 4; 2011 if (notesPerSec > 6 || tempo >= 120) return 3; 2012 if (notesPerSec > 3) return 2; 2013 return 1; 2014 }; 2015 reactExports.useEffect(() => { 2016 let isCurrent = true; 2017 if (currentUser == null ? void 0 : currentUser.id) { 2018 api.getUserWatchLater(currentUser.id).then((ids) => { 2019 if (isCurrent && Array.isArray(ids)) { 2020 setWatchLaterIds(new Set(ids.map(String))); 2021 } 2022 }).catch(() => { 2023 }); 2024 } else {
2025 try { 2026 const guestSaved = localStorage.getItem("arcade_bookmarks_guest") || localStorage.getItem("arcade_play_later_guest"); 2027 if (guestSaved) { 2028 const parsed = JSON.parse(guestSaved); 2029 if (Array.isArray(parsed) && isCurrent) { 2030 setWatchLaterIds(new Set(parsed.map(String))); 2031 } 2032 } 2033 } catch { 2034 } 2035 } 2036 return () => { 2037 isCurrent = false; 2038 }; 2039 }, [currentUser == null ? void 0 : currentUser.id]); 2040 reactExports.useEffect(() => { 2041 let isCurrent = true; 2042 const fetchTracks = async () => { 2043 setIsLoadingTracks(true); 2044 try { 2045 let res = []; 2046 if (activeTab === "bookmarks") { 2047 if (currentUser == null ? void 0 : currentUser.id) { 2048 res = await api.getWatchLaterTracks(currentUser.id); 2049 } else { 2050 const guestIds = Array.from(watchLaterIds); 2051 if (guestIds.length > 0) { 2052 const trackPromises = guestIds.slice(0, 50).map((id) => api.getTrack(id).catch(() => null)); 2053 const loaded = await Promise.all(trackPromises); 2054 res = loaded.filter(Boolean); 2055 } else { 2056 res = []; 2057 } 2058 } 2059 } else if (activeTab === "site") { 2060 let catParam = selectedCategory === "all" ? "" : selectedCategory; 2061 if (selectedCategory === "fast") catParam = ""; 2062 const sortMode = searchQuery ? "views_desc" : "random"; 2063 res = await api.getTracks(searchQuery, catParam, sortMode, 50); 2064 } else { 2065 return; 2066 } 2067 if (isCurrent && Array.isArray(res)) { 2068 const completedTracks = res.filter((t2) => t2.status === "completed"); 2069 const playable = completedTracks.length > 0 ? completedTracks : res; 2070 let filtered = playable; 2071 if (activeTab === "bookmarks" && searchQuery.trim()) { 2072 const q = searchQuery.toLowerCase(); 2073 filtered = filtered.filter( 2074 (t2) => t2.title && t2.title.toLowerCase().includes(q) || t2.artist && t2.artist.toLowerCase().includes(q) 2075 ); 2076 } 2077 if (selectedCategory === "fast") { 2078 filtered = playable.filter((t2) => { 2079 var _a; 2080 return (((_a = t2.midiMetadata) == null ? void 0 : _a.tempo) || 120) >= 140; 2081 }); 2082 } else if (selectedCategory !== "all" && activeTab === "bookmarks") { 2083 filtered = filtered.filter((t2) => { 2084 var _a, _b; 2085 const cat = typeof t2.category === "string" ? t2.category.toLowerCase() : ((_b = (_a = t2.category) == null ? void 0 : _a.label) == null ? void 0 : _b.toLowerCase()) || ""; 2086 return cat.includes(selectedCategory.toLowerCase()); 2087 }); 2088 } 2089 const enriched = filtered.map((t2) => { 2090 var _a; 2091 return { 2092 ...t2, 2093 _cachedStars: calculateStars(t2), 2094 _cachedTempo: Math.round(((_a = t2.midiMetadata) == null ? void 0 : _a.tempo) || 120), 2095 _cachedArtist: t2.artist || "" 2096 }; 2097 }); 2098 setTracks(enriched); 2099 targetIndexRef.current = 0; 2100 currentIndexRef.current = 0; 2101 setDisplayPos(0); 2102 setSelectedWheelIndex(0); 2103 selectedWheelIndexRef.current = 0; 2104 if (enriched.length > 0) { 2105 pickTrackData(enriched[0], false); 2106 } 2107 } 2108 } catch (err) { 2109 console.error("Failed to fetch tracks:", err); 2110 } finally { 2111 if (isCurrent) setIsLoadingTracks(false); 2112 } 2113 }; 2114 const timeoutId = setTimeout(fetchTracks, searchQuery ? 300 : 0); 2115 return () => { 2116 isCurrent = false; 2117 clearTimeout(timeoutId); 2118 }; 2119 }, [searchQuery, selectedCategory, refreshTrigger, activeTab, currentUser == null ? void 0 : currentUser.id]); 2120 const playChorusPreview = reactExports.useCallback((song) => { 2121 if (previewAudioRef.current) { 2122 previewAudioRef.current.pause(); 2123 previewAudioRef.current.src = ""; 2124 } 2125 if (!song || !song.opusPath) { 2126 setIsPlayingPreview(false); 2127 arcadeAudio.duckBGM(false); 2128 return; 2129 } 2130 try { 2131 if (!previewAudioRef.current) { 2132 previewAudioRef.current = new Audio(); 2133 } 2134 const audio = previewAudioRef.current; 2135 audio.src = song.opusPath; 2136 audio.crossOrigin = "anonymous"; 2137 const cue = Math.max(0, Math.floor(song.duration * 0.35)); 2138 audio.currentTime = cue; 2139 setPreviewCurrentTime(cue); 2140 audio.onloadedmetadata = () => { 2141 audio.currentTime = cue; 2142 }; 2143 audio.ontimeupdate = () => { 2144 setPreviewCurrentTime(audio.currentTime); 2145 const progress = song.duration > 0 ? audio.currentTime / song.duration : 0; 2146 setPreviewProgress(progress); 2147 if (audio.currentTime >= cue + 25 || audio.currentTime >= song.duration) { 2148 audio.currentTime = cue; 2149 } 2150 }; 2151 audio.onplay = () => { 2152 setIsPlayingPreview(true); 2153 arcadeAudio.duckBGM(true); 2154 }; 2155 audio.onpause = () => { 2156 setIsPlayingPreview(false); 2157 arcadeAudio.duckBGM(false); 2158 }; 2159 audio.onerror = () => { 2160 setIsPlayingPreview(false); 2161 arcadeAudio.duckBGM(false); 2162 }; 2163 audio.play().catch(() => { 2164 setIsPlayingPreview(false); 2165 arcadeAudio.duckBGM(false); 2166 }); 2167 } catch (err) { 2168 console.warn("Could not initiate preview playback:", err); 2169 setIsPlayingPreview(false); 2170 arcadeAudio.duckBGM(false); 2171 } 2172 }, []); 2173 playChorusPreviewRef.current = playChorusPreview; 2174 const pickTrackData = reactExports.useCallback((track, playAudio = true) => { 2175 var _a, _b, _c; 2176 if (!track) return; 2177 const durSec = parseDurationSec(track.durationSec || track.duration); 2178 const numId = parseInt(String(track.id)); 2179 const group = numId >= 1e7 ? numId % 1e3 : Math.floor(numId / 1e3); 2180 const resolvedPianoRoll = typeof track.pianoRollPath === "string" && track.pianoRollPath ? track.pianoRollPath : track.status === "completed" ? `/storage/tracks/piano_rolls/${group}/${track.id}.json` : void 0; 2181 const resolvedOpus = typeof track.opusPath === "string" && track.opusPath ? track.opusPath : track.status === "completed" ? `/storage/tracks/opus/${group}/${track.id}.opus` : void 0; 2182 const songData = { 2183 source: "site", 2184 id: String(track.id), 2185 title: track.title || "Untitled Track", 2186 artist: track.artist || void 0, 2187 category: typeof track.category === "string" ? track.category : ((_a = track.category) == null ? void 0 : _a.label) || "General", 2188 bpm: Math.round(((_b = track.midiMetadata) == null ? void 0 : _b.tempo) || 120), 2189 duration: durSec, 2190 timeSignature: ((_c = track.midiMetadata) == null ? void 0 : _c.timeSignature) ? track.midiMetadata.timeSignature.split("/").map(Number) : [4, 4], 2191 instruments: track.instruments || [], 2192 midiPath: track.midiPath || void 0, 2193 pianoRollPath: resolvedPianoRoll, 2194 melodyPath: track.melodyPath || void 0, 2195 opusPath: resolvedOpus, 2196 difficultyStars: calculateStars(track), 2197 rawTrack: track 2198 }; 2199 onSelectSongRef.current(songData); 2200 if (previewDebounceTimerRef.current) { 2201 clearTimeout(previewDebounceTimerRef.current); 2202 previewDebounceTimerRef.current = null; 2203 } 2204 if (playAudio) { 2205 previewDebounceTimerRef.current = setTimeout(() => { 2206 playChorusPreviewRef.current(songData); 2207 if (songData.pianoRollPath) { 2208 fetch(songData.pianoRollPath).then((res) => res.json()).then((data) => { 2209 if (Array.isArray(data)) { 2210 songData.pianoRollData = data; 2211 onSelectSongRef.current({ ...songData, pianoRollData: data }); 2212 } 2213 }).catch(() => { 2214 }); 2215 } 2216 if (songData.melodyPath) { 2217 fetch(songData.melodyPath).then((res) => res.json()).then((data) => { 2218 if (Array.isArray(data)) { 2219 songData.melodyData = data; 2220 onSelectSongRef.current({ ...songData, melodyData: data }); 2221 } 2222 }).catch(() => { 2223 }); 2224 } 2225 }, 140); 2226 } 2227 }, []); 2228 const togglePreview = reactExports.useCallback(() => { 2229 if (!previewAudioRef.current) { 2230 if (selectedSongRef.current) playChorusPreviewRef.current(selectedSongRef.current); 2231 return; 2232 } 2233 if (isPlayingPreview) { 2234 previewAudioRef.current.pause(); 2235 setIsPlayingPreview(false); 2236 arcadeAudio.duckBGM(false); 2237 } else { 2238 previewAudioRef.current.play().then(() => { 2239 setIsPlayingPreview(true); 2240 arcadeAudio.duckBGM(true); 2241 }).catch(() => { 2242 }); 2243 } 2244 }, [isPlayingPreview]); 2245 const togglePreviewRef = reactExports.useRef(togglePreview); 2246 togglePreviewRef.current = togglePreview; 2247 reactExports.useEffect(() => { 2248 return () => { 2249 if (previewDebounceTimerRef.current) { 2250 clearTimeout(previewDebounceTimerRef.current); 2251 previewDebounceTimerRef.current = null; 2252 } 2253 if (previewAudioRef.current) { 2254 previewAudioRef.current.pause(); 2255 previewAudioRef.current.src = ""; 2256 } 2257 arcadeAudio.duckBGM(false); 2258 }; 2259 }, []); 2260 const stepAnimation = reactExports.useCallback(() => { 2261 const diff = targetIndexRef.current - currentIndexRef.current; 2262 if (Math.abs(diff) < 5e-3) { 2263 currentIndexRef.current = targetIndexRef.current; 2264 setDisplayPos(targetIndexRef.current); 2265 animFrameRef.current = null; 2266 return; 2267 } 2268 currentIndexRef.current += diff * 0.32; 2269 setDisplayPos(currentIndexRef.current); 2270 animFrameRef.current = requestAnimationFrame(stepAnimation); 2271 }, []); 2272 const setWheelTarget = reactExports.useCallback((newTarget, animate = true) => { 2273 targetIndexRef.current = newTarget; 2274 const curTracks = tracksRef.current; 2275 if (curTracks.length > 0) { 2276 const rawIdx = Math.round(newTarget); 2277 const normalizedIdx = (rawIdx % curTracks.length + curTracks.length) % curTracks.length; 2278 if (normalizedIdx !== selectedWheelIndexRef.current) { 2279 selectedWheelIndexRef.current = normalizedIdx; 2280 setSelectedWheelIndex(normalizedIdx); 2281 arcadeAudio.playWheelTick(); 2282 pickTrackData(curTracks[normalizedIdx], true); 2283 } 2284 } 2285 if (animate) { 2286 if (!animFrameRef.current) { 2287 animFrameRef.current = requestAnimationFrame(stepAnimation); 2288 } 2289 } else { 2290 if (animFrameRef.current) { 2291 cancelAnimationFrame(animFrameRef.current); 2292 animFrameRef.current = null; 2293 } 2294 currentIndexRef.current = newTarget; 2295 setDisplayPos(newTarget); 2296 } 2297 }, [pickTrackData, stepAnimation]); 2298 const stepWheel = reactExports.useCallback((step) => { 2299 setWheelTarget(Math.round(targetIndexRef.current) + step, true); 2300 }, [setWheelTarget]); 2301 const stepWheelRef = reactExports.useRef(stepWheel); 2302 stepWheelRef.current = stepWheel; 2303 const setWheelTargetRef = reactExports.useRef(setWheelTarget); 2304 setWheelTargetRef.current = setWheelTarget; 2305 const handleSelectWheelTrack = reactExports.useCallback((index) => { 2306 const curTracks = tracksRef.current; 2307 if (curTracks.length === 0) return; 2308 setWheelTarget(index, true); 2309 }, [setWheelTarget]); 2310 const handleSelectWheelTrackRef = reactExports.useRef(handleSelectWheelTrack); 2311 handleSelectWheelTrackRef.current = handleSelectWheelTrack; 2312 reactExports.useEffect(() => { 2313 return () => { 2314 if (animFrameRef.current) { 2315 cancelAnimationFrame(animFrameRef.current); 2316 animFrameRef.current = null; 2317 } 2318 }; 2319 }, []); 2320 reactExports.useEffect(() => { 2321 const el = wheelContainerRef.current; 2322 if (!el) return; 2323 const onNativeWheel = (e) => { 2324 e.preventDefault(); 2325 e.stopPropagation(); 2326 const curTracks = tracksRef.current; 2327 if (curTracks.length === 0) return; 2328 if (Math.abs(e.deltaY) < 3) return; 2329 const now = performance.now(); 2330 if (now - wheelLastTimeRef.current > 160) { 2331 wheelAccumulatorRef.current = 0; 2332 } 2333 wheelLastTimeRef.current = now; 2334 wheelAccumulatorRef.current += e.deltaY; 2335 const threshold = 26; 2336 if (Math.abs(wheelAccumulatorRef.current) >= threshold) { 2337 const step = wheelAccumulatorRef.current > 0 ? 1 : -1; 2338 wheelAccumulatorRef.current = 0; 2339 stepWheelRef.current(step); 2340 } 2341 }; 2342 el.addEventListener("wheel", onNativeWheel, { passive: false }); 2343 return () => { 2344 el.removeEventListener("wheel", onNativeWheel); 2345 }; 2346 }, []); 2347 const handlePointerDown = (e) => { 2348 if (tracks.length === 0) return; 2349 isDraggingRef.current = true; 2350 dragMovedRef.current = false;
2351 dragStartYRef.current = e.clientY; 2352 dragStartTargetRef.current = targetIndexRef.current; 2353 }; 2354 const handlePointerMove = (e) => { 2355 if (!isDraggingRef.current) return; 2356 const deltaY = e.clientY - dragStartYRef.current; 2357 if (Math.abs(deltaY) > 6) { 2358 dragMovedRef.current = true; 2359 try { 2360 if (!e.currentTarget.hasPointerCapture(e.pointerId)) { 2361 e.currentTarget.setPointerCapture(e.pointerId); 2362 } 2363 } catch (_) { 2364 } 2365 } 2366 if (dragMovedRef.current) { 2367 const slotDelta = -deltaY / 60; 2368 setWheelTarget(dragStartTargetRef.current + slotDelta, true); 2369 } 2370 }; 2371 const handlePointerUp = (e) => { 2372 if (!isDraggingRef.current) return; 2373 isDraggingRef.current = false; 2374 try { 2375 if (e.currentTarget.hasPointerCapture(e.pointerId)) { 2376 e.currentTarget.releasePointerCapture(e.pointerId); 2377 } 2378 } catch (_) { 2379 } 2380 if (dragMovedRef.current) { 2381 setWheelTarget(Math.round(targetIndexRef.current), true); 2382 } 2383 }; 2384 const handleSwap50Songs = reactExports.useCallback(() => { 2385 arcadeAudio.playRoulette(); 2386 setRefreshTrigger((v) => v + 1); 2387 }, []); 2388 const handleSwap50SongsRef = reactExports.useRef(handleSwap50Songs); 2389 handleSwap50SongsRef.current = handleSwap50Songs; 2390 const handleSpinRoulette = reactExports.useCallback(() => { 2391 const curTracks = tracksRef.current; 2392 if (curTracks.length === 0 || isSpinningRouletteRef.current) return; 2393 setIsSpinningRoulette(true); 2394 isSpinningRouletteRef.current = true; 2395 arcadeAudio.playRoulette(); 2396 const extraSteps = 18 + Math.floor(Math.random() * 10); 2397 const finalTarget = Math.round(targetIndexRef.current) + extraSteps; 2398 setWheelTarget(finalTarget, true); 2399 setTimeout(() => { 2400 setIsSpinningRoulette(false); 2401 isSpinningRouletteRef.current = false; 2402 arcadeAudio.playCoin(); 2403 }, 900); 2404 }, [setWheelTarget]); 2405 const handleSpinRouletteRef = reactExports.useRef(handleSpinRoulette); 2406 handleSpinRouletteRef.current = handleSpinRoulette; 2407 const setDeckViewRef = reactExports.useRef(setDeckView); 2408 setDeckViewRef.current = setDeckView; 2409 const handleToggleBookmark = reactExports.useCallback(async () => { 2410 const curSong = selectedSongRef.current; 2411 if (!curSong) return; 2412 if (curSong.source === "upload") { 2413 arcadeAudio.playBuzzer(); 2414 showToast("LOCAL CARTRIDGE CANNOT BE BOOKMARKED"); 2415 return; 2416 } 2417 const songId = String(curSong.id); 2418 const isSaved = watchLaterIds.has(songId); 2419 const nextSet = new Set(watchLaterIds); 2420 if (isSaved) { 2421 nextSet.delete(songId); 2422 arcadeAudio.playSelect(); 2423 showToast("REMOVED FROM BOOKMARKS"); 2424 } else { 2425 nextSet.add(songId); 2426 arcadeAudio.playCoin(); 2427 showToast("â SAVED TO BOOKMARKS!"); 2428 } 2429 setWatchLaterIds(nextSet); 2430 try { 2431 if (currentUser == null ? void 0 : currentUser.id) { 2432 await api.toggleWatchLaterTrack(songId, currentUser.id); 2433 } else { 2434 localStorage.setItem("arcade_bookmarks_guest", JSON.stringify(Array.from(nextSet))); 2435 } 2436 } catch (err) { 2437 console.error("Failed to toggle bookmark:", err); 2438 } 2439 if (activeTab === "bookmarks" && isSaved) { 2440 setRefreshTrigger((v) => v + 1); 2441 } 2442 }, [watchLaterIds, currentUser == null ? void 0 : currentUser.id, activeTab, showToast]); 2443 const handleToggleBookmarkRef = reactExports.useRef(handleToggleBookmark); 2444 handleToggleBookmarkRef.current = handleToggleBookmark; 2445 const handleShareSong = reactExports.useCallback(() => { 2446 var _a; 2447 const curSong = selectedSongRef.current; 2448 if (!curSong) return; 2449 arcadeAudio.playCoin(); 2450 let shareUrl = `${window.location.origin}/games`; 2451 if (curSong.source === "site" && curSong.id) { 2452 shareUrl += `?track=${curSong.id}`; 2453 } 2454 if ((_a = navigator == null ? void 0 : navigator.clipboard) == null ? void 0 : _a.writeText) { 2455 navigator.clipboard.writeText(shareUrl).then(() => { 2456 setIsCopiedShare(true); 2457 showToast("GAME LINK COPIED TO CLIPBOARD!"); 2458 setTimeout(() => setIsCopiedShare(false), 2500); 2459 }).catch(() => { 2460 showToast("COPIED LINK TO CLIPBOARD"); 2461 }); 2462 } else { 2463 showToast("SHARE: " + shareUrl); 2464 } 2465 }, [showToast]); 2466 const handleOpenWatchPage = reactExports.useCallback(() => { 2467 const curSong = selectedSongRef.current; 2468 if (!curSong) return; 2469 if (curSong.source === "upload") { 2470 arcadeAudio.playBuzzer(); 2471 showToast("LOCAL CARTRIDGE - NO WEB WATCH PAGE"); 2472 return; 2473 } 2474 arcadeAudio.playSelect(); 2475 const trackObj = curSong.rawTrack || { 2476 id: Number(curSong.id), 2477 title: curSong.title, 2478 artist: curSong.artist 2479 }; 2480 const watchUrl = generateTrackUrl(trackObj); 2481 window.open(watchUrl, "_blank", "noopener,noreferrer"); 2482 showToast("OPENING TRACK WATCH PAGE..."); 2483 }, [showToast]); 2484 reactExports.useEffect(() => { 2485 const handleKeyDown = (e) => { 2486 var _a, _b, _c; 2487 const isInputFocused = ((_a = document.activeElement) == null ? void 0 : _a.tagName) === "INPUT"; 2488 if (e.key === "Escape") { 2489 if (isInputFocused) { 2490 (_b = document.activeElement) == null ? void 0 : _b.blur(); 2491 } 2492 return; 2493 } 2494 if (isInputFocused) { 2495 if (e.key === "Enter") { 2496 (_c = document.activeElement) == null ? void 0 : _c.blur(); 2497 } 2498 return; 2499 } 2500 tracksRef.current; 2501 selectedWheelIndexRef.current; 2502 if (e.key === "ArrowUp" || e.key === "w" || e.key === "W") { 2503 e.preventDefault(); 2504 stepWheelRef.current(-1); 2505 } else if (e.key === "ArrowDown" || e.key === "s" || e.key === "S") { 2506 e.preventDefault(); 2507 stepWheelRef.current(1); 2508 } else if (e.key === "x" || e.key === "X") { 2509 e.preventDefault(); 2510 handleSwap50SongsRef.current(); 2511 } else if (e.key === "b" || e.key === "B" || e.key === "p" || e.key === "P") { 2512 e.preventDefault(); 2513 handleToggleBookmarkRef.current(); 2514 } else if (e.key === "Enter") { 2515 e.preventDefault(); 2516 if (selectedSongRef.current) { 2517 if (previewAudioRef.current) previewAudioRef.current.pause(); 2518 arcadeAudio.duckBGM(false); 2519 arcadeAudio.playStageStart(); 2520 onProceedToCabinetRef.current(); 2521 } 2522 } else if (e.key === " ") { 2523 e.preventDefault(); 2524 togglePreviewRef.current(); 2525 } else if (e.key === "r" || e.key === "R") { 2526 e.preventDefault(); 2527 handleSpinRouletteRef.current(); 2528 } else if (e.key === "t" || e.key === "T") { 2529 e.preventDefault(); 2530 arcadeAudio.playWheelTick(); 2531 onCycleThemeRef.current(); 2532 } else if (e.key === "g" || e.key === "G") { 2533 e.preventDefault(); 2534 arcadeAudio.playSelect(); 2535 setDeckViewRef.current((prev) => prev === "radar" ? "vinyl" : "radar"); 2536 } else if (e.key === "Tab") { 2537 e.preventDefault(); 2538 const curIdx = CATEGORIES.findIndex((c) => c.key === selectedCategoryRef.current); 2539 const nextCat = CATEGORIES[(curIdx + 1) % CATEGORIES.length].key; 2540 arcadeAudio.playSelect();
2541 setSelectedCategory(nextCat); 2542 } 2543 }; 2544 window.addEventListener("keydown", handleKeyDown); 2545 return () => window.removeEventListener("keydown", handleKeyDown); 2546 }, []); 2547 const parseMidiFileBuffer = async (buffer, fileName) => { 2548 setIsParsingFile(true); 2549 try { 2550 const parsed = new Midi.Midi(buffer); 2551 const duration = Math.round(parsed.duration) || 120; 2552 const tempo = parsed.header.tempos.length > 0 ? Math.round(parsed.header.tempos[0].bpm) : 120; 2553 const timeSig = parsed.header.timeSignatures.length > 0 ? [parsed.header.timeSignatures[0].timeSignature[0], parsed.header.timeSignatures[0].timeSignature[1]] : [4, 4]; 2554 const instruments = []; 2555 parsed.tracks.forEach((track, idx) => { 2556 instruments.push({ 2557 trackId: idx, 2558 name: track.name || track.instrument.name || `Track ${idx + 1}`, 2559 family: track.instrument.family, 2560 program: track.instrument.number !== void 0 ? track.instrument.number : 0, 2561 midiChannel: track.notes.length === 0 ? -1 : track.channel !== void 0 ? track.channel : 0, 2562 notesCount: track.notes.length 2563 }); 2564 }); 2565 const pianoRollData = []; 2566 let musicalTrackIdx = 0; 2567 let totalNotes = 0; 2568 parsed.tracks.forEach((track, i) => { 2569 if (track.notes.length > 0) { 2570 track.notes.forEach((n) => { 2571 totalNotes++; 2572 pianoRollData.push({ 2573 t: parseFloat(n.time.toFixed(3)), 2574 n: n.midi, 2575 d: parseFloat(n.duration.toFixed(3)), 2576 v: Math.round(n.velocity * 127), 2577 i: track.name || track.instrument.name || `Track ${i + 1}`, 2578 trackIdx: musicalTrackIdx, 2579 p: track.instrument.number !== void 0 ? track.instrument.number : 0, 2580 isDrum: track.instrument.family === "drums" 2581 }); 2582 }); 2583 musicalTrackIdx++; 2584 } 2585 }); 2586 const notesPerSec = duration > 0 ? totalNotes / duration : 5; 2587 let difficultyStars = 3; 2588 if (notesPerSec > 16) difficultyStars = 5; 2589 else if (notesPerSec > 10) difficultyStars = 4; 2590 else if (notesPerSec > 6) difficultyStars = 3; 2591 else if (notesPerSec > 3) difficultyStars = 2; 2592 else difficultyStars = 1; 2593 const cleanTitle = fileName.replace(/\.[^/.]+$/, "").replace(/[_-]/g, " "); 2594 const songData = { 2595 source: "upload", 2596 id: "custom-" + Date.now(), 2597 title: cleanTitle, 2598 bpm: tempo, 2599 duration, 2600 timeSignature: timeSig, 2601 instruments, 2602 pianoRollData, 2603 difficultyStars 2604 }; 2605 arcadeAudio.playCoin(); 2606 onSelectSong(songData); 2607 } catch (err) { 2608 console.error("Failed to parse local MIDI:", err); 2609 alert("Could not parse this MIDI file. Please ensure it is a valid Standard MIDI file (.mid)."); 2610 } finally { 2611 setIsParsingFile(false); 2612 } 2613 }; 2614 const handleFileChange = async (e) => { 2615 var _a; 2616 const file = (_a = e.target.files) == null ? void 0 : _a[0]; 2617 if (!file) return; 2618 const buf = await file.arrayBuffer(); 2619 parseMidiFileBuffer(buf, file.name); 2620 }; 2621 const handleDrop = async (e) => { 2622 var _a; 2623 e.preventDefault(); 2624 setDragOver(false); 2625 const file = (_a = e.dataTransfer.files) == null ? void 0 : _a[0]; 2626 if (file && (file.name.endsWith(".mid") || file.name.endsWith(".midi"))) { 2627 const buf = await file.arrayBuffer(); 2628 parseMidiFileBuffer(buf, file.name); 2629 } else { 2630 alert("Please drop a valid .mid or .midi file."); 2631 } 2632 }; 2633 const handleUrlSubmit = async (e) => { 2634 var _a; 2635 e.preventDefault(); 2636 setUrlError(null); 2637 const cleanUrl = urlInput.trim(); 2638 if (!cleanUrl) return; 2639 setIsParsingFile(true); 2640 try { 2641 const idMatch = cleanUrl.match(/\/track\/(\d+)/) || cleanUrl.match(/\/watch\?.*v=(\d+)/) || cleanUrl.match(/\/midi(?:[a-zA-Z0-9_\-]+\/)?(\d+)_/); 2642 if (idMatch) { 2643 const trackId = idMatch[1]; 2644 const trackData = await api.getTrack(trackId); 2645 if (trackData) { 2646 pickTrackData(trackData, true);
2647 setIsParsingFile(false); 2648 return; 2649 } 2650 } 2651 const res = await fetch(cleanUrl); 2652 if (!res.ok) throw new Error(`HTTP Error ${res.status}`); 2653 const buf = await res.arrayBuffer(); 2654 const fileName = ((_a = cleanUrl.split("/").pop()) == null ? void 0 : _a.split("?")[0]) || "Online Song.mid"; 2655 await parseMidiFileBuffer(buf, fileName); 2656 } catch (err) { 2657 console.error("URL Ingestion error:", err); 2658 setUrlError("Unable to fetch or parse this MIDI URL. Please check the link or CORS policy."); 2659 } finally { 2660 setIsParsingFile(false); 2661 } 2662 }; 2663 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full max-h-[calc(100vh-80px)] flex flex-col justify-between overflow-hidden select-none font-mono text-white animate-in fade-in duration-300 relative", children: [ 2664 toastMessage && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed top-16 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-xl bg-slate-950/95 border-2 border-cyan-400 text-cyan-300 text-xs font-mono font-black tracking-wider uppercase shadow-[0_0_25px_rgba(6,182,212,0.6)] animate-in fade-in slide-in-from-top-2 duration-200 flex items-center gap-2 pointer-events-none", children: [
2665 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 14, className: "text-yellow-400 animate-spin" }), 2666 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: toastMessage }) 2667 ] }), 2668 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2 bg-slate-950/80 border-2 border-cyan-500/30 rounded-xl shadow-[0_0_15px_rgba(6,182,212,0.15)] shrink-0", children: [ 2669 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [ 2670 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2671 "button", 2672 { 2673 onClick: () => { 2674 arcadeAudio.playSelect(); 2675 setActiveTab("site"); 2676 }, 2677 className: `flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-black tracking-wider transition-all cursor-pointer ${activeTab === "site" ? "bg-gradient-to-r from-cyan-500 to-blue-600 text-white shadow-[0_0_12px_rgba(6,182,212,0.5)] scale-105" : "text-slate-400 hover:text-white hover:bg-slate-900"}`, 2678 children: [
2679 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 14, className: activeTab === "site" ? "animate-spin" : "" }), 2680 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ARCADE JUKEBOX" }) 2681 ] 2682 } 2683 ), 2684 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2685 "button", 2686 { 2687 onClick: () => { 2688 arcadeAudio.playSelect(); 2689 setActiveTab("bookmarks"); 2690 }, 2691 className: `flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-black tracking-wider transition-all cursor-pointer ${activeTab === "bookmarks" ? "bg-gradient-to-r from-amber-500 to-yellow-600 text-slate-950 font-black shadow-[0_0_12px_rgba(245,158,11,0.6)] scale-105" : "text-slate-400 hover:text-white hover:bg-slate-900"}`, 2692 children: [ 2693 /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { size: 14, className: watchLaterIds.size > 0 ? "fill-current text-amber-400" : "" }), 2694 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "BOOKMARKS" }), 2695 watchLaterIds.size > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-1.5 py-0.5 rounded-full text-[10px] font-mono font-black ${activeTab === "bookmarks" ? "bg-black text-amber-300" : "bg-amber-400 text-black"}`, children: watchLaterIds.size }) 2696 ] 2697 } 2698 ),
2699 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2700 "button", 2701 { 2702 onClick: () => { 2703 arcadeAudio.playSelect(); 2704 setActiveTab("upload"); 2705 }, 2706 className: `flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-black tracking-wider transition-all cursor-pointer ${activeTab === "upload" ? "bg-gradient-to-r from-pink-500 to-rose-600 text-white shadow-[0_0_12px_rgba(244,63,94,0.5)] scale-105" : "text-slate-400 hover:text-white hover:bg-slate-900"}`, 2707 children: [ 2708 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14 }), 2709 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "INSERT CARTRIDGE (.MID)" }) 2710 ] 2711 } 2712 ), 2713 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2714 "button", 2715 { 2716 onClick: () => { 2717 arcadeAudio.playSelect(); 2718 setActiveTab("url"); 2719 }, 2720 className: `flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-black tracking-wider transition-all cursor-pointer ${activeTab === "url" ? "bg-gradient-to-r from-purple-500 to-indigo-600 text-white shadow-[0_0_12px_rgba(168,85,247,0.5)] scale-105" : "text-slate-400 hover:text-white hover:bg-slate-900"}`, 2721 children: [
2722 /* @__PURE__ */ jsxRuntimeExports.jsx(Link, { size: 14 }), 2723 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "CYBER TERMINAL" }) 2724 ] 2725 } 2726 ) 2727 ] }), 2728 (activeTab === "site" || activeTab === "bookmarks") && /* @__PURE__ */ jsxRuntimeExports.jsxs( 2729 "button", 2730 { 2731 onClick: handleSpinRoulette, 2732 disabled: isSpinningRoulette || tracks.length === 0, 2733 className: `group flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500 via-pink-500 to-purple-600 text-white text-xs font-black tracking-wider shadow-[0_0_15px_rgba(236,72,153,0.5)] hover:shadow-[0_0_25px_rgba(236,72,153,0.8)] active:scale-95 transition-all cursor-pointer ${isSpinningRoulette ? "animate-pulse pointer-events-none" : ""}`, 2734 title: "Shortcut: Press [R]", 2735 children: [ 2736 /* @__PURE__ */ jsxRuntimeExports.jsx(Dices, { size: 14, className: `text-yellow-300 ${isSpinningRoulette ? "animate-spin" : "group-hover:rotate-45 transition-transform"}` }),
2737 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isSpinningRoulette ? "SPINNING..." : "ARCADE ROULETTE [R]" }) 2738 ] 2739 } 2740 ) 2741 ] }), 2742 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 grid grid-cols-1 lg:grid-cols-12 gap-4 my-2 min-h-0 overflow-hidden", children: [ 2743 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2744 "div", 2745 { 2746 className: `lg:col-span-7 flex flex-col h-full overflow-hidden rounded-2xl p-3.5 bg-slate-950/90 relative border-2 ${theme.deckBorder}`, 2747 children: [ 2748 (activeTab === "site" || activeTab === "bookmarks") && selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col justify-between overflow-hidden", children: [ 2749 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center justify-between gap-3 px-3 py-1.5 rounded-lg border font-mono ${theme.hudStageBoxClass} shrink-0`, children: [ 2750 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
2751 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping" }), 2752 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-black tracking-wider uppercase", children: t("gameCenter.musicSelect", "MUSIC SELECT") }) 2753 ] }), 2754 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 2755 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-xs font-black tracking-widest text-amber-300", children: [ 2756 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-70", children: "SPEED:" }), 2757 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-mono text-sm", children: selectedSong.bpm }), 2758 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-amber-400", children: "bpm" }) 2759 ] }),
2760 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2761 "button", 2762 { 2763 onClick: () => { 2764 arcadeAudio.playSelect(); 2765 setDeckView(deckView === "radar" ? "vinyl" : "radar"); 2766 }, 2767 className: "flex items-center gap-1 px-2 py-0.5 rounded border text-[9px] font-mono font-bold transition-all bg-black/60 hover:bg-black/90 active:scale-95 cursor-pointer", 2768 style: { borderColor: `${theme.primary}60`, color: theme.primary }, 2769 title: "Toggle between Pentagon Groove Radar and Vinyl Graphic [G]", 2770 children: [ 2771 deckView === "radar" ? /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 11 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Radar, { size: 11 }), 2772 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: deckView === "radar" ? "RADAR" : "VINYL" }), 2773 /* @__P
2773URE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] text-zinc-500", children: "[G]" }) 2774 ] 2775 } 2776 ) 2777 ] }) 2778 ] }), 2779 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative overflow-hidden rounded-xl p-3 my-1.5 border-4 ${theme.bannerBorderClass} ${theme.bannerBgClass} shadow-xl shrink-0 group`, children: [ 2780 /* @__PURE__ */ jsxRuntimeExports.jsx( 2781 "div", 2782 { 2783 className: "absolute inset-0 opacity-15 pointer-events-none", 2784 style: { 2785 backgroundImage: "repeating-linear-gradient(0deg, #000, #000 2px, transparent 2px, transparent 4px)", 2786 backgroundSize: "100% 4px" 2787 } 2788 } 2789 ), 2790 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-1 left-1 w-1.5 h-1.5 rounded-full bg-zinc-400 border border-zinc-700 shadow-sm" }),
2791 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-zinc-400 border border-zinc-700 shadow-sm" }), 2792 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-1 left-1 w-1.5 h-1.5 rounded-full bg-zinc-400 border border-zinc-700 shadow-sm" }), 2793 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-1 right-1 w-1.5 h-1.5 rounded-full bg-zinc-400 border border-zinc-700 shadow-sm" }), 2794 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-10 flex items-center justify-between gap-3", children: [ 2795 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [ 2796 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-0.5", children: [ 2797 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black uppercase px-2 py-0.5 rounded-full bg-black/50 border border-white/20 tracking-widest", style: { color: theme.primary }, children: selectedSong.category || theme.eraLabel }), 2798 selectedSong && watchLaterIds.has(String(selectedSong.id)) && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[9px] font-black uppercase px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-300 border border-amber-400/50 tracking-wider flex items-center gap-1", children: [
2799 /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { size: 9, className: "fill-current" }), 2800 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "SAVED" }) 2801 ] }) 2802 ] }), 2803 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg sm:text-xl font-black italic tracking-wide text-white drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)] truncate", "data-deck-title": true, children: selectedSong.title }), 2804 selectedSong.artist && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs font-bold mt-0.5 truncate", style: { color: `${theme.primary}ee` }, children: [ 2805 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "/" }), 2806 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 truncate", children: selectedSong.artist }) 2807 ] }) 2808 ] }), 2809 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 shrink-0", children: [
2810 /* @__PURE__ */ jsxRuntimeExports.jsx( 2811 "button", 2812 { 2813 type: "button", 2814 onClick: (e) => { 2815 e.stopPropagation(); 2816 handleToggleBookmark(); 2817 }, 2818 className: `p-2 rounded-xl border-2 transition-all active:scale-95 flex items-center justify-center cursor-pointer ${selectedSong && watchLaterIds.has(String(selectedSong.id)) ? "bg-amber-400 text-slate-950 border-amber-300 shadow-[0_0_15px_rgba(245,158,11,0.6)]" : "bg-black/60 border-zinc-700 hover:border-amber-400/80 text-zinc-400 hover:text-amber-300"}`, 2819 title: selectedSong && watchLaterIds.has(String(selectedSong.id)) ? "Remove from Bookmarks [B]" : "Save to Bookmarks [B]", 2820 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { size: 16, className: selectedSong && watchLaterIds.has(String(selectedSong.id)) ? "fill-current" : "" }) 2821 } 2822 ),
2823 /* @__PURE__ */ jsxRuntimeExports.jsx( 2824 "button", 2825 { 2826 type: "button", 2827 onClick: (e) => { 2828 e.stopPropagation(); 2829 handleShareSong(); 2830 }, 2831 className: `p-2 rounded-xl border-2 transition-all active:scale-95 flex items-center justify-center cursor-pointer ${isCopiedShare ? "bg-emerald-500 text-black border-emerald-400 shadow-[0_0_15px_rgba(16,185,129,0.6)]" : "bg-black/60 border-zinc-700 hover:border-cyan-400/80 text-zinc-400 hover:text-cyan-300"}`, 2832 title: "Share Game Link [Copies /games?track=id]", 2833 children: isCopiedShare ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 16, className: "stroke-[3]" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { size: 16 }) 2834 } 2835 ), 2836 /* @__PURE__ */ jsxRuntimeExports.jsx( 2837 "button", 2838 { 2839 type: "button", 2840 onClick: (e) => { 2841 e.stopPropagation(); 2842 handleOpenWatchPage(); 2843 }, 2844 className: "p-2 rounded-xl border-2 border-zinc-700 hover:border-pink-400/80 bg-black/60 text-zinc-400 hover:text-pink-300 transition-all active:scale-95 flex items-center justify-center cursor-pointer", 2845 title: "Open Track Watch & Sheet Music Page in New Tab", 2846 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 16 }) 2847 } 2848 ),
2849 /* @__PURE__ */ jsxRuntimeExports.jsx( 2850 "button", 2851 { 2852 onClick: togglePreview, 2853 className: "shrink-0 p-2.5 rounded-xl border-2 transition-all active:scale-95 shadow-lg flex items-center justify-center cursor-pointer ml-0.5", 2854 style: { 2855 backgroundColor: isPlayingPreview ? `${theme.primary}30` : "rgba(0,0,0,0.6)", 2856 borderColor: theme.primary, 2857 color: theme.primary, 2858 boxShadow: `0 0 15px ${theme.primary}50` 2859 }, 2860 title: "Preview Audio [Space]", 2861 children: isPlayingPreview ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 18, className: "fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 18, className: "fill-current ml-0.5" }) 2862 } 2863 ) 2864 ] }) 2865 ] }) 2866 ] }),
2867 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "my-auto flex flex-col md:flex-row items-center justify-around gap-3 py-1 overflow-hidden", children: [ 2868 deckView === "radar" ? ( 2869 /* Pentagon Groove Radar */ 2870 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative flex flex-col items-center justify-center p-2 rounded-2xl bg-black/40 border border-zinc-800/80 shadow-2xl shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2871 ArcadeGrooveRadar, 2872 { 2873 stats: grooveRadarStats, 2874 theme, 2875 size: 195 2876 } 2877 ) }) 2878 ) : ( 2879 /* Holographic Spinning Vinyl Record */ 2880 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative group cursor-pointer shrink-0", onClick: togglePreview, title: "Click to Toggle Preview [Space]", children: [ 2881 /* @__PURE__ */ jsxRuntimeExports.jsx( 2882 "div", 2883 { 2884 className: `w-36 h-36 md:w-40 md:h-40 rounded-full bg-gradient-to-tr from-slate-900 via-slate-950 to-cyan-950 border-4 shadow-xl flex items-center justify-center transition-transform ${isPlayingPreview ? "animate-spin [animation-duration:4s]" : "group-hover:scale-105"}`, 2885 style: { borderColor: theme.primary, boxShadow: `0 0 30px ${theme.primary}40` }, 2886 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-28 h-28 md:w-32 md:h-32 rounded-full border border-white/10 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-18 h-18 md:w-20 md:h-20 rounded-full border border-pink-500/30 flex items-center justify-center bg-slate-900", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 32, style: { color: theme.primary } }) }) }) 2887 } 2888 ),
2889 /* @__PURE__ */ jsxRuntimeExports.jsx( 2890 "button", 2891 { 2892 className: "absolute inset-0 m-auto w-10 h-10 rounded-full text-slate-950 flex items-center justify-center shadow-lg active:scale-95 transition-all cursor-pointer", 2893 style: { backgroundColor: theme.primary }, 2894 children: isPlayingPreview ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 18, className: "fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 18, className: "fill-current ml-0.5" }) 2895 } 2896 ) 2897 ] }) 2898 ), 2899 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col gap-2 w-full max-w-sm", children: [ 2900 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2 text-xs", children: [ 2901 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 rounded-xl bg-slate-900/80 border border-slate-800", children: [
2902 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 block font-bold", children: t("gameCenter.tempo", "TEMPO") }), 2903 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm font-black", style: { color: theme.primary }, children: [ 2904 "BPM ", 2905 selectedSong.bpm 2906 ] }) 2907 ] }), 2908 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 rounded-xl bg-slate-900/80 border border-slate-800", children: [ 2909 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 block font-bold", children: t("gameCenter.fullDuration", "FULL DURATION") }), 2910 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-black text-pink-300", children: formatArcadeDuration(selectedSong.duration) }) 2911 ] }) 2912 ] }), 2913 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2.5 rounded-xl bg-slate-900/90 border border-zinc-800 flex flex-col gap-1.5", children: [
2914 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-[10px] font-bold text-zinc-400", children: [ 2915 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-wider", children: t("gameCenter.difficulty", "DIFFICULTY") }), 2916 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500 font-mono", children: [ 2917 t("gameCenter.stars", "STARS"), 2918 ": ", 2919 selectedSong.difficultyStars, 2920 "/5" 2921 ] }) 2922 ] }), 2923 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-2.5 py-1.5 rounded-lg bg-black/60 border border-zinc-800/80", children: [ 2924 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1.5", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 2925 HamieFaceIcon, 2926 { 2927 size: 16, 2928 active: i < selectedSong.difficultyStars, 2929 variant: "don", 2930 title: `Difficulty: ${selectedSong.difficultyStars}/5` 2931 }, 2932 i 2933 )) }),
2934 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-black px-2 py-0.5 rounded font-mono ${selectedSong.difficultyStars >= 5 ? "bg-red-950 text-red-400 border border-red-800/60" : selectedSong.difficultyStars === 4 ? "bg-amber-950 text-amber-300 border border-amber-800/60" : selectedSong.difficultyStars === 3 ? "bg-emerald-950 text-emerald-300 border border-emerald-800/60" : "bg-blue-950 text-cyan-300 border border-cyan-800/60"}`, children: selectedSong.difficultyStars >= 5 ? "MANIAC" : selectedSong.difficultyStars === 4 ? "TRICK" : selectedSong.difficultyStars === 3 ? "STANDARD" : "BASIC" }) 2935 ] }) 2936 ] }), 2937 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 rounded-xl bg-slate-900/90 border border-zinc-800 flex flex-col gap-1", children: [ 2938 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-[10px] font-bold uppercase", style: { color: theme.primary }, children: [
2939 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.previewTime", "CHORUS PREVIEW") }), 2940 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono", children: [ 2941 formatArcadeDuration(previewCurrentTime), 2942 " / 0:25" 2943 ] }) 2944 ] }), 2945 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-end justify-between h-6 gap-1 pt-1", children: [1, 2, 3, 4, 5, 6, 7, 8].map((band) => { 2946 const height = isPlayingPreview ? Math.max(15, Math.floor(Math.sin(previewCurrentTime * 6 + band) * 40 + 55)) : 10; 2947 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2948 "div", 2949 { 2950 className: "flex-1 rounded-t-sm transition-all duration-75", 2951 style: { 2952 height: `${height}%`, 2953 backgroundImage: `linear-gradient(to top, ${theme.primary}, ${theme.secondary}, ${theme.accent})` 2954 } 2955 }, 2956 band 2957 ); 2958 }) }) 2959 ] }) 2960 ] }) 2961 ] }),
2962 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-2 border-t border-slate-800/80 flex items-center justify-between gap-4 shrink-0", children: [ 2963 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-slate-400 flex items-center gap-2", children: [ 2964 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [ 2965 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 font-bold text-[10px]", style: { color: theme.primary }, children: "SPACE" }), 2966 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "PREVIEW" }) 2967 ] }), 2968 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [ 2969 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 font-bold text-[10px]", style: { color: theme.primary }, children: "ENTER" }),
2970 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "LOCK" }) 2971 ] }) 2972 ] }), 2973 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2974 "button", 2975 { 2976 "data-proceed-cabinet": "true", 2977 onClick: () => { 2978 if (previewAudioRef.current) previewAudioRef.current.pause(); 2979 arcadeAudio.duckBGM(false); 2980 arcadeAudio.playStageStart(); 2981 onProceedToCabinet(); 2982 }, 2983 className: "px-6 py-2.5 rounded-xl text-black font-black text-xs tracking-widest uppercase shadow-xl active:scale-95 transition-all flex items-center gap-2 cursor-pointer", 2984 style: { 2985 backgroundImage: `linear-gradient(to right, ${theme.primary}, ${theme.secondary})`, 2986 boxShadow: `0 0 25px ${theme.primary}60` 2987 }, 2988 children: [
2989 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "SELECT GAME CABINET [ENTER]" }), 2990 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 16 }) 2991 ] 2992 } 2993 ) 2994 ] }) 2995 ] }), 2996 activeTab === "bookmarks" && !selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col items-center justify-center p-8 text-center my-auto", children: [ 2997 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 rounded-full bg-amber-500/10 border-2 border-amber-400/30 flex items-center justify-center mb-3 shadow-[0_0_20px_rgba(245,158,11,0.2)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { className: "w-8 h-8 text-amber-400 animate-pulse" }) }), 2998 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-black text-amber-300 tracking-wider uppercase mb-1", children: t("gameCenter.bookmarksVault", "BOOKMARKS VAULT") }),
2999 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 max-w-sm mb-4", children: t("gameCenter.bookmarksDesc", "Bookmark your favorite tunes with [B] or the bookmark button to build your personal arcade playlist.") }), 3000 /* @__PURE__ */ jsxRuntimeExports.jsx( 3001 "button", 3002 { 3003 onClick: () => { 3004 arcadeAudio.playSelect(); 3005 setActiveTab("site"); 3006 }, 3007 className: "px-4 py-2 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 text-white font-black text-xs tracking-wider transition-all active:scale-95 shadow-[0_0_15px_rgba(6,182,212,0.4)] cursor-pointer", 3008 children: t("gameCenter.browseJukebox", "BROWSE ARCADE JUKEBOX") 3009 } 3010 ) 3011 ] }), 3012 activeTab === "upload" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col items-center justify-center p-6 border-2 border-dashed border-pink-500/40 rounded-2xl bg-slate-950/60 text-center font-mono", children: [
3013 /* @__PURE__ */ jsxRuntimeExports.jsx( 3014 "input", 3015 { 3016 type: "file", 3017 ref: fileInputRef, 3018 onChange: handleFileChange, 3019 accept: ".mid,.midi", 3020 className: "hidden" 3021 } 3022 ), 3023 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3024 "div", 3025 { 3026 onDragOver: (e) => { 3027 e.preventDefault(); 3028 setDragOver(true); 3029 }, 3030 onDragLeave: () => setDragOver(false), 3031 onDrop: handleDrop, 3032 className: `w-full max-w-md p-6 rounded-2xl flex flex-col items-center gap-3 transition-all ${dragOver ? "bg-pink-950/40 scale-102 border border-pink-500" : "bg-slate-900/60"}`, 3033 children: [ 3034 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 rounded-2xl bg-pink-500/10 border border-pink-500/30 text-pink-400 shadow-[0_0_20px_rgba(236,72,153,0.3)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(FileMusic, { size: 36, className: "animate-pulse" }) }),
3035 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3036 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-black text-white tracking-wider mb-1", children: "DROP .MID / .MIDI CARTRIDGE" }), 3037 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-pink-400 font-bold uppercase tracking-widest", children: "AUTOPARSER READY" }) 3038 ] }), 3039 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3040 "button", 3041 { 3042 onClick: () => { 3043 var _a; 3044 return (_a = fileInputRef.current) == null ? void 0 : _a.click(); 3045 }, 3046 disabled: isParsingFile, 3047 className: "flex items-center gap-2 px-5 py-2.5 rounded-xl bg-gradient-to-r from-pink-500 to-rose-600 hover:from-pink-400 text-white font-black text-xs tracking-wider shadow-[0_0_20px_rgba(244,63,94,0.4)] active:scale-95 transition-all cursor-pointer", 3048 children: [ 3049 isParsingFile ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin", size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14 }),
3050 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isParsingFile ? "PARSING..." : "CHOOSE LOCAL MIDI FILE" }) 3051 ] 3052 } 3053 ) 3054 ] 3055 } 3056 ) 3057 ] }), 3058 activeTab === "url" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full flex flex-col items-center justify-center p-6 border-2 border-amber-500/40 rounded-2xl bg-slate-950/60 font-mono", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleUrlSubmit, className: "w-full max-w-md flex flex-col gap-4", children: [ 3059 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-amber-400 text-xs font-black tracking-widest uppercase", children: [ 3060 /* @__PURE__ */ jsxRuntimeExports.jsx(Link, { size: 16 }), 3061 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "CYBER TERMINAL // URL INGESTION" }) 3062 ] }),
3063 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3", children: [ 3064 /* @__PURE__ */ jsxRuntimeExports.jsx( 3065 "input", 3066 { 3067 type: "url", 3068 required: true, 3069 value: urlInput, 3070 onChange: (e) => setUrlInput(e.target.value), 3071 placeholder: "https://hamienet.com/track/1234 or https://site.com/song.mid", 3072 className: "w-full px-4 py-2.5 rounded-xl bg-slate-900 border border-amber-500/40 text-amber-200 placeholder-slate-600 text-xs focus:outline-none focus:border-amber-400" 3073 } 3074 ), 3075 /* @__PURE__ */ jsxRuntimeExports.jsx( 3076 "button", 3077 { 3078 type: "submit", 3079 disabled: isParsingFile || !urlInput.trim(), 3080 className: "w-full py-2.5 rounded-xl bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-400 text-white font-black text-xs tracking-wider shadow-[0_0_15px_rgba(245,158,11,0.4)] active:scale-95 transition-all", 3081 children: isParsingFile ? "FETCHING..." : "LOAD URL INTO ARCADE" 3082 } 3083 ) 3084 ] }), 3085 urlError && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-red-400 bg-red-950/50 border border-red-500/40 px-3 py-2 rounded-lg", children: urlError }) 3086 ] }) }) 3087 ] 3088 } 3089 ),
3090 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lg:col-span-5 flex flex-col h-full overflow-hidden relative select-none pr-1", children: [ 3091 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 mb-2 shrink-0 px-1", children: [ 3092 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full", children: [ 3093 /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-cyan-400 w-3.5 h-3.5" }), 3094 /* @__PURE__ */ jsxRuntimeExports.jsx( 3095 "input", 3096 { 3097 type: "text", 3098 value: searchQuery, 3099 onChange: (e) => setSearchQuery(e.target.value), 3100 placeholder: "FILTER TRACKS...", 3101 className: "w-full pl-9 pr-3 py-1.5 rounded-lg bg-slate-900/90 border border-cyan-500/40 text-cyan-200 placeholder-slate-500 text-xs font-mono focus:outline-none focus:border-cyan-400 focus:shadow-[0_0_12px_rgba(6,182,212,0.4)]" 3102 } 3103 ) 3104 ] }),
3105 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1 overflow-x-auto custom-scrollbar text-[10px]", children: CATEGORIES.map((cat) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3106 "button", 3107 { 3108 onClick: () => { 3109 arcadeAudio.playSelect(); 3110 setSelectedCategory(cat.key); 3111 }, 3112 className: `px-2 py-1 rounded font-bold whitespace-nowrap transition-all ${selectedCategory === cat.key ? "bg-cyan-500 text-slate-950 font-black shadow-[0_0_10px_rgba(6,182,212,0.6)]" : "bg-slate-900/80 text-slate-400 border border-slate-800 hover:text-cyan-300"}`, 3113 children: cat.label 3114 }, 3115 cat.key 3116 )) }) 3117 ] }), 3118 activeTab === "bookmarks" ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 3119 "button", 3120 { 3121 onClick: () => { 3122 arcadeAudio.playRoulette(); 3123 setRefreshTrigger((v) => v + 1); 3124 }, 3125 disabled: isLoadingTracks, 3126 className: "w-full py-1.5 px-3 rounded-lg bg-gradient-to-r from-amber-500/20 via-yellow-500/30 to-amber-500/20 hover:from-amber-500/30 hover:via-yellow-500/40 hover:to-amber-500/30 border border-amber-400/50 hover:border-amber-300 text-amber-300 flex items-center justify-center gap-2 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer shadow-[0_0_15px_rgba(245,158,11,0.25)] hover:shadow-[0_0_25px_rgba(245,158,11,0.45)] mb-1.5 shrink-0", 3127 title: "Refresh Bookmarks list [X]", 3128 children: [
3129 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 13, className: isLoadingTracks ? "animate-spin text-amber-300" : "text-amber-400" }), 3130 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.refreshBookmarks", "REFRESH BOOKMARKS [X]") }) 3131 ] 3132 } 3133 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 3134 "button", 3135 { 3136 onClick: handleSwap50Songs, 3137 disabled: isLoadingTracks, 3138 className: "w-full py-1.5 px-3 rounded-lg bg-gradient-to-r from-amber-500/20 via-orange-500/30 to-amber-500/20 hover:from-amber-500/30 hover:via-orange-500/40 hover:to-amber-500/30 border border-amber-400/50 hover:border-amber-300 text-amber-300 flex items-center justify-center gap-2 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer shadow-[0_0_15px_rgba(245,158,11,0.25)] hover:shadow-[0_0_25px_rgba(245,158,11,0.45)] mb-1.5 shrink-0", 3139 title: "Swap for 50 new random tracks [X]", 3140 children: [
3141 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 13, className: isLoadingTracks ? "animate-spin text-amber-300" : "text-amber-400" }), 3142 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.swap50Songs", "SWAP 50 SONGS [X]") }) 3143 ] 3144 } 3145 ), 3146 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3147 "button", 3148 { 3149 onClick: () => stepWheel(-1), 3150 disabled: tracks.length === 0, 3151 className: "w-full py-1 rounded bg-slate-900/90 border border-cyan-500/30 hover:border-cyan-400 text-cyan-300 flex items-center justify-center gap-1 text-[10px] font-black uppercase tracking-widest shrink-0 transition-all active:scale-95 cursor-pointer mb-1 shadow-md", 3152 children: [ 3153 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14 }), 3154 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â² UP [W]" }) 3155 ] 3156 } 3157 ), 3158 /* @__PURE__ */ jsxRuntimeExports.jsx( 3159 "div", 3160 { 3161 id: "arcade-song-wheel-list", 3162 ref: wheelContainerRef, 3163 onPointerDown: handlePointerDown, 3164 onPointerMove: handlePointerMove, 3165 onPointerUp: handlePointerUp, 3166 onPointerCancel: handlePointerUp, 3167 style: { perspective: 900 }, 3168 className: "flex-1 relative my-1 overflow-hidden select-none touch-none cursor-grab active:cursor-grabbing flex flex-col justify-center", 3169 children: isLoadingTracks ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col items-center justify-center gap-2 py-10", children: [
3170 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-6 h-6 text-cyan-400 animate-spin" }), 3171 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-cyan-300 tracking-widest uppercase animate-pulse", children: t("gameCenter.loadingWheel", "LOADING WHEEL...") }) 3172 ] }) : tracks.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full flex flex-col items-center justify-center text-center p-6 border border-dashed border-slate-800 rounded-xl", children: activeTab === "bookmarks" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3173 /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { className: "w-8 h-8 text-amber-400/60 mb-2 animate-bounce" }), 3174 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-amber-300 font-bold mb-1", children: t("gameCenter.noBookmarks", "NO BOOKMARKED SONGS") }), 3175 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-slate-400 max-w-[220px]", children: [ 3176 "Press ",
3177 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1 py-0.5 rounded bg-slate-800 border border-slate-700 text-amber-400 font-bold", children: "[B]" }), 3178 " or tap the bookmark icon on any song to save it!" 3179 ] }) 3180 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3181 /* @__PURE__ */ jsxRuntimeExports.jsx(Music2, { className: "w-8 h-8 text-slate-600 mb-2" }), 3182 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-slate-400 font-bold", children: t("gameCenter.noTracksFound", "NO TRACKS FOUND") }) 3183 ] }) }) : ( 3184 // Render 11 infinite cyclic virtual slots (-5 to +5 around displayPos) 3185 [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5].map((k) => { 3186 var _a; 3187 const centerSlot = Math.round(displayPos); 3188 const slotIdx = centerSlot + k; 3189 const delta = slotIdx - displayPos; 3190 const absDelta = Math.abs(delta); 3191 const normalizedIdx = (slotIdx % tracks.length + tracks.length) % tracks.length; 3192 const track = tracks[normalizedIdx]; 3193 if (!track) return null; 3194 const isSelected = absDelta < 0.45; 3195 const isAbove = delta < -0.3; 3196 const curveX = isSelected ? 0 : Math.min(32, Math.round(Math.pow(absDelta, 1.2) * 6) + Math.round(12 * Math.min(1, absDelta))); 3197 const opacity = isSelected ? 1 : Math.max(0.2, 1 - absDelta * 0.15); 3198 const zIndex = isSelected ? 30 : Math.max(1, Math.round(25 - absDelta * 4)); 3199 const stars = track._cachedStars ?? calculateStars(track); 3200 const tempo = track._cachedTempo ?? Math.round(((_a = track.midiMetadata) == null ? void 0 : _a.tempo) || 120); 3201 const gradeBadge = stars >= 4 ? "AA" : stars === 3 ? "A" : "NO DATA"; 3202 const kanjiBadge = stars >= 5 ? "鬼" : stars === 4 ? "æ¿" : stars === 3 ? "è¸" : "楽"; 3203 const titleColor = isSelected ? "#ffffff" : isAbove ? "#ffe600" : "#00ff66"; 3204 const borderedTitleStyle = { 3205 paintOrder: "stroke fill", 3206 WebkitTextStroke: isSelected ? "3px #000000" : "2.2px #000000", 3207 color: titleColor, 3208 textShadow: isSelected ? "0 0 16px rgba(0, 240, 255, 0.9), 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 0 3px 6px rgba(0,0,0,0.9)" : "2px 2px 0 #000, -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 0 2px 4px rgba(0,0,0,0.8)" 3209 }; 3210 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3211 "div", 3212 { 3213 onClick: () => { 3214 if (!dragMovedRef.current) { 3215 if (isSelected) { 3216 if (previewAudioRef.current) previewAudioRef.current.pause(); 3217 arcadeAudio.duckBGM(false); 3218 arcadeAudio.playStageStart(); 3219 onProceedToCabinet(); 3220 } else { 3221 setWheelTarget(slotIdx, true); 3222 } 3223 } 3224 }, 3225 "data-cartridge-id": track.id, 3226 "data-cartridge-idx": normalizedIdx, 3227 "data-slot-k": k, 3228 style: { 3229 position: "absolute", 3230 top: "50%", 3231 left: `${6 + curveX}px`, 3232 right: "8px", 3233 transform: `translateY(-50%) translateY(${delta * 60}px) scaleY(${isSelected ? 1.05 : 0.95})`, 3234 transformOrigin: "center center", 3235 zIndex, 3236 opacity, 3237 ...isSelected ? { 3238 boxShadow: `0 0 30px ${theme.cartridgeActiveGlow}, inset 0 0 16px rgba(255,255,255,0.35)`, 3239 borderColor: "#67e8f9" 3240 } : {} 3241 }, 3242 className: `group flex items-center justify-between cursor-pointer select-none transition-colors duration-100 ${isSelected ? "py-2 px-3 rounded-l-full rounded-r-xl border-y-2 border-l-2 bg-gradient-to-r from-blue-700 via-cyan-600 to-blue-950/90 shadow-2xl ring-1 ring-white/60" : "py-1.5 px-2 rounded-lg hover:bg-white/10 border border-transparent"}`, 3243 children: [
3244 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5 min-w-0 flex-1", children: [ 3245 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 flex items-center", children: isSelected ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 rounded-md flex items-center justify-center bg-gradient-to-tr from-cyan-400 to-white shadow-[0_0_12px_rgba(0,240,255,0.9)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 18, className: "stroke-[3.5] text-slate-950 ml-0.5" }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-5 h-5 rounded flex items-center justify-center bg-black/40 border border-white/10 group-hover:border-white/30 transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 14, className: `stroke-[3] ${isAbove ? "text-yellow-400" : "text-emerald-400"}` }) }) }), 3246 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [
3247 /* @__PURE__ */ jsxRuntimeExports.jsx( 3248 "h4", 3249 { 3250 className: "text-sm sm:text-base font-black italic tracking-wide truncate select-none uppercase drop-shadow-md", 3251 style: borderedTitleStyle, 3252 children: track.title 3253 } 3254 ), 3255 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs truncate", children: [ 3256 (track._cachedArtist || track.artist) && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3257 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `truncate max-w-[120px] sm:max-w-[180px] font-bold italic tracking-wide ${isSelected ? "text-cyan-100" : isAbove ? "text-yellow-200/80" : "text-emerald-200/80"}`, children: [ 3258 "/ ", 3259 track._cachedArtist || track.artist 3260 ] }),
3261 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-40 text-white", children: "â¢" }) 3262 ] }), 3263 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-black font-mono text-[11px] ${isSelected ? "text-white" : isAbove ? "text-yellow-300" : "text-emerald-300"}`, children: [ 3264 "BPM ", 3265 tempo 3266 ] }) 3267 ] }) 3268 ] }) 3269 ] }), 3270 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 shrink-0 ml-2", children: [ 3271 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx( 3272 "button", 3273 { 3274 type: "button", 3275 onClick: (e) => { 3276 e.stopPropagation(); 3277 if (previewAudioRef.current) previewAudioRef.current.pause(); 3278 arcadeAudio.duckBGM(false); 3279 arcadeAudio.playStageStart(); 3280 onProceedToCabinet(); 3281 }, 3282 className: "px-2 py-0.5 rounded bg-yellow-400 hover:bg-yellow-300 active:scale-95 text-black font-black text-[9px] tracking-wider uppercase animate-pulse shrink-0 shadow-md cursor-pointer transition-all", 3283 title: "Select Game Cabinet [ENTER]", 3284 children: "â PLAY" 3285 } 3286 ),
3287 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsxs( 3288 "button", 3289 { 3290 type: "button", 3291 onClick: (e) => { 3292 e.stopPropagation(); 3293 if (isPlayingPreview && previewAudioRef.current) { 3294 previewAudioRef.current.pause(); 3295 setIsPlayingPreview(false); 3296 arcadeAudio.duckBGM(false); 3297 } else if (selectedSongRef.current) { 3298 playChorusPreviewRef.current(selectedSongRef.current); 3299 } 3300 }, 3301 className: "flex items-end gap-0.5 h-4 px-1.5 py-0.5 rounded border border-cyan-300 bg-cyan-400/20 hover:bg-cyan-400/40 cursor-pointer transition-all active:scale-95 shrink-0", 3302 title: isPlayingPreview ? "Pause audio preview [SPACE]" : "Play audio preview [SPACE]", 3303 children: [
3304 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-0.5 h-2 bg-cyan-300 ${isPlayingPreview ? "animate-pulse" : "h-1.5"}` }), 3305 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-0.5 h-3 bg-cyan-300 ${isPlayingPreview ? "animate-pulse delay-75" : "h-2"}` }), 3306 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-0.5 h-1.5 bg-cyan-300 ${isPlayingPreview ? "animate-pulse delay-150" : "h-1"}` }) 3307 ] 3308 } 3309 ), 3310 theme.badgeStyle === "grade" ? /* @__PURE__ */ jsxRuntimeExports.jsx( 3311 "span", 3312 { 3313 className: `px-1.5 py-0.5 rounded-full font-black text-[9px] border shadow-sm shrink-0 ${gradeBadge === "NO DATA" ? "bg-blue-950/60 text-cyan-400 border-cyan-800/40 text-[8px]" : "bg-cyan-400/20 text-cyan-300 border-cyan-400/80 shadow-[0_0_8px_rgba(0,240,255,0.4)]"}`, 3314 children: gradeBadge 3315 } 3316 ) : /* @__PURE__ */ jsxRuntimeExports.jsx( 3317 "span", 3318 { 3319 className: `px-1.5 py-0.5 rounded font-black text-[10px] shadow-md shrink-0 ${kanjiBadge === "鬼" ? "bg-red-600 text-white" : kanjiBadge === "æ¿" ? "bg-emerald-500 text-black font-extrabold" : kanjiBadge === "è¸" ? "bg-amber-500 text-black font-extrabold" : "bg-blue-500 text-white"}`, 3320 children: kanjiBadge 3321 } 3322 ),
3323 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-0.5 shrink-0", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3324 HamieFaceIcon, 3325 { 3326 size: 11, 3327 active: i < stars, 3328 variant: theme.id === "sunset" ? "neon" : theme.id === "5thmix" ? "don" : "gold", 3329 title: `Difficulty: ${stars}/5` 3330 }, 3331 i 3332 )) }) 3333 ] }) 3334 ] 3335 }, 3336 slotIdx 3337 ); 3338 }) 3339 ) 3340 } 3341 ), 3342 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3343 "button", 3344 { 3345 onClick: () => stepWheel(1), 3346 disabled: tracks.length === 0, 3347 className: "w-full py-1 rounded bg-slate-900/90 border border-cyan-500/30 hover:border-cyan-400 text-cyan-300 flex items-center justify-center gap-1 text-[10px] font-black uppercase tracking-widest shrink-0 transition-all active:scale-95 cursor-pointer mt-1", 3348 children: [
3349 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 14 }), 3350 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 3351 "â¼ ", 3352 t("gameCenter.down", "DOWN"), 3353 " [S]" 3354 ] }) 3355 ] 3356 } 3357 ) 3358 ] }) 3359 ] }), 3360 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 px-3 py-1.5 rounded-xl bg-slate-950/90 border border-cyan-500/30 text-[11px] font-mono text-slate-400 shrink-0", children: [ 3361 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3.5 flex-wrap", children: [ 3362 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-cyan-400 font-bold uppercase tracking-widest flex items-center gap-1", children: [ 3363 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 12 }), 3364 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "HAMIE ARCADE:" }) 3365 ] }), 3366 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-yellow-300 font-bold", children: [
3367 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-yellow-300 font-bold text-[10px]", children: "â² â¼ / W S" }), 3368 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.select", "SELECT") }) 3369 ] }), 3370 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-cyan-300 font-bold", children: [ 3371 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-cyan-300 font-bold text-[10px]", children: "ENTER" }), 3372 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.decide", "DECIDE") }) 3373 ] }), 3374 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 3375 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-pink-300 font-bold text-[10px]", children: "SPACE" }),
3376 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.preview", "PREVIEW") }) 3377 ] }), 3378 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 3379 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-amber-300 font-bold text-[10px]", children: "R" }), 3380 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.roulette", "ROULETTE") }) 3381 ] }), 3382 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 3383 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-purple-300 font-bold text-[10px]", children: "T" }), 3384 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.style", "STYLE") }) 3385 ] }), 3386 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-amber-300 font-bold", children: [
3387 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-amber-300 font-bold text-[10px]", children: "B" }), 3388 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.bookmarks", "BOOKMARKS") }) 3389 ] }), 3390 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 3391 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-slate-900 border border-slate-700 text-emerald-300 font-bold text-[10px]", children: "X" }), 3392 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.swap", "SWAP") }) 3393 ] }) 3394 ] }), 3395 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-emerald-400 font-bold hidden md:flex items-center gap-1.5", children: [ 3396 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" }), 3397 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.eventMode", "EVENT MODE // ALL TRACKS UNLOCKED") }) 3398 ] }) 3399 ] }) 3400 ] }); 3401}; 3402const PIANO_SPEEDS = [1, 1.2, 1.5, 2]; 3403const TAIKO_DIFFICULTIES = ["kantan", "futsuu", "muzukashii", "oni"]; 3404const MEOW_CHOIR_DIFFICULTIES = ["kitten", "playful", "wildcat", "meowstro"]; 3405const RHYTHM_TEST_DIFFICULTIES = ["easy", "medium", "hard", "master"]; 3406const GameModeSelector = ({ 3407 selectedSong, 3408 onBackToSongSelect, 3409 onLaunchGame, 3410 theme = ARCADE_THEMES["5thmix"] 3411}) => { 3412 const { t } = useTranslation(); 3413 const [selectedModuleId, setSelectedModuleId] = reactExports.useState("piano-roll"); 3414 const [pianoSpeed, setPianoSpeed] = reactExports.useState(1.2); 3415 const [melodyOnly, setMelodyOnly] = reactExports.useState(false); 3416 const [taikoDifficulty, setTaikoDifficulty] = reactExports.useState("futsuu"); 3417 const [meowChoirDifficulty, setMeowChoirDifficulty] = reactExports.useState("playful"); 3418 const [rhythmDifficulty, setRhythmDifficulty] = reactExports.useState("medium"); 3419 const activeModule = GAME_MODULES.find((m) => m.id === selectedModuleId) || GAME_MODULES[0]; 3420 const handleLaunch = () => { 3421 arcadeAudio.playStageStart(); 3422 const settings = {}; 3423 if (selectedModuleId === "piano-roll") { 3424 settings.speed = pianoSpeed; 3425 settings.melodyOnly = melodyOnly; 3426 } else if (selectedModuleId === "taiko") { 3427 settings.difficulty = taikoDifficulty; 3428 } else if (selectedModuleId === "meow-choir") { 3429 settings.difficulty = meowChoirDifficulty; 3430 } else if (selectedModuleId === "rhythm-test") { 3431 settings.difficulty = rhythmDifficulty; 3432 } 3433 onLaunchGame(activeModule, settings); 3434 }; 3435 reactExports.useEffect(() => { 3436 const handleKeyDown = (e) => { 3437 const moduleIds = GAME_MODULES.map((m) => m.id); 3438 const curIdx = moduleIds.indexOf(selectedModuleId); 3439 if (e.key === "ArrowLeft" || e.key === "a" || e.key === "A") { 3440 e.preventDefault(); 3441 arcadeAudio.playWheelTick(); 3442 const nextIdx = (curIdx - 1 + moduleIds.length) % moduleIds.length; 3443 setSelectedModuleId(moduleIds[nextIdx]); 3444 } else if (e.key === "ArrowRight" || e.key === "d" || e.key === "D") { 3445 e.preventDefault(); 3446 arcadeAudio.playWheelTick(); 3447 const nextIdx = (curIdx + 1) % moduleIds.length; 3448 setSelectedModuleId(moduleIds[nextIdx]); 3449 } else if (e.key === "ArrowUp" || e.key === "w" || e.key === "W") { 3450 e.preventDefault(); 3451 arcadeAudio.playSelect(); 3452 if (selectedModuleId === "piano-roll") { 3453 const idx = PIANO_SPEEDS.indexOf(pianoSpeed); 3454 const next = Math.min(PIANO_SPEEDS.length - 1, idx + 1); 3455 setPianoSpeed(PIANO_SPEEDS[next]); 3456 } else if (selectedModuleId === "taiko") { 3457 const idx = TAIKO_DIFFICULTIES.indexOf(taikoDifficulty); 3458 const next = Math.min(TAIKO_DIFFICULTIES.length - 1, idx + 1); 3459 setTaikoDifficulty(TAIKO_DIFFICULTIES[next]); 3460 } else if (selectedModuleId === "meow-choir") { 3461 const idx = MEOW_CHOIR_DIFFICULTIES.indexOf(meowChoirDifficulty); 3462 const next = Math.min(MEOW_CHOIR_DIFFICULTIES.length - 1, idx + 1); 3463 setMeowChoirDifficulty(MEOW_CHOIR_DIFFICULTIES[next]); 3464 } else if (selectedModuleId === "rhythm-test") { 3465 const idx = RHYTHM_TEST_DIFFICULTIES.indexOf(rhythmDifficulty); 3466 const next = Math.min(RHYTHM_TEST_DIFFICULTIES.length - 1, idx + 1);
3467 setRhythmDifficulty(RHYTHM_TEST_DIFFICULTIES[next]); 3468 } 3469 } else if (e.key === "ArrowDown" || e.key === "s" || e.key === "S") { 3470 e.preventDefault(); 3471 arcadeAudio.playSelect(); 3472 if (selectedModuleId === "piano-roll") { 3473 const idx = PIANO_SPEEDS.indexOf(pianoSpeed); 3474 const next = Math.max(0, idx - 1); 3475 setPianoSpeed(PIANO_SPEEDS[next]); 3476 } else if (selectedModuleId === "taiko") { 3477 const idx = TAIKO_DIFFICULTIES.indexOf(taikoDifficulty); 3478 const next = Math.max(0, idx - 1); 3479 setTaikoDifficulty(TAIKO_DIFFICULTIES[next]); 3480 } else if (selectedModuleId === "meow-choir") { 3481 const idx = MEOW_CHOIR_DIFFICULTIES.indexOf(meowChoirDifficulty); 3482 const next = Math.max(0, idx - 1); 3483 setMeowChoirDifficulty(MEOW_CHOIR_DIFFICULTIES[next]); 3484 } else if (selectedModuleId === "rhythm-test") { 3485 const idx = RHYTHM_TEST_DIFFICULTIES.indexOf(rhythmDifficulty); 3486 const next = Math.max(0, idx - 1); 3487 setRhythmDifficulty(RHYTHM_TEST_DIFFICULTIES[next]); 3488 } 3489 } else if (e.key === "Enter" || e.key === " ") { 3490 e.preventDefault(); 3491 handleLaunch(); 3492 } else if (e.key === "Escape") { 3493 e.preventDefault(); 3494 arcadeAudio.playSelect(); 3495 onBackToSongSelect(); 3496 } 3497 }; 3498 window.addEventListener("keydown", handleKeyDown); 3499 return () => window.removeEventListener("keydown", handleKeyDown); 3500 }, [selectedModuleId, pianoSpeed, taikoDifficulty, meowChoirDifficulty, rhythmDifficulty, onBackToSongSelect]); 3501 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full max-h-[calc(100vh-80px)] flex flex-col justify-between overflow-hidden select-none font-mono text-white animate-in fade-in duration-300", children: [ 3502 /* @__PURE__ */ jsxRuntimeExports.jsx( 3503 "div", 3504 { 3505 className: "flex items-center justify-between gap-3 px-3 py-2 rounded-xl bg-slate-950/80 border-2 shadow-lg shrink-0 transition-all duration-300", 3506 style: { 3507 borderColor: `${theme.primary}55`, 3508 boxShadow: `0 0 15px ${theme.primary}20` 3509 }, 3510 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
3511 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3512 "button", 3513 { 3514 onClick: () => { 3515 arcadeAudio.playSelect(); 3516 onBackToSongSelect(); 3517 }, 3518 className: "flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-700 hover:border-cyan-400 text-slate-300 hover:text-white text-xs font-bold transition-all active:scale-95 cursor-pointer", 3519 children: [ 3520 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 14 }), 3521 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 3522 "[ESC] ", 3523 t("gameCenter.changeSong", "CHANGE SONG") 3524 ] }) 3525 ] 3526 } 3527 ), 3528 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3529 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 16, className: "animate-spin", style: { color: theme.primary } }), 3530 /* @__P
3530URE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3531 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold uppercase tracking-widest block", style: { color: theme.primary }, children: [ 3532 t("gameCenter.selectedTrack", "SELECTED TRACK"), 3533 " ⢠BPM ", 3534 selectedSong.bpm 3535 ] }), 3536 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-black text-white line-clamp-1", children: selectedSong.title }) 3537 ] }) 3538 ] }) 3539 ] }) 3540 } 3541 ), 3542 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 my-2 min-h-0 overflow-hidden", children: GAME_MODULES.map((module) => { 3543 var _a; 3544 const isSelected = module.id === selectedModuleId; 3545 const isPiano = module.id === "piano-roll"; 3546 const isTaiko = module.id === "taiko"; 3547 const isMeowChoir = module.id === "meow-choir"; 3548 const isRhythmTest = module.id === "rhythm-test"; 3549 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3550 "div", 3551 { 3552 onClick: () => { 3553 arcadeAudio.playWheelTick(); 3554 setSelectedModuleId(module.id); 3555 }, 3556 className: `group relative flex flex-col justify-between p-4 rounded-2xl border-2 transition-all duration-200 cursor-pointer overflow-hidden ${isSelected ? isPiano ? "border-cyan-400 bg-gradient-to-b from-cyan-950/70 via-slate-900/90 to-slate-950 shadow-[0_0_30px_rgba(6,182,212,0.4)] scale-[1.01]" : isTaiko ? "border-rose-400 bg-gradient-to-b from-rose-950/70 via-slate-900/90 to-slate-950 shadow-[0_0_30px_rgba(244,63,94,0.4)] scale-[1.01]" : isMeowChoir ? "border-purple-400 bg-gradient-to-b from-purple-950/70 via-slate-900/90 to-slate-950 shadow-[0_0_30px_rgba(168,85,247,0.4)] scale-[1.01]" : "border-amber-400 bg-gradient-to-b from-purple-950/70 via-slate-900/90 to-amber-950/50 shadow-[0_0_30px_rgba(245,158,11,0.4)] scale-[1.01]" : "border-slate-800 bg-slate-950/60 hover:border-slate-700 opacity-75 hover:opacity-100"}`, 3557 children: [
3558 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-3 border-b border-slate-800/80", children: [ 3559 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 3560 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-2 rounded-xl ${isPiano ? "bg-cyan-500/20 text-cyan-300 border border-cyan-500/40" : isTaiko ? "bg-rose-500/20 text-rose-300 border border-rose-500/40" : isMeowChoir ? "bg-purple-500/20 text-purple-300 border border-purple-500/40" : "bg-amber-500/20 text-amber-300 border border-amber-500/40"}`, children: isPiano ? /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 24 }) : isTaiko ? /* @__PURE__ */ jsxRuntimeExports.jsx(Drum, { size: 24 }) : isMeowChoir ? /* @__PURE__ */ jsxRuntimeExports.jsx(PawPrint, { size: 24 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Boxes, { size: 24 }) }), 3561 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3562 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 font-bold uppercase tracking-widest block", children: ((_a = module.cabinetStyle) == null ? void 0 : _a.marqueeText) || module.tagline }),
3563 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-black text-white tracking-wider", children: module.name }) 3564 ] }) 3565 ] }), 3566 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider ${isPiano ? "bg-cyan-950 border border-cyan-500/40 text-cyan-300" : isTaiko ? "bg-rose-950 border border-rose-500/40 text-rose-300" : isMeowChoir ? "bg-purple-950 border border-purple-500/40 text-purple-300" : "bg-amber-950 border border-amber-500/40 text-amber-300"}`, children: isPiano ? "RHYTHM SYNTH" : isTaiko ? "ARCADE DRUM" : isMeowChoir ? "KITTEN CHOIR" : "RHYTHM TEST" }) 3567 ] }), 3568 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative my-2 rounded-xl overflow-hidden border-2 border-slate-700/80 bg-slate-950 aspect-video flex flex-col items-center justify-center group/preview shadow-[inset_0_0_25px_rgba(0,0,0,0.9)]", children: [ 3569 isPiano && /* @__PURE__ */ jsxRuntimeExports.jsx( 3570 "img", 3571 { 3572 src: "/assets/games/preview_pianoroll.png", 3573 alt: "Piano Roll Rush Gameplay", 3574 className: "w-full h-full object-cover group-hover/preview:scale-105 transition-transform duration-500" 3575 } 3576 ), 3577 isTaiko && /* @__PURE__ */ jsxRuntimeExports.jsx( 3578 "img", 3579 { 3580 src: "/assets/games/preview_taiko.png", 3581 alt: "Taiko no Hamie Gameplay", 3582 className: "w-full h-full object-cover group-hover/preview:scale-105 transition-transform duration-500" 3583 } 3584 ), 3585 isMeowChoir && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full bg-gradient-to-br from-indigo-950 via-slate-900 to-purple-950 flex flex-col items-center justify-center relative overflow-hidden group-hover/preview:scale-105 transition-transform duration-500", children: [
3586 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -top-6 -left-6 w-24 h-24 rounded-full bg-purple-500/20 blur-xl animate-pulse" }), 3587 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-6 -right-6 w-24 h-24 rounded-full bg-pink-500/20 blur-xl animate-pulse" }), 3588 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-2 left-6 w-8 h-8 rounded-full border-2 border-purple-300/80 bg-purple-400/20 shadow-[0_0_12px_rgba(192,132,252,0.6)] flex items-center justify-center text-xs animate-bounce", children: "âª" }), 3589 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-6 right-8 w-10 h-10 rounded-full border-2 border-amber-300/80 bg-amber-400/20 shadow-[0_0_12px_rgba(251,191,36,0.6)] flex items-center justify-center text-xs animate-pulse", children: "â " }), 3590 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-6 left-12 w-9 h-9 rounded-full border-2 border-pink-300/80 bg-pink-400/20 shadow-[0_0_12px_rgba(244,114,182,0.6)] flex items-center justify-center text-xs", children: "â«" }),
3591 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center z-10", children: [ 3592 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-14 h-14 rounded-full bg-purple-500/20 border-2 border-purple-400 flex items-center justify-center shadow-[0_0_20px_rgba(168,85,247,0.5)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PawPrint, { size: 28, className: "text-purple-300 animate-pulse" }) }), 3593 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-black text-purple-300 tracking-wider mt-1 uppercase", children: "MEOW CHOIR" }) 3594 ] }) 3595 ] }), 3596 isRhythmTest && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full bg-gradient-to-br from-purple-950 via-zinc-900 to-stone-950 flex flex-col items-center justify-center relative overflow-hidden group-hover/preview:scale-105 transition-transform duration-500", children: [ 3597 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1 p-2 bg-black/40 rounded-lg border border-purple-500/30", children: [1, 1, 1, 4, 1, 1, 1, 4, 2, 1, 2, 4, 2, 1, 2, 4].map((d, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3598 "div", 3599 { 3600 className: `w-3.5 h-3.5 rounded flex items-center justify-center ${i < 3 ? "bg-[#e05244]" : i === 3 ? "bg-purple-600 shadow-[0_0_6px_purple]" : "bg-zinc-700"}`, 3601 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1 h-1 rounded-full bg-white" }) 3602 }, 3603 i 3604 )) }),
3605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] font-black text-amber-300 tracking-wider mt-1.5 uppercase flex items-center gap-1", children: [ 3606 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â© = 160 BPM" }), 3607 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 3608 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "BONGO CAT" }) 3609 ] }) 3610 ] }), 3611 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/85 via-transparent to-black/30 pointer-events-none" }), 3612 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-2 left-3 right-3 flex items-center justify-between pointer-events-none", children: [ 3613 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] font-black uppercase tracking-wider text-white drop-shadow-[0_2px_4px_rgba(0,0,0,0.9)] flex items-center gap-1.5", children: [
3614 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 12, className: isPiano ? "text-cyan-400" : isTaiko ? "text-rose-400" : isMeowChoir ? "text-purple-400" : "text-amber-400" }), 3615 isPiano ? t("gameCenter.keyboardHighway", "KEYBOARD HIGHWAY // 88 KEYS") : isTaiko ? t("gameCenter.wadaikoDrum", "WADAIKO DRUM // FESTIVAL") : isMeowChoir ? t("gameCenter.meowChoirStage", "KITTEN STAGE // 4 KEYS") : "MEASURE GRID // BONGO CAT" 3616 ] }), 3617 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-mono font-bold px-1.5 py-0.5 rounded bg-black/60 backdrop-blur-sm border border-white/20 text-slate-300", children: t("gameCenter.preview", "PREVIEW") }) 3618 ] }) 3619 ] }), 3620 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-2 border-t border-slate-800/80", children: [ 3621 isPiano && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [ 3622 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-slate-300", children: [
3623 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-cyan-400", children: [ 3624 /* @__PURE__ */ jsxRuntimeExports.jsx(Gauge, { size: 13 }), 3625 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.scrollSpeed", "SCROLL SPEED") }) 3626 ] }), 3627 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-cyan-300", children: [ 3628 pianoSpeed.toFixed(1), 3629 "x" 3630 ] }) 3631 ] }), 3632 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1.5", children: PIANO_SPEEDS.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 3633 "button", 3634 { 3635 type: "button", 3636 onClick: (e) => { 3637 e.stopPropagation(); 3638 arcadeAudio.playSelect(); 3639 setSelectedModuleId(module.id); 3640 setPianoSpeed(s); 3641 }, 3642 className: `py-1 rounded text-xs font-bold transition-all border cursor-pointer ${pianoSpeed === s ? "bg-cyan-500 border-cyan-400 text-slate-950 font-black shadow-[0_0_10px_rgba(6,182,212,0.5)]" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-white"}`, 3643 children: [ 3644 s.toFixed(1), 3645 "x" 3646 ] 3647 }, 3648 s 3649 )) }) 3650 ] }), 3651 isTaiko && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
3652 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-slate-300", children: [ 3653 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-rose-400", children: [ 3654 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 13 }), 3655 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.difficulty", "DIFFICULTY") }) 3656 ] }), 3657 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-white", children: taikoDifficulty }) 3658 ] }), 3659 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1.5", children: TAIKO_DIFFICULTIES.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3660 "button", 3661 { 3662 type: "button", 3663 onClick: (e) => { 3664 e.stopPropagation(); 3665 arcadeAudio.playSelect(); 3666 setSelectedModuleId(module.id); 3667 setTaikoDifficulty(d); 3668 }, 3669 className: `py-1 rounded text-[10px] font-bold uppercase transition-all border cursor-pointer ${taikoDifficulty === d ? "bg-rose-500 border-rose-400 text-white font-black shadow-[0_0_10px_rgba(244,63,94,0.6)]" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-white"}`, 3670 children: d === "kantan" ? "EASY" : d === "futsuu" ? "NORM" : d === "muzukashii" ? "HARD" : "ONI" 3671 }, 3672 d 3673 )) }) 3674 ] }), 3675 isMeowChoir && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
3676 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-slate-300", children: [ 3677 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-purple-400", children: [ 3678 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 13 }), 3679 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.difficulty", "DIFFICULTY") }) 3680 ] }), 3681 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-white", children: meowChoirDifficulty }) 3682 ] }), 3683 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1.5", children: MEOW_CHOIR_DIFFICULTIES.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3684 "button", 3685 { 3686 type: "button", 3687 onClick: (e) => { 3688 e.stopPropagation(); 3689 arcadeAudio.playSelect(); 3690 setSelectedModuleId(module.id); 3691 setMeowChoirDifficulty(d); 3692 }, 3693 className: `py-1 rounded text-[10px] font-bold uppercase transition-all border cursor-pointer ${meowChoirDifficulty === d ? "bg-purple-500 border-purple-400 text-slate-950 font-black shadow-[0_0_10px_rgba(168,85,247,0.6)]" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-white"}`, 3694 children: d === "kitten" ? "KITTEN" : d === "playful" ? "PLAY" : d === "wildcat" ? "WILD" : "CHOIR" 3695 }, 3696 d 3697 )) }) 3698 ] }), 3699 isRhythmTest && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
3700 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-slate-300", children: [ 3701 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-amber-400", children: [ 3702 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 13 }), 3703 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.difficulty", "DIFFICULTY") }) 3704 ] }), 3705 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-white", children: rhythmDifficulty }) 3706 ] }), 3707 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1.5", children: RHYTHM_TEST_DIFFICULTIES.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3708 "button", 3709 { 3710 type: "button", 3711 onClick: (e) => { 3712 e.stopPropagation(); 3713 arcadeAudio.playSelect(); 3714 setSelectedModuleId(module.id); 3715 setRhythmDifficulty(d); 3716 }, 3717 className: `py-1 rounded text-[10px] font-bold uppercase transition-all border cursor-pointer ${rhythmDifficulty === d ? "bg-amber-500 border-amber-400 text-slate-950 font-black shadow-[0_0_10px_rgba(245,158,11,0.6)]" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-white"}`, 3718 children: d === "easy" ? "EASY" : d === "medium" ? "MED" : d === "hard" ? "HARD" : "MASTER" 3719 }, 3720 d 3721 )) }) 3722 ] }) 3723 ] }) 3724 ] 3725 }, 3726 module.id 3727 ); 3728 }) }), 3729 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3730 "div", 3731 { 3732 className: "flex items-center justify-between gap-4 p-3 rounded-xl bg-slate-950/95 border-2 shadow-xl shrink-0 transition-all duration-300", 3733 style: { 3734 borderColor: theme.secondary, 3735 boxShadow: `0 0 25px ${theme.secondary}40` 3736 }, 3737 children: [
3738 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5", children: [ 3739 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block w-2.5 h-2.5 rounded-full bg-emerald-400 animate-ping" }), 3740 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 3741 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-black text-white uppercase tracking-wider", children: t("gameCenter.stageReady", "STAGE READY") }), 3742 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-zinc-400 font-bold truncate max-w-sm", children: [ 3743 activeModule.name, 3744 " ⢠", 3745 selectedSong.title 3746 ] }) 3747 ] }) 3748 ] }), 3749 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3750 "button", 3751 { 3752 onClick: handleLaunch, 3753 className: "px-8 py-3 rounded-xl text-slate-950 font-black text-xs tracking-widest uppercase active:scale-95 transition-all flex items-center justify-center gap-2 shadow-xl cursor-pointer", 3754 style: { 3755 background: `linear-gradient(135deg, ${theme.secondary}, ${theme.primary})`, 3756 boxShadow: `0 0 25px ${theme.primary}80`, 3757 color: theme.id === "max2" ? "#022c22" : "#030712" 3758 }, 3759 children: [
3760 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { size: 16, className: "fill-current text-yellow-300 animate-bounce" }), 3761 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("gameCenter.pressStart", "PRESS START [ENTER]") }) 3762 ] 3763 } 3764 ) 3765 ] 3766 } 3767 ), 3768 /* @__PURE__ */ jsxRuntimeExports.jsx( 3769 "div", 3770 { 3771 className: "flex items-center justify-between gap-2 px-3 py-1 rounded-xl bg-slate-950/90 border text-[11px] font-mono text-slate-400 shrink-0 transition-all duration-300", 3772 style: { borderColor: `${theme.primary}40` }, 3773 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 text-[10px] flex-wrap", children: [ 3774 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold uppercase tracking-widest text-cyan-400", children: [ 3775 t("gameCenter.cabinetControls", "CABINET CONTROLS"), 3776 ":" 3777 ] }), 3778 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â ⺠CABINET" }),
3779 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 3780 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â² â¼ CONFIG" }), 3781 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 3782 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ENTER START" }), 3783 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 3784 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ESC BACK" }) 3785 ] }) 3786 } 3787 ) 3788 ] }); 3789}; 3790const MeowChoirStage = ({ 3791 score, 3792 combo, 3793 activeLaneHit, 3794 isSinging, 3795 isMissed, 3796 isFever 3797}) => { 3798 const singingNotes = reactExports.useMemo(() => { 3799 if (!isSinging) return []; 3800 return [ 3801 { id: 1, symbol: "âª", x: 42, y: 55, delay: 0 }, 3802 { id: 2, symbol: "â«", x: 57, y: 52, delay: 60 }, 3803 { id: 3, symbol: "âª", x: 38, y: 50, delay: 120 } 3804 ]; 3805 }, [isSinging]); 3806 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full flex flex-col justify-between overflow-hidden select-none bg-[#0a0f29]", children: [ 3807 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3808 "div", 3809 { 3810 className: "absolute inset-0 z-0 pointer-events-none", 3811 style: { 3812 background: ` 3813 linear-gradient(180deg, rgba(10,14,45,0.7) 0%, rgba(15,23,68,0.2) 60%, rgba(20,30,85,0.8) 100%), 3814 repeating-linear-gradient( 3815 90deg, 3816 #182357 0px, 3817 #25368a 12px, 3818 #3246a8 20px, 3819 #1c2966 28px, 3820 #121b44 36px 3821 ) 3822 ` 3823 }, 3824 children: [
3825 /* @__PURE__ */ jsxRuntimeExports.jsx( 3826 "div", 3827 { 3828 className: "absolute top-0 left-1/2 -translate-x-1/2 w-3/4 h-full pointer-events-none", 3829 style: { 3830 background: "radial-gradient(ellipse 65% 55% at 50% 15%, rgba(255, 235, 180, 0.22) 0%, rgba(147, 197, 253, 0.08) 50%, transparent 100%)" 3831 } 3832 } 3833 ), 3834 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 inset-x-0 h-4 bg-gradient-to-b from-[#0b102e] to-transparent flex items-center justify-around opacity-90", children: Array.from({ length: 12 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 3835 "div", 3836 { 3837 className: "w-10 h-3 rounded-b-full bg-[#1b2762] border-b border-[#3b82f6]/40 shadow-sm" 3838 }, 3839 i 3840 )) }) 3841 ] 3842 } 3843 ), 3844 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-30 w-full pt-3 pb-1 flex flex-col items-center justify-center shrink-0 pointer-events-none", children: [
3845 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3846 "span", 3847 { 3848 className: "text-4xl sm:text-5xl font-black tracking-widest text-white transition-all transform duration-75", 3849 style: { 3850 fontFamily: '"Comic Sans MS", "Chalkboard SE", "Arial Rounded MT Bold", cursive, sans-serif', 3851 textShadow: ` 3852 -2px -2px 0 #000, 3853 2px -2px 0 #000, 3854 -2px 2px 0 #000, 3855 2px 2px 0 #000, 3856 0px 4px 0 #000, 3857 0 6px 14px rgba(0,0,0,0.65) 3858 `, 3859 filter: isSinging ? "brightness(1.15) scale(1.04)" : "none" 3860 }, 3861 children: score 3862 } 3863 ) }), 3864 combo > 2 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 3865 "div", 3866 { 3867 className: `mt-0.5 px-3 py-0.5 rounded-full text-xs font-black uppercase tracking-wider flex items-center gap-1 shadow-md transition-all ${isFever ? "bg-gradient-to-r from-amber-400 to-pink-500 text-slate-950 animate-bounce" : "bg-pink-600/90 text-white border border-pink-300/40"}`, 3868 style: { 3869 fontFamily: '"Arial Rounded MT Bold", sans-serif' 3870 }, 3871 children: [
3872 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 3873 combo, 3874 " COMBO!" 3875 ] }), 3876 isFever && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ð¥ x2" }) 3877 ] 3878 } 3879 ) 3880 ] }), 3881 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-10 w-full h-[64%] flex items-end justify-center overflow-hidden", children: [ 3882 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3883 "div", 3884 { 3885 className: "absolute inset-x-0 bottom-0 h-[60%] border-t-2 border-[#5c3a1a]", 3886 style: { 3887 background: ` 3888 linear-gradient(180deg, #d49a5b 0%, #ba8145 35%, #9b6329 80%, #7d4918 100%) 3889 `, 3890 boxShadow: "inset 0 10px 25px rgba(0,0,0,0.35)" 3891 }, 3892 children: [ 3893 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full flex flex-col justify-around opacity-40 pointer-events-none", children: [ 3894 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[1px] bg-[#543011]" }),
3895 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[1px] bg-[#543011]" }), 3896 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[1px] bg-[#543011]" }) 3897 ] }), 3898 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex justify-between px-12 opacity-30 pointer-events-none", children: [ 3899 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1px] h-full bg-[#543011]" }), 3900 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1px] h-full bg-[#543011]" }), 3901 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1px] h-full bg-[#543011]" }) 3902 ] }), 3903 /* @__PURE__ */ jsxRuntimeExports.jsx( 3904 "div", 3905 { 3906 className: "absolute bottom-2 left-1/2 -translate-x-1/2 w-64 h-24 rounded-full pointer-events-none opacity-45", 3907 style: { 3908 background: "radial-gradient(ellipse at center, rgba(255,248,220,0.6) 0%, rgba(212,154,91,0) 70%)" 3909 } 3910 } 3911 ) 3912 ] 3913 } 3914 ),
3915 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-lg h-full flex items-end justify-center px-4 pb-2", children: [ 3916 /* @__PURE__ */ jsxRuntimeExports.jsx( 3917 "div", 3918 { 3919 className: "absolute bottom-4 left-6 sm:left-12 flex flex-col items-center transition-transform duration-100", 3920 style: { 3921 transform: isSinging && activeLaneHit === 0 ? "scale(1.08) translateY(-4px)" : "scale(1.0)" 3922 }, 3923 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3924 KittenFigure, 3925 { 3926 type: "ginger", 3927 isHeadUp: isSinging && (activeLaneHit === 0 || activeLaneHit === null), 3928 isSinging, 3929 isMissed, 3930 size: 72 3931 } 3932 ) 3933 } 3934 ), 3935 /* @__PURE__ */ jsxRuntimeExports.jsx( 3936 "div", 3937 { 3938 className: "absolute bottom-4 left-[30%] sm:left-[34%] -translate-x-1/2 flex flex-col items-center z-15 transition-transform duration-100", 3939 style: { 3940 transform: isSinging && (activeLaneHit === 1 || activeLaneHit === null) ? "scale(1.1) translateY(-6px)" : "scale(1.0)" 3941 }, 3942 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3943 KittenFigure, 3944 { 3945 type: "grey", 3946 isHeadUp: isSinging, 3947 isSinging, 3948 isMissed, 3949 size: 84 3950 } 3951 ) 3952 } 3953 ),
3954 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-20 flex flex-col items-center mb-1", children: [ 3955 /* @__PURE__ */ jsxRuntimeExports.jsx( 3956 "div", 3957 { 3958 className: "relative w-8 h-8 rounded-full shadow-lg border border-red-700/60 transition-transform duration-100", 3959 style: { 3960 background: "radial-gradient(circle at 35% 30%, #ff7350 0%, #e11d48 70%, #9f1239 100%)", 3961 boxShadow: isSinging ? "0 0 16px rgba(244, 63, 94, 0.8), 0 4px 8px rgba(0,0,0,0.5)" : "0 4px 8px rgba(0,0,0,0.5)", 3962 transform: isSinging ? "scale(1.06)" : "scale(1.0)" 3963 }, 3964 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-1 rounded-full border border-red-300/30 opacity-70" }) 3965 } 3966 ), 3967 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-2.5 h-1.5 bg-neutral-700 rounded-sm -mt-0.5" }), 3968 /* @__PURE__ */ jsxRuntimeExports.jsx( 3969 "div", 3970 { 3971 className: "w-1.5 h-16 sm:h-20 bg-gradient-to-r from-neutral-800 via-neutral-600 to-neutral-900 shadow-md" 3972 } 3973 ),
3974 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-2 bg-neutral-800 rounded-sm -mb-0.5" }), 3975 /* @__PURE__ */ jsxRuntimeExports.jsx( 3976 "div", 3977 { 3978 className: "w-14 h-4 rounded-full bg-gradient-to-b from-neutral-700 via-neutral-900 to-black border border-neutral-700 shadow-xl", 3979 style: { 3980 transform: "rotateX(55deg)" 3981 } 3982 } 3983 ) 3984 ] }), 3985 /* @__PURE__ */ jsxRuntimeExports.jsx( 3986 "div", 3987 { 3988 className: "absolute bottom-4 right-[30%] sm:right-[34%] translate-x-1/2 flex flex-col items-center z-15 transition-transform duration-100", 3989 style: { 3990 transform: isSinging && (activeLaneHit === 2 || activeLaneHit === null) ? "scale(1.1) translateY(-6px)" : "scale(1.0)" 3991 }, 3992 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3993 KittenFigure, 3994 { 3995 type: "white", 3996 isHeadUp: isSinging, 3997 isSinging, 3998 isMissed, 3999 size: 84 4000 } 4001 ) 4002 } 4003 ),
4004 /* @__PURE__ */ jsxRuntimeExports.jsx( 4005 "div", 4006 { 4007 className: "absolute bottom-4 right-6 sm:right-12 flex flex-col items-center transition-transform duration-100", 4008 style: { 4009 transform: isSinging && activeLaneHit === 3 ? "scale(1.08) translateY(-4px)" : "scale(1.0)" 4010 }, 4011 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4012 KittenFigure, 4013 { 4014 type: "calico", 4015 isHeadUp: isSinging && (activeLaneHit === 3 || activeLaneHit === null), 4016 isSinging, 4017 isMissed, 4018 size: 72 4019 } 4020 ) 4021 } 4022 ), 4023 singingNotes.map((n) => /* @__PURE__ */ jsxRuntimeExports.jsx( 4024 "div", 4025 { 4026 className: "absolute pointer-events-none text-xl sm:text-2xl font-black text-pink-300 animate-ping opacity-80", 4027 style: { 4028 left: `${n.x}%`, 4029 top: `${n.y}%`, 4030 filter: "drop-shadow(0 0 6px rgba(244,114,182,0.9))" 4031 }, 4032 children: n.symbol 4033 }, 4034 n.id 4035 )) 4036 ] }) 4037 ] }) 4038 ] }); 4039}; 4040const KittenFigure = ({ 4041 type, 4042 isHeadUp, 4043 isSinging, 4044 isMissed, 4045 size 4046}) => { 4047 const gradId = reactExports.useMemo(() => `cat-${type}-${Math.random().toString(36).substr(2, 6)}`, [type]); 4048 const palette = reactExports.useMemo(() => { 4049 switch (type) { 4050 case "grey": 4051 return { 4052 bodyTop: "#b0c4de", 4053 bodyBottom: "#64748b", 4054 belly: "#e2e8f0", 4055 stripes: "#475569", 4056 earsInner: "#f472b6", 4057 earsInnerDark: "#db2777", 4058 nose: "#fb7185", 4059 irisTop: "#059669", 4060 irisBottom: "#34d399", 4061 bowtie: "#e11d48", 4062 collar: "#be123c", 4063 bell: "#fbbf24" 4064 }; 4065 case "white": 4066 return { 4067 bodyTop: "#ffffff", 4068 bodyBottom: "#cbd5e1", 4069 belly: "#f8fafc", 4070 stripes: "#94a3b8", 4071 earsInner: "#f9a8d4", 4072 earsInnerDark: "#f472b6", 4073 nose: "#fda4af", 4074 irisTop: "#0284c7", 4075 irisBottom: "#38bdf8", 4076 bowtie: "#3b82f6", 4077 collar: "#1d4ed8", 4078 bell: "#fbbf24" 4079 }; 4080 case "ginger": 4081 return { 4082 bodyTop: "#fb923c", 4083 bodyBottom: "#c2410c", 4084 belly: "#ffedd5", 4085 stripes: "#9a3412", 4086 earsInner: "#fda4af", 4087 earsInnerDark: "#f43f5e", 4088 nose: "#e11d48", 4089 irisTop: "#d97706", 4090 irisBottom: "#fcd34d", 4091 bowtie: "#8b5cf6", 4092 collar: "#6d28d9", 4093 bell: "#fbbf24" 4094 }; 4095 case "calico": 4096 return { 4097 bodyTop: "#fed7aa", 4098 bodyBottom: "#b45309", 4099 belly: "#ffffff", 4100 stripes: "#78350f", 4101 earsInner: "#fda4af", 4102 earsInnerDark: "#fb7185", 4103 nose: "#e11d48", 4104 irisTop: "#059669", 4105 irisBottom: "#f59e0b", 4106 bowtie: "#10b981", 4107 collar: "#047857", 4108 bell: "#fbbf24" 4109 }; 4110 } 4111 }, [type]); 4112 return /* @__PURE__ */ jsxRuntimeExports.jsx( 4113 "div", 4114 { 4115 className: "relative flex flex-col items-center group cursor-pointer", 4116 style: { width: size, height: size * 1.18 }, 4117 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 4118 "svg", 4119 {
4120 viewBox: "0 0 100 118", 4121 width: size, 4122 height: size * 1.18, 4123 className: "overflow-visible", 4124 children: [ 4125 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [ 4126 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: `${gradId}-body`, cx: "45%", cy: "35%", r: "65%", children: [ 4127 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: palette.bodyTop }), 4128 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: palette.bodyTop }), 4129 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: palette.bodyBottom }) 4130 ] }), 4131 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: `${gradId}-belly`, x1: "0", y1: "0", x2: "0", y2: "1", children: [ 4132 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff" }), 4133 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: palette.belly }) 4134 ] }),
4135 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: `${gradId}-ear`, x1: "0", y1: "0", x2: "0", y2: "1", children: [ 4136 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: palette.earsInner }), 4137 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: palette.earsInnerDark }) 4138 ] }), 4139 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: `${gradId}-iris`, x1: "0", y1: "0", x2: "0", y2: "1", children: [ 4140 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: palette.irisTop }), 4141 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: palette.irisBottom }) 4142 ] }), 4143 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: `${gradId}-bell`, cx: "35%", cy: "30%", r: "60%", children: [ 4144 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#fef08a" }), 4145 /* @__P
4145URE__ */ jsxRuntimeExports.jsx("stop", { offset: "60%", stopColor: "#eab308" }), 4146 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#854d0e" }) 4147 ] }) 4148 ] }), 4149 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "110", rx: "36", ry: "6", fill: "rgba(0,0,0,0.4)", filter: "blur(1px)" }), 4150 /* @__PURE__ */ jsxRuntimeExports.jsx( 4151 "path", 4152 { 4153 d: isSinging ? "M 22 92 C 5 80 8 52 18 42 C 20 40 23 43 21 46 C 14 56 12 76 25 88 Z" : "M 22 94 C 12 85 14 68 20 60 C 22 58 24 60 22 63 C 18 70 17 80 25 90 Z", 4154 fill: `url(#${gradId}-body)`, 4155 className: "transition-all duration-150" 4156 } 4157 ), 4158 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "86", rx: "29", ry: "23", fill: `url(#${gradId}-body)` }), 4159 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "88", rx: "19", ry: "16", fill: `url(#${gradId}-belly)` }),
4160 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "24", cy: "103", rx: "8", ry: "5.5", fill: palette.bodyBottom, opacity: "0.85" }), 4161 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "76", cy: "103", rx: "8", ry: "5.5", fill: palette.bodyBottom, opacity: "0.85" }), 4162 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 4163 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "38", cy: "105", rx: "8.5", ry: "6", fill: `url(#${gradId}-body)` }), 4164 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "36", cy: "107", r: "1.2", fill: palette.bodyBottom, opacity: "0.6" }), 4165 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "40", cy: "107", r: "1.2", fill: palette.bodyBottom, opacity: "0.6" }), 4166 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "62", cy: "105", rx: "8.5", ry: "6", fill: `url(#${gradId}-body)` }), 4167 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "60", cy: "107", r: "1.2", fill: palette.bodyBottom, opacity: "0.6" }),
4168 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "64", cy: "107", r: "1.2", fill: palette.bodyBottom, opacity: "0.6" }) 4169 ] }), 4170 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "transition-transform duration-100 origin-[50px_70px]", children: [ 4171 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 71 L 40 66 C 36 64 36 78 40 76 Z", fill: palette.bowtie }), 4172 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 50 71 L 60 66 C 64 64 64 78 60 76 Z", fill: palette.bowtie }), 4173 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "71", r: "3.2", fill: palette.collar }), 4174 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "75", r: "3.2", fill: `url(#${gradId}-bell)`, stroke: "#a16207", strokeWidth: "0.5" }), 4175 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "50", cy: "76", r: "0.7", fill: "#713f12" }) 4176 ] }), 4177 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4178 "g", 4179 { 4180 className: "transition-transform duration-100 origin-[50px_60px]", 4181 style: { 4182 transform: isHeadUp ? "translateY(-7px) scaleY(1.06)" : isMissed ? "translateY(3px) rotate(4deg)" : "translateY(0px)" 4183 }, 4184 children: [
4185 /* @__PURE__ */ jsxRuntimeExports.jsx( 4186 "path", 4187 { 4188 d: "M 24 40 C 20 28 20 16 26 14 C 32 16 42 26 44 34 Z", 4189 fill: `url(#${gradId}-body)` 4190 } 4191 ), 4192 /* @__PURE__ */ jsxRuntimeExports.jsx( 4193 "path", 4194 { 4195 d: "M 26 36 C 24 28 24 20 27 18 C 31 20 38 27 40 32 Z", 4196 fill: `url(#${gradId}-ear)` 4197 } 4198 ), 4199 /* @__PURE__ */ jsxRuntimeExports.jsx( 4200 "path", 4201 { 4202 d: "M 76 40 C 80 28 80 16 74 14 C 68 16 58 26 56 34 Z", 4203 fill: `url(#${gradId}-body)` 4204 } 4205 ), 4206 /* @__PURE__ */ jsxRuntimeExports.jsx( 4207 "path", 4208 { 4209 d: "M 74 36 C 76 28 76 20 73 18 C 69 20 62 27 60 32 Z", 4210 fill: `url(#${gradId}-ear)` 4211 } 4212 ),
4213 /* @__PURE__ */ jsxRuntimeExports.jsx( 4214 "path", 4215 { 4216 d: "M 50 16 C 30 16 20 27 20 43 C 20 54 26 63 36 66 C 41 67.5 59 67.5 64 66 C 74 63 80 54 80 43 C 80 27 70 16 50 16 Z", 4217 fill: `url(#${gradId}-body)` 4218 } 4219 ), 4220 type === "calico" && /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 24 30 C 22 42 30 48 38 42 C 40 32 32 24 24 30 Z", fill: "#b45309", opacity: "0.85" }), 4221 type !== "white" && type !== "calico" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { stroke: palette.stripes, strokeWidth: "2.5", strokeLinecap: "round", opacity: "0.85", children: [ 4222 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "50", y1: "20", x2: "50", y2: "29" }), 4223 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "42", y1: "22", x2: "44", y2: "30" }), 4224 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "58", y1: "22", x2: "56", y2: "30" }) 4225 ] }),
4226 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "32", cy: "49", rx: "5.5", ry: "3.5", fill: "#f43f5e", opacity: "0.35" }), 4227 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "68", cy: "49", rx: "5.5", ry: "3.5", fill: "#f43f5e", opacity: "0.35" }), 4228 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { stroke: "#ffffff", strokeWidth: "1.3", strokeLinecap: "round", opacity: "0.9", children: [ 4229 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 22 48 Q 12 46 4 45" }), 4230 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 22 52 Q 13 54 5 57" }), 4231 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 78 48 Q 88 46 96 45" }), 4232 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 78 52 Q 87 54 95 57" }) 4233 ] }), 4234 isHeadUp ? ( 4235 // Singing / Ecstatic Happy Eyes (Crescent arcs "^ ^" with lashes) 4236 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { stroke: "#0f172a", strokeWidth: "2.8", strokeLinecap: "round", fill: "none", children: [
4237 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 33 43 Q 40 37 47 43" }), 4238 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 53 43 Q 60 37 67 43" }), 4239 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "28", cy: "38", r: "1.5", fill: "#fbcfe8" }), 4240 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "72", cy: "38", r: "1.5", fill: "#fbcfe8" }) 4241 ] }) 4242 ) : isMissed ? ( 4243 // Sad Embarrassed Eyes ("o o" + sweatdrop) 4244 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 4245 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "40", cy: "43", r: "3.5", fill: "#1e293b" }), 4246 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "60", cy: "43", r: "3.5", fill: "#1e293b" }), 4247 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "39", cy: "41.5", r: "1.2", fill: "#ffffff" }), 4248 /* @__P
4248URE__ */ jsxRuntimeExports.jsx("circle", { cx: "59", cy: "41.5", r: "1.2", fill: "#ffffff" }), 4249 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 73 34 C 73 31 75 28 75 28 C 75 28 77 31 77 34 C 77 35.5 75.5 36.5 74.5 36.5 C 73.5 36.5 73 35.5 73 34 Z", fill: "#38bdf8" }) 4250 ] }) 4251 ) : ( 4252 // Gorgeous Anime Chibi Eyes with Multi-layer glints 4253 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 4254 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "40", cy: "43", rx: "6.5", ry: "7.5", fill: "#0f172a" }), 4255 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "60", cy: "43", rx: "6.5", ry: "7.5", fill: "#0f172a" }), 4256 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "40", cy: "44", rx: "5.5", ry: "6.2", fill: `url(#${gradId}-iris)` }), 4257 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "60", cy: "44", rx: "5.5", ry: "6.2", fill: `url(#${gradId}-iris)` }),
4258 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "40", cy: "44", rx: "3.5", ry: "4.5", fill: "#020617" }), 4259 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "60", cy: "44", rx: "3.5", ry: "4.5", fill: "#020617" }), 4260 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "42.5", cy: "41", r: "2.2", fill: "#ffffff" }), 4261 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "62.5", cy: "41", r: "2.2", fill: "#ffffff" }), 4262 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "37.5", cy: "46", r: "1.2", fill: "#ffffff", opacity: "0.8" }), 4263 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "57.5", cy: "46", r: "1.2", fill: "#ffffff", opacity: "0.8" }) 4264 ] }) 4265 ), 4266 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 47.5 49 C 48.5 48 51.5 48 52.5 49 C 52 52 50 53 50 53 C 50 53 48 52 47.5 49 Z", fill: palette.nose }), 4267 isHeadUp ? (
4268 // Wide Open Singing Mouth with Pink Tongue! 4269 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 4270 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "57", rx: "7.5", ry: "8.5", fill: "#881337" }), 4271 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "55", rx: "5.5", ry: "5", fill: "#4c0519" }), 4272 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 45 59 C 45 64 55 64 55 59 C 55 57 45 57 45 59 Z", fill: "#fda4af" }) 4273 ] }) 4274 ) : ( 4275 // Cute Feline "Ï" Smile 4276 /* @__PURE__ */ jsxRuntimeExports.jsx( 4277 "path", 4278 { 4279 d: "M 44 52 Q 47 55 50 52.5 Q 53 55 56 52", 4280 stroke: "#0f172a", 4281 strokeWidth: "2.0", 4282 strokeLinecap: "round", 4283 fill: "none" 4284 } 4285 ) 4286 ) 4287 ] 4288 } 4289 ) 4290 ] 4291 } 4292 ) 4293 } 4294 ); 4295}; 4296class MeowChoirAudioEngine { 4297 constructor() { 4298 this.ctx = null; 4299 this.masterGain = null; 4300 this.limiter = null; 4301 this.vocalGain = null; 4302 this.sfxGain = null; 4303 this.audioOffsetMs = 0; 4304 this.vocalVolume = 0.9; 4305 this.activeVoices = []; 4306 this.sustainVoices = /* @__PURE__ */ new Map(); 4307 this.maxPolyphony = 6; 4308 if (typeof window !== "undefined") { 4309 const savedOffset = localStorage.getItem("meow_choir_audio_offset"); 4310 if (savedOffset !== null) { 4311 this.audioOffsetMs = Number(savedOffset) || 0; 4312 } 4313 const savedVol = localStorage.getItem("meow_choir_vocal_vol"); 4314 if (savedVol !== null) { 4315 this.vocalVolume = Number(savedVol) || 0.9; 4316 } 4317 } 4318 } 4319 initContext() { 4320 if (!this.ctx) { 4321 const AudioCtx = window.AudioContext || window.webkitAudioContext; 4322 if (!AudioCtx) return false; 4323 this.ctx = new AudioCtx(); 4324 this.limiter = this.ctx.createDynamicsCompressor(); 4325 this.limiter.threshold.setValueAtTime(-3, this.ctx.currentTime); 4326 this.limiter.knee.setValueAtTime(4, this.ctx.currentTime); 4327 this.limiter.ratio.setValueAtTime(16, this.ctx.currentTime); 4328 this.limiter.attack.setValueAtTime(3e-3, this.ctx.currentTime); 4329 this.limiter.release.setValueAtTime(0.08, this.ctx.currentTime); 4330 this.limiter.connect(this.ctx.destination); 4331 this.masterGain = this.ctx.createGain(); 4332 this.masterGain.gain.setValueAtTime(0.85, this.ctx.currentTime); 4333 this.masterGain.connect(this.limiter); 4334 this.vocalGain = this.ctx.createGain(); 4335 this.vocalGain.gain.setValueAtTime(this.vocalVolume, this.ctx.currentTime); 4336 this.vocalGain.connect(this.masterGain); 4337 this.sfxGain = this.ctx.createGain(); 4338 this.sfxGain.gain.setValueAtTime(0.7, this.ctx.currentTime); 4339 this.sfxGain.connect(this.masterGain); 4340 } 4341 if (this.ctx && this.ctx.state === "suspended") { 4342 this.ctx.resume().catch(() => { 4343 }); 4344 } 4345 return true; 4346 } 4347 getOffsetMs() { 4348 return this.audioOffsetMs; 4349 } 4350 setOffsetMs(offset) { 4351 this.audioOffsetMs = Math.max(-250, Math.min(250, offset)); 4352 if (typeof window !== "undefined") { 4353 localStorage.setItem("meow_choir_audio_offset", String(this.audioOffsetMs)); 4354 } 4355 } 4356 getVocalVolume() { 4357 return this.vocalVolume; 4358 } 4359 setVocalVolume(vol) { 4360 this.vocalVolume = Math.max(0, Math.min(1.2, vol)); 4361 if (this.vocalGain && this.ctx) { 4362 this.vocalGain.gain.setValueAtTime(this.vocalVolume, this.ctx.currentTime); 4363 } 4364 if (typeof window !== "undefined") { 4365 localStorage.setItem("meow_choir_vocal_vol", String(this.vocalVolume)); 4366 } 4367 } 4368 /** 4369 * Polyphony manager to prevent volume inflation and CPU overload. 4370 * Safely fades oldest voice without calling duplicate .stop() 4371 */ 4372 managePolyphony(now) { 4373 this.activeVoices = this.activeVoices.filter((v) => v.stopTime > now && !v.isStopped); 4374 while (this.activeVoices.length >= this.maxPolyphony) { 4375 const oldest = this.activeVoices.shift(); 4376 if (oldest && !oldest.isStopped) { 4377 oldest.isStopped = true; 4378 try { 4379 oldest.ampGain.gain.cancelScheduledValues(now); 4380 oldest.ampGain.gain.setValueAtTime(Math.max(1e-4, oldest.ampGain.gain.value), now); 4381 oldest.ampGain.gain.linearRampToValueAtTime(1e-4, now + 0.02); 4382 } catch (e) { 4383 } 4384 } 4385 } 4386 } 4387 /** 4388 * Synthesizes an adorable, sweet kitten "meow" tuned to the specific melody pitch. 4389 * @param midiPitch Standard MIDI note number (60 = C4, 69 = A440) 4390 * @param duration Duration of the meow in seconds (supports short taps and held meows) 4391 * @param isStarBonus If true, adds sparkle chime harmonics 4392 */ 4393 playMeow(midiPitch = 60, duration = 0.35, isStarBonus = false) { 4394 this.initContext(); 4395 if (!this.ctx || !this.vocalGain || this.vocalVolume <= 0.01) return; 4396 const now = this.ctx.currentTime; 4397 this.managePolyphony(now); 4398 const dur = Math.max(0.22, Math.min(2.5, duration)); 4399 const clampedMidi = Math.max(55, Math.min(88, midiPitch)); 4400 const baseFreq = 440 * Math.pow(2, (clampedMidi - 69) / 12); 4401 const oscSine = this.ctx.createOscillator(); 4402 const oscTri = this.ctx.createOscillator(); 4403 const oscHarm = this.ctx.createOscillator(); 4404 const gainSine = this.ctx.createGain(); 4405 const gainTri = this.ctx.createGain(); 4406 const gainHarm = this.ctx.createGain(); 4407 const voiceMixer = this.ctx.createGain(); 4408 oscSine.type = "sine"; 4409 oscTri.type = "triangle"; 4410 oscHarm.type = "sine"; 4411 gainSine.gain.setValueAtTime(0.7, now); 4412 gainTri.gain.setValueAtTime(0.4, now); 4413 gainHarm.gain.setValueAtTime(0.18, now); 4414 const scoopPitch = baseFreq * 0.92; 4415 const midPitch = baseFreq; 4416 const tailPitch = baseFreq * 0.96; 4417 const attackTime = Math.min(0.05, dur * 0.2); 4418 const releaseTime = Math.min(0.08, dur * 0.25);
4419 [oscSine, oscTri].forEach((osc) => { 4420 osc.frequency.setValueAtTime(scoopPitch, now); 4421 osc.frequency.linearRampToValueAtTime(midPitch, now + attackTime); 4422 osc.frequency.setValueAtTime(midPitch, now + dur - releaseTime); 4423 osc.frequency.linearRampToValueAtTime(tailPitch, now + dur); 4424 }); 4425 oscHarm.frequency.setValueAtTime(scoopPitch * 2, now); 4426 oscHarm.frequency.linearRampToValueAtTime(midPitch * 2, now + attackTime); 4427 oscHarm.frequency.setValueAtTime(midPitch * 2, now + dur - releaseTime); 4428 oscHarm.frequency.linearRampToValueAtTime(tailPitch * 2, now + dur); 4429 const lfo = this.ctx.createOscillator(); 4430 const lfoGain = this.ctx.createGain(); 4431 const lfoGainHarm = this.ctx.createGain(); 4432 const lfoTremolo = this.ctx.createGain(); 4433 lfo.frequency.setValueAtTime(5.8, now); 4434 const vibDepth = baseFreq * 0.038; 4435 const attackSwellTime = Math.min(0.1, dur * 0.35); 4436 lfoGain.gain.setValueAtTime(baseFreq * 6e-3, now); 4437 lfoGain.gain.linearRampToValueAtTime(vibDepth, now + attackSwellTime); 4438 lfoGainHarm.gain.setValueAtTime(baseFreq * 2 * 6e-3, now); 4439 lfoGainHarm.gain.linearRampToValueAtTime(vibDepth * 2, now + attackSwellTime); 4440 lfoTremolo.gain.setValueAtTime(0.06, now); 4441 lfo.connect(lfoGain); 4442 lfo.connect(lfoGainHarm); 4443 lfo.connect(lfoTremolo); 4444 lfoGain.connect(oscSine.frequency); 4445 lfoGain.connect(oscTri.frequency); 4446 lfoGainHarm.connect(oscHarm.frequency); 4447 lfoTremolo.connect(voiceMixer.gain); 4448 const f1 = this.ctx.createBiquadFilter(); 4449 f1.type = "bandpass"; 4450 f1.Q.setValueAtTime(3.2, now); 4451 f1.frequency.setValueAtTime(680, now); 4452 f1.frequency.linearRampToValueAtTime(1020, now + dur * 0.28); 4453 f1.frequency.linearRampToValueAtTime(580, now + dur); 4454 const f2 = this.ctx.createBiquadFilter(); 4455 f2.type = "bandpass"; 4456 f2.Q.setValueAtTime(3.8, now); 4457 f2.frequency.setValueAtTime(1900, now); 4458 f2.frequency.linearRampToValueAtTime(2500, now + dur * 0.3); 4459 f2.frequency.linearRampToValueAtTime(1400, now + dur); 4460 const f3 = this.ctx.createBiquadFilter(); 4461 f3.type = "bandpass"; 4462 f3.Q.setValueAtTime(2.5, now); 4463 f3.frequency.setValueAtTime(3200, now); 4464 const ampGain = this.ctx.createGain(); 4465 ampGain.gain.setValueAtTime(1e-4, now); 4466 ampGain.gain.linearRampToValueAtTime(isStarBonus ? 0.9 : 0.78, now + attackTime); 4467 ampGain.gain.setValueAtTime(isStarBonus ? 0.85 : 0.74, now + dur - releaseTime); 4468 ampGain.gain.linearRampToValueAtTime(1e-4, now + dur); 4469 oscSine.connect(gainSine); 4470 oscTri.connect(gainTri); 4471 oscHarm.connect(gainHarm); 4472 gainSine.connect(voiceMixer); 4473 gainTri.connect(voiceMixer); 4474 gainHarm.connect(voiceMixer); 4475 voiceMixer.connect(f1); 4476 voiceMixer.connect(f2); 4477 voiceMixer.connect(f3); 4478 f1.connect(ampGain); 4479 f2.connect(ampGain); 4480 f3.connect(ampGain); 4481 ampGain.connect(this.vocalGain); 4482 const stopTime = now + dur + 0.05; 4483 lfo.start(now); 4484 oscSine.start(now); 4485 oscTri.start(now); 4486 oscHarm.start(now); 4487 lfo.stop(stopTime); 4488 oscSine.stop(stopTime); 4489 oscTri.stop(stopTime); 4490 oscHarm.stop(stopTime); 4491 if (isStarBonus) { 4492 const chime = this.ctx.createOscillator(); 4493 const chimeGain = this.ctx.createGain(); 4494 chime.type = "sine"; 4495 chime.frequency.setValueAtTime(baseFreq * 2, now); 4496 chime.frequency.linearRampToValueAtTime(baseFreq * 3, now + 0.15); 4497 chimeGain.gain.setValueAtTime(0.2, now); 4498 chimeGain.gain.linearRampToValueAtTime(1e-4, now + 0.25); 4499 chime.connect(chimeGain); 4500 chimeGain.connect(this.vocalGain); 4501 chime.start(now); 4502 chime.stop(now + 0.26); 4503 } 4504 this.activeVoices.push({ 4505 oscSine, 4506 oscTri, 4507 oscHarm, 4508 lfo, 4509 ampGain, 4510 f1, 4511 f2, 4512 f3, 4513 stopTime, 4514 isStopped: false 4515 }); 4516 } 4517 /** 4518 * Starts continuous sustain meow for hold notes. 4519 */ 4520 startHoldMeow(lane, midiPitch = 60, isStarBonus = false) { 4521 this.initContext(); 4522 if (!this.ctx || !this.vocalGain || this.vocalVolume <= 0.01) return; 4523 this.stopHoldMeow(lane); 4524 const now = this.ctx.currentTime; 4525 const clampedMidi = Math.max(55, Math.min(88, midiPitch)); 4526 const baseFreq = 440 * Math.pow(2, (clampedMidi - 69) / 12); 4527 const oscSine = this.ctx.createOscillator(); 4528 const oscTri = this.ctx.createOscillator(); 4529 const oscHarm = this.ctx.createOscillator(); 4530 const gainSine = this.ctx.createGain(); 4531 const gainTri = this.ctx.createGain(); 4532 const gainHarm = this.ctx.createGain(); 4533 const voiceMixer = this.ctx.createGain(); 4534 oscSine.type = "sine"; 4535 oscTri.type = "triangle"; 4536 oscHarm.type = "sine"; 4537 gainSine.gain.setValueAtTime(0.7, now); 4538 gainTri.gain.setValueAtTime(0.4, now); 4539 gainHarm.gain.setValueAtTime(0.18, now); 4540 oscSine.frequency.setValueAtTime(baseFreq * 0.94, now); 4541 oscTri.frequency.setValueAtTime(baseFreq * 0.94, now); 4542 oscHarm.frequency.setValueAtTime(baseFreq * 1.88, now); 4543 oscSine.frequency.linearRampToValueAtTime(baseFreq, now + 0.045); 4544 oscTri.frequency.linearRampToValueAtTime(baseFreq, now + 0.045); 4545 oscHarm.frequency.linearRampToValueAtTime(baseFreq * 2, now + 0.045); 4546 const lfo = this.ctx.createOscillator(); 4547 const lfoGain = this.ctx.createGain(); 4548 const lfoGainHarm = this.ctx.createGain(); 4549 const lfoTremolo = this.ctx.createGain(); 4550 lfo.frequency.setValueAtTime(5.8, now); 4551 const vibDepth = baseFreq * 0.042; 4552 lfoGain.gain.setValueAtTime(baseFreq * 8e-3, now); 4553 lfoGain.gain.linearRampToValueAtTime(vibDepth, now + 0.18); 4554 lfoGainHarm.gain.setValueAtTime(baseFreq * 2 * 8e-3, now); 4555 lfoGainHarm.gain.linearRampToValueAtTime(vibDepth * 2, now + 0.18); 4556 lfoTremolo.gain.setValueAtTime(0.06, now); 4557 lfo.connect(lfoGain); 4558 lfo.connect(lfoGainHarm); 4559 lfo.connect(lfoTremolo); 4560 lfoGain.connect(oscSine.frequency); 4561 lfoGain.connect(oscTri.frequency); 4562 lfoGainHarm.connect(oscHarm.frequency); 4563 lfoTremolo.connect(voiceMixer.gain); 4564 lfo.start(now); 4565 const f1 = this.ctx.createBiquadFilter(); 4566 f1.type = "bandpass"; 4567 f1.Q.setValueAtTime(3.2, now); 4568 f1.frequency.setValueAtTime(950, now); 4569 const f2 = this.ctx.createBiquadFilter(); 4570 f2.type = "bandpass"; 4571 f2.Q.setValueAtTime(3.8, now); 4572 f2.frequency.setValueAtTime(2400, now); 4573 const f3 = this.ctx.createBiquadFilter(); 4574 f3.type = "bandpass"; 4575 f3.Q.setValueAtTime(2.5, now); 4576 f3.frequency.setValueAtTime(3200, now); 4577 const ampGain = this.ctx.createGain(); 4578 ampGain.gain.setValueAtTime(1e-4, now); 4579 ampGain.gain.linearRampToValueAtTime(isStarBonus ? 0.9 : 0.78, now + 0.045); 4580 oscSine.connect(gainSine); 4581 oscTri.connect(gainTri); 4582 oscHarm.connect(gainHarm); 4583 gainSine.connect(voiceMixer); 4584 gainTri.connect(voiceMixer); 4585 gainHarm.connect(voiceMixer); 4586 voiceMixer.connect(f1); 4587 voiceMixer.connect(f2); 4588 voiceMixer.connect(f3); 4589 f1.connect(ampGain); 4590 f2.connect(ampGain); 4591 f3.connect(ampGain); 4592 ampGain.connect(this.vocalGain); 4593 oscSine.start(now); 4594 oscTri.start(now); 4595 oscHarm.start(now); 4596 this.sustainVoices.set(lane, { 4597 oscSine, 4598 oscTri, 4599 oscHarm, 4600 lfo, 4601 ampGain, 4602 f1, 4603 f2, 4604 f3, 4605 baseFreq, 4606 isStopped: false 4607 }); 4608 } 4609 /** 4610 * Smoothly releases a hold meow with cute "-ow" decay. 4611 */ 4612 stopHoldMeow(lane) { 4613 const voice = this.sustainVoices.get(lane); 4614 if (!voice || !this.ctx || voice.isStopped) return; 4615 this.sustainVoices.delete(lane); 4616 voice.isStopped = true; 4617 const now = this.ctx.currentTime; 4618 const releaseTime = 0.08; 4619 try { 4620 voice.ampGain.gain.cancelScheduledValues(now); 4621 voice.ampGain.gain.setValueAtTime(Math.max(1e-4, voice.ampGain.gain.value), now); 4622 voice.ampGain.gain.linearRampToValueAtTime(1e-4, now + releaseTime); 4623 voice.oscSine.frequency.linearRampToValueAtTime(voice.baseFreq * 0.94, now + releaseTime); 4624 voice.oscTri.frequency.linearRampToValueAtTime(voice.baseFreq * 0.94, now + releaseTime); 4625 voice.oscHarm.frequency.linearRampToValueAtTime(voice.baseFreq * 1.88, now + releaseTime); 4626 const stopAt = now + releaseTime + 0.02; 4627 voice.oscSine.stop(stopAt); 4628 voice.oscTri.stop(stopAt); 4629 voice.oscHarm.stop(stopAt); 4630 voice.lfo.stop(stopAt); 4631 } catch (e) { 4632 } 4633 } 4634 /** 4635 * Stop all sustain voices on reset or pause. 4636 */ 4637 stopAllHolds() { 4638 for (const lane of Array.from(this.sustainVoices.keys())) { 4639 this.stopHoldMeow(lane); 4640 } 4641 } 4642 /** 4643 * Crisp, playful soap bubble popping sound. 4644 */ 4645 playBubblePop(pitchMod = 1) { 4646 this.initContext(); 4647 if (!this.ctx || !this.sfxGain) return; 4648 const now = this.ctx.currentTime; 4649 const popOsc = this.ctx.createOscillator();
4650 const popGain = this.ctx.createGain(); 4651 popOsc.type = "sine"; 4652 const startFreq = 480 * pitchMod; 4653 const peakFreq = 1200 * pitchMod; 4654 popOsc.frequency.setValueAtTime(startFreq, now); 4655 popOsc.frequency.linearRampToValueAtTime(peakFreq, now + 0.035); 4656 popGain.gain.setValueAtTime(0.5, now); 4657 popGain.gain.linearRampToValueAtTime(1e-4, now + 0.045); 4658 popOsc.connect(popGain); 4659 popGain.connect(this.sfxGain); 4660 popOsc.start(now); 4661 popOsc.stop(now + 0.05); 4662 } 4663 /** 4664 * Playful Nintendo DS countdown chime (3, 2, 1, MEOW!). 4665 * @param step 3, 2, 1, or 0 (0 = GO/MEOW) 4666 */ 4667 playCountdownTick(step) { 4668 this.initContext(); 4669 if (!this.ctx || !this.sfxGain) return; 4670 const now = this.ctx.currentTime; 4671 const osc = this.ctx.createOscillator(); 4672 const gain = this.ctx.createGain(); 4673 osc.type = "sine"; 4674 if (step === 3) { 4675 osc.frequency.setValueAtTime(523.25, now); 4676 } else if (step === 2) { 4677 osc.frequency.setValueAtTime(659.25, now); 4678 } else if (step === 1) { 4679 osc.frequency.setValueAtTime(783.99, now); 4680 } else { 4681 osc.frequency.setValueAtTime(1046.5, now); 4682 this.playCheerNya(); 4683 } 4684 const dur = step === 0 ? 0.35 : 0.22; 4685 gain.gain.setValueAtTime(0.45, now); 4686 gain.gain.linearRampToValueAtTime(1e-4, now + dur); 4687 osc.connect(gain); 4688 gain.connect(this.sfxGain); 4689 osc.start(now); 4690 osc.stop(now + dur + 0.02); 4691 } 4692 /** 4693 * Soft disappointed miss mew (short, low volume, single-shot). 4694 */ 4695 playMissMew() { 4696 this.initContext(); 4697 if (!this.ctx || !this.sfxGain) return; 4698 const now = this.ctx.currentTime; 4699 const osc = this.ctx.createOscillator(); 4700 const gain = this.ctx.createGain(); 4701 osc.type = "sine"; 4702 osc.frequency.setValueAtTime(320, now); 4703 osc.frequency.linearRampToValueAtTime(160, now + 0.18); 4704 gain.gain.setValueAtTime(0.22, now); 4705 gain.gain.linearRampToValueAtTime(1e-4, now + 0.18); 4706 osc.connect(gain); 4707 gain.connect(this.sfxGain); 4708 osc.start(now); 4709 osc.stop(now + 0.2); 4710 } 4711 /** 4712 * Celebratory "Nya!" cheerful chirp for combo streaks or stage clear. 4713 */ 4714 playCheerNya() { 4715 this.initContext(); 4716 if (!this.ctx || !this.sfxGain) return; 4717 const now = this.ctx.currentTime; 4718 const osc = this.ctx.createOscillator(); 4719 const gain = this.ctx.createGain(); 4720 osc.type = "triangle"; 4721 osc.frequency.setValueAtTime(700, now); 4722 osc.frequency.linearRampToValueAtTime(1300, now + 0.07); 4723 osc.frequency.linearRampToValueAtTime(950, now + 0.22); 4724 gain.gain.setValueAtTime(0.4, now); 4725 gain.gain.linearRampToValueAtTime(1e-4, now + 0.22); 4726 osc.connect(gain); 4727 gain.connect(this.sfxGain); 4728 osc.start(now); 4729 osc.stop(now + 0.24); 4730 } 4731} 4732const meowChoirAudio = new MeowChoirAudioEngine(); 4733const LANE_KEYS = ["D", "F", "J", "K"]; 4734const MeowChoirTouchScreen = ({ 4735 notes, 4736 currentTime, 4737 isPlaying, 4738 speed = 1, 4739 approachTime: propApproachTime = 2.8, 4740 onNoteHit, 4741 onNoteMiss, 4742 onEmptyTap, 4743 lanePressStates 4744}) => { 4745 const canvasRef = reactExports.useRef(null); 4746 const containerRef = reactExports.useRef(null); 4747 const lastAudioTimeRef = reactExports.useRef(currentTime); 4748 const lastAudioTimestampRef = reactExports.useRef(performance.now()); 4749 const isPlayingRef = reactExports.useRef(isPlaying); 4750 const notesRef = reactExports.useRef([]); 4751 const processedIdsRef = reactExports.useRef(/* @__PURE__ */ new Set()); 4752 const particlesRef = reactExports.useRef([]); 4753 const popupsRef = reactExports.useRef([]); 4754 const lanePressRef = reactExports.useRef([false, false, false, false]); 4755 const activeHoldsRef = reactExports.useRef(/* @__PURE__ */ new Map()); 4756 const getSmoothCurrentTime = reactExports.useCallback(() => { 4757 if (!isPlayingRef.current) { 4758 return lastAudioTimeRef.current; 4759 } 4760 const deltaSec = (performance.now() - lastAudioTimestampRef.current) / 1e3; 4761 return lastAudioTimeRef.current + Math.max(0, deltaSec); 4762 }, []); 4763 reactExports.useEffect(() => { 4764 if (currentTime < 0.8 || currentTime < lastAudioTimeRef.current - 1.2) { 4765 processedIdsRef.current.clear(); 4766 notesRef.current = [...notes]; 4767 activeHoldsRef.current.clear(); 4768 meowChoirAudio.stopAllHolds(); 4769 } else if (currentTime > lastAudioTimeRef.current + 1.5) {
4770 notes.forEach((n) => { 4771 if (n.t < currentTime) { 4772 processedIdsRef.current.add(n.id); 4773 } 4774 }); 4775 notesRef.current = notes.filter((n) => !processedIdsRef.current.has(n.id)); 4776 } 4777 lastAudioTimeRef.current = currentTime; 4778 lastAudioTimestampRef.current = performance.now(); 4779 }, [currentTime, notes]); 4780 reactExports.useEffect(() => { 4781 isPlayingRef.current = isPlaying; 4782 lastAudioTimestampRef.current = performance.now(); 4783 if (!isPlaying) { 4784 activeHoldsRef.current.clear(); 4785 meowChoirAudio.stopAllHolds(); 4786 } else { 4787 lastAudioTimeRef.current = currentTime; 4788 } 4789 }, [isPlaying, currentTime]); 4790 reactExports.useEffect(() => { 4791 lanePressRef.current = [...lanePressStates]; 4792 }, [lanePressStates]); 4793 reactExports.useEffect(() => { 4794 notesRef.current = notes.filter((n) => !processedIdsRef.current.has(n.id)); 4795 }, [notes]); 4796 const handleLanePress = reactExports.useCallback((lane) => { 4797 if (!isPlayingRef.current) return; 4798 lanePressRef.current[lane] = true; 4799 const canvas = canvasRef.current; 4800 const width = (canvas == null ? void 0 : canvas.clientWidth) || 400; 4801 const height = (canvas == null ? void 0 : canvas.clientHeight) || 450; 4802 const laneWidth = width / 4; 4803 const targetX = laneWidth * lane + laneWidth / 2; 4804 const targetY = height - 44; 4805 const now = getSmoothCurrentTime(); 4806 const currentNotes = notesRef.current;
4807 let closestNote = null; 4808 let closestDelta = 999; 4809 let closestIdx = -1; 4810 for (let i = 0; i < currentNotes.length; i++) { 4811 const n = currentNotes[i]; 4812 if (n.lane === lane && !processedIdsRef.current.has(n.id)) { 4813 const delta = Math.abs(n.t - now); 4814 if (delta < closestDelta && delta <= 0.32) { 4815 closestDelta = delta; 4816 closestNote = n; 4817 closestIdx = i; 4818 } 4819 } 4820 } 4821 if (closestNote && closestIdx >= 0) { 4822 processedIdsRef.current.add(closestNote.id); 4823 let hitType = "GOOD"; 4824 let popupColor = "#34d399"; 4825 let popupText = "GOOD"; 4826 let pts = "+300"; 4827 if (closestDelta <= 0.09) { 4828 hitType = "PURRFECT"; 4829 popupColor = "#facc15"; 4830 popupText = "PURRFECT!"; 4831 pts = "+1000"; 4832 } else if (closestDelta <= 0.18) { 4833 hitType = "PAWSOME"; 4834 popupColor = "#c084fc"; 4835 popupText = "PAWSOME!"; 4836 pts = "+650"; 4837 } 4838 const particleCount = closestNote.type === "star" ? 20 : 14; 4839 const pColor = closestNote.type === "star" ? "#facc15" : "#c084fc"; 4840 for (let p = 0; p < particleCount; p++) { 4841 const angle = Math.PI * 2 * p / particleCount + (Math.random() - 0.5); 4842 const spd = 2.5 + Math.random() * 5; 4843 particlesRef.current.push({ 4844 x: targetX, 4845 y: targetY, 4846 vx: Math.cos(angle) * spd, 4847 vy: Math.sin(angle) * spd - 1.5, 4848 radius: 2.5 + Math.random() * 3.5, 4849 color: pColor, 4850 alpha: 1, 4851 decay: 0.035 + Math.random() * 0.03 4852 }); 4853 } 4854 popupsRef.current.push({ 4855 text: popupText, 4856 color: popupColor, 4857 x: targetX, 4858 y: targetY - 26, 4859 alpha: 1, 4860 points: pts 4861 }); 4862 if (closestNote.isHold && closestNote.duration >= 0.35) { 4863 activeHoldsRef.current.set(lane, { 4864 note: closestNote, 4865 lane, 4866 startTime: now, 4867 isStar: closestNote.type === "star" 4868 }); 4869 meowChoirAudio.startHoldMeow(lane, closestNote.pitch, closestNote.type === "star"); 4870 } 4871 onNoteHit(lane, hitType, closestNote); 4872 } else { 4873 meowChoirAudio.playBubblePop(0.85); 4874 if (onEmptyTap) onEmptyTap(lane); 4875 } 4876 }, [getSmoothCurrentTime, onNoteHit, onEmptyTap]); 4877 const handleLaneRelease = reactExports.useCallback((lane) => { 4878 lanePressRef.current[lane] = false; 4879 const activeHold = activeHoldsRef.current.get(lane); 4880 if (activeHold) { 4881 activeHoldsRef.current.delete(lane); 4882 meowChoirAudio.stopHoldMeow(lane); 4883 const now = getSmoothCurrentTime(); 4884 const heldDuration = now - activeHold.startTime; 4885 const targetDuration = activeHold.note.duration; 4886 if (heldDuration >= targetDuration * 0.65) { 4887 const canvas = canvasRef.current; 4888 const width = (canvas == null ? void 0 : canvas.clientWidth) || 400; 4889 const height = (canvas == null ? void 0 : canvas.clientHeight) || 450; 4890 const laneWidth = width / 4; 4891 const targetX = laneWidth * lane + laneWidth / 2; 4892 const targetY = height - 44; 4893 popupsRef.current.push({ 4894 text: "PURRFECT HOLD!", 4895 color: "#facc15", 4896 x: targetX, 4897 y: targetY - 36, 4898 alpha: 1, 4899 points: "+500" 4900 }); 4901 } 4902 } 4903 }, [getSmoothCurrentTime]); 4904 const activePointerLanes = reactExports.useRef(/* @__PURE__ */ new Map()); 4905 const handlePointerDown = (e) => { 4906 var _a; 4907 const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect(); 4908 if (!rect) return; 4909 const clientX = e.clientX - rect.left; 4910 const lane = Math.min(3, Math.max(0, Math.floor(clientX / rect.width * 4))); 4911 activePointerLanes.current.set(e.pointerId, lane); 4912 handleLanePress(lane); 4913 }; 4914 const handlePointerUp = (e) => { 4915 const lane = activePointerLanes.current.get(e.pointerId); 4916 if (lane !== void 0) { 4917 activePointerLanes.current.delete(e.pointerId); 4918 handleLaneRelease(lane); 4919 } 4920 }; 4921 const handlePointerCancel = (e) => { 4922 const lane = activePointerLanes.current.get(e.pointerId); 4923 if (lane !== void 0) { 4924 activePointerLanes.current.delete(e.pointerId); 4925 handleLaneRelease(lane); 4926 } 4927 }; 4928 reactExports.useEffect(() => { 4929 const handleKeyDown = (e) => { 4930 if (e.repeat) return;
4931 const k = e.key.toLowerCase(); 4932 let lane = -1; 4933 if (k === "d" || k === "arrowleft") lane = 0; 4934 else if (k === "f" || k === "arrowdown") lane = 1; 4935 else if (k === "j" || k === "arrowup") lane = 2; 4936 else if (k === "k" || k === "arrowright") lane = 3; 4937 if (lane !== -1) { 4938 e.preventDefault(); 4939 handleLanePress(lane); 4940 } 4941 }; 4942 const handleKeyUp = (e) => { 4943 const k = e.key.toLowerCase(); 4944 let lane = -1; 4945 if (k === "d" || k === "arrowleft") lane = 0; 4946 else if (k === "f" || k === "arrowdown") lane = 1; 4947 else if (k === "j" || k === "arrowup") lane = 2; 4948 else if (k === "k" || k === "arrowright") lane = 3; 4949 if (lane !== -1) { 4950 e.preventDefault(); 4951 handleLaneRelease(lane); 4952 } 4953 }; 4954 window.addEventListener("keydown", handleKeyDown); 4955 window.addEventListener("keyup", handleKeyUp); 4956 return () => { 4957 window.removeEventListener("keydown", handleKeyDown); 4958 window.removeEventListener("keyup", handleKeyUp); 4959 }; 4960 }, [handleLanePress, handleLaneRelease]); 4961 reactExports.useEffect(() => { 4962 let animId; 4963 const canvas = canvasRef.current; 4964 if (!canvas) return; 4965 const ctx = canvas.getContext("2d"); 4966 if (!ctx) return; 4967 const approachTime = propApproachTime; 4968 const render = () => { 4969 var _a; 4970 const dpr = window.devicePixelRatio || 1; 4971 const width = canvas.clientWidth; 4972 const height = canvas.clientHeight; 4973 if (canvas.width !== width * dpr || canvas.height !== height * dpr) { 4974 canvas.width = width * dpr; 4975 canvas.height = height * dpr; 4976 } 4977 ctx.save(); 4978 ctx.scale(dpr, dpr); 4979 ctx.clearRect(0, 0, width, height); 4980 const laneWidth = width / 4; 4981 const targetY = height - 44; 4982 const ringRadius = Math.min(26, Math.max(20, laneWidth * 0.26)); 4983 const now = getSmoothCurrentTime(); 4984 const topTrimHeight = 12; 4985 ctx.fillStyle = "#b47b42"; 4986 ctx.fillRect(0, 0, width, topTrimHeight); 4987 ctx.fillStyle = "#6e4318"; 4988 ctx.fillRect(0, topTrimHeight - 2, width, 2); 4989 const pianoGrad = ctx.createLinearGradient(0, topTrimHeight, 0, height); 4990 pianoGrad.addColorStop(0, "#e2e8f0"); 4991 pianoGrad.addColorStop(0.7, "#f8fafc"); 4992 pianoGrad.addColorStop(1, "#ffffff"); 4993 ctx.fillStyle = pianoGrad; 4994 ctx.fillRect(0, topTrimHeight, width, height - topTrimHeight); 4995 ctx.strokeStyle = "#cbd5e1"; 4996 ctx.lineWidth = 1.5; 4997 for (let i = 1; i < 4; i++) { 4998 const lx = laneWidth * i; 4999 ctx.beginPath(); 5000 ctx.moveTo(lx, topTrimHeight); 5001 ctx.lineTo(lx, height); 5002 ctx.stroke(); 5003 } 5004 const blackKeyWidth = Math.min(22, laneWidth * 0.3); 5005 const blackKeyHeight = (height - topTrimHeight) * 0.45; 5006 [1, 2, 3].forEach((dividerLane) => { 5007 const bx = laneWidth * dividerLane - blackKeyWidth / 2; 5008 ctx.fillStyle = "rgba(0, 0, 0, 0.20)"; 5009 ctx.fillRect(bx + 2, topTrimHeight + 2, blackKeyWidth, blackKeyHeight); 5010 const bkGrad = ctx.createLinearGradient(bx, topTrimHeight, bx + blackKeyWidth, topTrimHeight); 5011 bkGrad.addColorStop(0, "#1e293b"); 5012 bkGrad.addColorStop(0.5, "#334155"); 5013 bkGrad.addColorStop(1, "#0f172a"); 5014 ctx.fillStyle = bkGrad; 5015 ctx.beginPath(); 5016 ctx.roundRect(bx, topTrimHeight, blackKeyWidth, blackKeyHeight, [0, 0, 4, 4]); 5017 ctx.fill(); 5018 ctx.fillStyle = "#475569"; 5019 ctx.fillRect(bx, topTrimHeight + blackKeyHeight - 3, blackKeyWidth, 3); 5020 }); 5021 for (let i = 0; i < 4; i++) { 5022 const targetX = laneWidth * i + laneWidth / 2; 5023 const isPressed = lanePressRef.current[i]; 5024 const isHolding = activeHoldsRef.current.has(i); 5025 ctx.save(); 5026 if (isPressed || isHolding) { 5027 const glowGrad = ctx.createLinearGradient(0, topTrimHeight, 0, targetY + ringRadius); 5028 glowGrad.addColorStop(0, "rgba(192, 132, 252, 0.05)"); 5029 glowGrad.addColorStop(0.7, "rgba(192, 132, 252, 0.25)"); 5030 glowGrad.addColorStop(1, isHolding ? "rgba(250, 204, 21, 0.45)" : "rgba(192, 132, 252, 0.50)"); 5031 ctx.fillStyle = glowGrad; 5032 ctx.fillRect(laneWidth * i, topTrimHeight, laneWidth, height - topTrimHeight); 5033 } 5034 ctx.beginPath(); 5035 ctx.arc(targetX, targetY + 2, ringRadius + 2, 0, Math.PI * 2); 5036 ctx.fillStyle = "rgba(0, 0, 0, 0.22)"; 5037 ctx.fill(); 5038 ctx.beginPath(); 5039 ctx.arc(targetX, targetY, ringRadius, 0, Math.PI * 2); 5040 ctx.fillStyle = isPressed || isHolding ? "rgba(192, 132, 252, 0.65)" : "rgba(51, 65, 85, 0.40)"; 5041 ctx.fill(); 5042 ctx.strokeStyle = isPressed || isHolding ? "#ffffff" : "#cbd5e1"; 5043 ctx.lineWidth = isPressed || isHolding ? 4 : 3; 5044 ctx.stroke(); 5045 ctx.beginPath(); 5046 ctx.arc(targetX, targetY, ringRadius - 5, 0, Math.PI * 2); 5047 ctx.strokeStyle = isPressed || isHolding ? "#facc15" : "rgba(255, 255, 255, 0.4)"; 5048 ctx.lineWidth = 1.5; 5049 ctx.stroke(); 5050 const isMobile = typeof window !== "undefined" && (isMobileDevice() || window.matchMedia && window.matchMedia("(pointer: coarse)").matches && !window.matchMedia("(pointer: fine)").matches); 5051 if (!isMobile) {
5052 const keyLetter = LANE_KEYS[i]; 5053 const badgeWidth = 24; 5054 const badgeHeight = 16; 5055 const badgeX = targetX - badgeWidth / 2; 5056 const badgeY = targetY - badgeHeight / 2; 5057 ctx.save(); 5058 ctx.beginPath(); 5059 ctx.roundRect(badgeX, badgeY, badgeWidth, badgeHeight, 4); 5060 if (isPressed || isHolding) { 5061 ctx.fillStyle = "#facc15"; 5062 ctx.fill(); 5063 ctx.strokeStyle = "#ffffff"; 5064 ctx.lineWidth = 1.5; 5065 ctx.stroke(); 5066 ctx.fillStyle = "#0f172a"; 5067 ctx.font = "black 11px monospace"; 5068 ctx.textAlign = "center"; 5069 ctx.textBaseline = "middle"; 5070 ctx.fillText(keyLetter, targetX, targetY); 5071 } else { 5072 ctx.fillStyle = "rgba(15, 23, 42, 0.75)"; 5073 ctx.fill(); 5074 ctx.strokeStyle = "rgba(255, 255, 255, 0.45)"; 5075 ctx.lineWidth = 1; 5076 ctx.stroke(); 5077 ctx.fillStyle = "#ffffff"; 5078 ctx.font = "bold 10px monospace"; 5079 ctx.textAlign = "center"; 5080 ctx.textBaseline = "middle"; 5081 ctx.fillText(keyLetter, targetX, targetY); 5082 } 5083 ctx.restore(); 5084 } else { 5085 ctx.save(); 5086 ctx.beginPath(); 5087 ctx.arc(targetX, targetY, isPressed || isHolding ? 7 : 4, 0, Math.PI * 2); 5088 ctx.fillStyle = isPressed || isHolding ? "#facc15" : "rgba(255, 255, 255, 0.45)"; 5089 ctx.fill(); 5090 ctx.restore(); 5091 } 5092 if (isHolding) { 5093 const h = activeHoldsRef.current.get(i); 5094 const pCol = (h == null ? void 0 : h.isStar) ? "#facc15" : "#c084fc"; 5095 for (let s = 0; s < 2; s++) { 5096 const spd = 2 + Math.random() * 3.5; 5097 const ang = -Math.PI / 2 + (Math.random() - 0.5) * 1.5; 5098 particlesRef.current.push({ 5099 x: targetX + (Math.random() - 0.5) * 16, 5100 y: targetY - 4, 5101 vx: Math.cos(ang) * spd, 5102 vy: Math.sin(ang) * spd, 5103 radius: 2 + Math.random() * 2.5, 5104 color: pCol, 5105 alpha: 1, 5106 decay: 0.05 5107 }); 5108 } 5109 } 5110 ctx.restore(); 5111 } 5112 const notesList = notesRef.current; 5113 const scrollDistance = targetY - topTrimHeight; 5114 for (let i = 0; i < notesList.length; i++) { 5115 const note = notesList[i]; 5116 const dt = note.t - now; 5117 const isProcessed = processedIdsRef.current.has(note.id); 5118 const isStar = note.type === "star"; 5119 const bubbleR = ringRadius * 0.9; 5120 const bubbleX = laneWidth * note.lane + laneWidth / 2; 5121 if (dt > approachTime || dt < -0.3 && !note.isHold) continue; 5122 const progress = 1 - dt / approachTime; 5123 const bubbleY = topTrimHeight + progress * scrollDistance; 5124 if (note.isHold && note.duration >= 0.35) { 5125 const tailLengthPx = note.duration / approachTime * scrollDistance; 5126 const tailTopY = bubbleY - tailLengthPx; 5127 const isLaneHolding = activeHoldsRef.current.has(note.lane) && ((_a = activeHoldsRef.current.get(note.lane)) == null ? void 0 : _a.note.id) === note.id; 5128 if (bubbleY >= topTrimHeight && tailTopY <= targetY + 20) { 5129 ctx.save(); 5130 const ribbonStartY = Math.max(topTrimHeight, tailTopY); 5131 const ribbonEndY = isLaneHolding ? targetY : Math.min(targetY + 20, bubbleY); 5132 const ribbonWidth = bubbleR * 1.25; 5133 if (ribbonEndY > ribbonStartY) { 5134 const ribGrad = ctx.createLinearGradient(0, ribbonStartY, 0, ribbonEndY); 5135 if (isStar) { 5136 ribGrad.addColorStop(0, "rgba(250, 204, 21, 0.3)"); 5137 ribGrad.addColorStop(1, "rgba(250, 204, 21, 0.75)"); 5138 } else { 5139 ribGrad.addColorStop(0, "rgba(168, 85, 247, 0.3)"); 5140 ribGrad.addColorStop(1, "rgba(216, 180, 254, 0.75)"); 5141 } 5142 ctx.beginPath(); 5143 ctx.roundRect(bubbleX - ribbonWidth / 2, ribbonStartY, ribbonWidth, ribbonEndY - ribbonStartY, 8); 5144 ctx.fillStyle = ribGrad; 5145 ctx.fill(); 5146 ctx.strokeStyle = isStar ? "rgba(254, 240, 138, 0.8)" : "rgba(255, 255, 255, 0.7)"; 5147 ctx.lineWidth = 1.5; 5148 ctx.stroke(); 5149 ctx.beginPath(); 5150 ctx.moveTo(bubbleX, ribbonStartY + 4); 5151 ctx.lineTo(bubbleX, ribbonEndY - 4); 5152 ctx.strokeStyle = "rgba(255, 255, 255, 0.6)"; 5153 ctx.lineWidth = 2; 5154 ctx.stroke(); 5155 }
5156 ctx.restore(); 5157 } 5158 } 5159 if (isProcessed && !note.isHold) continue; 5160 let bubbleAlpha = 1; 5161 if (dt < 0) { 5162 bubbleAlpha = Math.max(0, 1 + dt / 0.3); 5163 } 5164 ctx.save(); 5165 ctx.globalAlpha = bubbleAlpha; 5166 ctx.beginPath(); 5167 ctx.arc(bubbleX, bubbleY, bubbleR + 5, 0, Math.PI * 2); 5168 ctx.fillStyle = isStar ? "rgba(250, 204, 21, 0.40)" : "rgba(192, 132, 252, 0.45)"; 5169 ctx.fill(); 5170 const bGrad = ctx.createRadialGradient( 5171 bubbleX - bubbleR * 0.3, 5172 bubbleY - bubbleR * 0.3, 5173 bubbleR * 0.1, 5174 bubbleX, 5175 bubbleY, 5176 bubbleR 5177 ); 5178 if (isStar) { 5179 bGrad.addColorStop(0, "#ffffff"); 5180 bGrad.addColorStop(0.3, "rgba(254, 240, 138, 0.9)"); 5181 bGrad.addColorStop(1, "#eab308"); 5182 } else { 5183 bGrad.addColorStop(0, "#ffffff"); 5184 bGrad.addColorStop(0.35, "rgba(216, 180, 254, 0.9)"); 5185 bGrad.addColorStop(0.85, "#9333ea"); 5186 bGrad.addColorStop(1, "#6b21a8"); 5187 } 5188 ctx.beginPath(); 5189 ctx.arc(bubbleX, bubbleY, bubbleR, 0, Math.PI * 2); 5190 ctx.fillStyle = bGrad; 5191 ctx.fill(); 5192 ctx.beginPath(); 5193 ctx.ellipse( 5194 bubbleX - bubbleR * 0.35, 5195 bubbleY - bubbleR * 0.35, 5196 bubbleR * 0.3, 5197 bubbleR * 0.15, 5198 -Math.PI / 4, 5199 0, 5200 Math.PI * 2 5201 ); 5202 ctx.fillStyle = "rgba(255, 255, 255, 0.92)"; 5203 ctx.fill(); 5204 ctx.beginPath(); 5205 ctx.arc(bubbleX, bubbleY, bubbleR, 0, Math.PI * 2); 5206 ctx.strokeStyle = isStar ? "#fef08a" : "#ffffff"; 5207 ctx.lineWidth = 2; 5208 ctx.stroke(); 5209 ctx.fillStyle = "#ffffff"; 5210 ctx.textAlign = "center"; 5211 ctx.textBaseline = "middle"; 5212 ctx.font = isStar ? "bold 15px sans-serif" : "bold 17px serif"; 5213 ctx.fillText(isStar ? "â " : "âª", bubbleX, bubbleY + 1); 5214 ctx.restore(); 5215 } 5216 if (isPlayingRef.current) { 5217 for (let i = notesList.length - 1; i >= 0; i--) { 5218 const n = notesList[i]; 5219 if (processedIdsRef.current.has(n.id)) continue; 5220 if (n.t < now - 0.25) { 5221 processedIdsRef.current.add(n.id); 5222 onNoteMiss(n.lane, n); 5223 } 5224 } 5225 } 5226 const particles = particlesRef.current; 5227 for (let i = particles.length - 1; i >= 0; i--) { 5228 const p = particles[i]; 5229 p.x += p.vx; 5230 p.y += p.vy; 5231 p.vy += 0.16; 5232 p.alpha -= p.decay; 5233 if (p.alpha <= 0) { 5234 particles.splice(i, 1); 5235 continue; 5236 } 5237 ctx.save(); 5238 ctx.globalAlpha = p.alpha; 5239 ctx.fillStyle = p.color; 5240 ctx.beginPath(); 5241 ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); 5242 ctx.fill(); 5243 ctx.restore(); 5244 } 5245 const popups = popupsRef.current; 5246 for (let i = popups.length - 1; i >= 0; i--) { 5247 const pop = popups[i]; 5248 pop.y -= 1.1; 5249 pop.alpha -= 0.025; 5250 if (pop.alpha <= 0) { 5251 popups.splice(i, 1); 5252 continue; 5253 } 5254 ctx.save(); 5255 ctx.globalAlpha = pop.alpha; 5256 ctx.textAlign = "center"; 5257 ctx.textBaseline = "middle"; 5258 ctx.font = "black 14px monospace"; 5259 ctx.fillStyle = "#000000"; 5260 ctx.fillText(pop.text, pop.x + 1, pop.y + 1); 5261 ctx.fillStyle = pop.color; 5262 ctx.fillText(pop.text, pop.x, pop.y); 5263 if (pop.points) { 5264 ctx.font = "bold 11px monospace"; 5265 ctx.fillStyle = "#ffffff"; 5266 ctx.fillText(pop.points, pop.x, pop.y + 13); 5267 } 5268 ctx.restore(); 5269 } 5270 ctx.restore(); 5271 animId = requestAnimationFrame(render); 5272 }; 5273 animId = requestAnimationFrame(render); 5274 return () => { 5275 cancelAnimationFrame(animId); 5276 }; 5277 }, [propApproachTime, onNoteMiss, getSmoothCurrentTime]); 5278 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5279 "div", 5280 { 5281 ref: containerRef, 5282 className: "w-full h-full relative overflow-hidden select-none cursor-pointer", 5283 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5284 "canvas", 5285 { 5286 ref: canvasRef, 5287 onPointerDown: handlePointerDown, 5288 onPointerUp: handlePointerUp, 5289 onPointerCancel: handlePointerCancel, 5290 className: "w-full h-full block touch-none" 5291 } 5292 ) 5293 } 5294 ); 5295}; 5296const MeowChoirResultsScreen = ({ 5297 isOpen, 5298 result, 5299 locale, 5300 songTitle, 5301 onPlayAgain, 5302 onBackToArcade 5303}) => { 5304 if (!isOpen || !result) return null; 5305 const grade = result.accuracy >= 95 ? "S" : result.accuracy >= 85 ? "A" : result.accuracy >= 70 ? "B" : "C"; 5306 const gradeColor = grade === "S" ? "from-amber-300 via-yellow-400 to-amber-500" : grade === "A" ? "from-purple-400 via-pink-400 to-rose-400" : grade === "B" ? "from-sky-4
530600 to-blue-500" : "from-slate-400 to-zinc-500"; 5307 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/85 backdrop-blur-md animate-in fade-in zoom-in-95 duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-gradient-to-b from-[#11183c] via-[#0f172a] to-[#0a0f24] border-2 border-purple-500/50 rounded-3xl p-6 shadow-[0_0_50px_rgba(168,85,247,0.35)] flex flex-col items-center text-center", children: [ 5308 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-900/60 border border-purple-400/40 text-purple-300 text-xs font-bold uppercase tracking-wider mb-2", children: [ 5309 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 13, className: "text-amber-300" }), 5310 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.resultsTitle }) 5311 ] }), 5312 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl sm:text-2xl font-black text-white tracking-wide mb-1", children: locale.stageClear }), 5313 songTitle && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-purple-200/70 truncate max-w-[280px] mb-4", children: songTitle }), 5314 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full bg-slate-900/80 border border-purple-500/30 rounded-2xl p-4 mb-4 flex items-center justify-around", children: [
5315 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 5316 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 uppercase tracking-widest font-mono", children: "GRADE" }), 5317 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-6xl font-black text-transparent bg-clip-text bg-gradient-to-br ${gradeColor} drop-shadow-[0_0_15px_rgba(250,204,21,0.5)]`, children: grade }) 5318 ] }), 5319 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-end", children: [ 5320 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 uppercase tracking-widest font-mono", children: locale.finalScore }), 5321 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-3xl sm:text-4xl font-black text-white tracking-wider", children: result.score.toLocaleString() }), 5322 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-amber-400 font-bold mt-1", children: [ 5323 result.accuracy.toFixed(1), 5324 "% ", 5325 locale.accuracy 5326 ] }) 5327 ] }) 5328 ] }), 5329 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full grid grid-cols-2 gap-2 text-xs font-mono mb-6", children: [
5330 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-slate-900/60 border border-slate-800 rounded-xl p-2.5 flex items-center justify-between", children: [ 5331 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400 font-bold", children: locale.purrfect }), 5332 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-black", children: result.purrfectCount }) 5333 ] }), 5334 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-slate-900/60 border border-slate-800 rounded-xl p-2.5 flex items-center justify-between", children: [ 5335 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-purple-400 font-bold", children: locale.pawsome }), 5336 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-black", children: result.pawsomeCount }) 5337 ] }), 5338 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-slate-900/60 border border-slate-800 rounded-xl p-2.5 flex items-center justify-between", children: [
5339 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-emerald-400 font-bold", children: locale.good }), 5340 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-black", children: result.goodCount }) 5341 ] }), 5342 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-slate-900/60 border border-slate-800 rounded-xl p-2.5 flex items-center justify-between", children: [ 5343 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-rose-400 font-bold", children: locale.miss }), 5344 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-black", children: result.missCount }) 5345 ] }), 5346 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2 bg-slate-900/60 border border-slate-800 rounded-xl p-2.5 flex items-center justify-between", children: [ 5347 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-purple-300 font-bold", children: locale.maxCombo }), 5348 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-amber-400 font-black", children: [ 5349 result.maxCombo, 5350 "x" 5351 ] }) 5352 ] }) 5353 ] }), 5354 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex items-center gap-3", children: [
5355 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5356 "button", 5357 { 5358 type: "button", 5359 onClick: onPlayAgain, 5360 className: "flex-1 py-3 px-4 rounded-xl bg-gradient-to-r from-purple-600 via-pink-600 to-rose-600 hover:from-purple-500 hover:to-rose-500 text-white font-black text-sm uppercase tracking-wider shadow-[0_0_20px_rgba(168,85,247,0.4)] flex items-center justify-center gap-2 transition-all transform active:scale-95 cursor-pointer", 5361 children: [ 5362 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }), 5363 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.playAgain }) 5364 ] 5365 } 5366 ), 5367 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5368 "button", 5369 { 5370 type: "button", 5371 onClick: onBackToArcade, 5372 className: "py-3 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white font-bold text-xs uppercase tracking-wider border border-slate-700 flex items-center justify-center gap-1.5 transition-all cursor-pointer", 5373 children: [
5374 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 15 }), 5375 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.backToArcade }) 5376 ] 5377 } 5378 ) 5379 ] }) 5380 ] }) }); 5381}; 5382const DIFFICULTY_SPECS = { 5383 kitten: { 5384 minInterval: 0.85, 5385 minGlobalInterval: 0.45, 5386 pitchRange: [60, 76], 5387 starChance: 0.12, 5388 speedMultiplier: 0.85, 5389 approachTime: 3.5 5390 }, 5391 playful: { 5392 minInterval: 0.6, 5393 minGlobalInterval: 0.3, 5394 pitchRange: [55, 84], 5395 starChance: 0.15, 5396 speedMultiplier: 1, 5397 approachTime: 2.9 5398 }, 5399 wildcat: { 5400 minInterval: 0.45, 5401 minGlobalInterval: 0.22, 5402 pitchRange: [50, 88], 5403 starChance: 0.18, 5404 speedMultiplier: 1.15, 5405 approachTime: 2.4 5406 }, 5407 meowstro: { 5408 minInterval: 0.35, 5409 minGlobalInterval: 0.16, 5410 pitchRange: [48, 92], 5411 starChance: 0.22, 5412 speedMultiplier: 1.3, 5413 approachTime: 1.95 5414 } 5415}; 5416function createSeededPRNG(seedInput) { 5417 let h = 2166136261; 5418 const str = String(seedInput); 5419 for (let i = 0; i < str.length; i++) { 5420 h ^= str.charCodeAt(i); 5421 h = Math.imul(h, 16777619); 5422 } 5423 let s = h >>> 0; 5424 return function() { 5425 s |= 0; 5426 s = s + 1831565813 | 0; 5427 let t = Math.imul(s ^ s >>> 15, 1 | s); 5428 t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; 5429 return ((t ^ t >>> 14) >>> 0) / 4294967296; 5430 }; 5431} 5432function pitchToLane(pitch, minPitch, maxPitch) { 5433 const norm = Math.max(0, Math.min(1, (pitch - minPitch) / Math.max(1, maxPitch - minPitch))); 5434 return Math.min(3, Math.max(0, Math.floor(norm * 4))); 5435} 5436function extractNotesFromData(data) { 5437 if (!data) return []; 5438 const result = []; 5439 const parseSingle = (item) => { 5440 if (!item || typeof item !== "object") return null; 5441 const t = item.t !== void 0 ? Number(item.t) : item.time !== void 0 ? Number(item.time) : item.start !== void 0 ? Number(item.start) : null; 5442 const pitch = item.n !== void 0 ? Number(item.n) : item.midi !== void 0 ? Number(item.midi) : item.pitch !== void 0 ? Number(item.pitch) : null; 5443 const duration = item.d !== void 0 ? Number(item.d) : item.duration !== void 0 ? Number(item.duration) : 0.25; 5444 if (t === null || pitch === null || isNaN(t) || isNaN(pitch)) return null; 5445 return { 5446 t: Math.max(0, t), 5447 pitch: Math.min(127, Math.max(0, Math.round(pitch))), 5448 duration: isNaN(duration) || duration <= 0 ? 0.25 : duration, 5449 instrument: String(item.i || item.instrument || ""), 5450 trackIdx: item.trackIdx !== void 0 ? Number(item.trackIdx) : item.track !== void 0 ? Number(item.track) : void 0 5451 }; 5452 }; 5453 if (Array.isArray(data)) { 5454 if (data.length > 0) { 5455 const first = data[0]; 5456 if (first && typeof first === "object" && (Array.isArray(first.notes) || Array.isArray(first.events))) { 5457 data.forEach((track) => { 5458 const list = track.notes || track.events || []; 5459 if (Array.isArray(list)) { 5460 list.forEach((n) => { 5461 const p = parseSingle(n); 5462 if (p) { 5463 if (p.trackIdx === void 0 && track.trackIdx !== void 0) p.trackIdx = track.trackIdx; 5464 if (!p.instrument && track.name) p.instrument = track.name; 5465 result.push(p); 5466 } 5467 }); 5468 } 5469 }); 5470 } else { 5471 data.forEach((n) => { 5472 const p = parseSingle(n); 5473 if (p) result.push(p); 5474 }); 5475 } 5476 } 5477 } else if (typeof data === "object") { 5478 if (Array.isArray(data.notes)) { 5479 data.notes.forEach((n) => { 5480 const p = parseSingle(n); 5481 if (p) result.push(p); 5482 }); 5483 } else if (Array.isArray(data.tracks)) { 5484 data.tracks.forEach((track) => { 5485 const list = track.notes || track.events || []; 5486 if (Array.isArray(list)) { 5487 list.forEach((n) => { 5488 const p = parseSingle(n); 5489 if (p) result.push(p); 5490 }); 5491 } 5492 }); 5493 } 5494 } 5495 return result; 5496} 5497function isDrum(n) { 5498 if (n.trackIdx === 9 || n.trackIdx === 10) return true; 5499 const inst = (n.instrument || "").toLowerCase(); 5500 return inst.includes("drum") || inst.includes("percussion") || inst.includes("cymbals") || inst.includes("sfx"); 5501} 5502function generateMeowChoirChart(pianoData, melodyData, instruments, midiMetadata, difficulty = "playful", songDuration = 180, seedOrTrackId) { 5503 const spec = DIFFICULTY_SPECS[difficulty]; 5504 const rawEvents = []; 5505 const seed = seedOrTrackId || (midiMetadata == null ? void 0 : midiMetadata.title) || (midiMetadata == null ? void 0 : midiMetadata.tempo) || songDuration || 42; 5506 const rng = createSeededPRNG(seed); 5507 const minFirstNoteTime = 3.2; 5508 const parsedMelody = extractNotesFromData(melodyData).filter((n) => n.t >= minFirstNoteTime && n.t < songDuration && !isDrum(n)); 5509 let parsedPiano = extractNotesFromData(pianoData).filter((n) => n.t >= minFirstNoteTime && n.t < songDuration && !isDrum(n)); 5510 if (parsedMelody.length < 15 && parsedPiano.length > 0) { 5511 const leadInstNames = ["vocal", "voice", "sing", "lead", "melody", "solo", "sax", "flute", "trumpet", "violin", "clarinet"]; 5512 const leadTrackIndices = /* @__PURE__ */ new Set(); 5513 const leadInstrumentNames = /* @__PURE__ */ new Set(); 5514 if (Array.isArray(instruments)) {
5515 instruments.forEach((inst, idx) => { 5516 const name = String(inst.name || inst.displayName || inst.originalName || "").toLowerCase(); 5517 if (leadInstNames.some((keyword) => name.includes(keyword)) && inst.midiChannel !== 9) { 5518 leadTrackIndices.add(inst.trackId !== void 0 ? inst.trackId : idx); 5519 if (inst.name) leadInstrumentNames.add(inst.name.toLowerCase()); 5520 if (inst.displayName) leadInstrumentNames.add(inst.displayName.toLowerCase()); 5521 } 5522 }); 5523 } 5524 let filteredPianoNotes = []; 5525 if (leadTrackIndices.size > 0 || leadInstrumentNames.size > 0) { 5526 filteredPianoNotes = parsedPiano.filter((n) => { 5527 if (n.trackIdx !== void 0 && leadTrackIndices.has(n.trackIdx)) return true; 5528 if (n.instrument && leadInstrumentNames.has(n.instrument.toLowerCase())) return true; 5529 return false; 5530 }); 5531 } 5532 if (filteredPianoNotes.length >= 15) { 5533 parsedPiano = filteredPianoNotes; 5534 } else { 5535 parsedPiano = parsedPiano.filter((n) => n.pitch >= 52); 5536 } 5537 } 5538 if (parsedMelody.length >= 15) { 5539 parsedMelody.sort((a, b) => a.t - b.t); 5540 const firstMelodyTime = parsedMelody[0].t; 5541 if (firstMelodyTime > minFirstNoteTime + 3 && parsedPiano.length > 0) { 5542 const introNotes = parsedPiano.filter((n) => n.t >= minFirstNoteTime && n.t < firstMelodyTime - 0.4 && n.pitch >= 55); 5543 rawEvents.push(...introNotes); 5544 } 5545 rawEvents.push(...parsedMelody); 5546 } else if (parsedPiano.length > 0) { 5547 rawEvents.push(...parsedPiano); 5548 } 5549 if (rawEvents.length === 0) { 5550 const bpm = Number(midiMetadata == null ? void 0 : midiMetadata.tempo) || 120; 5551 const beatSec = 60 / bpm; 5552 let t = minFirstNoteTime + 0.5; 5553 const scale = [60, 62, 64, 65, 67, 69, 71, 72]; 5554 while (t < songDuration - 2) { 5555 const pitch = scale[Math.floor(rng() * scale.length)]; 5556 const isLong = rng() < 0.25; 5557 rawEvents.push({ t, duration: isLong ? beatSec * 1.8 : beatSec * 0.7, pitch }); 5558 t += beatSec * (difficulty === "kitten" ? 2 : 1); 5559 } 5560 } 5561 rawEvents.sort((a, b) => a.t - b.t); 5562 const isEasy = difficulty === "kitten" || difficulty === "playful"; 5563 const melodyEvents = []; 5564 for (let i = 0; i < rawEvents.length; i++) { 5565 const ev = rawEvents[i]; 5566 if (melodyEvents.length === 0) { 5567 melodyEvents.push(ev); 5568 } else { 5569 const last = melodyEvents[melodyEvents.length - 1]; 5570 const isSimultaneous = Math.abs(ev.t - last.t) < 0.08; 5571 if (isSimultaneous) { 5572 if (isEasy) { 5573 if (ev.pitch > last.pitch) { 5574 melodyEvents[melodyEvents.length - 1] = { 5575 t: last.t, 5576 duration: Math.max(last.duration, ev.duration), 5577 pitch: ev.pitch 5578 }; 5579 } 5580 } else { 5581 const countSimultaneous = melodyEvents.filter((m) => Math.abs(m.t - ev.t) < 0.08).length; 5582 if (countSimultaneous < 2 && Math.abs(ev.pitch - last.pitch) >= 4) { 5583 melodyEvents.push(ev); 5584 } else if (ev.pitch > last.pitch) { 5585 melodyEvents[melodyEvents.length - 1] = { 5586 t: last.t, 5587 duration: Math.max(last.duration, ev.duration), 5588 pitch: ev.pitch 5589 }; 5590 } 5591 } 5592 } else { 5593 melodyEvents.push(ev); 5594 } 5595 } 5596 } 5597 let minP = 127; 5598 let maxP = 0; 5599 melodyEvents.forEach((e) => { 5600 if (e.pitch < minP) minP = e.pitch; 5601 if (e.pitch > maxP) maxP = e.pitch; 5602 }); 5603 if (minP >= maxP) { 5604 minP = spec.pitchRange[0]; 5605 maxP = spec.pitchRange[1]; 5606 } 5607 const notes = []; 5608 const lastLaneTimes = [-999, -999, -999, -999]; 5609 let lastGlobalTime = -999; 5610 let activeHoldEndTime = -999; 5611 let noteIdCounter = 1; 5612 let starCount = 0; 5613 for (let i = 0; i < melodyEvents.length; i++) { 5614 const ev = melodyEvents[i]; 5615 if (isEasy) { 5616 if (ev.t < activeHoldEndTime + 0.15) { 5617 continue; 5618 } 5619 if (ev.t - lastGlobalTime < spec.minGlobalInterval) { 5620 continue; 5621 } 5622 } else { 5623 const isChord = Math.abs(ev.t - lastGlobalTime) < 0.08; 5624 if (!isChord && ev.t - lastGlobalTime < spec.minGlobalInterval) { 5625 continue; 5626 } 5627 } 5628 let lane = pitchToLane(ev.pitch, minP, maxP); 5629 if (ev.t - lastLaneTimes[lane] < spec.minInterval) { 5630 const altLanes = [(lane + 1) % 4, (lane + 3) % 4, (lane + 2) % 4]; 5631 const freeLane = altLanes.find((l) => ev.t - lastLaneTimes[l] >= spec.minInterval); 5632 if (freeLane !== void 0) { 5633 lane = freeLane; 5634 } else { 5635 continue; 5636 } 5637 } 5638 lastLaneTimes[lane] = ev.t; 5639 lastGlobalTime = ev.t; 5640 const isStar = rng() < spec.starChance; 5641 if (isStar) starCount++; 5642 const isHold = ev.duration >= 0.38; 5643 const duration = isHold ? Math.min(2.2, Number(ev.duration.toFixed(3))) : 0.25; 5644 if (isEasy) { 5645 activeHoldEndTime = ev.t + (isHold ? duration : 0.25); 5646 } 5647 notes.push({ 5648 id: noteIdCounter++, 5649 lane, 5650 t: Number(ev.t.toFixed(3)), 5651 duration, 5652 pitch: ev.pitch, 5653 type: isStar ? "star" : "normal", 5654 isHold 5655 }); 5656 } 5657 const durationSpan = Math.max(1, songDuration); 5658 const density = Number((notes.length / durationSpan).toFixed(2)); 5659 return { 5660 notes, 5661 meta: { 5662 difficulty, 5663 totalNotes: notes.length, 5664 starNotes: starCount, 5665 density, 5666 approachTime: spec.approachTime 5667 } 5668 }; 5669} 5670const MEOW_CHOIR_LOCALES = { 5671 en: { 5672 gameTitle: "Meow Choir", 5673 gameSubtitle: "Kitten Stage Chorus Rhythm Adventure", 5674 tapToPlay: "TAP TO PLAY", 5675 tapToPlaySub: "Lead the kitten chorus around the mic!", 5676 keyboardHint: "Keys [D] [F] [J] [K] or Arrows", 5677 touchHint: "Tap the piano rings or purple bubbles", 5678 score: "SCORE", 5679 combo: "COMBO", 5680 purrfect: "PURRFECT!", 5681 pawsome: "PAWSOME!", 5682 good: "GOOD", 5683 miss: "MISS", 5684 fever: "CATNIP FEVER", 5685 feverActive: "FEVER x2!", 5686 paused: "PAUSED", 5687 resume: "RESUME", 5688 restart: "RESTART", 5689 backToArcade: "EXIT TO ARCADE", 5690 resultsTitle: "STAGE CLEAR!", 5691 stageClear: "MEOWVELOUS PERFORMANCE!", 5692 finalScore: "FINAL SCORE", 5693 maxCombo: "MAX COMBO", 5694 accuracy: "ACCURACY", 5695 playAgain: "SING AGAIN", 5696 difficulty: "DIFFICULTY", 5697 kitten: "KITTEN", 5698 playful: "PLAYFUL", 5699 wildcat: "WILDCAT", 5700 meowstro: "MEOWSTRO" 5701 }, 5702 ja: { 5703 gameTitle: "ã«ãããåå±å£", 5704 gameSubtitle: "ãã¤ã¯ãå²ãã§æãåç«ãã¡ã®ãªãºã åå±å", 5705 tapToPlay: "ã¿ãããã¦ã¹ã¿ã¼ã", 5706 tapToPlaySub: "ãã¤ã¯ã®å¨ãã§åç«ãã¡ã¨ä¸ç·ã«æããï¼", 5707 keyboardHint: "ãã¼ãã¼ã [D] [F] [J] [K] ã¾ã㯠ç¢å°ãã¼", 5708 touchHint: "ãã¢ãã®ãªã³ã°ã¾ãã¯ç´«ã®é³ç¬¦ããã«ãã¿ãã", 5709 score: "ã¹ã³ã¢", 5710 combo: "ã³ã³ã", 5711 purrfect: "ãã¼ãã§ã¯ãã«ãï¼",
5712 pawsome: "ãã°ãããã«ãï¼", 5713 good: "ãããï¼", 5714 miss: "ãã¹â¦", 5715 fever: "ãã¿ã¿ããã£ã¼ãã¼", 5716 feverActive: "ãã£ã¼ãã¼ x2ï¼", 5717 paused: "䏿忢", 5718 resume: "ã¤ã¥ãã", 5719 restart: "ããç´ã", 5720 backToArcade: "ã²ã¼ã ã»ã³ã¿ã¼ã«æ»ã", 5721 resultsTitle: "ã¹ãã¼ã¸ã¯ãªã¢ï¼", 5722 stageClear: "ç´ æ´ãããåå±ã§ããã«ãï¼", 5723 finalScore: "æçµã¹ã³ã¢", 5724 maxCombo: "æå¤§ã³ã³ã", 5725 accuracy: "æ£ç¢ºã", 5726 playAgain: "ããä¸åº¦æã", 5727 difficulty: "é£æåº¦", 5728 kitten: "ããã", 5729 playful: "ãããã", 5730 wildcat: "ã®ããã", 5731 meowstro: "ãã£ã¨ã¹ãã" 5732 }, 5733 "zh-CN": { 5734 gameTitle: "åµåµåå±å¢", 5735 gameSubtitle: "å´ç»éº¦å é£é½å£°æå±çç«åªèå°è奿¸¸æ", 5736 tapToPlay: "ç¹å»å¼å§", 5737 tapToPlaySub: "带é¢ç«åªåå±å¢ç»å°å¼å±ï¼", 5738 keyboardHint: "æé® [D] [F] [J] [K] æ æ¹åé®", 5739 touchHint: "ç¹å»ç´é®ä¸çåç¯æç´«è²é³ç¬¦æ°æ³¡", 5740 score: "å¾å", 5741 combo: "è¿å»", 5742 purrfect: "å®ç¾åµï¼", 5743 pawsome: "精彩åµï¼", 5744 good: "è¯å¥½", 5745 miss: "失误", 5746 fever: "ç«èè·çç", 5747 feverActive: "ååçç x2ï¼", 5748 paused: "å·²æå", 5749 resume: "ç»§ç»æ¸¸æ", 5750 restart: "éæ°å¼å§", 5751 backToArcade: "éåºå°è¡æºå ", 5752 resultsTitle: "æ¼åºéå ³ï¼", 5753 stageClear: "ç¾å¦ç»ä¼¦çç«åªå声ï¼", 5754 finalScore: "æç»å¾å", 5755 maxCombo: "æé«è¿å»", 5756 accuracy: "åç¡®ç", 5757 playAgain: "åå±ä¸æ²", 5758 difficulty: "é¾åº¦", 5759 kitten: "å¹¼ç«çº§", 5760 playful: "欢快级", 5761 wildcat: "éæ§çº§", 5762 meowstro: "ç«ç大å¸" 5763 }, 5764 "zh-TW": { 5765 gameTitle: "åµåµåå±å", 5766 gameSubtitle: "åç¹éº¥å 風é½è²åå±çè²åªèå°ç¯å¥éæ²", 5767 tapToPlay: "黿éå§", 5768 tapToPlaySub: "帶é å°è²åå±åç»å°éå±ï¼", 5769 keyboardHint: "æéµ [D] [F] [J] [K] æ æ¹åéµ", 5770 touchHint: "黿ç´éµä¸çåç°æç´«è²é³ç¬¦æ°£æ³¡", 5771 score: "å¾å", 5772 combo: "飿", 5773 purrfect: "å®ç¾åµï¼", 5774 pawsome: "精彩åµï¼", 5775 good: "è¯å¥½", 5776 miss: "失誤", 5777 fever: "è²èè·çç±", 5778 feverActive: "éåçç± x2ï¼", 5779 paused: "å·²æ«å", 5780 resume: "ç¹¼çºéæ²", 5781 restart: "éæ°éå§", 5782 backToArcade: "è¿åè¡æ©ä¸å¿", 5783 resultsTitle: "æ¼åºééï¼", 5784 stageClear: "ç¾å¦çµå«çè²åªåè²ï¼", 5785 finalScore: "æçµå¾å", 5786 maxCombo: "æé«é£æ", 5787 accuracy: "æºç¢ºç", 5788 playAgain: "åå±ä¸æ¬¡", 5789 difficulty: "é£åº¦", 5790 kitten: "å¹¼è²ç´", 5791 playful: "æ¡å¿«ç´", 5792 wildcat: "éæ§ç´", 5793 meowstro: "è²ç大師" 5794 }, 5795 ko: { 5796 gameTitle: "ì¼ì¹ì´ í©ì°½ë¨", 5797 gameSubtitle: "ë§ì´í¬ 주ììì í¨ê» ë ¸ëíë ìê¸°ê³ ìì´ ë¦¬ë¬ í©ì°½ë¨", 5798 tapToPlay: "ííì¬ ìì", 5799 tapToPlaySub: "ë§ì´í¬ ììì ê·ì¬ì´ ê³ ìì´ë¤ê³¼ í©ì°½í´ì!", 5800 keyboardHint: "í¤ë³´ë [D] [F] [J] [K] ëë ë°©í¥í¤", 5801 touchHint: "í¼ìë ¸ ê±´ë°ì ë§ì´ë ë³´ë¼ì ìí ë°©ì¸ì í°ì¹íì¸ì", 5802 score: "ì ì", 5803 combo: "콤보", 5804 purrfect: "ìë²½ë¥!", 5805 pawsome: "ëë¨í´ë¥!", 5806 good: "ì¢ìì", 5807 miss: "미ì¤", 5808 fever: "캣ë í¼ë²", 5809 feverActive: "í¼ë² 2ë°°!", 5810 paused: "ì¼ìì ì§", 5811 resume: "ê³ìí기", 5812 restart: "ë¤ìí기", 5813 backToArcade: "ìì¼ì´ëë¡ ëê°ê¸°", 5814 resultsTitle: "ì¤í ì´ì§ í´ë¦¬ì´!", 5815 stageClear: "íìì ì¸ ì¼ì¹ì´ í©ì°½ì´ììµëë¤!", 5816 finalScore: "ìµì¢ ì ì", 5817 maxCombo: "ìµë 콤보", 5818 accuracy: "ì íë", 5819 playAgain: "ë¤ì ë ¸ëí기", 5820 difficulty: "ëì´ë", 5821 kitten: "ìê¸°ê³ ìì´", 5822 playful: "ë°ëë¥", 5823 wildcat: "ì©ë§¹ë¥", 5824 meowstro: "ë§ìì¤í¸ë¡" 5825 }, 5826 es: { 5827 gameTitle: "Coro Miau", 5828 gameSubtitle: "Aventura rÃtmica de gatitos cantando al micrófono", 5829 tapToPlay: "TOCA PARA JUGAR", 5830 tapToPlaySub: "¡Dirige el coro de gatitos alrededor del micrófono!", 5831 keyboardHint: "Teclas [D] [F] [J] [K] o Flechas", 5832 touchHint: "Toca los aros del piano o las burbujas púrpuras", 5833 score: "PUNTUACIÃN", 5834 combo: "COMBO", 5835 purrfect: "¡MIAU-FECTO!", 5836 pawsome: "¡FANTÃSTICO!", 5837 good: "BIEN", 5838 miss: "FALLO", 5839 fever: "FIEBRE GATUNA", 5840 feverActive: "¡FIEBRE x2!", 5841 paused: "PAUSADO", 5842 resume: "CONTINUAR", 5843 restart: "REINICIAR", 5844 backToArcade: "VOLVER AL ARCADE", 5845 resultsTitle: "¡NIVEL COMPLETADO!", 5846 stageClear: "¡UNA ACTUACIÃN MIAU-RAVILLOSA!", 5847 finalScore: "PUNTOS FINALES", 5848 maxCombo: "COMBO MÃXIMO", 5849 accuracy: "PRECISIÃN", 5850 playAgain: "CANTAR OTRA VEZ", 5851 difficulty: "DIFICULTAD", 5852 kitten: "GATITO", 5853 playful: "JUGUETÃN", 5854 wildcat: "SALVAJE", 5855 meowstro: "MAESTRO" 5856 }, 5857 fr: { 5858 gameTitle: "ChÅur des Miaous", 5859 gameSubtitle: "ChÅur de chatons chantant autour du micro", 5860 tapToPlay: "TOUCHER POUR JOUER", 5861 tapToPlaySub: "Dirigez la chorale de chatons sur scène !", 5862 keyboardHint: "Touches [D] [F] [J] [K] ou Flèches", 5863 touchHint: "Touchez les anneaux du piano ou les bulles violettes", 5864 score: "SCORE", 5865 combo: "COMBO", 5866 purrfect: "PARCHAT!", 5867 pawsome: "MIAOU-NIFIQUE!", 5868 good: "BIEN", 5869 miss: "MANQUÃ", 5870 fever: "FOLIE DâHERBE AUX CHATS", 5871 feverActive: "FIÃVRE x2!", 5872 paused: "EN PAUSE", 5873 resume: "REPRENDRE", 5874 restart: "RECOMMENCER", 5875 backToArcade: "RETOURNER à LâARCADE", 5876 resultsTitle: "CONCERT TERMINà !", 5877 stageClear: "UNE HARMONIE MIAOU-VEILLEUSE !", 5878 finalScore: "SCORE FINAL", 5879 maxCombo: "COMBO MAX", 5880 accuracy: "PRÃCISION", 5881 playAgain: "RECHANTER", 5882 difficulty: "DIFFICULTÃ", 5883 kitten: "CHATON", 5884 playful: "JOUEUR", 5885 wildcat: "SAUVAGE", 5886 meowstro: "MAESTRO" 5887 }, 5888 de: { 5889 gameTitle: "Miau-Chor", 5890 gameSubtitle: "Kätzchen-Bühnenchor Rhythmusspiel", 5891 tapToPlay: "ZUM STARTEN TIPPEN",
5892 tapToPlaySub: "Leite den singenden Kätzchenchor am Mikrofon!", 5893 keyboardHint: "Tasten [D] [F] [J] [K] oder Pfeiltasten", 5894 touchHint: "Tippe auf die Klavier-Ringe oder violette Notenblasen", 5895 score: "PUNKTE", 5896 combo: "KOMBO", 5897 purrfect: "SCHNURR-FEKT!", 5898 pawsome: "PFOTASTISCH!", 5899 good: "GUT", 5900 miss: "VERFEHLT", 5901 fever: "KATZENMINZE-FIEBER", 5902 feverActive: "FIEBER x2!", 5903 paused: "PAUSIERT", 5904 resume: "WEITER", 5905 restart: "NEUSTART", 5906 backToArcade: "ZURÃCK ZUR ARCADE", 5907 resultsTitle: "BÃHNE GEMEISTERT!", 5908 stageClear: "EIN SCHNURRIG WUNDERBARES KONZERT!", 5909 finalScore: "ENDPUNKTE", 5910 maxCombo: "MAX KOMBO", 5911 accuracy: "GENAUIGKEIT", 5912 playAgain: "NOCHMAL SINGEN", 5913 difficulty: "SCHWIERIGKEIT", 5914 kitten: "KÃTZCHEN", 5915 playful: "VERSPIELT", 5916 wildcat: "WILDKATZE", 5917 meowstro: "MIAUSTRO" 5918 }, 5919 it: { 5920 gameTitle: "Coro Miao", 5921 gameSubtitle: "Coro di gattini che cantano al microfono", 5922 tapToPlay: "TOCCA PER GIOCARE", 5923 tapToPlaySub: "Guida il coro dei gattini sul palco!", 5924 keyboardHint: "Tasti [D] [F] [J] [K] o Frecce", 5925 touchHint: "Tocca i cerchi del pianoforte o le bolle viola", 5926 score: "PUNTEGGIO", 5927 combo: "COMBO", 5928 purrfect: "FUSA-TICO!", 5929 pawsome: "MIAO-VIGLIOSO!", 5930 good: "BENE", 5931 miss: "MANCATO", 5932 fever: "FEBBRE DI ERBA GATTA", 5933 feverActive: "FEBBRE x2!", 5934 paused: "IN PAUSA", 5935 resume: "RIPRENDI", 5936 restart: "RICOMINCIA", 5937 backToArcade: "TORNA ALLâARCADE", 5938 resultsTitle: "SPETTACOLO COMPLETATO!", 5939 stageClear: "UNâESIBIZIONE MIAO-RAVIGLIOSA!", 5940 finalScore: "PUNTEGGIO FINALE", 5941 maxCombo: "COMBO MASSIMA", 5942 accuracy: "PRECISIONE", 5943 playAgain: "CANTA ANCORA", 5944 difficulty: "DIFFICOLTÃ", 5945 kitten: "GATTINO", 5946 playful: "GIOCHERELLONE", 5947 wildcat: "SELVAGGIO", 5948 meowstro: "MEOWSTRO" 5949 }, 5950 pt: { 5951 gameTitle: "Coro Miau", 5952 gameSubtitle: "Aventura rÃtmica de gatinhos cantando ao microfone", 5953 tapToPlay: "TOQUE PARA JOGAR", 5954 tapToPlaySub: "Comande o coral de gatinhos em volta do microfone!", 5955 keyboardHint: "Teclas [D] [F] [J] [K] ou Setas", 5956 touchHint: "Toque nos anéis do piano ou nas bolhas roxas", 5957 score: "PONTOS", 5958 combo: "COMBO", 5959 purrfect: "RONROM-FEITO!", 5960 pawsome: "MIAU-RAVILHOSO!", 5961 good: "BOM", 5962 miss: "ERROU", 5963 fever: "FEBRE DE ERVA-DOS-GATOS", 5964 feverActive: "FEBRE x2!", 5965 paused: "PAUSADO", 5966 resume: "CONTINUAR", 5967 restart: "REINICIAR", 5968 backToArcade: "VOLTAR AO ARCADE", 5969 resultsTitle: "SHOW CONCLUÃDO!", 5970 stageClear: "UMA APRESENTAÃÃO MIAU-RÃSSIMA!", 5971 finalScore: "PONTUAÃÃO FINAL", 5972 maxCombo: "COMBO MÃXIMO", 5973 accuracy: "PRECISÃO", 5974 playAgain: "CANTAR DE NOVO", 5975 difficulty: "DIFICULDADE", 5976 kitten: "GATINHO", 5977 playful: "BRINCALHÃO", 5978 wildcat: "SELVAGEM", 5979 meowstro: "MAESTRO" 5980 }, 5981 ru: { 5982 gameTitle: "ÐÑÑ-ХоÑ", 5983 gameSubtitle: "РиÑм-Ñ Ð¾Ñ ÐºÐ¾ÑÑÑ, поÑÑÐ¸Ñ Ñ Ð¼Ð¸ÐºÑоÑона", 5984 tapToPlay: "ÐÐÐÐÐТРÐÐЯ СТÐРТÐ", 5985 tapToPlaySub: "УпÑавлÑйÑе Ñ Ð¾Ñом коÑÑÑ Ð²Ð¾ÐºÑÑг микÑоÑона!", 5986 keyboardHint: "ÐлавиÑи [D] [F] [J] [K] или СÑÑелки", 5987 touchHint: "ÐажимайÑе на колÑÑа пианино или ÑиолеÑовÑе ноÑÑ", 5988 score: "СЧÐТ", 5989 combo: "ÐÐÐÐÐ", 5990 purrfect: "ÐУРР-ФÐÐТ!", 5991 pawsome: "ÐЯУ-ÐÐÐÐÐÐÐÐÐÐÐ!", 5992 good: "Ð¥ÐÐ ÐШÐ", 5993 miss: "ÐÐÐÐ", 5994 fever: "ÐÐШÐЧЬЯ ÐЯТÐ", 5995 feverActive: "ФÐÐÐÐ x2!", 5996 paused: "ÐÐУÐÐ", 5997 resume: "ÐÐ ÐÐÐÐÐÐТЬ", 5998 restart: "ÐÐÐÐÐÐ", 5999 backToArcade: "Ð ÐÐÐ ÐÐÐÐ ÐÐÐ", 6000 resultsTitle: "ÐÐÐЦÐРТ ÐÐÐÐЧÐÐ!", 6001 stageClear: "ÐÐТРЯСÐЮЩÐÐ ÐЯУ-ÐЫСТУÐÐÐÐÐÐ!", 6002 finalScore: "ÐТÐÐÐÐЫРСЧÐТ", 6003 maxCombo: "ÐÐÐС. ÐÐÐÐÐ", 6004 accuracy: "ТÐЧÐÐСТЬ", 6005 playAgain: "СÐÐТЬ ÐЩРРÐÐ", 6006 difficulty: "СÐÐÐÐÐСТЬ", 6007 kitten: "ÐÐТÐÐÐÐ", 6008 playful: "ÐÐÐ ÐÐЫÐ", 6009 wildcat: "ÐÐÐÐÐ", 6010 meowstro: "ÐÐÐСТРÐ" 6011 }, 6012 id: { 6013 gameTitle: "Paduan Suara Kucing", 6014 gameSubtitle: "Petualangan ritme paduan suara anak kucing di sekitar mikrofon", 6015 tapToPlay: "KETUK UNTUK MAIN", 6016 tapToPlaySub: "Pimpin paduan suara anak kucing bernyanyi di panggung!", 6017 keyboardHint: "Tombol [D] [F] [J] [K] atau Tombol Panah", 6018 touchHint: "Ketuk cincin piano atau gelembung nada ungu", 6019 score: "SKOR", 6020 combo: "KOMBO", 6021 purrfect: "PURRFECT!", 6022 pawsome: "HEBAT!", 6023 good: "BAGUS", 6024 miss: "MELESET", 6025 fever: "DEMAM CATNIP", 6026 feverActive: "FEVER x2!", 6027 paused: "DIJEDA", 6028 resume: "LANJUTKAN", 6029 restart: "ULANGI", 6030 backToArcade: "KEMBALI KE ARCADE", 6031 resultsTitle: "PANGGUNG SELESAI!", 6032 stageClear: "PENAMPILAN KUCING YANG LUAR BIASA!", 6033 finalScore: "SKOR AKHIR", 6034 maxCombo: "KOMBO MAKSIMAL", 6035 accuracy: "AKURASI", 6036 playAgain: "NYANYI LAGI", 6037 difficulty: "TINGKAT KESULITAN", 6038 kitten: "ANAK KUCING", 6039 playful: "LINCAH", 6040 wildcat: "LIAR", 6041 meowstro: "MEOWSTRO" 6042 }, 6043 ar: { 6044 gameTitle: "جÙÙØ© اÙÙ ÙØ§Ø¡", 6045 gameSubtitle: "٠غا٠رة Ø¥ÙÙØ§Ø¹ÙØ© ÙÙØ±ÙØ© ÙØ·Ø· تغÙÙ ØÙ٠اÙÙ ÙÙØ±ÙÙÙÙ", 6046 tapToPlay: "اÙ٠س ÙÙØ¨Ø¯Ø¡", 6047 tapToPlaySub: "ÙØ¯ جÙÙØ© اÙÙØ·Ø· اÙÙØ·ÙÙØ© ØÙ٠اÙÙ ÙÙØ±ÙÙÙÙ!", 6048 keyboardHint: "اÙÙ ÙØ§ØªÙØ [D] [F] [J] [K] Ø£Ù Ø§ÙØ£Ø³ÙÙ ", 6049 touchHint: "اÙ٠س ØÙÙØ§Øª Ø§ÙØ¨ÙاÙ٠أ٠ÙÙØ§Ø¹Ø§Øª اÙÙÙØªØ§Øª Ø§ÙØ¨ÙÙØ³Ø¬ÙØ©", 6050 score: "اÙÙÙØ§Ø·", 6051 combo: "ÙÙ٠بÙ", 6052 purrfect: "Ù ÙØ§Ø¡ ٠ثاÙÙ!", 6053 pawsome: "رائع جداÙ!", 6054 good: "Ø¬ÙØ¯", 6055 miss: "ÙØ§Ø¦Øª", 6056 fever: "ØÙ اس اÙÙØ¹Ùاع Ø§ÙØ¨Ø±Ù", 6057 feverActive: "٠ضاع٠x2!", 6058 paused: "Ù Ø¤ÙØª", 6059 resume: "Ø§Ø³ØªØ¦ÙØ§Ù", 6060 restart: "إعادة", 6061 backToArcade: "Ø®Ø±ÙØ¬ Ø¥ÙÙ Ø§ÙØµØ§ÙØ©", 6062 resultsTitle: "ØªÙ Ø¥ØªÙ Ø§Ù Ø§ÙØ¹Ø±Ø¶!", 6063 stageClear: "أداء Ù ÙØ§Ø¡ ٠ذÙÙ ÙØ±Ø§Ø¦Ø¹!", 6064 finalScore: "اÙÙØªÙجة اÙÙÙØ§Ø¦ÙØ©", 6065 maxCombo: "أعÙÙ ÙÙ٠بÙ", 6066 accuracy: "Ø§ÙØ¯ÙØ©", 6067 playAgain: "ØºÙØ§Ø¡ ٠رة أخرÙ", 6068 difficulty: "اÙ٠ستÙÙ",
6069 kitten: "ÙØ· ØµØºÙØ±", 6070 playful: "٠رØ", 6071 wildcat: "برÙ", 6072 meowstro: "Ù Ø§ÙØ³ØªØ±Ù" 6073 } 6074}; 6075function getMeowChoirLocale(language = "en") { 6076 if (MEOW_CHOIR_LOCALES[language]) return MEOW_CHOIR_LOCALES[language]; 6077 const prefix = language.split("-")[0]; 6078 if (MEOW_CHOIR_LOCALES[prefix]) return MEOW_CHOIR_LOCALES[prefix]; 6079 return MEOW_CHOIR_LOCALES.en; 6080} 6081const MeowChoirGame = ({ 6082 data, 6083 melodyData, 6084 processedInstruments, 6085 midiMetadata, 6086 currentTime, 6087 isPlaying, 6088 duration, 6089 tempo = 120, 6090 timeSignature = [4, 4], 6091 trackId, 6092 userId, 6093 userName, 6094 onPlayPause, 6095 onSeek, 6096 isAudioLoaded = true, 6097 isLoadingAudio = false, 6098 difficulty: initialDifficulty = "playful", 6099 restartKey 6100}) => { 6101 const { i18n } = useTranslation(); 6102 const locale = reactExports.useMemo(() => getMeowChoirLocale(i18n.language), [i18n.language]); 6103 const isMobile = reactExports.useMemo(() => typeof window !== "undefined" && (isMobileDevice() || window.matchMedia && window.matchMedia("(pointer: coarse)").matches && !window.matchMedia("(pointer: fine)").matches), []); 6104 const [hasStarted, setHasStarted] = reactExports.useState(false); 6105 const [countdown, setCountdown] = reactExports.useState(null); 6106 const [difficulty, setDifficulty] = reactExports.useState(initialDifficulty); 6107 const [score, setScore] = reactExports.useState(0); 6108 const [combo, setCombo] = reactExports.useState(0); 6109 const [maxCombo, setMaxCombo] = reactExports.useState(0); 6110 const [isSinging, setIsSinging] = reactExports.useState(false); 6111 const [isMissed, setIsMissed] = reactExports.useState(false); 6112 const [activeLaneHit, setActiveLaneHit] = reactExports.useState(null); 6113 const [isFever, setIsFever] = reactExports.useState(false); 6114 const [lanePressStates, setLanePressStates] = reactExports.useState([false, false, false, false]); 6115 const [isResultsOpen, setIsResultsOpen] = reactExports.useState(false); 6116 const [gameResult, setGameResult] = reactExports.useState(null); 6117 const purrfectRef = reactExports.useRef(0); 6118 const pawsomeRef = reactExports.useRef(0); 6119 const goodRef = reactExports.useRef(0); 6120 const missRef = reactExports.useRef(0); 6121 const singingTimeoutRef = reactExports.useRef(null); 6122 const countdownTimerRef = reactExports.useRef(null); 6123 const prevCurrentTimeRef = reactExports.useRef(currentTime); 6124 const chart = reactExports.useMemo(() => { 6125 return generateMeowChoirChart( 6126 data, 6127 melodyData, 6128 processedInstruments, 6129 midiMetadata, 6130 difficulty, 6131 duration, 6132 trackId 6133 ); 6134 }, [data, melodyData, difficulty, duration, trackId]); 6135 reactExports.useEffect(() => { 6136 return () => { 6137 if (countdownTimerRef.current) { 6138 clearInterval(countdownTimerRef.current); 6139 } 6140 if (singingTimeoutRef.current) { 6141 clearTimeout(singingTimeoutRef.current); 6142 } 6143 }; 6144 }, []); 6145 const startCountdownSequence = reactExports.useCallback(() => { 6146 if (countdownTimerRef.current) { 6147 clearInterval(countdownTimerRef.current); 6148 countdownTimerRef.current = null; 6149 } 6150 meowChoirAudio.initContext(); 6151 setHasStarted(true); 6152 if (onSeek) onSeek(0); 6153 if (onPlayPause) onPlayPause(false); 6154 let count = 3; 6155 setCountdown(3); 6156 meowChoirAudio.playCountdownTick(3); 6157 countdownTimerRef.current = setInterval(() => { 6158 count--; 6159 if (count > 0) { 6160 setCountdown(count); 6161 meowChoirAudio.playCountdownTick(count); 6162 } else if (count === 0) { 6163 setCountdown(0); 6164 meowChoirAudio.playCountdownTick(0); 6165 } else { 6166 clearInterval(countdownTimerRef.current); 6167 countdownTimerRef.current = null; 6168 setCountdown(null); 6169 if (onSeek) onSeek(0); 6170 if (onPlayPause) onPlayPause(true); 6171 } 6172 }, 700); 6173 }, [onSeek, onPlayPause]); 6174 const handleReset = reactExports.useCallback(() => { 6175 setScore(0); 6176 setCombo(0); 6177 setMaxCombo(0); 6178 setIsSinging(false); 6179 setIsMissed(false); 6180 setIsFever(false); 6181 setIsResultsOpen(false); 6182 purrfectRef.current = 0; 6183 pawsomeRef.current = 0; 6184 goodRef.current = 0; 6185 missRef.current = 0; 6186 if (onSeek) onSeek(0); 6187 if (onPlayPause) onPlayPause(false); 6188 startCountdownSequence(); 6189 }, [onSeek, onPlayPause, startCountdownSequence]); 6190 const handleInitialStart = reactExports.useCallback(() => { 6191 startCountdownSequence(); 6192 }, [startCountdownSequence]); 6193 reactExports.useEffect(() => { 6194 if (restartKey !== void 0 && restartKey > 0) { 6195 handleReset(); 6196 } 6197 }, [restartKey, handleReset]); 6198 reactExports.useEffect(() => { 6199 if (isPlaying && !hasStarted && countdown === null) { 6200 startCountdownSequence(); 6201 } 6202 }, [isPlaying, hasStarted, countdown, startCountdownSequence]); 6203 reactExports.useEffect(() => { 6204 if (hasStarted && prevCurrentTimeRef.current > 1 && currentTime < 0.3 && isPlaying) { 6205 handleReset(); 6206 } 6207 prevCurrentTimeRef.current = currentTime; 6208 }, [currentTime, hasStarted, isPlaying, handleReset]); 6209 const handleNoteHit = reactExports.useCallback((lane, type, note) => {
6210 setLanePressStates((prev) => { 6211 const next = [...prev]; 6212 next[lane] = true; 6213 return next; 6214 }); 6215 setTimeout(() => { 6216 setLanePressStates((prev) => { 6217 const next = [...prev]; 6218 next[lane] = false; 6219 return next; 6220 }); 6221 }, 110); 6222 let pts = 300; 6223 if (type === "PURRFECT") { 6224 pts = 1e3; 6225 purrfectRef.current++; 6226 } else if (type === "PAWSOME") { 6227 pts = 650; 6228 pawsomeRef.current++; 6229 } else { 6230 goodRef.current++; 6231 } 6232 if (note.type === "star") { 6233 pts = Math.round(pts * 1.5); 6234 } 6235 const multiplier = isFever ? 2 : 1; 6236 setScore((s) => s + pts * multiplier); 6237 setCombo((c) => { 6238 const nextC = c + 1; 6239 setMaxCombo((m) => Math.max(m, nextC)); 6240 if (nextC % 20 === 0 && nextC > 0) { 6241 meowChoirAudio.playCheerNya(); 6242 } 6243 return nextC; 6244 }); 6245 setActiveLaneHit(lane); 6246 setIsSinging(true); 6247 if (singingTimeoutRef.current) clearTimeout(singingTimeoutRef.current); 6248 singingTimeoutRef.current = setTimeout(() => { 6249 setIsSinging(false); 6250 setActiveLaneHit(null); 6251 }, 220); 6252 meowChoirAudio.playBubblePop(note.type === "star" ? 1.25 : 1); 6253 meowChoirAudio.playMeow(note.pitch, note.duration, note.type === "star"); 6254 }, [isFever]); 6255 const handleNoteMiss = reactExports.useCallback((lane, note) => { 6256 missRef.current++; 6257 setCombo(0); 6258 }, []); 6259 reactExports.useEffect(() => { 6260 if (currentTime >= duration && duration > 5 && !isResultsOpen && hasStarted && countdown === null) { 6261 const total = chart.notes.length || 1; 6262 const accuracy = Math.min(100, Math.max(0, (purrfectRef.current * 1 + pawsomeRef.current * 0.7 + goodRef.current * 0.4) / total * 100)); 6263 setGameResult({ 6264 score, 6265 maxCombo, 6266 purrfectCount: purrfectRef.current, 6267 pawsomeCount: pawsomeRef.current, 6268 goodCount: goodRef.current, 6269 missCount: missRef.current, 6270 totalNotes: total, 6271 accuracy 6272 }); 6273 setIsResultsOpen(true); 6274 meowChoirAudio.playCheerNya(); 6275 if (onPlayPause) onPlayPause(false); 6276 } 6277 }, [currentTime, duration, isResultsOpen, hasStarted, countdown, chart.notes.length, score, maxCombo, onPlayPause]); 6278 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-full flex flex-col items-center justify-center bg-[#070b1a] text-white relative select-none font-sans overflow-hidden", children: [ 6279 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md h-full flex flex-col justify-between shadow-[0_0_50px_rgba(0,0,0,0.8)] border-x sm:border-2 border-slate-800 rounded-none sm:rounded-3xl overflow-hidden bg-slate-950", children: [
6280 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[48%] relative overflow-hidden border-b-4 border-slate-900 shadow-inner", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6281 MeowChoirStage, 6282 { 6283 score, 6284 combo, 6285 activeLaneHit, 6286 isSinging, 6287 isMissed, 6288 isFever 6289 } 6290 ) }), 6291 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-4 bg-gradient-to-b from-slate-900 via-slate-800 to-slate-950 flex items-center justify-between px-6 shrink-0 shadow-md border-y border-slate-800", children: [ 6292 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [ 6293 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }), 6294 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }), 6295 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }) 6296 ] }),
6297 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-black uppercase tracking-widest text-slate-400", children: [ 6298 "MEOW CHOIR ⢠", 6299 difficulty.toUpperCase() 6300 ] }), 6301 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1", children: [ 6302 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }), 6303 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }), 6304 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-slate-950 border border-slate-700" }) 6305 ] }) 6306 ] }), 6307 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[50%] relative overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6308 MeowChoirTouchScreen, 6309 { 6310 notes: chart.notes, 6311 currentTime, 6312 isPlaying: isPlaying && hasStarted && countdown === null, 6313 speed: chart.meta.density, 6314 approachTime: chart.meta.approachTime, 6315 onNoteHit: handleNoteHit, 6316 onNoteMiss: handleNoteMiss, 6317 lanePressStates 6318 } 6319 ) }), 6320 countdown !== null && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-40 flex flex-col items-center justify-center bg-slate-950/65 backdrop-blur-[2px] animate-in fade-in duration-150 pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-center transform animate-in zoom-in-75 duration-200", children: countdown > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-24 h-24 rounded-full bg-gradient-to-tr from-purple-600 via-pink-500 to-amber-400 p-1 shadow-[0_0_40px_rgba(236,72,153,0.8)] flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full rounded-full bg-slate-950 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-5xl font-black text-transparent bg-clip-text bg-gradient-to-br from-amber-300 via-pink-400 to-purple-400 font-mono", children: countdown }) }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-8 py-3 rounded-full bg-gradient-to-r from-pink-500 via-purple-600 to-amber-400 shadow-[0_0_50px_rgba(236,72,153,0.9)] flex items-center gap-2", children: [
6321 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 24, className: "text-yellow-300 animate-spin" }), 6322 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl font-black text-white tracking-widest uppercase", children: "MEOW!" }), 6323 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 24, className: "text-yellow-300 animate-spin" }) 6324 ] }) }, countdown) }), 6325 !hasStarted && countdown === null && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-40 flex flex-col items-center justify-center p-4 pointer-events-none animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex flex-col items-center text-center max-w-sm px-6 py-4 rounded-3xl bg-slate-950/70 border-2 border-pink-500/50 shadow-[0_0_35px_rgba(236,72,153,0.4)] backdrop-blur-[2px] pointer-events-auto", children: [ 6326 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [
6327 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block w-2.5 h-2.5 rounded-full bg-pink-400 animate-ping" }), 6328 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-2xl sm:text-3xl font-black tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-pink-400 via-purple-300 to-amber-300", children: locale.gameTitle }) 6329 ] }), 6330 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-purple-200/90 mb-3", children: locale.tapToPlaySub }), 6331 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6332 "button", 6333 { 6334 type: "button", 6335 onClick: handleInitialStart, 6336 className: "w-full py-3 px-6 rounded-2xl bg-gradient-to-r from-pink-500 via-purple-600 to-indigo-600 hover:from-pink-400 hover:to-indigo-500 text-white font-black text-base tracking-wider uppercase shadow-[0_0_25px_rgba(236,72,153,0.6)] transform hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2 cursor-pointer", 6337 children: [
6338 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 20, fill: "currentColor" }), 6339 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6340 locale.tapToPlay, 6341 " ", 6342 !isMobile ? "[Space]" : "" 6343 ] }) 6344 ] 6345 } 6346 ), 6347 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 flex items-center justify-center gap-3 text-[10px] text-slate-300 font-mono", children: [ 6348 !isMobile && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6349 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6350 "â¨ï¸ ", 6351 locale.keyboardHint 6352 ] }), 6353 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }) 6354 ] }), 6355 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6356 "ð± ", 6357 locale.touchHint 6358 ] }) 6359 ] }) 6360 ] }) }) 6361 ] }), 6362 /* @__PURE__ */ jsxRuntimeExports.jsx( 6363 MeowChoirResultsScreen, 6364 { 6365 isOpen: isResultsOpen, 6366 result: gameResult, 6367 locale, 6368 songTitle: midiMetadata == null ? void 0 : midiMetadata.title, 6369 onPlayAgain: () => { 6370 handleReset(); 6371 }, 6372 onBackToArcade: () => { 6373 setIsResultsOpen(false); 6374 if (onPlayPause) onPlayPause(false); 6375 } 6376 } 6377 ) 6378 ] }); 6379}; 6380const RhythmTestGrid = ({ 6381 patterns, 6382 currentMeasureIndex, 6383 currentSubBeatOffset, 6384 isPlaying, 6385 isEditorMode = false, 6386 onMeasureClick, 6387 className = "", 6388 pageOffset = 0, 6389 inputMode = "keyboard" 6390}) => { 6391 const currentQuarterBeat = Math.max(0, Math.min(3, Math.floor(currentSubBeatOffset))); 6392 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `w-full h-full flex flex-col items-center justify-center max-w-full max-h-full select-none ${className}`, children: [ 6393 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center gap-2 mb-1.5 shrink-0", children: [0, 1, 2, 3].map((beat) => { 6394 const isCurrentBeat = isPlaying && currentQuarterBeat === beat; 6395 return /* @__PURE__ */ jsxRuntimeExports.jsx( 6396 "div", 6397 { 6398 className: `h-1.5 rounded-full transition-all duration-75 flex items-center justify-center ${isCurrentBeat ? "w-7 sm:w-9 bg-amber-400 shadow-[0_0_10px_rgba(251,191,36,0.9)] scale-105" : "w-3.5 sm:w-4 bg-zinc-300 dark:bg-zinc-700 opacity-50"}` 6399 }, 6400 beat 6401 ); 6402 }) }), 6403 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full flex-1 min-h-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 grid-rows-4 gap-1.5 sm:gap-2 md:gap-2.5 w-full h-full max-h-full aspect-square max-w-full", children: patterns.slice(0, 16).map((pattern, idx) => { 6404 const globalMeasureIdx = pageOffset + idx; 6405 const isPast = isPlaying && globalMeasureIdx < currentMeasureIndex; 6406 const isActive = isPlaying && globalMeasureIdx === currentMeasureIndex; 6407 const isUpcoming = !isPlaying || globalMeasureIdx > currentMeasureIndex; 6408 return /* @__PURE__ */ jsxRuntimeExports.jsx( 6409 MeasureBox, 6410 { 6411 index: idx, 6412 globalMeasureIndex: globalMeasureIdx, 6413 pattern, 6414 isPast, 6415 isActive, 6416 isUpcoming, 6417 currentSubBeatOffset: isActive ? currentSubBeatOffset : -1, 6418 isEditorMode, 6419 inputMode, 6420 onClick: () => onMeasureClick == null ? void 0 : onMeasureClick(globalMeasureIdx) 6421 }, 6422 globalMeasureIdx 6423 ); 6424 }) }) }) 6425 ] }); 6426}; 6427const MeasureBox = ({ 6428 index, 6429 globalMeasureIndex, 6430 pattern, 6431 isPast, 6432 isActive, 6433 isUpcoming, 6434 currentSubBeatOffset, 6435 isEditorMode, 6436 inputMode, 6437 onClick 6438}) => { 6439 let boxClasses = "relative w-full h-full rounded-lg sm:rounded-xl md:rounded-2xl transition-all duration-150 flex items-center justify-center overflow-hidden "; 6440 if (isActive) { 6441 boxClasses += "bg-gradient-to-br from-purple-600 via-purple-700 to-indigo-800 shadow-[0_0_20px_rgba(168,85,247,0.7)] scale-[1.02] z-10 border-2 border-purple-300 ring-2 ring-purple-400/50"; 6442 } else if (isPast) { 6443 boxClasses += "bg-[#e05244] shadow-md border border-red-400/30"; 6444 } else { 6445 boxClasses += "bg-zinc-600/90 dark:bg-zinc-700/80 shadow-sm border border-zinc-500/20"; 6446 } 6447 if (isEditorMode) { 6448 boxClasses += " cursor-pointer hover:opacity-85 hover:scale-[1.02] active:scale-95"; 6449 } 6450 let nextTargetBeat = -1; 6451 if (isActive && pattern.beatOffsets && pattern.beatOffsets.length > 0) { 6452 for (const bOff of pattern.beatOffsets) { 6453 if (currentSubBeatOffset <= bOff + 0.35) { 6454 nextTargetBeat = bOff; 6455 break; 6456 } 6457 } 6458 } 6459 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6460 "div", 6461 { 6462 onClick, 6463 className: boxClasses, 6464 title: `Measure ${globalMeasureIndex + 1} (${pattern.dots} beat${pattern.dots === 1 ? "" : "s"})`, 6465 children: [
6466 isPast && /* @__PURE__ */ jsxRuntimeExports.jsx( 6467 "div", 6468 { 6469 className: "absolute inset-0 pointer-events-none opacity-25", 6470 style: { 6471 backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(0,0,0,0.3) 5px, rgba(0,0,0,0.3) 10px)` 6472 } 6473 } 6474 ), 6475 isActive && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 pointer-events-none bg-purple-400/15 animate-pulse" }), 6476 isActive && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-1 left-1.5 z-20 pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-1 py-0.2 rounded text-[7px] sm:text-[8px] font-mono font-black uppercase tracking-wider bg-black/60 text-amber-300 border border-amber-400/50 shadow-sm animate-pulse", children: inputMode === "keyboard" ? "SPACE / F,J" : inputMode === "touch" ? "TAP" : "CLAP" }) }), 6477 /* @__PURE__ */ jsxRuntimeExports.jsx( 6478 DotLayout, 6479 { 6480 dots: pattern.dots, 6481 isPast, 6482 isActive, 6483 currentSubBeatOffset, 6484 nextTargetBeat 6485 } 6486 ),
6487 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute bottom-0.5 right-1 sm:bottom-1 sm:right-1.5 text-[8px] sm:text-[9px] font-mono font-bold text-white/50 pointer-events-none", children: globalMeasureIndex + 1 }) 6488 ] 6489 } 6490 ); 6491}; 6492const DotLayout = ({ 6493 dots, 6494 isPast, 6495 isActive, 6496 currentSubBeatOffset, 6497 nextTargetBeat 6498}) => { 6499 if (dots === 0) { 6500 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-zinc-400/50 text-[10px] sm:text-xs font-mono font-black tracking-widest uppercase", children: "REST" }); 6501 } 6502 const renderDot = (beatIdx, beatOffset) => { 6503 const isHit = isActive && currentSubBeatOffset >= beatOffset - 0.05; 6504 const isTarget = isActive && nextTargetBeat === beatOffset; 6505 let dotClasses = "w-2.5 h-2.5 sm:w-3.5 sm:h-3.5 rounded-full relative transition-all duration-75 "; 6506 if (isActive) { 6507 if (isHit) { 6508 dotClasses += "bg-white shadow-[0_0_12px_rgba(255,255,255,1)] scale-110"; 6509 } else if (isTarget) { 6510 dotClasses += "bg-amber-300 shadow-[0_0_8px_rgba(251,191,36,0.9)] scale-105"; 6511 } else { 6512 dotClasses += "bg-purple-950/70 border border-purple-400/40"; 6513 } 6514 } else if (isPast) { 6515 dotClasses += "bg-white shadow-[0_0_4px_rgba(255,255,255,0.4)]"; 6516 } else { 6517 dotClasses += "bg-zinc-800/85 dark:bg-zinc-900/90 shadow-inner"; 6518 } 6519 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-center", children: [ 6520 isTarget && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -inset-1.5 sm:-inset-2 rounded-full border-2 border-amber-300/80 animate-ping pointer-events-none" }), 6521 isTarget && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -inset-1 rounded-full border border-white/60 animate-pulse pointer-events-none" }),
6522 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: dotClasses }) 6523 ] }, beatIdx); 6524 }; 6525 if (dots === 1) { 6526 return renderDot(0, 0); 6527 } 6528 if (dots === 2) { 6529 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-2.5 sm:gap-4 md:gap-5 w-full", children: [ 6530 renderDot(0, 0), 6531 renderDot(1, 2) 6532 ] }); 6533 } 6534 if (dots === 3) { 6535 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-1.5 sm:gap-2.5", children: [ 6536 renderDot(0, 0), 6537 renderDot(1, 1), 6538 renderDot(2, 2) 6539 ] }); 6540 } 6541 if (dots === 4) { 6542 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 grid-rows-2 gap-x-2.5 gap-y-2.5 sm:gap-x-3.5 sm:gap-y-3.5 items-center justify-center", children: [ 6543 renderDot(0, 0), 6544 renderDot(1, 1), 6545 renderDot(2, 2), 6546 renderDot(3, 3) 6547 ] }); 6548 } 6549 if (dots === 8) { 6550 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 grid-rows-2 gap-1 sm:gap-1.5 items-center justify-center", children: [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5].map((off, bIdx) => renderDot(bIdx, off)) }); 6551 } 6552 return null; 6553}; 6554const RhythmBongoCat = ({ 6555 leftPawDown, 6556 rightPawDown, 6557 mood = "idle", 6558 combo = 0, 6559 className = "", 6560 onCatClick 6561}) => { 6562 const [leftImpact, setLeftImpact] = reactExports.useState(false); 6563 const [rightImpact, setRightImpact] = reactExports.useState(false); 6564 reactExports.useEffect(() => { 6565 if (leftPawDown) { 6566 setLeftImpact(true); 6567 const timer = setTimeout(() => setLeftImpact(false), 120); 6568 return () => clearTimeout(timer); 6569 } 6570 }, [leftPawDown]); 6571 reactExports.useEffect(() => { 6572 if (rightPawDown) { 6573 setRightImpact(true); 6574 const timer = setTimeout(() => setRightImpact(false), 120); 6575 return () => clearTimeout(timer); 6576 } 6577 }, [rightPawDown]); 6578 return /* @__PURE__ */ jsxRuntimeExports.jsx( 6579 "div", 6580 { 6581 onClick: onCatClick, 6582 className: `relative select-none pointer-events-auto cursor-pointer group ${className}`, 6583 title: "Bongo Cat! Tap or click to jam!", 6584 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 6585 "svg", 6586 {
6587 viewBox: "0 0 240 160", 6588 className: "w-full h-full overflow-visible", 6589 style: { filter: "drop-shadow(0 2px 8px rgba(0,0,0,0.15))" }, 6590 children: [ 6591 leftImpact && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "animate-in fade-in zoom-in duration-75", children: [ 6592 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "72", y1: "124", x2: "60", y2: "132", stroke: "#ea580c", strokeWidth: "2.5", strokeLinecap: "round" }), 6593 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "82", y1: "128", x2: "80", y2: "140", stroke: "#f59e0b", strokeWidth: "2.5", strokeLinecap: "round" }), 6594 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "66", y1: "120", x2: "54", y2: "120", stroke: "#ea580c", strokeWidth: "2", strokeLinecap: "round" }), 6595 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "75", cy: "125", r: "3", fill: "#fbbf24" }) 6596 ] }), 6597 rightImpact && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "animate-in fade-in zoom-in duration-75", children: [
6598 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "152", y1: "138", x2: "162", y2: "148", stroke: "#8b5cf6", strokeWidth: "2.5", strokeLinecap: "round" }), 6599 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "140", y1: "142", x2: "138", y2: "154", stroke: "#ec4899", strokeWidth: "2.5", strokeLinecap: "round" }), 6600 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "158", y1: "132", x2: "170", y2: "134", stroke: "#8b5cf6", strokeWidth: "2", strokeLinecap: "round" }), 6601 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "148", cy: "138", r: "3", fill: "#c084fc" }) 6602 ] }), 6603 /* @__PURE__ */ jsxRuntimeExports.jsx( 6604 "line", 6605 { 6606 x1: "20", 6607 y1: "110", 6608 x2: "225", 6609 y2: "150", 6610 stroke: "currentColor", 6611 strokeWidth: "3.2", 6612 strokeLinecap: "round", 6613 className: "text-zinc-800 dark:text-zinc-200" 6614 } 6615 ),
6616 /* @__PURE__ */ jsxRuntimeExports.jsx( 6617 "path", 6618 { 6619 d: "M 55,116 C 55,60 70,30 115,30 C 160,30 178,60 178,142", 6620 fill: "#ffffff", 6621 stroke: "currentColor", 6622 strokeWidth: "3.2", 6623 strokeLinecap: "round", 6624 strokeLinejoin: "round", 6625 className: "text-zinc-800 dark:text-zinc-200" 6626 } 6627 ), 6628 /* @__PURE__ */ jsxRuntimeExports.jsx( 6629 "path", 6630 { 6631 d: "M 66,54 L 56,22 C 58,20 64,18 78,28 L 88,40", 6632 fill: "#ffffff", 6633 stroke: "currentColor", 6634 strokeWidth: "3.2", 6635 strokeLinecap: "round", 6636 strokeLinejoin: "round", 6637 className: "text-zinc-800 dark:text-zinc-200" 6638 } 6639 ), 6640 /* @__PURE__ */ jsxRuntimeExports.jsx( 6641 "path", 6642 { 6643 d: "M 64,30 L 74,34", 6644 stroke: "currentColor", 6645 strokeWidth: "2", 6646 strokeLinecap: "round", 6647 className: "text-pink-400" 6648 } 6649 ),
6650 /* @__PURE__ */ jsxRuntimeExports.jsx( 6651 "path", 6652 { 6653 d: "M 136,36 L 152,18 C 158,16 166,20 166,24 L 160,54", 6654 fill: "#ffffff", 6655 stroke: "currentColor", 6656 strokeWidth: "3.2", 6657 strokeLinecap: "round", 6658 strokeLinejoin: "round", 6659 className: "text-zinc-800 dark:text-zinc-200" 6660 } 6661 ), 6662 /* @__PURE__ */ jsxRuntimeExports.jsx( 6663 "path", 6664 { 6665 d: "M 152,28 L 158,36", 6666 stroke: "currentColor", 6667 strokeWidth: "2", 6668 strokeLinecap: "round", 6669 className: "text-pink-400" 6670 } 6671 ), 6672 mood === "idle" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "text-zinc-800 dark:text-zinc-200", children: [ 6673 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "86", cy: "62", r: "3.2", fill: "currentColor" }), 6674 /* @__P
6674URE__ */ jsxRuntimeExports.jsx("circle", { cx: "128", cy: "66", r: "3.2", fill: "currentColor" }), 6675 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 102,68 Q 106,71 110,68", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round" }) 6676 ] }), 6677 mood === "happy" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "text-zinc-800 dark:text-zinc-200", children: [ 6678 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 81,63 Q 86,57 91,63", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round" }), 6679 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 123,67 Q 128,61 133,67", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round" }), 6680 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 101,69 Q 107,76 113,69", fill: "#f43f5e", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round" }), 6681 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "76", cy: "68", r: "4", fill: "#fda4af", opacity: "0.7" }),
6682 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "138", cy: "72", r: "4", fill: "#fda4af", opacity: "0.7" }) 6683 ] }), 6684 mood === "focused" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "text-zinc-800 dark:text-zinc-200", children: [ 6685 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "80", y1: "58", x2: "92", y2: "64", stroke: "currentColor", strokeWidth: "2.8", strokeLinecap: "round" }), 6686 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "134", y1: "62", x2: "122", y2: "68", stroke: "currentColor", strokeWidth: "2.8", strokeLinecap: "round" }), 6687 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "86", cy: "64", r: "3", fill: "currentColor" }), 6688 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "127", cy: "68", r: "3", fill: "currentColor" }), 6689 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 102,72 L 110,72", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" }) 6690 ] }), 6691 mood === "miss" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "text-zinc-800 dark:text-zinc-200", children: [
6692 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 83,62 Q 86,58 89,62 T 86,66 T 83,62", fill: "none", stroke: "currentColor", strokeWidth: "2.4" }), 6693 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 125,66 Q 128,62 131,66 T 128,70 T 125,66", fill: "none", stroke: "currentColor", strokeWidth: "2.4" }), 6694 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 102,74 Q 106,71 110,74", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round" }), 6695 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 148,46 Q 152,54 148,58 Q 144,58 144,54 Z", fill: "#38bdf8" }) 6696 ] }), 6697 mood === "celebrate" && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 6698 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 81,63 Q 86,57 91,63", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", className: "text-zinc-800 dark:text-zinc-200" }), 6699 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 123,67 Q 128,61 133,67", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", className: "text-zinc-800 dark:text-zinc-200" }),
6700 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 101,69 Q 107,76 113,69", fill: "#f43f5e", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", className: "text-zinc-800 dark:text-zinc-200" }), 6701 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "106,10 90,36 122,34", fill: "#f59e0b", stroke: "#d97706", strokeWidth: "2" }), 6702 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "106", cy: "10", r: "4.5", fill: "#ef4444" }), 6703 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "93", y1: "28", x2: "119", y2: "26", stroke: "#3b82f6", strokeWidth: "2.5" }) 6704 ] }), 6705 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "transition-all duration-75", children: leftPawDown ? ( 6706 // Left Paw Pressed Down on Desk 6707 /* @__PURE__ */ jsxRuntimeExports.jsx( 6708 "path", 6709 { 6710 d: "M 64,88 C 64,106 66,126 78,124 C 88,122 84,100 80,88 Z", 6711 fill: "#ffffff", 6712 stroke: "currentColor", 6713 strokeWidth: "3.2", 6714 strokeLinecap: "round", 6715 strokeLinejoin: "round", 6716 className: "text-zinc-800 dark:text-zinc-200" 6717 } 6718 ) 6719 ) : ( 6720 // Left Paw Raised Resting
6721 /* @__PURE__ */ jsxRuntimeExports.jsx( 6722 "path", 6723 { 6724 d: "M 66,86 C 66,100 74,116 86,112 C 96,108 86,92 80,84 Z", 6725 fill: "#ffffff", 6726 stroke: "currentColor", 6727 strokeWidth: "3.2", 6728 strokeLinecap: "round", 6729 strokeLinejoin: "round", 6730 className: "text-zinc-800 dark:text-zinc-200" 6731 } 6732 ) 6733 ) }), 6734 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "transition-all duration-75", children: rightPawDown ? ( 6735 // Right Paw Pressed Down on Desk 6736 /* @__PURE__ */ jsxRuntimeExports.jsx( 6737 "path", 6738 { 6739 d: "M 132,94 C 132,114 138,142 150,138 C 160,134 150,110 144,92 Z", 6740 fill: "#ffffff", 6741 stroke: "currentColor", 6742 strokeWidth: "3.2", 6743 strokeLinecap: "round", 6744 strokeLinejoin: "round", 6745 className: "text-zinc-800 dark:text-zinc-200" 6746 } 6747 ) 6748 ) : ( 6749 // Right Paw Raised Resting
6750 /* @__PURE__ */ jsxRuntimeExports.jsx( 6751 "path", 6752 { 6753 d: "M 130,92 C 132,106 142,128 152,122 C 162,116 150,100 142,90 Z", 6754 fill: "#ffffff", 6755 stroke: "currentColor", 6756 strokeWidth: "3.2", 6757 strokeLinecap: "round", 6758 strokeLinejoin: "round", 6759 className: "text-zinc-800 dark:text-zinc-200" 6760 } 6761 ) 6762 ) }), 6763 combo >= 5 && /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "text-pink-500 animate-pulse", children: [ 6764 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "180", y: "45", fontSize: "18", fill: "currentColor", children: "âª" }), 6765 combo >= 15 && /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "195", y: "25", fontSize: "14", fill: "#8b5cf6", children: "â«" }) 6766 ] }) 6767 ] 6768 } 6769 ) 6770 } 6771 ); 6772}; 6773class RhythmAudioEngine { 6774 constructor() { 6775 this.ctx = null; 6776 this.masterGain = null; 6777 this.isMuted = false; 6778 this.volume = 0.85; 6779 } 6780 init() { 6781 if (!this.ctx) { 6782 const AudioCtx = window.AudioContext || window.webkitAudioContext; 6783 if (AudioCtx) { 6784 this.ctx = new AudioCtx(); 6785 this.masterGain = this.ctx.createGain(); 6786 this.masterGain.gain.setValueAtTime(this.volume, this.ctx.currentTime); 6787 this.masterGain.connect(this.ctx.destination); 6788 } 6789 } 6790 if (this.ctx && this.ctx.state === "suspended") { 6791 this.ctx.resume().catch(() => { 6792 }); 6793 } 6794 } 6795 getAudioContext() { 6796 this.init(); 6797 return this.ctx; 6798 } 6799 setVolume(vol) { 6800 this.volume = Math.max(0, Math.min(1, vol)); 6801 if (this.masterGain && this.ctx) { 6802 this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : this.volume, this.ctx.currentTime); 6803 } 6804 } 6805 toggleMute() { 6806 this.isMuted = !this.isMuted; 6807 if (this.masterGain && this.ctx) { 6808 this.masterGain.gain.setValueAtTime(this.isMuted ? 0 : this.volume, this.ctx.currentTime); 6809 } 6810 return this.isMuted; 6811 } 6812 getIsMuted() { 6813 return this.isMuted; 6814 } 6815 /** 6816 * Synthesize High Bongo (crisp, higher pitch resonant drum) 6817 */ 6818 playHighBongo(timeOffset = 0) { 6819 this.init(); 6820 if (!this.ctx || !this.masterGain || this.isMuted) return; 6821 const now = this.ctx.currentTime + timeOffset; 6822 const osc = this.ctx.createOscillator(); 6823 const gain = this.ctx.createGain(); 6824 const filter = this.ctx.createBiquadFilter(); 6825 osc.type = "sine"; 6826 osc.frequency.setValueAtTime(380, now); 6827 osc.frequency.exponentialRampToValueAtTime(260, now + 0.12); 6828 filter.type = "bandpass"; 6829 filter.frequency.setValueAtTime(420, now); 6830 filter.Q.setValueAtTime(3.5, now); 6831 gain.gain.setValueAtTime(0.9, now); 6832 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.15); 6833 osc.connect(filter); 6834 filter.connect(gain); 6835 gain.connect(this.masterGain); 6836 osc.start(now); 6837 osc.stop(now + 0.16); 6838 this.playDrumTransient(now, 1400, 0.25); 6839 } 6840 /** 6841 * Synthesize Low Bongo (warmer, deeper resonant drum) 6842 */ 6843 playLowBongo(timeOffset = 0) { 6844 this.init(); 6845 if (!this.ctx || !this.masterGain || this.isMuted) return; 6846 const now = this.ctx.currentTime + timeOffset; 6847 const osc = this.ctx.createOscillator(); 6848 const gain = this.ctx.createGain(); 6849 const filter = this.ctx.createBiquadFilter(); 6850 osc.type = "sine"; 6851 osc.frequency.setValueAtTime(220, now); 6852 osc.frequency.exponentialRampToValueAtTime(140, now + 0.2); 6853 filter.type = "bandpass"; 6854 filter.frequency.setValueAtTime(260, now); 6855 filter.Q.setValueAtTime(3, now); 6856 gain.gain.setValueAtTime(1, now); 6857 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.22); 6858 osc.connect(filter); 6859 filter.connect(gain); 6860 gain.connect(this.masterGain); 6861 osc.start(now); 6862 osc.stop(now + 0.23); 6863 this.playDrumTransient(now, 900, 0.3); 6864 } 6865 /** 6866 * Clean wood click / transient for drum attack 6867 */ 6868 playDrumTransient(now, freq, strength) { 6869 if (!this.ctx || !this.masterGain) return; 6870 const osc = this.ctx.createOscillator(); 6871 const gain = this.ctx.createGain(); 6872 osc.type = "triangle"; 6873 osc.frequency.setValueAtTime(freq, now); 6874 osc.frequency.exponentialRampToValueAtTime(200, now + 0.02); 6875 gain.gain.setValueAtTime(strength, now); 6876 gain.gain.exponentialRampToValueAtTime(1e-4, now + 0.02); 6877 osc.connect(gain); 6878 gain.connect(this.masterGain); 6879 osc.start(now); 6880 osc.stop(now + 0.025); 6881 } 6882 /** 6883 * Metronome Tick (Accented - Beat 1) 6884 */ 6885 playMetronomeAccent(timeOffset = 0) { 6886 this.init(); 6887 if (!this.ctx || !this.masterGain || this.isMuted) return; 6888 const now = this.ctx.currentTime + timeOffset; 6889 const osc = this.ctx.createOscillator(); 6890 const gain = this.ctx.createGain(); 6891 osc.type = "sine"; 6892 osc.frequency.setValueAtTime(1600, now); 6893 osc.frequency.exponentialRampToValueAtTime(800, now + 0.04); 6894 gain.gain.setValueAtTime(0.85, now); 6895 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.05); 6896 osc.connect(gain); 6897 gain.connect(this.masterGain); 6898 osc.start(now); 6899 osc.stop(now + 0.055); 6900 } 6901 /** 6902 * Metronome Tick (Regular - Beats 2, 3, 4) 6903 */
6904 playMetronomeTick(timeOffset = 0) { 6905 this.init(); 6906 if (!this.ctx || !this.masterGain || this.isMuted) return; 6907 const now = this.ctx.currentTime + timeOffset; 6908 const osc = this.ctx.createOscillator(); 6909 const gain = this.ctx.createGain(); 6910 osc.type = "sine"; 6911 osc.frequency.setValueAtTime(1e3, now); 6912 osc.frequency.exponentialRampToValueAtTime(500, now + 0.035); 6913 gain.gain.setValueAtTime(0.55, now); 6914 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.04); 6915 osc.connect(gain); 6916 gain.connect(this.masterGain); 6917 osc.start(now); 6918 osc.stop(now + 0.045); 6919 } 6920 /** 6921 * Acoustic Handclap Synthesis 6922 */ 6923 playClap(timeOffset = 0) { 6924 this.init(); 6925 if (!this.ctx || !this.masterGain || this.isMuted) return; 6926 const now = this.ctx.currentTime + timeOffset; 6927 const bufferSize = this.ctx.sampleRate * 0.18; 6928 const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); 6929 const data = buffer.getChannelData(0); 6930 for (let i = 0; i < bufferSize; i++) { 6931 data[i] = Math.random() * 2 - 1; 6932 } 6933 const noise = this.ctx.createBufferSource(); 6934 noise.buffer = buffer; 6935 const filter = this.ctx.createBiquadFilter(); 6936 filter.type = "bandpass"; 6937 filter.frequency.setValueAtTime(1200, now); 6938 filter.Q.setValueAtTime(1.5, now); 6939 const gain = this.ctx.createGain(); 6940 gain.gain.setValueAtTime(0.3, now); 6941 gain.gain.setValueAtTime(0.6, now + 0.012); 6942 gain.gain.setValueAtTime(0.95, now + 0.024); 6943 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.16); 6944 noise.connect(filter); 6945 filter.connect(gain); 6946 gain.connect(this.masterGain); 6947 noise.start(now); 6948 noise.stop(now + 0.17); 6949 } 6950 /** 6951 * Resonant Woodblock 6952 */ 6953 playWoodblock(high = false, timeOffset = 0) { 6954 this.init(); 6955 if (!this.ctx || !this.masterGain || this.isMuted) return; 6956 const now = this.ctx.currentTime + timeOffset; 6957 const osc = this.ctx.createOscillator(); 6958 const gain = this.ctx.createGain(); 6959 osc.type = "sine"; 6960 const baseFreq = high ? 920 : 640; 6961 osc.frequency.setValueAtTime(baseFreq, now); 6962 osc.frequency.exponentialRampToValueAtTime(baseFreq * 0.7, now + 0.05); 6963 gain.gain.setValueAtTime(0.7, now); 6964 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.06); 6965 osc.connect(gain); 6966 gain.connect(this.masterGain); 6967 osc.start(now); 6968 osc.stop(now + 0.065); 6969 } 6970 /** 6971 * Play sound according to active Sound Type 6972 */ 6973 playBeatSound(soundType, isLeft = false, isAccent = false) { 6974 switch (soundType) { 6975 case "bongo": 6976 if (isAccent || isLeft) { 6977 this.playHighBongo(); 6978 } else { 6979 this.playLowBongo(); 6980 } 6981 break; 6982 case "metronome": 6983 if (isAccent) { 6984 this.playMetronomeAccent(); 6985 } else { 6986 this.playMetronomeTick(); 6987 } 6988 break; 6989 case "clap": 6990 this.playClap(); 6991 break; 6992 case "woodblock": 6993 this.playWoodblock(isAccent || isLeft); 6994 break; 6995 } 6996 } 6997 /** 6998 * Count-In Voice Cue (Beats 1, 2, 3, 4) 6999 */ 7000 playCountIn(beat) { 7001 this.init(); 7002 if (!this.ctx || !this.masterGain || this.isMuted) return; 7003 const now = this.ctx.currentTime; 7004 const osc = this.ctx.createOscillator(); 7005 const gain = this.ctx.createGain(); 7006 osc.type = "triangle"; 7007 const freq = beat === 1 ? 880 : beat === 2 ? 660 : beat === 3 ? 660 : 1100; 7008 osc.frequency.setValueAtTime(freq, now); 7009 gain.gain.setValueAtTime(0.65, now); 7010 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.12); 7011 osc.connect(gain); 7012 gain.connect(this.masterGain); 7013 osc.start(now); 7014 osc.stop(now + 0.13); 7015 } 7016 /** 7017 * Perfect / Good Judgement Chime 7018 */ 7019 playHitFeedback(judgement) { 7020 this.init(); 7021 if (!this.ctx || !this.masterGain || this.isMuted) return; 7022 const now = this.ctx.currentTime; 7023 if (judgement === "miss") { 7024 const osc2 = this.ctx.createOscillator(); 7025 const gain2 = this.ctx.createGain(); 7026 osc2.type = "sawtooth"; 7027 osc2.frequency.setValueAtTime(140, now); 7028 osc2.frequency.linearRampToValueAtTime(70, now + 0.12); 7029 gain2.gain.setValueAtTime(0.25, now); 7030 gain2.gain.exponentialRampToValueAtTime(1e-3, now + 0.12); 7031 osc2.connect(gain2); 7032 gain2.connect(this.masterGain); 7033 osc2.start(now); 7034 osc2.stop(now + 0.13); 7035 return; 7036 } 7037 const baseFreq = judgement === "perfect" ? 1046.5 : 880; 7038 const osc = this.ctx.createOscillator(); 7039 const gain = this.ctx.createGain(); 7040 osc.type = "sine"; 7041 osc.frequency.setValueAtTime(baseFreq, now); 7042 gain.gain.setValueAtTime(judgement === "perfect" ? 0.35 : 0.2, now); 7043 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.1); 7044 osc.connect(gain); 7045 gain.connect(this.masterGain); 7046 osc.start(now); 7047 osc.stop(now + 0.11); 7048 } 7049 /** 7050 * Victory Fanfare Chord 7051 */ 7052 playVictory() { 7053 this.init(); 7054 if (!this.ctx || !this.masterGain || this.isMuted) return; 7055 const chords = [523.25, 659.25, 783.99, 1046.5]; 7056 chords.forEach((freq, idx) => { 7057 if (!this.ctx || !this.masterGain) return; 7058 const now = this.ctx.currentTime + idx * 0.08; 7059 const osc = this.ctx.createOscillator(); 7060 const gain = this.ctx.createGain(); 7061 osc.type = "triangle"; 7062 osc.frequency.setValueAtTime(freq, now); 7063 gain.gain.setValueAtTime(0.35, now); 7064 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.5); 7065 osc.connect(gain); 7066 gain.connect(this.masterGain); 7067 osc.start(now); 7068 osc.stop(now + 0.55); 7069 }); 7070 } 7071} 7072const rhythmAudio = new RhythmAudioEngine(); 7073class RhythmMicInput { 7074 constructor(initialSettings) { 7075 this.stream = null; 7076 this.ctx = null; 7077 this.sourceNode = null; 7078 this.filterNode = null; 7079 this.analyser = null; 7080 this.animationId = null; 7081 this.sensitivity = 1.3; 7082 this.threshold = 0.22; 7083 this.isRunning = false;
7084 this.lastTriggerTime = 0; 7085 this.refractoryMs = 110; 7086 this.baselineNoise = 0.02; 7087 this.onOnsetCallback = null; 7088 this.onLevelCallback = null; 7089 if ((initialSettings == null ? void 0 : initialSettings.sensitivity) !== void 0) this.sensitivity = initialSettings.sensitivity; 7090 if ((initialSettings == null ? void 0 : initialSettings.threshold) !== void 0) this.threshold = initialSettings.threshold; 7091 } 7092 async start() { 7093 var _a; 7094 if (this.isRunning) return true; 7095 if (typeof window === "undefined" || !((_a = navigator.mediaDevices) == null ? void 0 : _a.getUserMedia)) { 7096 console.warn("[RhythmMicInput] Microphone not supported on this platform"); 7097 return false; 7098 } 7099 try { 7100 this.stream = await navigator.mediaDevices.getUserMedia({ 7101 audio: { 7102 echoCancellation: false, 7103 noiseSuppression: false, 7104 autoGainControl: false 7105 } 7106 }); 7107 const AudioCtx = window.AudioContext || window.webkitAudioContext; 7108 this.ctx = new AudioCtx(); 7109 this.sourceNode = this.ctx.createMediaStreamSource(this.stream); 7110 this.filterNode = this.ctx.createBiquadFilter(); 7111 this.filterNode.type = "highpass"; 7112 this.filterNode.frequency.setValueAtTime(120, this.ctx.currentTime); 7113 this.filterNode.Q.setValueAtTime(1, this.ctx.currentTime); 7114 this.analyser = this.ctx.createAnalyser(); 7115 this.analyser.fftSize = 512; 7116 this.analyser.smoothingTimeConstant = 0.15; 7117 this.sourceNode.connect(this.filterNode); 7118 this.filterNode.connect(this.analyser); 7119 this.isRunning = true; 7120 this.startAnalysisLoop(); 7121 return true; 7122 } catch (err) { 7123 console.warn("[RhythmMicInput] Could not initialize microphone:", err); 7124 this.stop(); 7125 return false; 7126 } 7127 } 7128 stop() { 7129 this.isRunning = false; 7130 if (this.animationId !== null) { 7131 cancelAnimationFrame(this.animationId); 7132 this.animationId = null; 7133 } 7134 if (this.stream) { 7135 this.stream.getTracks().forEach((track) => track.stop()); 7136 this.stream = null; 7137 } 7138 if (this.sourceNode) { 7139 try { 7140 this.sourceNode.disconnect(); 7141 } catch { 7142 } 7143 this.sourceNode = null; 7144 } 7145 if (this.filterNode) { 7146 try { 7147 this.filterNode.disconnect(); 7148 } catch { 7149 } 7150 this.filterNode = null; 7151 } 7152 if (this.ctx) { 7153 try { 7154 this.ctx.close(); 7155 } catch { 7156 } 7157 this.ctx = null; 7158 } 7159 } 7160 setSensitivity(val) { 7161 this.sensitivity = Math.max(0.2, Math.min(4, val)); 7162 } 7163 setThreshold(val) { 7164 this.threshold = Math.max(0.04, Math.min(0.8, val)); 7165 } 7166 getSensitivity() { 7167 return this.sensitivity; 7168 } 7169 getThreshold() { 7170 return this.threshold; 7171 } 7172 isListening() { 7173 return this.isRunning; 7174 } 7175 setOnOnset(callback) { 7176 this.onOnsetCallback = callback; 7177 } 7178 setOnLevel(callback) { 7179 this.onLevelCallback = callback; 7180 } 7181 startAnalysisLoop() { 7182 if (!this.analyser) return; 7183 const dataArray = new Uint8Array(this.analyser.frequencyBinCount); 7184 const analyze = () => { 7185 if (!this.isRunning || !this.analyser) return; 7186 this.analyser.getByteTimeDomainData(dataArray); 7187 let sumSq = 0; 7188 for (let i = 0; i < dataArray.length; i++) { 7189 const norm = (dataArray[i] - 128) / 128; 7190 sumSq += norm * norm; 7191 } 7192 const rms = Math.sqrt(sumSq / dataArray.length); 7193 const scaledLevel = Math.min(1, rms * this.sensitivity * 3.5); 7194 this.baselineNoise = this.baselineNoise * 0.96 + scaledLevel * 0.04; 7195 const isAbove = scaledLevel > this.threshold && scaledLevel - this.baselineNoise > 0.06; 7196 const now = performance.now(); 7197 if (isAbove && now - this.lastTriggerTime > this.refractoryMs) { 7198 this.lastTriggerTime = now; 7199 if (this.onOnsetCallback) { 7200 this.onOnsetCallback(now, scaledLevel); 7201 } 7202 } 7203 if (this.onLevelCallback) { 7204 this.onLevelCallback(scaledLevel, isAbove); 7205 } 7206 this.animationId = requestAnimationFrame(analyze); 7207 }; 7208 this.animationId = requestAnimationFrame(analyze); 7209 } 7210} 7211class RhythmInputManager { 7212 constructor() { 7213 this.onHitCallback = null; 7214 this.activeKeys = /* @__PURE__ */ new Set(); 7215 this.micInput = null; 7216 this.isEnabled = true; 7217 this.lastSide = "left"; 7218 this.handleKeyDown = this.handleKeyDown.bind(this); 7219 this.handleKeyUp = this.handleKeyUp.bind(this); 7220 } 7221 attach() { 7222 if (typeof window === "undefined") return; 7223 window.addEventListener("keydown", this.handleKeyDown, { passive: false }); 7224 window.addEventListener("keyup", this.handleKeyUp); 7225 } 7226 detach() { 7227 if (typeof window === "undefined") return; 7228 window.removeEventListener("keydown", this.handleKeyDown); 7229 window.removeEventListener("keyup", this.handleKeyUp); 7230 this.activeKeys.clear(); 7231 if (this.micInput) { 7232 this.micInput.stop(); 7233 this.micInput = null; 7234 } 7235 } 7236 setOnHit(callback) { 7237 this.onHitCallback = callback; 7238 } 7239 setEnabled(enabled) { 7240 this.isEnabled = enabled; 7241 if (!enabled) { 7242 this.activeKeys.clear(); 7243 } 7244 } 7245 setMicInput(mic) { 7246 this.micInput = mic; 7247 if (this.micInput) { 7248 this.micInput.setOnOnset((timestamp) => { 7249 if (!this.isEnabled) return; 7250 this.lastSide = this.lastSide === "left" ? "right" : "left"; 7251 this.dispatchHit({ 7252 timestamp, 7253 source: "mic", 7254 side: this.lastSide 7255 }); 7256 }); 7257 } 7258 } 7259 triggerManualHit(side = "center", source = "touch") { 7260 if (!this.isEnabled) return; 7261 const resolvedSide = side === "center" ? this.lastSide = this.lastSide === "left" ? "right" : "left" : side; 7262 this.dispatchHit({ 7263 timestamp: performance.now(), 7264 source, 7265 side: resolvedSide 7266 }); 7267 } 7268 handleKeyDown(e) { 7269 if (!this.isEnabled) return; 7270 const target = e.target; 7271 if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) { 7272 return; 7273 } 7274 if (e.repeat || this.activeKeys.has(e.code)) return; 7275 this.activeKeys.add(e.code); 7276 let side = "center"; 7277 let handled = false;
7278 if (e.code === "KeyD" || e.code === "KeyF" || e.code === "KeyA" || e.code === "ArrowLeft") { 7279 side = "left"; 7280 handled = true; 7281 } else if (e.code === "KeyJ" || e.code === "KeyK" || e.code === "KeyL" || e.code === "ArrowRight") { 7282 side = "right"; 7283 handled = true; 7284 } else if (e.code === "Space" || e.code === "Enter" || e.code === "ArrowDown" || e.code === "ArrowUp") { 7285 this.lastSide = this.lastSide === "left" ? "right" : "left"; 7286 side = this.lastSide; 7287 handled = true; 7288 } 7289 if (handled) { 7290 e.preventDefault(); 7291 this.dispatchHit({ 7292 timestamp: performance.now(), 7293 source: "keyboard", 7294 side, 7295 rawKey: e.code 7296 }); 7297 } 7298 } 7299 handleKeyUp(e) { 7300 this.activeKeys.delete(e.code); 7301 } 7302 dispatchHit(event) { 7303 if (this.onHitCallback) { 7304 this.onHitCallback(event); 7305 } 7306 } 7307} 7308function getBeatOffsetsForDots(dots) { 7309 switch (dots) { 7310 case 0: 7311 return []; 7312 case 1: 7313 return [0]; 7314 case 2: 7315 return [0, 2]; 7316 case 3: 7317 return [0, 1, 2]; 7318 case 4: 7319 return [0, 1, 2, 3]; 7320 case 8: 7321 return [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5]; 7322 default: 7323 return [0]; 7324 } 7325} 7326const SCREENSHOT_DEFAULT_PATTERN = [ 7327 1, 7328 1, 7329 1, 7330 4, 7331 1, 7332 1, 7333 1, 7334 4, 7335 2, 7336 1, 7337 2, 7338 4, 7339 2, 7340 1, 7341 2, 7342 4 7343]; 7344function createPatternFromDotList(dotList, totalMeasures) { 7345 const count = totalMeasures || Math.max(16, dotList.length); 7346 const list = []; 7347 for (let i = 0; i < count; i++) { 7348 list.push(dotList[i % dotList.length]); 7349 } 7350 return list.map((dots, idx) => ({ 7351 measureIndex: idx, 7352 dots, 7353 beatOffsets: getBeatOffsetsForDots(dots) 7354 })); 7355} 7356function generateRhythmPattern(difficulty = "medium", totalMeasures = 16) { 7357 const dots = []; 7358 switch (difficulty) { 7359 case "easy": { 7360 const pool = [1, 1, 2, 1, 2, 2, 1, 2]; 7361 for (let i = 0; i < totalMeasures; i++) { 7362 dots.push(pool[Math.floor(Math.random() * pool.length)]); 7363 } 7364 break; 7365 } 7366 case "medium": { 7367 for (let i = 0; i < totalMeasures; i++) { 7368 const row = Math.floor(i / 4) % 2; 7369 if (row === 0) { 7370 dots.push([1, 1, 1, 4][i % 4]); 7371 } else { 7372 dots.push([2, 1, 2, 4][i % 4]); 7373 } 7374 } 7375 break; 7376 } 7377 case "hard": { 7378 const options = [2, 4, 4, 2, 4, 3, 4, 1]; 7379 for (let i = 0; i < totalMeasures; i++) { 7380 if (i % 4 === 3) { 7381 dots.push(4); 7382 } else { 7383 dots.push(options[Math.floor(Math.random() * options.length)]); 7384 } 7385 } 7386 break; 7387 } 7388 case "master": { 7389 const options = [4, 4, 8, 4, 8, 2, 4, 8]; 7390 for (let i = 0; i < totalMeasures; i++) { 7391 dots.push(options[Math.floor(Math.random() * options.length)]); 7392 } 7393 break; 7394 } 7395 } 7396 return createPatternFromDotList(dots, totalMeasures); 7397} 7398function convertMidiToMeasurePattern(pianoData, melodyData, bpm, songDuration = 180, difficulty = "medium") { 7399 const secPerBeat = 60 / Math.max(40, bpm || 120); 7400 const secPerMeasure = secPerBeat * 4; 7401 const totalMeasures = Math.max(16, Math.ceil(songDuration / secPerMeasure)); 7402 const rawOnsets = []; 7403 const melNotes = (melodyData == null ? void 0 : melodyData.notes) || (Array.isArray(melodyData) ? melodyData : null); 7404 if (Array.isArray(melNotes) && melNotes.length > 0) { 7405 melNotes.forEach((n) => { 7406 const t = n.time !== void 0 ? Number(n.time) : n.t !== void 0 ? Number(n.t) : n.start !== void 0 ? Number(n.start) : -1; 7407 if (t >= 0 && t <= songDuration) { 7408 rawOnsets.push(t); 7409 } 7410 }); 7411 } 7412 if (rawOnsets.length < 15 && pianoData) { 7413 const tracks = Array.isArray(pianoData) ? pianoData : pianoData.tracks || [pianoData]; 7414 tracks.forEach((track) => { 7415 const notes = track.notes || track.events || (Array.isArray(track) ? track : []); 7416 if (Array.isArray(notes)) { 7417 notes.forEach((n) => { 7418 const t = n.time !== void 0 ? Number(n.time) : n.t !== void 0 ? Number(n.t) : n.start !== void 0 ? Number(n.start) : -1; 7419 if (t >= 0 && t <= songDuration) { 7420 rawOnsets.push(t); 7421 } 7422 }); 7423 } 7424 }); 7425 } 7426 if (rawOnsets.length === 0) { 7427 return generateRhythmPattern(difficulty, totalMeasures); 7428 } 7429 rawOnsets.sort((a, b) => a - b); 7430 const patterns = []; 7431 for (let m = 0; m < totalMeasures; m++) { 7432 const measureStart = m * secPerMeasure; 7433 const measureEnd = (m + 1) * secPerMeasure; 7434 const notesInMeasure = rawOnsets.filter((t) => t >
7434= measureStart && t < measureEnd); 7435 const distinctBeats = []; 7436 notesInMeasure.forEach((t) => { 7437 const relBeat = (t - measureStart) / secPerBeat; 7438 const quantizedBeat = Math.round(relBeat * 2) / 2; 7439 if (quantizedBeat >= 0 && quantizedBeat < 4) { 7440 const alreadyExists = distinctBeats.some((b) => Math.abs(b - quantizedBeat) < 0.25); 7441 if (!alreadyExists) { 7442 distinctBeats.push(quantizedBeat); 7443 } 7444 } 7445 }); 7446 distinctBeats.sort((a, b) => a - b); 7447 let dots = 1; 7448 let beatOffsets = [0]; 7449 const count = distinctBeats.length; 7450 if (count === 0) { 7451 dots = m === 0 ? 1 : 0; 7452 beatOffsets = dots === 0 ? [] : [0]; 7453 } else if (count === 1) { 7454 dots = 1; 7455 beatOffsets = [0]; 7456 } else if (count === 2) { 7457 dots = 2; 7458 beatOffsets = [0, 2]; 7459 } else if (count === 3) { 7460 dots = 3; 7461 beatOffsets = [0, 1, 2]; 7462 } else if (count === 4) { 7463 dots = 4; 7464 beatOffsets = [0, 1, 2, 3]; 7465 } else { 7466 if (difficulty === "master" || difficulty === "hard") { 7467 dots = 8; 7468 beatOffsets = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5]; 7469 } else { 7470 dots = 4; 7471 beatOffsets = [0, 1, 2, 3]; 7472 } 7473 } 7474 const targetNotes = beatOffsets.map((bOff) => ({ 7475 time: measureStart + bOff * secPerBeat, 7476 measureIndex: m, 7477 beatOffset: bOff, 7478 hit: false 7479 })); 7480 patterns.push({ 7481 measureIndex: m, 7482 dots, 7483 beatOffsets, 7484 targetNotes 7485 }); 7486 } 7487 return patterns; 7488} 7489const RhythmTestResults = ({ 7490 result, 7491 locale, 7492 onRetry, 7493 onNextPattern, 7494 onSelectSong, 7495 onChangeCabinet 7496}) => { 7497 reactExports.useEffect(() => { 7498 rhythmAudio.playVictory(); 7499 }, []); 7500 let rank = "C"; 7501 let rankColor = "text-zinc-400 border-zinc-500"; 7502 let rankGlow = "rgba(161, 161, 170, 0.4)"; 7503 let rankTitle = "Rhythm Novice"; 7504 if (result.accuracy >= 98) { 7505 rank = "SSS"; 7506 rankColor = "text-amber-400 border-amber-400"; 7507 rankGlow = "rgba(251, 191, 36, 0.8)"; 7508 rankTitle = "Grand Meowstro!"; 7509 } else if (result.accuracy >= 95) { 7510 rank = "SS"; 7511 rankColor = "text-yellow-400 border-yellow-400"; 7512 rankGlow = "rgba(250, 204, 21, 0.7)"; 7513 rankTitle = "Master of Metronome"; 7514 } else if (result.accuracy >= 90) { 7515 rank = "S"; 7516 rankColor = "text-emerald-400 border-emerald-400"; 7517 rankGlow = "rgba(52, 211, 153, 0.6)"; 7518 rankTitle = "Precision Drummer"; 7519 } else if (result.accuracy >= 80) { 7520 rank = "A"; 7521 rankColor = "text-cyan-400 border-cyan-400"; 7522 rankGlow = "rgba(34, 211, 238, 0.5)"; 7523 rankTitle = "Solid Groover"; 7524 } else if (result.accuracy >= 70) { 7525 rank = "B"; 7526 rankColor = "text-purple-400 border-purple-400"; 7527 rankGlow = "rgba(192, 132, 252, 0.5)"; 7528 rankTitle = "Rhythm Apprentice"; 7529 } 7530 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in duration-300", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-zinc-900 border border-zinc-700/80 rounded-2xl shadow-2xl p-6 sm:p-7 text-white text-center flex flex-col items-center overflow-hidden", children: [ 7531 /* @__PURE__ */ jsxRuntimeExports.jsx( 7532 "div", 7533 { 7534 className: "absolute -top-24 left-1/2 -translate-x-1/2 w-64 h-64 rounded-full blur-3xl pointer-events-none opacity-20", 7535 style: { background: rankGlow } 7536 } 7537 ), 7538 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
7539 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-5 h-5 text-amber-400" }), 7540 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl sm:text-2xl font-black tracking-wider uppercase", children: locale.resultsTitle }) 7541 ] }), 7542 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-400 font-mono mb-4", children: [ 7543 rankTitle, 7544 " ⢠", 7545 result.bpm, 7546 " ", 7547 locale.bpm 7548 ] }), 7549 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-36 h-24 mb-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7550 RhythmBongoCat, 7551 { 7552 leftPawDown: false, 7553 rightPawDown: false, 7554 mood: "celebrate", 7555 combo: result.maxStreak 7556 } 7557 ) }), 7558 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-6 my-2", children: [ 7559 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7560 "div", 7561 { 7562 className: `w-20 h-20 rounded-2xl border-4 flex flex-col items-center justify-center font-black shadow-lg ${rankColor}`, 7563 style: { boxShadow: `0 0 20px ${rankGlow}` }, 7564 children: [
7565 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-3xl leading-none", children: rank }), 7566 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] tracking-widest uppercase mt-0.5", children: locale.rhythmSense }) 7567 ] 7568 } 7569 ), 7570 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-left font-mono", children: [ 7571 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-400 uppercase", children: locale.accuracy }), 7572 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-3xl font-black text-white", children: [ 7573 result.accuracy.toFixed(1), 7574 "%" 7575 ] }), 7576 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-amber-400 font-bold mt-1", children: [ 7577 locale.score, 7578 ": ", 7579 result.score.toLocaleString() 7580 ] }) 7581 ] }) 7582 ] }), 7583 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full bg-zinc-800/80 border border-zinc-700 rounded-xl p-3 my-4 font-mono text-xs", children: [
7584 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-2 border-b border-zinc-700/60 mb-2", children: [ 7585 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-400", children: [ 7586 locale.timingOffset, 7587 ":" 7588 ] }), 7589 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-cyan-300", children: [ 7590 result.avgOffsetMs >= 0 ? `+${result.avgOffsetMs.toFixed(1)}ms` : `${result.avgOffsetMs.toFixed(1)}ms`, 7591 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-400 ml-1", children: [ 7592 "(", 7593 result.avgOffsetMs >= 0 ? locale.late : locale.early, 7594 ")" 7595 ] }) 7596 ] }) 7597 ] }), 7598 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-4 gap-2 text-center", children: [ 7599 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/60 rounded p-1.5 border border-emerald-500/20", children: [
7600 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-emerald-400 font-bold", children: "PERFECT" }), 7601 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-black text-white", children: result.perfectCount }) 7602 ] }), 7603 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/60 rounded p-1.5 border border-amber-500/20", children: [ 7604 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-amber-400 font-bold", children: "GOOD" }), 7605 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-black text-white", children: result.goodCount }) 7606 ] }), 7607 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/60 rounded p-1.5 border border-rose-500/20", children: [ 7608 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-rose-400 font-bold", children: "MISS" }), 7609 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-black text-white", children: result.missCount }) 7610 ] }),
7611 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/60 rounded p-1.5 border border-purple-500/20", children: [ 7612 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-purple-400 font-bold", children: locale.maxStreak }), 7613 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-black text-white", children: result.maxStreak }) 7614 ] }) 7615 ] }) 7616 ] }), 7617 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row items-center gap-2.5 w-full mt-1", children: [ 7618 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7619 "button", 7620 { 7621 onClick: onRetry, 7622 className: "flex-1 w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold transition-all shadow-lg active:scale-95 cursor-pointer", 7623 children: [ 7624 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }), 7625 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.retry }) 7626 ] 7627 } 7628 ),
7629 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7630 "button", 7631 { 7632 onClick: onNextPattern, 7633 className: "flex-1 w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-zinc-800 hover:bg-zinc-700 text-cyan-300 border border-cyan-500/30 font-bold transition-all active:scale-95 cursor-pointer", 7634 children: [ 7635 /* @__PURE__ */ jsxRuntimeExports.jsx(Shuffle, { size: 16 }), 7636 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.nextPattern }) 7637 ] 7638 } 7639 ) 7640 ] }), 7641 (onSelectSong || onChangeCabinet) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 mt-3 text-xs text-zinc-400", children: [ 7642 onSelectSong && /* @__PURE__ */ jsxRuntimeExports.jsxs( 7643 "button", 7644 { 7645 onClick: onSelectSong, 7646 className: "hover:text-cyan-300 transition-colors flex items-center gap-1 cursor-pointer", 7647 children: [ 7648 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 13 }),
7649 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.changeSong }) 7650 ] 7651 } 7652 ), 7653 onChangeCabinet && /* @__PURE__ */ jsxRuntimeExports.jsxs( 7654 "button", 7655 { 7656 onClick: onChangeCabinet, 7657 className: "hover:text-amber-300 transition-colors flex items-center gap-1 cursor-pointer", 7658 children: [ 7659 /* @__PURE__ */ jsxRuntimeExports.jsx(Award, { size: 13 }), 7660 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.changeCabinet }) 7661 ] 7662 } 7663 ) 7664 ] }) 7665 ] }) }); 7666}; 7667const RHYTHM_TEST_TRANSLATIONS = { 7668 en: { 7669 gameTitle: "Rhythm Test", 7670 gameSubtitle: "Measure Beat Pattern Training", 7671 tagline: "Follow or reproduce beat patterns measure by measure using keyboard, fingers, or microphone!", 7672 modePlayAlong: "Continuous Play-Along", 7673 modeEcho: "Call & Response (Echo)", 7674 modeSandbox: "Freeplay / Editor", 7675 inputKeyboard: "Keyboard", 7676 inputTouch: "Touch / Fingers", 7677 inputMic: "Microphone (Clap / Voice)", 7678 micCalibrate: "Calibrate Microphone", 7679 micSensitivity: "Mic Sensitivity", 7680 micThreshold: "Clap Detection Threshold", 7681 micTestClap: "Clap your hands or tap the table to test!", 7682 micActive: "Mic Active", 7683 micDenied: "Microphone access denied. Using keyboard/touch instead.", 7684 micPrompt: "Allow microphone access to play with clapping, desk taps, or voice!", 7685 micEnable: "Enable Mic", 7686 difficulty: "Difficulty", 7687 diffEasy: "Easy (Whole & Half notes)", 7688 diffMedium: "Medium (Quarter notes)", 7689 diffHard: "Hard (Mixed & Syncopated)", 7690 diffMaster: "Master (8th notes & Rapid rolls)", 7691 perfect: "PERFECT!", 7692 good: "GOOD!", 7693 ok: "OK", 7694 miss: "MISS", 7695 score: "Score", 7696 accuracy: "Accuracy", 7697 streak: "Streak", 7698 maxStreak: "Max Streak", 7699 bpm: "BPM", 7700 tapTempo: "Tap Tempo", 7701 measure: "Measure", 7702 resultsTitle: "Rhythm Test Completed!", 7703 rhythmSense: "Rhythm Aptitude", 7704 timingOffset: "Avg Timing Offset", 7705 early: "Early", 7706 late: "Late", 7707 retry: "Try Again", 7708 nextPattern: "New Pattern", 7709 changeSong: "Select Song", 7710 changeCabinet: "Change Cabinet", 7711 soundType: "Drum Sound", 7712 soundBongo: "Bongos", 7713 soundMetronome: "Metronome", 7714 soundClap: "Handclap", 7715 soundWoodblock: "Woodblock", 7716 countIn: "Count In", 7717 paused: "PAUSED", 7718 resume: "Resume", 7719 restart: "Restart", 7720 listenPhase: "LISTEN TO PATTERN", 7721 yourTurnPhase: "YOUR TURN! TAP NOW", 7722 instructions: "Tap the dots on each beat! Spacebar, touch screen, or clap into mic.", 7723 page: "Page", 7724 tapKeyHint: "Tap [SPACE] or [F / J] on the beat!", 7725 touchHint: "Tap screen on the beat!", 7726 hitNow: "HIT!", 7727 rest: "REST" 7728 }, 7729 ja: { 7730 gameTitle: "ãªãºã ãã¹ã", 7731 gameSubtitle: "å°ç¯ãã¼ããã¿ã¼ã³ãã¬ã¼ãã³ã°", 7732 tagline: "ãã¼ãã¼ãã»æã¿ããã»ãã¤ã¯ï¼ææã»å£°ï¼ã§å°ç¯ãã¨ã®ãã¼ããã¿ã¼ã³ãæ¼å¥ãããï¼", 7733 modePlayAlong: "é£ç¶ãã¬ã¤ã¢ãã³ã°", 7734 modeEcho: "ã³ã¼ã«ï¼ã¬ã¹ãã³ã¹ï¼ã¨ã³ã¼ï¼", 7735 modeSandbox: "ããªã¼ãã¬ã¤ / ã¨ãã£ã¿ã¼", 7736 inputKeyboard: "ãã¼ãã¼ã", 7737 inputTouch: "ã¿ãã / æ", 7738 inputMic: "ãã¤ã¯ï¼ææå / é³å£°ï¼", 7739 micCalibrate: "ãã¤ã¯æåº¦èª¿æ´", 7740 micSensitivity: "ãã¤ã¯æåº¦", 7741 micThreshold: "æææ¤ç¥ãããå¤", 7742 micTestClap: "æãå©ããæºãã¿ãããã¦ãã¹ããã¦ãã ããï¼", 7743 micActive: "ãã¤ã¯ç¨¼åä¸", 7744 micDenied: "ãã¤ã¯ã¸ã®ã¢ã¯ã»ã¹ãæå¦ããã¾ããããã¼ãã¼ãã¾ãã¯ã¿ããããå©ç¨ãã ããã", 7745 micPrompt: "ææåã声ã§ãã¬ã¤ããã«ã¯ãã¤ã¯ã®æ¨©éã許å¯ãã¦ãã ããï¼", 7746 micEnable: "ãã¤ã¯ãæå¹å", 7747 difficulty: "é£æåº¦", 7748 diffEasy: "ããããï¼å ¨é³ç¬¦ã»2åé³ç¬¦ï¼", 7749 diffMedium: "ãµã¤ãï¼4åé³ç¬¦ï¼", 7750 diffHard: "ãããããï¼ã·ã³ã³ãã¼ã·ã§ã³ï¼", 7751 diffMaster: "ãã«ï¼8åé³ç¬¦ã»é«é飿ï¼", 7752 perfect: "ãã¼ãã§ã¯ãï¼", 7753 good: "ã°ããï¼", 7754 ok: "OK", 7755 miss: "ãã¹", 7756 score: "ã¹ã³ã¢", 7757 accuracy: "精度", 7758 streak: "ã³ã³ã", 7759 maxStreak: "æå¤§ã³ã³ã", 7760 bpm: "BPM", 7761 tapTempo: "ã¿ãããã³ã", 7762 measure: "å°ç¯", 7763 resultsTitle: "ãªãºã ãã¹ãå®äºï¼", 7764 rhythmSense: "ãªãºã æå¤å®", 7765 timingOffset: "å¹³åã¿ã¤ãã³ã°ãºã¬", 7766 early: "æ©ã", 7767 late: "é ã", 7768 retry: "ããä¸åº¦ææ¦", 7769 nextPattern: "æ°ãããã¿ã¼ã³", 7770 changeSong: "æ²ãé¸ã¶", 7771 changeCabinet: "ã²ã¼ã 夿´", 7772 soundType: "ææ¥½å¨é³", 7773 soundBongo: "ãã³ã´", 7774 soundMetronome: "ã¡ãããã¼ã ", 7775 soundClap: "ãã³ãã¯ã©ãã", 7776 soundWoodblock: "ã¦ãããããã¯",
7777 countIn: "ã«ã¦ã³ãã¤ã³", 7778 paused: "䏿忢ä¸", 7779 resume: "åé", 7780 restart: "æåãã", 7781 listenPhase: "ãææ¬ãè´ãã¦ã", 7782 yourTurnPhase: "ããªãã®çªï¼å©ãã¦ï¼", 7783 instructions: "ãã¼ãã®ãããã«åããã¦ã¿ããï¼ã¹ãã¼ã¹ãã¼ãã¿ãããã¾ãã¯ææåã§ã", 7784 page: "ãã¼ã¸", 7785 tapKeyHint: "ãã¼ãã«åãã㦠[SPACE] ã¾ã㯠[F / J] ãå©ããï¼", 7786 touchHint: "ãã¼ãã«åããã¦ç»é¢ãã¿ããï¼", 7787 hitNow: "ãããï¼", 7788 rest: "ä¼ç¬¦" 7789 }, 7790 "zh-CN": { 7791 gameTitle: "è奿µè¯", 7792 gameSubtitle: "å°èèææ¨¡å¼è®ç»", 7793 tagline: "使ç¨é®çãææè§¦æ§æéº¦å é£ï¼å»æ/声é³ï¼éå°èææèææ¨¡å¼ï¼", 7794 modePlayAlong: "è¿ç»è·å¼¹", 7795 modeEcho: "å¬è¾©å¤è¿°ï¼åé³ï¼", 7796 modeSandbox: "èªç±æ²ç / ç¼è¾å¨", 7797 inputKeyboard: "é®ç", 7798 inputTouch: "è§¦æ¸ / ææ", 7799 inputMic: "麦å é£ï¼å»æ / 声é³ï¼", 7800 micCalibrate: "麦å 飿 ¡å", 7801 micSensitivity: "麦å é£çµæåº¦", 7802 micThreshold: "å»ææ£æµéå¼", 7803 micTestClap: "å»æææ²å»æ¡é¢ä»¥æµè¯ï¼", 7804 micActive: "麦å é£çå¬ä¸", 7805 micDenied: "麦å 飿é被æç»ï¼å·²åæ¢è³é®ç/è§¦æ¸æ¨¡å¼ã", 7806 micPrompt: "请å 许麦å 飿éï¼å³å¯éè¿å»ææå£°é³ç©æ¸¸æï¼", 7807 micEnable: "å¯ç¨éº¦å é£", 7808 difficulty: "é¾åº¦", 7809 diffEasy: "ç®åï¼å ¨é³ç¬¦ä¸äºåé³ç¬¦ï¼", 7810 diffMedium: "æ®éï¼ååé³ç¬¦ï¼", 7811 diffHard: "å°é¾ï¼ååé³ä¸æ··åï¼", 7812 diffMaster: "大å¸ï¼å «åé³ç¬¦ä¸æéè¿å»ï¼", 7813 perfect: "ç²¾åï¼", 7814 good: "å¾å¥½ï¼", 7815 ok: "è¯å¥½", 7816 miss: "失误", 7817 score: "å¾å", 7818 accuracy: "å确度", 7819 streak: "è¿å»", 7820 maxStreak: "æé«è¿å»", 7821 bpm: "BPM", 7822 tapTempo: "ç¹ææµé", 7823 measure: "å°è", 7824 resultsTitle: "è奿µè¯å®æï¼", 7825 rhythmSense: "è奿è¯çº§", 7826 timingOffset: "å¹³åèæåå·®", 7827 early: "åæ©", 7828 late: "åæ", 7829 retry: "éæ°ææ", 7830 nextPattern: "æ¢ä¸ªæ°å¾æ¡", 7831 changeSong: "éæ©ææ²", 7832 changeCabinet: "æ´æ¢æºå°", 7833 soundType: "æå»ä¹é³è²", 7834 soundBongo: "馿é¼", 7835 soundMetronome: "èæå¨", 7836 soundClap: "廿声", 7837 soundWoodblock: "æ¨é±¼ / æ¨å", 7838 countIn: "é¢å¤æ", 7839 paused: "å·²æå", 7840 resume: "ç»§ç»", 7841 restart: "éæ°å¼å§", 7842 listenPhase: "è嬿¼ç¤ºèå¥", 7843 yourTurnPhase: "è½®å°ä½ äºï¼å¼å§æ²å»", 7844 instructions: "卿¯ä¸ªåç¹èæä¸æ²å»ï¼æ¯æç©ºæ ¼é®ã触æ¸å±æéº¦å é£å»æã", 7845 page: "页", 7846 tapKeyHint: "è·éèææ²å» [ç©ºæ ¼] æ [F / J]ï¼", 7847 touchHint: "è·éèæè½»ç¹å±å¹ï¼", 7848 hitNow: "廿ï¼", 7849 rest: "伿¢" 7850 }, 7851 "zh-TW": { 7852 gameTitle: "ç¯å¥æ¸¬è©¦", 7853 gameSubtitle: "å°ç¯ç¯ææ¨¡å¼è¨ç·´", 7854 tagline: "使ç¨éµç¤ãææè§¸æ§æéº¥å é¢¨ï¼ææ/è²é³ï¼éå°ç¯ææ°ç¯ææ¨¡å¼ï¼", 7855 modePlayAlong: "é£çºè·å½", 7856 modeEcho: "è½è¾¨è¤è¿°ï¼åé³ï¼", 7857 modeSandbox: "èªç±æ²ç / 編輯å¨", 7858 inputKeyboard: "éµç¤", 7859 inputTouch: "è§¸æ§ / ææ", 7860 inputMic: "麥å é¢¨ï¼ææ / è²é³ï¼", 7861 micCalibrate: "麥å é¢¨æ ¡æº", 7862 micSensitivity: "麥å 風éæåº¦", 7863 micThreshold: "æææª¢æ¸¬é¾å¼", 7864 micTestClap: "ææææ²ææ¡é¢ä»¥æ¸¬è©¦ï¼", 7865 micActive: "麥å 風ç£è½ä¸", 7866 micDenied: "麥å 風æ¬é被æçµï¼å·²åæè³éµç¤/è§¸æ§æ¨¡å¼ã", 7867 micPrompt: "è«å 許麥å 風æ¬éï¼å³å¯ééæææè²é³ç©éæ²ï¼", 7868 micEnable: "åç¨éº¥å 風", 7869 difficulty: "é£åº¦", 7870 diffEasy: "ç°¡å®ï¼å ¨é³ç¬¦èäºåé³ç¬¦ï¼", 7871 diffMedium: "æ®éï¼ååé³ç¬¦ï¼", 7872 diffHard: "å°é£ï¼ååé³èæ··åï¼", 7873 diffMaster: "大師ï¼å «åé³ç¬¦è極é飿ï¼", 7874 perfect: "ç²¾æºï¼", 7875 good: "å¾å¥½ï¼", 7876 ok: "è¯å¥½", 7877 miss: "失誤", 7878 score: "å¾å", 7879 accuracy: "æºç¢ºåº¦", 7880 streak: "飿", 7881 maxStreak: "æé«é£æ", 7882 bpm: "BPM", 7883 tapTempo: "æ¸¬éææ", 7884 measure: "å°ç¯", 7885 resultsTitle: "ç¯å¥æ¸¬è©¦å®æï¼", 7886 rhythmSense: "ç¯å¥æè©ç´", 7887 timingOffset: "å¹³åç¯æåå·®", 7888 early: "åæ©", 7889 late: "åæ", 7890 retry: "éæ°ææ°", 7891 nextPattern: "æåæ°åæ¡", 7892 changeSong: "é¸æææ²", 7893 changeCabinet: "æ´ææ©å°", 7894 soundType: "æææ¨é³è²", 7895 soundBongo: "馿é¼", 7896 soundMetronome: "ç¯æå¨", 7897 soundClap: "ææè²", 7898 soundWoodblock: "æ¨é / æ¨å¡", 7899 countIn: "é åæ", 7900 paused: "å·²æ«å", 7901 resume: "ç¹¼çº", 7902 restart: "éæ°éå§", 7903 listenPhase: "èè½ç¤ºç¯ç¯å¥", 7904 yourTurnPhase: "輪å°ä½ äºï¼éå§æ²æ", 7905 instructions: "卿¯ååé»ç¯æä¸æ²æï¼æ¯æ´ç©ºç½éµã觸æ§è¢å¹æéº¥å 風ææã", 7906 page: "é ", 7907 tapKeyHint: "è·é¨ç¯ææ²æ [空ç½éµ] æ [F / J]ï¼", 7908 touchHint: "è·é¨ç¯æè¼è§¸è¢å¹ï¼", 7909 hitNow: "ææï¼", 7910 rest: "伿¢" 7911 }, 7912 ko: { 7913 gameTitle: "ë¦¬ë¬ í ì¤í¸", 7914 gameSubtitle: "ë§ë ë¹í¸ í¨í´ í¸ë ì´ë", 7915 tagline: "í¤ë³´ë, íë©´ í°ì¹, ë§ì´í¬(ë°ì/목ì리)ë¡ ë§ëë³ ë¹í¸ í¨í´ì ì°ì£¼í´ë³´ì¸ì!", 7916 modePlayAlong: "ì°ì íë ì´ì´ë¡±", 7917 modeEcho: "ì½ & 리ì¤í°ì¤ (ìì½)", 7918 modeSandbox: "ìì ì°ìµ / ìëí°", 7919 inputKeyboard: "í¤ë³´ë", 7920 inputTouch: "í°ì¹ / ìê°ë½", 7921 inputMic: "ë§ì´í¬ (ë°ì / ìì±)", 7922 micCalibrate: "ë§ì´í¬ ê°ë ì¤ì ", 7923 micSensitivity: "ë§ì´í¬ ê°ë", 7924 micThreshold: "ë°ì ê°ì§ ìê³ê°", 7925 micTestClap: "ë°ì를 ì¹ê±°ë ì± ìì ëëë ¤ í ì¤í¸í´ë³´ì¸ì!", 7926 micActive: "ë§ì´í¬ ìë ì¤", 7927 micDenied: "ë§ì´í¬ ì ê·¼ì´ ê±°ë¶ëììµëë¤. í¤ë³´ë ëë í°ì¹ë¥¼ ì¬ì©í©ëë¤.", 7928 micPrompt: "ë°ìë 목ìë¦¬ë¡ íë ì´íë ¤ë©´ ë§ì´í¬ ê¶íì íì©í´ì£¼ì¸ì!", 7929 micEnable: "ë§ì´í¬ íì±í", 7930 difficulty: "ëì´ë", 7931 diffEasy: "ì¬ì (ì¨ìí & 2ë¶ìí)", 7932 diffMedium: "ë³´íµ (4ë¶ìí)", 7933 diffHard: "ì´ë ¤ì (ì±ì½íì´ì & í¼í©)", 7934 diffMaster: "ë§ì¤í° (8ë¶ìí & ì´ê³ ì ì°í)", 7935 perfect: "í¼íí¸!", 7936 good: "굿!", 7937 ok: "OK", 7938 miss: "미ì¤", 7939 score: "ì ì", 7940 accuracy: "ì íë", 7941 streak: "콤보", 7942 maxStreak: "ìµë 콤보", 7943 bpm: "BPM", 7944 tapTempo: "í í í¬", 7945 measure: "ë§ë", 7946 resultsTitle: "ë¦¬ë¬ í ì¤í¸ ìë£!", 7947 rhythmSense: "리ë¬ê° ë±ê¸", 7948 timingOffset: "íê· íì´ë° ì¤ì°¨", 7949 early: "ë¹ ë¦", 7950 late: "ë림", 7951 retry: "ë¤ì ëì ", 7952 nextPattern: "ì í¨í´", 7953 changeSong: "곡 ì í", 7954 changeCabinet: "기기 ë³ê²½", 7955 soundType: "íì 기 ì¬ì´ë", 7956 soundBongo: "ë´ê³ ", 7957 soundMetronome: "ë©í¸ë¡ë", 7958 soundClap: "ìë°ì", 7959 soundWoodblock: "ì°ëë¸ë¡", 7960 countIn: "ì¹´ì´í¸ ì¸", 7961 paused: "ì¼ì ì ì§", 7962 resume: "ì´ì´í기", 7963 restart: "ì²ìë¶í°", 7964 listenPhase: "ìë² í¨í´ì ë¤ì´ë³´ì¸ì", 7965 yourTurnPhase: "ë¹ì ì ì°¨ë¡! ëë리ì¸ì", 7966 instructions: "ì ì íì´ë°ì ë§ì¶° ëë리ì¸ì! ì¤íì´ì¤ë°, íë©´ í°ì¹, ë§ì´í¬ ë°ì를 ì§ìí©ëë¤.", 7967 page: "íì´ì§", 7968 tapKeyHint: "ë°ìì ë§ì¶° [ì¤íì´ì¤] ëë [F / J]를 ë르ì¸ì!", 7969 touchHint: "ë°ìì ë§ì¶° íë©´ì ííì¸ì!", 7970 hitNow: "íí¸!", 7971 rest: "ì¼í" 7972 }, 7973 es: { 7974 gameTitle: "Test de Ritmo", 7975 gameSubtitle: "Entrenamiento de Compases y Ritmos", 7976 tagline: "¡Sigue o reproduce patrones de compás usando teclado, dedos o micrófono!", 7977 modePlayAlong: "Tocar en Continuo", 7978 modeEcho: "Llamada y Respuesta (Eco)", 7979 modeSandbox: "Modo Libre / Editor", 7980 inputKeyboard: "Teclado", 7981 inputTouch: "Táctil / Dedos", 7982 inputMic: "Micrófono (Aplauso / Voz)", 7983 micCalibrate: "Calibrar Micrófono", 7984 micSensitivity: "Sensibilidad del Mic", 7985 micThreshold: "Umbral de Detección de Aplauso", 7986 micTestClap: "¡Aplaude o golpea la mesa para probar!", 7987 micActive: "Micrófono Activo", 7988 micDenied: "Permiso de micrófono denegado. Usando teclado/táctil.", 7989 micPrompt: "¡Permite el acceso al micrófono para jugar aplaudiendo o usando tu voz!", 7990 micEnable: "Activar Micrófono", 7991 difficulty: "Dificultad", 7992 diffEasy: "Fácil (Redondas y Blancas)", 7993 diffMedium: "Medio (Negras)", 7994 diffHard: "DifÃcil (Sincopado y Mixto)", 7995 diffMaster: "Maestro (Corcheas y Redobles Rápidos)", 7996 perfect: "¡PERFECTO!", 7997 good: "¡BIEN!", 7998 ok: "OK", 7999 miss: "FALLO", 8000 score: "Puntuación", 8001 accuracy: "Precisión", 8002 streak: "Combo", 8003 maxStreak: "Combo Máximo", 8004 bpm: "BPM", 8005 tapTempo: "Marcar Tempo", 8006 measure: "Compás", 8007 resultsTitle: "¡Test de Ritmo Completado!", 8008 rhythmSense: "Sentido del Ritmo", 8009 timingOffset: "DesvÃo Promedio", 8010 early: "Adelantado", 8011 late: "Atrasado", 8012 retry: "Reintentar", 8013 nextPattern: "Nuevo Patrón", 8014 changeSong: "Elegir Canción", 8015 changeCabinet: "Cambiar Máquina", 8016 soundType: "Sonido de Percusión", 8017 soundBongo: "Bongós", 8018 soundMetronome: "Metrónomo", 8019 soundClap: "Aplauso", 8020 soundWoodblock: "Bloque de Madera", 8021 countIn: "Cuenta Inicial", 8022 paused: "EN PAUSA", 8023 resume: "Continuar", 8024 restart: "Reiniciar", 8025 listenPhase: "ESCUCHA EL PATRÃN", 8026 yourTurnPhase: "¡TU TURNO! TOCA AHORA", 8027 instructions: "¡Toca en los puntos marcados! Usa espacio, pantalla táctil o aplaude al micrófono.", 8028 page: "Página", 8029 tapKeyHint: "¡Presiona [ESPACIO] o [F / J] en el compás!", 8030 touchHint: "¡Toca la pantalla en el compás!", 8031 hitNow: "¡TOCA!", 8032 rest: "SILENCIO" 8033 }, 8034 fr: { 8035 gameTitle: "Test de Rythme", 8036 gameSubtitle: "Entraînement de Mesures et Motifs Rhythmiques", 8037 tagline: "Suivez ou reproduisez les motifs mesure par mesure au clavier, au toucher ou au micro !", 8038 modePlayAlong: "Jeu Continu", 8039 modeEcho: "Appel et Réponse (Ãcho)", 8040 modeSandbox: "Mode Libre / Ãditeur", 8041 inputKeyboard: "Clavier", 8042 inputTouch: "Tactile / Doigts", 8043 inputMic: "Microphone (Claquement / Voix)", 8044 micCalibrate: "Calibrer le Microphone", 8045 micSensitivity: "Sensibilité du Micro", 8046 micThreshold: "Seuil de Détection de Claquement", 8047 micTestClap: "Tapez dans vos mains ou sur la table pour tester !", 8048 micActive: "Microphone Actif", 8049 micDenied: "Accès micro refusé. Utilisation du clavier/tactile.", 8050 micPrompt: "Autorisez le micro pour jouer en tapant dans les mains ou avec votre voix !", 8051 micEnable: "Activer le Micro", 8052 difficulty: "Difficulté", 8053 diffEasy: "Facile (Rondes et Blanches)", 8054 diffMedium: "Moyen (Noires)", 8055 diffHard: "Difficile (Syncopes & Mixtes)", 8056 diffMaster: "Maître (Croches & Roulements Rapides)", 8057 perfect: "PARFAIT !", 8058 good: "BIEN !", 8059 ok: "OK", 8060 miss: "RATÃ", 8061 score: "Score", 8062 accuracy: "Précision", 8063 streak: "Combo", 8064 maxStreak: "Combo Max", 8065 bpm: "BPM", 8066 tapTempo: "Tap Tempo", 8067 measure: "Mesure", 8068 resultsTitle: "Test de Rythme Terminé !", 8069 rhythmSense: "Sens du Rythme", 8070 timingOffset: "Décalage Moyen", 8071 early: "En avance", 8072 late: "En retard", 8073 retry: "Réessayer", 8074 nextPattern: "Nouveau Motif", 8075 changeSong: "Choisir un Morceau", 8076 changeCabinet: "Changer de Borne", 8077 soundType: "Son de Percussion", 8078 soundBongo: "Bongos", 8079 soundMetronome: "Métronome", 8080 soundClap: "Claquement de mains", 8081 soundWoodblock: "Wood-block", 8082 countIn: "Décompte", 8083 paused: "EN PAUSE", 8084 resume: "Reprendre", 8085 restart: "Recommencer", 8086 listenPhase: "ÃCOUTEZ LE MOTIF", 8087 yourTurnPhase: "à VOUS ! TAPEZ MAINTENANT", 8088 instructions: "Tapez au rythme des points ! Espace, écran tactile ou claquements au micro.", 8089 page: "Page", 8090 tapKeyHint: "Appuyez sur [ESPACE] ou [F / J] en rythme !", 8091 touchHint: "Touchez l'écran en rythme !", 8092 hitNow: "FRAPPEZ !", 8093 rest: "PAUSE" 8094 }, 8095 de: { 8096 gameTitle: "Rhythmustest", 8097 gameSubtitle: "Takt- und Beatmuster-Training", 8098 tagline: "Folge oder reproduziere Beatmuster Takt für Takt per Tastatur, Touch oder Mikrofon!", 8099 modePlayAlong: "Kontinuierliches Mitspielen", 8100 modeEcho: "Call & Response (Echo)", 8101 modeSandbox: "Freispiel / Editor", 8102 inputKeyboard: "Tastatur", 8103 inputTouch: "Touch / Finger", 8104 inputMic: "Mikrofon (Klatschen / Stimme)", 8105 micCalibrate: "Mikrofon kalibrieren", 8106 micSensitivity: "Mikrofon-Empfindlichkeit", 8107 micThreshold: "Klatsch-Schwellenwert", 8108 micTestClap: "Klatschen oder auf den Tisch klopfen zum Testen!", 8109 micActive: "Mikrofon aktiv", 8110 micDenied: "Mikrofonzugriff verweigert. Tastatur/Touch wird verwendet.", 8111 micPrompt: "Mikrofonzugriff erlauben, um mit Klatschen oder Stimme zu spielen!", 8112 micEnable: "Mikrofon aktivieren", 8113 difficulty: "Schwierigkeit", 8114 diffEasy: "Einfach (Ganze & Halbe Noten)", 8115 diffMedium: "Mittel (Viertelnoten)", 8116 diffHard: "Schwer (Synkopen & Gemischt)", 8117 diffMaster: "Meister (Achtelnoten & Wirbel)", 8118 perfect: "PERFEKT!", 8119 good: "GUT!", 8120 ok: "OK",
8121 miss: "FEHLSCHLAG", 8122 score: "Punkte", 8123 accuracy: "Genauigkeit", 8124 streak: "Combo", 8125 maxStreak: "Max. Combo", 8126 bpm: "BPM", 8127 tapTempo: "Tap Tempo", 8128 measure: "Takt", 8129 resultsTitle: "Rhythmustest abgeschlossen!", 8130 rhythmSense: "Rhythmusgefühl-Rang", 8131 timingOffset: "Durchschnittl. Abweichung", 8132 early: "Zu früh", 8133 late: "Zu spät", 8134 retry: "Nochmal versuchen", 8135 nextPattern: "Neues Muster", 8136 changeSong: "Song wählen", 8137 changeCabinet: "Automat wechseln", 8138 soundType: "Percussion-Sound", 8139 soundBongo: "Bongos", 8140 soundMetronome: "Metronom", 8141 soundClap: "Klatschen", 8142 soundWoodblock: "Holzblock", 8143 countIn: "Einzähler", 8144 paused: "PAUSIERT", 8145 resume: "Fortsetzen", 8146 restart: "Neu starten", 8147 listenPhase: "MUSTER ANHÃREN", 8148 yourTurnPhase: "DU BIST DRAN! JETZT TIPPEN", 8149 instructions: "Tippe passend zu den Punkten! Leertaste, Touchscreen oder Klatschen ins Mikrofon.", 8150 page: "Seite", 8151 tapKeyHint: "Drücke [LEERTASTE] oder [F / J] im Takt!", 8152 touchHint: "Tippe im Takt auf den Bildschirm!", 8153 hitNow: "TREFFER!", 8154 rest: "PAUSE" 8155 }, 8156 it: { 8157 gameTitle: "Test del Ritmo", 8158 gameSubtitle: "Allenamento di Battute e Pattern Ritmici", 8159 tagline: "Segui o riproduci i pattern battuta per battuta usando tastiera, tocco o microfono!", 8160 modePlayAlong: "Suona Continuo", 8161 modeEcho: "Botta e Risposta (Eco)", 8162 modeSandbox: "Modalità Libera / Editor", 8163 inputKeyboard: "Tastiera", 8164 inputTouch: "Tocco / Dita", 8165 inputMic: "Microfono (Battito di mani / Voce)", 8166 micCalibrate: "Calibra Microfono", 8167 micSensitivity: "Sensibilità Microfono", 8168 micThreshold: "Soglia Rilevamento Battito", 8169 micTestClap: "Batti le mani o tocca il tavolo per testare!", 8170 micActive: "Microfono Attivo", 8171 micDenied: "Accesso al microfono negato. Utilizzo di tastiera/tocco.", 8172 micPrompt: "Consenti l'accesso al microfono per giocare battendo le mani o con la voce!", 8173 micEnable: "Attiva Microfono", 8174 difficulty: "Difficoltà ", 8175 diffEasy: "Facile (Intere e Minime)", 8176 diffMedium: "Medio (Semiminime)", 8177 diffHard: "Difficile (Sincope e Misto)", 8178 diffMaster: "Maestro (Crome e Rulli Rapidi)", 8179 perfect: "PERFETTO!", 8180 good: "BENE!", 8181 ok: "OK", 8182 miss: "MANCATO", 8183 score: "Punteggio", 8184 accuracy: "Precisione", 8185 streak: "Combo", 8186 maxStreak: "Combo Max", 8187 bpm: "BPM", 8188 tapTempo: "Tap Tempo", 8189 measure: "Battuta", 8190 resultsTitle: "Test del Ritmo Completato!", 8191 rhythmSense: "Senso del Ritmo", 8192 timingOffset: "Scostamento Medio", 8193 early: "In anticipo", 8194 late: "In ritardo", 8195 retry: "Riprova", 8196 nextPattern: "Nuovo Pattern", 8197 changeSong: "Scegli Brano", 8198 changeCabinet: "Cambia Cabinato", 8199 soundType: "Suono Percussione", 8200 soundBongo: "Bongo", 8201 soundMetronome: "Metronomo", 8202 soundClap: "Battito di mani", 8203 soundWoodblock: "Woodblock", 8204 countIn: "Conteggio Iniziale", 8205 paused: "IN PAUSA", 8206 resume: "Riprendi", 8207 restart: "Ricomincia", 8208 listenPhase: "ASCOLTA IL PATTERN", 8209 yourTurnPhase: "TOCCA A TE! BATTI ORA", 8210 instructions: "Batti sui punti ritmici! Barra spaziatrice, touchscreen o battito di mani al microfono.", 8211 page: "Pagina", 8212 tapKeyHint: "Premi [SPAZIO] o [F / J] a tempo di battuta!", 8213 touchHint: "Tocca lo schermo a tempo!", 8214 hitNow: "COLPISCI!", 8215 rest: "PAUSA" 8216 }, 8217 pt: { 8218 gameTitle: "Teste de Ritmo", 8219 gameSubtitle: "Treino de Compassos e Padrões RÃtmicos", 8220 tagline: "Siga ou reproduza padrões rÃtmicos compasso a compasso usando teclado, toque ou microfone!", 8221 modePlayAlong: "Tocar ContÃnuo", 8222 modeEcho: "Chamada e Resposta (Eco)", 8223 modeSandbox: "Modo Livre / Editor", 8224 inputKeyboard: "Teclado", 8225 inputTouch: "Toque / Dedos", 8226 inputMic: "Microfone (Palmas / Voz)", 8227 micCalibrate: "Calibrar Microfone", 8228 micSensitivity: "Sensibilidade do Mic", 8229 micThreshold: "Limiar de Detecção de Palmas", 8230 micTestClap: "Bata palmas ou bata na mesa para testar!", 8231 micActive: "Microfone Ativo", 8232 micDenied: "Acesso ao microfone negado. Usando teclado/toque.", 8233 micPrompt: "Permita o microfone para jogar batendo palmas ou usando a voz!", 8234 micEnable: "Ativar Microfone", 8235 difficulty: "Dificuldade", 8236 diffEasy: "Fácil (Semibreves e MÃnimas)", 8237 diffMedium: "Médio (SemÃnimas)", 8238 diffHard: "DifÃcil (Sincopado e Misto)", 8239 diffMaster: "Mestre (Colcheias e Rufos Rápidos)", 8240 perfect: "PERFEITO!", 8241 good: "BOM!", 8242 ok: "OK", 8243 miss: "ERRO", 8244 score: "Pontuação", 8245 accuracy: "Precisão", 8246 streak: "Combo", 8247 maxStreak: "Combo Máximo", 8248 bpm: "BPM", 8249 tapTempo: "Tap Tempo", 8250 measure: "Compasso", 8251 resultsTitle: "Teste de Ritmo ConcluÃdo!", 8252 rhythmSense: "Senso RÃtmico", 8253 timingOffset: "Desvio Médio", 8254 early: "Adiantado", 8255 late: "Atrasado", 8256 retry: "Tentar Novamente", 8257 nextPattern: "Novo Padrão", 8258 changeSong: "Escolher Música", 8259 changeCabinet: "Trocar de Máquina", 8260 soundType: "Som de Percussão", 8261 soundBongo: "Bongôs", 8262 soundMetronome: "Metrônomo", 8263 soundClap: "Palmas", 8264 soundWoodblock: "Bloco de Madeira", 8265 countIn: "Contagem Inicial", 8266 paused: "PAUSADO", 8267 resume: "Retomar", 8268 restart: "Reiniciar", 8269 listenPhase: "OUÃA O PADRÃO", 8270 yourTurnPhase: "SUA VEZ! TOQUE AGORA", 8271 instructions: "Toque nos pontos de cada batida! Barra de espaço, tela sensÃvel ou palmas no microfone.", 8272 page: "Página", 8273 tapKeyHint: "Pressione [ESPAÃO] ou [F / J] na batida!", 8274 touchHint: "Toque na tela no ritmo!", 8275 hitNow: "BATA!", 8276 rest: "PAUSA" 8277 }, 8278 ru: { 8279 gameTitle: "РиÑм-ТеÑÑ", 8280 gameSubtitle: "ТÑениÑовка ÑакÑов и ÑиÑмиÑеÑÐºÐ¸Ñ ÑиÑÑнков", 8281 tagline: "ÐовÑоÑÑйÑе или оÑбивайÑе ÑиÑм ÑÐ°ÐºÑ Ð·Ð° ÑакÑом Ñ Ð¿Ð¾Ð¼Ð¾ÑÑÑ ÐºÐ»Ð°Ð²Ð¸Ð°ÑÑÑÑ, ÑенÑоÑа или микÑоÑона!", 8282 modePlayAlong: "ÐепÑеÑÑÐ²Ð½Ð°Ñ Ð¸Ð³Ñа", 8283 modeEcho: "ÐÑ Ð¾ (СлÑÑай и повÑоÑÑй)", 8284 modeSandbox: "СвободнÑй Ñежим / РедакÑоÑ", 8285 inputKeyboard: "ÐлавиаÑÑÑа", 8286 inputTouch: "СенÑÐ¾Ñ / ÐалÑÑÑ", 8287 inputMic: "ÐикÑоÑон (Хлопки / ÐолоÑ)", 8288 micCalibrate: "ÐалибÑовка микÑоÑона", 8289 micSensitivity: "ЧÑвÑÑвиÑелÑноÑÑÑ Ð¼Ð¸ÐºÑоÑона", 8290 micThreshold: "ÐоÑог ÑÑабаÑÑÐ²Ð°Ð½Ð¸Ñ Ñ Ð»Ð¾Ð¿ÐºÐ°", 8291 micTestClap: "ХлопниÑе в ладоÑи или поÑÑÑÑиÑе по ÑÑÐ¾Ð»Ñ Ð´Ð»Ñ Ð¿ÑовеÑки!", 8292 micActive: "ÐикÑоÑон акÑивен", 8293 micDenied: "ÐоÑÑÑп к микÑоÑÐ¾Ð½Ñ Ð¾Ñклонен. ÐÑполÑзÑеÑÑÑ ÐºÐ»Ð°Ð²Ð¸Ð°ÑÑÑа/ÑенÑоÑ.", 8294 micPrompt: "РазÑеÑиÑе доÑÑÑп к микÑоÑонÑ, ÑÑÐ¾Ð±Ñ Ð¸Ð³ÑаÑÑ Ñ Ð»Ð¾Ð¿ÐºÐ°Ð¼Ð¸ или голоÑом!", 8295 micEnable: "ÐклÑÑиÑÑ Ð¼Ð¸ÐºÑоÑон", 8296 difficulty: "СложноÑÑÑ", 8297 diffEasy: "Ðегко (ЦелÑе и половиннÑе ноÑÑ)", 8298 diffMedium: "СÑедне (ЧеÑвеÑÑнÑе ноÑÑ)", 8299 diffHard: "Сложно (Ð¡Ð¸Ð½ÐºÐ¾Ð¿Ñ Ð¸ ÑмеÑаннÑе)", 8300 diffMaster: "ÐаÑÑÐµÑ (ÐоÑÑмÑе ноÑÑ Ð¸ бÑÑÑÑÑе дÑоби)", 8301 perfect: "ÐÐÐÐÐЬÐÐ!", 8302 good: "Ð¥ÐÐ ÐШÐ!", 8303 ok: "ÐÐ", 8304 miss: "ÐÐ ÐÐÐÐ¥", 8305 score: "СÑеÑ", 8306 accuracy: "ТоÑноÑÑÑ", 8307 streak: "Ðомбо", 8308 maxStreak: "ÐакÑ. комбо", 8309 bpm: "BPM", 8310 tapTempo: "ÐадаÑÑ Ñемп", 8311 measure: "ТакÑ", 8312 resultsTitle: "РиÑм-ÑеÑÑ Ð·Ð°Ð²ÐµÑÑен!", 8313 rhythmSense: "ЧÑвÑÑво ÑиÑма", 8314 timingOffset: "СÑеднее оÑклонение", 8315 early: "РанÑÑе", 8316 late: "Ðозже", 8317 retry: "ÐовÑоÑиÑÑ", 8318 nextPattern: "ÐовÑй паÑÑеÑн", 8319 changeSong: "ÐÑбÑаÑÑ ÑÑек", 8320 changeCabinet: "СмениÑÑ Ð°Ð²ÑомаÑ", 8321 soundType: "ÐвÑк пеÑкÑÑÑии",
8322 soundBongo: "Ðонго", 8323 soundMetronome: "ÐеÑÑоном", 8324 soundClap: "Хлопок в ладоÑи", 8325 soundWoodblock: "ÐÑд-блок", 8326 countIn: "ÐÑÑÑеÑ", 8327 paused: "ÐÐУÐÐ", 8328 resume: "ÐÑодолжиÑÑ", 8329 restart: "ÐаÑаÑÑ Ð·Ð°Ð½Ð¾Ð²Ð¾", 8330 listenPhase: "СÐУШÐÐТРÐÐТТÐÐ Ð", 8331 yourTurnPhase: "ÐÐШРÐЧÐÐ ÐÐЬ! ÐÐÐÐÐÐÐТÐ", 8332 instructions: "ÐажимайÑе по ÑоÑкам в ÑакÑ! ÐÑобел, ÑенÑоÑнÑй ÑкÑан или Ñ Ð»Ð¾Ð¿ÐºÐ¸ в микÑоÑон.", 8333 page: "СÑÑаниÑа", 8334 tapKeyHint: "ÐажимайÑе [ÐÐ ÐÐÐÐ] или [F / J] в ÑакÑ!", 8335 touchHint: "ÐаÑайÑеÑÑ ÑкÑана в ÑакÑ!", 8336 hitNow: "УÐÐÐ !", 8337 rest: "ÐÐУÐÐ" 8338 }, 8339 id: { 8340 gameTitle: "Tes Ritme", 8341 gameSubtitle: "Latihan Pola Ketukan Birama", 8342 tagline: "Ikuti atau mainkan ulang pola ketukan per birama lewat keyboard, sentuhan, atau mikrofon!", 8343 modePlayAlong: "Main Bersama Berlanjut", 8344 modeEcho: "Tanya & Jawab (Gema)", 8345 modeSandbox: "Main Bebas / Editor", 8346 inputKeyboard: "Keyboard", 8347 inputTouch: "Sentuh / Jari", 8348 inputMic: "Mikrofon (Tepuk Tangan / Suara)", 8349 micCalibrate: "Kalibrasi Mikrofon", 8350 micSensitivity: "Sensitivitas Mic", 8351 micThreshold: "Ambang Deteksi Tepukan", 8352 micTestClap: "Tepuk tangan atau ketuk meja untuk menguji!", 8353 micActive: "Mikrofon Aktif", 8354 micDenied: "Akses mikrofon ditolak. Beralih ke keyboard/sentuhan.", 8355 micPrompt: "Izinkan mikrofon untuk bermain dengan tepuk tangan atau suara!", 8356 micEnable: "Aktifkan Mikrofon", 8357 difficulty: "Tingkat Kesulitan", 8358 diffEasy: "Mudah (Ketukan Utuh & Setengah)", 8359 diffMedium: "Sedang (Ketukan Seperempat)", 8360 diffHard: "Sulit (Sinkopasi & Campuran)", 8361 diffMaster: "Master (Ketukan Seperdelapan & Cepat)", 8362 perfect: "SEMPURNA!", 8363 good: "BAGUS!", 8364 ok: "OKE", 8365 miss: "MELESET", 8366 score: "Skor", 8367 accuracy: "Akurasi", 8368 streak: "Kombo", 8369 maxStreak: "Kombo Maksimum", 8370 bpm: "BPM", 8371 tapTempo: "Tap Tempo", 8372 measure: "Birama", 8373 resultsTitle: "Tes Ritme Selesai!", 8374 rhythmSense: "Peringkat Rasa Ritme", 8375 timingOffset: "Rata-rata Selisih Waktu", 8376 early: "Terlalu Cepat", 8377 late: "Terlambat", 8378 retry: "Coba Lagi", 8379 nextPattern: "Pola Baru", 8380 changeSong: "Pilih Lagu", 8381 changeCabinet: "Ganti Permainan", 8382 soundType: "Suara Perkusi", 8383 soundBongo: "Bongo", 8384 soundMetronome: "Metronom", 8385 soundClap: "Tepuk Tangan", 8386 soundWoodblock: "Blok Kayu", 8387 countIn: "Hitungan Masuk", 8388 paused: "JEDA", 8389 resume: "Lanjutkan", 8390 restart: "Mulai Ulang", 8391 listenPhase: "DENGARKAN POLANYA", 8392 yourTurnPhase: "GILIRANMU! KETUK SEKARANG", 8393 instructions: "Ketuk sesuai titik pada setiap ketukan! Spasi, layar sentuh, atau tepukan ke mikrofon.", 8394 page: "Halaman", 8395 tapKeyHint: "Tekan [SPASI] atau [F / J] tepat pada ketukan!", 8396 touchHint: "Ketuk layar tepat pada ketukan!", 8397 hitNow: "KETUK!", 8398 rest: "ISTIRAHAT" 8399 }, 8400 ar: { 8401 gameTitle: "اختبار Ø§ÙØ¥ÙÙØ§Ø¹", 8402 gameSubtitle: "ØªØ¯Ø±ÙØ¨ Ø£Ù٠اط Ø§ÙØ¥ÙÙØ§Ø¹ ÙÙÙ Ù ÙÙØ§Ø³", 8403 tagline: "اتبع أ٠أعد عز٠أÙ٠اط Ø§ÙØ¥ÙÙØ§Ø¹ Ù ÙÙØ§Ø³Ø§Ù ب٠ÙÙØ§Ø³ باستخدا٠ÙÙØØ© اÙÙ ÙØ§ØªÙØ Ø£Ù Ø§ÙÙ٠س أ٠اÙÙ ÙÙØ±ÙÙÙÙ!", 8404 modePlayAlong: "عز٠٠ست٠ر ٠تزا٠Ù", 8405 modeEcho: "ÙØ¯Ø§Ø¡ ÙØ§Ø³ØªØ¬Ø§Ø¨Ø© (صدÙ)", 8406 modeSandbox: "ÙØ¶Ø¹ ØØ± / Ù ØØ±Ø±", 8407 inputKeyboard: "ÙÙØØ© اÙÙ ÙØ§ØªÙØ", 8408 inputTouch: "اÙÙ٠س / Ø§ÙØ£ØµØ§Ø¨Ø¹", 8409 inputMic: "اÙÙ ÙÙØ±ÙÙÙÙ (تصÙÙÙ / ØµÙØª)", 8410 micCalibrate: "Ù Ø¹Ø§ÙØ±Ø© اÙÙ ÙÙØ±ÙÙÙÙ", 8411 micSensitivity: "ØØ³Ø§Ø³ÙØ© اÙÙ ÙÙØ±ÙÙÙÙ", 8412 micThreshold: "عتبة رصد Ø§ÙØªØµÙÙÙ", 8413 micTestClap: "صÙÙ Ø¨ÙØ¯Ù٠أ٠اÙÙØ± عÙÙ Ø§ÙØ·Ø§ÙÙØ© ÙÙØ§Ø®ØªØ¨Ø§Ø±!", 8414 micActive: "اÙÙ ÙÙØ±ÙÙÙÙ ÙÙØ¯ Ø§ÙØ§Ø³ØªÙ اع", 8415 micDenied: "ØªÙ Ø±ÙØ¶ إذ٠اÙÙ ÙÙØ±ÙÙÙÙ. جار٠استخدا٠ÙÙØØ© اÙÙ ÙØ§ØªÙØ/اÙÙ٠س.", 8416 micPrompt: "ÙØ±Ø¬Ù Ø§ÙØ³Ù Ø§Ø Ø¨Ø§ÙÙØµÙÙ ÙÙÙ ÙÙØ±ÙÙÙÙ ÙÙØ¹Ø¨ Ø¨Ø§ÙØªØµÙÙÙ Ø£Ù Ø§ÙØµÙت!", 8417 micEnable: "ØªÙØ¹Ù٠اÙÙ ÙÙØ±ÙÙÙÙ", 8418 difficulty: "٠ستÙÙ Ø§ÙØµØ¹Ùبة", 8419 diffEasy: "سÙÙ (Ø¹ÙØ§Ù ات ÙØ§Ù ÙØ© ÙÙØµÙÙØ©)", 8420 diffMedium: "Ù ØªÙØ³Ø· (Ø¹ÙØ§Ù ات Ø±Ø¨Ø¹ÙØ©)", 8421 diffHard: "صعب (Ø¥ÙÙØ§Ø¹Ø§Øª Ù Ø¹ÙØ¯Ø©)", 8422 diffMaster: "Ù ØØªØ±Ù (Ø¹ÙØ§Ù ات ث٠ÙÙØ© ÙÙÙØ±Ø§Øª Ø³Ø±ÙØ¹Ø©)", 8423 perfect: "٠٠تاز!", 8424 good: "Ø¬ÙØ¯ جداÙ!", 8425 ok: "Ø¬ÙØ¯", 8426 miss: "Ø¥Ø®ÙØ§Ù", 8427 score: "اÙÙÙØ§Ø·", 8428 accuracy: "Ø§ÙØ¯ÙØ©", 8429 streak: "Ø§ÙØ¶Ø±Ø¨Ø§Øª اÙ٠تتاÙÙØ©", 8430 maxStreak: "أعÙ٠تتابع", 8431 bpm: "ÙØ¨Ø¶Ø© ÙÙ Ø§ÙØ¯ÙÙÙØ©", 8432 tapTempo: "ØªØØ¯Ùد Ø§ÙØ³Ø±Ø¹Ø© باÙÙ٠س", 8433 measure: "اÙÙ ÙÙØ§Ø³", 8434 resultsTitle: "Ø§ÙØªÙ ٠اختبار Ø§ÙØ¥ÙÙØ§Ø¹!", 8435 rhythmSense: "تÙÙÙÙ Ø§ÙØØ³ Ø§ÙØ¥ÙÙØ§Ø¹Ù", 8436 timingOffset: "Ù ØªÙØ³Ø· ÙØ§Ø±Ù Ø§ÙØªÙÙÙØª", 8437 early: "Ù Ø¨ÙØ±", 8438 late: "٠تأخر", 8439 retry: "إعادة اÙÙ ØØ§ÙÙØ©", 8440 nextPattern: "ÙÙ Ø· Ø¬Ø¯ÙØ¯", 8441 changeSong: "Ø§Ø®ØªÙØ§Ø± أغÙÙØ©", 8442 changeCabinet: "تغÙÙØ± Ø§ÙØ¬Ùاز", 8443 soundType: "ØµÙØª Ø§ÙØ¥ÙÙØ§Ø¹", 8444 soundBongo: "بÙÙØºÙ", 8445 soundMetronome: "Ø¨ÙØ¯ÙÙ Ø§ÙØ¥ÙÙØ§Ø¹", 8446 soundClap: "تصÙÙ٠اÙÙØ¯ÙÙ", 8447 soundWoodblock: "ÙØªÙØ© Ø®Ø´Ø¨ÙØ©", 8448 countIn: "عد ØªÙØ§Ø²ÙÙ", 8449 paused: "Ù ÙÙÙÙ Ù Ø¤ÙØªØ§Ù", 8450 resume: "٠تابعة", 8451 restart: "إعادة Ø§ÙØ¨Ø¯Ø¡", 8452 listenPhase: "است٠ع Ø¥Ù٠اÙÙÙ Ø·", 8453 yourTurnPhase: "Ø¯ÙØ±Ù Ø§ÙØ¢Ù! اÙÙØ±", 8454 instructions: "اÙÙØ± Ø¨Ø¯ÙØ© Ø¹ÙØ¯ ÙØµÙ٠اÙÙÙØ·Ø©! Ù Ø³Ø§ÙØ© ÙÙØØ© اÙÙ ÙØ§ØªÙØ Ø£Ù Ø§ÙÙ٠س Ø£Ù Ø§ÙØªØµÙÙÙ.", 8455 page: "ØµÙØØ©", 8456 tapKeyHint: "اضغط عÙÙ [Ù Ø³Ø§ÙØ©] Ø£Ù [F / J] ٠ع Ø§ÙØ¥ÙÙØ§Ø¹!", 8457 touchHint: "اÙÙØ± عÙÙ Ø§ÙØ´Ø§Ø´Ø© ٠ع Ø§ÙØ¥ÙÙØ§Ø¹!", 8458 hitNow: "اÙÙØ± Ø§ÙØ¢Ù!", 8459 rest: "Ø³ÙØªØ©" 8460 } 8461}; 8462function getRhythmTestLocale(lang) { 8463 if (!lang) return RHYTHM_TEST_TRANSLATIONS.en; 8464 const lower = lang.toLowerCase(); 8465 if (RHYTHM_TEST_TRANSLATIONS[lower]) return RHYTHM_TEST_TRANSLATIONS[lower]; 8466 if (lower.startsWith("zh-cn") || lower === "zh") return RHYTHM_TEST_TRANSLATIONS["zh-CN"]; 8467 if (lower.startsWith("zh-tw") || lower.startsWith("zh-hk")) return RHYTHM_TEST_TRANSLATIONS["zh-TW"]; 8468 const prefix = lower.split("-")[0]; 8469 return RHYTHM_TEST_TRANSLATIONS[prefix] || RHYTHM_TEST_TRANSLATIONS.en; 8470} 8471const RhythmTestGame = ({ 8472 data, 8473 melodyData, 8474 currentTime = 0, 8475 isPlaying: parentIsPlaying = false, 8476 duration = 180, 8477 tempo = 120, 8478 timeSignature = [4, 4], 8479 trackId, 8480 userId, 8481 userName, 8482 onPlayPause, 8483 onSeek, 8484 onBackToSongSelect, 8485 onBackToCabinetSelect 8486}) => { 8487 const { i18n } = useTranslation(); 8488 const locale = reactExports.useMemo(() => getRhythmTestLocale(i18n.language), [i18n.language]); 8489 const [playMode, setPlayMode] = reactExports.useState("play-along"); 8490 const [difficulty, setDifficulty] = reactExports.useState("medium"); 8491 const [bpm, setBpm] = reactExports.useState(() => Math.max(60, Math.min(240, Math.round(tempo) || 120))); 8492 const [soundType, setSoundType] = reactExports.useState("bongo"); 8493 const [inputMode, setInputMode] = reactExports.useState("keyboard"); 8494 const [micEnabled, setMicEnabled] = reactExports.useState(false); 8495 const [micLevel, setMicLevel] = reactExports.useState(0); 8496 const [micThreshold, setMicThreshold] = reactExports.useState(0.22); 8497 const [micSensitivity, setMicSensitivity] = reactExports.useState(1.3); 8498 const [showMicCalibration, setShowMicCalibration] = reactExports.useState(false); 8499 const [showSettingsModal, setShowSettingsModal] = reactExports.useState(false); 8500 const [showHelpModal, setShowHelpModal] = reactExports.useState(false); 8501 const [isPlaying, setIsPlaying] = reactExports.useState(false); 8502 const [countInBeat, setCountInBeat] = reactExports.useState(null); 8503 const [currentMeasureIndex, setCurrentMeasureIndex] = reactExports.useState(0); 8504 const [currentSubBeatOffset, setCurrentSubBeatOffset] = reactExports.useState(-1);
8505 const [echoPhase, setEchoPhase] = reactExports.useState("listen"); 8506 const [leftPawDown, setLeftPawDown] = reactExports.useState(false); 8507 const [rightPawDown, setRightPawDown] = reactExports.useState(false); 8508 const [catMood, setCatMood] = reactExports.useState("idle"); 8509 const secPerBeat = 60 / Math.max(40, bpm || 120); 8510 const secPerMeasure = secPerBeat * 4; 8511 const [patterns, setPatterns] = reactExports.useState(() => { 8512 var _a; 8513 if (data && data.length > 0 || melodyData && (((_a = melodyData.notes) == null ? void 0 : _a.length) || Array.isArray(melodyData) && melodyData.length > 0)) { 8514 return convertMidiToMeasurePattern(data, melodyData, tempo || 120, duration || 180, "medium"); 8515 } 8516 return createPatternFromDotList(SCREENSHOT_DEFAULT_PATTERN); 8517 }); 8518 reactExports.useEffect(() => { 8519 var _a; 8520 if (data && data.length > 0 || melodyData && (((_a = melodyData.notes) == null ? void 0 : _a.length) || Array.isArray(melodyData) && melodyData.length > 0)) { 8521 const songPatterns = convertMidiToMeasurePattern( 8522 data, 8523 melodyData, 8524 bpm, 8525 duration || 180, 8526 difficulty 8527 ); 8528 setPatterns(songPatterns); 8529 } else { 8530 const totalM = Math.max(16, Math.ceil((duration || 180) / secPerMeasure)); 8531 setPatterns(generateRhythmPattern(difficulty, totalM)); 8532 } 8533 }, [data, melodyData, bpm, duration, difficulty, secPerMeasure]); 8534 const [score, setScore] = reactExports.useState(0); 8535 const [combo, setCombo] = reactExports.useState(0); 8536 const [maxCombo, setMaxCombo] = reactExports.useState(0); 8537 const [perfectCount, setPerfectCount] = reactExports.useState(0); 8538 const [goodCount, setGoodCount] = reactExports.useState(0); 8539 const [missCount, setMissCount] = reactExports.useState(0); 8540 const [timingOffsets, setTimingOffsets] = reactExports.useState([]); 8541 const [lastJudgement, setLastJudgement] = reactExports.useState(null); 8542 const [gameResult, setGameResult] = reactExports.useState(null); 8543 const inputManagerRef = reactExports.useRef(null); 8544 const micInputRef = reactExports.useRef(null); 8545 const scheduledNotesRef = reactExports.useRef([]); 8546 const gameStartTimeRef = reactExports.useRef(0); 8547 const animationFrameRef = reactExports.useRef(null); 8548 const isPlayingRef = reactExports.useRef(isPlaying); 8549 isPlayingRef.current = isPlaying; 8550 reactExports.useEffect(() => { 8551 const manager = new RhythmInputManager(); 8552 inputManagerRef.current = manager; 8553 manager.attach(); 8554 return () => { 8555 manager.detach(); 8556 }; 8557 }, []); 8558 const handleToggleMic = reactExports.useCallback(async () => { 8559 var _a, _b, _c; 8560 if (micEnabled) { 8561 (_a = micInputRef.current) == null ? void 0 : _a.stop(); 8562 micInputRef.current = null; 8563 (_b = inputManagerRef.current) == null ? void 0 : _b.setMicInput(null); 8564 setMicEnabled(false); 8565 if (inputMode === "mic") setInputMode("keyboard"); 8566 } else { 8567 const mic = new RhythmMicInput({ 8568 sensitivity: micSensitivity, 8569 threshold: micThreshold 8570 }); 8571 mic.setOnLevel((lvl) => { 8572 setMicLevel(lvl); 8573 }); 8574 const granted = await mic.start(); 8575 if (granted) { 8576 micInputRef.current = mic; 8577 (_c = inputManagerRef.current) == null ? void 0 : _c.setMicInput(mic); 8578 setMicEnabled(true); 8579 setInputMode("mic"); 8580 setShowMicCalibration(true); 8581 } else { 8582 alert(locale.micDenied); 8583 } 8584 } 8585 }, [micEnabled, inputMode, micSensitivity, micThreshold, locale.micDenied]); 8586 reactExports.useEffect(() => { 8587 if (micInputRef.current) { 8588 micInputRef.current.setSensitivity(micSensitivity); 8589 micInputRef.current.setThreshold(micThreshold); 8590 } 8591 }, [micSensitivity, micThreshold]); 8592 const triggerPaw = reactExports.useCallback((side) => { 8593 if (side === "left") { 8594 setLeftPawDown(true); 8595 setTimeout(() => setLeftPawDown(false), 90); 8596 } else { 8597 setRightPawDown(true); 8598 setTimeout(() => setRightPawDown(false), 90); 8599 } 8600 }, []); 8601 const handleUserHit = reactExports.useCallback((event) => { 8602 triggerPaw(event.side); 8603 if (!isPlayingRef.current) { 8604 rhythmAudio.playBeatSound(soundType, event.side === "left", false); 8605 return; 8606 } 8607 if (playMode === "echo" && echoPhase === "listen") { 8608 rhythmAudio.playBeatSound(soundType, event.side === "left", false); 8609 return; 8610 } 8611 const now = performance.now(); 8612 const activeTime = (now - gameStartTimeRef.current) / 1e3; 8613 const notes = scheduledNotesRef.current; 8614 let bestIdx = -1; 8615 let minDiff = 0.18; 8616 for (let i = 0; i < notes.length; i++) { 8617 if (notes[i].hit) continue; 8618 const diff = activeTime - notes[i].time; 8619 if (Math.abs(diff) < Math.abs(minDiff)) { 8620 minDiff = diff; 8621 bestIdx = i; 8622 } 8623 } 8624 if (bestIdx !== -1) { 8625 notes[bestIdx].hit = true; 8626 const diffMs = minDiff * 1e3; 8627 const absDiffMs = Math.abs(diffMs); 8628 setTimingOffsets((prev) => [...prev, diffMs]); 8629 if (absDiffMs <= 50) { 8630 rhythmAudio.playHitFeedback("perfect"); 8631 rhythmAudio.playBeatSound(soundType, event.side === "left", true); 8632 setScore((s) => s + 300 + combo * 10); 8633 setCombo((c) => { 8634 const next = c + 1;
8635 setMaxCombo((m) => Math.max(m, next)); 8636 return next; 8637 }); 8638 setPerfectCount((p) => p + 1); 8639 setCatMood("happy"); 8640 setLastJudgement({ text: "PERFECT", color: "text-amber-400" }); 8641 } else if (absDiffMs <= 110) { 8642 rhythmAudio.playHitFeedback("good"); 8643 rhythmAudio.playBeatSound(soundType, event.side === "left", false); 8644 setScore((s) => s + 150 + combo * 5); 8645 setCombo((c) => { 8646 const next = c + 1; 8647 setMaxCombo((m) => Math.max(m, next)); 8648 return next; 8649 }); 8650 setGoodCount((g) => g + 1); 8651 setCatMood("happy"); 8652 setLastJudgement({ text: "GOOD", color: "text-emerald-400" }); 8653 } else { 8654 rhythmAudio.playHitFeedback("good"); 8655 rhythmAudio.playBeatSound(soundType, event.side === "left", false); 8656 setScore((s) => s + 50); 8657 setGoodCount((g) => g + 1); 8658 setCatMood("focused"); 8659 setLastJudgement({ text: "OK", color: "text-cyan-400" }); 8660 } 8661 } else { 8662 rhythmAudio.playBeatSound(soundType, event.side === "left", false); 8663 } 8664 }, [triggerPaw, soundType, playMode, echoPhase, combo]); 8665 reactExports.useEffect(() => { 8666 var _a; 8667 (_a = inputManagerRef.current) == null ? void 0 : _a.setOnHit(handleUserHit); 8668 }, [handleUserHit]); 8669 const buildTargetNotes = reactExports.useCallback((offsetSec = 0) => { 8670 const notes = []; 8671 patterns.forEach((pat, mIdx) => { 8672 const measureStart = offsetSec + mIdx * secPerMeasure; 8673 pat.beatOffsets.forEach((bOff) => { 8674 notes.push({ 8675 time: measureStart + bOff * secPerBeat, 8676 measureIdx: mIdx, 8677 beatOffset: bOff, 8678 hit: false 8679 }); 8680 }); 8681 }); 8682 return notes; 8683 }, [patterns, secPerMeasure, secPerBeat]); 8684 const startGame = reactExports.useCallback(() => { 8685 rhythmAudio.init(); 8686 setIsPlaying(true); 8687 setGameResult(null); 8688 setScore(0); 8689 setCombo(0); 8690 setMaxCombo(0); 8691 setPerfectCount(0); 8692 setGoodCount(0); 8693 setMissCount(0); 8694 setTimingOffsets([]); 8695 setCatMood("focused"); 8696 setCurrentMeasureIndex(0); 8697 setCurrentSubBeatOffset(-1); 8698 let currentLeadBeat = 1; 8699 setCountInBeat(1); 8700 rhythmAudio.playCountIn(1); 8701 const countInterval = setInterval(() => { 8702 currentLeadBeat++; 8703 if (currentLeadBeat <= 4) { 8704 setCountInBeat(currentLeadBeat); 8705 rhythmAudio.playCountIn(currentLeadBeat); 8706 } else { 8707 clearInterval(countInterval); 8708 setCountInBeat(null); 8709 if (onPlayPause) { 8710 onPlayPause(true); 8711 } 8712 const startTime = performance.now(); 8713 gameStartTimeRef.current = startTime; 8714 scheduledNotesRef.current = buildTargetNotes(0); 8715 const songTotalSec = duration && duration > 5 ? duration : patterns.length * secPerMeasure; 8716 const tick = () => { 8717 const elapsedSec = (performance.now() - startTime) / 1e3; 8718 if (elapsedSec >= songTotalSec) { 8719 finishGame(); 8720 return; 8721 } 8722 const mIdx = Math.min(patterns.length - 1, Math.floor(elapsedSec / secPerMeasure)); 8723 const timeInMeasure = elapsedSec % secPerMeasure; 8724 const subBeat = timeInMeasure / secPerBeat; 8725 setCurrentMeasureIndex(mIdx); 8726 setCurrentSubBeatOffset(subBeat); 8727 scheduledNotesRef.current.forEach((note) => { 8728 if (!note.hit && elapsedSec - note.time > 0.16) { 8729 note.hit = true; 8730 setMissCount((m) => m + 1); 8731 setCombo(0); 8732 setCatMood("miss"); 8733 rhythmAudio.playHitFeedback("miss"); 8734 setLastJudgement({ text: "MISS", color: "text-rose-500" }); 8735 } 8736 }); 8737 if (playMode === "echo" && echoPhase === "listen") { 8738 scheduledNotesRef.current.forEach((note) => { 8739 if (Math.abs(elapsedSec - note.time) < 0.02) { 8740 triggerPaw(note.beatOffset % 2 === 0 ? "left" : "right"); 8741 rhythmAudio.playBeatSound(soundType, note.beatOffset % 2 === 0, note.beatOffset === 0); 8742 } 8743 }); 8744 } 8745 animationFrameRef.current = requestAnimationFrame(tick); 8746 }; 8747 animationFrameRef.current = requestAnimationFrame(tick); 8748 } 8749 }, secPerBeat * 1e3); 8750 }, [secPerBeat, secPerMeasure, buildTargetNotes, playMode, echoPhase, soundType, triggerPaw, onPlayPause, duration, patterns.length]); 8751 const stopGame = reactExports.useCallback(() => { 8752 setIsPlaying(false); 8753 setCountInBeat(null); 8754 if (animationFrameRef.current !== null) { 8755 cancelAnimationFrame(animationFrameRef.current); 8756 animationFrameRef.current = null; 8757 } 8758 if (onPlayPause) { 8759 onPlayPause(false); 8760 } 8761 setCatMood("idle"); 8762 }, [onPlayPause]); 8763 const finishGame = reactExports.useCallback(() => { 8764 stopGame(); 8765 const totalTargets = scheduledNotesRef.current.length || 1; 8766 const totalHits = perfectCount + goodCount; 8767 const rawAccuracy = Math.max(0, Math.min(100, totalHits / totalTargets * 100)); 8768 let avgOffset = 0; 8769 if (timingOffsets.length > 0) { 8770 avgOffset = timingOffsets.reduce((a, b) => a + b, 0) / timingOffsets.length; 8771 } 8772 setGameResult({ 8773 score, 8774 accuracy: rawAccuracy, 8775 perfectCount, 8776 goodCount, 8777 missCount, 8778 maxStreak: maxCombo, 8779 avgOffsetMs: avgOffset, 8780 totalTargetBeats: totalTargets, 8781 bpm, 8782 difficulty 8783 }); 8784 }, [stopGame, score, perfectCount, goodCount, missCount, maxCombo, timingOffsets, bpm, difficulty]); 8785 const handleMeasureClick = reactExports.useCallback((mIdx) => { 8786 if (isPlaying) return; 8787 setPatterns((prev) => { 8788 const next = [...prev]; 8789 if (!next[mIdx]) return prev; 8790 const current = next[mIdx].dots; 8791 const cycle = [1, 2, 4, 3, 8, 0]; 8792 const nextIdx = (cycle.indexOf(current) + 1) % cycle.length; 8793 const newDots = cycle[nextIdx]; 8794 next[mIdx] = { 8795 measureIndex: mIdx, 8796 dots: newDots, 8797 beatOffsets: newDots === 0 ? [] : newDots === 1 ? [0] : newDots === 2 ? [0, 2] : newDots === 4 ? [0, 1, 2, 3] : newDots === 3 ? [0, 1, 2] : [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] 8798 }; 8799 return next; 8800 }); 8801 }, [isPlaying]); 8802 const handleNextPattern = reactExports.useCallback(() => { 8803 const totalM = Math.max(16, Math.ceil((duration || 180) / secPerMeasure)); 8804 setPatterns(generateRhythmPattern(difficulty, totalM)); 8805 setGameResult(null); 8806 }, [difficulty, duration, secPerMeasure]); 8807 const tapTimesRef = reactExports.useRef([]); 8808 const handleTapTempo = reactExports.useCallback(() => { 8809 const now = performance.now(); 8810 const taps = tapTimesRef.current; 8811 taps.push(now); 8812 if (taps.length > 1 && now - taps[taps.length - 2] > 2500) { 8813 tapTimesRef.current = [now]; 8814 return; 8815 } 8816 if (taps.length > 4) taps.shift(); 8817 if (taps.length >= 2) { 8818 let totalDiff = 0; 8819 for (let i = 1; i < taps.length; i++) { 8820 totalDiff += taps[i] - taps[i - 1]; 8821 } 8822 const avgDiffSec = totalDiff / (taps.length - 1) / 1e3; 8823 const calculatedBpm = Math.round(60 / avgDiffSec); 8824 if (calculatedBpm >= 60 && calculatedBpm <= 240) { 8825 setBpm(calculatedBpm); 8826 } 8827 } 8828 }, []); 8829 const totalPages = Math.max(1, Math.ceil(patterns.length / 16)); 8830 const currentPage = Math.min(totalPages - 1, Math.floor(currentMeasureIndex / 16)); 8831 const currentDisplayPatterns = reactExports.useMemo(() => { 8832 const start = currentPage * 16; 8833 return patterns.slice(start, start + 16); 8834 }, [patterns, currentPage]); 8835 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full max-h-screen bg-[#f7f7f8] dark:bg-[#0c0d12] text-zinc-900 dark:text-zinc-100 flex flex-col justify-between overflow-hidden p-2 sm:p-3 md:p-4 select-none font-sans", children: [
8836 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-[500px] mx-auto flex items-center justify-between pb-1.5 border-b border-zinc-200 dark:border-zinc-800 shrink-0", children: [ 8837 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 8838 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-lg sm:text-xl font-black tracking-tight text-zinc-900 dark:text-white", children: locale.gameTitle }), 8839 lastJudgement && isPlaying && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] sm:text-xs font-black tracking-wider uppercase px-2 py-0.5 rounded-full bg-black/10 dark:bg-white/10 ${lastJudgement.color} animate-bounce`, children: lastJudgement.text }) 8840 ] }), 8841 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2.5 text-zinc-600 dark:text-zinc-400", children: [ 8842 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-200/80 dark:bg-zinc-800 rounded-lg p-0.5", children: [
8843 /* @__PURE__ */ jsxRuntimeExports.jsx( 8844 "button", 8845 { 8846 onClick: () => { 8847 setInputMode("keyboard"); 8848 if (micEnabled) handleToggleMic(); 8849 }, 8850 className: `p-1 sm:p-1.5 rounded-md transition-all ${inputMode === "keyboard" ? "bg-white dark:bg-zinc-700 text-purple-600 dark:text-purple-400 shadow-sm font-bold" : "hover:text-zinc-900"}`, 8851 title: "Keyboard Controls (Space / F,J / D,K)", 8852 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 15 }) 8853 } 8854 ), 8855 /* @__PURE__ */ jsxRuntimeExports.jsx( 8856 "button", 8857 { 8858 onClick: () => { 8859 setInputMode("touch"); 8860 if (micEnabled) handleToggleMic(); 8861 }, 8862 className: `p-1 sm:p-1.5 rounded-md transition-all ${inputMode === "touch" ? "bg-white dark:bg-zinc-700 text-purple-600 dark:text-purple-400 shadow-sm font-bold" : "hover:text-zinc-900"}`, 8863 title: "Touch Controls (Fingers on Screen)", 8864 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Smartphone, { size: 15 }) 8865 } 8866 ),
8867 /* @__PURE__ */ jsxRuntimeExports.jsx( 8868 "button", 8869 { 8870 onClick: handleToggleMic, 8871 className: `p-1 sm:p-1.5 rounded-md transition-all ${inputMode === "mic" ? "bg-purple-600 text-white shadow-sm font-bold" : "hover:text-zinc-900"}`, 8872 title: micEnabled ? "Microphone On (Clap / Voice)" : "Enable Microphone Clapping Input", 8873 children: micEnabled ? /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 15, className: "text-amber-300 animate-pulse" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(MicOff, { size: 15 }) 8874 } 8875 ) 8876 ] }), 8877 /* @__PURE__ */ jsxRuntimeExports.jsx( 8878 "button", 8879 { 8880 onClick: () => setShowSettingsModal(true), 8881 className: "p-1 sm:p-1.5 rounded-lg hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors cursor-pointer", 8882 title: "Rhythm Test Settings", 8883 children: /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 16 }) 8884 } 8885 ), 8886 onBackToSongSelect && /* @__PURE__ */ jsxRuntimeExports.jsx( 8887 "button", 8888 { 8889 onClick: onBackToSongSelect, 8890 className: "p-1 sm:p-1.5 rounded-lg hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors cursor-pointer", 8891 title: locale.changeSong, 8892 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 16 }) 8893 } 8894 ) 8895 ] }) 8896 ] }), 8897 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-[500px] mx-auto flex items-center justify-between py-1 text-[11px] font-mono shrink-0", children: [
8898 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 sm:gap-1.5", children: [ 8899 /* @__PURE__ */ jsxRuntimeExports.jsx( 8900 "button", 8901 { 8902 onClick: () => setPlayMode("play-along"), 8903 className: `px-2 py-0.5 rounded font-bold transition-all ${playMode === "play-along" ? "bg-purple-600 text-white" : "text-zinc-500 hover:text-zinc-900 dark:hover:text-white"}`, 8904 children: locale.modePlayAlong 8905 } 8906 ), 8907 /* @__PURE__ */ jsxRuntimeExports.jsx( 8908 "button", 8909 { 8910 onClick: () => setPlayMode("echo"), 8911 className: `px-2 py-0.5 rounded font-bold transition-all ${playMode === "echo" ? "bg-purple-600 text-white" : "text-zinc-500 hover:text-zinc-900 dark:hover:text-white"}`, 8912 children: locale.modeEcho 8913 } 8914 ), 8915 /* @__PURE__ */ jsxRuntimeExports.jsx( 8916 "button", 8917 { 8918 onClick: () => setPlayMode("sandbox"), 8919 className: `px-2 py-0.5 rounded font-bold transition-all ${playMode === "sandbox" ? "bg-purple-600 text-white" : "text-zinc-500 hover:text-zinc-900 dark:hover:text-white"}`, 8920 children: locale.modeSandbox 8921 } 8922 ) 8923 ] }),
8924 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5", children: [ 8925 totalPages > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-purple-600 dark:text-purple-400 font-bold flex items-center gap-1", children: [ 8926 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 11 }), 8927 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 8928 locale.page, 8929 " ", 8930 currentPage + 1, 8931 "/", 8932 totalPages 8933 ] }) 8934 ] }), 8935 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500 font-bold", children: [ 8936 locale.score, 8937 ": ", 8938 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-900 dark:text-white", children: score }) 8939 ] }), 8940 combo > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-amber-500 font-bold animate-pulse", children: [ 8941 combo, 8942 " ", 8943 locale.streak 8944 ] }) 8945 ] }) 8946 ] }), 8947 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1 min-h-0 w-full flex flex-col items-center justify-center my-auto overflow-hidden py-1", children: [
8948 /* @__PURE__ */ jsxRuntimeExports.jsx( 8949 RhythmTestGrid, 8950 { 8951 patterns: currentDisplayPatterns, 8952 currentMeasureIndex, 8953 currentSubBeatOffset, 8954 isPlaying, 8955 isEditorMode: !isPlaying || playMode === "sandbox", 8956 pageOffset: currentPage * 16, 8957 inputMode, 8958 onMeasureClick: handleMeasureClick 8959 } 8960 ), 8961 countInBeat !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm rounded-2xl z-30 animate-in fade-in duration-100", children: [ 8962 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-6xl sm:text-7xl font-black text-amber-400 drop-shadow-[0_0_20px_rgba(251,191,36,0.8)] animate-ping", children: countInBeat }), 8963 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold tracking-widest text-white uppercase mt-4", children: locale.countIn }) 8964 ] }) 8965 ] }), 8966 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-[500px] mx-auto text-[10px] sm:text-[11px] font-mono font-bold text-zinc-500 dark:text-zinc-400 py-0.5 text-center shrink-0 flex items-center justify-center gap-1.5", children: [
8967 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ð¡" }), 8968 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: inputMode === "keyboard" ? locale.tapKeyHint : inputMode === "touch" ? locale.touchHint : locale.micTestClap }) 8969 ] }), 8970 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-[500px] mx-auto flex items-end justify-between pt-1 border-t border-zinc-200 dark:border-zinc-800 shrink-0", children: [ 8971 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-start gap-0.5 pb-0.5", children: [ 8972 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8973 "div", 8974 { 8975 onClick: handleTapTempo, 8976 className: "flex items-center gap-1.5 text-base sm:text-lg font-bold font-mono text-zinc-900 dark:text-zinc-100 hover:text-purple-600 dark:hover:text-purple-400 cursor-pointer transition-colors", 8977 title: "Click repeatedly to Tap Tempo!", 8978 children: [ 8979 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-lg", children: "â©" }),
8980 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "=" }), 8981 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-black text-purple-600 dark:text-purple-400", children: bpm }), 8982 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs uppercase text-zinc-500 ml-0.5", children: locale.bpm }) 8983 ] 8984 } 8985 ), 8986 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [ 8987 /* @__PURE__ */ jsxRuntimeExports.jsx( 8988 "button", 8989 { 8990 onClick: () => setBpm((b) => Math.max(60, b - 5)), 8991 className: "p-1 rounded bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 text-xs transition-colors", 8992 title: "Decrease BPM", 8993 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Minus, { size: 11 }) 8994 } 8995 ), 8996 /* @__PURE__ */ jsxRuntimeExports.jsx( 8997 "button", 8998 { 8999 onClick: () => setBpm((b) => Math.min(240, b + 5)), 9000 className: "p-1 rounded bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 text-xs transition-colors", 9001 title: "Increase BPM", 9002 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 11 }) 9003 } 9004 ),
9005 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9006 "button", 9007 { 9008 onClick: isPlaying ? stopGame : startGame, 9009 className: `ml-2 px-3 py-1 rounded-lg font-bold text-xs tracking-wider flex items-center gap-1.5 transition-all shadow-md active:scale-95 cursor-pointer ${isPlaying ? "bg-rose-500 hover:bg-rose-600 text-white" : "bg-purple-600 hover:bg-purple-500 text-white shadow-purple-500/30"}`, 9010 children: [ 9011 isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 12 }), 9012 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isPlaying ? locale.paused : "START" }) 9013 ] 9014 } 9015 ) 9016 ] }), 9017 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-400 font-mono mt-0.5", children: inputMode === "mic" ? "ðï¸ " + locale.micActive : "â¨ï¸ [SPACE] or [F / J]" }) 9018 ] }), 9019 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-24 h-18 sm:w-28 sm:h-22 md:w-32 md:h-24 -mb-1 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 9020 RhythmBongoCat, 9021 { 9022 leftPawDown, 9023 rightPawDown, 9024 mood: catMood, 9025 combo, 9026 onCatClick: () => { 9027 var _a; 9028 return (_a = inputManagerRef.current) == null ? void 0 : _a.triggerManualHit("center", "touch"); 9029 } 9030 } 9031 ) }) 9032 ] }), 9033 inputMode === "touch" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-[500px] mx-auto grid grid-cols-2 gap-2 mt-1.5 pt-1 border-t border-zinc-200 dark:border-zinc-800/80 shrink-0", children: [
9034 /* @__PURE__ */ jsxRuntimeExports.jsx( 9035 "button", 9036 { 9037 onPointerDown: (e) => { 9038 var _a; 9039 e.preventDefault(); 9040 (_a = inputManagerRef.current) == null ? void 0 : _a.triggerManualHit("left", "touch"); 9041 }, 9042 className: "h-10 sm:h-12 rounded-xl bg-purple-500/20 active:bg-purple-600 border border-purple-400/40 text-purple-700 dark:text-purple-300 active:text-white font-bold text-xs flex items-center justify-center gap-2 select-none shadow-sm active:scale-95 transition-all", 9043 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 9044 "ð¾ ", 9045 locale.instructions ? "LEFT" : "LEFT" 9046 ] }) 9047 } 9048 ), 9049 /* @__PURE__ */ jsxRuntimeExports.jsx( 9050 "button", 9051 { 9052 onPointerDown: (e) => { 9053 var _a; 9054 e.preventDefault(); 9055 (_a = inputManagerRef.current) == null ? void 0 : _a.triggerManualHit("right", "touch"); 9056 }, 9057 className: "h-10 sm:h-12 rounded-xl bg-purple-500/20 active:bg-purple-600 border border-purple-400/40 text-purple-700 dark:text-purple-300 active:text-white font-bold text-xs flex items-center justify-center gap-2 select-none shadow-sm active:scale-95 transition-all", 9058 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 9059 "ð¾ ", 9060 locale.instructions ? "RIGHT" : "RIGHT" 9061 ] }) 9062 } 9063 ) 9064 ] }),
9065 micEnabled && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-14 right-4 z-20 flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-900/90 text-white text-[11px] font-mono border border-zinc-700 shadow-xl", children: [ 9066 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 12, className: "text-amber-400 animate-pulse" }), 9067 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "MIC:" }), 9068 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-16 h-2 rounded-full bg-zinc-700 overflow-hidden relative", children: [ 9069 /* @__PURE__ */ jsxRuntimeExports.jsx( 9070 "div", 9071 { 9072 className: `h-full transition-all duration-75 ${micLevel > micThreshold ? "bg-amber-400" : "bg-emerald-400"}`, 9073 style: { width: `${Math.min(100, micLevel * 100)}%` } 9074 } 9075 ), 9076 /* @__PURE__ */ jsxRuntimeExports.jsx( 9077 "div", 9078 { 9079 className: "absolute top-0 bottom-0 w-0.5 bg-rose-500", 9080 style: { left: `${micThreshold * 100}%` } 9081 } 9082 ) 9083 ] }) 9084 ] }), 9085 gameResult && /* @__PURE__ */ jsxRuntimeExports.jsx( 9086 RhythmTestResults, 9087 { 9088 result: gameResult, 9089 locale, 9090 onRetry: startGame, 9091 onNextPattern: handleNextPattern, 9092 onSelectSong: onBackToSongSelect, 9093 onChangeCabinet: onBackToCabinetSelect 9094 } 9095 ), 9096 showSettingsModal && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-sm bg-zinc-900 border border-zinc-700 rounded-2xl p-5 text-white shadow-2xl", children: [
9097 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-3 border-b border-zinc-800", children: [ 9098 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-base flex items-center gap-2", children: [ 9099 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { size: 16, className: "text-purple-400" }), 9100 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Settings" }) 9101 ] }), 9102 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowSettingsModal(false), className: "text-zinc-400 hover:text-white cursor-pointer", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) }) 9103 ] }), 9104 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-4 mt-4 font-mono text-xs", children: [ 9105 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 9106 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-zinc-400 mb-1.5 block font-bold", children: locale.soundType }),
9107 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 gap-2", children: ["bongo", "metronome", "clap", "woodblock"].map((type) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 9108 "button", 9109 { 9110 onClick: () => { 9111 setSoundType(type); 9112 rhythmAudio.playBeatSound(type, true, true); 9113 }, 9114 className: `p-2 rounded-lg border text-center transition-all cursor-pointer ${soundType === type ? "bg-purple-600 border-purple-400 text-white font-bold" : "bg-zinc-800 border-zinc-700 text-zinc-300 hover:border-zinc-500"}`, 9115 children: [ 9116 type === "bongo" && locale.soundBongo, 9117 type === "metronome" && locale.soundMetronome, 9118 type === "clap" && locale.soundClap, 9119 type === "woodblock" && locale.soundWoodblock 9120 ] 9121 }, 9122 type 9123 )) }) 9124 ] }), 9125 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
9126 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-zinc-400 mb-1.5 block font-bold", children: locale.difficulty }), 9127 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 gap-2", children: ["easy", "medium", "hard", "master"].map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx( 9128 "button", 9129 { 9130 onClick: () => { 9131 setDifficulty(d); 9132 const totalM = Math.max(16, Math.ceil((duration || 180) / secPerMeasure)); 9133 setPatterns(generateRhythmPattern(d, totalM)); 9134 }, 9135 className: `p-2 rounded-lg border text-center transition-all cursor-pointer ${difficulty === d ? "bg-purple-600 border-purple-400 text-white font-bold" : "bg-zinc-800 border-zinc-700 text-zinc-300 hover:border-zinc-500"}`, 9136 children: d.toUpperCase() 9137 }, 9138 d 9139 )) }) 9140 ] }), 9141 micEnabled && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-t border-zinc-800 pt-3 flex flex-col gap-3", children: [
9142 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 9143 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-zinc-400 mb-1", children: [ 9144 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.micSensitivity }), 9145 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 9146 micSensitivity.toFixed(1), 9147 "x" 9148 ] }) 9149 ] }), 9150 /* @__PURE__ */ jsxRuntimeExports.jsx( 9151 "input", 9152 { 9153 type: "range", 9154 min: "0.5", 9155 max: "3.0", 9156 step: "0.1", 9157 value: micSensitivity, 9158 onChange: (e) => setMicSensitivity(parseFloat(e.target.value)), 9159 className: "w-full accent-purple-500 cursor-pointer" 9160 } 9161 ) 9162 ] }), 9163 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 9164 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-zinc-400 mb-1", children: [
9165 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.micThreshold }), 9166 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 9167 Math.round(micThreshold * 100), 9168 "%" 9169 ] }) 9170 ] }), 9171 /* @__PURE__ */ jsxRuntimeExports.jsx( 9172 "input", 9173 { 9174 type: "range", 9175 min: "0.05", 9176 max: "0.80", 9177 step: "0.02", 9178 value: micThreshold, 9179 onChange: (e) => setMicThreshold(parseFloat(e.target.value)), 9180 className: "w-full accent-amber-500 cursor-pointer" 9181 } 9182 ) 9183 ] }) 9184 ] }) 9185 ] }), 9186 /* @__PURE__ */ jsxRuntimeExports.jsx( 9187 "button", 9188 { 9189 onClick: () => setShowSettingsModal(false), 9190 className: "w-full mt-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold transition-all shadow-lg cursor-pointer", 9191 children: "CLOSE" 9192 } 9193 ) 9194 ] }) }),
9195 showMicCalibration && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-sm bg-zinc-900 border border-zinc-700 rounded-2xl p-6 text-white text-center shadow-2xl", children: [ 9196 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 rounded-full bg-purple-500/20 text-purple-400 border border-purple-500/40 mx-auto flex items-center justify-center mb-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 24 }) }), 9197 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-lg", children: locale.micCalibrate }), 9198 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 mt-2 mb-4 leading-relaxed font-mono", children: locale.micTestClap }), 9199 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full h-4 rounded-full bg-zinc-800 overflow-hidden relative border border-zinc-700 my-4", children: [
9200 /* @__PURE__ */ jsxRuntimeExports.jsx( 9201 "div", 9202 { 9203 className: `h-full transition-all duration-75 ${micLevel > micThreshold ? "bg-amber-400" : "bg-purple-500"}`, 9204 style: { width: `${Math.min(100, micLevel * 100)}%` } 9205 } 9206 ), 9207 /* @__PURE__ */ jsxRuntimeExports.jsx( 9208 "div", 9209 { 9210 className: "absolute top-0 bottom-0 w-1 bg-rose-500 shadow-[0_0_8px_red]", 9211 style: { left: `${micThreshold * 100}%` } 9212 } 9213 ) 9214 ] }), 9215 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-3 font-mono text-xs text-left", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 9216 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-zinc-400 block mb-1", children: [ 9217 locale.micThreshold, 9218 ": ", 9219 Math.round(micThreshold * 100), 9220 "%" 9221 ] }), 9222 /* @__PURE__ */ jsxRuntimeExports.jsx( 9223 "input", 9224 { 9225 type: "range", 9226 min: "0.05", 9227 max: "0.80", 9228 step: "0.02", 9229 value: micThreshold, 9230 onChange: (e) => setMicThreshold(parseFloat(e.target.value)), 9231 className: "w-full accent-amber-500 cursor-pointer" 9232 } 9233 ) 9234 ] }) }),
9235 /* @__PURE__ */ jsxRuntimeExports.jsx( 9236 "button", 9237 { 9238 onClick: () => setShowMicCalibration(false), 9239 className: "w-full mt-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold transition-all shadow-lg cursor-pointer", 9240 children: "READY TO PLAY" 9241 } 9242 ) 9243 ] }) }) 9244 ] }); 9245}; 9246const GameArena = ({ 9247 module, 9248 selectedSong, 9249 gameSettings, 9250 currentUser, 9251 onBackToCabinetSelect, 9252 onBackToSongSelect 9253}) => { 9254 var _a; 9255 const arenaContainerRef = reactExports.useRef(null); 9256 const audioRef = reactExports.useRef(null); 9257 const effectiveOpusPath = reactExports.useMemo(() => { 9258 if (selectedSong.opusPath) return selectedSong.opusPath; 9259 if (selectedSong.source === "site" && selectedSong.id) { 9260 const numId = parseInt(selectedSong.id); 9261 const group = numId >= 1e7 ? numId % 1e3 : Math.floor(numId / 1e3); 9262 return `/storage/tracks/opus/${group}/${selectedSong.id}.opus`; 9263 } 9264 return void 0; 9265 }, [selectedSong.opusPath, selectedSong.source, selectedSong.id]); 9266 const effectivePianoRollPath = reactExports.useMemo(() => { 9267 if (selectedSong.pianoRollPath) return selectedSong.pianoRollPath; 9268 if (selectedSong.source === "site" && selectedSong.id) { 9269 const numId = parseInt(selectedSong.id); 9270 const group = numId >= 1e7 ? numId % 1e3 : Math.floor(numId / 1e3); 9271 return `/storage/tracks/piano_rolls/${group}/${selectedSong.id}.json`; 9272 } 9273 return void 0; 9274 }, [selectedSong.pianoRollPath, selectedSong.source, selectedSong.id]); 9275 const effectiveMelodyPath = reactExports.useMemo(() => { 9276 var _a2; 9277 if (selectedSong.melodyPath) return selectedSong.melodyPath; 9278 if ((_a2 = selectedSong.rawTrack) == null ? void 0 : _a2.melodyPath) return selectedSong.rawTrack.melodyPath; 9279 if (selectedSong.source === "site" && selectedSong.id) { 9280 const numId = parseInt(selectedSong.id); 9281 const group = numId >= 1e7 ? numId % 1e3 : Math.floor(numId / 1e3); 9282 return `/storage/tracks/melody/${group}/${selectedSong.id}_standard.json`; 9283 } 9284 return void 0; 9285 }, [selectedSong.melodyPath, (_a = selectedSong.rawTrack) == null ? void 0 : _a.melodyPath, selectedSong.source, selectedSong.id]); 9286 const { t } = useTranslation(); 9287 const [copiedShare, setCopiedShare] = reactExports.useState(false); 9288 const normalizedTimeSignature = reactExports.useMemo(() => { 9289 if (selectedSong.timeSignature && selectedSong.timeSignature.length >= 2) { 9290 return [Number(selectedSong.timeSignature[0]) || 4, Number(selectedSong.timeSignature[1]) || 4]; 9291 } 9292 return [4, 4]; 9293 }, [selectedSong.timeSignature]); 9294 const [isPlaying, setIsPlaying] = reactExports.useState(false); 9295 const [isPaused, setIsPaused] = reactExports.useState(false); 9296 const [currentTime, setCurrentTime] = reactExports.useState(0); 9297 const [isFullscreen, setIsFullscreen] = reactExports.useState(false); 9298 const [isAudioLoaded, setIsAudioLoaded] = reactExports.useState(false); 9299 const [isLoadingAudio, setIsLoadingAudio] = reactExports.useState(!!effectiveOpusPath); 9300 const [audioBlocked, setAudioBlocked] = reactExports.useState(false); 9301 reactExports.useEffect(() => { 9302 if (typeof window === "undefined") return; 9303 try { 9304 const url = new URL(window.location.href); 9305 url.searchParams.set("track", selectedSong.id); 9306 url.searchParams.set("mode", module.id); 9307 window.history.replaceState(null, "", url.toString()); 9308 } catch { 9309 } 9310 }, [selectedSong.id, module.id]); 9311 const handleShareGame = reactExports.useCallback(() => { 9312 arcadeAudio.playDecide(); 9313 if (typeof window === "undefined") return; 9314 const shareUrl = `${window.location.origin}/games?track=${selectedSong.id}&mode=${module.id}`; 9315 if (navigator.clipboard && navigator.clipboard.writeText) { 9316 navigator.clipboard.writeText(shareUrl).then(() => { 9317 setCopiedShare(true); 9318 setTimeout(() => setCopiedShare(false), 2500); 9319 }).catch(() => { 9320 }); 9321 } 9322 }, [selectedSong.id, module.id]); 9323 const [beatmapData, setBeatmapData] = reactExports.useState(selectedSong.pianoRollData || []); 9324 const [melodyBeatmap, setMelodyBeatmap] = reactExports.useState(selectedSong.melodyData || []); 9325 const [isLoadingBeatmap, setIsLoadingBeatmap] = reactExports.useState(!selectedSong.pianoRollData && !!effectivePi
9325anoRollPath); 9326 reactExports.useEffect(() => { 9327 arcadeAudio.pauseBgm(); 9328 return () => { 9329 arcadeAudio.resumeBgm(); 9330 }; 9331 }, []); 9332 reactExports.useEffect(() => { 9333 var _a2, _b, _c; 9334 let isCurrent = true; 9335 if (!selectedSong.pianoRollData && effectivePianoRollPath) { 9336 setIsLoadingBeatmap(true); 9337 fetch(effectivePianoRollPath).then((res) => { 9338 if (!res.ok) throw new Error(`HTTP ${res.status}`); 9339 return res.json(); 9340 }).then((data) => { 9341 if (!isCurrent) return; 9342 setBeatmapData(Array.isArray(data) ? data : []); 9343 setIsLoadingBeatmap(false); 9344 }).catch((err) => { 9345 console.warn("[GameArena] Could not fetch pianoRollPath:", err); 9346 if (isCurrent) setIsLoadingBeatmap(false); 9347 }); 9348 } else if (selectedSong.pianoRollData) { 9349 setBeatmapData(selectedSong.pianoRollData); 9350 setIsLoadingBeatmap(false); 9351 } else { 9352 setIsLoadingBeatmap(false); 9353 } 9354 const melodyUrl = effectiveMelodyPath || selectedSong.melodyPath || ((_b = (_a2 = selectedSong.rawTrack) == null ? void 0 : _a2.quantizedScorePaths) == null ? void 0 : _b.studio_melody) || ((_c = selectedSong.rawTrack) == null ? void 0 : _c.melodyPath); 9355 if (!selectedSong.melodyData && melodyUrl) { 9356 fetch(melodyUrl).then((res) => { 9357 if (!res.ok) throw new Error(`HTTP ${res.status}`); 9358 return res.json(); 9359 }).then((data) => { 9360 if (isCurrent && Array.isArray(data)) { 9361 setMelodyBeatmap(data); 9362 } 9363 }).catch(() => { 9364 }); 9365 } else if (selectedSong.melodyData) { 9366 setMelodyBeatmap(selectedSong.melodyData); 9367 } 9368 return () => { 9369 isCurrent = false; 9370 }; 9371 }, [selectedSong.pianoRollData, effectivePianoRollPath, selectedSong.rawTrack, selectedSong.melodyPath, selectedSong.melodyData, effectiveMelodyPath]); 9372 const derivedColorMap = reactExports.useMemo(() => { 9373 const map = {}; 9374 const palette = [ 9375 "#06b6d4", 9376 "#ec4899", 9377 "#8b5cf6", 9378 "#f59e0b", 9379 "#10b981", 9380 "#3b82f6", 9381 "#f43f5e", 9382 "#84cc16" 9383 ]; 9384 (selectedSong.instruments || []).forEach((inst, idx) => { 9385 map[inst.name] = palette[idx % palette.length]; 9386 }); 9387 return map; 9388 }, [selectedSong.instruments]); 9389 const uniqueNames = reactExports.useMemo(() => { 9390 return (selectedSong.instruments || []).map((i) => i.name); 9391 }, [selectedSong.instruments]); 9392 const selectedInstruments = reactExports.useMemo(() => selectedSong.instruments || [], [selectedSong.instruments]); 9393 const meowMidiMetadata = reactExports.useMemo(() => ({ 9394 title: selectedSong.title, 9395 artist: selectedSong.artist, 9396 tempo: selectedSong.bpm, 9397 timeSignature: (selectedSong.timeSignature || [4, 4]).join("/") 9398 }), [selectedSong.title, selectedSong.artist, selectedSong.bpm, selectedSong.timeSignature]); 9399 const lastTickRef = reactExports.useRef(performance.now()); 9400 const isPlayingRef = reactExports.useRef(isPlaying); 9401 isPlayingRef.current = isPlaying; 9402 reactExports.useEffect(() => { 9403 if (effectiveOpusPath) return; 9404 lastTickRef.current = performance.now(); 9405 let animationFrameId; 9406 const loop = (now) => { 9407 if (isPlayingRef.current && !isPaused) { 9408 const deltaSec = (now - lastTickRef.current) / 1e3; 9409 setCurrentTime((prev) => { 9410 const nextTime = prev + deltaSec; 9411 if (nextTime >= selectedSong.duration) { 9412 setIsPlaying(false); 9413 return selectedSong.duration; 9414 } 9415 return nextTime; 9416 }); 9417 } 9418 lastTickRef.current = now; 9419 animationFrameId = requestAnimationFrame(loop); 9420 }; 9421 animationFrameId = requestAnimationFrame(loop); 9422 return () => cancelAnimationFrame(animationFrameId); 9423 }, [effectiveOpusPath, selectedSong.duration, isPaused]); 9424 const handleAudioTimeUpdate = () => { 9425 if (audioRef.current) { 9426 setCurrentTime(audioRef.current.currentTime); 9427 } 9428 }; 9429 const handleAudioLoadedMetadata = () => { 9430 setIsAudioLoaded(true); 9431 setIsLoadingAudio(false); 9432 }; 9433 const handleAudioEnded = () => { 9434 setIsPlaying(false); 9435 setIsPaused(false); 9436 }; 9437 const [restartKey, setRestartKey] = reactExports.useState(0); 9438 const handleTogglePlay = reactExports.useCallback((forcePlay, openPauseModal = false) => { 9439 arcadeAudio.playSelect(); 9440 const targetState = forcePlay !== void 0 ? forcePlay : !isPlaying;
9441 if (targetState) { 9442 setIsPlaying(true); 9443 setIsPaused(false); 9444 if (audioRef.current) { 9445 audioRef.current.play().catch((err) => { 9446 console.warn("[GameArena] Audio playback blocked by browser autoplay policy:", err); 9447 setAudioBlocked(true); 9448 }); 9449 } 9450 } else { 9451 setIsPlaying(false); 9452 if (openPauseModal) { 9453 setIsPaused(true); 9454 } 9455 if (audioRef.current) { 9456 audioRef.current.pause(); 9457 } 9458 } 9459 }, [isPlaying]); 9460 const handleResume = reactExports.useCallback(() => { 9461 arcadeAudio.playSelect(); 9462 setIsPaused(false); 9463 setIsPlaying(true); 9464 setAudioBlocked(false); 9465 if (audioRef.current) { 9466 audioRef.current.play().catch((err) => { 9467 console.warn("[GameArena] Audio play error on resume:", err); 9468 }); 9469 } 9470 }, []); 9471 const handleSeek = reactExports.useCallback((targetTime) => { 9472 const clamped = Math.max(0, Math.min(targetTime, selectedSong.duration)); 9473 setCurrentTime(clamped); 9474 if (audioRef.current) { 9475 audioRef.current.currentTime = clamped; 9476 } 9477 }, [selectedSong.duration]); 9478 const handleRestart = reactExports.useCallback(() => { 9479 arcadeAudio.playStageStart(); 9480 setCurrentTime(0); 9481 setIsPaused(false); 9482 setIsPlaying(false); 9483 setAudioBlocked(false); 9484 if (audioRef.current) { 9485 audioRef.current.currentTime = 0; 9486 audioRef.current.pause(); 9487 } 9488 setRestartKey((prev) => prev + 1); 9489 }, []); 9490 const handleToggleFullscreen = () => { 9491 var _a2, _b, _c; 9492 arcadeAudio.playSelect(); 9493 if (!document.fullscreenElement) { 9494 (_b = (_a2 = arenaContainerRef.current) == null ? void 0 : _a2.requestFullscreen) == null ? void 0 : _b.call(_a2).then(() => setIsFullscreen(true)).catch(() => { 9495 }); 9496 } else { 9497 (_c = document.exitFullscreen) == null ? void 0 : _c.call(document).then(() => setIsFullscreen(false)).catch(() => { 9498 }); 9499 } 9500 }; 9501 reactExports.useEffect(() => { 9502 const handleKeyDown = (e) => { 9503 const target = e.target; 9504 if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) return; 9505 if (e.code === "Escape" || e.code === "KeyP") { 9506 e.preventDefault(); 9507 e.stopPropagation(); 9508 if (isPaused) { 9509 handleResume(); 9510 } else { 9511 handleTogglePlay(false, true); 9512 } 9513 } else if (e.code === "Space") { 9514 if (module.id === "rhythm-test") { 9515 return; 9516 } 9517 e.preventDefault(); 9518 if (isPaused) { 9519 handleResume(); 9520 } else if (isPlaying) { 9521 handleTogglePlay(false, true); 9522 } else { 9523 handleTogglePlay(true, false); 9524 } 9525 } else if (e.key === "r" || e.key === "R") { 9526 e.preventDefault(); 9527 handleRestart(); 9528 } 9529 }; 9530 window.addEventListener("keydown", handleKeyDown); 9531 return () => window.removeEventListener("keydown", handleKeyDown); 9532 }, [isPaused, isPlaying, handleResume, handleTogglePlay, handleRestart, module.id]); 9533 const isAudioReady = (isAudioLoaded || !effectiveOpusPath) && !isLoadingBeatmap; 9534 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 9535 "div", 9536 { 9537 ref: arenaContainerRef, 9538 className: `w-full h-full max-h-[calc(100vh-56px)] flex flex-col bg-black text-white relative select-none rounded-2xl overflow-hidden border-2 border-cyan-500/50 shadow-[0_0_50px_rgba(6,182,212,0.3)] ${isFullscreen ? "fixed inset-0 z-50 h-screen w-screen rounded-none border-none" : ""}`, 9539 children: [
9540 effectiveOpusPath && /* @__PURE__ */ jsxRuntimeExports.jsx( 9541 "audio", 9542 { 9543 ref: audioRef, 9544 src: effectiveOpusPath, 9545 preload: "auto", 9546 onCanPlayThrough: handleAudioLoadedMetadata, 9547 onLoadedMetadata: handleAudioLoadedMetadata, 9548 onTimeUpdate: handleAudioTimeUpdate, 9549 onEnded: handleAudioEnded, 9550 onError: (err) => { 9551 console.warn("[GameArena] Opus audio load error:", err); 9552 setIsAudioLoaded(true); 9553 setIsLoadingAudio(false); 9554 } 9555 } 9556 ), 9557 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-30 w-full py-1.5 px-3 bg-slate-950/95 border-b-2 border-cyan-500/40 backdrop-blur-md flex items-center justify-between gap-3 font-mono text-xs", children: [ 9558 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9559 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9560 "button", 9561 { 9562 onClick: handleRestart, 9563 className: "flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-900 border border-slate-700 hover:border-amber-400 text-amber-300 hover:text-amber-200 transition-all active:scale-95 cursor-pointer text-xs font-mono font-bold", 9564 title: "Restart Stage [R]", 9565 children: [
9566 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 13 }), 9567 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "RESTART [R]" }) 9568 ] 9569 } 9570 ), 9571 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9572 "button", 9573 { 9574 onClick: handleShareGame, 9575 className: `flex items-center gap-1.5 px-2.5 py-1 rounded-lg border transition-all active:scale-95 cursor-pointer ${copiedShare ? "bg-emerald-500/20 border-emerald-400 text-emerald-300 shadow-[0_0_10px_rgba(16,185,129,0.3)]" : "bg-slate-900 border-slate-700 hover:border-cyan-400 text-slate-300 hover:text-white"}`, 9576 title: "Share Game Link", 9577 children: [ 9578 copiedShare ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 13, className: "text-emerald-400" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { size: 13, className: "text-cyan-400" }), 9579 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline text-xs font-bold font-mono", children: copiedShare ? t("gameCenter.copiedToClipboard", "COPIED!") : t("gameCenter.shareGame", "SHARE") }) 9580 ] 9581 } 9582 ) 9583 ] }),
9584 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 text-center", children: [ 9585 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 px-3 py-1 rounded-full bg-slate-900 border border-cyan-500/40 text-cyan-300 font-bold shadow-[0_0_10px_rgba(6,182,212,0.2)]", children: [ 9586 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `inline-block w-2 h-2 rounded-full ${isPlaying ? "bg-emerald-400 animate-pulse" : "bg-amber-400"}` }), 9587 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "line-clamp-1 max-w-[160px] sm:max-w-xs", children: selectedSong.title }) 9588 ] }), 9589 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-amber-400 font-mono font-bold tracking-widest hidden sm:block", children: [ 9590 formatArcadeDuration(currentTime), 9591 " / ", 9592 formatArcadeDuration(selectedSong.duration) 9593 ] }) 9594 ] }), 9595 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
9596 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9597 "button", 9598 { 9599 onClick: () => { 9600 if (isPlaying) { 9601 handleTogglePlay(false, true); 9602 } else { 9603 handleTogglePlay(true, false); 9604 } 9605 }, 9606 className: `flex items-center gap-1 px-3 py-1 rounded-lg font-black transition-all active:scale-95 ${isPlaying ? "bg-cyan-500 hover:bg-cyan-400 text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.5)]" : "bg-amber-500 hover:bg-amber-400 text-slate-950 shadow-[0_0_15px_rgba(245,158,11,0.5)]"}`, 9607 title: isPlaying ? "Pause [Space / Esc]" : "Play [Space]", 9608 children: [ 9609 isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 13 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 13 }), 9610 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: isPlaying ? "PAUSE" : "PLAY" }) 9611 ] 9612 } 9613 ),
9614 /* @__PURE__ */ jsxRuntimeExports.jsx( 9615 "button", 9616 { 9617 onClick: handleToggleFullscreen, 9618 className: "p-1 rounded-lg bg-slate-900 border border-slate-700 hover:border-slate-500 text-slate-300 hover:text-white transition-all", 9619 title: isFullscreen ? "Exit Fullscreen" : "Fullscreen", 9620 children: isFullscreen ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { size: 14 }) 9621 } 9622 ) 9623 ] }) 9624 ] }), 9625 isLoadingBeatmap && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 z-40 bg-black/80 flex flex-col items-center justify-center gap-3 pointer-events-none", children: [ 9626 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-10 h-10 text-cyan-400 animate-spin" }), 9627 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-black tracking-widest text-cyan-300 uppercase animate-pulse", children: "TUNING RHYTHM BEATMAP..." }) 9628 ] }),
9629 audioBlocked && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-14 left-1/2 -translate-x-1/2 z-40 bg-gradient-to-r from-amber-500 to-pink-500 text-slate-950 px-4 py-2 rounded-xl font-black text-xs tracking-wider shadow-[0_0_25px_rgba(245,158,11,0.8)] flex items-center gap-3 animate-bounce", children: [ 9630 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 16 }), 9631 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "BROWSER AUDIO MUTED: CLICK TO START SOUND" }), 9632 /* @__PURE__ */ jsxRuntimeExports.jsx( 9633 "button", 9634 { 9635 onClick: handleResume, 9636 className: "px-2.5 py-1 rounded bg-black text-white text-[10px] font-mono hover:bg-zinc-900 transition-colors uppercase cursor-pointer", 9637 children: "ENABLE AUDIO" 9638 } 9639 ) 9640 ] }), 9641 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 w-full relative overflow-hidden bg-[#030712]", children: [ 9642 module.id === "piano-roll" && /* @__PURE__ */ jsxRuntimeExports.jsx( 9643 PianoRollGame, 9644 { 9645 data: beatmapData, 9646 melodyData: melodyBeatmap, 9647 currentTime, 9648 isPlaying: isPlaying && !isPaused, 9649 hiddenTracks: /* @__PURE__ */ new Set(), 9650 colorMap: derivedColorMap, 9651 processedInstruments: selectedSong.instruments || [], 9652 uniqueNames, 9653 onPlayPause: (force) => handleTogglePlay(force), 9654 onSeek: handleSeek, 9655 toggleGameMode: () => handleTogglePlay(false), 9656 duration: selectedSong.duration, 9657 tempo: selectedSong.bpm, 9658 timeSignature: normalizedTimeSignature, 9659 trackId: selectedSong.id, 9660 userId: currentUser == null ? void 0 : currentUser.id, 9661 userName: currentUser == null ? void 0 : currentUser.username, 9662 track: selectedSong.rawTrack || {}, 9663 isAudioLoaded: isAudioReady, 9664 isLoadingAudio: !isAudioReady 9665 } 9666 ), 9667 module.id === "taiko" && /* @__PURE__ */ jsxRuntimeExports.jsx( 9668 TaikoGame, 9669 { 9670 data: beatmapData, 9671 melodyData: melodyBeatmap, 9672 processedInstruments: selectedSong.instruments || [], 9673 midiMetadata: { 9674 tempo: selectedSong.bpm, 9675 timeSignature: (selectedSong.timeSignature || [4, 4]).join("/") 9676 }, 9677 currentTime, 9678 isPlaying: isPlaying && !isPaused, 9679 duration: selectedSong.duration, 9680 tempo: selectedSong.bpm, 9681 timeSignature: normalizedTimeSignature, 9682 trackId: selectedSong.id, 9683 userId: currentUser == null ? void 0 : currentUser.id, 9684 userName: currentUser == null ? void 0 : currentUser.username, 9685 onPlayPause: (force) => handleTogglePlay(force), 9686 onSeek: handleSeek, 9687 theme: "dark", 9688 isAudioLoaded: isAudioReady, 9689 isLoadingAudio: !isAudioReady 9690 } 9691 ), 9692 module.id === "meow-choir" && /* @__PURE__ */ jsxRuntimeExports.jsx( 9693 MeowChoirGame, 9694 { 9695 restartKey, 9696 data: beatmapData, 9697 melodyData: melodyBeatmap, 9698 processedInstruments: selectedInstruments, 9699 midiMetadata: meowMidiMetadata, 9700 currentTime, 9701 isPlaying: isPlaying && !isPaused, 9702 duration: selectedSong.duration, 9703 tempo: selectedSong.bpm, 9704 timeSignature: normalizedTimeSignature, 9705 trackId: selectedSong.id, 9706 userId: currentUser == null ? void 0 : currentUser.id, 9707 userName: currentUser == null ? void 0 : currentUser.username, 9708 onPlayPause: (force) => handleTogglePlay(force, false), 9709 onSeek: handleSeek, 9710 isAudioLoaded: isAudioReady, 9711 isLoadingAudio: !isAudioReady, 9712 difficulty: (gameSettings == null ? void 0 : gameSettings.difficulty) || "playful" 9713 } 9714 ),
9715 module.id === "rhythm-test" && /* @__PURE__ */ jsxRuntimeExports.jsx( 9716 RhythmTestGame, 9717 { 9718 data: beatmapData, 9719 melodyData: melodyBeatmap, 9720 processedInstruments: selectedSong.instruments || [], 9721 midiMetadata: { 9722 title: selectedSong.title, 9723 artist: selectedSong.artist, 9724 tempo: selectedSong.bpm, 9725 timeSignature: (selectedSong.timeSignature || [4, 4]).join("/") 9726 }, 9727 currentTime, 9728 isPlaying: isPlaying && !isPaused, 9729 duration: selectedSong.duration, 9730 tempo: selectedSong.bpm, 9731 timeSignature: normalizedTimeSignature, 9732 trackId: selectedSong.id, 9733 userId: currentUser == null ? void 0 : currentUser.id, 9734 userName: currentUser == null ? void 0 : currentUser.username, 9735 onPlayPause: (force) => handleTogglePlay(force), 9736 onSeek: handleSeek, 9737 isAudioLoaded: isAudioReady, 9738 isLoadingAudio: !isAudioReady, 9739 onBackToSongSelect, 9740 onBackToCabinetSelect 9741 } 9742 ) 9743 ] }),
9744 isPaused && /* @__PURE__ */ jsxRuntimeExports.jsx( 9745 "div", 9746 { 9747 "data-arcade-pause-modal": "true", 9748 className: "absolute inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in duration-150", 9749 onClick: (e) => { 9750 if (e.target === e.currentTarget) handleResume(); 9751 }, 9752 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-md w-full bg-slate-950 border-4 border-cyan-400 rounded-3xl p-6 shadow-[0_0_60px_rgba(6,182,212,0.5)] flex flex-col items-center gap-5 text-center font-mono select-none", children: [ 9753 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-1", children: [ 9754 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-black text-cyan-400 tracking-[0.3em] uppercase", children: "STAGE INTERRUPTED" }), 9755 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-4xl font-black italic tracking-widest text-transparent bg-clip-text bg-gradient-to-r from-yellow-300 via-pink-400 to-cyan-400 drop-shadow-[0_0_20px_rgba(236,72,153,0.8)] animate-pulse", children: "PAUSED" }),
9756 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-bold truncate max-w-xs mt-1", children: selectedSong.title }) 9757 ] }), 9758 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex flex-col gap-3", children: [ 9759 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9760 "button", 9761 { 9762 onClick: handleResume, 9763 className: "w-full py-3 px-4 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-slate-950 font-black text-sm tracking-widest uppercase shadow-[0_0_25px_rgba(6,182,212,0.6)] active:scale-95 transition-all flex items-center justify-center gap-2 cursor-pointer", 9764 children: [ 9765 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 16, className: "fill-current" }), 9766 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "RESUME PLAY [ESC / SPACE]" }) 9767 ] 9768 } 9769 ),
9770 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9771 "button", 9772 { 9773 onClick: handleRestart, 9774 className: "w-full py-2.5 px-4 rounded-xl bg-slate-900 border border-amber-500/50 hover:border-amber-400 text-amber-300 hover:text-white font-black text-xs tracking-wider uppercase transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer", 9775 children: [ 9776 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 14 }), 9777 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "RESTART STAGE [R]" }) 9778 ] 9779 } 9780 ), 9781 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9782 "button", 9783 { 9784 onClick: handleShareGame, 9785 className: `w-full py-2.5 px-4 rounded-xl border font-black text-xs tracking-wider uppercase transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer ${copiedShare ? "bg-emerald-500/20 border-emerald-400 text-emerald-300 shadow-[0_0_15px_rgba(16,185,129,0.4)]" : "bg-slate-900 border-cyan-500/50 hover:border-cyan-400 text-cyan-300 hover:text-white"}`, 9786 children: [
9787 copiedShare ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14, className: "text-emerald-400" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { size: 14, className: "text-cyan-400" }), 9788 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: copiedShare ? t("gameCenter.copiedToClipboard", "LINK COPIED TO CLIPBOARD!") : t("gameCenter.shareGame", "SHARE GAME LINK") }) 9789 ] 9790 } 9791 ), 9792 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2 mt-1", children: [ 9793 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9794 "button", 9795 { 9796 onClick: () => { 9797 if (audioRef.current) audioRef.current.pause(); 9798 arcadeAudio.playSelect(); 9799 onBackToCabinetSelect(); 9800 }, 9801 className: "py-2.5 px-3 rounded-xl bg-slate-900 border border-slate-700 hover:border-cyan-400 text-slate-300 hover:text-white font-bold text-xs tracking-wider transition-all active:scale-95 flex items-center justify-center gap-1.5 cursor-pointer", 9802 children: [
9803 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 13 }), 9804 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "CABINETS" }) 9805 ] 9806 } 9807 ), 9808 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9809 "button", 9810 { 9811 onClick: () => { 9812 if (audioRef.current) audioRef.current.pause(); 9813 arcadeAudio.playSelect(); 9814 onBackToSongSelect(); 9815 }, 9816 className: "py-2.5 px-3 rounded-xl bg-slate-900 border border-slate-700 hover:border-pink-400 text-slate-300 hover:text-white font-bold text-xs tracking-wider transition-all active:scale-95 flex items-center justify-center gap-1.5 cursor-pointer", 9817 children: [ 9818 /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { size: 13, className: "text-pink-400" }), 9819 /* @__P
9819URE__ */ jsxRuntimeExports.jsx("span", { children: "SONG WHEEL" }) 9820 ] 9821 } 9822 ) 9823 ] }) 9824 ] }), 9825 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 font-bold uppercase tracking-widest pt-1 border-t border-zinc-900 w-full", children: "ESC / SPACE TO RESUME ⢠R TO RESTART" }) 9826 ] }) 9827 } 9828 ) 9829 ] 9830 } 9831 ); 9832}; 9833function HamieGameCenterPage({ 9834 currentUser, 9835 onNavigate 9836}) { 9837 const [stage, setStage] = reactExports.useState("SONG"); 9838 const [selectedSong, setSelectedSong] = reactExports.useState(null); 9839 const [selectedModule, setSelectedModule] = reactExports.useState(GAME_MODULES[0]); 9840 const [gameSettings, setGameSettings] = reactExports.useState({}); 9841 const [enableScanlines, setEnableScanlines] = reactExports.useState(true); 9842 const [themeMode, setThemeMode] = reactExports.useState("random"); 9843 const [currentThemeId, setCurrentThemeId] = reactExports.useState(() => getRandomTheme()); 9844 const activeTheme = ARCADE_THEMES[currentThemeId] || ARCADE_THEMES["5thmix"]; 9845 const handleCycleTheme = reactExports.useCallback(() => { 9846 arcadeAudio.playWheelTick(); 9847 if (themeMode === "random") { 9848 setThemeMode("5thmix"); 9849 setCurrentThemeId("5thmix"); 9850 } else if (themeMode === "5thmix") { 9851 setThemeMode("max2"); 9852 setCurrentThemeId("max2"); 9853 } else if (themeMode === "max2") { 9854 setThemeMode("sunset"); 9855 setCurrentThemeId("sunset"); 9856 } else { 9857 setThemeMode("random"); 9858 setCurrentThemeId(getRandomTheme()); 9859 } 9860 }, [themeMode]); 9861 const handleSelectTheme = reactExports.useCallback((mode) => { 9862 arcadeAudio.playWheelTick(); 9863 setThemeMode(mode); 9864 if (mode === "random") { 9865 setCurrentThemeId(getRandomTheme()); 9866 } else { 9867 setCurrentThemeId(mode); 9868 } 9869 }, []); 9870 reactExports.useEffect(() => { 9871 if (typeof window === "undefined") return; 9872 const params = new URLSearchParams(window.location.search); 9873 const trackId = params.get("track") || params.get("id"); 9874 const mode = params.get("mode"); 9875 if (trackId) { 9876 api.getTrack(trackId).then((track) => { 9877 var _a, _b, _c; 9878 if (track) { 9879 const durSec = parseDurationSec(track.durationSec || track.duration); 9880 const numId = parseInt(String(track.id)); 9881 const group = numId >= 1e7 ? numId % 1e3 : Math.floor(numId / 1e3); 9882 const resolvedPianoRoll = typeof track.pianoRollPath === "string" && track.pianoRollPath ? track.pianoRollPath : track.status === "completed" ? `/storage/tracks/piano_rolls/${group}/${track.id}.json` : void 0; 9883 const resolvedOpus = typeof track.opusPath === "string" && track.opusPath ? track.opusPath : track.status === "completed" ? `/storage/tracks/opus/${group}/${track.id}.opus` : void 0; 9884 const songData = { 9885 source: "site", 9886 id: String(track.id), 9887 title: track.title, 9888 artist: track.artist || void 0, 9889 category: typeof track.category === "string" ? track.category : ((_a = track.category) == null ? void 0 : _a.label) || "General", 9890 bpm: Math.round(((_b = track.midiMetadata) == null ? void 0 : _b.tempo) || 120), 9891 duration: durSec, 9892 timeSignature: ((_c = track.midiMetadata) == null ? void 0 : _c.timeSignature) ? track.midiMetadata.timeSignature.split("/").map(Number) : [4, 4], 9893 instruments: track.instruments || [], 9894 midiPath: track.midiPath || void 0, 9895 pianoRollPath: resolvedPianoRoll, 9896 opusPath: resolvedOpus, 9897 difficultyStars: 3, 9898 rawTrack: track 9899 }; 9900 setSelectedSong(songData); 9901 if (resolvedPianoRoll) { 9902 fetch(resolvedPianoRoll).then((res) => res.json()).then((data) => { 9903 if (Array.isArray(data)) { 9904 setSelectedSong((prev) => prev ? { ...prev, pianoRollData: data } : null); 9905 } 9906 }).catch(() => { 9907 }); 9908 } 9909 if (mode) { 9910 const targetModule = GAME_MODULES.find((m) => m.id === mode); 9911 if (targetModule) { 9912 setSelectedModule(targetModule); 9913 setStage("ARENA"); 9914 return; 9915 } 9916 } 9917 setStage("CABINET"); 9918 } 9919 }).catch(() => { 9920 }); 9921 } 9922 }, []); 9923 const handleLaunchGame = (module, settings) => { 9924 setSelectedModule(module); 9925 setGameSettings(settings); 9926 setStage("ARENA"); 9927 }; 9928 reactExports.useEffect(() => { 9929 if (typeof window === "undefined") return; 9930 try { 9931 if (stage === "ARENA" && selectedSong) { 9932 window.history.replaceState(null, "", `/games?track=${selectedSong.id}&mode=${selectedModule.id}`); 9933 } else if (stage === "CABINET" && selectedSong) { 9934 window.history.replaceState(null, "", `/games?track=${selectedSong.id}`); 9935 } else if (stage === "SONG") { 9936 window.history.replaceState(null, "", "/games"); 9937 } 9938 } catch { 9939 } 9940 }, [stage, selectedSong == null ? void 0 : selectedSong.id, selectedModule.id]); 9941 const handleQuickSwitchModule = (module) => { 9942 setSelectedModule(module); 9943 setGameSettings(module.defaultSettings || {}); 9944 }; 9945 reactExports.useEffect(() => { 9946 const handleGlobalKeyDown = (e) => { 9947 var _a; 9948 const targetTag = (_a = e.target) == null ? void 0 : _a.tagName; 9949 if (targetTag === "INPUT" || targetTag === "TEXTAREA") return; 9950 if (e.key === "Escape") { 9951 if (stage === "CABINET") { 9952 e.preventDefault(); 9953 setStage("SONG"); 9954 } 9955 } else if (e.key === "1") { 9956 e.preventDefault(); 9957 setStage("SONG"); 9958 } else if (e.key === "2" && selectedSong) { 9959 e.preventDefault(); 9960 setStage("CABINET"); 9961 } else if (e.key === "3" && selectedSong) { 9962 e.preventDefault(); 9963 setStage("ARENA"); 9964 } 9965 }; 9966 window.addEventListener("keydown", handleGlobalKeyDown); 9967 return () => window.removeEventListener("keydown", handleGlobalKeyDown); 9968 }, [stage, selectedSong]); 9969 reactExports.useEffect(() => { 9970 arcadeAudio.enableBgm(); 9971 return () => { 9972 arcadeAudio.stopBgm(); 9973 }; 9974 }, []); 9975 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-screen max-h-screen w-full bg-[#030712] text-white flex flex-col overflow-hidden selection:bg-cyan-500 selection:text-black", children: [
9976 /* @__PURE__ */ jsxRuntimeExports.jsx(ArcadeBackground, { enableScanlines, theme: activeTheme, isActive: stage !== "ARENA" }), 9977 /* @__PURE__ */ jsxRuntimeExports.jsx( 9978 ArcadeMarqueeHeader, 9979 { 9980 stage, 9981 selectedSong, 9982 selectedModule, 9983 onGoToStage: (targetStage) => { 9984 if (targetStage === "ARENA" && !selectedSong) return; 9985 if (targetStage === "CABINET" && !selectedSong) return; 9986 setStage(targetStage); 9987 }, 9988 onSelectModule: handleQuickSwitchModule, 9989 onExit: () => { 9990 arcadeAudio.stopBgm(); 9991 onNavigate("/"); 9992 }, 9993 enableScanlines, 9994 setEnableScanlines, 9995 theme: activeTheme, 9996 themeMode, 9997 onCycleTheme: handleCycleTheme, 9998 onSelectTheme: handleSelectTheme, 9999 onSelectMusic: () => setStage("SONG") 10000 } 10001 ), 10002 /* @__PURE__ */ jsxRuntimeExports.jsxs("main", { className: "relative z-10 flex-1 max-w-7xl w-full mx-auto px-4 py-2 flex flex-col overflow-hidden min-h-0 justify-between", children: [ 10003 stage === "SONG" && /* @__PURE__ */ jsxRuntimeExports.jsx( 10004 ArcadeSongSelector, 10005 { 10006 currentUser, 10007 selectedSong, 10008 onSelectSong: (song) => setSelectedSong(song), 10009 onProceedToCabinet: () => setStage("CABINET"), 10010 theme: activeTheme, 10011 onCycleTheme: handleCycleTheme 10012 } 10013 ), 10014 stage === "CABINET" && selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsx( 10015 GameModeSelector, 10016 { 10017 selectedSong, 10018 onBackToSongSelect: () => setStage("SONG"), 10019 onLaunchGame: handleLaunchGame, 10020 theme: activeTheme 10021 } 10022 ), 10023 stage === "ARENA" && selectedSong && /* @__PURE__ */ jsxRuntimeExports.jsx( 10024 GameArena, 10025 { 10026 module: selectedModule, 10027 selectedSong, 10028 gameSettings, 10029 currentUser, 10030 onBackToCabinetSelect: () => setStage("CABINET"), 10031 onBackToSongSelect: () => setStage("SONG") 10032 } 10033 ) 10034 ] }) 10035 ] }); 10036} 10037export { 10038 HamieGameCenterPage as default 10039};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.