🔬 AI発明ギャラリー tag 音ゲーNO
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>白桜の歩み - Sakura Rhythm Game</title>
    <!-- Tailwind CSS for rich modern UI styling -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Google Fonts for premium Japanese elegant look -->
    <link href="https://fonts.googleapis.com/css2?family=Kiwi+Maru:wght@400;500&family=Murecho:wght@400;700&display=swap" rel="stylesheet">
    <!-- FontAwesome icons for elegant control buttons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    
    <style>
        body {
            font-family: 'Murecho', 'Kiwi Maru', sans-serif;
            background: #05020a;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
        }
        
        /* Pulse glow animation for glowing assets */
        @keyframes pulseGlow {
            0%, 100% { filter: drop-shadow(0 0 5px rgba(244, 63, 94, 0.4)); }
            50% { filter: drop-shadow(0 0 15px rgba(244, 63, 94, 0.8)); }
        }
        .glowing-sakura {
            animation: pulseGlow 3s infinite ease-in-out;
        }

        /* Fancy scrollbar styling */
        ::-webkit-scrollbar {
            width: 4px;
        }
        ::-webkit-scrollbar-track {
            background: rgba(255, 255, 255, 0.05);
        }
        ::-webkit-scrollbar-thumb {
            background: rgba(244, 63, 94, 0.3);
            border-radius: 2px;
        }
    </style>
</head>
<body class="relative h-screen w-screen text-white flex justify-center items-center">

    <!-- Main game container styled as a premium centered game window (like mobile rhythm apps) -->
    <div class="relative w-full h-screen max-w-4xl bg-[#08020d] shadow-2xl flex flex-col justify-between border-x border-pink-500/20 overflow-hidden">
        
        <!-- Decorative Floating Sakura Petals in Background (HTML Particle Elements) -->
        <div id="html-petals-container" class="absolute inset-0 pointer-events-none overflow-hidden z-0"></div>

        <!-- Start / Splash Screen Overlay -->
        <div id="start-screen" class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-gradient-to-br from-[#0c0312] via-[#14081c] to-[#040107] transition-all duration-700 overflow-y-auto py-8">
            <div class="text-center px-4 max-w-xl w-full">
                <!-- Glowing Kitsune Mascot Icon -->
                <div class="mb-2 text-pink-400 text-5xl glowing-sakura flex justify-center gap-4">
                    <span class="text-rose-400 transform -rotate-12"><i class="fa-solid fa-mask"></i></span>
                    <span class="text-pink-300 animate-bounce"><i class="fa-solid fa-wind"></i></span>
                </div>
                <h1 class="text-4xl md:text-5xl font-bold tracking-widest text-transparent bg-clip-text bg-gradient-to-r from-pink-300 via-rose-400 to-orange-200 mb-1 font-['Kiwi_Maru']">
                    白桜の歩み
                </h1>
                <p class="text-[10px] text-pink-300 tracking-widest uppercase mb-4 opacity-80">~ Kitsune Lantern Festival Walk ~</p>
                
                <!-- Play Guide -->
                <div class="bg-white/5 border border-pink-500/20 rounded-2xl p-4 md:p-5 backdrop-blur-md mb-4 text-left text-xs md:text-sm text-pink-100/80 leading-relaxed">
                    <p class="mb-2 text-pink-300 font-semibold"><i class="fa-solid fa-circle-info mr-2"></i>遊び方 :</p>
                    <p class="mb-1.5">・お狐様の夜桜並木を歩きながら、奥から流れてくる四角が判定線に重なる瞬間に対応するキー(D, F, J, K)を押してください。</p>
                    <p class="mb-2 font-semibold text-orange-300">💡 スマホの方は、画面下部の各ボタンを直接タップすることでも遊べます!</p>
                    <div class="grid grid-cols-4 gap-2 text-center my-2 font-mono font-bold text-pink-400">
                        <span class="bg-pink-950/40 py-1 rounded border border-pink-500/30">D (左)</span>
                        <span class="bg-pink-950/40 py-1 rounded border border-pink-500/30">F (中左)</span>
                        <span class="bg-pink-950/40 py-1 rounded border border-pink-500/30">J (中右)</span>
                        <span class="bg-pink-950/40 py-1 rounded border border-pink-500/30">K (右)</span>
                    </div>
                </div>

                <!-- Difficulty Selection Panel -->
                <div class="bg-white/5 border border-pink-500/20 rounded-2xl p-4 backdrop-blur-md mb-6 text-left">
                    <p class="mb-3 text-pink-300 font-semibold text-xs md:text-sm text-center">
                        <i class="fa-solid fa-sliders mr-2"></i>難易度を選んでください (Select Difficulty)
                    </p>
                    <div class="grid grid-cols-3 gap-2">
                        <!-- Easy -->
                        <button id="diff-easy-btn" onclick="setDifficulty('easy')" class="py-2 px-3 rounded-xl border border-emerald-500/30 bg-emerald-950/20 hover:bg-emerald-950/40 text-emerald-300 text-xs md:text-sm font-semibold transition-all flex flex-col items-center gap-1">
                            <span class="text-base"><i class="fa-solid fa-feather-pointed"></i></span>
                            <span>かんたん</span>
                            <span class="text-[9px] opacity-70">ゆっくり</span>
                        </button>
                        <!-- Normal (Selected by default) -->
                        <button id="diff-normal-btn" onclick="setDifficulty('normal')" class="py-2 px-3 rounded-xl border-2 border-pink-500 bg-pink-950/40 text-white text-xs md:text-sm font-bold transition-all shadow-lg shadow-pink-500/10 flex flex-col items-center gap-1">
                            <span class="text-base"><i class="fa-solid fa-music"></i></span>
                            <span>ふつう</span>
                            <span class="text-[9px] opacity-70">標準</span>
                        </button>
                        <!-- Hard -->
                        <button id="diff-hard-btn" onclick="setDifficulty('hard')" class="py-2 px-3 rounded-xl border border-rose-500/30 bg-rose-950/20 hover:bg-rose-950/40 text-rose-300 text-xs md:text-sm font-semibold transition-all flex flex-col items-center gap-1">
                            <span class="text-base"><i class="fa-solid fa-fire"></i></span>
                            <span>むずかしい</span>
                            <span class="text-[9px] opacity-70">高密度・高速</span>
                        </button>
                    </div>
                </div>

                <!-- Loading Box and Play Button -->
                <div id="loading-box" class="flex flex-col items-center">
                    <!-- Music loading progress -->
                    <div class="w-64 h-2 bg-pink-950/60 rounded-full overflow-hidden mb-3 border border-pink-500/10">
                        <div id="loading-bar" class="w-0 h-full bg-gradient-to-r from-pink-500 via-rose-400 to-orange-400 transition-all duration-300"></div>
                    </div>
                    <p id="loading-status" class="text-xs text-pink-300/70 tracking-wider font-mono">神楽曲「白桜の舟」をロード中...</p>
                    
                    <button id="start-btn" class="hidden mt-4 px-10 py-4 bg-gradient-to-r from-rose-600 via-pink-600 to-orange-500 hover:from-pink-500 hover:to-rose-400 text-white font-bold text-lg rounded-full shadow-lg shadow-rose-500/20 transform hover:scale-105 active:scale-95 transition-all duration-200 uppercase tracking-widest border border-pink-400/30 animate-pulse">
                        <i class="fa-solid fa-play mr-2"></i> 夜宮を奏で歩む
                    </button>
                </div>
            </div>
        </div>

        <!-- Beautiful Background & Scenery Canvas (Full Height, behind the notes) -->
        <canvas id="scenery-canvas" class="absolute inset-0 w-full h-full object-cover z-10 pointer-events-none"></canvas>

        <!-- Rhythm Lane Overlay Canvas (renders falling 3D perspective glowing tiles) -->
        <canvas id="game-canvas" class="absolute inset-0 w-full h-full object-cover z-20 pointer-events-none"></canvas>

        <!-- Top HUD Panel (Glassmorphism layout) -->
        <div class="absolute top-4 left-4 right-4 flex justify-between items-center z-30 pointer-events-auto">
            <div class="flex items-center gap-3 bg-black/55 backdrop-blur-md px-4 py-2 rounded-full border border-pink-500/20">
                <div class="w-2.5 h-2.5 rounded-full bg-orange-400 animate-pulse"></div>
                <span class="text-xs md:text-sm font-semibold tracking-wide text-pink-100">
                    白桜の舟 <span id="hud-diff-badge" class="ml-2 px-2 py-0.5 rounded-full bg-pink-500/30 text-[10px] text-pink-300 font-bold border border-pink-500/40">ふつう</span>
                </span>
            </div>
            
            <div class="flex gap-2">
                <button id="restart-btn" class="p-2.5 bg-black/45 hover:bg-pink-600/30 backdrop-blur-md rounded-full border border-pink-500/20 hover:border-pink-500/50 transition-all duration-150 text-white text-xs" title="もう一度遊ぶ">
                    <i class="fa-solid fa-rotate-right"></i>
                </button>
            </div>
        </div>

        <!-- Combo Counter & Live Score HUD -->
        <div class="absolute inset-x-0 top-1/4 flex flex-col items-center justify-center pointer-events-none z-30 select-none">
            <div class="text-center">
                <p id="hud-combo" class="text-5xl font-extrabold tracking-wider text-white hidden transform scale-100 transition-transform duration-100 drop-shadow-[0_0_12px_rgba(244,63,94,0.8)]">
                    0 <span class="text-xs tracking-widest font-bold text-orange-300 block uppercase mt-1">Combo</span>
                </p>
                <div id="hud-judgment" class="text-3xl font-black tracking-widest text-pink-300 opacity-0 transform translate-y-2 transition-all duration-100 h-10 drop-shadow-[0_0_8px_rgba(255,255,255,0.7)]">
                    PERFECT
                </div>
            </div>
        </div>

        <!-- Interactive Invisible Touch Zones for Tap Response -->
        <div class="absolute inset-x-0 bottom-24 top-0 flex z-20 pointer-events-auto">
            <div id="lane-touch-0" class="flex-1 h-full cursor-pointer relative" onclick="handleLanePress(0)"></div>
            <div id="lane-touch-1" class="flex-1 h-full cursor-pointer relative" onclick="handleLanePress(1)"></div>
            <div id="lane-touch-2" class="flex-1 h-full cursor-pointer relative" onclick="handleLanePress(2)"></div>
            <div id="lane-touch-3" class="flex-1 h-full cursor-pointer relative" onclick="handleLanePress(3)"></div>
        </div>

        <!-- Real-time HUD and Tap Keys container at the bottom -->
        <div class="w-full absolute bottom-0 inset-x-0 z-40 flex flex-col items-center bg-gradient-to-t from-[#05010a] via-[#08020e]/95 to-transparent pt-6 pb-4 pointer-events-auto">
            
            <!-- Realtime Metrics: Score, Accuracy, Max Combo -->
            <div class="w-full max-w-2xl px-6 flex justify-between items-center text-xs text-pink-200/90 mb-3 font-mono">
                <div>SCORE: <span id="stat-score" class="font-bold text-white text-sm tracking-widest">0000000</span></div>
                <div>ACC: <span id="stat-accuracy" class="font-bold text-orange-300 text-sm">100.0%</span></div>
                <div>MAX COMBO: <span id="stat-maxcombo" class="font-bold text-pink-300 text-sm">0</span></div>
            </div>

            <!-- Tactile Input Buttons (Seamlessly matches the 4 perspective lanes width) -->
            <div class="w-full max-w-2xl px-4">
                <div class="grid grid-cols-4 border-t border-pink-500/20 bg-black/70 rounded-2xl overflow-hidden shadow-2xl">
                    <!-- Button 1 -->
                    <button id="tap-btn-0" class="py-4 border-r border-pink-500/10 bg-pink-950/20 hover:bg-pink-950/35 active:bg-pink-500/40 shadow-inner text-white font-black flex flex-col items-center justify-center transition-all duration-75 active:scale-95" onmousedown="triggerTapVisual(0)" onmouseup="releaseTapVisual(0)" ontouchstart="triggerTapVisual(0); event.stopPropagation();" ontouchend="releaseTapVisual(0); event.stopPropagation();">
                        <span class="text-pink-300 font-['Kiwi_Maru'] text-[10px] font-normal mb-0.5">桜色</span>
                        <span class="text-lg font-bold tracking-wider font-mono">D</span>
                    </button>
                    <!-- Button 2 -->
                    <button id="tap-btn-1" class="py-4 border-r border-pink-500/10 bg-pink-950/20 hover:bg-pink-950/35 active:bg-pink-500/40 shadow-inner text-white font-black flex flex-col items-center justify-center transition-all duration-75 active:scale-95" onmousedown="triggerTapVisual(1)" onmouseup="releaseTapVisual(1)" ontouchstart="triggerTapVisual(1); event.stopPropagation();" ontouchend="releaseTapVisual(1); event.stopPropagation();">
                        <span class="text-pink-300 font-['Kiwi_Maru'] text-[10px] font-normal mb-0.5">神楽</span>
                        <span class="text-lg font-bold tracking-wider font-mono">F</span>
                    </button>
                    <!-- Button 3 -->
                    <button id="tap-btn-2" class="py-4 border-r border-pink-500/10 bg-pink-950/20 hover:bg-pink-950/35 active:bg-pink-500/40 shadow-inner text-white font-black flex flex-col items-center justify-center transition-all duration-75 active:scale-95" onmousedown="triggerTapVisual(2)" onmouseup="releaseTapVisual(2)" ontouchstart="triggerTapVisual(2); event.stopPropagation();" ontouchend="releaseTapVisual(2); event.stopPropagation();">
                        <span class="text-pink-300 font-['Kiwi_Maru'] text-[10px] font-normal mb-0.5">狐面</span>
                        <span class="text-lg font-bold tracking-wider font-mono">J</span>
                    </button>
                    <!-- Button 4 -->
                    <button id="tap-btn-3" class="py-4 bg-pink-950/20 hover:bg-pink-950/35 active:bg-pink-500/40 shadow-inner text-white font-black flex flex-col items-center justify-center transition-all duration-75 active:scale-95" onmousedown="triggerTapVisual(3)" onmouseup="releaseTapVisual(3)" ontouchstart="triggerTapVisual(3); event.stopPropagation();" ontouchend="releaseTapVisual(3); event.stopPropagation();">
                        <span class="text-pink-300 font-['Kiwi_Maru'] text-[10px] font-normal mb-0.5">祭囃</span>
                        <span class="text-lg font-bold tracking-wider font-mono">K</span>
                    </button>
                </div>
            </div>
        </div>
    </div>

    <!-- Score Results Dialog -->
    <div id="results-screen" class="hidden absolute inset-0 z-50 bg-black/95 backdrop-blur-xl flex items-center justify-center p-4 transition-all duration-500">
        <div class="bg-gradient-to-b from-[#1b081e] to-[#040108] border border-pink-500/30 rounded-3xl p-8 max-w-md w-full text-center shadow-2xl shadow-pink-500/20">
            <h2 class="text-3xl font-bold text-pink-300 font-['Kiwi_Maru'] mb-2 tracking-wide">白桜の歩み 結び</h2>
            <p class="text-xs text-orange-400/60 uppercase tracking-widest mb-6">— Result Evaluation —</p>
            
            <!-- Overall Rank Badge -->
            <div class="flex justify-center mb-6">
                <div id="result-rank" class="w-24 h-24 rounded-full bg-pink-500/10 border-2 border-pink-500/40 flex items-center justify-center text-5xl font-black text-orange-300 glowing-sakura">
                    S
                </div>
            </div>

            <!-- Stats Table -->
            <div class="bg-black/40 rounded-2xl p-4 mb-6 space-y-2 text-sm font-mono text-left">
                <div class="flex justify-between border-b border-pink-500/10 pb-1.5 text-pink-100">
                    <span>SCORE :</span>
                    <span id="res-score" class="font-bold text-pink-400 text-base">000,000</span>
                </div>
                <div class="flex justify-between border-b border-pink-500/10 pb-1.5 text-pink-100">
                    <span>ACCURACY :</span>
                    <span id="res-acc" class="font-bold text-orange-400 text-base">100.00%</span>
                </div>
                <div class="flex justify-between border-b border-pink-500/10 pb-1.5 text-pink-100 text-pink-300/90">
                    <span>MAX COMBO :</span>
                    <span id="res-maxcombo" class="font-bold text-pink-400 text-base">0</span>
                </div>
                <div class="flex justify-between text-xs text-white/60 pt-2 grid grid-cols-4 gap-1 text-center font-bold">
                    <div class="bg-pink-900/10 border border-emerald-500/20 py-1.5 rounded">
                        <span class="block text-[10px] text-emerald-400">PERFECT</span>
                        <span id="res-perf">0</span>
                    </div>
                    <div class="bg-pink-900/10 border border-cyan-500/20 py-1.5 rounded">
                        <span class="block text-[10px] text-cyan-400">GREAT</span>
                        <span id="res-great">0</span>
                    </div>
                    <div class="bg-pink-900/10 border border-amber-500/20 py-1.5 rounded">
                        <span class="block text-[10px] text-amber-400">GOOD</span>
                        <span id="res-good">0</span>
                    </div>
                    <div class="bg-pink-900/10 border border-rose-500/20 py-1.5 rounded">
                        <span class="block text-[10px] text-rose-400">MISS</span>
                        <span id="res-miss">0</span>
                    </div>
                </div>
            </div>

            <!-- Buttons -->
            <div class="flex flex-col gap-2">
                <button id="res-retry-btn" class="w-full py-3.5 bg-gradient-to-r from-rose-600 to-pink-600 hover:from-pink-500 hover:to-rose-400 text-white font-bold rounded-xl transition-all duration-200 shadow-lg shadow-pink-500/20">
                    <i class="fa-solid fa-rotate-right mr-2"></i> もう一度歩む
                </button>
                <button id="res-close-btn" class="w-full py-2.5 bg-white/5 hover:bg-white/10 text-pink-300 text-xs rounded-xl border border-pink-500/20 transition-all duration-200">
                    閉じる
                </button>
            </div>
        </div>
    </div>

    <script>
        // Game state and calibration configurations
        const BPM = 128; 
        const BEAT_INTERVAL = 60 / BPM; // ~0.46875s per beat
        
        // Difficulty Setting Configuration
        let difficulty = 'normal'; // 'easy', 'normal', 'hard'
        let NOTE_SPEED = 1.1; // Seconds notes take to drop
        const JUDGMENT_LINE_Y_RATIO = 0.82; // Position of the horizontal judgment bar

        // Key guidance map helper
        const KEY_LABELS = ['D', 'F', 'J', 'K'];

        // Audio Object & Web Audio Setup for Interactive Clicking Sounds
        const audio = new Audio();
        audio.src = "https://petitacode.web2050.jp/users/original/tag/tmp/%E7%99%BD%E6%A1%9C%E3%81%AE%E8%88%9F.mp3";

        let audioContext;
        let isAudioFallbackMode = false; // BGMの読み込みに失敗した場合にシンセサイザーBGMに切り替える
        let synthBgmIntervalId = null;   // シンセサイザーBGM演奏用タイマー
        
        // 外部音楽が鳴らないときの「安全なミリ秒経過タイマー」
        let gameStartTime = 0;
        let elapsedTime = 0;

        // Initialize Web Audio API on click to satisfy user gesture policy
        function initSynthAudio() {
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
            }
            if (audioContext && audioContext.state === 'suspended') {
                audioContext.resume();
            }
        }

        // Tap synthesizer sound generator (beautiful retro bubble/cherry plop sound)
        function playTapSound(freq = 523.25, type = 'sine', duration = 0.08) {
            if (!audioContext) return;
            try {
                const osc = audioContext.createOscillator();
                const gain = audioContext.createGain();
                
                osc.type = type;
                osc.frequency.setValueAtTime(freq, audioContext.currentTime);
                osc.frequency.exponentialRampToValueAtTime(freq * 0.4, audioContext.currentTime + duration);

                gain.gain.setValueAtTime(0.12, audioContext.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration);

                osc.connect(gain);
                gain.connect(audioContext.destination);
                
                osc.start();
                osc.stop(audioContext.currentTime + duration);
            } catch (e) {
                console.warn("Audio synthesis error: ", e);
            }
        }

        // 🌟 100%音が鳴る!バックアップ用・和風琴風シンセサイザーBGMシステム
        let notePatternIndex = 0;
        const KOTO_SCALE = [261.63, 293.66, 311.13, 349.23, 392.00, 415.30, 466.16, 523.25]; // 陰旋法に近い、和風の情緒的なスケール

        function playSynthBgmStep() {
            if (!audioContext || !gamePlaying) return;
            try {
                // 拍子に合わせた和太鼓のようなポンというリズム
                const drumOsc = audioContext.createOscillator();
                const drumGain = audioContext.createGain();
                drumOsc.type = 'sine';
                drumOsc.frequency.setValueAtTime(100, audioContext.currentTime);
                drumOsc.frequency.exponentialRampToValueAtTime(30, audioContext.currentTime + 0.15);
                drumGain.gain.setValueAtTime(notePatternIndex % 4 === 0 ? 0.3 : 0.15, audioContext.currentTime);
                drumGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.15);
                drumOsc.connect(drumGain);
                drumGain.connect(audioContext.destination);
                drumOsc.start();
                drumOsc.stop(audioContext.currentTime + 0.15);

                // 和琴(Koto)のような涼しげできらきらした音階メロディ
                if (notePatternIndex % 2 === 0) {
                    const melodyOsc = audioContext.createOscillator();
                    const melodyGain = audioContext.createGain();
                    
                    // 情緒的に変化する和風メロディ
                    const pitch = KOTO_SCALE[(notePatternIndex + Math.floor(Math.sin(notePatternIndex) * 3) + 8) % KOTO_SCALE.length];
                    melodyOsc.type = 'triangle';
                    melodyOsc.frequency.setValueAtTime(pitch, audioContext.currentTime);
                    
                    // 減衰する余韻
                    melodyGain.gain.setValueAtTime(0.15, audioContext.currentTime);
                    melodyGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.55);
                    
                    melodyOsc.connect(melodyGain);
                    melodyGain.connect(audioContext.destination);
                    melodyOsc.start();
                    melodyOsc.stop(audioContext.currentTime + 0.6);
                }

                notePatternIndex++;
            } catch (e) {
                console.error("Synth BGM step play error: ", e);
            }
        }

        // BGMを開始
        function startSynthBgm() {
            stopSynthBgm();
            notePatternIndex = 0;
            // BPM 128に同期した間隔(8分音符で演奏:約0.234秒間隔)
            const intervalMs = (60 / BPM / 2) * 1000;
            synthBgmIntervalId = setInterval(playSynthBgmStep, intervalMs);
        }

        // BGMを停止
        function stopSynthBgm() {
            if (synthBgmIntervalId) {
                clearInterval(synthBgmIntervalId);
                synthBgmIntervalId = null;
            }
        }

        // Difficulty toggler handler UI
        function setDifficulty(level) {
            difficulty = level;
            
            // Adjust speeds based on easy/normal/hard
            if (difficulty === 'easy') {
                NOTE_SPEED = 1.4; // Slower drop speed for easier reading
            } else if (difficulty === 'normal') {
                NOTE_SPEED = 1.1;
            } else if (difficulty === 'hard') {
                NOTE_SPEED = 0.8; // Fast drop speed for high action response
            }

            // Update UI selected state
            const easyBtn = document.getElementById('diff-easy-btn');
            const normalBtn = document.getElementById('diff-normal-btn');
            const hardBtn = document.getElementById('diff-hard-btn');
            const hudDiffBadge = document.getElementById('hud-diff-badge');

            // Reset classes
            easyBtn.className = "py-2 px-3 rounded-xl border border-emerald-500/30 bg-emerald-950/20 hover:bg-emerald-950/40 text-emerald-300 text-xs md:text-sm font-semibold transition-all flex flex-col items-center gap-1";
            normalBtn.className = "py-2 px-3 rounded-xl border border-pink-500/30 bg-pink-950/20 hover:bg-pink-950/40 text-pink-300 text-xs md:text-sm font-semibold transition-all flex flex-col items-center gap-1";
            hardBtn.className = "py-2 px-3 rounded-xl border border-rose-500/30 bg-rose-950/20 hover:bg-rose-950/40 text-rose-300 text-xs md:text-sm font-semibold transition-all flex flex-col items-center gap-1";

            if (level === 'easy') {
                easyBtn.className = "py-2 px-3 rounded-xl border-2 border-emerald-500 bg-emerald-950/40 text-white text-xs md:text-sm font-bold transition-all shadow-lg shadow-emerald-500/10 flex flex-col items-center gap-1";
                hudDiffBadge.textContent = "かんたん";
                hudDiffBadge.className = "ml-2 px-2 py-0.5 rounded-full bg-emerald-500/30 text-[10px] text-emerald-300 font-bold border border-emerald-500/40";
            } else if (level === 'normal') {
                normalBtn.className = "py-2 px-3 rounded-xl border-2 border-pink-500 bg-pink-950/40 text-white text-xs md:text-sm font-bold transition-all shadow-lg shadow-pink-500/10 flex flex-col items-center gap-1";
                hudDiffBadge.textContent = "ふつう";
                hudDiffBadge.className = "ml-2 px-2 py-0.5 rounded-full bg-pink-500/30 text-[10px] text-pink-300 font-bold border border-pink-500/40";
            } else if (level === 'hard') {
                hardBtn.className = "py-2 px-3 rounded-xl border-2 border-rose-500 bg-rose-950/40 text-white text-xs md:text-sm font-bold transition-all shadow-lg shadow-rose-500/10 flex flex-col items-center gap-1";
                hudDiffBadge.textContent = "むずかしい";
                hudDiffBadge.className = "ml-2 px-2 py-0.5 rounded-full bg-rose-500/30 text-[10px] text-rose-300 font-bold border border-rose-500/40";
            }

            // High pitch note click for UI confirmation
            playTapSound(600, 'triangle', 0.05);
        }

        const startScreen = document.getElementById('start-screen');
        const resultsScreen = document.getElementById('results-screen');
        const loadingBar = document.getElementById('loading-bar');
        const loadingStatus = document.getElementById('loading-status');
        const startBtn = document.getElementById('start-btn');
        const restartBtn = document.getElementById('restart-btn');
        const resRetryBtn = document.getElementById('res-retry-btn');
        const resCloseBtn = document.getElementById('res-close-btn');

        const statScore = document.getElementById('stat-score');
        const statAccuracy = document.getElementById('stat-accuracy');
        const statMaxCombo = document.getElementById('stat-maxcombo');
        const hudCombo = document.getElementById('hud-combo');
        const hudJudgment = document.getElementById('hud-judgment');

        // Rhythm Map Definitions & Spawning Engine
        let notes = [];
        let score = 0;
        let combo = 0;
        let maxCombo = 0;
        let totalHitsAttempted = 0;
        let totalNotesCount = 0;
        
        // Detailed counters for final tally screen
        let countPerfect = 0;
        let countGreat = 0;
        let countGood = 0;
        let countMiss = 0;

        let gamePlaying = false;
        let spawnPointer = 0;

        // Structured procedural song rhythm track template
        let beatMap = [];

        function generateBeatMap() {
            beatMap = [];
            const trackLength = 120; // safe fallback length in seconds
            let currentTimer = 4.0; // Start note block around 4s to allow intro fade-in
            
            const patterns = [
                [0, 1, 2, 3],       // Straight ramp
                [0, 2, 1, 3],       // Zigzag
                [0, 0, 3, 3],       // Double outer
                [1, 2, 1, 2],       // Inner swing
                [0, 3, 1, 2],       // Inward focus
                [1, 1, 2, 2],       // Sync doublets
                [0, 2, 3, 1]        // Dynamic scatter
            ];

            let patternIndex = 0;
            
            while (currentTimer < trackLength) {
                const pattern = patterns[patternIndex % patterns.length];
                const section = Math.floor(currentTimer / 20); // change feel every 20 seconds
                let step = BEAT_INTERVAL; // 1 beat (~0.47s)

                if (difficulty === 'easy') {
                    // EASY MODE: Spawning notes sparsely (mostly every 2 or 4 beats)
                    const lane = Math.floor(Math.sin(currentTimer) * 2) + 1.5; // oscillate lane 1 or 2 comfortably
                    beatMap.push({ time: currentTimer, lane: Math.floor(lane) % 4 });
                    currentTimer += step * 2; // Slower cadence: two-beat spacing
                    
                    if (currentTimer % 12 === 0) {
                        currentTimer += step * 2; // occasional pauses
                    }
                } 
                else if (difficulty === 'normal') {
                    // NORMAL MODE: Traditional rhythm pacing
                    if (section === 0 || section === 5) {
                        // Soft introduction/outro: slow single notes
                        const lane = Math.floor(Math.sin(currentTimer) * 2) + 1;
                        beatMap.push({ time: currentTimer, lane: lane });
                        currentTimer += step * 2;
                    } else if (section === 1 || section === 3) {
                        for (let i = 0; i < 4; i++) {
                            beatMap.push({ time: currentTimer, lane: pattern[i] });
                            currentTimer += step;
                        }
                    } else if (section === 2) {
                        // Regular flow sequence
                        for (let i = 0; i < 6; i++) {
                            beatMap.push({ time: currentTimer, lane: Math.floor(Math.random() * 4) });
                            currentTimer += step;
                        }
                    } else {
                        // Mild dense syncopations
                        beatMap.push({ time: currentTimer, lane: pattern[0] });
                        currentTimer += step;
                        beatMap.push({ time: currentTimer, lane: pattern[3] });
                        currentTimer += step;
                    }
                } 
                else if (difficulty === 'hard') {
                    // HARD MODE: Intense 8th notes and dual-lane cords!
                    if (section === 0) {
                        // Rapid warmup
                        beatMap.push({ time: currentTimer, lane: pattern[0] });
                        currentTimer += step;
                        beatMap.push({ time: currentTimer, lane: pattern[2] });
                        currentTimer += step;
                    } else if (section === 2 || section === 4) {
                        // Fast consecutive streams (1/2 beats)
                        for (let i = 0; i < 8; i++) {
                            beatMap.push({ time: currentTimer, lane: Math.floor(Math.random() * 4) });
                            currentTimer += step * 0.5; // Extreme 8th-note streams!
                        }
                        currentTimer += step;
                    } else {
                        // Simultaneous double-hits!
                        beatMap.push({ time: currentTimer, lane: pattern[0] });
                        beatMap.push({ time: currentTimer, lane: pattern[3] });
                        currentTimer += step;
                        
                        beatMap.push({ time: currentTimer, lane: pattern[1] });
                        beatMap.push({ time: currentTimer, lane: pattern[2] });
                        currentTimer += step;
                    }
                }

                patternIndex++;
            }

            beatMap.sort((a, b) => a.time - b.time);
            totalNotesCount = beatMap.length;
        }

        // Web Audio & File load logic with solid error protection
        audio.addEventListener('canplaythrough', () => {
            loadingBar.style.width = '100%';
            loadingStatus.textContent = "白桜の舟 - ロード完了!";
            startBtn.classList.remove('hidden');
        });

        // Error fallback (if external domain MP3 block occurs, allow play immediately)
        audio.addEventListener('error', (e) => {
            console.warn("External MP3 load delay. Activating synthetic stabilizer backup.");
            loadingBar.style.width = '100%';
            loadingStatus.textContent = "和風シンセサイザー音源を準備しました!";
            isAudioFallbackMode = true;
            startBtn.classList.remove('hidden');
        });

        // 3秒経っても曲が読み込めない場合は、自動的にシンセサイザーBGMモードの準備をする(フリーズ防止!)
        setTimeout(() => {
            if (loadingBar.style.width !== '100%') {
                console.log("Audio load timeout. Activating fallback synthetic sound...");
                loadingBar.style.width = '100%';
                loadingStatus.textContent = "お狐様のお囃子シンセ演奏モードで開始可能!";
                isAudioFallbackMode = true;
                startBtn.classList.remove('hidden');
            }
        }, 3000);

        // Canvas element setups
        const sceneryCanvas = document.getElementById('scenery-canvas');
        const sCtx = sceneryCanvas.getContext('2d');

        // Game canvas
        const gameCanvas = document.getElementById('game-canvas');
        const gCtx = gameCanvas.getContext('2d');

        // Handle scaling across high PPI monitors dynamically
        function resizeCanvases() {
            const dpr = window.devicePixelRatio || 1;
            
            // Scenery Canvas dimensions (Fill screen)
            sceneryCanvas.width = sceneryCanvas.clientWidth * dpr;
            sceneryCanvas.height = sceneryCanvas.clientHeight * dpr;
            sCtx.scale(dpr, dpr);

            // Game Board Canvas dimensions (Fill screen)
            gameCanvas.width = gameCanvas.clientWidth * dpr;
            gameCanvas.height = gameCanvas.clientHeight * dpr;
            gCtx.scale(dpr, dpr);
        }

        window.addEventListener('resize', resizeCanvases);
        setTimeout(resizeCanvases, 200);

        // Parallax parameters
        let scrollOffset = 0;
        const speed = 1.2; // Scrolling velocity of the cherry blossom street

        // Sakura Petal Particle System
        class Petal {
            constructor(spawnOutside = false) {
                this.reset(spawnOutside);
            }

            reset(spawnOutside) {
                const w = sceneryCanvas.clientWidth || window.innerWidth;
                const h = sceneryCanvas.clientHeight || window.innerHeight;
                
                this.x = spawnOutside ? -50 : Math.random() * (w + 100) - 50;
                this.y = spawnOutside ? Math.random() * (h * 0.8) : -30;
                this.size = Math.random() * 6 + 4;
                this.speedX = Math.random() * 2 + 1.2; // drifting rightwards
                this.speedY = Math.random() * 1.5 + 0.8; // drifting downwards
                this.spin = Math.random() * Math.PI * 2;
                this.spinSpeed = Math.random() * 0.04 - 0.02;
                this.opacity = Math.random() * 0.5 + 0.4;
            }

            update(width, height) {
                this.x += this.speedX;
                this.y += this.speedY;
                this.spin += this.spinSpeed;
                
                if (this.x > width + 50 || this.y > height + 20) {
                    this.reset(true);
                }
            }

            draw(ctx) {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.spin);
                ctx.fillStyle = `rgba(255, 192, 203, ${this.opacity})`;
                ctx.shadowColor = 'rgba(244, 63, 94, 0.4)';
                ctx.shadowBlur = 4;
                
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.bezierCurveTo(-this.size/2, -this.size/2, -this.size, this.size/3, 0, this.size);
                ctx.bezierCurveTo(this.size, this.size/3, this.size/2, -this.size/2, 0, 0);
                ctx.closePath();
                ctx.fill();
                ctx.restore();
            }
        }

        const petalsList = [];
        for (let i = 0; i < 45; i++) {
            petalsList.push(new Petal(false));
        }

        // Background scenery: trees list
        class SakuraTree {
            constructor(x, scale, isForeground = false) {
                this.x = x;
                this.scale = scale;
                this.isForeground = isForeground;
            }

            draw(ctx, width, height) {
                const groundY = height * 0.72;
                const treeX = this.x;
                const size = 110 * this.scale;

                ctx.save();
                // Draw trunk
                ctx.strokeStyle = '#1b0918';
                ctx.lineWidth = 10 * this.scale;
                ctx.lineCap = 'round';
                ctx.beginPath();
                ctx.moveTo(treeX, groundY);
                ctx.quadraticCurveTo(treeX - 15 * this.scale, groundY - size * 0.5, treeX - 5 * this.scale, groundY - size);
                ctx.stroke();

                // Draw branches
                ctx.strokeStyle = '#1b0918';
                ctx.lineWidth = 5 * this.scale;
                ctx.beginPath();
                ctx.moveTo(treeX - 5 * this.scale, groundY - size);
                ctx.lineTo(treeX - 35 * this.scale, groundY - size - 30 * this.scale);
                ctx.moveTo(treeX - 5 * this.scale, groundY - size);
                ctx.lineTo(treeX + 25 * this.scale, groundY - size - 25 * this.scale);
                ctx.stroke();

                // Draw clouds of lush pink cherry blossoms
                const petalClouds = [
                    { rx: -25, ry: -25, r: 45 },
                    { rx: 25, ry: -15, r: 40 },
                    { rx: -5, ry: -45, r: 55 },
                    { rx: -45, ry: -10, r: 35 },
                    { rx: 40, ry: -30, r: 32 },
                ];

                ctx.shadowColor = 'rgba(251, 113, 133, 0.4)';
                ctx.shadowBlur = 12;
                
                petalClouds.forEach(cloud => {
                    const grad = ctx.createRadialGradient(
                        treeX + cloud.rx * this.scale, groundY - size + cloud.ry * this.scale, 5,
                        treeX + cloud.rx * this.scale, groundY - size + cloud.ry * this.scale, cloud.r * this.scale
                    );
                    grad.addColorStop(0, '#ffe4e6'); // super bright pink core
                    grad.addColorStop(0.5, '#fecdd3'); // standard blossom
                    grad.addColorStop(1, 'rgba(244, 63, 94, 0)'); // fade out edge
                    
                    ctx.fillStyle = grad;
                    ctx.beginPath();
                    ctx.arc(treeX + cloud.rx * this.scale, groundY - size + cloud.ry * this.scale, cloud.r * this.scale, 0, Math.PI * 2);
                    ctx.fill();
                });

                ctx.restore();
            }
        }

        const backgroundTrees = [];
        for (let i = 0; i < 10; i++) {
            const treeScale = 0.8 + Math.random() * 0.5;
            backgroundTrees.push(new SakuraTree(i * 180, treeScale, false));
        }

        // Draw the highly customizable anime girl matching image_73dc06.png (Yukata, Kitsune Mask, Side highlights)
        function drawCuteAnimeGirl(ctx, x, y, scale, tick) {
            ctx.save();
            ctx.translate(x, y);
            
            // Gentle idle walking motion variables
            const animSpeed = 5.0;
            const cycle = tick * animSpeed;
            const bobbing = Math.abs(Math.sin(cycle)) * 6.5 * scale;
            ctx.translate(0, -bobbing); // walk-bob

            // --- 1. Large Traditional Red Wagasa Umbrella (後ろの和傘) ---
            ctx.save();
            const umbrellaX = 5 * scale;
            const umbrellaY = -115 * scale;
            ctx.translate(umbrellaX, umbrellaY);
            ctx.rotate(0.04 * Math.sin(tick * 2) - 0.05);

            // Red/orange canopy gradient matching the vibrant warm tones of the image
            const uGrad = ctx.createRadialGradient(0, 0, 5, 0, 0, 95 * scale);
            uGrad.addColorStop(0, '#fecdd3'); // bright center
            uGrad.addColorStop(0.3, '#f43f5e'); // primary red-pink
            uGrad.addColorStop(0.85, '#9f1239'); // deep rose red
            uGrad.addColorStop(1, '#4c0519'); // dark border

            ctx.shadowColor = 'rgba(0, 0, 0, 0.35)';
            ctx.shadowBlur = 12;
            ctx.fillStyle = uGrad;
            ctx.beginPath();
            ctx.arc(0, 0, 95 * scale, 0, Math.PI * 2);
            ctx.fill();
            ctx.shadowBlur = 0; // reset

            // 24 fine bamboo rib lines
            ctx.strokeStyle = '#e11d48';
            ctx.lineWidth = 1.2 * scale;
            for (let i = 0; i < 24; i++) {
                const angle = (i * Math.PI * 2) / 24;
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(Math.cos(angle) * 95 * scale, Math.sin(angle) * 95 * scale);
                ctx.stroke();
            }
            // Concentric circle pattern
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
            ctx.lineWidth = 1 * scale;
            ctx.beginPath();
            ctx.arc(0, 0, 55 * scale, 0, Math.PI * 2);
            ctx.stroke();

            // Umbrella handle stick
            ctx.strokeStyle = '#27272a';
            ctx.lineWidth = 3.2 * scale;
            ctx.beginPath();
            ctx.moveTo(0, 25 * scale);
            ctx.lineTo(-45 * scale, 120 * scale); // tilted resting on shoulder
            ctx.stroke();
            ctx.restore();

            // --- 2. Elegant Dark Bun Hair (髪型・お団子) ---
            ctx.fillStyle = '#1c1917'; // rich dark hair
            
            // Left & Right buns
            ctx.beginPath();
            ctx.arc(-18 * scale, -125 * scale, 15 * scale, 0, Math.PI * 2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(18 * scale, -125 * scale, 15 * scale, 0, Math.PI * 2);
            ctx.fill();

            // Hair ribbons
            ctx.fillStyle = '#e11d48'; // red ribbon
            ctx.fillRect(-23 * scale, -118 * scale, 8 * scale, 4 * scale);
            ctx.fillRect(15 * scale, -118 * scale, 8 * scale, 4 * scale);

            // Flowing back hair strands
            ctx.fillStyle = '#1c1917';
            ctx.beginPath();
            ctx.moveTo(-25 * scale, -100 * scale);
            ctx.quadraticCurveTo(-38 * scale, -70 * scale + Math.sin(cycle)*4, -28 * scale, -40 * scale);
            ctx.lineTo(-18 * scale, -75 * scale);
            ctx.closePath();
            ctx.fill();

            // --- 3. Bare legs and Geta sandals (下半身・ゲタ) ---
            const leftLegSway = Math.sin(cycle) * 0.45;
            const rightLegSway = -Math.sin(cycle) * 0.45;

            // Left Leg (Back layer)
            ctx.save();
            ctx.translate(-7 * scale, -20 * scale);
            ctx.rotate(leftLegSway);
            ctx.fillStyle = '#ffe4e6'; // lovely skin
            ctx.fillRect(-4 * scale, 0, 8 * scale, 18 * scale);
            // Geta wooden sole
            ctx.fillStyle = '#7c2d12';
            ctx.fillRect(-5 * scale, 18 * scale, 11 * scale, 4 * scale);
            // Red strap
            ctx.fillStyle = '#ef4444';
            ctx.fillRect(-1 * scale, 16 * scale, 4 * scale, 3 * scale);
            ctx.restore();

            // Right Leg (Front layer)
            ctx.save();
            ctx.translate(7 * scale, -20 * scale);
            ctx.rotate(rightLegSway);
            ctx.fillStyle = '#ffe4e6';
            ctx.fillRect(-4 * scale, 0, 8 * scale, 18 * scale);
            // Geta
            ctx.fillStyle = '#7c2d12';
            ctx.fillRect(-5 * scale, 18 * scale, 11 * scale, 4 * scale);
            // Red strap
            ctx.fillStyle = '#ef4444';
            ctx.fillRect(-1 * scale, 16 * scale, 4 * scale, 3 * scale);
            ctx.restore();

            // --- 4. GORGEOUS GRADIENT YUKATA (淡いピンク〜水色グラデーション浴衣) ---
            ctx.save();
            
            // Draw yukata body silhouette
            const yukataGrad = ctx.createLinearGradient(0, -100 * scale, 0, -22 * scale);
            yukataGrad.addColorStop(0, '#fbcfe8'); // pale pink top
            yukataGrad.addColorStop(0.5, '#e0f2fe'); // ice blue middle
            yukataGrad.addColorStop(1, '#bae6fd'); // soft sky blue bottom

            ctx.fillStyle = yukataGrad;
            ctx.beginPath();
            ctx.moveTo(-16 * scale, -100 * scale);
            ctx.lineTo(16 * scale, -100 * scale);
            ctx.lineTo(24 * scale, -22 * scale);
            ctx.lineTo(-24 * scale, -22 * scale);
            ctx.closePath();
            ctx.fill();

            // --- Large red blossoms matching image_73dc06.png ---
            // Large red flower in center-bottom
            ctx.fillStyle = '#dc2626'; // vibrant red flower
            const drawFlower = (fx, fy, fr) => {
                ctx.beginPath();
                ctx.arc(fx, fy, fr, 0, Math.PI * 2);
                ctx.fill();
                // Outer petals details (using white/gold thin strokes)
                ctx.strokeStyle = '#fef08a'; // yellow-gold lines
                ctx.lineWidth = 0.8 * scale;
                for (let k = 0; k < 6; k++) {
                    const ang = (k * Math.PI) / 3;
                    ctx.beginPath();
                    ctx.moveTo(fx, fy);
                    ctx.lineTo(fx + Math.cos(ang) * fr * 1.2, fy + Math.sin(ang) * fr * 1.2);
                    ctx.stroke();
                }
            };

            // Draw beautiful large floral motifs from the image
            drawFlower(-8 * scale, -50 * scale, 11 * scale);
            drawFlower(10 * scale, -32 * scale, 8 * scale);
            
            // Small green leaves matching yukata motif
            ctx.fillStyle = '#15803d'; // green leaves
            ctx.beginPath();
            ctx.ellipse(-14 * scale, -42 * scale, 4 * scale, 2 * scale, Math.PI/4, 0, Math.PI * 2);
            ctx.ellipse(3 * scale, -38 * scale, 3 * scale, 1.5 * scale, -Math.PI/4, 0, Math.PI * 2);
            ctx.fill();

            ctx.restore();

            // --- 5. Rich Indigo-Black Obi with Golden thread (金和柄の帯) ---
            ctx.fillStyle = '#18181b'; // rich dark charcoal obi
            ctx.fillRect(-18 * scale, -70 * scale, 36 * scale, 15 * scale);
            
            // Gold details on Obi
            ctx.strokeStyle = '#ca8a04';
            ctx.lineWidth = 1.5 * scale;
            ctx.beginPath();
            ctx.moveTo(-18 * scale, -63 * scale);
            ctx.lineTo(18 * scale, -63 * scale);
            ctx.stroke();

            // Ribbon bow of obi in the back
            ctx.fillStyle = '#18181b';
            ctx.beginPath();
            ctx.ellipse(-15 * scale, -63 * scale, 6 * scale, 12 * scale, -Math.PI/6, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = '#ca8a04';
            ctx.stroke();

            // --- 6. Cute Head & Face with Warm Rim-light Glow ---
            ctx.fillStyle = '#ffe4e6'; // soft skin
            ctx.beginPath();
            ctx.arc(0, -110 * scale, 15 * scale, 0, Math.PI * 2);
            ctx.fill();

            // Hair fringe bangs (前髪)
            ctx.fillStyle = '#1c1917';
            ctx.beginPath();
            ctx.arc(0, -114 * scale, 15 * scale, Math.PI, 0);
            ctx.fill();
            
            // Side locks framing the face (Lighter left side to capture gorgeous sunset-matsuri rim light!)
            // Warm Golden Highlights (リムライト)
            ctx.fillStyle = '#fdba74'; // warm yellow/orange rim light
            ctx.fillRect(-16.5 * scale, -112 * scale, 4 * scale, 18 * scale);
            ctx.fillStyle = '#1c1917'; // dark hair overlap on right
            ctx.fillRect(-15 * scale, -112 * scale, 2 * scale, 18 * scale);
            ctx.fillRect(12.5 * scale, -112 * scale, 3.5 * scale, 18 * scale);

            // Blushing pink cheeks (ほっぺ)
            ctx.fillStyle = 'rgba(244, 63, 94, 0.45)';
            ctx.beginPath();
            ctx.arc(-8 * scale, -104 * scale, 3 * scale, 0, Math.PI * 2);
            ctx.arc(8 * scale, -104 * scale, 3 * scale, 0, Math.PI * 2);
            ctx.fill();

            // Cute happy smile (口)
            ctx.strokeStyle = '#991b1b';
            ctx.lineWidth = 1.2 * scale;
            ctx.beginPath();
            ctx.arc(0, -103 * scale, 3 * scale, 0.1, Math.PI - 0.1);
            ctx.stroke();

            // Blinking system
            const blinkCycle = tick % 4.0;
            const isBlinking = blinkCycle > 3.82; // close eyes briefly for 0.18s

            if (isBlinking) {
                // Closed eyes (cute arcs)
                ctx.strokeStyle = '#1c1917';
                ctx.lineWidth = 2 * scale;
                ctx.beginPath();
                ctx.arc(-7 * scale, -108 * scale, 3 * scale, Math.PI + 0.2, Math.PI * 2 - 0.2);
                ctx.arc(7 * scale, -108 * scale, 3 * scale, Math.PI + 0.2, Math.PI * 2 - 0.2);
                ctx.stroke();
            } else {
                // Sparkly anime eyes (purple & highlights)
                ctx.fillStyle = '#312e81'; // dark indigo
                ctx.beginPath();
                ctx.arc(-7 * scale, -108 * scale, 3.5 * scale, 0, Math.PI * 2);
                ctx.arc(7 * scale, -108 * scale, 3.5 * scale, 0, Math.PI * 2);
                ctx.fill();

                // Shiny white highlight reflections
                ctx.fillStyle = '#ffffff';
                ctx.beginPath();
                ctx.arc(-8.2 * scale, -109.5 * scale, 1.2 * scale, 0, Math.PI * 2);
                ctx.arc(5.8 * scale, -109.5 * scale, 1.2 * scale, 0, Math.PI * 2);
                ctx.fill();
            }

            // --- 7. EXQUISITE KITSUNE MASK TILTED ON HAIR (お狐様のお面) ---
            ctx.save();
            ctx.translate(14 * scale, -125 * scale); // tilted on upper right head
            ctx.rotate(0.25); // stylized angle

            // Mask Shadow
            ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
            ctx.shadowBlur = 6;

            // White base shape of Kitsune Mask
            ctx.fillStyle = '#fafafa';
            ctx.beginPath();
            ctx.moveTo(0, -12 * scale);
            ctx.bezierCurveTo(-10 * scale, -12 * scale, -14 * scale, -2 * scale, -14 * scale, 6 * scale);
            ctx.bezierCurveTo(-14 * scale, 14 * scale, -6 * scale, 24 * scale, 0, 24 * scale); // chin
            ctx.bezierCurveTo(6 * scale, 24 * scale, 14 * scale, 14 * scale, 14 * scale, 6 * scale);
            ctx.bezierCurveTo(14 * scale, -2 * scale, 10 * scale, -12 * scale, 0, -12 * scale);
            ctx.fill();
            ctx.shadowBlur = 0; // reset

            // Kitsune Ears (Left & Right)
            ctx.fillStyle = '#fafafa';
            ctx.beginPath();
            ctx.moveTo(-11 * scale, -3 * scale);
            ctx.lineTo(-12 * scale, -20 * scale); // pointy ear
            ctx.lineTo(-4 * scale, -9 * scale);
            ctx.closePath();
            ctx.fill();

            ctx.beginPath();
            ctx.moveTo(11 * scale, -3 * scale);
            ctx.lineTo(12 * scale, -20 * scale); // pointy ear
            ctx.lineTo(4 * scale, -9 * scale);
            ctx.closePath();
            ctx.fill();

            // 朱赤の耳飾りマーキング (Red inner ear markings)
            ctx.fillStyle = '#dc2626';
            ctx.beginPath();
            ctx.moveTo(-10.5 * scale, -6 * scale);
            ctx.lineTo(-11 * scale, -16 * scale);
            ctx.lineTo(-6 * scale, -9 * scale);
            ctx.closePath();
            ctx.fill();

            ctx.beginPath();
            ctx.moveTo(10.5 * scale, -6 * scale);
            ctx.lineTo(11 * scale, -16 * scale);
            ctx.lineTo(6 * scale, -9 * scale);
            ctx.closePath();
            ctx.fill();

            // 朱赤の狐ひげ・目元文様 (Red cheek swirl markings & closed eyes)
            ctx.strokeStyle = '#dc2626';
            ctx.lineWidth = 1.2 * scale;
            
            // Whisker curves
            ctx.beginPath();
            ctx.arc(-8 * scale, 12 * scale, 3 * scale, Math.PI, Math.PI * 1.5);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(8 * scale, 12 * scale, 3 * scale, Math.PI * 1.5, Math.PI * 2);
            ctx.stroke();

            // Kitsune elegant curved eyes (smiling closed arches)
            ctx.strokeStyle = '#111827'; // black eyes
            ctx.lineWidth = 1.5 * scale;
            ctx.beginPath();
            ctx.arc(-5 * scale, 3 * scale, 3 * scale, Math.PI + 0.2, Math.PI * 2 - 0.2);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(5 * scale, 3 * scale, 3 * scale, Math.PI + 0.2, Math.PI * 2 - 0.2);
            ctx.stroke();

            // Red eyebrow accents
            ctx.fillStyle = '#dc2626';
            ctx.beginPath();
            ctx.ellipse(-4 * scale, -1 * scale, 2 * scale, 0.8 * scale, 0.2, 0, Math.PI * 2);
            ctx.ellipse(4 * scale, -1 * scale, 2 * scale, 0.8 * scale, -0.2, 0, Math.PI * 2);
            ctx.fill();

            // Red nose dot
            ctx.fillRect(-1 * scale, 12 * scale, 2 * scale, 2 * scale);

            ctx.restore();

            // --- 8. Floral Kanzashi Hairpins & Tassels behind Mask ---
            ctx.save();
            ctx.translate(18 * scale, -112 * scale);
            
            // Flower Beads in hair (Red, blue, purple matching image_73dc06.png)
            const colors = ['#38bdf8', '#dc2626', '#a855f7']; // light blue, red, purple
            colors.forEach((col, idx) => {
                ctx.fillStyle = col;
                ctx.beginPath();
                ctx.arc((idx - 1) * 6 * scale, -4 * scale, 4 * scale, 0, Math.PI * 2);
                ctx.fill();
            });

            // Dangling golden tassels (揺れるかんざし飾り)
            ctx.save();
            const tasselSway = Math.sin(tick * 3.5) * 0.15;
            ctx.rotate(tasselSway);
            ctx.strokeStyle = '#facc15'; // golden tassels
            ctx.lineWidth = 1 * scale;
            ctx.beginPath();
            ctx.moveTo(0, 0);
            ctx.lineTo(-2 * scale, 18 * scale);
            ctx.moveTo(3 * scale, 0);
            ctx.lineTo(2 * scale, 14 * scale);
            ctx.stroke();
            ctx.restore();
            
            ctx.restore();

            // --- 9. Wide Kimono Sleeves (袂・たもと) ---
            // Left Sleeve
            ctx.save();
            ctx.translate(-15 * scale, -90 * scale);
            ctx.rotate(0.1 + 0.08 * Math.sin(cycle));
            ctx.fillStyle = '#fbcfe8'; // lavender/pink sleeve top
            ctx.beginPath();
            ctx.moveTo(0, 0);
            ctx.lineTo(-12 * scale, 15 * scale);
            ctx.lineTo(-12 * scale, 38 * scale);
            ctx.lineTo(2 * scale, 25 * scale);
            ctx.closePath();
            ctx.fill();
            ctx.restore();

            // Right Sleeve
            ctx.save();
            ctx.translate(15 * scale, -90 * scale);
            ctx.rotate(-0.1 - 0.08 * Math.sin(cycle));
            ctx.fillStyle = '#bae6fd'; // sky blue sleeve
            ctx.beginPath();
            ctx.moveTo(0, 0);
            ctx.lineTo(12 * scale, 15 * scale);
            ctx.lineTo(12 * scale, 38 * scale);
            ctx.lineTo(-2 * scale, 25 * scale);
            ctx.closePath();
            ctx.fill();
            ctx.restore();

            ctx.restore();
        }

        // Background painter rendering parallax tree rows, beautiful glowing lanterns and night moon
        class Lantern {
            constructor(x, y, scale) {
                this.x = x;
                this.y = y;
                this.scale = scale;
                this.phase = Math.random() * Math.PI * 2;
            }

            draw(ctx, tick) {
                const wave = Math.sin(tick * 2 + this.phase);
                const sway = Math.sin(tick * 0.8 + this.phase) * 3;
                const sizeW = 16 * this.scale;
                const sizeH = 26 * this.scale;

                ctx.save();
                ctx.translate(this.x + sway, this.y);

                // Glow shadow
                ctx.shadowColor = 'rgba(251, 146, 60, 0.8)'; // bright orange glow
                ctx.shadowBlur = 15 + wave * 5;

                // Lantern gradient (Warm paper glow)
                const lGrad = ctx.createLinearGradient(0, -sizeH/2, 0, sizeH/2);
                lGrad.addColorStop(0, '#ffedd5'); // white core
                lGrad.addColorStop(0.3, '#fed7aa'); // bright yellow-orange
                lGrad.addColorStop(0.8, '#ea580c'); // orange rim
                lGrad.addColorStop(1, '#9a3412'); // dark base

                ctx.fillStyle = lGrad;
                // Rounded paper lantern shape
                ctx.beginPath();
                ctx.moveTo(-sizeW/2, -sizeH/2 + 4);
                ctx.quadraticCurveTo(-sizeW/2 - 4, 0, -sizeW/2, sizeH/2 - 4);
                ctx.lineTo(sizeW/2, sizeH/2 - 4);
                ctx.quadraticCurveTo(sizeW/2 + 4, 0, sizeW/2, -sizeH/2 + 4);
                ctx.closePath();
                ctx.fill();

                ctx.shadowBlur = 0; // reset shadow

                // Black top/bottom wood caps
                ctx.fillStyle = '#1e293b';
                ctx.fillRect(-sizeW/2 + 2, -sizeH/2 - 2, sizeW - 4, 3);
                ctx.fillRect(-sizeW/2 + 2, sizeH/2 - 1, sizeW - 4, 3);

                // Hanging wire
                ctx.strokeStyle = '#1e293b';
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(0, -sizeH/2 - 2);
                ctx.lineTo(0, -sizeH/2 - 15);
                ctx.stroke();

                // Kanji character inside lantern ("桜" or traditional symbol)
                ctx.fillStyle = 'rgba(15, 23, 42, 0.65)';
                ctx.font = `bold ${10 * this.scale}px sans-serif`;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText('桜', 0, 0);

                ctx.restore();
            }
        }

        const backgroundLanterns = [];
        // Spawn glowing lanterns hanging at various heights
        for (let i = 0; i < 12; i++) {
            backgroundLanterns.push(new Lantern(60 + i * 110, 80 + Math.sin(i) * 20, 0.8 + Math.random() * 0.4));
        }

        function drawScenery(tick) {
            const w = sceneryCanvas.clientWidth;
            const h = sceneryCanvas.clientHeight;
            if (!w || !h) return;

            sCtx.clearRect(0, 0, w, h);

            // 1. NIGHT FESTIVAL SKY GRADIENT (深淵な夜祭り紺紫グラデーション)
            const skyGrad = sCtx.createLinearGradient(0, 0, 0, h);
            skyGrad.addColorStop(0, '#030008'); // pitch black sky
            skyGrad.addColorStop(0.3, '#0c0517'); // indigo night
            skyGrad.addColorStop(0.65, '#2e1035'); // twilight purple
            skyGrad.addColorStop(0.88, '#5b0e2d'); // lantern warm horizon glow
            skyGrad.addColorStop(1, '#1e0513'); // ground base dark
            sCtx.fillStyle = skyGrad;
            sCtx.fillRect(0, 0, w, h);

            // Large, fuzzy ambient light from the distant festival lanterns
            const ambientGrad = sCtx.createRadialGradient(w * 0.5, h * 0.5, 5, w * 0.5, h * 0.5, w * 0.6);
            ambientGrad.addColorStop(0, 'rgba(251, 146, 60, 0.08)'); // warm amber ambient
            ambientGrad.addColorStop(1, 'rgba(0,0,0,0)');
            sCtx.fillStyle = ambientGrad;
            sCtx.beginPath();
            sCtx.arc(w * 0.5, h * 0.5, w * 0.6, 0, Math.PI * 2);
            sCtx.fill();

            // 2. Far Shrine / Torii Gate Silhouettes in background
            sCtx.fillStyle = 'rgba(15, 4, 18, 0.8)';
            // Torii Gate drawing
            const toriiX = w * 0.72;
            const toriiY = h * 0.72;
            const tw = 80;
            const th = 90;
            sCtx.beginPath();
            // Pillars
            sCtx.rect(toriiX - tw/2, toriiY - th, 8, th);
            sCtx.rect(toriiX + tw/2 - 8, toriiY - th, 8, th);
            // Top beam
            sCtx.rect(toriiX - tw/2 - 12, toriiY - th - 5, tw + 24, 8);
            sCtx.rect(toriiX - tw/2 - 4, toriiY - th + 8, tw + 8, 6);
            sCtx.fill();

            // 3. Ground Walkway Base (桜色に照らされた石畳)
            const groundY = h * 0.72;
            const groundGrad = sCtx.createLinearGradient(0, groundY, 0, h);
            groundGrad.addColorStop(0, '#09030c');
            groundGrad.addColorStop(1, '#030105');
            sCtx.fillStyle = groundGrad;
            sCtx.fillRect(0, groundY, w, h - groundY);

            // 4. Update and Draw background trees (Avenue of cherry blossoms)
            backgroundTrees.forEach(tree => {
                if (gamePlaying) {
                    tree.x -= speed;
                }
                if (tree.x < -200) {
                    tree.x = w + 150 + Math.random() * 150;
                }
                tree.draw(sCtx, w, h);
            });

            // 5. Update & draw gorgeous glowing festival lanterns
            backgroundLanterns.forEach(lantern => {
                if (gamePlaying) {
                    lantern.x -= speed;
                }
                if (lantern.x < -100) {
                    lantern.x = w + 100;
                }
                lantern.draw(sCtx, tick);
            });

            // 6. Update and render drifting soft petals
            petalsList.forEach(petal => {
                if (gamePlaying) {
                    petal.update(w, h);
                }
                petal.draw(sCtx);
            });

            // 7. Cute standing/walking animated Yukata girl with Kitsune mask
            const girlX = w * 0.16;
            const girlScale = Math.min(w, h) / 360; // perfect responsive scale
            drawCuteAnimeGirl(sCtx, girlX, groundY, girlScale, tick);
        }

        // Tap hit explosion particles array
        const hitExplosions = [];

        class HitParticle {
            constructor(x, y, color) {
                this.x = x;
                this.y = y;
                this.color = color;
                this.vx = (Math.random() - 0.5) * 10;
                this.vy = (Math.random() - 0.5) * 10 - 4; // drift upward
                this.alpha = 1.0;
                this.size = Math.random() * 7 + 3;
                this.decay = Math.random() * 0.04 + 0.02;
            }
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += 0.12; // mild gravity
                this.alpha -= this.decay;
            }
            draw(ctx) {
                ctx.save();
                ctx.globalAlpha = this.alpha;
                ctx.shadowColor = this.color;
                ctx.shadowBlur = 12;
                ctx.fillStyle = this.color;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
                ctx.restore();
            }
        }

        // Triggers glowing splash particles at the target circles
        function triggerSplashParticles(lane) {
            const w = gameCanvas.clientWidth;
            const h = gameCanvas.clientHeight;
            
            // Layout calculations (guarantees 100% exact alignment with lanes)
            const centerX = w * 0.55; // aligned to match 3D center
            const bottomWidth = w;    // spans across the full width
            const bottomLeftX = 0;
            const bottomLaneWidth = bottomWidth / 4;
            
            const hitX = bottomLeftX + (lane + 0.5) * bottomLaneWidth;
            const hitY = h * JUDGMENT_LINE_Y_RATIO;
            
            const colors = ['#f43f5e', '#fb923c', '#fed7aa', '#fff']; // pink/orange/warm colors
            for (let i = 0; i < 18; i++) {
                const color = colors[Math.floor(Math.random() * colors.length)];
                hitExplosions.push(new HitParticle(hitX, hitY, color));
            }
        }

        // Custom Lane Activation (Flashes on press)
        const laneFlashTimer = [0, 0, 0, 0];

        function drawGameBoard() {
            const w = gameCanvas.clientWidth;
            const h = gameCanvas.clientHeight;
            if (!w || !h) return;

            gCtx.clearRect(0, 0, w, h);

            // 3D Perspective Projection Anchor Configurations (Exactly like reference image)
            const horizonX = w * 0.55; // Slightly offset right to give girl on the left room
            const horizonY = h * 0.28; // high up vanishing point
            const horizonWidth = 20;   // very narrow top
            
            const bottomWidth = w;     // wide bottom matching real tactile keys
            const bottomLeftX = 0;
            const bottomLaneWidth = bottomWidth / 4;

            const judgmentLineY = h * JUDGMENT_LINE_Y_RATIO;

            // 1. Draw 3D perspective board grid lanes (収束する桜路の3D枠線)
            for (let i = 0; i <= 4; i++) {
                const startX = horizonX + (i - 2) * (horizonWidth / 4);
                const endX = bottomLeftX + i * bottomLaneWidth;

                // Glowing neon path boundary
                gCtx.strokeStyle = 'rgba(251, 146, 60, 0.35)'; // warm orange border lines
                gCtx.lineWidth = i === 0 || i === 4 ? 3 : 1.2;
                gCtx.shadowColor = 'rgba(251, 146, 60, 0.5)';
                gCtx.shadowBlur = 8;
                gCtx.beginPath();
                gCtx.moveTo(startX, horizonY);
                gCtx.lineTo(endX, h);
                gCtx.stroke();
                gCtx.shadowBlur = 0; // reset
            }

            // Draw scrolling horizontal stone road markers
            const scrollTimeVal = isAudioFallbackMode ? (elapsedTime / 1000) : audio.currentTime;
            const scrollSpeed = (scrollTimeVal * 2) % 1.0;
            for (let j = 0; j < 8; j++) {
                const p = (j + scrollSpeed) / 8;
                const easedP = Math.pow(p, 2.2);
                const lineY = horizonY + easedP * (h - horizonY);
                
                const widthAtY = horizonWidth * (1 - easedP) + bottomWidth * easedP;
                const leftAtY = horizonX * (1 - easedP) + bottomLeftX * easedP - widthAtY / 2;

                gCtx.strokeStyle = 'rgba(251, 146, 60, 0.08)';
                gCtx.lineWidth = 2 * easedP;
                gCtx.beginPath();
                gCtx.moveTo(horizonX - widthAtY/2, lineY);
                gCtx.lineTo(horizonX + widthAtY/2, lineY);
                gCtx.stroke();
            }

            // 2. Draw Active Lane Glowing overlay flasher
            for (let i = 0; i < 4; i++) {
                if (laneFlashTimer[i] > 0) {
                    gCtx.fillStyle = `rgba(251, 146, 60, ${0.18 * laneFlashTimer[i]})`;
                    gCtx.beginPath();
                    // build 3D trapezoid
                    const tLeftTop = horizonX + (i - 2) * (horizonWidth / 4);
                    const tRightTop = horizonX + (i - 1) * (horizonWidth / 4);
                    const bLeftBot = bottomLeftX + i * bottomLaneWidth;
                    const bRightBot = bottomLeftX + (i + 1) * bottomLaneWidth;

                    gCtx.moveTo(tLeftTop, horizonY);
                    gCtx.lineTo(tRightTop, horizonY);
                    gCtx.lineTo(bRightBot, h);
                    gCtx.lineTo(bLeftBot, h);
                    gCtx.closePath();
                    gCtx.fill();

                    laneFlashTimer[i] -= 0.08; // fade out fast
                }
            }

            // 3. Draw Horizontal Neon Judgment Bar (お祭りの熱気ネオン判定線)
            gCtx.save();
            gCtx.strokeStyle = '#f43f5e';
            gCtx.shadowColor = '#fdba74';
            gCtx.shadowBlur = 15;
            gCtx.lineWidth = 5;
            gCtx.beginPath();
            gCtx.moveTo(0, judgmentLineY);
            gCtx.lineTo(w, judgmentLineY);
            gCtx.stroke();
            gCtx.restore();

            // 4. Draw Glow targets (D, F, J, K target circles matching exact lanes bottom center)
            for (let i = 0; i < 4; i++) {
                const targetX = bottomLeftX + (i + 0.5) * bottomLaneWidth;

                gCtx.save();
                gCtx.shadowColor = 'rgba(244, 63, 94, 0.85)';
                gCtx.shadowBlur = 12;
                gCtx.fillStyle = 'rgba(12, 3, 16, 0.85)';
                gCtx.strokeStyle = '#fff';
                gCtx.lineWidth = 3;
                
                gCtx.beginPath();
                gCtx.arc(targetX, judgmentLineY, 26, 0, Math.PI * 2);
                gCtx.fill();
                gCtx.stroke();

                // Inner white neon ring guide
                gCtx.strokeStyle = 'rgba(251, 146, 60, 0.8)';
                gCtx.lineWidth = 2;
                gCtx.beginPath();
                gCtx.arc(targetX, judgmentLineY, 20, 0, Math.PI * 2);
                gCtx.stroke();

                // Key Hint in circle
                gCtx.fillStyle = '#fff';
                gCtx.shadowBlur = 0;
                gCtx.font = 'bold 15px "Murecho", sans-serif';
                gCtx.textAlign = 'center';
                gCtx.textBaseline = 'middle';
                gCtx.fillText(KEY_LABELS[i], targetX, judgmentLineY);
                gCtx.restore();
            }

            // 5. Draw Perspective Falling Square Notes with Acceleration
            const currentTime = isAudioFallbackMode ? (elapsedTime / 1000) : audio.currentTime;

            for (let i = 0; i < notes.length; i++) {
                const note = notes[i];
                if (note.hit) continue;

                const timeDiff = note.time - currentTime;

                // Auto MISS detection
                if (timeDiff < -0.22) {
                    note.hit = true;
                    showFeedback("MISS", "text-rose-500");
                    combo = 0;
                    countMiss++;
                    updateHUD();
                    continue;
                }

                // Spawning filter based on timeline speed limit
                if (timeDiff > NOTE_SPEED) continue;

                const linearProgress = 1.0 - (timeDiff / NOTE_SPEED);
                const easedProgress = Math.pow(linearProgress, 2.2);

                const noteY = horizonY + easedProgress * (judgmentLineY - horizonY);
                
                const topX_i = horizonX + (note.lane - 1.5) * (horizonWidth / 4);
                const bottomX_i = bottomLeftX + (note.lane + 0.5) * bottomLaneWidth;
                const noteX = topX_i * (1 - easedProgress) + bottomX_i * easedProgress;

                // Size scaling (grows larger as it drops)
                const noteSize = 12 + easedProgress * 36;

                // Render note (Cute pink glowing frame with custom key text)
                gCtx.save();
                gCtx.shadowColor = '#fd923c';
                gCtx.shadowBlur = 12;
                gCtx.fillStyle = 'rgba(244, 63, 94, 0.95)'; // bright red-pink core
                gCtx.strokeStyle = '#fff';
                gCtx.lineWidth = 2.5;

                // Rounded Rect for note square
                const rx = noteX - noteSize / 2;
                const ry = noteY - noteSize / 2;
                const rad = Math.max(3, noteSize * 0.18);
                
                gCtx.beginPath();
                gCtx.moveTo(rx + rad, ry);
                gCtx.lineTo(rx + noteSize - rad, ry);
                gCtx.quadraticCurveTo(rx + noteSize, ry, rx + noteSize, ry + rad);
                gCtx.lineTo(rx + noteSize, ry + noteSize - rad);
                gCtx.quadraticCurveTo(rx + noteSize, ry + noteSize, rx + noteSize - rad, ry + noteSize);
                gCtx.lineTo(rx + rad, ry + noteSize);
                gCtx.quadraticCurveTo(rx, ry + noteSize, rx, ry + noteSize - rad);
                gCtx.lineTo(rx, ry + rad);
                gCtx.quadraticCurveTo(rx, ry, rx + rad, ry);
                gCtx.closePath();
                gCtx.fill();
                gCtx.stroke();

                // Key tag guide inside notes
                gCtx.shadowBlur = 0;
                gCtx.fillStyle = '#fff';
                gCtx.font = `bold ${Math.max(10, noteSize * 0.45)}px sans-serif`;
                gCtx.textAlign = 'center';
                gCtx.textBaseline = 'middle';
                gCtx.fillText(KEY_LABELS[note.lane], noteX, noteY);
                gCtx.restore();
            }

            // 6. Update and Render floating splash particle impacts
            for (let i = hitExplosions.length - 1; i >= 0; i--) {
                const p = hitExplosions[i];
                p.update();
                if (p.alpha <= 0) {
                    hitExplosions.splice(i, 1);
                } else {
                    p.draw(gCtx);
                }
            }
        }

        // Judgment Feedback overlays
        let feedbackTimeout;
        function showFeedback(text, colorClass) {
            hudJudgment.textContent = text;
            hudJudgment.className = `text-3xl font-black tracking-widest ${colorClass} transition-all transform scale-125 duration-75`;
            
            setTimeout(() => {
                hudJudgment.className = `text-3xl font-black tracking-widest ${colorClass} transition-all transform scale-100 duration-100`;
            }, 80);

            clearTimeout(feedbackTimeout);
            feedbackTimeout = setTimeout(() => {
                hudJudgment.className = "text-3xl font-black tracking-widest opacity-0 transform translate-y-2 h-10";
            }, 800);
        }

        // Update statistics HUD panel
        function updateHUD() {
            const formattedScore = String(score).padStart(7, '0');
            statScore.textContent = formattedScore;

            // Accuracy Calculation
            let acc = 100;
            if (totalHitsAttempted > 0) {
                const maxPossiblePoints = totalHitsAttempted * 300;
                const currentEarnedPoints = (countPerfect * 300) + (countGreat * 200) + (countGood * 100);
                acc = (currentEarnedPoints / maxPossiblePoints) * 100;
            }
            statAccuracy.textContent = acc.toFixed(1) + '%';

            // Combo display
            if (combo > 0) {
                hudCombo.textContent = combo;
                hudCombo.classList.remove('hidden');
                hudCombo.classList.remove('scale-100');
                hudCombo.classList.add('scale-115');
                setTimeout(() => {
                    hudCombo.classList.remove('scale-115');
                    hudCombo.classList.add('scale-100');
                }, 100);
            } else {
                hudCombo.classList.add('hidden');
            }

            if (combo > maxCombo) {
                maxCombo = combo;
            }
            statMaxCombo.textContent = maxCombo;
        }

        // Handles Tapping logic on keyboard presses & mouse/screen taps
        function handleLanePress(lane) {
            if (!gamePlaying) return;
            initSynthAudio();
            
            const currentTime = isAudioFallbackMode ? (elapsedTime / 1000) : audio.currentTime;
            
            let closestNote = null;
            let minDiff = Infinity;

            for (let i = 0; i < notes.length; i++) {
                const note = notes[i];
                if (note.lane === lane && !note.hit) {
                    const diff = Math.abs(note.time - currentTime);
                    if (diff < minDiff) {
                        minDiff = diff;
                        closestNote = note;
                    }
                }
            }

            playTapSound(440 + lane * 100, 'sine', 0.08);

            laneFlashTimer[lane] = 1.0;

            if (closestNote) {
                const diff = Math.abs(closestNote.time - currentTime);

                if (diff < 0.25) {
                    closestNote.hit = true;
                    totalHitsAttempted++;
                    triggerSplashParticles(lane);

                    if (diff <= 0.05) {
                        // PERFECT
                        score += 300;
                        combo++;
                        countPerfect++;
                        showFeedback("PERFECT", "text-pink-300");
                    } else if (diff <= 0.11) {
                        // GREAT
                        score += 200;
                        combo++;
                        countGreat++;
                        showFeedback("GREAT", "text-cyan-400");
                    } else if (diff <= 0.18) {
                        // GOOD
                        score += 100;
                        combo++;
                        countGood++;
                        showFeedback("GOOD", "text-amber-400");
                    } else {
                        combo = 0;
                        countMiss++;
                        showFeedback("MISS", "text-rose-500");
                    }
                    updateHUD();
                }
            }
        }

        // Tap Button Glow triggers (mouse & mobile friendly)
        function triggerTapVisual(lane) {
            handleLanePress(lane);
            const btn = document.getElementById(`tap-btn-${lane}`);
            if (btn) {
                btn.classList.add('bg-pink-500/40', 'border-pink-300', 'scale-95');
            }
        }

        function releaseTapVisual(lane) {
            const btn = document.getElementById(`tap-btn-${lane}`);
            if (btn) {
                btn.classList.remove('bg-pink-500/40', 'border-pink-300', 'scale-95');
            }
        }

        // Key Down handlers mapping D/F/J/K keys
        window.addEventListener('keydown', (e) => {
            if (e.repeat) return;
            const key = e.key.toLowerCase();
            
            if (key === 'd') {
                triggerTapVisual(0);
            } else if (key === 'f') {
                triggerTapVisual(1);
            } else if (key === 'j') {
                triggerTapVisual(2);
            } else if (key === 'k') {
                triggerTapVisual(3);
            }
        });

        window.addEventListener('keyup', (e) => {
            const key = e.key.toLowerCase();
            if (key === 'd') {
                releaseTapVisual(0);
            } else if (key === 'f') {
                releaseTapVisual(1);
            } else if (key === 'j') {
                releaseTapVisual(2);
            } else if (key === 'k') {
                releaseTapVisual(3);
            }
        });

        let tickCounter = 0;
        let animationFrameId;

        function gameLoop() {
            tickCounter += 0.05;

            if (gamePlaying) {
                elapsedTime = performance.now() - gameStartTime;
            }

            drawScenery(tickCounter);
            drawGameBoard();

            const isSongFinished = isAudioFallbackMode ? (elapsedTime > 120 * 1000) : audio.ended;
            if (isSongFinished && gamePlaying) {
                endGame();
            }

            animationFrameId = requestAnimationFrame(gameLoop);
        }

        function startGame() {
            initSynthAudio();
            
            if (isAudioFallbackMode) {
                console.log("Playing game in Synthetic BGM mode.");
                startSynthBgm();
            } else {
                audio.play().then(() => {
                    console.log("Main music track started playing successfully!");
                }).catch(err => {
                    console.warn("Main audio play failed, switching to backup synthetic sound immediately.", err);
                    isAudioFallbackMode = true;
                    startSynthBgm();
                });
            }

            score = 0;
            combo = 0;
            maxCombo = 0;
            totalHitsAttempted = 0;
            countPerfect = 0;
            countGreat = 0;
            countGood = 0;
            countMiss = 0;
            
            generateBeatMap();
            notes = JSON.parse(JSON.stringify(beatMap));

            gameStartTime = performance.now();
            elapsedTime = 0;
            gamePlaying = true;
            updateHUD();

            startScreen.classList.add('opacity-0', 'pointer-events-none');
            resultsScreen.classList.add('hidden');
        }

        function endGame() {
            gamePlaying = false;
            stopSynthBgm();
            if (!isAudioFallbackMode) {
                audio.pause();
                audio.currentTime = 0;
            }

            let acc = 100;
            if (totalHitsAttempted > 0) {
                const maxPossiblePoints = totalHitsAttempted * 300;
                const currentEarnedPoints = (countPerfect * 300) + (countGreat * 200) + (countGood * 100);
                acc = (currentEarnedPoints / maxPossiblePoints) * 100;
            }

            let rank = 'D';
            if (acc >= 95) rank = 'S';
            else if (acc >= 85) rank = 'A';
            else if (acc >= 75) rank = 'B';
            else if (acc >= 60) rank = 'C';

            document.getElementById('result-rank').textContent = rank;
            document.getElementById('res-score').textContent = score.toLocaleString();
            document.getElementById('res-acc').textContent = acc.toFixed(2) + '%';
            document.getElementById('res-maxcombo').textContent = maxCombo;
            
            document.getElementById('res-perf').textContent = countPerfect;
            document.getElementById('res-great').textContent = countGreat;
            document.getElementById('res-good').textContent = countGood;
            document.getElementById('res-miss').textContent = countMiss;

            resultsScreen.classList.remove('hidden');
        }

        // Connect button listeners
        startBtn.addEventListener('click', startGame);
        
        restartBtn.addEventListener('click', () => {
            endGame();
            startGame();
        });

        resRetryBtn.addEventListener('click', () => {
            resultsScreen.classList.add('hidden');
            startGame();
        });

        resCloseBtn.addEventListener('click', () => {
            resultsScreen.classList.add('hidden');
            startScreen.classList.remove('opacity-0', 'pointer-events-none');
        });

        // Initialize animation loop instantly
        window.onload = function() {
            gameLoop();
        };

        // Pre-create floating sakura overlay petals
        const htmlPetalsContainer = document.getElementById('html-petals-container');
        for (let j = 0; j < 15; j++) {
            const hPetal = document.createElement('div');
            hPetal.className = 'absolute bg-pink-300 rounded-full opacity-45 pointer-events-none transition-transform';
            hPetal.style.width = (Math.random() * 8 + 6) + 'px';
            hPetal.style.height = (Math.random() * 6 + 4) + 'px';
            hPetal.style.left = Math.random() * 100 + 'vw';
            hPetal.style.top = Math.random() * 100 + 'vh';
            
            hPetal.style.animation = `pulseGlow ${Math.random() * 4 + 2}s infinite alternate`;
            htmlPetalsContainer.appendChild(hPetal);
        }
    </script>
</body>
</html>

音ゲーNO

by tag

たまに違うのが飛び出してきます。気にしないでください。

👁 30 回閲覧
📅 投稿:2026年7月4日 🔄 更新:2026年8月13日
応援スタンプを押してみよう!