1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { S as SouthparkCharacter, B as BASE_ANIMALS, l as SOUTHPARK_INSTRUMENTS } from "./SouthparkCharacter-BNwA9Wd0.js"; 3import { g as getAnimalVariant, b as getVariantFilterId, r as renderSvgVariantFilter } from "./SouthparkVariants-DgWdjJ-T.js"; 4import { a as api } from "./index-D0Bmh-UX.js"; 5import { G as GlobeFlag } from "./GlobeFlag-V1uaxVEK.js"; 6import { u as useTranslation } from "./vendor-i18n-R-bMoYp_.js"; 7import { g6 as ChartNoAxesColumn, cy as Trophy, bU as Play, c3 as RotateCcw, ba as Info, fc as Smartphone, bF as CircleQuestionMark, b6 as LoaderCircle } from "./vendor-dVNaLJx_.js"; 8import "./IkMath-D6fi1ppd.js"; 9import "./vendor-framer-C8eEgkdL.js"; 10const DIFFICULTY_CONFIG = { 11 kantan: { 12 label: "Kantan (Easy)", 13 icon: "ð¸", 14 scoreMultiplier: 1, 15 minInterval: 0.35, 16 maxNotesPerSec: 2.5, 17 subdivision: 2, 18 allowBalloons: false, 19 allowDrumrolls: true, 20 donKaAlternationLimit: 2 21 }, 22 futsuu: { 23 label: "Futsuu (Normal)", 24 icon: "ð", 25 scoreMultiplier: 1.5, 26 minInterval: 0.22, 27 maxNotesPerSec: 4.5, 28 subdivision: 4, 29 allowBalloons: true, 30 allowDrumrolls: true, 31 donKaAlternationLimit: 4 32 }, 33 muzukashii: { 34 label: "Muzukashii (Hard)", 35 icon: "ðº", 36 scoreMultiplier: 2.2, 37 minInterval: 0.12, 38 maxNotesPerSec: 8.5, 39 subdivision: 8, 40 allowBalloons: true, 41 allowDrumrolls: true, 42 donKaAlternationLimit: 8 43 }, 44 oni: { 45 label: "Oni (Extreme)", 46 icon: "ð¹", 47 scoreMultiplier: 3, 48 minInterval: 0.05, 49 maxNotesPerSec: 18, 50 subdivision: 16, 51 allowBalloons: true, 52 allowDrumrolls: true, 53 donKaAlternationLimit: 999 54 } 55}; 56const GM_DON_PITCHES = /* @__PURE__ */ new Set([35, 36, 41, 43, 45, 47, 48, 50, 61, 64]); 57const GM_KA_PITCHES = /* @__PURE__ */ new Set([37, 38, 39, 40, 42, 44, 46, 49, 51, 52, 53, 54, 55, 56, 57, 58, 59, 69, 70]); 58function generateTaikoChart(pianoRollData, melodyData, processedInstruments, midiMetadata, difficulty = "futsuu", duration = 180) { 59 const tempo = Math.max(40, Math.min(300, Number(midiMetadata == null ? void 0 : midiMetadata.tempo) || 120)); 60 const rawNotes = []; 61 let chosenSource = "auto"; 62 const hasPercussion = processedInstruments == null ? void 0 : processedInstruments.some( 63 (inst) => inst.midiChannel === 9 && (inst.notesCount === void 0 || inst.notesCount > 0) 64 ); 65 const drumNotes = pianoRollData && hasPercussion ? filterDrumNotes(pianoRollData, processedInstruments) : []; 66 if (drumNotes.length >= 10) { 67 chosenSource = "drum"; 68 mapDrumNotes(drumNotes, rawNotes); 69 } else if (melodyData && melodyData.length >= 8) { 70 chosenSource = "melody"; 71 mapMelodyNotes(melodyData, rawNotes, tempo); 72 } else if (pianoRollData && pianoRollData.length >= 8) { 73 chosenSource = "melody"; 74 mapMelodyNotes(pianoRollData, rawNotes, tempo); 75 } else { 76 chosenSource = "auto"; 77 mapAutoGroove(tempo, duration, rawNotes); 78 } 79 rawNotes.sort((a, b) => a.t - b.t); 80 const config = DIFFICULTY_CONFIG[difficulty]; 81 const quantizedNotes = quantizeAndFilter(rawNotes, difficulty, tempo, config); 82 let donCount = 0; 83 let kaCount = 0; 84 let drumrollCount = 0; 85 let balloonCount = 0; 86 const finalNotes = quantizedNotes.map((n, idx) => { 87 if (n.type === "don" || n.type === "big_don") donCount++; 88 else if (n.type === "ka" || n.type === "big_ka") kaCount++; 89 else if (n.type === "drumroll") drumrollCount++; 90 else if (n.type === "balloon") balloonCount++; 91 return { 92 id: idx + 1, 93 t: Math.round(n.t * 1e3) / 1e3, 94 type: n.type, 95 duration: Math.round(n.duration * 1e3) / 1e3, 96 source: chosenSource, 97 velocity: n.velocity, 98 hitsRequired: n.type === "balloon" ? Math.max(5, Math.min(25, Math.floor(n.duration * 6))) : void 0, 99 hitsCount: 0 100 }; 101 }); 102 return { 103 notes: finalNotes, 104 meta: { 105 source: chosenSource, 106 difficulty, 107 totalNotes: finalNotes.length, 108 donCount, 109 kaCount, 110 drumrollCount, 111 balloonCount, 112 bpm: tempo, 113 scoreMultiplier: config.scoreMultiplier 114 } 115 }; 116} 117function filterDrumNotes(data, processedInstruments) { 118 const musicalInstruments = (processedInstruments == null ? void 0 : processedInstruments.filter((inst) => inst.midiChannel !== -1)) || []; 119 return data.filter((note) => { 120 let inst = musicalInstruments[note.trackIdx]; 121 if (!inst || inst.midiChannel !== 9) { 122 inst = processedInstruments == null ? void 0 : processedInstruments.find((i) => i.trackId === note.trackIdx); 123 } 124 if (!inst || inst.midiChannel !== 9) { 125 inst = processedInstruments == null ? void 0 : processedInstruments.find((i) => i.originalName === note.i || i.displayName === note.i || i.name === note.i); 126 } 127 if (inst) { 128 return inst.midiChannel === 9; 129 } 130 const nameLower = (note.i || "").toLowerCase(); 131 return nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal"); 132 }); 133} 134function mapDrumNotes(drumNotes, output) {
135 const clusters = /* @__PURE__ */ new Map(); 136 for (const note of drumNotes) { 137 const key = Math.round(note.t * 40) / 40; 138 let group = clusters.get(key); 139 if (!group) { 140 group = []; 141 clusters.set(key, group); 142 } 143 group.push(note); 144 } 145 for (const [, notes] of clusters.entries()) { 146 const time = notes[0].t; 147 const maxVel = Math.max(...notes.map((n) => n.v || 80)); 148 const hasBass = notes.some((n) => GM_DON_PITCHES.has(n.n)); 149 const hasCymbalOrSnare = notes.some((n) => GM_KA_PITCHES.has(n.n)); 150 if (hasBass && hasCymbalOrSnare) { 151 output.push({ 152 t: time, 153 type: "big_don", 154 duration: 0.1, 155 velocity: maxVel 156 }); 157 } else if (hasBass) { 158 output.push({ 159 t: time, 160 type: maxVel > 110 ? "big_don" : "don", 161 duration: 0.1, 162 velocity: maxVel 163 }); 164 } else if (hasCymbalOrSnare) { 165 output.push({ 166 t: time, 167 type: maxVel > 110 ? "big_ka" : "ka", 168 duration: 0.1, 169 velocity: maxVel 170 }); 171 } else { 172 output.push({ 173 t: time, 174 type: notes[0].n % 2 === 0 ? "don" : "ka", 175 duration: 0.1, 176 velocity: maxVel 177 }); 178 } 179 } 180} 181function mapMelodyNotes(melodyNotes, output, bpm) { 182 if (!melodyNotes || melodyNotes.length === 0) return; 183 let minP = 127; 184 let maxP = 0; 185 let sumP = 0; 186 for (const note of melodyNotes) { 187 if (note.n < minP) minP = note.n; 188 if (note.n > maxP) maxP = note.n; 189 sumP += note.n; 190 } 191 const avgP = sumP / melodyNotes.length; 192 const midThreshold = minP + (maxP - minP) * 0.52; 193 const beatDuration = 60 / bpm; 194 for (const note of melodyNotes) { 195 const vel = note.v || 80; 196 const dur = note.d || 0.2; 197 if (dur >= beatDuration * 2 && dur >= 0.8) { 198 output.push({ 199 t: note.t, 200 type: dur >= beatDuration * 3.5 ? "balloon" : "drumroll", 201 duration: Math.min(dur, 4), 202 velocity: vel 203 }); 204 continue; 205 } 206 const beatOffset = note.t % beatDuration / beatDuration; 207 const isOnBeat = beatOffset < 0.12 || beatOffset > 0.88; 208 let noteType = "don"; 209 if (note.n > midThreshold || !isOnBeat && note.n >= avgP) { 210 noteType = vel > 105 ? "big_ka" : "ka"; 211 } else { 212 noteType = isOnBeat && vel > 100 ? "big_don" : "don"; 213 } 214 output.push({ 215 t: note.t, 216 type: noteType, 217 duration: 0.1, 218 velocity: vel 219 }); 220 } 221} 222function mapAutoGroove(bpm, duration, output) { 223 const beatSec = 60 / bpm; 224 const totalBeats = Math.floor(duration / beatSec); 225 for (let b = 2; b < totalBeats - 2; b++) { 226 const t = b * beatSec; 227 const measureBeat = b % 4; 228 const measureNum = Math.floor(b / 4); 229 if (measureBeat === 0) { 230 output.push({ t, type: "big_don", duration: 0.1, velocity: 100 }); 231 } else if (measureBeat === 2) { 232 output.push({ t, type: "don", duration: 0.1, velocity: 85 }); 233 } else if (measureBeat === 1 || measureBeat === 3) { 234 if (measureNum % 4 === 3 && measureBeat === 3) { 235 output.push({ t, type: "don", duration: 0.1, velocity: 90 }); 236 output.push({ t: t + beatSec * 0.5, type: "don", duration: 0.1, velocity: 90 }); 237 } else { 238 output.push({ t, type: "ka", duration: 0.1, velocity: 80 }); 239 } 240 } 241 if (measureNum % 8 === 7 && measureBeat === 2) { 242 output.push({ 243 t, 244 type: "drumroll", 245 duration: beatSec * 2, 246 velocity: 95 247 }); 248 b += 1; 249 } 250 } 251} 252function quantizeAndFilter(notes, difficulty, bpm, config, source) { 253 const beatSec = 60 / bpm; 254 const step = beatSec / (config.subdivision / 2); 255 const result = []; 256 let lastEndT = -999; 257 let consecutiveSameType = 0; 258 let lastType = null; 259 for (let i = 0; i < notes.length; i++) { 260 const note = notes[i]; 261 if (note.t < lastEndT + config.minInterval) { 262 continue; 263 } 264 let qTime = Math.round(note.t / step) * step; 265 if (qTime < 0) qTime = 0; 266 if (qTime <= lastEndT) { 267 qTime = lastEndT + config.minInterval; 268 } 269 let finalType = note.type; 270 if (difficulty === "kantan") { 271 if (finalType === "balloon") finalType = "drumroll"; 272 if (finalType === "ka" && Math.random() > 0.25) finalType = "don"; 273 if (finalType === "big_ka") finalType = "big_don"; 274 } else if (difficulty === "futsuu") { 275 if (finalType === lastType) { 276 consecutiveSameType++; 277 if (consecutiveSameType > config.donKaAlternationLimit) { 278 finalType = finalType === "don" || finalType === "big_don" ? "ka" : "don"; 279 consecutiveSameType = 1; 280 } 281 } else { 282 consecutiveSameType = 1; 283 } 284 } 285 if (!config.allowBalloons && finalType === "balloon") { 286 finalType = "drumroll"; 287 } 288 if (!config.allowDrumrolls && finalType === "drumroll") { 289 finalType = "big_don"; 290 } 291 result.push({ 292 t: qTime, 293 type: finalType, 294 duration: note.duration, 295 velocity: note.velocity 296 }); 297 const noteDur = finalType === "drumroll" || finalType === "balloon" ? note.duration || 0.8 : 0; 298 lastEndT = qTime + noteDur; 299 lastType = finalType; 300 } 301 return result; 302} 303const HSF3_KEY = new TextEncoder().encode("HamieNET_HSF3_Secret_2026_Key!"); 304function deobfuscateHsf3Buffer(buffer) { 305 const bytes = new Uint8Array(buffer); 306 const obfLen = Math.min(bytes.length, 4096); 307 const keyLen = HSF3_KEY.length; 308 for (let i = 0; i < obfLen; i++) { 309 bytes[i] ^= HSF3_KEY[i % keyLen]; 310 } 311 return buffer; 312} 313class TaikoAudioEngine {
314 constructor() { 315 this.ctx = null; 316 this.masterGain = null; 317 this.audioOffsetMs = 0; 318 this.donBuffer = null; 319 this.kaBuffer = null; 320 this.isHsf3Loading = false; 321 this.hsf3Loaded = false; 322 if (typeof window !== "undefined") { 323 const savedOffset = localStorage.getItem("taiko_audio_offset"); 324 if (savedOffset !== null) { 325 this.audioOffsetMs = Number(savedOffset) || 0; 326 } 327 setTimeout(() => this.loadHsf3Soundfont().catch(() => { 328 }), 100); 329 } 330 } 331 async loadHsf3Soundfont(url = "/storage/soundfonts/GeneralUser-GS_HSF3Bank/0_116.hsf3") { 332 if (this.hsf3Loaded || this.isHsf3Loading) return; 333 this.isHsf3Loading = true; 334 this.initContext(); 335 try { 336 const res = await fetch(url); 337 if (!res.ok) throw new Error(`HTTP ${res.status}`); 338 let buf = await res.arrayBuffer(); 339 buf = deobfuscateHsf3Buffer(buf); 340 const u8 = new Uint8Array(buf); 341 let firstOgg = -1; 342 for (let i = 0; i < Math.min(u8.length - 4, 25e3); i++) { 343 if (u8[i] === 79 && u8[i + 1] === 103 && u8[i + 2] === 103 && u8[i + 3] === 83) { 344 firstOgg = i; 345 break; 346 } 347 } 348 if (firstOgg !== -1 && this.ctx) { 349 const view = new DataView(buf); 350 const smplSize = view.getUint32(firstOgg - 4, true); 351 const oggData = buf.slice(firstOgg, firstOgg + smplSize); 352 const decoded = await this.ctx.decodeAudioData(oggData); 353 this.donBuffer = decoded; 354 this.kaBuffer = decoded; 355 this.hsf3Loaded = true; 356 } 357 } catch (err) { 358 } finally { 359 this.isHsf3Loading = false; 360 } 361 } 362 initContext() { 363 if (!this.ctx) { 364 const AudioCtx = window.AudioContext || window.webkitAudioContext; 365 if (AudioCtx) { 366 this.ctx = new AudioCtx(); 367 this.masterGain = this.ctx.createGain(); 368 this.masterGain.gain.setValueAtTime(0.85, this.ctx.currentTime); 369 this.masterGain.connect(this.ctx.destination); 370 } 371 } 372 if (this.ctx && this.ctx.state === "suspended") { 373 this.ctx.resume(); 374 } 375 } 376 getOffsetMs() { 377 return this.audioOffsetMs; 378 } 379 setOffsetMs(offset) { 380 this.audioOffsetMs = Math.max(-250, Math.min(250, offset)); 381 if (typeof window !== "undefined") { 382 localStorage.setItem("taiko_audio_offset", String(this.audioOffsetMs)); 383 } 384 } 385 /** 386 * Play authentic Wadaiko Center strike (DON) 387 */ 388 playDon(isBig = false, volume = 1) { 389 this.initContext(); 390 if (!this.ctx || !this.masterGain) return; 391 const now = this.ctx.currentTime; 392 const vol = isBig ? volume * 1.35 : volume; 393 if (this.donBuffer) { 394 const source = this.ctx.createBufferSource(); 395 source.buffer = this.donBuffer; 396 source.playbackRate.setValueAtTime(isBig ? 0.9 : 1.02, now); 397 const gain = this.ctx.createGain(); 398 gain.gain.setValueAtTime(vol * 1.15, now); 399 gain.gain.exponentialRampToValueAtTime(1e-3, now + Math.min(0.65, this.donBuffer.duration)); 400 source.connect(gain); 401 gain.connect(this.masterGain); 402 source.start(now); 403 if (isBig) { 404 const subOsc2 = this.ctx.createOscillator(); 405 const subGain2 = this.ctx.createGain(); 406 subOsc2.type = "triangle"; 407 subOsc2.frequency.setValueAtTime(75, now); 408 subOsc2.frequency.exponentialRampToValueAtTime(32, now + 0.22); 409 subGain2.gain.setValueAtTime(vol * 0.7, now); 410 subGain2.gain.exponentialRampToValueAtTime(1e-3, now + 0.22); 411 subOsc2.connect(subGain2); 412 subGain2.connect(this.masterGain); 413 subOsc2.start(now); 414 subOsc2.stop(now + 0.22); 415 } 416 return; 417 } 418 const osc = this.ctx.createOscillator(); 419 const oscGain = this.ctx.createGain(); 420 const startFreq = isBig ? 130 : 160; 421 const endFreq = isBig ? 45 : 55; 422 const duration = isBig ? 0.35 : 0.22; 423 osc.type = "sine"; 424 osc.frequency.setValueAtTime(startFreq, now); 425 osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration); 426 oscGain.gain.setValueAtTime(vol * 0.9, now); 427 oscGain.gain.exponentialRampToValueAtTime(1e-3, now + duration); 428 osc.connect(oscGain); 429 oscGain.connect(this.masterGain); 430 osc.start(now); 431 osc.stop(now + duration); 432 const subOsc = this.ctx.createOscillator(); 433 const subGain = this.ctx.createGain();
434 subOsc.type = "triangle"; 435 subOsc.frequency.setValueAtTime(isBig ? 65 : 85, now); 436 subOsc.frequency.exponentialRampToValueAtTime(35, now + 0.18); 437 subGain.gain.setValueAtTime(vol * 0.6, now); 438 subGain.gain.exponentialRampToValueAtTime(1e-3, now + 0.18); 439 subOsc.connect(subGain); 440 subGain.connect(this.masterGain); 441 subOsc.start(now); 442 subOsc.stop(now + 0.18); 443 this.playNoiseTransient(now, 600, 1400, 0.035, vol * 0.4); 444 } 445 /** 446 * Play sharp Wadaiko Rim strike (KA / KATSU) 447 */ 448 playKa(isBig = false, volume = 1) { 449 this.initContext(); 450 if (!this.ctx || !this.masterGain) return; 451 const now = this.ctx.currentTime; 452 const vol = isBig ? volume * 1.3 : volume; 453 if (this.kaBuffer) { 454 const source = this.ctx.createBufferSource(); 455 source.buffer = this.kaBuffer; 456 source.playbackRate.setValueAtTime(isBig ? 1.65 : 2.05, now); 457 const filter = this.ctx.createBiquadFilter(); 458 filter.type = "highpass"; 459 filter.frequency.setValueAtTime(850, now); 460 const gain = this.ctx.createGain(); 461 gain.gain.setValueAtTime(vol * 1.05, now); 462 gain.gain.exponentialRampToValueAtTime(1e-3, now + 0.16); 463 source.connect(filter); 464 filter.connect(gain); 465 gain.connect(this.masterGain); 466 source.start(now); 467 source.stop(now + 0.18); 468 this.playNoiseTransient(now, 2400, 6e3, 0.032, vol * 0.45); 469 return; 470 } 471 const duration = isBig ? 0.14 : 0.09; 472 const osc1 = this.ctx.createOscillator(); 473 const oscGain1 = this.ctx.createGain(); 474 osc1.type = "triangle"; 475 osc1.frequency.setValueAtTime(isBig ? 620 : 850, now); 476 osc1.frequency.exponentialRampToValueAtTime(350, now + duration); 477 oscGain1.gain.setValueAtTime(vol * 0.7, now); 478 oscGain1.gain.exponentialRampToValueAtTime(1e-3, now + duration); 479 osc1.connect(oscGain1); 480 oscGain1.connect(this.masterGain); 481 osc1.start(now); 482 osc1.stop(now + duration); 483 this.playNoiseTransient(now, 2200, 5e3, 0.045, vol * 0.65); 484 } 485 /** 486 * Noise transient filter helper for attack snap 487 */ 488 playNoiseTransient(time, lowFreq, highFreq, duration, gainAmount) { 489 if (!this.ctx || !this.masterGain) return; 490 const bufferSize = Math.floor(this.ctx.sampleRate * duration); 491 const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); 492 const data = buffer.getChannelData(0); 493 for (let i = 0; i < bufferSize; i++) { 494 data[i] = Math.random() * 2 - 1; 495 } 496 const noise = this.ctx.createBufferSource(); 497 noise.buffer = buffer; 498 const filter = this.ctx.createBiquadFilter(); 499 filter.type = "bandpass"; 500 filter.frequency.setValueAtTime((lowFreq + highFreq) / 2, time); 501 filter.Q.setValueAtTime(1.8, time); 502 const gainNode = this.ctx.createGain(); 503 gainNode.gain.setValueAtTime(gainAmount, time); 504 gainNode.gain.exponentialRampToValueAtTime(1e-3, time + duration); 505 noise.connect(filter); 506 filter.connect(gainNode); 507 gainNode.connect(this.masterGain); 508 noise.start(time); 509 noise.stop(time + duration); 510 } 511 /** 512 * Celebratory balloon pop sound effect 513 */ 514 playBalloonPop() { 515 this.initContext(); 516 if (!this.ctx || !this.masterGain) return; 517 const now = this.ctx.currentTime; 518 this.playDon(true, 1.2); 519 this.playNoiseTransient(now, 400, 3e3, 0.15, 0.8); 520 } 521} 522const taikoAudio = new TaikoAudioEngine(); 523const DEFAULT_PLAYER_KEYS = { 524 1: { 525 leftKa: ["KeyD"], 526 leftDon: ["KeyF"], 527 rightDon: ["KeyJ"], 528 rightKa: ["KeyK"] 529 }, 530 2: { 531 leftKa: ["ArrowLeft"], 532 leftDon: ["ArrowDown"], 533 rightDon: ["ArrowRight", "Numpad2"], 534 rightKa: ["ArrowUp", "Numpad3"] 535 }, 536 3: { 537 leftKa: ["KeyC"], 538 leftDon: ["KeyV"], 539 rightDon: ["KeyB"], 540 rightKa: ["KeyN"] 541 }, 542 4: { 543 leftKa: ["Numpad7", "Digit7"], 544 leftDon: ["Numpad8", "Digit8"], 545 rightDon: ["Numpad9", "Digit9"], 546 rightKa: ["NumpadAdd", "Digit0"] 547 } 548}; 549class TaikoInputManager { 550 constructor() { 551 this.activePlayers = 1; 552 this.onHitCallback = null; 553 this.keyStates = /* @__PURE__ */ new Map(); 554 this.lastHitTimes = /* @__PURE__ */ new Map(); 555 this.isListening = false;
556 this.boundKeyDown = this.handleKeyDown.bind(this); 557 this.boundKeyUp = this.handleKeyUp.bind(this); 558 } 559 setActivePlayers(count) { 560 this.activePlayers = Math.max(1, Math.min(2, count)); 561 } 562 setOnHit(callback) { 563 this.onHitCallback = callback; 564 } 565 isKeyActive(playerId, subKey) { 566 const config = DEFAULT_PLAYER_KEYS[playerId]; 567 if (!config) return false; 568 const codes = config[subKey]; 569 return codes ? codes.some((code) => this.keyStates.get(`${playerId}_${code}`) === true) : false; 570 } 571 start() { 572 if (typeof window === "undefined" || this.isListening) return; 573 window.addEventListener("keydown", this.boundKeyDown); 574 window.addEventListener("keyup", this.boundKeyUp); 575 this.isListening = true; 576 } 577 stop() { 578 if (typeof window === "undefined" || !this.isListening) return; 579 window.removeEventListener("keydown", this.boundKeyDown); 580 window.removeEventListener("keyup", this.boundKeyUp); 581 this.keyStates.clear(); 582 this.lastHitTimes.clear(); 583 this.isListening = false; 584 } 585 /** 586 * Programmatic trigger for Touch screens or Gamepads 587 */ 588 triggerHit(playerId, action, subKey) { 589 if (this.onHitCallback) { 590 this.onHitCallback(playerId, action, action.startsWith("big_"), subKey); 591 } 592 } 593 handleKeyDown(e) { 594 var _a, _b, _c, _d; 595 const target = e.target; 596 if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) { 597 return; 598 } 599 if (e.repeat) return; 600 const code = e.code; 601 const now = performance.now(); 602 for (let p = 1; p <= this.activePlayers; p++) { 603 const config = DEFAULT_PLAYER_KEYS[p]; 604 if (!config) continue; 605 const isLeftKa = config.leftKa.includes(code); 606 const isLeftDon = config.leftDon.includes(code); 607 const isRightDon = config.rightDon.includes(code); 608 const isRightKa = config.rightKa.includes(code); 609 if (!isLeftKa && !isLeftDon && !isRightDon && !isRightKa) continue; 610 if (code.startsWith("Arrow") || code.startsWith("Numpad")) { 611 e.preventDefault(); 612 } 613 this.keyStates.set(`${p}_${code}`, true); 614 if (isLeftDon || isRightDon) { 615 const subKey = isLeftDon ? "leftDon" : "rightDon"; 616 const otherKey = isLeftDon ? config.rightDon[0] : config.leftDon[0]; 617 const otherTime = this.lastHitTimes.get(`${p}_${otherKey}`) || 0; 618 const isDual = now - otherTime < 75; 619 this.lastHitTimes.set(`${p}_${code}`, now); 620 if (isDual) { 621 (_a = this.onHitCallback) == null ? void 0 : _a.call(this, p, "big_don", true, subKey); 622 } else { 623 (_b = this.onHitCallback) == null ? void 0 : _b.call(this, p, "don", false, subKey); 624 } 625 return; 626 } 627 if (isLeftKa || isRightKa) { 628 const subKey = isLeftKa ? "leftKa" : "rightKa"; 629 const otherKey = isLeftKa ? config.rightKa[0] : config.leftKa[0]; 630 const otherTime = this.lastHitTimes.get(`${p}_${otherKey}`) || 0; 631 const isDual = now - otherTime < 75; 632 this.lastHitTimes.set(`${p}_${code}`, now); 633 if (isDual) { 634 (_c = this.onHitCallback) == null ? void 0 : _c.call(this, p, "big_ka", true, subKey); 635 } else { 636 (_d = this.onHitCallback) == null ? void 0 : _d.call(this, p, "ka", false, subKey); 637 } 638 return; 639 } 640 } 641 } 642 handleKeyUp(e) { 643 const code = e.code; 644 for (let p = 1; p <= this.activePlayers; p++) { 645 this.keyStates.set(`${p}_${code}`, false); 646 } 647 } 648} 649const TaikoHamsterMascot = ({ 650 combo, 651 isHit, 652 lastHitType, 653 lastSubKey, 654 strikeTick, 655 soulGauge = 0, 656 playerColor = "#ef4444", 657 scale = 1, 658 playerId = 1, 659 currentTime = 0 660}) => { 661 const isFever = soulGauge >= 100; 662 const isGaugeFull = soulGauge >= 80; 663 const costumeId = playerId === 2 ? "taiko_headband_blue" : "taiko_headband"; 664 const hamsterVariant = reactExports.useMemo(() => getAnimalVariant("hamster", 0), []); 665 const variantFilterId = reactExports.useMemo(() => getVariantFilterId("hamster", 0), []); 666 const taikoInst = reactExports.useMemo(() => { 667 return SOUTHPARK_INSTRUMENTS[116] || { 668 program: 116, 669 name: "Taiko Drum", 670 holdStyle: "drumming", 671 scale: 1, 672 offsetX: 0, 673 offsetY: 0 674 }; 675 }, []); 676 const [leftStrike, setLeftStrike] = reactExports.useState({ active: false, target: "don" }); 677 const [rightStrike, setRightStrike] = reactExports.useState({ active: false, target: "don" }); 678 const [jumpY, setJumpY] = reactExports.useState(0); 679 const [jumpScale, setJumpScale] = reactExports.useState(1); 680 const altArmRef = reactExports.useRef("right"); 681 const lastTickRef = reactExports.useRef(strikeTick ?? 0); 682 const timerLeftRef = reactExports.useRef(null); 683 const timerRightRef = reactExports.useRef(null); 684 const timerJumpRef = reactExports.useRef(null); 685 reactExports.useEffect(() => { 686 return () => { 687 if (timerLeftRef.current) clearTimeout(timerLeftRef.current); 688 if (timerRightRef.current) clearTimeout(timerRightRef.current); 689 if (timerJumpRef.current) clearTimeout(timerJumpRef.current); 690 }; 691 }, []); 692 reactExports.useEffect(() => { 693 const isNewTick = strikeTick !== void 0 && strikeTick !== lastTickRef.current; 694 if (strikeTick !== void 0) { 695 lastTickRef.current = strikeTick; 696 } 697 if (!isNewTick && !isHit) return; 698 const isBig = lastHitType === "big_don" || lastHitType === "big_ka"; 699 const targetType = lastHitType === "ka" || lastHitType === "big_ka" || lastSubKey === "leftKa" || lastSubKey === "rightKa" ? "ka" : "don"; 700 let hitLeft = false;
701 let hitRight = false; 702 if (isBig) { 703 hitLeft = true; 704 hitRight = true; 705 } else if (lastSubKey === "leftDon" || lastSubKey === "leftKa") { 706 hitLeft = true; 707 } else if (lastSubKey === "rightDon" || lastSubKey === "rightKa") { 708 hitRight = true; 709 } else if (isHit) { 710 if (altArmRef.current === "left") { 711 hitLeft = true; 712 altArmRef.current = "right"; 713 } else { 714 hitRight = true; 715 altArmRef.current = "left"; 716 } 717 } 718 if (hitLeft) { 719 setLeftStrike({ active: true, target: targetType }); 720 if (timerLeftRef.current) clearTimeout(timerLeftRef.current); 721 timerLeftRef.current = setTimeout(() => { 722 setLeftStrike((prev) => ({ ...prev, active: false })); 723 }, 180); 724 } 725 if (hitRight) { 726 setRightStrike({ active: true, target: targetType }); 727 if (timerRightRef.current) clearTimeout(timerRightRef.current); 728 timerRightRef.current = setTimeout(() => { 729 setRightStrike((prev) => ({ ...prev, active: false })); 730 }, 180); 731 } 732 if (hitLeft || hitRight || isBig) { 733 if (timerJumpRef.current) clearTimeout(timerJumpRef.current); 734 if (isBig) { 735 setJumpY(-14); 736 setJumpScale(1.06); 737 } else { 738 setJumpY(-5); 739 setJumpScale(1.02); 740 } 741 timerJumpRef.current = setTimeout(() => { 742 setJumpY(0); 743 setJumpScale(1); 744 }, isBig ? 200 : 140); 745 } 746 }, [strikeTick, isHit, lastHitType, lastSubKey]); 747 const manualLimbAngles = reactExports.useMemo(() => { 748 const groove = Math.sin((currentTime || 0) * 8) * 1.5; 749 let shoulderL = 18 + groove; 750 let relElbowL = -48 - groove; 751 let wristL = -5; 752 let stickL = 145; 753 let shoulderR = -18 - groove; 754 let relElbowR = 48 + groove; 755 let wristR = 5; 756 let stickR = -145; 757 if (leftStrike.active) { 758 if (leftStrike.target === "don") { 759 shoulderL = 10; 760 relElbowL = -20; 761 wristL = 12; 762 stickL = 45; 763 } else { 764 shoulderL = 8; 765 relElbowL = -18; 766 wristL = -8; 767 stickL = -10; 768 } 769 } 770 if (rightStrike.active) { 771 if (rightStrike.target === "don") { 772 shoulderR = -10; 773 relElbowR = 20; 774 wristR = -12; 775 stickR = -45; 776 } else { 777 shoulderR = -8; 778 relElbowR = 18; 779 wristR = 8; 780 stickR = 10; 781 } 782 } 783 return { 784 shoulderL, 785 relElbowL, 786 wristL, 787 stickL, 788 shoulderR, 789 relElbowR, 790 wristR, 791 stickR 792 }; 793 }, [leftStrike, rightStrike, currentTime]); 794 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 795 "div", 796 { 797 className: "relative flex items-center justify-center select-none pointer-events-none", 798 style: { 799 width: 120, 800 height: 140, 801 transform: `scale(${scale * jumpScale}) translateY(${jumpY}px)`, 802 transition: "transform 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275)" 803 }, 804 children: [
805 (isFever || isGaugeFull) && /* @__PURE__ */ jsxRuntimeExports.jsx( 806 "div", 807 { 808 className: "absolute inset-0 rounded-full animate-pulse opacity-40 blur-xl pointer-events-none", 809 style: { backgroundColor: isGaugeFull ? "#fbbf24" : playerColor } 810 } 811 ), 812 /* @__PURE__ */ jsxRuntimeExports.jsxs( 813 "svg", 814 { 815 viewBox: "-70 -110 140 200", 816 className: "w-full h-full overflow-visible drop-shadow-md", 817 children: [ 818 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [ 819 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "miniTaikoBarrelGrad", x1: "0", y1: "0", x2: "1", y2: "0", children: [ 820 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#7f1d1d" }), 821 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "35%", stopColor: "#b91c1c" }), 822 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: "#991b1b" }),
823 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#7f1d1d" }) 824 ] }), 825 renderSvgVariantFilter(variantFilterId, hamsterVariant) 826 ] }), 827 /* @__PURE__ */ jsxRuntimeExports.jsx( 828 SouthparkCharacter, 829 { 830 animal: BASE_ANIMALS.hamster, 831 instrument: taikoInst, 832 isActive: leftStrike.active || rightStrike.active, 833 velocity: 127, 834 currentTime, 835 isPlaying: true, 836 physicsMode: "snappy", 837 showInstrument: false, 838 costumes: [costumeId], 839 manualLimbAngles, 840 variant: hamsterVariant, 841 variantFilterId, 842 hideArms: true 843 } 844 ), 845 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { id: "hamster-mini-taiko", transform: "translate(0, 52)", children: [ 846 /* @__P
846URE__ */ jsxRuntimeExports.jsx("line", { x1: "-24", y1: "4", x2: "-32", y2: "30", stroke: "#3b1d11", strokeWidth: "4.5", strokeLinecap: "round" }), 847 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "24", y1: "4", x2: "32", y2: "30", stroke: "#3b1d11", strokeWidth: "4.5", strokeLinecap: "round" }), 848 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "-29", y1: "22", x2: "29", y2: "22", stroke: "#522513", strokeWidth: "3", strokeLinecap: "round" }), 849 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "-4", y: "8", width: "8", height: "22", rx: "2", fill: "#451a03", stroke: "#2c1409", strokeWidth: "1" }), 850 /* @__PURE__ */ jsxRuntimeExports.jsx( 851 "path", 852 { 853 d: "M -27 0 C -33 12, -31 20, -25 26 L 25 26 C 31 20, 33 12, 27 0 Z", 854 fill: "url(#miniTaikoBarrelGrad)", 855 stroke: "#2c1409", 856 strokeWidth: "1.8" 857 } 858 ), 859 /* @__P
859URE__ */ jsxRuntimeExports.jsx("path", { d: "M -26 18 C -16 22, 16 22, 26 18", fill: "none", stroke: "#1c1917", strokeWidth: "2.5" }), 860 [-18, -9, 0, 9, 18].map((x) => /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: x, cy: 19 + (x === 0 ? 1 : 0), r: "1.4", fill: "#fbbf24", stroke: "#78350f", strokeWidth: "0.5" }, x)), 861 /* @__PURE__ */ jsxRuntimeExports.jsx( 862 "ellipse", 863 { 864 cx: "0", 865 cy: "0", 866 rx: "27", 867 ry: "12", 868 fill: "#78350f", 869 stroke: "#2c1409", 870 strokeWidth: "1.5" 871 } 872 ), 873 leftStrike.active && leftStrike.target === "ka" && /* @__PURE__ */ jsxRuntimeExports.jsx( 874 "path", 875 { 876 d: "M -26.5 0 C -26.5 -7, -10 -11.5, 0 -11.5", 877 fill: "none", 878 stroke: "#38bdf8", 879 strokeWidth: "4.5", 880 strokeLinecap: "round", 881 style: { filter: "drop-shadow(0 0 6px #0284c7)" } 882 } 883 ), 884 rightStrike.active && rightStrike.target === "ka" && /* @__PURE__ */ jsxRuntimeExports.jsx( 885 "path", 886 { 887 d: "M 0 -11.5 C 10 -11.5, 26.5 -7, 26.5 0", 888 fill: "none", 889 stroke: "#38bdf8", 890 strokeWidth: "4.5", 891 strokeLinecap: "round", 892 style: { filter: "drop-shadow(0 0 6px #0284c7)" } 893 } 894 ),
895 /* @__PURE__ */ jsxRuntimeExports.jsx( 896 "ellipse", 897 { 898 cx: "0", 899 cy: "0", 900 rx: "23", 901 ry: "9.5", 902 fill: "#fef3c7", 903 stroke: "#b45309", 904 strokeWidth: "1.2" 905 } 906 ), 907 leftStrike.active && leftStrike.target === "don" && /* @__PURE__ */ jsxRuntimeExports.jsx( 908 "path", 909 { 910 d: "M 0 -9.5 C -14 -9.5, -23 -4.5, -23 0 C -23 4.5, -14 9.5, 0 9.5 Z", 911 fill: "#ef4444", 912 fillOpacity: "0.9", 913 style: { filter: "drop-shadow(0 0 8px #ef4444) drop-shadow(0 0 14px #f97316)" } 914 } 915 ), 916 rightStrike.active && rightStrike.target === "don" && /* @__PURE__ */ jsxRuntimeExports.jsx( 917 "path", 918 { 919 d: "M 0 -9.5 C 14 -9.5, 23 -4.5, 23 0 C 23 4.5, 14 9.5, 0 9.5 Z", 920 fill: "#ef4444", 921 fillOpacity: "0.9", 922 style: { filter: "drop-shadow(0 0 8px #ef4444) drop-shadow(0 0 14px #f97316)" } 923 } 924 ), 925 leftStrike.active && rightStrike.active && leftStrike.target === "don" && /* @__PURE__ */ jsxRuntimeExports.jsx( 926 "ellipse", 927 { 928 cx: "0", 929 cy: "0", 930 rx: "16", 931 ry: "7", 932 fill: "#fef08a", 933 fillOpacity: "0.95", 934 style: { filter: "drop-shadow(0 0 10px #fbbf24)" } 935 } 936 ),
937 /* @__PURE__ */ jsxRuntimeExports.jsx( 938 "text", 939 { 940 x: "0", 941 y: "1.5", 942 textAnchor: "middle", 943 dominantBaseline: "central", 944 fontSize: "9", 945 fontWeight: "900", 946 fontFamily: "sans-serif", 947 fill: leftStrike.active || rightStrike.active ? "#ffffff" : "#b45309", 948 fillOpacity: "0.9", 949 style: { pointerEvents: "none" }, 950 children: "太" 951 } 952 ), 953 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { opacity: "0.75", fontSize: "6.5", fontWeight: "bold", fontFamily: "monospace", textAnchor: "middle", children: [ 954 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "-18", y: "-0.5", fill: leftStrike.active && leftStrike.target === "ka" ? "#ffffff" : "#0369a1", children: "D" }), 955 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "-8", y: "1", fill: leftStrike.active && leftStrike.target === "don" ? "#ffffff" : "#b91c1c", children: "F" }),
956 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "8", y: "1", fill: rightStrike.active && rightStrike.target === "don" ? "#ffffff" : "#b91c1c", children: "J" }), 957 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "18", y: "-0.5", fill: rightStrike.active && rightStrike.target === "ka" ? "#ffffff" : "#0369a1", children: "K" }) 958 ] }), 959 leftStrike.active && /* @__PURE__ */ jsxRuntimeExports.jsx( 960 "circle", 961 { 962 cx: leftStrike.target === "ka" ? -22 : -10, 963 cy: leftStrike.target === "ka" ? -2 : 0, 964 r: "7", 965 fill: "none", 966 stroke: leftStrike.target === "ka" ? "#38bdf8" : "#fbbf24", 967 strokeWidth: "2.2", 968 style: { filter: leftStrike.target === "ka" ? "drop-shadow(0 0 5px #0284c7)" : "drop-shadow(0 0 5px #ea580c)" } 969 } 970 ), 971 rightStrike.active && /* @__PURE__ */ jsxRuntimeExports.jsx( 972 "circle", 973 { 974 cx: rightStrike.target === "ka" ? 22 : 10, 975 cy: rightStrike.target === "ka" ? -2 : 0, 976 r: "7", 977 fill: "none", 978 stroke: rightStrike.target === "ka" ? "#38bdf8" : "#fbbf24", 979 strokeWidth: "2.2", 980 style: { filter: rightStrike.target === "ka" ? "drop-shadow(0 0 5px #0284c7)" : "drop-shadow(0 0 5px #ea580c)" } 981 } 982 ), 983 leftStrike.active && rightStrike.active && /* @__PURE__ */ jsxRuntimeExports.jsx( 984 "ellipse", 985 { 986 cx: "0", 987 cy: "0", 988 rx: "25", 989 ry: "11", 990 fill: "none", 991 stroke: leftStrike.target === "ka" ? "#38bdf8" : "#fbbf24", 992 strokeWidth: "2.5", 993 style: { filter: "drop-shadow(0 0 8px #f59e0b)" } 994 } 995 ) 996 ] }),
997 /* @__PURE__ */ jsxRuntimeExports.jsx( 998 SouthparkCharacter, 999 { 1000 animal: BASE_ANIMALS.hamster, 1001 instrument: taikoInst, 1002 isActive: leftStrike.active || rightStrike.active, 1003 velocity: 127, 1004 currentTime, 1005 isPlaying: true, 1006 physicsMode: "snappy", 1007 showInstrument: false, 1008 costumes: [costumeId], 1009 manualLimbAngles, 1010 variant: hamsterVariant, 1011 variantFilterId, 1012 renderArmsOnly: true 1013 } 1014 ) 1015 ] 1016 } 1017 ) 1018 ] 1019 } 1020 ); 1021}; 1022const TAIKO_LOCALES = { 1023 en: { 1024 gameTitle: "Taiko no Hamie", 1025 gameSubtitle: "RHYTHM DRUM VISUALIZER", 1026 start: "START GAME", 1027 ready: "READY", 1028 loadingAudio: "Loading Audio...", 1029 lobby: "Lobby", 1030 players: "PLAYERS", 1031 difficulty: "DIFFICULTY", 1032 stageTheme: "STAGE THEME", 1033 speed: "SPEED", 1034 speedSlow: "0.8x (Slow)", 1035 speedNormal: "1.0x (Normal)", 1036 speedFast: "1.2x (Swift)", 1037 speedVeryFast: "1.5x (Fast)", 1038 mode: "GAME MODE", 1039 competitive: "Battle (Competitive)", 1040 cooperative: "Team (Co-op)", 1041 keyboardControls: "KEYBOARD CONTROLS", 1042 keyTip: "Press both Don keys simultaneously for Big Don!", 1043 don: "DON", 1044 ka: "KA", 1045 drumroll: "ROLL", 1046 balloon: "POP!", 1047 perfect: "PERFECT!", 1048 good: "GOOD", 1049 miss: "MISS", 1050 combo: "COMBO", 1051 score: "Score", 1052 maxCombo: "Max Combo", 1053 accuracy: "Accuracy", 1054 source: "Source", 1055 sourceDrums: "MIDI Drums", 1056 sourceMelody: "Melody Rhythm", 1057 sourceBeatGrid: "Groove Grid", 1058 resultsTitle: "STAGE CLEAR!", 1059 clearCrown: "CLEAR", 1060 silverCrown: "FULL COMBO", 1061 goldCrown: "ALL PERFECT", 1062 retry: "PLAY AGAIN", 1063 shareScore: "SHARE SCORE", 1064 kantan: "EASY", 1065 futsuu: "NORMAL", 1066 muzukashii: "HARD", 1067 oni: "EXTREME", 1068 themeMatsuri: "ð® Festival Lanterns", 1069 themeSakura: "ð¸ Sakura Fuji", 1070 themeThunder: "â¡ Thunder Torii", 1071 themeNeon: "ð Neo-Tokyo Neon", 1072 p1Name: "Player 1", 1073 p2Name: "Player 2", 1074 p3Name: "Player 3", 1075 p4Name: "Player 4", 1076 p1Solo: "1P Solo", 1077 p2Duo: "2P Duo (Versus)", 1078 restart: "Restart", 1079 play: "Play", 1080 pause: "Pause", 1081 setup: "Setup", 1082 topTaikoMasters: "TOP TAIKO MASTERS", 1083 leaderboard: "Leaderboard", 1084 noHighscoresYet: "No highscores yet!", 1085 beFirstToSetRecord: "Be the first Taiko Master to set a record!", 1086 emptySlot: "-", 1087 submittingScore: "Submitting score...", 1088 points: "pts", 1089 winner: "WINNER", 1090 teamTotalScore: "Team Total Score", 1091 teamMaxCombo: "Team Max Combo", 1092 greatLabel: "è¯ (Great)", 1093 goodLabel: "å¯ (Good)", 1094 missLabel: "ä¸å¯ (Miss)", 1095 rollsLabel: "飿 (Rolls)", 1096 competitiveBattle: "Competitive Battle", 1097 cooperativeTeam: "Cooperative Team", 1098 pressFOrJNow: "Press F or J now!", 1099 donCenter: "DON (Center)", 1100 tapDrumheadCenter: "Tap drumhead center", 1101 pressFAndJNow: "Press F + J now!", 1102 bigDonBothHands: "BIG DON (Both Hands)", 1103 strikeCenterBothHands: "Strike center with both hands!", 1104 pressDAndKNow: "Press D + K now!", 1105 bigKaBothRims: "BIG KA (Both Rims)", 1106 strikeRimsBothHands: "Strike rims with both hands!", 1107 pressDOrKNow: "Press D or K now!", 1108 kaRim: "KA (Rim)", 1109 tapDrumRimEdge: "Tap drum rim edge", 1110 mashFAndJFast: "Mash F & J fast!", 1111 drumrollTitle: "DRUMROLL", 1112 alternateRapidStrikes: "Alternate rapid strikes!", 1113 touchDrum: "Touch Drum", 1114 touchDrumPrompt: "Tap drum pads to play!", 1115 tabletRecommended: "Tablet or keyboard recommended for 2-Player mode", 1116 touchKaLeft: "Left Ka", 1117 touchDonLeft: "Left Don", 1118 touchDonRight: "Right Don", 1119 touchKaRight: "Right Ka", 1120 improvementSupport: "Improvement Support", 1121 section: "Section", 1122 practiceSection: "Practice this section", 1123 coachFullCombo: "Donderful! Flawless rhythm from start to finish! You are a true Taiko Master!", 1124 coachCleared: "Stage Cleared! Section {{sec}} had a few tricky notes. Practice that phrase to aim for a Full Combo!", 1125 coachFailed: "Donmai! Don't give up! Focus on Section {{sec}} where the tempo shifts and you'll clear it next time!", 1126 coachCoop: "Great teamwork! Both drummers played in sync!", 1127 tamashii: "Soul", 1128 clearThreshold: "Clear 80%", 1129 drumrolls: "Drumrolls", 1130 performanceTab: "Performance Report", 1131 leaderboardTab: "Top Masters" 1132 }, 1133 ja: { 1134 gameTitle: "太é¼ã®ããã¼", 1135 gameSubtitle: "太é¼ãªãºã ãã¸ã¥ã¢ã©ã¤ã¶ã¼",
1136 start: "ã¹ã¿ã¼ã (æ¼å¥éå§)", 1137 ready: "æºå", 1138 loadingAudio: "é³å£°ãèªã¿è¾¼ã¿ä¸...", 1139 lobby: "ããã¼", 1140 players: "åå 人æ°", 1141 difficulty: "é£æåº¦", 1142 stageTheme: "ã¹ãã¼ã¸èæ¯", 1143 speed: "ã°ããã (é度)", 1144 speedSlow: "0.8å (ãã£ãã)", 1145 speedNormal: "1.0å (æ¨æº)", 1146 speedFast: "1.2å (ã¯ãã)", 1147 speedVeryFast: "1.5å (è¶ ã¯ãã)", 1148 mode: "対æ¦ã¢ã¼ã", 1149 competitive: "å¯¾æ¦ (ããã«)", 1150 cooperative: "åå (ã¿ããªã§)", 1151 keyboardControls: "æä½æ¹æ³ (ãã¼ãã¼ã)", 1152 keyTip: "ãã³ã2ãã¼åææ¼ãã§å¤§é³ç¬¦ãå©ãã¾ãï¼", 1153 don: "ãã³", 1154 ka: "ã«ã", 1155 drumroll: "飿", 1156 balloon: "風è¹", 1157 perfect: "è¯", 1158 good: "å¯", 1159 miss: "ä¸å¯", 1160 combo: "ã³ã³ã", 1161 score: "ã¹ã³ã¢", 1162 maxCombo: "æå¤§ã³ã³ã", 1163 accuracy: "精度", 1164 source: "èé¢é³æº", 1165 sourceDrums: "GM ãã©ã ãã©ãã¯", 1166 sourceMelody: "ã¡ããã£ãªãºã æ½åº", 1167 sourceBeatGrid: "ãªã¼ããã¼ãã°ãªãã", 1168 resultsTitle: "æ¼å¥çµæçºè¡¨", 1169 clearCrown: "ã¯ãªã¢éæ", 1170 silverCrown: "ãã«ã³ã³ã", 1171 goldCrown: "ãã³ããã« (å ¨è¯)", 1172 retry: "ããä¸åéã¶", 1173 shareScore: "ã¹ã³ã¢ãå ±æ", 1174 kantan: "ãããã", 1175 futsuu: "ãµã¤ã", 1176 muzukashii: "ããããã", 1177 oni: "ãã«", 1178 themeMatsuri: "ð® å¤ç¥ãè±ç«", 1179 themeSakura: "ð¸ æ¡å¯å£«å·", 1180 themeThunder: "â¡ é·é³¥å± å±±", 1181 themeNeon: "ð ããªæ±äº¬", 1182 p1Name: "1P", 1183 p2Name: "2P", 1184 p3Name: "3P", 1185 p4Name: "4P", 1186 p1Solo: "1人ãã¬ã¤", 1187 p2Duo: "2人ãã¬ã¤ (対æ¦)", 1188 restart: "ãªã¹ã¿ã¼ã", 1189 play: "åç", 1190 pause: "䏿忢", 1191 setup: "è¨å®", 1192 topTaikoMasters: "太é¼é人ã©ã³ãã³ã°", 1193 leaderboard: "ãªã¼ãã¼ãã¼ã", 1194 noHighscoresYet: "ãã¤ã¹ã³ã¢ã¯ã¾ã ããã¾ããï¼", 1195 beFirstToSetRecord: "æåã®å¤ªé¼é人ã¨ãã¦è¨é²ãæ®ããï¼", 1196 emptySlot: "-", 1197 submittingScore: "ã¹ã³ã¢éä¿¡ä¸...", 1198 points: "ç¹", 1199 winner: "åè ", 1200 teamTotalScore: "ãã¼ã åè¨ã¹ã³ã¢", 1201 teamMaxCombo: "ãã¼ã æå¤§ã³ã³ã", 1202 greatLabel: "è¯ (Great)", 1203 goodLabel: "å¯ (Good)", 1204 missLabel: "ä¸å¯ (Miss)", 1205 rollsLabel: "飿 (Rolls)", 1206 competitiveBattle: "対æ¦ããã«", 1207 cooperativeTeam: "ååãã¼ã ", 1208 pressFOrJNow: "F ã¾ã㯠J ãå©ãã¦ï¼", 1209 donCenter: "ãã³ (é¢)", 1210 tapDrumheadCenter: "太é¼ã®é¢ãå©ã", 1211 pressFAndJNow: "F + J åææ¼ãï¼", 1212 bigDonBothHands: "大ãã³ (両æ)", 1213 strikeCenterBothHands: "両æã§å¤ªé¼ã®é¢ãå©ãï¼", 1214 pressDAndKNow: "D + K åææ¼ãï¼", 1215 bigKaBothRims: "大ã«ã (両æ)", 1216 strikeRimsBothHands: "両æã§å¤ªé¼ã®ãããå©ãï¼", 1217 pressDOrKNow: "D ã¾ã㯠K ãå©ãã¦ï¼", 1218 kaRim: "ã«ã (ãã)", 1219 tapDrumRimEdge: "太é¼ã®ãããå©ã", 1220 mashFAndJFast: "F 㨠J ã飿ï¼", 1221 drumrollTitle: "飿", 1222 alternateRapidStrikes: "ç´ æ©ã交äºã«å©ãï¼", 1223 touchDrum: "ã¿ãã太é¼", 1224 touchDrumPrompt: "太é¼ãå©ãã¦æ¼å¥ï¼", 1225 tabletRecommended: "2人ãã¬ã¤ã¯ã¿ãã¬ããã¾ãã¯ãã¼ãã¼ãæ¨å¥¨", 1226 touchKaLeft: "å·¦ ã«ã", 1227 touchDonLeft: "å·¦ ãã³", 1228 touchDonRight: "å³ ãã³", 1229 touchKaRight: "å³ ã«ã", 1230 improvementSupport: "æ¼å¥ãªãã¼ã", 1231 section: "åºé", 1232 practiceSection: "ãã®åºéãç·´ç¿", 1233 coachFullCombo: "ãã³ããã«ã³ã³ãï¼å ¨è¯ã®ç¥æ¥ã ãã³ï¼çã®å¤ªé¼ã®é人ã ï¼", 1234 coachCleared: "ã¯ãªã¢æåã ãã³ï¼ç¬¬{{sec}}åºéã«å°ãæããæé³ããã£ããã³ã復ç¿ãã¦ãã«ã³ã³ããç®æããï¼", 1235 coachFailed: "ã©ãã¾ãï¼è«¦ããããã¡ã ãã³ï¼ç¬¬{{sec}}åºéã®ãªãºã ãç·´ç¿ããã°æ¬¡ã¯å¿ ãã¯ãªã¢ã§ãããã³ï¼", 1236 coachCoop: "æ¯ã´ã£ããã®æ¼å¥ã ãã³ï¼äºäººã®å¿ãä¸ã¤ã«ãªã£ããã³ï¼", 1237 tamashii: "é", 1238 clearThreshold: "ã¯ãªã¢ 80%", 1239 drumrolls: "飿æ°", 1240 performanceTab: "æ¼å¥ãªãã¼ã", 1241 leaderboardTab: "ã©ã³ãã³ã°" 1242 }, 1243 "zh-CN": { 1244 gameTitle: "太é¼ä¹åç±³", 1245 gameSubtitle: "èå¥å¤ªé¼å¾å¨æ¸¸æ", 1246 start: "å¼å§æ¼å¥", 1247 ready: "åå¤", 1248 loadingAudio: "æ£å¨å è½½é³é¢...", 1249 lobby: "游æå¤§å ", 1250 players: "ç©å®¶äººæ°", 1251 difficulty: "é¾åº¦éæ©", 1252 stageTheme: "èå°ä¸»é¢", 1253 speed: "é³ç¬¦æµé", 1254 speedSlow: "0.8x (æ ¢é)", 1255 speedNormal: "1.0x (æ å)", 1256 speedFast: "1.2x (ç¨å¿«)", 1257 speedVeryFast: "1.5x (æé)", 1258 mode: "æ¸¸ææ¨¡å¼", 1259 competitive: "对æç«æ", 1260 cooperative: "åä½åå¥", 1261 keyboardControls: "é®çæä½è¯´æ", 1262 keyTip: "åæåæ¶æä¸åé®å¯è§¦å大é³ç¬¦ï¼", 1263 don: "å",
1264 ka: "å", 1265 drumroll: "è¿æ", 1266 balloon: "æ°ç", 1267 perfect: "è¯", 1268 good: "å¯", 1269 miss: "å·®", 1270 combo: "è¿å»", 1271 score: "å¾å", 1272 maxCombo: "æé«è¿å»", 1273 accuracy: "ç²¾å度", 1274 source: "è°±é¢æ¥æº", 1275 sourceDrums: "é¼ç¹é³è½¨", 1276 sourceMelody: "æå¾èå¥", 1277 sourceBeatGrid: "æå·èæ", 1278 resultsTitle: "æ¼å¥å®æï¼", 1279 clearCrown: "éå ³", 1280 silverCrown: "å ¨è¿å»", 1281 goldCrown: "å ¨é¨å®ç¾", 1282 retry: "忬¡æ¸¸ç©", 1283 shareScore: "å享æç»©", 1284 kantan: "ç®å", 1285 futsuu: "æ®é", 1286 muzukashii: "å°é¾", 1287 oni: "éç", 1288 themeMatsuri: "ð® 夿¥ç¥è±ç«", 1289 themeSakura: "ð¸ å¯å£«æ¨±è±æ²³", 1290 themeThunder: "â¡ é·é¸£é¸å± å³°", 1291 themeNeon: "ð èµåéè¹å", 1292 p1Name: "ç©å®¶ 1", 1293 p2Name: "ç©å®¶ 2", 1294 p3Name: "ç©å®¶ 3", 1295 p4Name: "ç©å®¶ 4", 1296 p1Solo: "å人æ¼å¥ (1P)", 1297 p2Duo: "å人对æ (2P)", 1298 restart: "éæ°å¼å§", 1299 play: "ææ¾", 1300 pause: "æå", 1301 setup: "设置", 1302 topTaikoMasters: "太é¼å¤§å¸æè¡æ¦", 1303 leaderboard: "æè¡æ¦", 1304 noHighscoresYet: "ææ é«åè®°å½ï¼", 1305 beFirstToSetRecord: "æä¸ºé¦ä½åä¸çºªå½ç太é¼å¤§å¸å§ï¼", 1306 emptySlot: "-", 1307 submittingScore: "æ£å¨æäº¤å¾å...", 1308 points: "å", 1309 winner: "è·èè ", 1310 teamTotalScore: "å¢éæ»å", 1311 teamMaxCombo: "å¢éæé«è¿å»", 1312 greatLabel: "è¯ (Great)", 1313 goodLabel: "å¯ (Good)", 1314 missLabel: "ä¸å¯ (Miss)", 1315 rollsLabel: "è¿æ (Rolls)", 1316 competitiveBattle: "å¯¹ææ¨¡å¼", 1317 cooperativeTeam: "åä½å¢é", 1318 pressFOrJNow: "ç«å³æ F æ Jï¼", 1319 donCenter: "å (é¼é¢)", 1320 tapDrumheadCenter: "è½»å»é¼é¢ä¸å¿", 1321 pressFAndJNow: "åæ¶æ F + Jï¼", 1322 bigDonBothHands: "大å (åæ)", 1323 strikeCenterBothHands: "åæåæ¶å»æé¼é¢ï¼", 1324 pressDAndKNow: "åæ¶æ D + Kï¼", 1325 bigKaBothRims: "大å (åæ)", 1326 strikeRimsBothHands: "åæåæ¶å»æé¼è¾¹ï¼", 1327 pressDOrKNow: "ç«å³æ D æ Kï¼", 1328 kaRim: "å (é¼è¾¹)", 1329 tapDrumRimEdge: "è½»å»é¼è¾¹è¾¹ç¼", 1330 mashFAndJFast: "å¿«éè¿æ F ä¸ Jï¼", 1331 drumrollTitle: "è¿æ", 1332 alternateRapidStrikes: "åæå¿«é交æ¿è¿å»ï¼", 1333 touchDrum: "触æ¸é¼", 1334 touchDrumPrompt: "ç¹å»é¼é¢å¼å§æ¼å¥ï¼", 1335 tabletRecommended: "å人模å¼å»ºè®®ä½¿ç¨å¹³æ¿æé®ç", 1336 touchKaLeft: "å·¦ å", 1337 touchDonLeft: "å·¦ å", 1338 touchDonRight: "å³ å", 1339 touchKaRight: "å³ å", 1340 improvementSupport: "æ¼å¥åææ¥å", 1341 section: "åºé´", 1342 practiceSection: "ç»ä¹ æ¤åºé´", 1343 coachFullCombo: "太ç¥å¦ï¼å ¨æ²æ å¤±è¯¯å ¨è¿å»ï¼ä½ å°±æ¯çæ£ç太é¼è¾¾äººï¼", 1344 coachCleared: "éå ³æåï¼ç¬¬{{sec}}åºé´æå å¤å¤±æï¼é对ç»ä¹ å³å¯å²å»å ¨è¿ï¼", 1345 coachFailed: "å«ç°å¿ï¼ç¬¬{{sec}}åºé´çèå¥ç¨æé¾åº¦ï¼é对ç»ä¹ 䏿¬¡ä¸å®è½è¿å ³ï¼", 1346 coachCoop: "é»å¥ç»ä½³ï¼ä¸¤ä½é¼æçé å天衣æ ç¼ï¼", 1347 tamashii: "é", 1348 clearThreshold: "éå ³ 80%", 1349 drumrolls: "è¿ææ°", 1350 performanceTab: "æ¼å¥æ¥å", 1351 leaderboardTab: "髿æ¦" 1352 }, 1353 "zh-TW": { 1354 gameTitle: "太é¼ä¹åç±³", 1355 gameSubtitle: "ç¯å¥å¤ªé¼å¾åéæ²", 1356 start: "éå§æ¼å¥", 1357 ready: "æºå", 1358 loadingAudio: "æ£å¨è¼å ¥é³è¨...", 1359 lobby: "鿲大廳", 1360 players: "ç©å®¶äººæ¸", 1361 difficulty: "é£åº¦é¸æ", 1362 stageTheme: "èå°ä¸»é¡", 1363 speed: "é³ç¬¦æµé", 1364 speedSlow: "0.8x (æ ¢é)", 1365 speedNormal: "1.0x (æ¨æº)", 1366 speedFast: "1.2x (ç¨å¿«)", 1367 speedVeryFast: "1.5x (極é)", 1368 mode: "éæ²æ¨¡å¼", 1369 competitive: "å°æ°æ¨¡å¼", 1370 cooperative: "å使¼å¥", 1371 keyboardControls: "éµç¤æä½æå", 1372 keyTip: "åææä¸å ©åãåãæéµå¯æ²æå¤§é³ç¬¦ï¼", 1373 don: "å", 1374 ka: "å", 1375 drumroll: "飿", 1376 balloon: "æ°£ç", 1377 perfect: "è¯", 1378 good: "å¯", 1379 miss: "ä¸å¯", 1380 combo: "飿®µ", 1381 score: "å¾å", 1382 maxCombo: "æå¤§é£æ®µ", 1383 accuracy: "ç²¾æºåº¦", 1384 source: "èé¢ä¾æº", 1385 sourceDrums: "GM é¼çµè»é", 1386 sourceMelody: "æå¾ç¯å¥æå", 1387 sourceBeatGrid: "èªåç¯æå¾å", 1388 resultsTitle: "æ¼å¥æç¸¾çµç®", 1389 clearCrown: "éééæ", 1390 silverCrown: "å ¨é£æ®µéæ", 1391 goldCrown: "åå å ¨è¯", 1392 retry: "忬¡éç©", 1393 shareScore: "å享æç¸¾", 1394 kantan: "ç°¡å®", 1395 futsuu: "æ®é", 1396 muzukashii: "å°é£", 1397 oni: "éç", 1398 themeMatsuri: "ð® 夿¥ç¥è±ç«", 1399 themeSakura: "ð¸ å¯å£«æ«»è±æ²³", 1400 themeThunder: "â¡ é·é³´é³¥å± å³°", 1401 themeNeon: "ð è³½åéè¹å", 1402 p1Name: "ç©å®¶ 1", 1403 p2Name: "ç©å®¶ 2", 1404 p3Name: "ç©å®¶ 3", 1405 p4Name: "ç©å®¶ 4", 1406 p1Solo: "å®äººæ¼å¥ (1P)", 1407 p2Duo: "éäººå°æ° (2P)", 1408 restart: "éæ°éå§", 1409 play: "ææ¾", 1410 pause: "æ«å", 1411 setup: "è¨å®", 1412 topTaikoMasters: "太é¼å¤§å¸«æè¡æ¦", 1413 leaderboard: "æè¡æ¦", 1414 noHighscoresYet: "æ«ç¡é«åç´éï¼", 1415 beFirstToSetRecord: "æçºé¦ä½åµä¸ç´éç太é¼å¤§å¸«å§ï¼", 1416 emptySlot: "-", 1417 submittingScore: "æ£å¨æäº¤å¾å...", 1418 points: "å", 1419 winner: "ç²åè ",
1420 teamTotalScore: "åé總å", 1421 teamMaxCombo: "åéæé«é£æ", 1422 greatLabel: "è¯ (Great)", 1423 goodLabel: "å¯ (Good)", 1424 missLabel: "ä¸å¯ (Miss)", 1425 rollsLabel: "飿 (Rolls)", 1426 competitiveBattle: "å°æ°æ¨¡å¼", 1427 cooperativeTeam: "åä½åé", 1428 pressFOrJNow: "ç«å³æ F æ Jï¼", 1429 donCenter: "å (é¼é¢)", 1430 tapDrumheadCenter: "æ²æé¼é¢ä¸å¿", 1431 pressFAndJNow: "åææ F + Jï¼", 1432 bigDonBothHands: "大å (éæ)", 1433 strikeCenterBothHands: "éæåæææé¼é¢ï¼", 1434 pressDAndKNow: "åææ D + Kï¼", 1435 bigKaBothRims: "大å (éæ)", 1436 strikeRimsBothHands: "éæåæææé¼éï¼", 1437 pressDOrKNow: "ç«å³æ D æ Kï¼", 1438 kaRim: "å (é¼é)", 1439 tapDrumRimEdge: "æ²æé¼ééç·£", 1440 mashFAndJFast: "å¿«é飿 F è Jï¼", 1441 drumrollTitle: "飿", 1442 alternateRapidStrikes: "éæå¿«é交æ¿é£æï¼", 1443 touchDrum: "觸æ§é¼", 1444 touchDrumPrompt: "黿é¼é¢éå§æ¼å¥ï¼", 1445 tabletRecommended: "é人模å¼å»ºè°ä½¿ç¨å¹³æ¿æéµç¤", 1446 touchKaLeft: "å·¦ å", 1447 touchDonLeft: "å·¦ å", 1448 touchDonRight: "å³ å", 1449 touchKaRight: "å³ å", 1450 improvementSupport: "æ¼å¥åæå ±å", 1451 section: "åé", 1452 practiceSection: "ç·´ç¿æ¤åé", 1453 coachFullCombo: "太ç¥å¦ï¼å ¨æ²ç¡å¤±èª¤å ¨é£æï¼ä½ å°±æ¯çæ£ç太é¼é人ï¼", 1454 coachCleared: "ééæåï¼ç¬¬{{sec}}åéæå¹¾èç¨ä¸æ³¨æçæ²æï¼å¤å ç·´ç¿å³å¯è¡æå ¨é£ï¼", 1455 coachFailed: "奿°£é¤ï¼ç¬¬{{sec}}åéçç¯å¥ç¨æé£åº¦ï¼éå°ç·´ç¿ä¸æ¬¡ä¸å®è½é å©ééï¼", 1456 coachCoop: "é»å¥çµä½³ï¼å ©ä½é¼æçé å天衣ç¡ç¸«ï¼", 1457 tamashii: "é", 1458 clearThreshold: "éé 80%", 1459 drumrolls: "飿æ¸", 1460 performanceTab: "æ¼å¥å ±å", 1461 leaderboardTab: "髿æ¦" 1462 }, 1463 ko: { 1464 gameTitle: "íì´ì½ ë ¸ í미", 1465 gameSubtitle: "ë¦¬ë¬ ë¶ ìê°í ê²ì", 1466 start: "ì°ì£¼ ìì", 1467 ready: "ì¤ë¹", 1468 loadingAudio: "ì¤ëì¤ ë¡ë© ì¤...", 1469 lobby: "ë¡ë¹", 1470 players: "íë ì´ì´ ì", 1471 difficulty: "ëì´ë", 1472 stageTheme: "ì¤í ì´ì§ ë°°ê²½", 1473 speed: "ìë ì¡°ì ", 1474 speedSlow: "0.8x (ë림)", 1475 speedNormal: "1.0x (ë³´íµ)", 1476 speedFast: "1.2x (ë¹ ë¦)", 1477 speedVeryFast: "1.5x (ë§¤ì° ë¹ ë¦)", 1478 mode: "ê²ì 모ë", 1479 competitive: "ë°°í (ê²½ì)", 1480 cooperative: "ííë ì´ (íë)", 1481 keyboardControls: "í¤ë³´ë ì¡°ìë²", 1482 keyTip: "ììì ë í¤ë¥¼ ëìì ë르면 í° ìí í격!", 1483 don: "쿵(ë)", 1484 ka: "ë±(ì¹´)", 1485 drumroll: "ì°í", 1486 balloon: "íì !", 1487 perfect: "ì¼ì¨êµ¬(è¯)", 1488 good: "ì¢ë¤(å¯)", 1489 miss: "ì구(ä¸å¯)", 1490 combo: "ì°ì ì±ê³µ", 1491 score: "ì ì", 1492 maxCombo: "ìµë 콤보", 1493 accuracy: "ì íë", 1494 source: "ì±ë³´ ì¶ì²", 1495 sourceDrums: "ëë¼ í¸ë", 1496 sourceMelody: "ë©ë¡ë 리ë¬", 1497 sourceBeatGrid: "ë°ì 그리ë", 1498 resultsTitle: "ì°ì£¼ ìë£!", 1499 clearCrown: "í´ë¦¬ì´", 1500 silverCrown: "í 콤보", 1501 goldCrown: "ì ë(í¼íí¸)", 1502 retry: "ë¤ì í기", 1503 shareScore: "ì ì ê³µì ", 1504 kantan: "ì´ê¸", 1505 futsuu: "ë³´íµ", 1506 muzukashii: "ì´ë ¤ì", 1507 oni: "ê·ì ", 1508 themeMatsuri: "ð® ì¶ì ë¶ê½ëì´", 1509 themeSakura: "ð¸ ë²ê½ íì§ê°", 1510 themeThunder: "â¡ ë²ê° í 리ì´", 1511 themeNeon: "ð ë¤ì¤ ëì¿", 1512 p1Name: "íë ì´ì´ 1", 1513 p2Name: "íë ì´ì´ 2", 1514 p3Name: "íë ì´ì´ 3", 1515 p4Name: "íë ì´ì´ 4", 1516 restart: "ë¤ì ìì", 1517 play: "ì¬ì", 1518 pause: "ì¼ìì ì§", 1519 setup: "ì¤ì ", 1520 topTaikoMasters: "íê³ ë§ì¤í° ììí", 1521 leaderboard: "ììí", 1522 noHighscoresYet: "ìì§ ìµê³ 기ë¡ì´ ììµëë¤!", 1523 beFirstToSetRecord: "첫 ë²ì§¸ íê³ ë§ì¤í°ë¡ 기ë¡ì ë¨ê²¨ë³´ì¸ì!", 1524 emptySlot: "-", 1525 submittingScore: "ì ì ë±ë¡ ì¤...", 1526 points: "ì ", 1527 winner: "ì°ì¹ì", 1528 teamTotalScore: "í ì´ì ", 1529 teamMaxCombo: "í ìµë 콤보", 1530 greatLabel: "ì¼ì¨êµ¬ (Great)", 1531 goodLabel: "ì¢ë¤ (Good)", 1532 missLabel: "ì구 (Miss)", 1533 rollsLabel: "ì°í (Rolls)", 1534 competitiveBattle: "ëì ë°°í", 1535 cooperativeTeam: "íë í", 1536 pressFOrJNow: "ì§ê¸ F ëë J를 ë르ì¸ì!", 1537 donCenter: "쿵 (ë¶ë©´)", 1538 tapDrumheadCenter: "ë¶ë©´ ì¤ì ì¹ê¸°", 1539 pressFAndJNow: "F + J ëìì ë르기!", 1540 bigDonBothHands: "ë 쿵 (ìì)", 1541 strikeCenterBothHands: "ììì¼ë¡ ë¶ë©´ ì¤ì ì¹ê¸°!", 1542 pressDAndKNow: "D + K ëìì ë르기!", 1543 bigKaBothRims: "ë ë± (ìì)", 1544 strikeRimsBothHands: "ììì¼ë¡ ë¶ í ë리 ì¹ê¸°!", 1545 pressDOrKNow: "ì§ê¸ D ëë K를 ë르ì¸ì!", 1546 kaRim: "ë± (í ë리)", 1547 tapDrumRimEdge: "ë¶ í ë리 ì¹ê¸°", 1548 mashFAndJFast: "Fì J를 ë¹ ë¥´ê² ì°í!", 1549 drumrollTitle: "ì°í", 1550 alternateRapidStrikes: "ììì¼ë¡ ë¹ ë¥´ê² ë²ê°ì ì¹ê¸°!", 1551 touchDrum: "í°ì¹ ë¶", 1552 touchDrumPrompt: "ë¶ì í°ì¹íì¬ ì°ì£¼íì¸ì!", 1553 tabletRecommended: "2ì¸ íë ì´ë íë¸ë¦¿ ëë í¤ë³´ë를 ê¶ì¥í©ëë¤", 1554 touchKaLeft: "ì¼ìª½ ë±", 1555 touchDonLeft: "ì¼ìª½ 쿵", 1556 touchDonRight: "ì¤ë¥¸ìª½ 쿵", 1557 touchKaRight: "ì¤ë¥¸ìª½ ë±", 1558 improvementSupport: "ì°ì£¼ 리í¬í¸", 1559 section: "구ê°", 1560 practiceSection: "ì´ êµ¬ê° ì°ìµí기", 1561 coachFullCombo: "ëë¨í´ì! ì²ìë¶í° ëê¹ì§ ìë²½í í콤보! ì§ì í íê³ ì ë¬ì¸ì ëë¤!", 1562 coachCleared: "í´ë¦¬ì´ ì±ê³µ! ì {{sec}}구ê°ì ìì¬ì´ ë ¸í¸ê° ììì´ì. ë³µìµí´ì í콤보ì ëì í´ë³´ì¸ì!", 1563 coachFailed: "ìì¬ìì! í¬ê¸°íì§ ë§ì¸ì! ì {{sec}}êµ¬ê° ë¦¬ë¬ì ì°ìµíë©´ ë¤ìì ê¼ í´ë¦¬ì´í ì ìì´ì!", 1564 coachCoop: "íìì í¸í¡! ë ì¬ëì 리ë¬ì´ ìë²½íê² íëê° ëìì´ì!", 1565 tamashii: "í¼", 1566 clearThreshold: "í´ë¦¬ì´ 80%", 1567 drumrolls: "ì°í ì", 1568 performanceTab: "ì°ì£¼ 리í¬í¸", 1569 leaderboardTab: "ëí¹" 1570 }, 1571 es: { 1572 gameTitle: "Taiko no Hamie", 1573 gameSubtitle: "VISUALIZADOR DE RITMO TAIKO", 1574 start: "COMENZAR PARTIDA", 1575 ready: "LISTO", 1576 loadingAudio: "Cargando audio...", 1577 lobby: "Sala", 1578 players: "JUGADORES", 1579 difficulty: "DIFICULTAD", 1580 stageTheme: "ESCENARIO", 1581 speed: "VELOCIDAD", 1582 speedSlow: "0.8x (Lento)", 1583 speedNormal: "1.0x (Normal)", 1584 speedFast: "1.2x (Rápido)", 1585 speedVeryFast: "1.5x (Muy rápido)", 1586 mode: "MODO DE JUEGO",
1587 competitive: "Batalla (Competitivo)", 1588 cooperative: "Equipo (Cooperativo)", 1589 keyboardControls: "CONTROLES DE TECLADO", 1590 keyTip: "¡Pulsa ambas teclas Don al mismo tiempo para el Gran Don!", 1591 don: "DON", 1592 ka: "KA", 1593 drumroll: "REDOMBLE", 1594 balloon: "¡GLOBO!", 1595 perfect: "¡PERFECTO!", 1596 good: "BIEN", 1597 miss: "FALLO", 1598 combo: "COMBO", 1599 score: "Puntuación", 1600 maxCombo: "Combo Máx", 1601 accuracy: "Precisión", 1602 source: "Origen", 1603 sourceDrums: "BaterÃa MIDI", 1604 sourceMelody: "Ritmo Melódico", 1605 sourceBeatGrid: "Rejilla de Compás", 1606 resultsTitle: "¡ESCENARIO SUPERADO!", 1607 clearCrown: "SUPERADO", 1608 silverCrown: "COMBO COMPLETO", 1609 goldCrown: "TODO PERFECTO", 1610 retry: "JUGAR DE NUEVO", 1611 shareScore: "COMPARTIR RESULTADO", 1612 kantan: "FÃCIL", 1613 futsuu: "NORMAL", 1614 muzukashii: "DIFÃCIL", 1615 oni: "EXTREMO", 1616 themeMatsuri: "ð® Fiesta de Farolillos", 1617 themeSakura: "ð¸ RÃo de Cerezos", 1618 themeThunder: "â¡ Monte del Trueno", 1619 themeNeon: "ð Neón Neo-Tokio", 1620 p1Name: "Jugador 1", 1621 p2Name: "Jugador 2", 1622 p3Name: "Jugador 3", 1623 p4Name: "Jugador 4", 1624 restart: "Reiniciar", 1625 play: "Reproducir", 1626 pause: "Pausar", 1627 setup: "Ajustes", 1628 topTaikoMasters: "MEJORES MAESTROS TAIKO", 1629 leaderboard: "Clasificación", 1630 noHighscoresYet: "¡Sin puntuaciones aún!", 1631 beFirstToSetRecord: "¡Sé el primer maestro Taiko en establecer un récord!", 1632 emptySlot: "-", 1633 submittingScore: "Enviando puntuación...", 1634 points: "pts", 1635 winner: "GANADOR", 1636 teamTotalScore: "Puntuación Total del Equipo", 1637 teamMaxCombo: "Combo Máximo del Equipo", 1638 greatLabel: "è¯ (Excelente)", 1639 goodLabel: "å¯ (Bien)", 1640 missLabel: "ä¸å¯ (Fallo)", 1641 rollsLabel: "飿 (Redobles)", 1642 competitiveBattle: "Batalla Competitiva", 1643 cooperativeTeam: "Equipo Cooperativo", 1644 pressFOrJNow: "¡Pulsa F o J ahora!", 1645 donCenter: "DON (Centro)", 1646 tapDrumheadCenter: "Golpea el centro del parche", 1647 pressFAndJNow: "¡Pulsa F + J ahora!", 1648 bigDonBothHands: "GRAN DON (Dos manos)", 1649 strikeCenterBothHands: "¡Golpea el centro con ambas manos!", 1650 pressDAndKNow: "¡Pulsa D + K ahora!", 1651 bigKaBothRims: "GRAN KA (Dos manos)", 1652 strikeRimsBothHands: "¡Golpea el borde con ambas manos!", 1653 pressDOrKNow: "¡Pulsa D o K ahora!", 1654 kaRim: "KA (Borde)", 1655 tapDrumRimEdge: "Golpea el borde del tambor", 1656 mashFAndJFast: "¡Repite F y J rápido!", 1657 drumrollTitle: "REDOBLE", 1658 alternateRapidStrikes: "¡Golpes rápidos alternados!", 1659 touchDrum: "Tambor Táctil", 1660 touchDrumPrompt: "¡Toca los tambores para tocar!", 1661 tabletRecommended: "Se recomienda tableta o teclado para 2 jugadores", 1662 touchKaLeft: "Ka Izq", 1663 touchDonLeft: "Don Izq", 1664 touchDonRight: "Don Der", 1665 touchKaRight: "Ka Der", 1666 improvementSupport: "Informe de Rendimiento", 1667 section: "Sección", 1668 practiceSection: "Practicar esta sección", 1669 coachFullCombo: "¡IncreÃble! ¡Combo completo impecable de principio a fin! ¡Eres un verdadero Maestro Taiko!", 1670 coachCleared: "¡Nivel superado! La sección {{sec}} tuvo algunas notas difÃciles. ¡PractÃcala para lograr el Combo Completo!", 1671 coachFailed: "¡Ãnimo! ¡No te rindas! ¡Concéntrate en la sección {{sec}} y lo lograrás la próxima vez!", 1672 coachCoop: "¡Gran trabajo en equipo! ¡Ambos tocaron en perfecta armonÃa!", 1673 tamashii: "Alma", 1674 clearThreshold: "Superado 80%", 1675 drumrolls: "Redobles", 1676 performanceTab: "Informe", 1677 leaderboardTab: "Clasificación" 1678 }, 1679 fr: { 1680 gameTitle: "Taiko no Hamie", 1681 gameSubtitle: "VISUALISEUR DE RYTHME TAIKO", 1682 start: "COMMENCER", 1683 ready: "PRÃT", 1684 loadingAudio: "Chargement de l'audio...", 1685 lobby: "Salon", 1686 players: "JOUEURS", 1687 difficulty: "DIFFICULTÃ", 1688 stageTheme: "THÃME DU DÃCOR", 1689 speed: "VITESSE", 1690 speedSlow: "0.8x (Lent)", 1691 speedNormal: "1.0x (Normal)", 1692 speedFast: "1.2x (Rapide)", 1693 speedVeryFast: "1.5x (Très rapide)", 1694 mode: "MODE DE JEU", 1695 competitive: "Bataille (Compétitif)", 1696 cooperative: "Ãquipe (Coopératif)", 1697 keyboardControls: "CONTRÃLES DU CLAVIER", 1698 keyTip: "Appuyez sur les deux touches Don en même temps pour le Gros Don !", 1699 don: "DON", 1700 ka: "KA", 1701 drumroll: "ROULEMENT", 1702 balloon: "BALLON !", 1703 perfect: "PARFAIT !", 1704 good: "BIEN", 1705 miss: "MANQUÃ", 1706 combo: "COMBO", 1707 score: "Score", 1708 maxCombo: "Combo Max", 1709 accuracy: "Précision", 1710 source: "Source", 1711 sourceDrums: "Batterie MIDI", 1712 sourceMelody: "Rythme mélodique", 1713 sourceBeatGrid: "Grille de tempo", 1714 resultsTitle: "MORCEAU TERMINà !", 1715 clearCrown: "RÃUSSI", 1716 silverCrown: "COMBO COMPLET", 1717 goldCrown: "TOUT PARFAIT", 1718 retry: "REJOUER", 1719 shareScore: "PARTAGER LE SCORE", 1720 kantan: "FACILE", 1721 futsuu: "NORMAL", 1722 muzukashii: "DIFFICILE", 1723 oni: "EXTRÃME", 1724 themeMatsuri: "ð® Fête des Lanternes", 1725 themeSakura: "ð¸ Rivière des Cerisiers", 1726 themeThunder: "â¡ Montagne du Tonnerre", 1727 themeNeon: "ð Néo-Tokyo Néon", 1728 p1Name: "Joueur 1", 1729 p2Name: "Joueur 2", 1730 p3Name: "Joueur 3", 1731 p4Name: "Joueur 4", 1732 restart: "Recommencer",
1733 play: "Lecture", 1734 pause: "Pause", 1735 setup: "Configuration", 1736 topTaikoMasters: "TOP MAÃTRES TAIKO", 1737 leaderboard: "Classement", 1738 noHighscoresYet: "Aucun record pour le moment !", 1739 beFirstToSetRecord: "Soyez le premier maître Taiko à établir un record !", 1740 emptySlot: "-", 1741 submittingScore: "Envoi du score...", 1742 points: "pts", 1743 winner: "GAGNANT", 1744 teamTotalScore: "Score Total de l'Ãquipe", 1745 teamMaxCombo: "Combo Max de l'Ãquipe", 1746 greatLabel: "è¯ (Parfait)", 1747 goodLabel: "å¯ (Bien)", 1748 missLabel: "ä¸å¯ (Manqué)", 1749 rollsLabel: "飿 (Roulements)", 1750 competitiveBattle: "Bataille Compétitive", 1751 cooperativeTeam: "Ãquipe Coopérative", 1752 pressFOrJNow: "Appuyez sur F ou J !", 1753 donCenter: "DON (Centre)", 1754 tapDrumheadCenter: "Frappez le centre du tambour", 1755 pressFAndJNow: "Appuyez sur F + J !", 1756 bigDonBothHands: "GRAND DON (Deux mains)", 1757 strikeCenterBothHands: "Frappez le centre à deux mains !", 1758 pressDAndKNow: "Appuyez sur D + K !", 1759 bigKaBothRims: "GRAND KA (Deux mains)", 1760 strikeRimsBothHands: "Frappez le rebord à deux mains !", 1761 pressDOrKNow: "Appuyez sur D ou K !", 1762 kaRim: "KA (Rebord)", 1763 tapDrumRimEdge: "Frappez le rebord du tambour", 1764 mashFAndJFast: "Mitraillez F et J vite !", 1765 drumrollTitle: "ROULEMENT", 1766 alternateRapidStrikes: "Frappes rapides alternées !", 1767 touchDrum: "Tambour Tactile", 1768 touchDrumPrompt: "Touchez le tambour pour jouer !", 1769 tabletRecommended: "Tablette ou clavier recommandé pour le mode 2 joueurs", 1770 touchKaLeft: "Ka Gauche", 1771 touchDonLeft: "Don Gauche", 1772 touchDonRight: "Don Droit", 1773 touchKaRight: "Ka Droit", 1774 improvementSupport: "Rapport de Performance", 1775 section: "Section", 1776 practiceSection: "S'entraîner sur cette section", 1777 coachFullCombo: "Incroyable ! Combo complet parfait du début à la fin ! Vous êtes un véritable Maître Taiko !", 1778 coachCleared: "Morceau réussi ! La section {{sec}} comportait quelques notes délicates. Entraînez-vous pour le Combo Complet !", 1779 coachFailed: "Courage ! N'abandonnez pas ! Révisez la section {{sec}} et vous réussirez la prochaine fois !", 1780 coachCoop: "Superbe travail d'équipe ! Les deux joueurs étaient parfaitement synchronisés !", 1781 tamashii: "Ãme", 1782 clearThreshold: "Succès 80%", 1783 drumrolls: "Roulements", 1784 performanceTab: "Rapport", 1785 leaderboardTab: "Classement" 1786 }, 1787 de: { 1788 gameTitle: "Taiko no Hamie", 1789 gameSubtitle: "TAIKO-RHYTHMUS-VISUALISIERER", 1790 start: "SPIEL STARTEN", 1791 ready: "BEREIT", 1792 loadingAudio: "Audio wird geladen...", 1793 lobby: "Lobby", 1794 players: "SPIELER", 1795 difficulty: "SCHWIERIGKEIT", 1796 stageTheme: "BÃHNENTHEMA", 1797 speed: "GESCHWINDIGKEIT", 1798 speedSlow: "0.8x (Langsam)", 1799 speedNormal: "1.0x (Normal)", 1800 speedFast: "1.2x (Flott)", 1801 speedVeryFast: "1.5x (Schnell)", 1802 mode: "SPIELMODUS", 1803 competitive: "Wettkampf (Duell)", 1804 cooperative: "Team (Koop)", 1805 keyboardControls: "TASTATUR-STEUERUNG", 1806 keyTip: "Drücke beide Don-Tasten gleichzeitig für den GroÃen Don!", 1807 don: "DON", 1808 ka: "KA", 1809 drumroll: "WIRBEL", 1810 balloon: "BALLON!", 1811 perfect: "PERFEKT!", 1812 good: "GUT", 1813 miss: "FEHLER", 1814 combo: "COMBO", 1815 score: "Punkte", 1816 maxCombo: "Max Combo", 1817 accuracy: "Genauigkeit", 1818 source: "Quelle", 1819 sourceDrums: "MIDI-Schlagzeug", 1820 sourceMelody: "Melodie-Rhythmus", 1821 sourceBeatGrid: "Takt-Raster", 1822 resultsTitle: "LEVEL GESCHAFFT!", 1823 clearCrown: "GESCHAFFT", 1824 silverCrown: "FULL COMBO", 1825 goldCrown: "ALLES PERFEKT", 1826 retry: "ERNEUT SPIELEN", 1827 shareScore: "PUNKTE TEILEN", 1828 kantan: "LEICHT", 1829 futsuu: "NORMAL", 1830 muzukashii: "SCHWER", 1831 oni: "EXTREM", 1832 themeMatsuri: "ð® Laternenfest", 1833 themeSakura: "ð¸ Kirschblüten-Fluss", 1834 themeThunder: "â¡ Donnerberg", 1835 themeNeon: "ð Neo-Tokio Neon", 1836 p1Name: "Spieler 1", 1837 p2Name: "Spieler 2", 1838 p3Name: "Spieler 3", 1839 p4Name: "Spieler 4", 1840 restart: "Neustart", 1841 play: "Wiedergabe", 1842 pause: "Pause", 1843 setup: "Setup", 1844 topTaikoMasters: "TOP TAIKO-MEISTER", 1845 leaderboard: "Bestenliste", 1846 noHighscoresYet: "Noch keine Rekorde vorhanden!", 1847 beFirstToSetRecord: "Sei der erste Taiko-Meister, der einen Rekord aufstellt!", 1848 emptySlot: "-", 1849 submittingScore: "Punktzahl wird übermittelt...", 1850 points: "Pkt.", 1851 winner: "SIEGER", 1852 teamTotalScore: "Gesamtpunktzahl des Teams", 1853 teamMaxCombo: "Maximaler Team-Combo", 1854 greatLabel: "è¯ (Perfekt)", 1855 goodLabel: "å¯ (Gut)", 1856 missLabel: "ä¸å¯ (Verfehlt)", 1857 rollsLabel: "飿 (Trommelwirbel)", 1858 competitiveBattle: "Wettkampf-Schlacht", 1859 cooperativeTeam: "Kooperatives Team", 1860 pressFOrJNow: "Jetzt F oder J drücken!", 1861 donCenter: "DON (Mitte)", 1862 tapDrumheadCenter: "Mitte des Trommelfells treffen", 1863 pressFAndJNow: "Jetzt F + J drücken!", 1864 bigDonBothHands: "GROSSER DON (Beide Hände)", 1865 strikeCenterBothHands: "Mit beiden Händen in die Mitte schlagen!", 1866 pressDAndKNow: "Jetzt D + K drücken!", 1867 bigKaBothRims: "GROSSER KA (Beide Hände)",
1868 strikeRimsBothHands: "Mit beiden Händen auf den Rand schlagen!", 1869 pressDOrKNow: "Jetzt D oder K drücken!", 1870 kaRim: "KA (Rand)", 1871 tapDrumRimEdge: "Auf den Trommelrand schlagen", 1872 mashFAndJFast: "F & J schnell hämmern!", 1873 drumrollTitle: "WIRBEL", 1874 alternateRapidStrikes: "Schnelle abwechselnde Schläge!", 1875 touchDrum: "Touch-Trommel", 1876 touchDrumPrompt: "Trommelpads berühren zum Spielen!", 1877 tabletRecommended: "Tablet oder Tastatur für 2 Spieler empfohlen", 1878 touchKaLeft: "Links Ka", 1879 touchDonLeft: "Links Don", 1880 touchDonRight: "Rechts Don", 1881 touchKaRight: "Rechts Ka", 1882 improvementSupport: "Leistungsbericht", 1883 section: "Abschnitt", 1884 practiceSection: "Diesen Abschnitt üben", 1885 coachFullCombo: "Unglaublich! Perfekte Full Combo von Anfang bis Ende! Ein wahrer Taiko-Meister!", 1886 coachCleared: "Level geschafft! In Abschnitt {{sec}} gab es ein paar knifflige Noten. Ãbe diesen Teil für die Full Combo!", 1887 coachFailed: "Kopf hoch! Nicht aufgeben! Konzentriere dich auf Abschnitt {{sec}}, dann klappt es beim nächsten Mal!", 1888 coachCoop: "GroÃartiges Teamwork! Beide Spieler waren perfekt im Takt!", 1889 tamashii: "Seele", 1890 clearThreshold: "Geschafft 80%", 1891 drumrolls: "Wirbel", 1892 performanceTab: "Bericht", 1893 leaderboardTab: "Bestenliste" 1894 }, 1895 it: { 1896 gameTitle: "Taiko no Hamie", 1897 gameSubtitle: "VISUALIZZATORE DI RITMO TAIKO", 1898 start: "INIZIA GIOCO", 1899 ready: "PRONTO", 1900 loadingAudio: "Caricamento audio...", 1901 lobby: "Atrio", 1902 players: "GIOCATORI", 1903 difficulty: "DIFFICOLTÃ", 1904 stageTheme: "TEMA DEL PALCO", 1905 speed: "VELOCITÃ", 1906 speedSlow: "0.8x (Lento)", 1907 speedNormal: "1.0x (Normale)", 1908 speedFast: "1.2x (Veloce)", 1909 speedVeryFast: "1.5x (Molto veloce)", 1910 mode: "MODALITà DI GIOCO", 1911 competitive: "Battaglia (Competitivo)", 1912 cooperative: "Squadra (Cooperativo)", 1913 keyboardControls: "CONTROLLI DA TASTIERA", 1914 keyTip: "Premi entrambi i tasti Don contemporaneamente per il Grande Don!", 1915 don: "DON", 1916 ka: "KA", 1917 drumroll: "RULLO", 1918 balloon: "PALLONCINO!", 1919 perfect: "PERFETTO!", 1920 good: "BUONO", 1921 miss: "MANCATO", 1922 combo: "COMBO", 1923 score: "Punteggio", 1924 maxCombo: "Combo Max", 1925 accuracy: "Precisione", 1926 source: "Sorgente", 1927 sourceDrums: "Batteria MIDI", 1928 sourceMelody: "Ritmo Melodico", 1929 sourceBeatGrid: "Griglia di Battuta", 1930 resultsTitle: "BRANO COMPLETATO!", 1931 clearCrown: "SUPERATO", 1932 silverCrown: "FULL COMBO", 1933 goldCrown: "TUTTO PERFETTO", 1934 retry: "GIOCA ANCORA", 1935 shareScore: "CONDIVIDI PUNTEGGIO", 1936 kantan: "FACILE", 1937 futsuu: "NORMALE", 1938 muzukashii: "DIFFICILE", 1939 oni: "ESTREMO", 1940 themeMatsuri: "ð® Festival delle Lanterne",
1941 themeSakura: "ð¸ Fiume dei Ciliegi", 1942 themeThunder: "â¡ Monte del Tuono", 1943 themeNeon: "ð Neo-Tokyo Neon", 1944 p1Name: "Giocatore 1", 1945 p2Name: "Giocatore 2", 1946 p3Name: "Giocatore 3", 1947 p4Name: "Giocatore 4", 1948 restart: "Riavvia", 1949 play: "Riproduci", 1950 pause: "Pausa", 1951 setup: "Configura", 1952 topTaikoMasters: "TOP MAESTRI TAIKO", 1953 leaderboard: "Classifica", 1954 noHighscoresYet: "Nessun record registrato!", 1955 beFirstToSetRecord: "Sii il primo Maestro Taiko a segnare un record!", 1956 emptySlot: "-", 1957 submittingScore: "Invio punteggio...", 1958 points: "pt", 1959 winner: "VINCITORE", 1960 teamTotalScore: "Punteggio Totale Squadra", 1961 teamMaxCombo: "Combo Massimo Squadra", 1962 greatLabel: "è¯ (Ottimo)", 1963 goodLabel: "å¯ (Buono)", 1964 missLabel: "ä¸å¯ (Mancato)", 1965 rollsLabel: "飿 (Rulli)", 1966 competitiveBattle: "Sfida Competitiva", 1967 cooperativeTeam: "Squadra Cooperativa", 1968 pressFOrJNow: "Premi F o J adesso!", 1969 donCenter: "DON (Centro)", 1970 tapDrumheadCenter: "Colpisci il centro del tamburo", 1971 pressFAndJNow: "Premi F + J adesso!", 1972 bigDonBothHands: "GRAN DON (Due mani)", 1973 strikeCenterBothHands: "Colpisci il centro con entrambe le mani!", 1974 pressDAndKNow: "Premi D + K adesso!", 1975 bigKaBothRims: "GRAN KA (Due mani)", 1976 strikeRimsBothHands: "Colpisci il bordo con entrambe le mani!", 1977 pressDOrKNow: "Premi D o K adesso!", 1978 kaRim: "KA (Bordo)", 1979 tapDrumRimEdge: "Colpisci il bordo del tamburo", 1980 mashFAndJFast: "Premi F e J velocemente!", 1981 drumrollTitle: "RULLO", 1982 alternateRapidStrikes: "Colpi rapidi alternati!", 1983 touchDrum: "Tamburo Touch", 1984 touchDrumPrompt: "Tocca i tamburi per suonare!", 1985 tabletRecommended: "Tablet o tastiera consigliati per la modalità a 2 giocatori", 1986 touchKaLeft: "Ka Sinistro", 1987 touchDonLeft: "Don Sinistro", 1988 touchDonRight: "Don Destro", 1989 touchKaRight: "Ka Destro", 1990 improvementSupport: "Report Prestazioni", 1991 section: "Sezione", 1992 practiceSection: "Esercitati su questa sezione", 1993 coachFullCombo: "Incredibile! Combo completa impeccabile dall'inizio alla fine! Sei un vero Maestro Taiko!", 1994 coachCleared: "Brano superato! La sezione {{sec}} conteneva note difficili. Esercitati per la Combo Completa!", 1995 coachFailed: "Coraggio! Non mollare! Rivedi la sezione {{sec}} e la prossima volta ce la farai!", 1996 coachCoop: "Ottimo lavoro di squadra! I due tamburi erano perfettamente all'unisono!", 1997 tamashii: "Anima", 1998 clearThreshold: "Completato 80%", 1999 drumrolls: "Rulli", 2000 performanceTab: "Report", 2001 leaderboardTab: "Classifica" 2002 }, 2003 pt: { 2004 gameTitle: "Taiko no Hamie", 2005 gameSubtitle: "VISUALIZADOR DE RITMO TAIKO", 2006 start: "INICIAR JOGO", 2007 ready: "PRONTO", 2008 loadingAudio: "Carregando áudio...", 2009 lobby: "Saguão", 2010 players: "JOGADORES", 2011 difficulty: "DIFICULDADE", 2012 stageTheme: "TEMA DO CENÃRIO", 2013 speed: "VELOCIDADE", 2014 speedSlow: "0.8x (Lento)", 2015 speedNormal: "1.0x (Normal)", 2016 speedFast: "1.2x (Rápido)", 2017 speedVeryFast: "1.5x (Muito Rápido)", 2018 mode: "MODO DE JUEGO", 2019 competitive: "Batalha (Competitivo)", 2020 cooperative: "Equipe (Cooperativo)", 2021 keyboardControls: "CONTROLES DO TECLADO", 2022 keyTip: "Pressione as duas teclas Don ao mesmo tempo para o Grande Don!", 2023 don: "DON", 2024 ka: "KA", 2025 drumroll: "RUFLO", 2026 balloon: "BALÃO!", 2027 perfect: "PERFEITO!", 2028 good: "BOM", 2029 miss: "ERRO", 2030 combo: "COMBO", 2031 score: "Pontuação", 2032 maxCombo: "Combo Máx", 2033 accuracy: "Precisão", 2034 source: "Origem", 2035 sourceDrums: "Bateria MIDI", 2036 sourceMelody: "Ritmo Melódico", 2037 sourceBeatGrid: "Grade de Batida", 2038 resultsTitle: "MÃSICA CONCLUÃDA!", 2039 clearCrown: "CONCLUÃDO", 2040 silverCrown: "COMBO COMPLETO", 2041 goldCrown: "TUDO PERFEITO", 2042 retry: "JOGAR NOVAMENTE", 2043 shareScore: "COMPARTILHAR PONTUAÃÃO", 2044 kantan: "FÃCIL", 2045 futsuu: "NORMAL", 2046 muzukashii: "DIFÃCIL", 2047 oni: "EXTREMO", 2048 themeMatsuri: "ð® Festa das Lanternas", 2049 themeSakura: "ð¸ Rio das Cerejeiras", 2050 themeThunder: "â¡ Montanha do Trovão",
2051 themeNeon: "ð Neo-Tóquio Neon", 2052 p1Name: "Jogador 1", 2053 p2Name: "Jogador 2", 2054 p3Name: "Jogador 3", 2055 p4Name: "Jogador 4", 2056 restart: "Reiniciar", 2057 play: "Reproduzir", 2058 pause: "Pausar", 2059 setup: "Configurações", 2060 topTaikoMasters: "MELHORES MESTRES TAIKO", 2061 leaderboard: "Classificação", 2062 noHighscoresYet: "Nenhum recorde registrado!", 2063 beFirstToSetRecord: "Seja o primeiro Mestre Taiko a bater o recorde!", 2064 emptySlot: "-", 2065 submittingScore: "Enviando pontuação...", 2066 points: "pts", 2067 winner: "VENCEDOR", 2068 teamTotalScore: "Pontuação Total da Equipe", 2069 teamMaxCombo: "Combo Máximo da Equipe", 2070 greatLabel: "è¯ (Excelente)", 2071 goodLabel: "å¯ (Bom)", 2072 missLabel: "ä¸å¯ (Erro)", 2073 rollsLabel: "飿 (Rufos)", 2074 competitiveBattle: "Batalha Competitiva", 2075 cooperativeTeam: "Equipe Cooperativa", 2076 pressFOrJNow: "Pressione F ou J agora!", 2077 donCenter: "DON (Centro)", 2078 tapDrumheadCenter: "Toque no centro da pele", 2079 pressFAndJNow: "Pressione F + J agora!", 2080 bigDonBothHands: "GRANDE DON (Ambas as mãos)", 2081 strikeCenterBothHands: "Bata no centro com as duas mãos!", 2082 pressDAndKNow: "Pressione D + K agora!", 2083 bigKaBothRims: "GRANDE KA (Ambas as mãos)", 2084 strikeRimsBothHands: "Bata no aro com as duas mãos!", 2085 pressDOrKNow: "Pressione D ou K agora!", 2086 kaRim: "KA (Aro)", 2087 tapDrumRimEdge: "Toque no aro do tambor", 2088 mashFAndJFast: "Aperte F e J rápido!", 2089 drumrollTitle: "RUFO", 2090 alternateRapidStrikes: "Batidas rápidas alternadas!", 2091 touchDrum: "Tambor Tátil", 2092 touchDrumPrompt: "Toque no tambor para jogar!", 2093 tabletRecommended: "Tablet ou teclado recomendado para 2 jogadores", 2094 touchKaLeft: "Ka Esq", 2095 touchDonLeft: "Don Esq", 2096 touchDonRight: "Don Dir", 2097 touchKaRight: "Ka Dir", 2098 improvementSupport: "Relatório de Desempenho", 2099 section: "Seção", 2100 practiceSection: "Praticar esta seção", 2101 coachFullCombo: "IncrÃvel! Combo completo impecável do inÃcio ao fim! Você é um verdadeiro Mestre Taiko!", 2102 coachCleared: "Música concluÃda! A seção {{sec}} teve algumas notas difÃceis. Pratique para alcançar o Combo Completo!", 2103 coachFailed: "Ãnimo! Não desista! Foque na seção {{sec}} e você passará na próxima vez!", 2104 coachCoop: "Ãtimo trabalho em equipe! Ambos tocaram em perfeita harmonia!", 2105 tamashii: "Alma", 2106 clearThreshold: "ConcluÃdo 80%", 2107 drumrolls: "Rufos", 2108 performanceTab: "Relatório", 2109 leaderboardTab: "Placar" 2110 }, 2111 ru: { 2112 gameTitle: "Тайко но Хами", 2113 gameSubtitle: "Ð ÐТÐ-ÐÐÐУÐÐÐÐÐТÐÐ ÐÐÐ ÐÐÐÐÐÐ", 2114 start: "ÐÐЧÐТЬ ÐÐРУ", 2115 ready: "ÐÐТÐÐ", 2116 loadingAudio: "ÐагÑÑзка аÑдио...", 2117 lobby: "Ðобби", 2118 players: "ÐÐÐ ÐÐÐ", 2119 difficulty: "СÐÐÐÐÐСТЬ", 2120 stageTheme: "ТÐÐРСЦÐÐЫ", 2121 speed: "СÐÐÐ ÐСТЬ", 2122 speedSlow: "0.8x (Ðедленно)", 2123 speedNormal: "1.0x (ÐбÑÑно)", 2124 speedFast: "1.2x (ÐÑÑÑÑо)", 2125 speedVeryFast: "1.5x (ÐÑÐµÐ½Ñ Ð±ÑÑÑÑо)", 2126 mode: "Ð ÐÐÐÐ ÐÐРЫ", 2127 competitive: "ÐиÑва (СоÑевнование)", 2128 cooperative: "Ðоманда (Ðооп)", 2129 keyboardControls: "УÐÐ ÐÐÐÐÐÐÐ ÐÐÐÐÐÐТУРÐÐ", 2130 keyTip: "ÐажмиÑе обе клавиÑи Ðон одновÑеменно Ð´Ð»Ñ ÐолÑÑого Ðона!", 2131 don: "ÐÐÐ", 2132 ka: "ÐÐ", 2133 drumroll: "ÐÐ ÐÐЬ", 2134 balloon: "ШÐÐ ÐÐ!", 2135 perfect: "ÐТÐÐЧÐÐ!", 2136 good: "Ð¥ÐÐ ÐШÐ", 2137 miss: "ÐÐ ÐÐÐÐ¥", 2138 combo: "ÐÐÐÐÐ", 2139 score: "СÑеÑ", 2140 maxCombo: "ÐакÑ. комбо", 2141 accuracy: "ТоÑноÑÑÑ", 2142 source: "ÐÑÑоÑник", 2143 sourceDrums: "MIDI-баÑабанÑ", 2144 sourceMelody: "РиÑм мелодии", 2145 sourceBeatGrid: "СеÑка ÑакÑов", 2146 resultsTitle: "ТРÐÐ ÐÐ ÐÐÐÐÐ!", 2147 clearCrown: "ÐÐ ÐÐÐÐÐÐ", 2148 silverCrown: "ÐÐÐÐÐÐ ÐÐÐÐÐ", 2149 goldCrown: "ÐСРÐÐÐÐÐЬÐÐ", 2150 retry: "ÐÐÐ ÐТЬ СÐÐÐÐ", 2151 shareScore: "ÐÐÐÐÐÐТЬСЯ СЧÐТÐÐ", 2152 kantan: "ÐÐÐÐÐ", 2153 futsuu: "ÐÐÐ ÐÐÐЬÐÐ", 2154 muzukashii: "СÐÐÐÐÐ", 2155 oni: "ÐÐÐ (ÐÐСТРÐÐ)", 2156 themeMatsuri: "ð® ФеÑÑÐ¸Ð²Ð°Ð»Ñ ÑонаÑиков", 2157 themeSakura: "ð¸ СакÑÑа и ФÑдзи", 2158 themeThunder: "â¡ ÐÑÐ¾Ð¼Ð¾Ð²Ð°Ñ Ð³Ð¾Ñа", 2159 themeNeon: "ð Ðео-Токио Ðеон", 2160 p1Name: "ÐгÑок 1", 2161 p2Name: "ÐгÑок 2", 2162 p3Name: "ÐгÑок 3", 2163 p4Name: "ÐгÑок 4", 2164 restart: "ÐеÑезапÑÑк", 2165 play: "ÐгÑаÑÑ", 2166 pause: "ÐаÑза", 2167 setup: "ÐаÑÑÑойки", 2168 topTaikoMasters: "ТÐÐÐÐЦРÐÐÐÐÐ ÐРТÐÐÐÐ", 2169 leaderboard: "ТаблиÑа лидеÑов", 2170 noHighscoresYet: "РекоÑдов пока неÑ!", 2171 beFirstToSetRecord: "СÑанÑÑе пеÑвÑм маÑÑеÑом Тайко Ñ ÑекоÑдом!", 2172 emptySlot: "-", 2173 submittingScore: "ÐÑпÑавка ÑезÑлÑÑаÑа...", 2174 points: "оÑков", 2175 winner: "ÐÐÐÐÐÐТÐÐЬ", 2176 teamTotalScore: "ÐбÑий ÑÑÐµÑ ÐºÐ¾Ð¼Ð°Ð½Ð´Ñ", 2177 teamMaxCombo: "ÐакÑ. комбо командÑ", 2178 greatLabel: "è¯ (ÐÑлиÑно)", 2179 goodLabel: "å¯ (ХоÑоÑо)", 2180 missLabel: "ä¸å¯ (Ðимо)", 2181 rollsLabel: "飿 (ÐÑобÑ)", 2182 competitiveBattle: "ÐиÑва (СоÑевнование)", 2183 cooperativeTeam: "Ðоманда (ÐоопеÑаÑив)", 2184 pressFOrJNow: "ÐажмиÑе F или J ÑейÑаÑ!", 2185 donCenter: "ÐÐÐ (ЦенÑÑ)", 2186 tapDrumheadCenter: "Ð£Ð´Ð°Ñ Ð¿Ð¾ ÑенÑÑÑ Ð±Ð°Ñабана", 2187 pressFAndJNow: "ÐажмиÑе F + J вмеÑÑе!", 2188 bigDonBothHands: "ÐÐÐЬШÐÐ ÐÐÐ (Ðве ÑÑки)", 2189 strikeCenterBothHands: "Ð£Ð´Ð°Ñ Ð¿Ð¾ ÑенÑÑÑ Ð´Ð²ÑÐ¼Ñ ÑÑками!", 2190 pressDAndKNow: "ÐажмиÑе D + K вмеÑÑе!", 2191 bigKaBothRims: "ÐÐÐЬШÐÐ ÐÐ (Ðве ÑÑки)", 2192 strikeRimsBothHands: "Ð£Ð´Ð°Ñ Ð¿Ð¾ Ð¾Ð±Ð¾Ð´Ñ Ð´Ð²ÑÐ¼Ñ ÑÑками!", 2193 pressDOrKNow: "ÐажмиÑе D или K ÑейÑаÑ!", 2194 kaRim: "ÐÐ (Ðбод)", 2195 tapDrumRimEdge: "Ð£Ð´Ð°Ñ Ð¿Ð¾ Ð¾Ð±Ð¾Ð´Ñ Ð±Ð°Ñабана", 2196 mashFAndJFast: "ÐÑÑÑÑо жмиÑе F и J!", 2197 drumrollTitle: "ÐÐ ÐÐЬ", 2198 alternateRapidStrikes: "ÐÑÑÑÑÑе ÑеÑедÑÑÑиеÑÑ ÑдаÑÑ!", 2199 touchDrum: "СенÑоÑнÑй баÑабан", 2200 touchDrumPrompt: "ÐажимайÑе на баÑабан Ð´Ð»Ñ Ð¸Ð³ÑÑ!", 2201 tabletRecommended: "ÐÐ»Ñ Ñежима на 2 игÑока ÑекомендÑеÑÑÑ Ð¿Ð»Ð°Ð½ÑÐµÑ Ð¸Ð»Ð¸ клавиаÑÑÑа", 2202 touchKaLeft: "Ðа Слева", 2203 touchDonLeft: "Ðон Слева", 2204 touchDonRight: "Ðон СпÑава", 2205 touchKaRight: "Ðа СпÑава", 2206 improvementSupport: "ÐÑÑÐµÑ Ð¾Ð± игÑе", 2207 section: "СекÑиÑ", 2208 practiceSection: "ТÑениÑоваÑÑ ÑÑÑ ÑекÑиÑ", 2209 coachFullCombo: "ÐевеÑоÑÑно! ÐезÑпÑеÑное полное комбо Ð¾Ñ Ð½Ð°Ñала до конÑа! ÐаÑÑоÑÑий маÑÑÐµÑ Ð¢Ð°Ð¹ÐºÐ¾!", 2210 coachCleared: "ТÑек пÑойден! Ð ÑекÑии {{sec}} бÑли ÑложнÑе ноÑÑ. ÐоÑÑениÑÑйÑеÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð»Ð½Ð¾Ð³Ð¾ комбо!", 2211 coachFailed: "Ðе ÑнÑвайÑе! Ðе ÑдавайÑеÑÑ! ÐоÑÑениÑÑйÑе ÑекÑÐ¸Ñ {{sec}}, и в ÑледÑÑÑий Ñаз вÑÑ Ð¿Ð¾Ð»ÑÑиÑÑÑ!", 2212 coachCoop: "ÐÑлиÑÐ½Ð°Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð°Ñ ÑабоÑа! Ðба баÑабанÑика игÑали в ÑниÑон!", 2213 tamashii: "ÐÑÑа", 2214 clearThreshold: "ÐÑойдено 80%", 2215 drumrolls: "ÐÑоби", 2216 performanceTab: "ÐÑÑеÑ", 2217 leaderboardTab: "ТаблиÑа лидеÑов" 2218 }, 2219 id: { 2220 gameTitle: "Taiko no Hamie",
2221 gameSubtitle: "VISUALISASI RITME GENDANG TAIKO", 2222 start: "MULAI MAIN", 2223 ready: "SIAP", 2224 loadingAudio: "Memuat audio...", 2225 lobby: "Lobi", 2226 players: "PEMAIN", 2227 difficulty: "TINGKAT KESULITAN", 2228 stageTheme: "TEMA PANGGUNG", 2229 speed: "KECEPATAN", 2230 speedSlow: "0.8x (Lambat)", 2231 speedNormal: "1.0x (Normal)", 2232 speedFast: "1.2x (Cepat)", 2233 speedVeryFast: "1.5x (Sangat Cepat)", 2234 mode: "MODE PERMAINAN", 2235 competitive: "Pertarungan (Kompetitif)", 2236 cooperative: "Tim (Kerja Sama)", 2237 keyboardControls: "KONTROL KEYBOARD", 2238 keyTip: "Tekan kedua tombol Don secara bersamaan untuk Don Besar!", 2239 don: "DON", 2240 ka: "KA", 2241 drumroll: "GULUNGAN", 2242 balloon: "BALON!", 2243 perfect: "SEMPURNA!", 2244 good: "BAGUS", 2245 miss: "LEWAT", 2246 combo: "KOMBO", 2247 score: "Skor", 2248 maxCombo: "Kombo Maks", 2249 accuracy: "Akurasi", 2250 source: "Sumber", 2251 sourceDrums: "Drum MIDI", 2252 sourceMelody: "Ritme Melodi", 2253 sourceBeatGrid: "Kisi Ketukan", 2254 resultsTitle: "LAGU SELESAI!", 2255 clearCrown: "SELESAI", 2256 silverCrown: "KOMBO LENGKAP", 2257 goldCrown: "SEMUA SEMPURNA", 2258 retry: "MAIN LAGI", 2259 shareScore: "BAGIKAN SKOR", 2260 kantan: "MUDAH", 2261 futsuu: "NORMAL", 2262 muzukashii: "SULIT", 2263 oni: "EKSTREM", 2264 themeMatsuri: "ð® Festival Lentera", 2265 themeSakura: "ð¸ Sungai Sakura", 2266 themeThunder: "â¡ Gunung Petir", 2267 themeNeon: "ð Neon Neo-Tokyo", 2268 p1Name: "Pemain 1", 2269 p2Name: "Pemain 2", 2270 p3Name: "Pemain 3", 2271 p4Name: "Pemain 4", 2272 restart: "Mulai Ulang", 2273 play: "Putar", 2274 pause: "Jeda", 2275 setup: "Pengaturan", 2276 topTaikoMasters: "MASTER TAIKO TERATAS", 2277 leaderboard: "Papan Peringkat", 2278 noHighscoresYet: "Belum ada rekor skor!", 2279 beFirstToSetRecord: "Jadilah Master Taiko pertama yang mencetak rekor!", 2280 emptySlot: "-", 2281 submittingScore: "Mengirimkan skor...", 2282 points: "poin", 2283 winner: "PEMENANG", 2284 teamTotalScore: "Total Skor Tim", 2285 teamMaxCombo: "Kombo Maksimal Tim", 2286 greatLabel: "è¯ (Luar Biasa)", 2287 goodLabel: "å¯ (Bagus)", 2288 missLabel: "ä¸å¯ (Meleset)", 2289 rollsLabel: "飿 (Ketukan Cepat)", 2290 competitiveBattle: "Pertarungan Kompetitif", 2291 cooperativeTeam: "Tim Kooperatif", 2292 pressFOrJNow: "Tekan F atau J sekarang!", 2293 donCenter: "DON (Tengah)", 2294 tapDrumheadCenter: "Ketuk bagian tengah drum", 2295 pressFAndJNow: "Tekan F + J sekarang!", 2296 bigDonBothHands: "DON BESAR (Kedua Tangan)", 2297 strikeCenterBothHands: "Pukul bagian tengah dengan kedua tangan!", 2298 pressDAndKNow: "Tekan D + K sekarang!", 2299 bigKaBothRims: "KA BESAR (Kedua Tangan)", 2300 strikeRimsBothHands: "Pukul bagian pinggir dengan kedua tangan!", 2301 pressDOrKNow: "Tekan D atau K sekarang!", 2302 kaRim: "KA (Pinggir)", 2303 tapDrumRimEdge: "Ketuk pinggiran drum", 2304 mashFAndJFast: "Pukul F & J cepat!", 2305 drumrollTitle: "KETUKAN CEPAT", 2306 alternateRapidStrikes: "Pukulan cepat bergantian!", 2307 touchDrum: "Drum Sentuh", 2308 touchDrumPrompt: "Ketuk drum untuk bermain!", 2309 tabletRecommended: "Tablet atau keyboard disarankan untuk 2 pemain", 2310 touchKaLeft: "Ka Kiri", 2311 touchDonLeft: "Don Kiri", 2312 touchDonRight: "Don Kanan", 2313 touchKaRight: "Ka Kanan", 2314 improvementSupport: "Laporan Performa", 2315 section: "Bagian", 2316 practiceSection: "Latih bagian ini", 2317 coachFullCombo: "Luar biasa! Full combo sempurna dari awal hingga akhir! Kamu adalah Master Taiko sejati!", 2318 coachCleared: "Lagu selesai! Bagian {{sec}} memiliki beberapa nada sulit. Berlatihlah untuk meraih Full Combo!", 2319 coachFailed: "Semangat! Jangan menyerah! Fokus pada Bagian {{sec}} dan kamu pasti bisa melewatinya!", 2320 coachCoop: "Kerja sama yang luar biasa! Kedua pemain bermain dengan ritme yang selaras!", 2321 tamashii: "Jiwa", 2322 clearThreshold: "Selesai 80%", 2323 drumrolls: "Roll drum", 2324 performanceTab: "Laporan", 2325 leaderboardTab: "Papan Peringkat" 2326 }, 2327 ar: { 2328 gameTitle: "تاÙÙÙ ÙÙ ÙØ§Ù Ù", 2329 gameSubtitle: "٠ؤثر Ø¥ÙÙØ§Ø¹ طبÙ٠تاÙÙÙ", 2330 start: "ابدأ Ø§ÙØ¹Ø²Ù", 2331 ready: "استعد", 2332 loadingAudio: "جار٠تØÙ ÙÙ Ø§ÙØµÙت...", 2333 lobby: "Ø§ÙØ±Ø¯ÙØ©", 2334 players: "اÙÙØ§Ø¹Ø¨ÙÙ", 2335 difficulty: "٠ستÙÙ Ø§ÙØµØ¹Ùبة", 2336 stageTheme: "Ø®ÙÙÙØ© اÙ٠سرØ", 2337 speed: "سرعة اÙÙØºÙ ات", 2338 speedSlow: "0.8x (Ø¨Ø·ÙØ¡)", 2339 speedNormal: "1.0x (عادÙ)", 2340 speedFast: "1.2x (Ø³Ø±ÙØ¹)", 2341 speedVeryFast: "1.5x (Ø³Ø±ÙØ¹ جداÙ)",
2342 mode: "ÙÙ Ø· اÙÙØ¹Ø¨", 2343 competitive: "ÙØ²Ø§Ù (ØªÙØ§ÙسÙ)", 2344 cooperative: "ÙØ±ÙÙ (تعاÙÙÙ)", 2345 keyboardControls: "إرشادات ÙÙØØ© اÙÙ ÙØ§ØªÙØ", 2346 keyTip: "اضغط عÙ٠زر٠دÙÙ Ù Ø¹Ø§Ù ÙØ¶Ø±Ø¨ ÙØºÙ Ø© دÙ٠اÙÙØ¨Ùرة!", 2347 don: "دÙÙ", 2348 ka: "ÙØ§", 2349 drumroll: "Ø¯ØØ±Ø¬Ø©", 2350 balloon: "باÙÙÙ!", 2351 perfect: "٠٠تاز!", 2352 good: "Ø¬ÙØ¯", 2353 miss: "ÙØ§Ø¦Øª", 2354 combo: "٠تتاÙÙ", 2355 score: "اÙÙÙØ§Ø·", 2356 maxCombo: "أعÙ٠تتابع", 2357 accuracy: "Ø§ÙØ¯ÙØ©", 2358 source: "اÙ٠صدر", 2359 sourceDrums: "طبÙÙ MIDI", 2360 sourceMelody: "Ø¥ÙÙØ§Ø¹ اÙÙØÙ", 2361 sourceBeatGrid: "Ø´Ø¨ÙØ© Ø§ÙØ¥ÙÙØ§Ø¹", 2362 resultsTitle: "ت٠إÙÙ Ø§Ù Ø§ÙØ¹Ø²Ù!", 2363 clearCrown: "Ø¥Ù٠اÙ", 2364 silverCrown: "تتابع ÙØ§Ù Ù", 2365 goldCrown: "Ù٠اÙÙØºÙ ات ٠٠تازة", 2366 retry: "Ø§ÙØ¹Ø¨ ٠جدداÙ", 2367 shareScore: "Ù Ø´Ø§Ø±ÙØ© اÙÙØªÙجة", 2368 kantan: "سÙÙ", 2369 futsuu: "عادÙ", 2370 muzukashii: "صعب", 2371 oni: "Ø£Ø³Ø·ÙØ±Ù", 2372 themeMatsuri: "ð® Ù ÙØ±Ø¬Ø§Ù اÙÙÙØ§ÙÙØ³", 2373 themeSakura: "ð¸ ÙÙØ± Ø£Ø²ÙØ§Ø± اÙÙØ±Ø²", 2374 themeThunder: "â¡ ÙÙ Ø© Ø§ÙØ¨Ø±Ù", 2375 themeNeon: "ð ÙÙÙ Ø·ÙÙÙÙ ÙÙÙÙ", 2376 p1Name: "اÙÙØ§Ø¹Ø¨ 1", 2377 p2Name: "اÙÙØ§Ø¹Ø¨ 2", 2378 p3Name: "اÙÙØ§Ø¹Ø¨ 3", 2379 p4Name: "اÙÙØ§Ø¹Ø¨ 4", 2380 restart: "إعادة Ø§ÙØ¨Ø¯Ø¡", 2381 play: "تشغÙÙ", 2382 pause: "Ø¥ÙÙØ§Ù Ù Ø¤ÙØª", 2383 setup: "إعداد", 2384 topTaikoMasters: "Ø£ÙØ¶Ù أساتذة Ø§ÙØªØ§ÙÙÙ", 2385 leaderboard: "ÙÙØØ© Ø§ÙØµØ¯Ø§Ø±Ø©", 2386 noHighscoresYet: "ÙØ§ ØªÙØ¬Ø¯ Ø£Ø±ÙØ§Ù ÙÙØ§Ø³ÙØ© بعد!", 2387 beFirstToSetRecord: "Ù٠أÙ٠أستاذ تاÙÙÙ ÙØ³Ø¬Ù رÙ٠ا٠ÙÙØ§Ø³ÙاÙ!", 2388 emptySlot: "-", 2389 submittingScore: "جار٠إرسا٠اÙÙØªÙجة...", 2390 points: "ÙÙØ·Ø©", 2391 winner: "اÙÙØ§Ø¦Ø²", 2392 teamTotalScore: "إج٠اÙÙ ÙÙØ§Ø· اÙÙØ±ÙÙ", 2393 teamMaxCombo: "أعÙ٠تتابع ÙÙÙØ±ÙÙ", 2394 greatLabel: "è¯ (٠٠تاز)", 2395 goodLabel: "å¯ (Ø¬ÙØ¯)", 2396 missLabel: "ä¸å¯ (ÙØ§Ø¦Øª)", 2397 rollsLabel: "飿 (Ø¯ØØ±Ø¬Ø©)", 2398 competitiveBattle: "Ù Ø¹Ø±ÙØ© ØªÙØ§ÙØ³ÙØ©", 2399 cooperativeTeam: "ÙØ±Ù٠تعاÙÙÙ", 2400 pressFOrJNow: "اضغط F Ø£Ù J Ø§ÙØ¢Ù!", 2401 donCenter: "دÙÙ (اÙÙ Ø±ÙØ²)", 2402 tapDrumheadCenter: "اضرب Ù Ø±ÙØ² Ø§ÙØ·Ø¨Ù", 2403 pressFAndJNow: "اضغط F + J ٠عاÙ!", 2404 bigDonBothHands: "دÙÙ ÙØ¨Ùر (باÙÙØ¯ÙÙ)", 2405 strikeCenterBothHands: "اضرب اÙÙ Ø±ÙØ² بÙÙØªØ§ اÙÙØ¯ÙÙ!", 2406 pressDAndKNow: "اضغط D + K ٠عاÙ!", 2407 bigKaBothRims: "ÙØ§ ÙØ¨Ùر (باÙÙØ¯ÙÙ)", 2408 strikeRimsBothHands: "اضرب Ø§ÙØØ§ÙØ© بÙÙØªØ§ اÙÙØ¯ÙÙ!", 2409 pressDOrKNow: "اضغط D Ø£Ù K Ø§ÙØ¢Ù!", 2410 kaRim: "ÙØ§ (Ø§ÙØØ§ÙØ©)", 2411 tapDrumRimEdge: "اضرب ØØ§ÙØ© Ø§ÙØ·Ø¨Ù", 2412 mashFAndJFast: "اÙÙØ± F Ù J بسرعة!", 2413 drumrollTitle: "Ø¯ØØ±Ø¬Ø© Ø§ÙØ·Ø¨Ù", 2414 alternateRapidStrikes: "ضربات Ø³Ø±ÙØ¹Ø© Ù ØªÙØ§Ùبة!", 2415 touchDrum: "Ø·Ø¨ÙØ© اÙÙ٠س", 2416 touchDrumPrompt: "اÙ٠س Ø§ÙØ·Ø¨ÙØ© ÙÙØ¹Ø²Ù!", 2417 tabletRecommended: "ÙÙØµÙ Ø¨Ø¬ÙØ§Ø² ÙÙØÙ Ø£Ù ÙÙØØ© Ù ÙØ§ØªÙØ ÙÙØ¶Ø¹ اÙÙØ§Ø¹Ø¨ÙÙ", 2418 touchKaLeft: "ÙØ§ ÙØ³Ø§Ø±", 2419 touchDonLeft: "دÙÙ ÙØ³Ø§Ø±", 2420 touchDonRight: "دÙÙ ÙÙ ÙÙ", 2421 touchKaRight: "ÙØ§ ÙÙ ÙÙ", 2422 improvementSupport: "ØªÙØ±Ùر Ø§ÙØ£Ø¯Ø§Ø¡", 2423 section: "اÙÙØ³Ù ", 2424 practiceSection: "ØªØ¯Ø±ÙØ¨ عÙÙ ÙØ°Ø§ اÙÙØ³Ù ", 2425 coachFullCombo: "٠ذÙÙ! Ø¹Ø²Ù Ù ØªÙØ§Ù ٠دÙ٠أ٠خطأ Ù Ù Ø§ÙØ¨Ø¯Ø§ÙØ© ØØªÙ اÙÙÙØ§ÙØ©! Ø£ÙØª أستاذ Ø§ÙØªØ§ÙÙÙ Ø§ÙØÙÙÙÙ!", 2426 coachCleared: "ØªÙ Ø§Ø¬ØªÙØ§Ø² اÙ٠عزÙÙØ©! تض٠٠اÙÙØ³Ù {{sec}} بعض اÙÙÙØªØ§Øª Ø§ÙØµØ¹Ø¨Ø©. ØªØ¯Ø±ÙØ¨ عÙÙÙ ÙØªØÙÙÙ Ø§ÙØ¹Ø²Ù اÙÙ ØªÙØ§Ù Ù!", 2427 coachFailed: "ÙØ§ بأس! ÙØ§ تستسÙÙ ! رÙÙØ² عÙ٠اÙÙØ³Ù {{sec}} ÙØ³ØªØ¬ØªØ§Ø²Ù Ù٠اÙ٠رة اÙÙØ§Ø¯Ù Ø©!", 2428 coachCoop: "ع٠٠ج٠اع٠رائع! عز٠ÙÙØ§ Ø§ÙØ¹Ø§Ø²ÙÙ٠بإÙÙØ§Ø¹ ÙØªÙاغ٠تا٠!", 2429 tamashii: "Ø§ÙØ±ÙØ", 2430 clearThreshold: "Ø§Ø¬ØªÙØ§Ø² 80%", 2431 drumrolls: "اÙÙØ±Ø¹ اÙÙ ØªÙØ±Ø±", 2432 performanceTab: "ØªÙØ±Ùر Ø§ÙØ£Ø¯Ø§Ø¡", 2433 leaderboardTab: "ÙØ§Ø¦Ù Ø© اÙ٠تصدرÙÙ" 2434 } 2435}; 2436function getTaikoLocale(currentLang) { 2437 if (!currentLang) return TAIKO_LOCALES.en; 2438 if (TAIKO_LOCALES[currentLang]) return TAIKO_LOCALES[currentLang]; 2439 const prefix = currentLang.split("-")[0]; 2440 if (TAIKO_LOCALES[prefix]) return TAIKO_LOCALES[prefix]; 2441 return TAIKO_LOCALES.en; 2442} 2443const TaikoResultsScreen = ({ 2444 trackId, 2445 userId, 2446 userName, 2447 results, 2448 isCooperative, 2449 difficulty, 2450 chartSource, 2451 onPlayAgain, 2452 onBackToLobby, 2453 onPracticeSection 2454}) => { 2455 const { i18n } = useTranslation(); 2456 const locale = reactExports.useMemo(() => getTaikoLocale(i18n.language), [i18n.language]); 2457 const [, startTransition] = reactExports.useTransition(); 2458 const [leaderboard, setLeaderboard] = reactExports.useState([]); 2459 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 2460 const [hasSubmitted, setHasSubmitted] = reactExports.useState(false); 2461 const submissionLockRef = reactExports.useRef(false); 2462 const [activeTab, setActiveTab] = reactExports.useState("report"); 2463 const [selectedSection, setSelectedSection] = reactExports.useState(null); 2464 const bestPlayer = reactExports.useMemo(() => [...results].sort((a, b) => b.score - a.score)[0] || results[0], [results]); 2465 const player2 = reactExports.useMemo(() => results.find((r) => r.playerId === 2), [results]); 2466 reactExports.useMemo(() => results.reduce((acc, r) => acc + r.score, 0), [results]); 2467 reactExports.useMemo(() => Math.max(...results.map((r) => r.maxCombo)), [results]); 2468 reactExports.useEffect(() => { 2469 if (!trackId) return; 2470 api.getHighscores(trackId, "taiko", userId).then((res) => { 2471 const list = Array.isArray(res) ? res : res && res.highscores ? res.highscores : []; 2472 setLeaderboard(list); 2473 }).catch(() => { 2474 }); 2475 if (!submissionLockRef.current && !hasSubmitted && bestPlayer && bestPlayer.score > 0) { 2476 submissionLockRef.current = true; 2477 setIsSubmitting(true); 2478 api.postHighscore(trackId, { 2479 userId, 2480 userName: userName || (results.length > 1 ? `Player ${bestPlayer.playerId}` : "Taiko Master"), 2481 score: bestPlayer.score, 2482 accuracy: bestPlayer.accuracy, 2483 maxCombo: bestPlayer.maxCombo, 2484 gameKey: "taiko", 2485 metadata: { 2486 difficulty, 2487 chartSource, 2488 perfect: bestPlayer.perfectCount, 2489 good: bestPlayer.goodCount, 2490 miss: bestPlayer.missCount, 2491 drumrollHits: bestPlayer.drumrollHits, 2492 cleared: bestPlayer.cleared, 2493 isCooperative, 2494 playerCount: results.length 2495 } 2496 }).then(() => { 2497 setHasSubmitted(true); 2498 return api.getHighscores(trackId, "taiko", userId); 2499 }).then((res) => { 2500 const list = Array.isArray(res) ? res : res && res.highscores ? res.highscores : []; 2501 startTransition(() => { 2502 setLeaderboard(list); 2503 }); 2504 }).catch(() => { 2505 }).finally(() => setIsSubmitting(false)); 2506 } 2507 }, [trackId, userId, userName, bestPlayer, difficulty, chartSource, isCooperative, results.length, hasSubmitted]); 2508 const uniqueLeaderboard = reactExports.useMemo(() => { 2509 const seen = /* @__PURE__ */ new Set(); 2510 const list = []; 2511 for (const item of leaderboard) {
2512 const key = item.userId ? `u_${item.userId}` : item.userName ? `n_${item.userName}` : `id_${item.id}`; 2513 if (!seen.has(key)) { 2514 seen.add(key); 2515 list.push(item); 2516 } 2517 } 2518 return list; 2519 }, [leaderboard]); 2520 const podiumSlots = [0, 1, 2].map((idx) => ({ 2521 rank: idx + 1, 2522 medal: idx === 0 ? "ð¥" : idx === 1 ? "ð¥" : "ð¥", 2523 item: uniqueLeaderboard[idx] || null 2524 })); 2525 const sections = reactExports.useMemo(() => { 2526 if ((bestPlayer == null ? void 0 : bestPlayer.sections) && bestPlayer.sections.length === 5) { 2527 return bestPlayer.sections; 2528 } 2529 const pAcc = (bestPlayer == null ? void 0 : bestPlayer.accuracy) || 85; 2530 const pPer = (bestPlayer == null ? void 0 : bestPlayer.perfectCount) || 30; 2531 const pGood = (bestPlayer == null ? void 0 : bestPlayer.goodCount) || 10; 2532 const pMiss = (bestPlayer == null ? void 0 : bestPlayer.missCount) || 2; 2533 return [0, 1, 2, 3, 4].map((idx) => ({ 2534 sectionIndex: idx, 2535 startTime: idx * 15, 2536 endTime: (idx + 1) * 15, 2537 perfectCount: Math.max(1, Math.round(pPer / 5)), 2538 goodCount: Math.round(pGood / 5), 2539 missCount: idx === 2 ? Math.max(1, pMiss) : 0, 2540 totalNotes: Math.max(1, Math.round((pPer + pGood + pMiss) / 5)), 2541 accuracy: idx === 2 ? Math.max(50, pAcc - 15) : Math.min(100, pAcc + 5), 2542 cleared: idx !== 2 || pAcc >= 70, 2543 crown: idx !== 2 && pMiss === 0 ? "gold" : pAcc >= 70 ? "silver" : "none" 2544 })); 2545 }, [bestPlayer]); 2546 const worstSection = reactExports.useMemo(() => { 2547 let maxMiss = -1; 2548 let worstIdx = 0; 2549 sections.forEach((s, idx) => { 2550 if (s.missCount > maxMiss) { 2551 maxMiss = s.missCount; 2552 worstIdx = idx; 2553 } 2554 }); 2555 return { index: worstIdx + 1, misses: maxMiss, stat: sections[worstIdx] }; 2556 }, [sections]); 2557 const coachMessage = reactExports.useMemo(() => { 2558 if (bestPlayer.missCount === 0 && bestPlayer.perfectCount > 0) { 2559 return { 2560 title: "â DONDERFUL FULL COMBO! â", 2561 text: locale.coachFullCombo, 2562 status: "perfect" 2563 }; 2564 } 2565 if (bestPlayer.cleared) { 2566 const secText2 = String(worstSection.index); 2567 const body2 = locale.coachCleared.replace("{{sec}}", secText2); 2568 return { 2569 title: "ð STAGE CLEARED! ð", 2570 text: body2, 2571 status: "cleared" 2572 }; 2573 } 2574 const secText = String(worstSection.index); 2575 const body = locale.coachFailed.replace("{{sec}}", secText); 2576 return { 2577 title: "ðª DONMAI! TRY AGAIN! ðª", 2578 text: body, 2579 status: "failed" 2580 }; 2581 }, [bestPlayer, worstSection, locale]); 2582 const difficultyEmblem = reactExports.useMemo(() => { 2583 switch (difficulty.toLowerCase()) { 2584 case "kantan": 2585 return { icon: "ð±", label: "Easy", color: "bg-emerald-600 text-emerald-100 border-emerald-400" }; 2586 case "muzukashii": 2587 case "hard": 2588 return { icon: "ð²", label: "Hard", color: "bg-emerald-800 text-emerald-100 border-emerald-500" }; 2589 case "oni": 2590 return { icon: "ð¹", label: "Oni", color: "bg-purple-900 text-purple-100 border-purple-500" }; 2591 case "futsuu": 2592 default: 2593 return { icon: "ð¸", label: "Normal", color: "bg-rose-700 text-rose-100 border-rose-400" }; 2594 } 2595 }, [difficulty]); 2596 const renderSoulGauge = (gauge) => { 2597 const totalSegments = 26; 2598 const filledSegments = Math.round(gauge / 100 * totalSegments); 2599 const isFullSoul = gauge >= 99.5; 2600 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 w-full max-w-md", children: [
2601 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1 h-6 sm:h-7 bg-zinc-950/90 rounded-md p-1 border border-white/20 flex items-center gap-0.5 shadow-inner overflow-hidden", children: [ 2602 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2603 "div", 2604 { 2605 className: "absolute top-0 bottom-0 z-20 pointer-events-none flex flex-col items-center justify-between", 2606 style: { left: "80%" }, 2607 children: [ 2608 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-0.5 h-full bg-yellow-300 shadow-[0_0_8px_#fde047]" }), 2609 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute -top-0.5 text-[8px] font-black uppercase text-yellow-300 tracking-tighter bg-black/80 px-1 rounded -translate-y-full whitespace-nowrap", children: locale.clearThreshold }) 2610 ] 2611 } 2612 ), 2613 Array.from({ length: totalSegments }).map((_, idx) => { 2614 const isFilled = idx < filledSegments; 2615 const isClearZone = idx >= Math.floor(totalSegments * 0.8); 2616 let segColor = "bg-zinc-800/60"; 2617 if (isFilled) { 2618 if (!isClearZone) { 2619 segColor = "bg-gradient-to-t from-amber-600 to-yellow-400 shadow-[0_0_4px_rgba(245,158,11,0.5)]"; 2620 } else { 2621 const rainbowColors = [ 2622 "from-emerald-500 to-teal-400", 2623 "from-teal-400 to-cyan-400", 2624 "from-sky-400 to-blue-500", 2625 "from-indigo-500 to-purple-500", 2626 "from-fuchsia-500 to-pink-500", 2627 "from-pink-500 to-rose-500" 2628 ]; 2629 const colorIdx = Math.min(rainbowColors.length - 1, idx - Math.floor(totalSegments * 0.8)); 2630 segColor = `bg-gradient-to-t ${rainbowColors[colorIdx]} shadow-[0_0_8px_rgba(56,189,248,0.7)]`; 2631 } 2632 } 2633 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2634 "div", 2635 { 2636 className: `flex-1 h-full rounded-xs transform -skew-x-12 transition-all ${segColor}` 2637 }, 2638 idx 2639 ); 2640 }) 2641 ] }),
2642 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [ 2643 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-black text-white/90 font-mono", children: [ 2644 Math.round(gauge), 2645 "% â½" 2646 ] }), 2647 /* @__PURE__ */ jsxRuntimeExports.jsx( 2648 "div", 2649 { 2650 className: `w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center font-black text-sm transition-all ${isFullSoul ? "bg-gradient-to-br from-pink-500 via-rose-500 to-amber-400 text-white shadow-[0_0_20px_#ec4899] border-2 border-pink-200 animate-pulse scale-110" : gauge >= 80 ? "bg-gradient-to-br from-emerald-600 to-teal-500 text-white shadow-[0_0_10px_#10b981] border border-emerald-300" : "bg-zinc-800 text-zinc-500 border border-zinc-700"}`, 2651 title: locale.tamashii, 2652 children: locale.tamashii 2653 } 2654 ) 2655 ] }) 2656 ] }); 2657 }; 2658 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 bg-black/90 backdrop-blur-md flex flex-col items-center justify-start p-2 sm:p-4 text-white select-none overflow-y-auto custom-scrollbar", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-4xl bg-zinc-950/90 border border-white/15 rounded-3xl p-3 sm:p-4 flex flex-col shadow-2xl backdrop-blur-xl my-auto flex-shrink-0", children: [ 2659 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2 pb-2 border-b border-white/10 flex-shrink-0", children: [
2660 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 2661 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl", children: "ð¥" }), 2662 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg sm:text-2xl font-black tracking-wide bg-gradient-to-r from-amber-300 via-orange-400 to-rose-400 bg-clip-text text-transparent uppercase drop-shadow-sm", children: locale.improvementSupport }) 2663 ] }), 2664 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-900 border border-white/10 rounded-xl p-0.5", children: [ 2665 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2666 "button", 2667 { 2668 type: "button", 2669 onClick: () => setActiveTab("report"), 2670 className: `px-3 py-1 rounded-lg text-xs font-black transition-all flex items-center gap-1.5 cursor-pointer ${activeTab === "report" ? "bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-md" : "text-zinc-400 hover:text-white"}`, 2671 children: [
2672 /* @__PURE__ */ jsxRuntimeExports.jsx(ChartNoAxesColumn, { size: 13 }), 2673 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.performanceTab }) 2674 ] 2675 } 2676 ), 2677 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2678 "button", 2679 { 2680 type: "button", 2681 onClick: () => setActiveTab("leaderboard"), 2682 className: `px-3 py-1 rounded-lg text-xs font-black transition-all flex items-center gap-1.5 cursor-pointer ${activeTab === "leaderboard" ? "bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-md" : "text-zinc-400 hover:text-white"}`, 2683 children: [ 2684 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 13 }), 2685 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.leaderboardTab }) 2686 ] 2687 } 2688 ) 2689 ] }) 2690 ] }), 2691 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 py-2 border-b border-white/10 flex-shrink-0", children: [
2692 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5 sm:gap-4 flex-wrap", children: [ 2693 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-1.5 px-2.5 py-1 rounded-xl border ${difficultyEmblem.color} shadow-sm`, children: [ 2694 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base sm:text-lg", children: difficultyEmblem.icon }), 2695 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-black uppercase tracking-wider", children: difficultyEmblem.label }) 2696 ] }), 2697 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/90 border border-white/20 rounded-2xl px-3.5 py-1 shadow-inner flex flex-col items-center justify-center min-w-[120px]", children: [ 2698 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black tracking-widest text-zinc-400 uppercase", children: locale.score }), 2699 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xl sm:text-2xl font-black text-amber-300 drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)]", children: bestPlayer.score.toLocaleString() }) 2700 ] }),
2701 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 2702 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-400 uppercase", children: locale.maxCombo }), 2703 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base sm:text-xl font-black text-emerald-400 font-mono", children: bestPlayer.maxCombo }) 2704 ] }), 2705 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 2706 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-400 uppercase", children: locale.drumrolls }), 2707 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base sm:text-xl font-black text-amber-400 font-mono", children: bestPlayer.drumrollHits }) 2708 ] }) 2709 ] }), 2710 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full sm:w-auto flex-1 flex justify-end min-w-[240px]", children: renderSoulGauge(bestPlayer.soulGauge) }) 2711 ] }), 2712 activeTab === "report" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col justify-between py-2 min-h-0 overflow-y-auto", children: [
2713 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/70 border border-white/10 rounded-2xl p-2.5 sm:p-3 flex flex-col shadow-lg mb-2", children: [ 2714 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs font-bold text-zinc-300 mb-1 px-1", children: [ 2715 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 2716 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-amber-400 font-black", children: [ 2717 locale.section, 2718 ":" 2719 ] }), 2720 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: selectedSection !== null ? `${locale.section} ${selectedSection + 1}` : "1 - 5" }) 2721 ] }), 2722 selectedSection !== null && onPracticeSection && /* @__PURE__ */ jsxRuntimeExports.jsxs( 2723 "button", 2724 { 2725 type: "button", 2726 onClick: () => onPracticeSection(sections[selectedSection].startTime, sections[selectedSection].endTime), 2727 className: "bg-gradient-to-r from-amber-500 to-yellow-400 text-black text-[11px] font-black px-3 py-0.5 rounded-full shadow hover:scale-105 transition-all flex items-center gap-1 cursor-pointer", 2728 children: [
2729 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 10, fill: "currentColor" }), 2730 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 2731 locale.practiceSection, 2732 " ", 2733 selectedSection + 1 2734 ] }) 2735 ] 2736 } 2737 ) 2738 ] }), 2739 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-28 sm:h-32 bg-black/50 border border-white/10 rounded-xl p-2 flex items-end justify-around overflow-hidden", children: [ 2740 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2741 "div", 2742 { 2743 className: "absolute left-0 right-0 z-10 pointer-events-none flex items-center", 2744 style: { bottom: "75%" }, 2745 children: [ 2746 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full border-b border-dashed border-yellow-400/80 shadow-[0_0_6px_#facc15]" }), 2747 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute left-1 -top-3 text-[8px] font-black uppercase text-yellow-300 bg-black/80 px-1 rounded", children: "Clear 80%" }) 2748 ] 2749 } 2750 ), 2751 sections.map((sec, idx) => { 2752 const isSelected = selectedSection === idx; 2753 const isFC = sec.crown === "gold"; 2754 const isCleared = sec.crown === "silver" || isFC; 2755 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2756 "div", 2757 { 2758 onClick: () => setSelectedSection((prev) => prev === idx ? null : idx), 2759 className: `relative flex-1 flex flex-col items-center justify-end h-full cursor-pointer rounded-lg transition-colors p-1 ${isSelected ? "bg-amber-400/15 ring-2 ring-amber-400" : "hover:bg-white/5"}`, 2760 children: [
2761 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center text-4xl sm:text-5xl font-black text-white/[0.04] pointer-events-none select-none", children: idx + 1 }), 2762 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-1 z-20 text-sm sm:text-base", children: isFC ? "ð" : isCleared ? "ð¥" : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "opacity-30 text-xs", children: "-" }) }), 2763 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 sm:w-12 bg-zinc-800/80 rounded-t-lg h-full flex items-end justify-center overflow-hidden border border-white/10 shadow-inner", children: results.length > 1 && player2 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex items-end h-full gap-0.5 px-0.5", children: [ 2764 /* @__PURE__ */ jsxRuntimeExports.jsx( 2765 "div", 2766 { 2767 className: "flex-1 bg-gradient-to-t from-red-600 via-rose-500 to-amber-300 rounded-t-sm transition-all duration-700 shadow-md", 2768 style: { height: `${Math.max(8, sec.accuracy)}%` }, 2769 title: `P1: ${sec.accuracy}%` 2770 } 2771 ),
2772 /* @__PURE__ */ jsxRuntimeExports.jsx( 2773 "div", 2774 { 2775 className: "flex-1 bg-gradient-to-t from-blue-600 via-sky-500 to-teal-300 rounded-t-sm transition-all duration-700 shadow-md", 2776 style: { height: `${Math.max(8, player2.sections && player2.sections[idx] ? player2.sections[idx].accuracy : sec.accuracy)}%` }, 2777 title: `P2: ${player2.sections && player2.sections[idx] ? player2.sections[idx].accuracy : sec.accuracy}%` 2778 } 2779 ) 2780 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx( 2781 "div", 2782 { 2783 className: "w-full bg-gradient-to-t from-emerald-500 via-lime-400 to-yellow-300 rounded-t-md transition-all duration-700 shadow-[0_0_12px_rgba(163,230,53,0.4)]", 2784 style: { height: `${Math.max(8, sec.accuracy)}%` } 2785 } 2786 ) }) 2787 ] 2788 }, 2789 idx 2790 ); 2791 }) 2792 ] }),
2793 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 bg-black/40 rounded-xl p-1.5 border border-white/5", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-center text-xs", children: [ 2794 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "text-[10px] text-zinc-400 font-bold border-b border-white/10", children: [ 2795 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "py-1 text-left px-2", children: locale.difficulty }), 2796 sections.map((_, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "py-1", children: idx + 1 }, idx)) 2797 ] }) }), 2798 /* @__PURE__ */ jsxRuntimeExports.jsxs("tbody", { className: "divide-y divide-white/5 font-mono font-bold", children: [ 2799 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2800 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-left px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block bg-gradient-to-r from-amber-500 to-orange-500 text-white font-black text-[10px] px-2 py-0.5 rounded shadow-sm", children: locale.greatLabel }) }), 2801 sections.map((sec, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-amber-300", children: sec.perfectCount }, idx)) 2802 ] }),
2803 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2804 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-left px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block bg-gradient-to-r from-slate-500 to-zinc-600 text-white font-black text-[10px] px-2 py-0.5 rounded shadow-sm", children: locale.goodLabel }) }), 2805 sections.map((sec, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-sky-300", children: sec.goodCount }, idx)) 2806 ] }), 2807 /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 2808 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-left px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block bg-gradient-to-r from-sky-600 to-blue-600 text-white font-black text-[10px] px-2 py-0.5 rounded shadow-sm", children: locale.missLabel }) }), 2809 sections.map((sec, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: `py-1 ${sec.missCount > 0 ? "text-rose-400 font-black" : "text-zinc-500"}`, children: sec.missCount }, idx)) 2810 ] }) 2811 ] }) 2812 ] }) }) 2813 ] }),
2814 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 bg-zinc-900/60 border border-white/10 rounded-2xl p-2.5 sm:p-3 shadow-md", children: [ 2815 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-shrink-0 flex flex-col items-center justify-center", children: [ 2816 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 sm:w-14 sm:h-14 rounded-full bg-gradient-to-b from-amber-400 to-orange-500 border-2 border-amber-300 p-0.5 shadow-md flex items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { viewBox: "0 0 100 100", className: "w-full h-full", children: [ 2817 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "25", cy: "25", r: "15", fill: "#d97706" }), 2818 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "25", cy: "25", r: "9", fill: "#fbcfe8" }), 2819 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "75", cy: "25", r: "15", fill: "#d97706" }), 2820 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "75", cy: "25", r: "9", fill: "#fbcfe8" }),
2821 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "55", rx: "38", ry: "34", fill: "#f59e0b" }), 2822 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "50", cy: "66", rx: "22", ry: "16", fill: "#fef3c7" }), 2823 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "36", cy: "50", r: "5", fill: "#1e293b" }), 2824 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "38", cy: "48", r: "2", fill: "#ffffff" }), 2825 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "64", cy: "50", r: "5", fill: "#1e293b" }), 2826 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "66", cy: "48", r: "2", fill: "#ffffff" }), 2827 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "28", cy: "60", rx: "6", ry: "3", fill: "#fda4af" }), 2828 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "72", cy: "60", rx: "6", ry: "3", fill: "#fda4af" }), 2829 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "47,60 53,60 50,64", fill: "#e11d48" }),
2830 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 46 66 Q 50 70 54 66", stroke: "#1e293b", strokeWidth: "2", fill: "none" }), 2831 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "16", y: "32", width: "68", height: "12", rx: "3", fill: "#dc2626" }), 2832 /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "50", y: "42", textAnchor: "middle", fill: "#ffffff", fontSize: "9", fontWeight: "900", children: "太" }) 2833 ] }) }), 2834 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black text-amber-300 mt-0.5", children: "HAMIE" }) 2835 ] }), 2836 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 bg-amber-50 dark:bg-zinc-900 border-2 border-amber-400/90 rounded-2xl p-2.5 sm:p-3 text-zinc-900 dark:text-zinc-100 shadow-md relative", children: [ 2837 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -left-2 top-1/2 -translate-y-1/2 w-0 h-0 border-t-8 border-t-transparent border-b-8 border-b-transparent border-r-8 border-r-amber-400" }),
2838 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-black text-amber-600 dark:text-amber-400 tracking-wide uppercase mb-0.5", children: coachMessage.title }), 2839 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold leading-relaxed text-zinc-800 dark:text-zinc-200", children: coachMessage.text }) 2840 ] }) 2841 ] }) 2842 ] }) : ( 2843 /* Leaderboard Tab Content */ 2844 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 py-3 min-h-0 flex flex-col justify-between overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/60 border border-white/10 rounded-2xl p-3 sm:p-4", children: [ 2845 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs font-black text-amber-300 mb-3 px-1", children: [ 2846 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-sm", children: [ 2847 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 16 }), 2848 " ", 2849 locale.topTaikoMasters 2850 ] }), 2851 isSubmitting && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 text-xs animate-pulse", children: locale.submittingScore }) 2852 ] }),
2853 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-2", children: podiumSlots.map(({ rank, medal, item }) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 2854 "div", 2855 { 2856 className: `flex items-center justify-between p-2.5 sm:p-3 rounded-xl border text-xs sm:text-sm ${item ? rank === 1 ? "bg-amber-500/10 border-amber-500/30 text-amber-200" : rank === 2 ? "bg-slate-400/10 border-slate-400/30 text-slate-200" : "bg-amber-700/10 border-amber-700/30 text-amber-300" : "bg-white/[0.02] border-white/5 text-zinc-500"}`, 2857 children: [ 2858 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [ 2859 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl", children: medal }), 2860 item ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "truncate font-bold text-zinc-200 flex items-center gap-1.5", children: [ 2861 item.countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: item.countryCode, size: 13 }),
2862 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children: item.userName || "Anonymous" }) 2863 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-500", children: "-" }) 2864 ] }), 2865 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: item ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-black text-amber-300 text-sm sm:text-base", children: [ 2866 Number(item.score).toLocaleString(), 2867 " ", 2868 locale.points 2869 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-zinc-500", children: "-" }) }) 2870 ] 2871 }, 2872 rank 2873 )) }), 2874 uniqueLeaderboard.length > 3 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 pt-2 border-t border-white/5 space-y-1.5", children: uniqueLeaderboard.slice(3, 5).map((item, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-zinc-400 px-2 py-1 bg-white/[0.02] rounded-lg", children: [
2875 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 truncate max-w-[160px]", children: [ 2876 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-600 font-bold", children: [ 2877 idx + 4, 2878 "." 2879 ] }), 2880 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", children: item.userName }) 2881 ] }), 2882 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold", children: Number(item.score).toLocaleString() }) 2883 ] }, idx)) }) 2884 ] }) }) 2885 ), 2886 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-3 pt-2 border-t border-white/10 flex-shrink-0", children: [ 2887 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2888 "button", 2889 { 2890 onClick: onPlayAgain, 2891 className: "flex items-center gap-2 bg-gradient-to-r from-red-600 to-rose-600 hover:from-red-500 hover:to-rose-500 text-white font-black px-6 py-2 rounded-xl shadow-lg transition-all transform hover:scale-105 text-xs sm:text-sm cursor-pointer", 2892 children: [
2893 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 15 }), 2894 " ", 2895 locale.retry 2896 ] 2897 } 2898 ), 2899 /* @__PURE__ */ jsxRuntimeExports.jsx( 2900 "button", 2901 { 2902 onClick: onBackToLobby, 2903 className: "flex items-center gap-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 font-bold px-5 py-2 rounded-xl border border-white/10 transition-all text-xs sm:text-sm cursor-pointer", 2904 children: locale.lobby 2905 } 2906 ) 2907 ] }) 2908 ] }) }); 2909}; 2910const TaikoTouchControls = ({ 2911 playerCount, 2912 onHit, 2913 locale 2914}) => { 2915 const [pressedKeys, setPressedKeys] = reactExports.useState(/* @__PURE__ */ new Set()); 2916 const touchMapRef = reactExports.useRef(/* @__PURE__ */ new Map()); 2917 const triggerVibrate = reactExports.useCallback(() => { 2918 try { 2919 if (typeof navigator !== "undefined" && navigator.vibrate) { 2920 navigator.vibrate(12); 2921 } 2922 } catch (e) { 2923 } 2924 }, []); 2925 const handlePointerDown = reactExports.useCallback((e, keyId, playerId, action, subKey) => { 2926 e.preventDefault(); 2927 e.stopPropagation(); 2928 touchMapRef.current.set(e.pointerId, keyId); 2929 setPressedKeys((prev) => { 2930 const next = new Set(prev); 2931 next.add(keyId); 2932 return next; 2933 }); 2934 triggerVibrate(); 2935 onHit(playerId, action, subKey); 2936 }, [onHit, triggerVibrate]); 2937 const handlePointerUp = reactExports.useCallback((e) => { 2938 e.preventDefault(); 2939 e.stopPropagation(); 2940 const keyId = touchMapRef.current.get(e.pointerId); 2941 if (keyId) { 2942 touchMapRef.current.delete(e.pointerId); 2943 setPressedKeys((prev) => { 2944 const next = new Set(prev); 2945 next.delete(keyId); 2946 return next; 2947 }); 2948 } 2949 }, []); 2950 const handlePointerCancel = reactExports.useCallback((e) => { 2951 const keyId = touchMapRef.current.get(e.pointerId); 2952 if (keyId) { 2953 touchMapRef.current.delete(e.pointerId); 2954 setPressedKeys((prev) => { 2955 const next = new Set(prev); 2956 next.delete(keyId); 2957 return next; 2958 }); 2959 } 2960 }, []); 2961 if (playerCount === 1) { 2962 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-950/85 backdrop-blur-md border-t border-white/10 px-2 py-2 sm:py-3 select-none touch-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-xl mx-auto flex items-center justify-between gap-1.5 sm:gap-3", children: [
2963 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2964 "button", 2965 { 2966 type: "button", 2967 "data-taiko-player": "1", 2968 "data-taiko-key": "ka", 2969 onPointerDown: (e) => handlePointerDown(e, "p1_ka_left", 1, "ka", "leftKa"), 2970 onPointerUp: handlePointerUp, 2971 onPointerCancel: handlePointerCancel, 2972 className: `flex-1 flex flex-col items-center justify-center h-16 sm:h-20 rounded-2xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_ka_left") ? "bg-sky-500 border-sky-300 shadow-[0_0_20px_rgba(56,189,248,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-sky-600 to-sky-900 border-sky-400/60 shadow-lg text-sky-100"}`, 2973 children: [ 2974 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs sm:text-sm font-black tracking-wider uppercase", children: locale.ka }), 2975 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] sm:text-[10px] opacity-75 font-semibold mt-0.5", children: locale.touchKaLeft }) 2976 ] 2977 } 2978 ),
2979 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2980 "button", 2981 { 2982 type: "button", 2983 "data-taiko-player": "1", 2984 "data-taiko-key": "don", 2985 onPointerDown: (e) => handlePointerDown(e, "p1_don_left", 1, "don", "leftDon"), 2986 onPointerUp: handlePointerUp, 2987 onPointerCancel: handlePointerCancel, 2988 className: `flex-[1.2] flex flex-col items-center justify-center h-16 sm:h-20 rounded-2xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_don_left") ? "bg-red-500 border-amber-300 shadow-[0_0_20px_rgba(239,68,68,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-red-600 to-red-900 border-red-400/60 shadow-lg text-red-100"}`, 2989 children: [ 2990 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base sm:text-lg font-black tracking-wider uppercase", children: locale.don }), 2991 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] sm:text-[10px] opacity-75 font-semibold mt-0.5", children: locale.touchDonLeft }) 2992 ] 2993 } 2994 ),
2995 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2996 "button", 2997 { 2998 type: "button", 2999 "data-taiko-player": "1", 3000 "data-taiko-key": "don", 3001 onPointerDown: (e) => handlePointerDown(e, "p1_don_right", 1, "don", "rightDon"), 3002 onPointerUp: handlePointerUp, 3003 onPointerCancel: handlePointerCancel, 3004 className: `flex-[1.2] flex flex-col items-center justify-center h-16 sm:h-20 rounded-2xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_don_right") ? "bg-red-500 border-amber-300 shadow-[0_0_20px_rgba(239,68,68,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-red-600 to-red-900 border-red-400/60 shadow-lg text-red-100"}`, 3005 children: [ 3006 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base sm:text-lg font-black tracking-wider uppercase", children: locale.don }), 3007 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] sm:text-[10px] opacity-75 font-semibold mt-0.5", children: locale.touchDonRight }) 3008 ] 3009 } 3010 ),
3011 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3012 "button", 3013 { 3014 type: "button", 3015 "data-taiko-player": "1", 3016 "data-taiko-key": "ka", 3017 onPointerDown: (e) => handlePointerDown(e, "p1_ka_right", 1, "ka", "rightKa"), 3018 onPointerUp: handlePointerUp, 3019 onPointerCancel: handlePointerCancel, 3020 className: `flex-1 flex flex-col items-center justify-center h-16 sm:h-20 rounded-2xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_ka_right") ? "bg-sky-500 border-sky-300 shadow-[0_0_20px_rgba(56,189,248,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-sky-600 to-sky-900 border-sky-400/60 shadow-lg text-sky-100"}`, 3021 children: [ 3022 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs sm:text-sm font-black tracking-wider uppercase", children: locale.ka }), 3023 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] sm:text-[10px] opacity-75 font-semibold mt-0.5", children: locale.touchKaRight }) 3024 ] 3025 } 3026 ) 3027 ] }) }); 3028 } 3029 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full bg-zinc-950/85 backdrop-blur-md border-t border-white/10 px-2 py-2 sm:py-3 select-none touch-none", children: [ 3030 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex md:hidden items-center justify-center gap-1.5 text-[10px] font-bold text-amber-300/90 mb-1.5", children: [
3031 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 12, className: "flex-shrink-0 text-amber-400" }), 3032 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.tabletRecommended }) 3033 ] }), 3034 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-4xl mx-auto grid grid-cols-2 gap-2 sm:gap-6", children: [ 3035 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 bg-red-950/20 border border-red-500/20 rounded-2xl p-1.5 sm:p-2", children: [ 3036 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-black uppercase tracking-wider text-red-400 flex items-center gap-1", children: [ 3037 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2 h-2 rounded-full bg-red-500 shadow-[0_0_6px_#ef4444]" }), 3038 locale.p1Name || "Player 1" 3039 ] }) }), 3040 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2", children: [
3041 /* @__PURE__ */ jsxRuntimeExports.jsx( 3042 "button", 3043 { 3044 type: "button", 3045 "data-taiko-player": "1", 3046 "data-taiko-key": "ka", 3047 onPointerDown: (e) => handlePointerDown(e, "p1_ka", 1, "ka", "leftKa"), 3048 onPointerUp: handlePointerUp, 3049 onPointerCancel: handlePointerCancel, 3050 className: `flex-1 flex flex-col items-center justify-center h-14 sm:h-18 rounded-xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_ka") ? "bg-sky-500 border-sky-300 shadow-[0_0_15px_rgba(56,189,248,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-sky-600 to-sky-900 border-sky-400/60 shadow-md text-sky-100"}`, 3051 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs sm:text-sm font-black tracking-wider uppercase", children: locale.ka }) 3052 } 3053 ), 3054 /* @__PURE__ */ jsxRuntimeExports.jsx( 3055 "button", 3056 { 3057 type: "button", 3058 "data-taiko-player": "1", 3059 "data-taiko-key": "don", 3060 onPointerDown: (e) => handlePointerDown(e, "p1_don", 1, "don", "leftDon"), 3061 onPointerUp: handlePointerUp, 3062 onPointerCancel: handlePointerCancel, 3063 className: `flex-[1.2] flex flex-col items-center justify-center h-14 sm:h-18 rounded-xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p1_don") ? "bg-red-500 border-amber-300 shadow-[0_0_15px_rgba(239,68,68,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-red-600 to-red-900 border-red-400/60 shadow-md text-red-100"}`, 3064 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm sm:text-base font-black tracking-wider uppercase", children: locale.don }) 3065 } 3066 ) 3067 ] }) 3068 ] }),
3069 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 bg-blue-950/20 border border-blue-500/20 rounded-2xl p-1.5 sm:p-2", children: [ 3070 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between px-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-black uppercase tracking-wider text-blue-400 flex items-center gap-1", children: [ 3071 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2 h-2 rounded-full bg-blue-500 shadow-[0_0_6px_#3b82f6]" }), 3072 locale.p2Name || "Player 2" 3073 ] }) }), 3074 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2", children: [ 3075 /* @__PURE__ */ jsxRuntimeExports.jsx( 3076 "button", 3077 { 3078 type: "button", 3079 "data-taiko-player": "2", 3080 "data-taiko-key": "don", 3081 onPointerDown: (e) => handlePointerDown(e, "p2_don", 2, "don", "leftDon"), 3082 onPointerUp: handlePointerUp, 3083 onPointerCancel: handlePointerCancel, 3084 className: `flex-[1.2] flex flex-col items-center justify-center h-14 sm:h-18 rounded-xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p2_don") ? "bg-red-500 border-amber-300 shadow-[0_0_15px_rgba(239,68,68,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-red-600 to-red-900 border-red-400/60 shadow-md text-red-100"}`, 3085 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm sm:text-base font-black tracking-wider uppercase", children: locale.don }) 3086 } 3087 ),
3088 /* @__PURE__ */ jsxRuntimeExports.jsx( 3089 "button", 3090 { 3091 type: "button", 3092 "data-taiko-player": "2", 3093 "data-taiko-key": "ka", 3094 onPointerDown: (e) => handlePointerDown(e, "p2_ka", 2, "ka", "leftKa"), 3095 onPointerUp: handlePointerUp, 3096 onPointerCancel: handlePointerCancel, 3097 className: `flex-1 flex flex-col items-center justify-center h-14 sm:h-18 rounded-xl border-2 transition-transform active:scale-95 cursor-pointer ${pressedKeys.has("p2_ka") ? "bg-sky-500 border-sky-300 shadow-[0_0_15px_rgba(56,189,248,0.7)] translate-y-1 text-white" : "bg-gradient-to-b from-sky-600 to-sky-900 border-sky-400/60 shadow-md text-sky-100"}`, 3098 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs sm:text-sm font-black tracking-wider uppercase", children: locale.ka }) 3099 } 3100 ) 3101 ] }) 3102 ] }) 3103 ] }) 3104 ] }); 3105}; 3106const createDefaultSectionHits = () => [ 3107 { perfect: 0, good: 0, miss: 0 }, 3108 { perfect: 0, good: 0, miss: 0 }, 3109 { perfect: 0, good: 0, miss: 0 }, 3110 { perfect: 0, good: 0, miss: 0 }, 3111 { perfect: 0, good: 0, miss: 0 } 3112]; 3113const PLAYER_COLORS = ["#ef4444", "#3b82f6", "#10b981", "#f59e0b"]; 3114const getTrackLayout = (height, width, count) => { 3115 let laneH = 112; 3116 let startY = Math.floor(height * 0.38); 3117 if (count === 1) { 3118 laneH = Math.min(116, Math.max(92, Math.floor(height * 0.29))); 3119 startY = Math.floor(height * 0.35); 3120 } else { 3121 laneH = Math.min(94, Math.max(76, Math.floor(height * 0.46 / 2))); 3122 startY = Math.max(16, Math.floor(height * 0.2)); 3123 } 3124 const hitZoneX = Math.max(180, Math.min(270, width * 0.2)); 3125 const ribbonH = laneH - 24; 3126 const hitRadius = Math.min(26, Math.round((ribbonH - 8) * 0.4)); 3127 const drumRadius = hitRadius * 1.15; 3128 const drumCenterX = Math.max(54, hitZoneX - hitRadius - drumRadius - 16); 3129 return { laneH, startY, hitZoneX, hitRadius, drumRadius, drumCenterX }; 3130}; 3131function drawKeycap(ctx, x, y, w, h, label, isPressed, theme = "don") { 3132 ctx.save(); 3133 const r = Math.min(4, Math.round(w * 0.2)); 3134 const pressOffset = isPressed ? 2 : 0; 3135 const drawY = y + pressOffset;
3136 const bevelH = isPressed ? 1 : 3; 3137 const roundRectPath = (rx, ry, rw, rh, radius) => { 3138 ctx.beginPath(); 3139 if (typeof ctx.roundRect === "function") { 3140 ctx.roundRect(rx, ry, rw, rh, radius); 3141 } else { 3142 ctx.moveTo(rx + radius, ry); 3143 ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, radius); 3144 ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, radius); 3145 ctx.arcTo(rx, ry + rh, rx, ry, radius); 3146 ctx.arcTo(rx, ry, rx + rw, ry, radius); 3147 } 3148 }; 3149 ctx.fillStyle = theme === "don" ? isPressed ? "#7f1d1d" : "#450a0a" : isPressed ? "#1e3a8a" : "#0f172a"; 3150 roundRectPath(x - w / 2, drawY - h / 2, w, h + bevelH, r); 3151 ctx.fill(); 3152 const topGrad = ctx.createLinearGradient(0, drawY - h / 2, 0, drawY + h / 2); 3153 if (theme === "don") { 3154 if (isPressed) { 3155 topGrad.addColorStop(0, "#fef08a"); 3156 topGrad.addColorStop(1, "#fde047"); 3157 } else { 3158 topGrad.addColorStop(0, "#ffffff"); 3159 topGrad.addColorStop(1, "#f3f4f6"); 3160 } 3161 } else { 3162 if (isPressed) { 3163 topGrad.addColorStop(0, "#bae6fd"); 3164 topGrad.addColorStop(1, "#7dd3fc"); 3165 } else { 3166 topGrad.addColorStop(0, "#ffffff"); 3167 topGrad.addColorStop(1, "#e2e8f0"); 3168 } 3169 } 3170 ctx.fillStyle = topGrad; 3171 roundRectPath(x - w / 2, drawY - h / 2, w, h, r); 3172 ctx.fill(); 3173 ctx.strokeStyle = isPressed ? theme === "don" ? "#eab308" : "#38bdf8" : "rgba(0, 0, 0, 0.35)"; 3174 ctx.lineWidth = 1; 3175 ctx.stroke(); 3176 ctx.fillStyle = isPressed ? theme === "don" ? "#b45309" : "#0369a1" : theme === "don" ? "#991b1b" : "#1e3a8a"; 3177 ctx.font = `900 ${Math.max(9, Math.round(h * 0.56))}px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`; 3178 ctx.textAlign = "center"; 3179 ctx.textBaseline = "middle"; 3180 ctx.fillText(label, x, drawY); 3181 ctx.restore(); 3182} 3183function drawHamieNoteFace(ctx, x, y, r, type) { 3184 const isDon = type === "don" || type === "big_don"; 3185 const isBig = type.startsWith("big_"); 3186 ctx.save(); 3187 const earRadius = r * 0.28; 3188 const earDist = r * 0.78; 3189 const earAngles = [-Math.PI * 0.72, -Math.PI * 0.28]; 3190 for (const angle of earAngles) {
3191 const ex = x + Math.cos(angle) * earDist; 3192 const ey = y + Math.sin(angle) * earDist; 3193 ctx.beginPath(); 3194 ctx.arc(ex, ey, earRadius, 0, Math.PI * 2); 3195 ctx.fillStyle = isDon ? "#dc2626" : "#1d4ed8"; 3196 ctx.fill(); 3197 ctx.lineWidth = 1.5; 3198 ctx.strokeStyle = "#ffffff"; 3199 ctx.stroke(); 3200 ctx.beginPath(); 3201 ctx.arc(ex, ey, earRadius * 0.58, 0, Math.PI * 2); 3202 ctx.fillStyle = isDon ? "#fca5a5" : "#93c5fd"; 3203 ctx.fill(); 3204 } 3205 ctx.beginPath(); 3206 ctx.arc(x, y, r, 0, Math.PI * 2); 3207 ctx.fillStyle = isDon ? "#ef4444" : "#2563eb"; 3208 ctx.fill(); 3209 ctx.lineWidth = isBig ? 4 : 2.5; 3210 ctx.strokeStyle = "#ffffff"; 3211 ctx.stroke(); 3212 const muzzleY = y + r * 0.2; 3213 const muzzleW = r * 0.92; 3214 const muzzleH = r * 0.54; 3215 ctx.beginPath(); 3216 ctx.ellipse(x, muzzleY, muzzleW * 0.5, muzzleH * 0.5, 0, 0, Math.PI * 2); 3217 ctx.fillStyle = isDon ? "#ffffff" : "#f0f9ff"; 3218 ctx.fill(); 3219 const cheekY = y + r * 0.18; 3220 const cheekDist = r * 0.44; 3221 const cheekRadius = r * 0.16; 3222 ctx.beginPath(); 3223 ctx.arc(x - cheekDist, cheekY, cheekRadius, 0, Math.PI * 2); 3224 ctx.arc(x + cheekDist, cheekY, cheekRadius, 0, Math.PI * 2); 3225 ctx.fillStyle = isDon ? "rgba(244, 63, 94, 0.85)" : "rgba(56, 189, 248, 0.85)"; 3226 ctx.fill(); 3227 const eyeY = y - r * 0.1; 3228 const eyeDist = r * 0.32; 3229 const eyeR = r * 0.15; 3230 if (isDon) { 3231 ctx.beginPath(); 3232 ctx.arc(x - eyeDist, eyeY, eyeR, 0, Math.PI * 2); 3233 ctx.fillStyle = "#18181b"; 3234 ctx.fill(); 3235 ctx.beginPath(); 3236 ctx.arc(x - eyeDist - eyeR * 0.3, eyeY - eyeR * 0.3, eyeR * 0.42, 0, Math.PI * 2); 3237 ctx.fillStyle = "#ffffff"; 3238 ctx.fill(); 3239 ctx.beginPath(); 3240 ctx.arc(x + eyeDist, eyeY, eyeR, 0, Math.PI * 2); 3241 ctx.fillStyle = "#18181b"; 3242 ctx.fill(); 3243 ctx.beginPath(); 3244 ctx.arc(x + eyeDist - eyeR * 0.3, eyeY - eyeR * 0.3, eyeR * 0.42, 0, Math.PI * 2); 3245 ctx.fillStyle = "#ffffff"; 3246 ctx.fill(); 3247 } else { 3248 ctx.lineWidth = Math.max(2, r * 0.11); 3249 ctx.strokeStyle = "#0f172a"; 3250 ctx.lineCap = "round"; 3251 ctx.beginPath(); 3252 ctx.arc(x - eyeDist, eyeY + eyeR * 0.35, eyeR, Math.PI * 1.15, Math.PI * 1.85); 3253 ctx.stroke(); 3254 ctx.beginPath(); 3255 ctx.arc(x + eyeDist, eyeY + eyeR * 0.35, eyeR, Math.PI * 1.15, Math.PI * 1.85); 3256 ctx.stroke(); 3257 } 3258 const noseY = y + r * 0.1; 3259 ctx.beginPath(); 3260 ctx.arc(x, noseY, r * 0.08, 0, Math.PI * 2); 3261 ctx.fillStyle = isDon ? "#e11d48" : "#0284c7"; 3262 ctx.fill(); 3263 ctx.lineWidth = Math.max(1.5, r * 0.075); 3264 ctx.strokeStyle = isDon ? "#881337" : "#0f172a"; 3265 ctx.lineCap = "round"; 3266 const mouthY = noseY + r * 0.06; 3267 const mouthW = r * 0.14; 3268 ctx.beginPath(); 3269 ctx.arc(x - mouthW * 0.5, mouthY, mouthW * 0.5, 0, Math.PI * 0.9); 3270 ctx.stroke(); 3271 ctx.beginPath(); 3272 ctx.arc(x + mouthW * 0.5, mouthY, mouthW * 0.5, Math.PI * 0.1, Math.PI); 3273 ctx.stroke(); 3274 ctx.lineWidth = 1; 3275 ctx.strokeStyle = "rgba(0, 0, 0, 0.3)"; 3276 ctx.beginPath(); 3277 ctx.moveTo(x - r * 0.44, y + r * 0.1); 3278 ctx.lineTo(x - r * 0.76, y + r * 0.05); 3279 ctx.moveTo(x - r * 0.44, y + r * 0.2); 3280 ctx.lineTo(x - r * 0.74, y + r * 0.26); 3281 ctx.moveTo(x + r * 0.44, y + r * 0.1); 3282 ctx.lineTo(x + r * 0.76, y + r * 0.05); 3283 ctx.moveTo(x + r * 0.44, y + r * 0.2); 3284 ctx.lineTo(x + r * 0.74, y + r * 0.26); 3285 ctx.stroke(); 3286 ctx.restore(); 3287} 3288function drawNoteLabelBelow(ctx, x, centerY, r, label, isDon, isBig) { 3289 ctx.save(); 3290 const labelY = centerY + r + (isBig ? 10 : 8); 3291 const fontSize = Math.max(9, Math.round(r * 0.42)); 3292 ctx.font = `900 ${fontSize}px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif`; 3293 ctx.textAlign = "center"; 3294 ctx.textBaseline = "middle"; 3295 const textMetrics = ctx.measureText(label); 3296 const pillW = textMetrics.width + 10; 3297 const pillH = fontSize + 4; 3298 const pillR = Math.round(pillH * 0.45); 3299 ctx.fillStyle = "rgba(0, 0, 0, 0.8)"; 3300 ctx.strokeStyle = isDon ? "#f87171" : "#60a5fa"; 3301 ctx.lineWidth = 1.2; 3302 ctx.beginPath(); 3303 if (typeof ctx.roundRect === "function") { 3304 ctx.roundRect(x - pillW / 2, labelY - pillH / 2, pillW, pillH, pillR); 3305 } else { 3306 ctx.rect(x - pillW / 2, labelY - pillH / 2, pillW, pillH); 3307 } 3308 ctx.fill(); 3309 ctx.stroke(); 3310 ctx.fillStyle = isDon ? "#fca5a5" : "#93c5fd"; 3311 ctx.fillText(label, x, labelY);
3312 ctx.restore(); 3313} 3314function drawSoulGauge(ctx, x, y, w, h, soulPercent) { 3315 ctx.save(); 3316 const radius = 5; 3317 ctx.beginPath(); 3318 if (typeof ctx.roundRect === "function") { 3319 ctx.roundRect(x - 2, y - 2, w + 4, h + 4, radius + 2); 3320 } else { 3321 ctx.rect(x - 2, y - 2, w + 4, h + 4); 3322 } 3323 ctx.fillStyle = "#090a10"; 3324 ctx.fill(); 3325 ctx.lineWidth = 2; 3326 ctx.strokeStyle = "#000000"; 3327 ctx.stroke(); 3328 const segWidth = 9; 3329 const segGap = 2; 3330 const slant = 4; 3331 const totalSegs = Math.max(12, Math.floor((w - slant) / (segWidth + segGap))); 3332 const actualStep = (w - slant) / totalSegs; 3333 const activeSegCount = Math.round(Math.min(100, Math.max(0, soulPercent)) / 100 * totalSegs); 3334 const clearThresholdSeg = Math.round(totalSegs * 0.8); 3335 ctx.save(); 3336 ctx.beginPath(); 3337 if (typeof ctx.roundRect === "function") { 3338 ctx.roundRect(x, y, w, h, radius); 3339 } else { 3340 ctx.rect(x, y, w, h); 3341 } 3342 ctx.clip(); 3343 ctx.fillStyle = "#06080e"; 3344 ctx.fillRect(x, y, w, h); 3345 for (let i = 0; i < totalSegs; i++) { 3346 const segX = x + i * actualStep; 3347 const isActive = i < activeSegCount; 3348 const isPastClear = i >= clearThresholdSeg; 3349 ctx.beginPath(); 3350 ctx.moveTo(segX + slant, y); 3351 ctx.lineTo(segX + segWidth + slant, y); 3352 ctx.lineTo(segX + segWidth, y + h); 3353 ctx.lineTo(segX, y + h); 3354 ctx.closePath(); 3355 if (isActive) { 3356 if (isPastClear) { 3357 const goldGrad = ctx.createLinearGradient(segX, y, segX, y + h); 3358 goldGrad.addColorStop(0, "#fef08a"); 3359 goldGrad.addColorStop(0.3, "#f59e0b"); 3360 goldGrad.addColorStop(0.7, "#ea580c"); 3361 goldGrad.addColorStop(1, "#b91c1c"); 3362 ctx.fillStyle = goldGrad; 3363 } else { 3364 const greenGrad = ctx.createLinearGradient(segX, y, segX, y + h); 3365 greenGrad.addColorStop(0, "#6ee7b7"); 3366 greenGrad.addColorStop(0.3, "#2dd4bf"); 3367 greenGrad.addColorStop(0.65, "#059669"); 3368 greenGrad.addColorStop(1, "#064e3b"); 3369 ctx.fillStyle = greenGrad; 3370 } 3371 ctx.fill(); 3372 ctx.save(); 3373 ctx.beginPath(); 3374 ctx.moveTo(segX + slant, y); 3375 ctx.lineTo(segX + segWidth + slant, y); 3376 ctx.lineTo(segX + segWidth + slant * 0.5, y + h * 0.48); 3377 ctx.lineTo(segX + slant * 0.5, y + h * 0.48); 3378 ctx.closePath(); 3379 const glossGrad = ctx.createLinearGradient(segX, y, segX, y + h * 0.48); 3380 glossGrad.addColorStop(0, "rgba(255, 255, 255, 0.85)"); 3381 glossGrad.addColorStop(0.4, "rgba(255, 255, 255, 0.45)"); 3382 glossGrad.addColorStop(1, "rgba(255, 255, 255, 0.05)"); 3383 ctx.fillStyle = glossGrad; 3384 ctx.fill(); 3385 ctx.restore(); 3386 } else { 3387 ctx.fillStyle = "rgba(255, 255, 255, 0.04)"; 3388 ctx.fill(); 3389 } 3390 ctx.lineWidth = 1; 3391 ctx.strokeStyle = "#06080e"; 3392 ctx.stroke(); 3393 } 3394 const clearX = x + clearThresholdSeg * actualStep; 3395 ctx.beginPath(); 3396 ctx.moveTo(clearX + slant, y - 2); 3397 ctx.lineTo(clearX, y + h + 2); 3398 ctx.lineWidth = 2.5; 3399 ctx.strokeStyle = "#ffffff"; 3400 ctx.stroke(); 3401 ctx.restore(); 3402 const isCleared = soulPercent >= 80; 3403 ctx.save(); 3404 ctx.font = "900 9px ui-sans-serif, system-ui, sans-serif"; 3405 ctx.textAlign = "center"; 3406 ctx.textBaseline = "bottom"; 3407 ctx.fillStyle = isCleared ? "#fbbf24" : "rgba(255, 255, 255, 0.75)"; 3408 ctx.shadowColor = isCleared ? "#f59e0b" : "transparent"; 3409 ctx.shadowBlur = isCleared ? 8 : 0; 3410 ctx.fillText("CLEAR 80%", clearX + 6, y - 2); 3411 ctx.restore(); 3412 ctx.restore(); 3413} 3414const TaikoGame = ({ 3415 data, 3416 melodyData, 3417 processedInstruments, 3418 midiMetadata, 3419 currentTime, 3420 isPlaying, 3421 duration, 3422 tempo, 3423 timeSignature, 3424 trackId, 3425 userId, 3426 userName, 3427 onPlayPause, 3428 onSeek, 3429 onGameModeMute, 3430 playbackRate = 1, 3431 theme, 3432 isLoadingAudio, 3433 isAudioLoaded 3434}) => { 3435 const { i18n } = useTranslation(); 3436 const locale = reactExports.useMemo(() => getTaikoLocale(i18n.language), [i18n.language]); 3437 const localeRef = reactExports.useRef(locale); 3438 localeRef.current = locale; 3439 const [gameState, setGameState] = reactExports.useState("lobby"); 3440 const gameStateRef = reactExports.useRef("lobby"); 3441 gameStateRef.current = gameState; 3442 const [playerCount, setPlayerCount] = reactExports.useState(1); 3443 const [isCooperative, setIsCooperative] = reactExports.useState(false); 3444 const [difficulty, setDifficulty] = reactExports.useState("futsuu");
3445 const [selectedBackground, setSelectedBackground] = reactExports.useState(() => { 3446 const bgs = ["matsuri", "sakura", "thunder", "neon"]; 3447 return bgs[Math.floor(Math.random() * bgs.length)]; 3448 }); 3449 const [scrollSpeedMultiplier, setScrollSpeedMultiplier] = reactExports.useState(1); 3450 const [countdownNumber, setCountdownNumber] = reactExports.useState(3); 3451 const [showStartFlash, setShowStartFlash] = reactExports.useState(false); 3452 const [showKeyHelp, setShowKeyHelp] = reactExports.useState(false); 3453 const [showTouchControls, setShowTouchControls] = reactExports.useState(() => { 3454 if (typeof window === "undefined") return false; 3455 return "ontouchstart" in window || navigator.maxTouchPoints > 0; 3456 }); 3457 reactExports.useEffect(() => { 3458 if (typeof window !== "undefined") { 3459 window.__toggleTaikoTouch = () => setShowTouchControls((prev) => !prev); 3460 window.__setTaikoTouch = (v) => setShowTouchControls(v); 3461 window.__startTaikoGame = () => { 3462 setGameState("playing"); 3463 gameStateRef.current = "playing"; 3464 }; 3465 window.__setTaikoPlayers = (p) => { 3466 setPlayerCount(p); 3467 }; 3468 } 3469 }, []); 3470 const [canvasSize, setCanvasSize] = reactExports.useState({ width: 800, height: 450 }); 3471 const trackLayout = reactExports.useMemo(() => { 3472 return getTrackLayout(canvasSize.height, canvasSize.width, playerCount); 3473 }, [canvasSize.height, canvasSize.width, playerCount]); 3474 const rootContainerRef = reactExports.useRef(null); 3475 const canvasWrapperRef = reactExports.useRef(null); 3476 const canvasRef = reactExports.useRef(null); 3477 const lastTimeRef = reactExports.useRef(currentTime); 3478 const lastTickRef = reactExports.useRef(performance.now()); 3479 const isPlayingRef = reactExports.useRef(isPlaying); 3480 isPlayingRef.current = isPlaying; 3481 const countdownStartTimeRef = reactExports.useRef(0); 3482 const countdownTotalDurationSecRef = reactExports.useRef(0); 3483 const startFlashTimeoutRef = reactExports.useRef(null); 3484 const particlesRef = reactExports.useRef([]); 3485 const shockwavesRef = reactExports.useRef([]); 3486 const judgementsRef = reactExports.useRef([]); 3487 const [chart, setChart] = reactExports.useState(null); 3488 const chartNotesRef = reactExports.useRef([]); 3489 const hitNotesPerPlayerRef = reactExports.useRef(/* @__PURE__ */ new Map()); 3490 const missedNotesPerPlayerRef = reactExports.useRef(/* @__PURE__ */ new Map()); 3491 const inputManagerRef = reactExports.useRef(new TaikoInputManager()); 3492 const lastDonKeyRef = reactExports.useRef(/* @__PURE__ */ new Map()); 3493 const durationRef = reactExports.useRef(duration); 3494 durationRef.current = duration; 3495 const livePlayerStatesRef = reactExports.useRef([ 3496 { 3497 id: 1, 3498 score: 0, 3499 combo: 0, 3500 maxCombo: 0, 3501 perfectCount: 0, 3502 goodCount: 0, 3503 missCount: 0, 3504 drumrollHits: 0, 3505 soulGauge: 0, 3506 lastHitType: null, 3507 lastSubKey: null, 3508 isHitting: false, 3509 hitDrumhead: false, 3510 hitRim: false, 3511 sectionHits: createDefaultSectionHits() 3512 } 3513 ]); 3514 const [mascotStats, setMascotStats] = reactExports.useState({ 3515 combo: 0, 3516 isHitting: false, 3517 lastHitType: null, 3518 lastSubKey: null, 3519 strikeTick: 0, 3520 soulGauge: 0 3521 }); 3522 const [p2MascotStats, setP2MascotStats] = reactExports.useState({ 3523 combo: 0, 3524 isHitting: false, 3525 lastHitType: null, 3526 lastSubKey: null, 3527 strikeTick: 0, 3528 soulGauge: 0 3529 }); 3530 const BACKGROUNDS = reactExports.useMemo(() => [ 3531 { id: "matsuri", label: locale.themeMatsuri, url: "/assets/visualizers/taiko/matsuri.jpg" }, 3532 { id: "sakura", label: locale.themeSakura, url: "/assets/visualizers/taiko/sakura.jpg" }, 3533 { id: "thunder", label: locale.themeThunder, url: "/assets/visualizers/taiko/thunder.jpg" }, 3534 { id: "neon", label: locale.themeNeon, url: "/assets/visualizers/taiko/neon.jpg" } 3535 ], [locale]); 3536 const [leaderboard, setLeaderboard] = reactExports.useState([]); 3537 const [lobbyTab, setLobbyTab] = reactExports.useState("settings"); 3538 const playStartTimeRef = reactExports.useRef(0); 3539 const consecutiveMissesRef = reactExports.useRef(0); 3540 const consecutiveHitsRef = reactExports.useRef(0); 3541 const isBeginnerModeRef = reactExports.useRef(false); 3542 const lastBubbleUpdateRef = reactExports.useRef(0); 3543 const [showBeginnerBubble, setShowBeginnerBubble] = reactExports.useState(false); 3544 const [beginnerHint, setBeginnerHint] = reactExports.useState(null); 3545 reactExports.useEffect(() => { 3546 if (trackId) { 3547 api.getHighscores(trackId, "taiko", userId).then((res) => { 3548 const rawList = Array.isArray(res) ? res : res && res.highscores ? res.highscores : []; 3549 const seen = /* @__PURE__ */ new Set(); 3550 const list = []; 3551 for (const item of rawList) {
3552 const key = item.userId ? `u_${item.userId}` : item.userName ? `n_${item.userName}` : `id_${item.id}`; 3553 if (!seen.has(key)) { 3554 seen.add(key); 3555 list.push(item); 3556 } 3557 } 3558 setLeaderboard(list); 3559 }).catch(() => { 3560 }); 3561 } 3562 }, [trackId, userId, gameState]); 3563 reactExports.useEffect(() => { 3564 const gen = generateTaikoChart( 3565 data, 3566 melodyData, 3567 processedInstruments, 3568 midiMetadata, 3569 difficulty, 3570 duration 3571 ); 3572 setChart(gen); 3573 chartNotesRef.current = gen.notes; 3574 }, [data, melodyData, processedInstruments, midiMetadata, difficulty, duration]); 3575 reactExports.useEffect(() => { 3576 if (gameStateRef.current === "countdown") { 3577 if (currentTime === 0) { 3578 lastTimeRef.current = 0; 3579 lastTickRef.current = performance.now(); 3580 } 3581 return; 3582 } 3583 lastTimeRef.current = currentTime; 3584 lastTickRef.current = performance.now(); 3585 }, [currentTime]); 3586 reactExports.useEffect(() => { 3587 if (gameStateRef.current === "countdown") return; 3588 if (isPlaying) { 3589 lastTickRef.current = performance.now(); 3590 } else { 3591 const elapsed = (performance.now() - lastTickRef.current) / 1e3; 3592 lastTimeRef.current += elapsed * playbackRate; 3593 } 3594 }, [isPlaying, playbackRate]); 3595 reactExports.useEffect(() => { 3596 const newStates = []; 3597 for (let p = 1; p <= playerCount; p++) { 3598 newStates.push({ 3599 id: p, 3600 score: 0, 3601 combo: 0, 3602 maxCombo: 0, 3603 perfectCount: 0, 3604 goodCount: 0, 3605 missCount: 0, 3606 drumrollHits: 0, 3607 soulGauge: 0, 3608 lastHitType: null, 3609 lastSubKey: null, 3610 isHitting: false, 3611 hitDrumhead: false, 3612 hitRim: false, 3613 sectionHits: createDefaultSectionHits() 3614 }); 3615 } 3616 livePlayerStatesRef.current = newStates; 3617 inputManagerRef.current.setActivePlayers(playerCount); 3618 hitNotesPerPlayerRef.current = /* @__PURE__ */ new Map(); 3619 missedNotesPerPlayerRef.current = /* @__PURE__ */ new Map(); 3620 for (let p = 1; p <= playerCount; p++) { 3621 hitNotesPerPlayerRef.current.set(p, /* @__PURE__ */ new Set()); 3622 missedNotesPerPlayerRef.current.set(p, /* @__PURE__ */ new Set()); 3623 } 3624 }, [playerCount]); 3625 reactExports.useEffect(() => { 3626 const im = inputManagerRef.current; 3627 im.setOnHit((pId, action, isSimultaneous, subKey) => { 3628 handlePlayerHit(pId, action, isSimultaneous, subKey); 3629 }); 3630 if (gameState === "playing" || gameState === "lobby" || gameState === "countdown") { 3631 im.start(); 3632 } else { 3633 im.stop(); 3634 } 3635 return () => im.stop(); 3636 }, [gameState]); 3637 const handlePlayerHit = reactExports.useCallback((pId, action, isSimultaneous, subKey) => { 3638 const isDon = action === "don" || action === "big_don"; 3639 const isBig = action.startsWith("big_"); 3640 if (isDon) { 3641 taikoAudio.playDon(isBig); 3642 } else { 3643 taikoAudio.playKa(isBig); 3644 } 3645 const pState = livePlayerStatesRef.current[pId - 1]; 3646 if (pState) { 3647 pState.isHitting = true; 3648 pState.lastHitType = action; 3649 pState.lastSubKey = subKey; 3650 pState.hitDrumhead = isDon; 3651 pState.hitRim = !isDon; 3652 setTimeout(() => { 3653 if (livePlayerStatesRef.current[pId - 1]) { 3654 livePlayerStatesRef.current[pId - 1].isHitting = false; 3655 livePlayerStatesRef.current[pId - 1].hitDrumhead = false; 3656 livePlayerStatesRef.current[pId - 1].hitRim = false; 3657 livePlayerStatesRef.current[pId - 1].lastSubKey = null; 3658 } 3659 }, 100); 3660 } 3661 if (pId === 1) { 3662 setMascotStats((prev) => ({ 3663 combo: (pState == null ? void 0 : pState.combo) || 0, 3664 isHitting: true, 3665 lastHitType: action, 3666 lastSubKey: subKey || null, 3667 strikeTick: prev.strikeTick + 1, 3668 soulGauge: (pState == null ? void 0 : pState.soulGauge) || 0 3669 })); 3670 setTimeout(() => { 3671 setMascotStats((prev) => ({ ...prev, isHitting: false })); 3672 }, 180); 3673 } else if (pId === 2) { 3674 setP2MascotStats((prev) => ({ 3675 combo: (pState == null ? void 0 : pState.combo) || 0, 3676 isHitting: true, 3677 lastHitType: action, 3678 lastSubKey: subKey || null,
3679 strikeTick: prev.strikeTick + 1, 3680 soulGauge: (pState == null ? void 0 : pState.soulGauge) || 0 3681 })); 3682 setTimeout(() => { 3683 setP2MascotStats((prev) => ({ ...prev, isHitting: false })); 3684 }, 180); 3685 } 3686 if (gameState !== "playing") return; 3687 const now = performance.now(); 3688 const currentT = isPlayingRef.current ? lastTimeRef.current + (now - lastTickRef.current) / 1e3 * playbackRate : lastTimeRef.current; 3689 const offsetSec = taikoAudio.getOffsetMs() / 1e3; 3690 const adjustedT = currentT - offsetSec; 3691 const playerHits = hitNotesPerPlayerRef.current.get(pId) || /* @__PURE__ */ new Set(); 3692 const playerMisses = missedNotesPerPlayerRef.current.get(pId) || /* @__PURE__ */ new Set(); 3693 const notes = chartNotesRef.current; 3694 let candidateNote = null; 3695 let minDiff = 999; 3696 for (const note of notes) { 3697 if (playerHits.has(note.id) || playerMisses.has(note.id)) continue; 3698 if ((note.type === "drumroll" || note.type === "balloon") && adjustedT >= note.t - 0.12 && adjustedT <= note.t + note.duration + 0.1) { 3699 candidateNote = note; 3700 minDiff = 0; 3701 break; 3702 } 3703 const diff = Math.abs(note.t - adjustedT); 3704 if (diff < minDiff && diff <= 0.17) { 3705 minDiff = diff; 3706 candidateNote = note; 3707 } 3708 } 3709 const canvas = canvasRef.current; 3710 const dpr = window.devicePixelRatio || 1; 3711 const width = canvas ? canvas.width / dpr : 800; 3712 const height = canvas ? canvas.height / dpr : 450; 3713 const { laneH, startY, hitZoneX } = getTrackLayout(height, width, playerCount); 3714 const laneY = startY + (pId - 1) * laneH; 3715 const ribbonTop = laneY + 12; 3716 const ribbonHeight = laneH - 14; 3717 const hitX = hitZoneX; 3718 const hitY = ribbonTop + ribbonHeight * 0.5; 3719 shockwavesRef.current.push({ 3720 x: hitX, 3721 y: hitY, 3722 radius: 20, 3723 maxRadius: isBig ? 65 : 45, 3724 color: isBig ? "#f59e0b" : isDon ? "#ef4444" : "#3b82f6", 3725 lineWidth: isBig ? 5 : 3, 3726 alpha: 1 3727 }); 3728 if (!candidateNote || !pState) return; 3729 if (candidateNote.type === "drumroll" || candidateNote.type === "balloon") { 3730 candidateNote.hitsCount = (candidateNote.hitsCount || 0) + 1; 3731 const pts = 30 * DIFFICULTY_CONFIG[difficulty].scoreMultiplier; 3732 pState.score += Math.round(pts); 3733 pState.drumrollHits += 1; 3734 pState.soulGauge = Math.min(100, pState.soulGauge + 0.6); 3735 if (candidateNote.type === "balloon" && candidateNote.hitsRequired && candidateNote.hitsCount >= candidateNote.hitsRequired) { 3736 taikoAudio.playBalloonPop(); 3737 playerHits.add(candidateNote.id); 3738 spawnJudgement(locale.balloon, "#fbbf24", hitX, hitY, "+5000"); 3739 pState.score += 5e3; 3740 spawnParticles(hitX, hitY, "#fbbf24", 24); 3741 } else { 3742 spawnParticles(hitX, hitY, "#f59e0b", 4); 3743 } 3744 if (pId === 1) { 3745 setMascotStats((prev) => ({ 3746 ...prev, 3747 combo: pState.combo, 3748 soulGauge: pState.soulGauge 3749 })); 3750 } 3751 return; 3752 } 3753 const isNoteDon = candidateNote.type === "don" || candidateNote.type === "big_don"; 3754 const isNoteKa = candidateNote.type === "ka" || candidateNote.type === "big_ka"; 3755 if (isDon && !isNoteDon || !isDon && !isNoteKa) { 3756 playerMisses.add(candidateNote.id); 3757 pState.combo = 0; 3758 pState.missCount += 1; 3759 pState.soulGauge = Math.max(0, pState.soulGauge - 2.5); 3760 if (pState.sectionHits) { 3761 const songDur = Math.max(10, durationRef.current || 60); 3762 const secIdx = Math.min(4, Math.max(0, Math.floor(candidateNote.t / (songDur / 5)))); 3763 if (pState.sectionHits[secIdx]) { 3764 pState.sectionHits[secIdx].miss += 1; 3765 } 3766 } 3767 spawnJudgement(locale.miss, "#f43f5e", hitX, hitY); 3768 if (pId === 1) { 3769 consecutiveMissesRef.current += 1; 3770 consecutiveHitsRef.current = 0; 3771 if (now - playStartTimeRef.current < 15e3 && consecutiveMissesRef.current >= 2) { 3772 isBeginnerModeRef.current = true; 3773 setShowBeginnerBubble(true); 3774 } else if (consecutiveMissesRef.current >= 4) { 3775 isBeginnerModeRef.current = true; 3776 setShowBeginnerBubble(true); 3777 } 3778 setMascotStats((prev) => ({ ...prev, combo: 0, soulGauge: pState.soulGauge })); 3779 } 3780 return; 3781 } 3782 const isPerfect = minDiff <= 0.055; 3783 const isGood = minDiff <= 0.135; 3784 playerHits.add(candidateNote.id); 3785 const isBigNote = candidateNote.type === "big_don" || candidateNote.type === "big_ka"; 3786 const diffMult = DIFFICULTY_CONFIG[difficulty].scoreMultiplier; 3787 let basePts = isPerfect ? 100 : 50;
3788 let stickingBonus = false; 3789 if (isDon && subKey) { 3790 const lastDon = lastDonKeyRef.current.get(pId); 3791 if (lastDon && now - lastDon.time < 260 && (difficulty === "muzukashii" || difficulty === "oni")) { 3792 if (lastDon.subKey !== subKey) { 3793 stickingBonus = true; 3794 } 3795 } 3796 lastDonKeyRef.current.set(pId, { subKey, time: now }); 3797 } 3798 let isDualBonus = false; 3799 if (isBigNote) { 3800 if (isSimultaneous) { 3801 basePts = Math.round(basePts * 2.5); 3802 isDualBonus = true; 3803 } else { 3804 basePts = Math.round(basePts * 1.2); 3805 } 3806 } else if (stickingBonus) { 3807 basePts = Math.round(basePts * 1.25); 3808 } 3809 pState.combo += 1; 3810 pState.maxCombo = Math.max(pState.maxCombo, pState.combo); 3811 if (isPerfect) { 3812 pState.perfectCount += 1; 3813 pState.soulGauge = Math.min(100, pState.soulGauge + (isDualBonus || stickingBonus ? 2.2 : 1.6)); 3814 } else if (isGood) { 3815 pState.goodCount += 1; 3816 pState.soulGauge = Math.min(100, pState.soulGauge + 0.8); 3817 } 3818 if (pState.sectionHits) { 3819 const songDur = Math.max(10, durationRef.current || 60); 3820 const secIdx = Math.min(4, Math.max(0, Math.floor(candidateNote.t / (songDur / 5)))); 3821 if (pState.sectionHits[secIdx]) { 3822 if (isPerfect) pState.sectionHits[secIdx].perfect += 1; 3823 else if (isGood) pState.sectionHits[secIdx].good += 1; 3824 } 3825 } 3826 const comboMult = Math.min(4, 1 + Math.floor(pState.combo / 15) * 0.5); 3827 const addedScore = Math.round(basePts * comboMult * diffMult); 3828 pState.score += addedScore; 3829 let judgeText = isPerfect ? locale.perfect : locale.good; 3830 if (isDualBonus) { 3831 judgeText = `${judgeText} (DUAL!)`; 3832 } else if (stickingBonus) { 3833 judgeText = `${judgeText} (STICKING!)`; 3834 } 3835 const judgeColor = isDualBonus ? "#fbbf24" : isPerfect ? "#fbbf24" : "#38bdf8"; 3836 spawnJudgement(judgeText, judgeColor, hitX, hitY, `+${addedScore}`); 3837 spawnParticles(hitX, hitY, judgeColor, isDualBonus ? 28 : isPerfect ? 16 : 8); 3838 if (pId === 1) { 3839 consecutiveHitsRef.current += 1; 3840 consecutiveMissesRef.current = 0; 3841 if (consecutiveHitsRef.current >= 4 && isBeginnerModeRef.current) { 3842 isBeginnerModeRef.current = false; 3843 setShowBeginnerBubble(false); 3844 setBeginnerHint(null); 3845 } 3846 setMascotStats((prev) => ({ 3847 ...prev, 3848 combo: pState.combo, 3849 soulGauge: pState.soulGauge 3850 })); 3851 } 3852 }, [difficulty, gameState, playbackRate, locale, playerCount]); 3853 const spawnJudgement = (text, color, x, y, points) => { 3854 judgementsRef.current.push({ 3855 text, 3856 color, 3857 x, 3858 y: y - 24, 3859 alpha: 1, 3860 scale: 1.25, 3861 points 3862 }); 3863 }; 3864 const spawnParticles = (x, y, color, count = 12) => { 3865 for (let i = 0; i < count; i++) { 3866 const angle = Math.random() * Math.PI * 2; 3867 const speed = 2 + Math.random() * 5; 3868 particlesRef.current.push({ 3869 x, 3870 y, 3871 vx: Math.cos(angle) * speed, 3872 vy: Math.sin(angle) * speed, 3873 color, 3874 size: 3 + Math.random() * 4, 3875 life: 1, 3876 decay: 0.035 + Math.random() * 0.02 3877 }); 3878 } 3879 }; 3880 const countdownIntervalRef = reactExports.useRef(null); 3881 const [waitingForAudio, setWaitingForAudio] = reactExports.useState(false); 3882 const isAudioReady = isAudioLoaded !== false && !isLoadingAudio; 3883 const startCountdownSequence = reactExports.useCallback((customStartTime) => { 3884 if (!chart || chart.notes.length === 0) return; 3885 const startAt = typeof customStartTime === "number" ? Math.max(0, customStartTime) : 0; 3886 if (onSeek) { 3887 onSeek(startAt); 3888 } 3889 lastTimeRef.current = startAt; 3890 lastTickRef.current = performance.now(); 3891 if (isPlaying && onPlayPause) { 3892 onPlayPause(false); 3893 } 3894 for (let p = 1; p <= playerCount; p++) { 3895 const pState = livePlayerStatesRef.current[p - 1]; 3896 if (pState) { 3897 pState.score = 0; 3898 pState.combo = 0; 3899 pState.maxCombo = 0; 3900 pState.perfectCount = 0; 3901 pState.goodCount = 0; 3902 pState.missCount = 0; 3903 pState.drumrollHits = 0; 3904 pState.soulGauge = 0; 3905 pState.lastHitType = null; 3906 pState.lastSubKey = null; 3907 pState.isHitting = false;
3908 pState.hitDrumhead = false; 3909 pState.hitRim = false; 3910 pState.sectionHits = createDefaultSectionHits(); 3911 } 3912 } 3913 hitNotesPerPlayerRef.current = /* @__PURE__ */ new Map([[1, /* @__PURE__ */ new Set()], [2, /* @__PURE__ */ new Set()]]); 3914 missedNotesPerPlayerRef.current = /* @__PURE__ */ new Map([[1, /* @__PURE__ */ new Set()], [2, /* @__PURE__ */ new Set()]]); 3915 chartNotesRef.current.forEach((n) => { 3916 n.hitsCount = 0; 3917 }); 3918 playStartTimeRef.current = performance.now(); 3919 consecutiveMissesRef.current = 0; 3920 consecutiveHitsRef.current = 0; 3921 isBeginnerModeRef.current = false; 3922 setShowBeginnerBubble(false); 3923 setBeginnerHint(null); 3924 const bpm = chart.meta.bpm || tempo || 120; 3925 const beatSec = 60 / bpm; 3926 const beatDurationMs = Math.max(550, Math.min(850, Math.round(beatSec * 1e3))); 3927 const totalCountdownMs = 3 * beatDurationMs; 3928 const totalCountdownSec = totalCountdownMs / 1e3; 3929 countdownStartTimeRef.current = performance.now(); 3930 countdownTotalDurationSecRef.current = totalCountdownSec; 3931 setGameState("countdown"); 3932 gameStateRef.current = "countdown"; 3933 setCountdownNumber(3); 3934 taikoAudio.playKa(false, 0.9); 3935 let count = 3; 3936 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 3937 countdownIntervalRef.current = setInterval(() => { 3938 count--; 3939 if (count > 0) { 3940 setCountdownNumber(count); 3941 taikoAudio.playKa(false, 0.9); 3942 } else if (count === 0) { 3943 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 3944 countdownIntervalRef.current = null; 3945 setCountdownNumber(locale.start); 3946 setShowStartFlash(true); 3947 taikoAudio.playDon(true, 1.4); 3948 setGameState("playing"); 3949 gameStateRef.current = "playing"; 3950 if (onPlayPause) { 3951 onPlayPause(true); 3952 } 3953 lastTimeRef.current = startAt; 3954 lastTickRef.current = performance.now(); 3955 playStartTimeRef.current = performance.now(); 3956 consecutiveMissesRef.current = 0; 3957 consecutiveHitsRef.current = 0; 3958 isBeginnerModeRef.current = false; 3959 setShowBeginnerBubble(false); 3960 setBeginnerHint(null); 3961 if (startFlashTimeoutRef.current) clearTimeout(startFlashTimeoutRef.current); 3962 startFlashTimeoutRef.current = setTimeout(() => { 3963 setShowStartFlash(false); 3964 }, 700); 3965 } 3966 }, beatDurationMs); 3967 }, [chart, onSeek, isPlaying, onPlayPause, playerCount, tempo, locale.start]); 3968 const handleStartGame = reactExports.useCallback((customStartTime) => { 3969 if (!chart || chart.notes.length === 0) return; 3970 if (!isAudioReady) { 3971 setWaitingForAudio(true); 3972 return; 3973 } 3974 const startAt = typeof customStartTime === "number" ? customStartTime : void 0; 3975 startCountdownSequence(startAt); 3976 }, [chart, isAudioReady, startCountdownSequence]); 3977 reactExports.useEffect(() => { 3978 if (waitingForAudio && isAudioReady) { 3979 setWaitingForAudio(false); 3980 startCountdownSequence(); 3981 } 3982 }, [waitingForAudio, isAudioReady, startCountdownSequence]); 3983 reactExports.useEffect(() => { 3984 return () => { 3985 if (countdownIntervalRef.current) { 3986 clearInterval(countdownIntervalRef.current); 3987 countdownIntervalRef.current = null; 3988 } 3989 if (startFlashTimeoutRef.current) { 3990 clearTimeout(startFlashTimeoutRef.current); 3991 startFlashTimeoutRef.current = null; 3992 } 3993 }; 3994 }, []); 3995 reactExports.useEffect(() => { 3996 const handleGlobalKey = (e) => { 3997 const target = e.target; 3998 if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return; 3999 if (e.code === "Space" || e.code === "Enter" && gameState === "lobby") { 4000 e.preventDefault(); 4001 e.stopPropagation(); 4002 if (e.code === "Space" && gameState === "playing") { 4003 if (onPlayPause) onPlayPause(!isPlaying); 4004 } else if (gameState === "lobby") { 4005 handleStartGame(); 4006 } 4007 } 4008 }; 4009 window.addEventListener("keydown", handleGlobalKey); 4010 return () => window.removeEventListener("keydown", handleGlobalKey); 4011 }, [gameState, isPlaying, onPlayPause, handleStartGame]); 4012 reactExports.useEffect(() => { 4013 if (gameState === "playing" && currentTime >= duration - 1.5 && duration > 5) { 4014 setGameState("results"); 4015 } 4016 }, [gameState, currentTime, duration]); 4017 reactExports.useEffect(() => { 4018 window.__triggerTaikoResults = () => { 4019 setGameState("results"); 4020 }; 4021 return () => { 4022 delete window.__triggerTaikoResults; 4023 }; 4024 }, []); 4025 reactExports.useEffect(() => { 4026 const resizeCanvas = () => { 4027 const wrapper = canvasWrapperRef.current; 4028 const canvas = canvasRef.current; 4029 if (!wrapper || !canvas) return; 4030 const rect = wrapper.getBoundingClientRect(); 4031 const dpr = window.devicePixelRatio || 1; 4032 canvas.width = Math.round(rect.width * dpr); 4033 canvas.height = Math.round(rect.height * dpr); 4034 canvas.style.width = `${rect.width}px`; 4035 canvas.style.height = `${rect.height}px`; 4036 setCanvasSize({ width: rect.width, height: rect.height }); 4037 }; 4038 resizeCanvas(); 4039 window.addEventListener("resize", resizeCanvas); 4040 return () => window.removeEventListener("resize", resizeCanvas); 4041 }, []); 4042 reactExports.useEffect(() => { 4043 const canvas = canvasRef.current; 4044 if (!canvas) return; 4045 const ctx = canvas.getContext("2d"); 4046 if (!ctx) return; 4047 let animationFrameId; 4048 const render = () => { 4049 const dpr = window.devicePixelRatio || 1; 4050 const width = canvas.width / dpr; 4051 const height = canvas.height / dpr;
4052 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 4053 const now = performance.now(); 4054 let visualTime = lastTimeRef.current; 4055 if (gameStateRef.current === "countdown") { 4056 const elapsedSec = (now - countdownStartTimeRef.current) / 1e3; 4057 visualTime = Math.min(0, -countdownTotalDurationSecRef.current + elapsedSec * playbackRate); 4058 } else if (isPlayingRef.current) { 4059 visualTime = lastTimeRef.current + (now - lastTickRef.current) / 1e3 * playbackRate; 4060 } 4061 const offsetSec = taikoAudio.getOffsetMs() / 1e3; 4062 visualTime -= offsetSec; 4063 ctx.clearRect(0, 0, width, height); 4064 const { laneH, startY, hitZoneX, hitRadius, drumRadius, drumCenterX } = getTrackLayout(height, width, playerCount); 4065 const scrollSpeed = width * 0.28 * scrollSpeedMultiplier; 4066 const notes = chartNotesRef.current; 4067 const bpm = (chart == null ? void 0 : chart.meta.bpm) || 120; 4068 const beatSec = 60 / bpm; 4069 const measureSec = beatSec * 4; 4070 for (let p = 1; p <= playerCount; p++) { 4071 const laneY = startY + (p - 1) * laneH; 4072 const ribbonTop = laneY + 22; 4073 const ribbonHeight = laneH - 24; 4074 const centerY = ribbonTop + ribbonHeight * 0.5; 4075 const pColor = PLAYER_COLORS[p - 1]; 4076 const playerHits = hitNotesPerPlayerRef.current.get(p); 4077 const playerMisses = missedNotesPerPlayerRef.current.get(p); 4078 const pState = livePlayerStatesRef.current[p - 1]; 4079 const trackGrad = ctx.createLinearGradient(0, ribbonTop, 0, ribbonTop + ribbonHeight); 4080 trackGrad.addColorStop(0, "#1c1e2b"); 4081 trackGrad.addColorStop(0.5, "#12131c"); 4082 trackGrad.addColorStop(1, "#0b0c13"); 4083 ctx.fillStyle = trackGrad; 4084 ctx.fillRect(0, ribbonTop, width, ribbonHeight); 4085 ctx.fillStyle = "#f59e0b"; 4086 ctx.fillRect(0, ribbonTop, width, 2.5); 4087 ctx.fillStyle = "#d97706"; 4088 ctx.fillRect(0, ribbonTop + ribbonHeight - 2, width, 2); 4089 const gaugeY = laneY + 2; 4090 const gaugeH = 16; 4091 const gaugeX = hitZoneX - 10; 4092 const gaugeW = width - gaugeX - 16; 4093 drawSoulGauge(ctx, gaugeX, gaugeY, gaugeW, gaugeH, (pState == null ? void 0 : pState.soulGauge) || 0); 4094 if (measureSec > 0.5) { 4095 const startMeasure = Math.floor((visualTime - 1) / measureSec); 4096 const endMeasure = Math.ceil((visualTime + width / scrollSpeed + 1) / measureSec); 4097 ctx.lineWidth = 1.5; 4098 for (let m = startMeasure; m <= endMeasure; m++) { 4099 const mTime = m * measureSec; 4100 const mX = hitZoneX + (mTime - visualTime) * scrollSpeed; 4101 if (mX >= hitZoneX && mX <= width) { 4102 ctx.strokeStyle = "rgba(255, 255, 255, 0.35)"; 4103 ctx.beginPath(); 4104 ctx.moveTo(mX, ribbonTop); 4105 ctx.lineTo(mX, ribbonTop + ribbonHeight); 4106 ctx.stroke(); 4107 } 4108 } 4109 } 4110 ctx.save(); 4111 ctx.beginPath(); 4112 ctx.rect(hitZoneX - hitRadius - 4, ribbonTop, width - (hitZoneX - hitRadius - 4), ribbonHeight); 4113 ctx.clip(); 4114 for (let i = 0; i < notes.length; i++) { 4115 const note = notes[i]; 4116 const timeDiff = note.t - visualTime; 4117 const noteX = hitZoneX + timeDiff * scrollSpeed; 4118 if (timeDiff < -0.18 && !(playerHits == null ? void 0 : playerHits.has(note.id)) && !(playerMisses == null ? void 0 : playerMisses.has(note.id))) { 4119 if (note.type !== "drumroll" && note.type !== "balloon") { 4120 playerMisses == null ? void 0 : playerMisses.add(note.id); 4121 if (pState) { 4122 pState.combo = 0; 4123 pState.missCount += 1; 4124 pState.soulGauge = Math.max(0, pState.soulGauge - 2.5); 4125 if (pState.sectionHits) { 4126 const songDur = Math.max(10, durationRef.current || 60); 4127 const secIdx = Math.min(4, Math.max(0, Math.floor(note.t / (songDur / 5)))); 4128 if (pState.sectionHits[secIdx]) { 4129 pState.sectionHits[secIdx].miss += 1; 4130 } 4131 } 4132 } 4133 spawnJudgement(locale.miss, "#f43f5e", hitZoneX, centerY); 4134 if (p === 1) { 4135 consecutiveMissesRef.current += 1; 4136 consecutiveHitsRef.current = 0; 4137 const curNow = performance.now(); 4138 if (curNow - playStartTimeRef.current < 15e3 && consecutiveMissesRef.current >= 2) { 4139 isBeginnerModeRef.current = true; 4140 setShowBeginnerBubble(true); 4141 } else if (consecutiveMissesRef.current >= 4) { 4142 isBeginnerModeRef.current = true; 4143 setShowBeginnerBubble(true); 4144 } 4145 } 4146 } 4147 }
4148 const isRoll = note.type === "drumroll" || note.type === "balloon"; 4149 const rollWidth = isRoll ? Math.max(60, note.duration * scrollSpeed) : 0; 4150 const isBig = note.type.startsWith("big_"); 4151 const r = isBig ? hitRadius * 1.25 : hitRadius * 0.9; 4152 const noteTailX = isRoll ? noteX + rollWidth : noteX + r; 4153 if (noteTailX < hitZoneX - hitRadius - 10 || noteX > width + 120) continue; 4154 if (playerHits == null ? void 0 : playerHits.has(note.id)) continue; 4155 if (isRoll) { 4156 ctx.fillStyle = note.type === "balloon" ? "#f59e0b" : "#fbbf24"; 4157 ctx.strokeStyle = "#78350f"; 4158 ctx.lineWidth = 3; 4159 ctx.beginPath(); 4160 ctx.roundRect(noteX, centerY - r * 0.65, rollWidth, r * 1.3, r * 0.65); 4161 ctx.fill(); 4162 ctx.stroke(); 4163 const visibleStart = Math.max(noteX, hitZoneX - hitRadius - 4); 4164 const visibleEnd = Math.min(noteX + rollWidth, width); 4165 if (visibleEnd - visibleStart > 40) { 4166 const textX = (visibleStart + visibleEnd) * 0.5; 4167 ctx.fillStyle = "#78350f"; 4168 ctx.font = "bold 12px sans-serif"; 4169 ctx.textAlign = "center"; 4170 ctx.textBaseline = "middle"; 4171 const hitsText = note.hitsCount ? ` Ã${note.hitsCount}` : ""; 4172 const label = note.type === "balloon" ? `${locale.balloon}${note.hitsRequired ? ` (${Math.max(0, note.hitsRequired - (note.hitsCount || 0))})` : ""}` : `${locale.drumroll}${hitsText}`; 4173 ctx.fillText(label, textX, centerY); 4174 } 4175 } else if (note.type === "don" || note.type === "big_don" || note.type === "ka" || note.type === "big_ka") { 4176 drawHamieNoteFace(ctx, noteX, centerY, r, note.type); 4177 const noteLabel = note.type === "big_don" ? `BIG ${locale.don}` : note.type === "big_ka" ? `BIG ${locale.ka}` : note.type === "don" ? locale.don : locale.ka; 4178 drawNoteLabelBelow(ctx, noteX, centerY, r, noteLabel, note.type.includes("don"), isBig); 4179 } 4180 } 4181 ctx.restore(); 4182 if (isBeginnerModeRef.current && p === 1) { 4183 const upcoming = notes.find((n) => !(playerHits == null ? void 0 : playerHits.has(n.id)) && !(playerMisses == null ? void 0 : playerMisses.has(n.id)) && n.t - visualTime >= -0.08 && n.t - visualTime <= 1.4); 4184 if (upcoming) { 4185 const isUpcomingDon = upcoming.type === "don" || upcoming.type === "big_don"; 4186 const isUpcomingKa = upcoming.type === "ka" || upcoming.type === "big_ka"; 4187 const isUpcomingBig = upcoming.type.startsWith("big_"); 4188 const isUpcomingRoll = upcoming.type === "drumroll" || upcoming.type === "balloon"; 4189 let text = localeRef.current.pressFOrJNow; 4190 let badge = localeRef.current.donCenter; 4191 let sub = localeRef.current.tapDrumheadCenter; 4192 let color = "#ef4444"; 4193 if (isUpcomingBig && isUpcomingDon) { 4194 text = localeRef.current.pressFAndJNow; 4195 badge = localeRef.current.bigDonBothHands; 4196 sub = localeRef.current.strikeCenterBothHands; 4197 color = "#f59e0b"; 4198 } else if (isUpcomingBig && isUpcomingKa) { 4199 text = localeRef.current.pressDAndKNow; 4200 badge = localeRef.current.bigKaBothRims; 4201 sub = localeRef.current.strikeRimsBothHands; 4202 color = "#0ea5e9"; 4203 } else if (isUpcomingKa) { 4204 text = localeRef.current.pressDOrKNow; 4205 badge = localeRef.current.kaRim; 4206 sub = localeRef.current.tapDrumRimEdge; 4207 color = "#3b82f6"; 4208 } else if (isUpcomingRoll) { 4209 text = localeRef.current.mashFAndJFast; 4210 badge = localeRef.current.drumrollTitle; 4211 sub = localeRef.current.alternateRapidStrikes; 4212 color = "#eab308"; 4213 } 4214 if (now - lastBubbleUpdateRef.current > 70) { 4215 lastBubbleUpdateRef.current = now; 4216 setBeginnerHint({ text, badge, sub, color, noteId: upcoming.id }); 4217 } 4218 } 4219 } 4220 ctx.lineWidth = 3; 4221 ctx.strokeStyle = "rgba(255, 255, 255, 0.75)"; 4222 ctx.beginPath(); 4223 ctx.arc(hitZoneX, centerY, hitRadius, 0, Math.PI * 2); 4224 ctx.stroke(); 4225 ctx.lineWidth = 2; 4226 ctx.strokeStyle = "rgba(255, 255, 255, 0.4)"; 4227 ctx.beginPath(); 4228 ctx.arc(hitZoneX, centerY, hitRadius * 0.65, 0, Math.PI * 2); 4229 ctx.stroke(); 4230 ctx.fillStyle = "#141622"; 4231 ctx.fillRect(0, ribbonTop, hitZoneX - hitRadius - 4, ribbonHeight); 4232 ctx.fillStyle = "#f59e0b"; 4233 ctx.fillRect(hitZoneX - hitRadius - 5, ribbonTop, 2, ribbonHeight); 4234 ctx.fillStyle = "#78350f"; 4235 ctx.beginPath(); 4236 ctx.arc(drumCenterX, centerY, drumRadius + 4, 0, Math.PI * 2); 4237 ctx.fill(); 4238 ctx.fillStyle = (pState == null ? void 0 : pState.hitRim) ? "#60a5fa" : "#2563eb"; 4239 ctx.beginPath(); 4240 ctx.arc(drumCenterX, centerY, drumRadius, 0, Math.PI * 2); 4241 ctx.fill(); 4242 ctx.fillStyle = (pState == null ? void 0 : pState.hitDrumhead) ? "#fca5a5" : "#fef3c7"; 4243 ctx.beginPath(); 4244 ctx.arc(drumCenterX, centerY, drumRadius * 0.72, 0, Math.PI * 2); 4245 ctx.fill(); 4246 ctx.fillStyle = (pState == null ? void 0 : pState.hitDrumhead) ? "#dc2626" : "#b45309"; 4247 ctx.font = `900 ${Math.round(drumRadius * 0.38)}px sans-serif`; 4248 ctx.textAlign = "center"; 4249 ctx.textBaseline = "middle"; 4250 ctx.fillText("太", drumCenterX, centerY - Math.round(drumRadius * 0.18)); 4251 const im = inputManagerRef.current; 4252 const isLeftKaActive = im.isKeyActive(p, "leftKa") || (pState == null ? void 0 : pState.hitRim) && (pState == null ? void 0 : pState.lastSubKey) === "leftKa"; 4253 const isRightKaActive = im.isKeyActive(p, "rightKa") || (pState == null ? void 0 : pState.hitRim) && (pState == null ? void 0 : pState.lastSubKey) === "rightKa"; 4254 const isLeftDonActive = im.isKeyActive(p, "leftDon") || (pState == null ? void 0 : pState.hitDrumhead) && (pState == null ? void 0 : pState.lastSubKey) === "leftDon"; 4255 const isRightDonActive = im.isKeyActive(p, "rightDon") || (pState == null ? void 0 : pState.hitDrumhead) && (pState == null ? void 0 : pState.lastSubKey) === "rightDon";
4256 const donKw = Math.max(16, Math.round(drumRadius * 0.36)); 4257 const donKh = Math.max(16, Math.round(drumRadius * 0.36)); 4258 const donY = centerY + Math.round(drumRadius * 0.28); 4259 const donSpacing = Math.round(drumRadius * 0.24); 4260 const kaKw = Math.max(15, Math.round(drumRadius * 0.32)); 4261 const kaKh = Math.max(15, Math.round(drumRadius * 0.32)); 4262 const kaLeftX = drumCenterX - Math.round(drumRadius * 0.84); 4263 const kaRightX = drumCenterX + Math.round(drumRadius * 0.84); 4264 if (p === 1) { 4265 drawKeycap(ctx, kaLeftX, centerY, kaKw, kaKh, "D", !!isLeftKaActive, "ka"); 4266 drawKeycap(ctx, kaRightX, centerY, kaKw, kaKh, "K", !!isRightKaActive, "ka"); 4267 drawKeycap(ctx, drumCenterX - donSpacing, donY, donKw, donKh, "F", !!isLeftDonActive, "don"); 4268 drawKeycap(ctx, drumCenterX + donSpacing, donY, donKw, donKh, "J", !!isRightDonActive, "don"); 4269 } else { 4270 drawKeycap(ctx, kaLeftX, centerY, kaKw, kaKh, "â", !!isLeftKaActive, "ka"); 4271 drawKeycap(ctx, kaRightX, centerY, kaKw, kaKh, "â", !!isRightKaActive, "ka"); 4272 drawKeycap(ctx, drumCenterX - donSpacing, donY, donKw, donKh, "â", !!isLeftDonActive, "don"); 4273 drawKeycap(ctx, drumCenterX + donSpacing, donY, donKw, donKh, "â", !!isRightDonActive, "don"); 4274 } 4275 ctx.fillStyle = "#f87171"; 4276 ctx.font = "bold 9px sans-serif"; 4277 ctx.textAlign = "center"; 4278 ctx.fillText("DON", drumCenterX, ribbonTop + 8); 4279 ctx.fillStyle = "#60a5fa"; 4280 ctx.font = "bold 9px sans-serif"; 4281 ctx.fillText("KA", kaLeftX, ribbonTop + 8); 4282 ctx.fillText("KA", kaRightX, ribbonTop + 8); 4283 const statsX = Math.max(14, drumCenterX - drumRadius - 42); 4284 ctx.fillStyle = pColor; 4285 ctx.font = "bold 12px sans-serif"; 4286 ctx.textAlign = "left"; 4287 ctx.textBaseline = "top"; 4288 ctx.fillText(`P${p}`, statsX, ribbonTop + 6); 4289 ctx.fillStyle = "#ffffff"; 4290 ctx.font = "bold 14px monospace"; 4291 ctx.fillText(`${((pState == null ? void 0 : pState.score) || 0).toLocaleString()}`, statsX, ribbonTop + 22); 4292 if (pState && pState.combo >= 5) { 4293 ctx.fillStyle = pState.combo >= 30 ? "#fbbf24" : "#fde047"; 4294 ctx.font = "900 12px sans-serif"; 4295 ctx.fillText(`${pState.combo} ${locale.combo}!`, statsX, ribbonTop + ribbonHeight - 18); 4296 } 4297 } 4298 for (let i = shockwavesRef.current.length - 1; i >= 0; i--) { 4299 const sw = shockwavesRef.current[i]; 4300 sw.radius += 3.5; 4301 sw.alpha -= 0.06; 4302 if (sw.alpha <= 0 || sw.radius >= sw.maxRadius) { 4303 shockwavesRef.current.splice(i, 1); 4304 continue; 4305 } 4306 ctx.save(); 4307 ctx.globalAlpha = Math.max(0, sw.alpha); 4308 ctx.strokeStyle = sw.color; 4309 ctx.lineWidth = sw.lineWidth; 4310 ctx.beginPath(); 4311 ctx.arc(sw.x, sw.y, sw.radius, 0, Math.PI * 2); 4312 ctx.stroke(); 4313 ctx.restore(); 4314 } 4315 for (let i = particlesRef.current.length - 1; i >= 0; i--) { 4316 const pt = particlesRef.current[i]; 4317 pt.x += pt.vx; 4318 pt.y += pt.vy; 4319 pt.life -= pt.decay; 4320 if (pt.life <= 0) { 4321 particlesRef.current.splice(i, 1); 4322 continue; 4323 } 4324 ctx.save(); 4325 ctx.globalAlpha = Math.max(0, pt.life); 4326 ctx.fillStyle = pt.color; 4327 ctx.beginPath(); 4328 ctx.arc(pt.x, pt.y, pt.size * pt.life, 0, Math.PI * 2); 4329 ctx.fill(); 4330 ctx.restore(); 4331 } 4332 for (let i = judgementsRef.current.length - 1; i >= 0; i--) { 4333 const j = judgementsRef.current[i]; 4334 j.y -= 0.9; 4335 j.alpha -= 0.035; 4336 if (j.alpha <= 0) { 4337 judgementsRef.current.splice(i, 1); 4338 continue; 4339 } 4340 ctx.save(); 4341 ctx.globalAlpha = Math.max(0, j.alpha); 4342 ctx.font = `900 18px sans-serif`; 4343 ctx.textAlign = "center"; 4344 ctx.textBaseline = "middle"; 4345 ctx.strokeStyle = "#000000"; 4346 ctx.lineWidth = 4; 4347 ctx.strokeText(j.text, j.x, j.y); 4348 ctx.fillStyle = j.color; 4349 ctx.fillText(j.text, j.x, j.y); 4350 if (j.points) { 4351 ctx.font = "bold 11px monospace"; 4352 ctx.fillStyle = "#ffffff"; 4353 ctx.strokeText(j.points, j.x, j.y + 16); 4354 ctx.fillText(j.points, j.x, j.y + 16); 4355 }
4356 ctx.restore(); 4357 } 4358 animationFrameId = requestAnimationFrame(render); 4359 }; 4360 animationFrameId = requestAnimationFrame(render); 4361 return () => cancelAnimationFrame(animationFrameId); 4362 }, [playerCount, playbackRate, scrollSpeedMultiplier, locale, chart]); 4363 const activeBgUrl = reactExports.useMemo(() => { 4364 const bg = BACKGROUNDS.find((b) => b.id === selectedBackground); 4365 return bg ? bg.url : "/assets/visualizers/taiko/matsuri.jpg"; 4366 }, [selectedBackground, BACKGROUNDS]); 4367 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4368 "div", 4369 { 4370 ref: rootContainerRef, 4371 className: "relative w-full h-full min-h-[420px] select-none overflow-hidden flex flex-col bg-zinc-950 font-sans", 4372 children: [ 4373 /* @__PURE__ */ jsxRuntimeExports.jsx( 4374 "div", 4375 { 4376 className: "absolute inset-0 bg-cover bg-center transition-all duration-700 pointer-events-none opacity-90", 4377 style: { backgroundImage: `url(${activeBgUrl})` } 4378 } 4379 ), 4380 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-black/50 to-transparent pointer-events-none" }),
4381 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative z-20 flex items-center justify-between px-4 py-2 border-b border-white/15 backdrop-blur-md bg-black/40", children: [ 4382 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5", children: [ 4383 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 rounded-xl bg-gradient-to-br from-red-600 to-rose-700 border border-amber-400/80 flex items-center justify-center font-black text-white text-sm shadow-md", children: "太" }), 4384 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4385 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-amber-300 font-black tracking-wide", children: locale.gameTitle }), 4386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-[10px]", children: [ 4387 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-zinc-200", children: [ 4388 DIFFICULTY_CONFIG[difficulty].icon, 4389 " ", 4390 DIFFICULTY_CONFIG[difficulty].label 4391 ] }),
4392 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bg-white/10 px-1.5 py-0.5 rounded text-zinc-400", children: (chart == null ? void 0 : chart.meta.source) === "drum" ? locale.sourceDrums : (chart == null ? void 0 : chart.meta.source) === "melody" ? locale.sourceMelody : locale.sourceBeatGrid }) 4393 ] }) 4394 ] }) 4395 ] }), 4396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden sm:flex items-center gap-2 text-xs font-bold text-zinc-300 bg-white/5 px-3 py-1 rounded-full border border-white/10", children: [ 4397 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-amber-400 font-black", children: [ 4398 playerCount, 4399 "P" 4400 ] }), 4401 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500", children: "â¢" }), 4402 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 4403 scrollSpeedMultiplier, 4404 "x ", 4405 locale.speed 4406 ] }) 4407 ] }),
4408 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 4409 /* @__PURE__ */ jsxRuntimeExports.jsx( 4410 "button", 4411 { 4412 onClick: () => setShowTouchControls((prev) => !prev), 4413 className: `p-1.5 rounded-lg border transition-colors ${showTouchControls ? "bg-amber-500/25 text-amber-300 border-amber-400/50 shadow-[0_0_10px_rgba(245,158,11,0.25)]" : "bg-zinc-800/80 hover:bg-zinc-700 text-zinc-400 border-white/10"}`, 4414 title: locale.touchDrum, 4415 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Smartphone, { size: 15 }) 4416 } 4417 ), 4418 /* @__PURE__ */ jsxRuntimeExports.jsx( 4419 "button", 4420 { 4421 onClick: () => setShowKeyHelp(!showKeyHelp), 4422 className: "p-1.5 rounded-lg bg-zinc-800/80 hover:bg-zinc-700 text-zinc-300 border border-white/10 transition-colors", 4423 title: locale.keyboardControls, 4424 children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleQuestionMark, { size: 15 }) 4425 } 4426 ), 4427 (gameState === "playing" || gameState === "countdown") && /* @__P
4427URE__ */ jsxRuntimeExports.jsxs( 4428 "button", 4429 { 4430 onClick: handleStartGame, 4431 className: "flex items-center gap-1 text-xs bg-gradient-to-r from-amber-600 to-red-600 hover:from-amber-500 hover:to-red-500 text-white px-2.5 py-1.5 rounded-lg border border-amber-400/40 font-bold shadow-md transition-all active:scale-95", 4432 title: "Restart Game", 4433 children: [ 4434 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 13 }), 4435 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Restart" }) 4436 ] 4437 } 4438 ), 4439 gameState === "playing" && /* @__PURE__ */ jsxRuntimeExports.jsx( 4440 "button", 4441 { 4442 onClick: () => setGameState("lobby"), 4443 className: "text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-200 px-3 py-1.5 rounded-lg border border-white/10 font-bold", 4444 children: locale.lobby 4445 } 4446 ) 4447 ] }) 4448 ] }),
4449 showKeyHelp && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-14 right-4 z-40 bg-zinc-900/95 border border-white/20 rounded-2xl p-4 shadow-2xl backdrop-blur-md max-w-sm text-xs text-white", children: [ 4450 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-black text-amber-300 text-sm mb-2 flex items-center justify-between", children: [ 4451 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 4452 "â¨ï¸ ", 4453 locale.keyboardControls 4454 ] }), 4455 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowKeyHelp(false), className: "text-zinc-500 hover:text-white", children: "â" }) 4456 ] }), 4457 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 4458 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/50 p-2 rounded-lg border border-white/5", children: [ 4459 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-red-400", children: locale.p1Name }),
4460 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4461 locale.don, 4462 " (Center): ", 4463 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "F" }), 4464 " / ", 4465 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "J" }) 4466 ] }), 4467 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4468 locale.ka, 4469 " (Rim): ", 4470 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "D" }), 4471 " / ", 4472 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "K" }) 4473 ] }) 4474 ] }), 4475 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/50 p-2 rounded-lg border border-white/5", children: [
4476 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-blue-400", children: locale.p2Name }), 4477 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4478 locale.don, 4479 " (Center): ", 4480 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "â" }), 4481 " / ", 4482 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "â" }) 4483 ] }), 4484 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4485 locale.ka, 4486 " (Rim): ", 4487 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "â" }), 4488 " / ", 4489 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-1.5 py-0.5 rounded text-white font-mono", children: "â" }) 4490 ] }) 4491 ] }),
4492 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/50 p-2 rounded-lg border border-white/5 flex items-center justify-between text-zinc-300", children: [ 4493 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Pause / Resume:" }), 4494 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-zinc-800 px-2 py-0.5 rounded text-white font-mono", children: "Space" }) 4495 ] }) 4496 ] }), 4497 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-[10px] text-zinc-400 italic", children: locale.keyTip }) 4498 ] }), 4499 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: canvasWrapperRef, className: "relative flex-1 w-full h-full overflow-hidden", children: [ 4500 /* @__PURE__ */ jsxRuntimeExports.jsx( 4501 "canvas", 4502 { 4503 ref: canvasRef, 4504 className: "absolute inset-0 block pointer-events-auto" 4505 } 4506 ), 4507 gameState !== "results" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 4508 "div", 4509 { 4510 className: "absolute pointer-events-none z-10 flex items-end select-none", 4511 style: { 4512 left: Math.max(16, Math.round(trackLayout.drumCenterX - 60)), 4513 top: Math.max(0, trackLayout.startY - 132) 4514 }, 4515 children: [
4516 /* @__PURE__ */ jsxRuntimeExports.jsx( 4517 TaikoHamsterMascot, 4518 { 4519 combo: mascotStats.combo, 4520 isHit: mascotStats.isHitting, 4521 lastHitType: mascotStats.lastHitType, 4522 lastSubKey: mascotStats.lastSubKey, 4523 strikeTick: mascotStats.strikeTick, 4524 soulGauge: mascotStats.soulGauge, 4525 playerColor: PLAYER_COLORS[0], 4526 playerId: 1, 4527 currentTime, 4528 scale: trackLayout.startY < 140 ? 0.88 : 1.05 4529 } 4530 ), 4531 playerCount >= 2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4532 TaikoHamsterMascot, 4533 { 4534 combo: p2MascotStats.combo, 4535 isHit: p2MascotStats.isHitting, 4536 lastHitType: p2MascotStats.lastHitType, 4537 lastSubKey: p2MascotStats.lastSubKey, 4538 strikeTick: p2MascotStats.strikeTick, 4539 soulGauge: p2MascotStats.soulGauge, 4540 playerColor: PLAYER_COLORS[1], 4541 playerId: 2, 4542 currentTime, 4543 scale: trackLayout.startY < 140 ? 0.88 : 1.05 4544 } 4545 ) }) 4546 ] 4547 } 4548 ), 4549 showBeginnerBubble && beginnerHint && gameState === "playing" && /* @__PURE__ */ jsxRuntimeExports.jsx( 4550 "div", 4551 { 4552 className: "absolute z-30 pointer-events-auto transition-all animate-bounce select-none", 4553 style: { 4554 left: Math.max(160, Math.round(trackLayout.hitZoneX + 25)), 4555 top: Math.max(8, Math.round(trackLayout.startY - 72)) 4556 }, 4557 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative bg-gradient-to-b from-amber-50 to-yellow-100 border-2 border-zinc-950 rounded-2xl px-3 py-1.5 shadow-[0_6px_20px_rgba(0,0,0,0.5)] text-zinc-950 max-w-[210px] text-center", children: [
4558 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-2.5 left-7 w-0 h-0 border-x-[7px] border-x-transparent border-t-[10px] border-t-zinc-950", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -top-[12px] -left-[5px] w-0 h-0 border-x-[5px] border-x-transparent border-t-[8px] border-t-yellow-100" }) }), 4559 /* @__PURE__ */ jsxRuntimeExports.jsx( 4560 "button", 4561 { 4562 onClick: () => { 4563 isBeginnerModeRef.current = false; 4564 setShowBeginnerBubble(false); 4565 }, 4566 className: "absolute top-1 right-1.5 text-zinc-400 hover:text-zinc-800 text-[10px] font-black", 4567 title: "Dismiss hint", 4568 children: "â" 4569 } 4570 ), 4571 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center gap-1 mb-0.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black uppercase px-1.5 py-0.2 rounded-full text-white", style: { backgroundColor: beginnerHint.color }, children: beginnerHint.badge }) }),
4572 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-black tracking-tight text-zinc-950 leading-tight", children: beginnerHint.text }), 4573 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[9px] text-zinc-600 font-bold mt-0.5", children: beginnerHint.sub }) 4574 ] }) 4575 } 4576 ), 4577 showTouchControls && (gameState === "playing" || gameState === "countdown") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-0 left-0 right-0 z-20", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4578 TaikoTouchControls, 4579 { 4580 playerCount, 4581 onHit: (pId, action, subKey) => { 4582 inputManagerRef.current.triggerHit(pId, action, subKey); 4583 }, 4584 locale 4585 } 4586 ) }), 4587 gameState !== "results" && !showTouchControls && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-1.5 left-1/2 -translate-x-1/2 z-20 pointer-events-none flex items-center flex-nowrap whitespace-nowrap gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-0.5 rounded-full bg-black/85 backdrop-blur-md border border-white/20 text-[10px] sm:text-[11px] font-bold text-zinc-300 shadow-xl select-none max-w-[96vw] overflow-x-auto", children: [ 4588 playerCount === 2 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
4589 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 4590 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.2 rounded bg-red-950 text-red-300 font-mono text-[9px] font-black border border-red-500/40 border-b-2 border-b-red-800 shadow-sm", children: "1P" }), 4591 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-400 font-black", children: [ 4592 locale.don, 4593 ":" 4594 ] }), 4595 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "F" }), 4596 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "J" }),
4597 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400 font-black ml-0.5", children: [ 4598 locale.ka, 4599 ":" 4600 ] }), 4601 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "D" }), 4602 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "K" }) 4603 ] }), 4604 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 4605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 4606 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.2 rounded bg-blue-950 text-blue-300 font-mono text-[9px] font-black border border-blue-500/40 border-b-2 border-b-blue-800 shadow-sm", children: "2P" }),
4607 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-400 font-black", children: [ 4608 locale.don, 4609 ":" 4610 ] }), 4611 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "â" }), 4612 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "â" }), 4613 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400 font-black ml-0.5", children: [ 4614 locale.ka, 4615 ":" 4616 ] }), 4617 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "â" }),
4618 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[17px] text-center", children: "â" }) 4619 ] }) 4620 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 4621 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 4622 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-red-500 shadow-[0_0_6px_#ef4444]" }), 4623 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-red-400 font-black", children: [ 4624 locale.don, 4625 ":" 4626 ] }), 4627 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[18px] text-center", children: "F" }),
4628 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-amber-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[18px] text-center", children: "J" }) 4629 ] }), 4630 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 4631 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 4632 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-blue-500 shadow-[0_0_6px_#3b82f6]" }), 4633 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400 font-black", children: [ 4634 locale.ka, 4635 ":" 4636 ] }), 4637 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[18px] text-center", children: "D" }),
4638 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-sky-200 font-mono text-[10px] font-black border border-white/20 border-b-2 border-b-zinc-600 shadow-sm min-w-[18px] text-center", children: "K" }) 4639 ] }) 4640 ] }), 4641 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 4642 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-zinc-400", children: [ 4643 /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "px-1.5 py-0.5 rounded bg-zinc-800 text-zinc-300 font-mono text-[10px] font-bold border border-white/10 border-b-2 border-b-zinc-600 shadow-sm", children: "Space" }), 4644 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: isPlaying ? locale.pause : locale.play }) 4645 ] }) 4646 ] }), 4647 (gameState === "countdown" || showStartFlash) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-[14%] sm:top-[16%] left-1/2 -translate-x-1/2 z-30 flex flex-col items-center pointer-events-none select-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 py-2 rounded-2xl bg-zinc-950/85 border-2 border-amber-400/90 shadow-[0_0_35px_rgba(245,158,11,0.5)] text-center", children: [
4648 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] sm:text-[11px] font-black tracking-widest text-amber-300 uppercase mb-0.5", children: locale.ready || "READY..." }), 4649 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-4xl sm:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-amber-300 via-rose-400 to-red-500 drop-shadow-[0_3px_10px_rgba(0,0,0,0.8)] animate-pulse", children: countdownNumber }) 4650 ] }) }), 4651 gameState === "lobby" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-30 flex items-center justify-center p-2 sm:p-4 bg-black/60 backdrop-blur-sm overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "my-auto w-full max-w-md sm:max-w-xl md:max-w-3xl bg-zinc-900/95 border border-white/15 rounded-2xl p-3.5 sm:p-5 shadow-2xl space-y-3", children: [ 4652 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between border-b border-white/10 pb-2", children: [
4653 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-left", children: [ 4654 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-base sm:text-xl font-black tracking-wide text-transparent bg-clip-text bg-gradient-to-r from-amber-400 via-rose-400 to-red-500", children: locale.gameTitle }), 4655 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] sm:text-xs text-zinc-400 font-bold uppercase tracking-wider", children: locale.gameSubtitle }) 4656 ] }), 4657 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex md:hidden items-center bg-zinc-800/90 p-0.5 rounded-xl border border-white/10 text-xs", children: [ 4658 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4659 "button", 4660 { 4661 onClick: () => setLobbyTab("settings"), 4662 className: `px-2.5 py-1 rounded-lg font-bold transition-all ${lobbyTab === "settings" ? "bg-amber-500 text-black shadow" : "text-zinc-400 hover:text-white"}`, 4663 children: [ 4664 "âï¸ ", 4665 locale.setup 4666 ] 4667 } 4668 ),
4669 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4670 "button", 4671 { 4672 onClick: () => setLobbyTab("leaderboard"), 4673 className: `px-2.5 py-1 rounded-lg font-bold transition-all flex items-center gap-1 ${lobbyTab === "leaderboard" ? "bg-amber-500 text-black shadow" : "text-zinc-400 hover:text-white"}`, 4674 children: [ 4675 "ð ", 4676 locale.leaderboard, 4677 " ", 4678 leaderboard.length > 0 && `(${leaderboard.length})` 4679 ] 4680 } 4681 ) 4682 ] }) 4683 ] }), 4684 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-12 gap-3 sm:gap-4", children: [ 4685 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `md:col-span-7 space-y-2.5 ${lobbyTab === "settings" ? "block" : "hidden md:block"}`, children: [ 4686 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: [
4687 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4688 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-zinc-400 font-bold block mb-1", children: locale.players }), 4689 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [ 4690 /* @__PURE__ */ jsxRuntimeExports.jsx( 4691 "button", 4692 { 4693 onClick: () => setPlayerCount(1), 4694 className: `py-1.5 rounded-xl text-xs font-black border transition-all ${playerCount === 1 ? "bg-red-600 border-red-400 text-white shadow-md" : "bg-zinc-800/80 border-white/10 text-zinc-400 hover:bg-zinc-700"}`, 4695 children: locale.p1Solo || "1P Solo" 4696 } 4697 ), 4698 /* @__PURE__ */ jsxRuntimeExports.jsx( 4699 "button", 4700 { 4701 onClick: () => setPlayerCount(2), 4702 className: `py-1.5 rounded-xl text-xs font-black border transition-all ${playerCount === 2 ? "bg-blue-600 border-blue-400 text-white shadow-md" : "bg-zinc-800/80 border-white/10 text-zinc-400 hover:bg-zinc-700"}`, 4703 children: locale.p2Duo || "2P Duo" 4704 } 4705 ) 4706 ] }) 4707 ] }),
4708 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4709 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-zinc-400 font-bold block mb-1", children: locale.speed }), 4710 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1", children: [0.8, 1, 1.2, 1.5].map((s) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 4711 "button", 4712 { 4713 onClick: () => setScrollSpeedMultiplier(s), 4714 className: `py-1 rounded-lg text-xs font-black border transition-all ${scrollSpeedMultiplier === s ? "bg-amber-600 border-amber-300 text-white shadow-md" : "bg-zinc-800/80 border-white/10 text-zinc-400 hover:bg-zinc-700"}`, 4715 children: [ 4716 s, 4717 "x" 4718 ] 4719 }, 4720 s 4721 )) }) 4722 ] }) 4723 ] }),
4724 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4725 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-zinc-400 font-bold block mb-1", children: locale.difficulty }), 4726 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-1.5", children: ["kantan", "futsuu", "muzukashii", "oni"].map((d) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 4727 "button", 4728 { 4729 onClick: () => setDifficulty(d), 4730 className: `py-1.5 px-1 rounded-xl text-xs font-black border transition-all flex flex-col sm:flex-row items-center justify-center gap-0.5 sm:gap-1 ${difficulty === d ? "bg-gradient-to-b from-amber-500 to-red-600 border-amber-300 text-white shadow-md scale-[1.02]" : "bg-zinc-800/80 border-white/10 text-zinc-400 hover:bg-zinc-700 hover:text-white"}`, 4731 children: [ 4732 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs", children: DIFFICULTY_CONFIG[d].icon }),
4733 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-tight text-[10px] sm:text-xs", children: d === "kantan" ? locale.kantan : d === "futsuu" ? locale.futsuu : d === "muzukashii" ? locale.muzukashii : locale.oni }) 4734 ] 4735 }, 4736 d 4737 )) }) 4738 ] }), 4739 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 4740 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-zinc-400 font-bold block mb-1", children: locale.stageTheme }), 4741 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 gap-1.5", children: BACKGROUNDS.map((bg) => /* @__PURE__ */ jsxRuntimeExports.jsx( 4742 "button", 4743 { 4744 onClick: () => setSelectedBackground(bg.id), 4745 className: `py-1 px-2 rounded-lg text-xs font-bold border truncate transition-all text-left ${selectedBackground === bg.id ? "bg-zinc-700/90 border-amber-400 text-amber-300 shadow" : "bg-zinc-800/60 border-white/10 text-zinc-400 hover:bg-zinc-700 hover:text-white"}`, 4746 children: bg.label 4747 }, 4748 bg.id 4749 )) }) 4750 ] }),
4751 /* @__PURE__ */ jsxRuntimeExports.jsx( 4752 "button", 4753 { 4754 onClick: handleStartGame, 4755 disabled: waitingForAudio, 4756 className: `w-full mt-1 py-2.5 ${waitingForAudio ? "bg-zinc-700/80 border border-zinc-600 text-zinc-300 cursor-wait" : "bg-gradient-to-r from-red-600 via-rose-600 to-amber-600 hover:from-red-500 hover:to-amber-500 text-white transform active:scale-[0.98]"} font-black text-sm rounded-xl shadow-xl transition-all flex items-center justify-center gap-2`, 4757 children: waitingForAudio ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 4758 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 16, className: "animate-spin text-amber-400" }), 4759 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: locale.loadingAudio || "Loading Audio..." }) 4760 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
4761 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 16, fill: "white" }), 4762 " ", 4763 locale.start 4764 ] }) 4765 } 4766 ) 4767 ] }), 4768 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `md:col-span-5 flex flex-col min-h-0 bg-black/40 rounded-xl border border-white/10 p-3 ${lobbyTab === "leaderboard" ? "flex" : "hidden md:flex"}`, children: [ 4769 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs font-black text-amber-400 mb-2 px-1", children: [ 4770 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 4771 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 14 }), 4772 " ", 4773 locale.topTaikoMasters 4774 ] }), 4775 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-normal", children: locale.leaderboard }) 4776 ] }), 4777 leaderboard.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center py-8 text-center text-zinc-500 text-xs", children: [
4778 /* @__PURE__ */ jsxRuntimeExports.jsx(Trophy, { size: 28, className: "text-zinc-600 mb-2 opacity-50" }), 4779 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold text-zinc-400", children: locale.noHighscoresYet }), 4780 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-500 mt-1", children: locale.beFirstToSetRecord }) 4781 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 flex-1 min-h-0", children: [ 4782 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-3 gap-1.5 items-end mb-1 pt-2", children: [ 4783 leaderboard[1] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-zinc-800/60 border border-slate-400/40 rounded-xl p-1.5 text-center", children: [ 4784 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base", children: "ð¥" }), 4785 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-slate-200 truncate w-full", title: leaderboard[1].userName, children: leaderboard[1].userName || "Player 2" }), 4786 leaderboard[1].countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: leaderboard[1].countryCode, size: 11 }),
4787 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono font-black text-emerald-400 mt-0.5", children: Number(leaderboard[1].score).toLocaleString() }) 4788 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-zinc-800/30 border border-white/5 rounded-xl p-1.5 text-center text-zinc-500", children: [ 4789 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base", children: "ð¥" }), 4790 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500 truncate w-full", children: "-" }), 4791 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-zinc-600 mt-0.5", children: "-" }) 4792 ] }), 4793 leaderboard[0] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-gradient-to-t from-amber-500/20 to-zinc-800/80 border border-amber-400/60 rounded-xl p-2 text-center shadow-[0_0_15px_rgba(245,158,11,0.2)] -mt-3", children: [
4794 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl", children: "ð¥" }), 4795 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-black text-amber-200 truncate w-full", title: leaderboard[0].userName, children: leaderboard[0].userName || "Taiko Master" }), 4796 leaderboard[0].countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: leaderboard[0].countryCode, size: 12 }), 4797 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-mono font-black text-amber-300 mt-0.5", children: Number(leaderboard[0].score).toLocaleString() }) 4798 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-zinc-800/30 border border-white/5 rounded-xl p-2 text-center text-zinc-500 -mt-3", children: [ 4799 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl", children: "ð¥" }), 4800 /* @__P
4800URE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-bold text-zinc-500 truncate w-full", children: "-" }), 4801 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-mono text-zinc-600 mt-0.5", children: "-" }) 4802 ] }), 4803 leaderboard[2] ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-zinc-800/60 border border-amber-700/40 rounded-xl p-1.5 text-center", children: [ 4804 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base", children: "ð¥" }), 4805 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-amber-300 truncate w-full", title: leaderboard[2].userName, children: leaderboard[2].userName || "Player 3" }), 4806 leaderboard[2].countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: leaderboard[2].countryCode, size: 11 }), 4807 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono font-black text-emerald-400 mt-0.5", children: Number(leaderboard[2].score).toLocaleString() }) 4808 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center bg-zinc-800/30 border border-white/5 rounded-xl p-1.5 text-center text-zinc-500", children: [
4809 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-base", children: "ð¥" }), 4810 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500 truncate w-full", children: "-" }), 4811 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-zinc-600 mt-0.5", children: "-" }) 4812 ] }) 4813 ] }), 4814 leaderboard.length > 3 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto custom-scrollbar space-y-1 max-h-[110px] pr-1", children: leaderboard.slice(3, 8).map((item, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-2 py-1 bg-white/5 rounded-lg text-xs", children: [ 4815 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [ 4816 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-500 text-[10px] w-3", children: idx + 4 }),
4817 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-200 truncate max-w-[85px]", children: item.userName }), 4818 item.countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: item.countryCode, size: 10 }) 4819 ] }), 4820 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-emerald-400 font-bold text-[10px]", children: Number(item.score).toLocaleString() }) 4821 ] }, idx)) }) 4822 ] }) 4823 ] }) 4824 ] }) 4825 ] }) }), 4826 gameState === "results" && /* @__PURE__ */ jsxRuntimeExports.jsx( 4827 TaikoResultsScreen, 4828 { 4829 trackId, 4830 userId, 4831 userName, 4832 results: livePlayerStatesRef.current.map((p) => { 4833 const totalHits = p.perfectCount + p.goodCount + p.missCount; 4834 const acc = totalHits > 0 ? Math.round((p.perfectCount + p.goodCount * 0.5) / totalHits * 100) : 0; 4835 const songDur = Math.max(10, durationRef.current || 60); 4836 const secDur = songDur / 5; 4837 const sections = (p.sectionHits || createDefaultSectionHits()).map((s, idx) => { 4838 const sTotal = s.perfect + s.good + s.miss; 4839 const sAcc = sTotal > 0 ? Math.round((s.perfect + s.good * 0.5) / sTotal * 100) : 100; 4840 const cleared = sAcc >= 70; 4841 const crown = s.miss === 0 && sTotal > 0 ? "gold" : cleared ? "silver" : "none"; 4842 return { 4843 sectionIndex: idx, 4844 startTime: idx * secDur, 4845 endTime: (idx + 1) * secDur, 4846 perfectCount: s.perfect, 4847 goodCount: s.good, 4848 missCount: s.miss, 4849 totalNotes: sTotal, 4850 accuracy: sAcc, 4851 cleared, 4852 crown 4853 }; 4854 }); 4855 return { 4856 playerId: p.id, 4857 score: p.score, 4858 accuracy: acc, 4859 maxCombo: p.maxCombo, 4860 perfectCount: p.perfectCount, 4861 goodCount: p.goodCount, 4862 missCount: p.missCount, 4863 drumrollHits: p.drumrollHits, 4864 cleared: p.soulGauge >= 80, 4865 soulGauge: p.soulGauge, 4866 sections 4867 }; 4868 }), 4869 isCooperative, 4870 difficulty, 4871 chartSource: (chart == null ? void 0 : chart.meta.source) || "auto", 4872 onPlayAgain: () => { 4873 setGameState("countdown"); 4874 handleStartGame(); 4875 }, 4876 onBackToLobby: () => setGameState("lobby"), 4877 onPracticeSection: (startTime) => { 4878 setGameState("countdown");
4879 handleStartGame(Math.max(0, startTime - 1)); 4880 } 4881 } 4882 ) 4883 ] }) 4884 ] 4885 } 4886 ); 4887}; 4888const Taiko = (props) => { 4889 return /* @__PURE__ */ jsxRuntimeExports.jsx(TaikoGame, { ...props }); 4890}; 4891export { 4892 Taiko, 4893 Taiko as default 4894};
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.