🔬 AI発明ギャラリー kizu スペシャルレースゲーム
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
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>3D カートレーサー - グランプリ拡張版</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: system-ui, -apple-system, sans-serif; user-select: none; }
        #ui-layer {
            position: absolute;
            top: 20px;
            left: 20px;
            right: 20px;
            color: white;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
            pointer-events: none;
            z-index: 10;
            display: none;
            justify-content: space-between;
        }
        .controls {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 15px;
            pointer-events: auto;
            z-index: 20;
            display: none;
        }
        .btn {
            width: 60px;
            height: 60px;
            background: rgba(255,255,255,0.25);
            border: 2px solid rgba(255,255,255,0.8);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 22px;
            font-weight: bold;
            touch-action: manipulation;
            backdrop-filter: blur(4px);
        }
        .btn:active { background: rgba(255,255,255,0.5); }
        #select-screen, #goal-screen {
            position: absolute;
            inset: 0;
            background: radial-gradient(circle at center, #1e293b, #090d16);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            color: white;
            z-index: 100;
            overflow-y: auto;
            padding: 20px 12px 40px;
        }
        #goal-screen { display: none; background: rgba(0,0,0,0.88); z-index: 110; justify-content: center; }
        .selection-card {
            background: rgba(255,255,255,0.08);
            padding: 10px 8px;
            border-radius: 12px;
            border: 2px solid rgba(255,255,255,0.15);
            transition: all 0.2s ease;
            cursor: pointer;
            text-align: center;
            position: relative;
        }
        .selection-card:hover {
            background: rgba(255,255,255,0.15);
        }
        .selection-card.active {
            border-color: #facc15;
            background: rgba(250, 204, 21, 0.22);
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(250, 204, 21, 0.35);
        }
        .char-icon {
            width: 40px;
            height: 40px;
            margin: 0 auto 6px;
            border-radius: 50%;
        }
        .cup-tab {
            padding: 6px 14px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: bold;
            background: rgba(255,255,255,0.1);
            border: 1px solid rgba(255,255,255,0.2);
            cursor: pointer;
            transition: all 0.2s;
            white-space: nowrap;
        }
        .cup-tab.active {
            background: #facc15;
            color: #000;
            border-color: #facc15;
            box-shadow: 0 0 10px rgba(250,204,21,0.5);
        }
        /* スクロールバー装飾 */
        #select-screen::-webkit-scrollbar { width: 6px; }
        #select-screen::-webkit-scrollbar-thumb { background: rgba(250,204,21,0.5); border-radius: 3px; }
    </style>
</head>
<body>

    <!-- 選択画面 -->
    <div id="select-screen">
        <h1 class="text-3xl md:text-5xl font-black mb-3 text-yellow-400 italic tracking-tighter text-center filter drop-shadow-md">
            3D マリオカート風<br><span class="text-white text-2xl md:text-4xl">グランプリ</span>
        </h1>

        <!-- 3D リアルタイム プレビュー表示エリア -->
        <div class="w-full flex justify-center mb-4 sticky top-1 z-30">
            <div id="preview-container" class="w-full max-w-md h-40 sm:h-48 bg-slate-950/90 rounded-2xl overflow-hidden border-2 border-yellow-400/80 shadow-[0_0_25px_rgba(250,204,21,0.4)] relative flex items-center justify-center cursor-grab active:cursor-grabbing backdrop-blur-md">
                <canvas id="preview-canvas" class="w-full h-full block"></canvas>
                <div class="absolute top-2 left-2.5 text-[11px] text-yellow-300 font-bold flex items-center gap-1 bg-black/70 px-2 py-0.5 rounded-lg border border-yellow-400/30 backdrop-blur">
                    <span>🏎️ リアルタイム 3D プレビュー</span>
                    <span class="text-gray-300 text-[9px] font-normal">(ドラッグで360度回転)</span>
                </div>
                <div id="preview-track-badge" class="absolute bottom-2 left-2.5 text-[10px] text-cyan-300 font-bold bg-black/70 px-2 py-0.5 rounded border border-cyan-400/30">
                    コース: シティ・サークル
                </div>
            </div>
        </div>
        
        <div class="flex flex-col gap-5 w-full max-w-4xl px-1">
            <!-- コース選択(カップカテゴリ分け&詳細バッジ付) -->
            <div class="bg-slate-900/60 p-3 rounded-2xl border border-white/10 backdrop-blur">
                <div class="flex items-center justify-between mb-2">
                    <p class="text-md md:text-lg font-bold text-yellow-300 flex items-center gap-1">
                        🏁 コースを選択 <span class="text-xs text-gray-400 font-normal">(全10コース)</span>
                    </p>
                </div>

                <!-- カップ切り替えタブ -->
                <div class="flex gap-1.5 mb-3 overflow-x-auto pb-1" id="cup-tabs">
                    <button class="cup-tab active" data-cup="all">🌟 全コース</button>
                    <button class="cup-tab" data-cup="mushroom">🍄 キノコ杯</button>
                    <button class="cup-tab" data-cup="flower">🌺 フラワー杯</button>
                    <button class="cup-tab" data-cup="star">⚡ スター杯</button>
                    <button class="cup-tab" data-cup="special">🌈 スペシャル杯</button>
                </div>

                <!-- コースカードグリッド -->
                <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2" id="track-options">
                    <!-- キノコカップ -->
                    <div class="selection-card active" data-track="circle" data-cup="mushroom">
                        <span class="absolute top-1 right-1 text-[9px] bg-green-500/80 text-white px-1.0 rounded font-bold">★☆☆</span>
                        <div class="text-xl mb-0.5">🏙️</div>
                        <p class="font-bold text-xs md:text-sm">シティ・サークル</p>
                        <p class="text-[9px] text-gray-300">標準オーバル</p>
                    </div>
                    <div class="selection-card" data-track="eight" data-cup="mushroom">
                        <span class="absolute top-1 right-1 text-[9px] bg-green-500/80 text-white px-1.0 rounded font-bold">★☆☆</span>
                        <div class="text-xl mb-0.5">🏜️</div>
                        <p class="font-bold text-xs md:text-sm">デザート・エイト</p>
                        <p class="text-[9px] text-gray-300">砂漠の8の字</p>
                    </div>
                    <div class="selection-card" data-track="jungle" data-cup="mushroom">
                        <span class="absolute top-1 right-1 text-[9px] bg-green-500/80 text-white px-1.0 rounded font-bold">★★☆</span>
                        <div class="text-xl mb-0.5">🌳</div>
                        <p class="font-bold text-xs md:text-sm">グリーン・ジャングル</p>
                        <p class="text-[9px] text-gray-300">森林ツイスター</p>
                    </div>

                    <!-- フラワーカップ -->
                    <div class="selection-card" data-track="ocean" data-cup="flower">
                        <span class="absolute top-1 right-1 text-[9px] bg-blue-500/80 text-white px-1.0 rounded font-bold">★★☆</span>
                        <div class="text-xl mb-0.5">🏝️</div>
                        <p class="font-bold text-xs md:text-sm">オーシャン・リゾート</p>
                        <p class="text-[9px] text-blue-300">海に落下注意!</p>
                    </div>
                    <div class="selection-card" data-track="sunset" data-cup="flower">
                        <span class="absolute top-1 right-1 text-[9px] bg-blue-500/80 text-white px-1.0 rounded font-bold">★★☆</span>
                        <div class="text-xl mb-0.5">🌅</div>
                        <p class="font-bold text-xs md:text-sm">サンセット・ビーチ</p>

                        <p class="text-[9px] text-amber-300">夕暮れのアーチ</p>
                    </div>
                    <div class="selection-card" data-track="snow" data-cup="flower">
                        <span class="absolute top-1 right-1 text-[9px] bg-blue-500/80 text-white px-1.0 rounded font-bold">★★☆</span>
                        <div class="text-xl mb-0.5">❄️</div>
                        <p class="font-bold text-xs md:text-sm">スノー・クライシス</p>
                        <p class="text-[9px] text-cyan-200">氷上の滑走</p>
                    </div>

                    <!-- スターカップ -->
                    <div class="selection-card" data-track="castle" data-cup="star">
                        <span class="absolute top-1 right-1 text-[9px] bg-red-500/80 text-white px-1.0 rounded font-bold">★★★</span>
                        <div class="text-xl mb-0.5">🏰</div>
                        <p class="font-bold text-xs md:text-sm">キャッスル・サーキット</p>
                        <p class="text-[9px] text-red-300">溶岩城塞クランク</p>
                    </div>
                    <div class="selection-card" data-track="volcano" data-cup="star">
                        <span class="absolute top-1 right-1 text-[9px] bg-red-500/80 text-white px-1.0 rounded font-bold">★★★</span>
                        <div class="text-xl mb-0.5">🌋</div>
                        <p class="font-bold text-xs md:text-sm">マグマ・コア</p>
                        <p class="text-[9px] text-orange-300">跳ね上がる波状路</p>
                    </div>

                    <!-- スペシャルカップ -->
                    <div class="selection-card" data-track="rainbow" data-cup="special">
                        <span class="absolute top-1 right-1 text-[9px] bg-purple-500/80 text-white px-1.0 rounded font-bold">EX</span>
                        <div class="text-xl mb-0.5">🌈</div>
                        <p class="font-bold text-xs md:text-sm">レインボー・ロード</p>
                        <p class="text-[9px] text-pink-300">宇宙の無ガード</p>
                    </div>
                    <div class="selection-card" data-track="cyber" data-cup="special">
                        <span class="absolute top-1 right-1 text-[9px] bg-purple-500/80 text-white px-1.0 rounded font-bold">EX</span>
                        <div class="text-xl mb-0.5">🌃</div>
                        <p class="font-bold text-xs md:text-sm">サイバー・シティ</p>
                        <p class="text-[9px] text-cyan-300">近未来超高速</p>
                    </div>
                </div>
            </div>

            <!-- キャラクター選択 -->
            <div>
                <p class="mb-2 text-center text-md md:text-lg font-bold text-blue-300">🏎️ レーサーを選択(全12キャラ)</p>
                <div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2" id="color-options">
                    <div class="selection-card active" data-color="#ff0000" data-char="M" data-speed="0" data-accel="0" data-turn="0">
                        <div class="char-icon" style="background: #ff0000; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">レッド</p>
                        <p class="text-[8px] opacity-60">バランス</p>
                    </div>
                    <div class="selection-card" data-color="#00dd00" data-char="L" data-speed="-0.05" data-accel="0.005" data-turn="0.005">
                        <div class="char-icon" style="background: #00dd00; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">グリーン</p>
                        <p class="text-[8px] opacity-60">加速・旋回</p>
                    </div>
                    <div class="selection-card" data-color="#ff88cc" data-char="P" data-speed="-0.08" data-accel="0.008" data-turn="0.008">
                        <div class="char-icon" style="background: #ff88cc; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ピンク</p>
                        <p class="text-[8px] opacity-60">軽量・高加速</p>
                    </div>
                    <div class="selection-card" data-color="#ffcc00" data-char="W" data-speed="0.12" data-accel="-0.005" data-turn="-0.004">
                        <div class="char-icon" style="background: #ffcc00; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">イエロー</p>
                        <p class="text-[8px] opacity-60">最高速重視</p>
                    </div>
                    <div class="selection-card" data-color="#1e90ff" data-char="B" data-speed="0.02" data-accel="0" data-turn="0.004">
                        <div class="char-icon" style="background: #1e90ff; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ブルー</p>
                        <p class="text-[8px] opacity-60">コーナリング</p>
                    </div>
                    <div class="selection-card" data-color="#9d4edd" data-char="V" data-speed="0.1" data-accel="-0.003" data-turn="-0.002">
                        <div class="char-icon" style="background: #9d4edd; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">パープル</p>
                        <p class="text-[8px] opacity-60">ハイスピード</p>
                    </div>
                    <div class="selection-card" data-color="#ff6b00" data-char="O" data-speed="-0.02" data-accel="0.006" data-turn="0.003">
                        <div class="char-icon" style="background: #ff6b00; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">オレンジ</p>
                        <p class="text-[8px] opacity-60">ダッシュ・旋回</p>
                    </div>
                    <div class="selection-card" data-color="#f1f5f9" data-char="S" data-speed="-0.06" data-accel="0.007" data-turn="0.006">
                        <div class="char-icon" style="background: #f1f5f9; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ホワイト</p>
                        <p class="text-[8px] opacity-60">小回り重視</p>
                    </div>
                    <div class="selection-card" data-color="#222222" data-char="K" data-speed="0.15" data-accel="-0.008" data-turn="-0.006">
                        <div class="char-icon" style="background: #222222; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ブラック</p>
                        <p class="text-[8px] opacity-60">超最高速</p>
                    </div>
                    <div class="selection-card" data-color="#06b6d4" data-char="C" data-speed="0.05" data-accel="0.002" data-turn="0.001">
                        <div class="char-icon" style="background: #06b6d4; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">シアン</p>
                        <p class="text-[8px] opacity-60">テクニカル</p>
                    </div>
                    <div class="selection-card" data-color="#8b4513" data-char="D" data-speed="0.03" data-accel="-0.002" data-turn="0.003">
                        <div class="char-icon" style="background: #8b4513; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ブラウン</p>
                        <p class="text-[8px] opacity-60">安定パワー</p>
                    </div>
                    <div class="selection-card" data-color="#ffd700" data-char="G" data-speed="0.08" data-accel="0.004" data-turn="0.004">
                        <div class="char-icon" style="background: #ffd700; border: 3px solid white;"></div>
                        <p class="font-bold text-xs">ゴールド</p>
                        <p class="text-[8px] opacity-60">全ステUP</p>
                    </div>
                </div>
            </div>

            <!-- タイプ選択 -->
            <div>
                <div class="grid grid-cols-2 gap-4" id="type-options">
                    <div class="selection-card active" data-type="speed">
                        <p class="text-lg font-black text-yellow-400">⚡ スピード重視</p>
                        <p class="text-[10px] opacity-60">最高速度が高い</p>
                    </div>
                    <div class="selection-card" data-type="accel">
                        <p class="text-lg font-black text-green-400">🌀 加速・コーナリング重視</p>
                        <p class="text-[10px] opacity-60">加速と曲がりやすさUP</p>
                    </div>
                </div>
            </div>

            <!-- ボディ(フレーム)選択 -->
            <div>
                <p class="mb-2 text-center text-md md:text-lg font-bold text-blue-300">🚗 ボディ(車の形)を選択(全6種)</p>
                <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2" id="body-options">
                    <div class="selection-card active" data-body="standard">
                        <div class="text-xl mb-1">🏎️</div>
                        <p class="font-bold text-xs">スタンダード</p>
                        <p class="text-[9px] opacity-60">バランス重視</p>
                    </div>
                    <div class="selection-card" data-body="sports">
                        <div class="text-xl mb-1">🏎️💨</div>
                        <p class="font-bold text-xs">スポーツ GT</p>
                        <p class="text-[9px] opacity-60">最高速UP / ウイング</p>
                    </div>
                    <div class="selection-card" data-body="buggy">
                        <div class="text-xl mb-1">🛻</div>
                        <p class="font-bold text-xs">バギー</p>
                        <p class="text-[9px] opacity-60">加速・旋回UP</p>
                    </div>
                    <div class="selection-card" data-body="formula">
                        <div class="text-xl mb-1">🏎️🏁</div>
                        <p class="font-bold text-xs">フォーミュラ</p>
                        <p class="text-[9px] opacity-60">超最高速 / ロングノーズ</p>
                    </div>
                    <div class="selection-card" data-body="hover">
                        <div class="text-xl mb-1">🚀</div>
                        <p class="font-bold text-xs">ホバー・ジェット</p>
                        <p class="text-[9px] opacity-60">ジェットエンジン / 機動性</p>
                    </div>
                    <div class="selection-card" data-body="monster">
                        <div class="text-xl mb-1">🚜</div>
                        <p class="font-bold text-xs">モンスター</p>
                        <p class="text-[9px] opacity-60">重厚パワー / 露出エンジン</p>
                    </div>
                </div>
            </div>

            <!-- タイヤ選択 -->
            <div>
                <p class="mb-2 text-center text-md md:text-lg font-bold text-blue-300">🛞 タイヤを選択</p>
                <div class="grid grid-cols-3 gap-3" id="tire-options">
                    <div class="selection-card active" data-tire="normal">
                        <div class="text-xl mb-1">⚪</div>
                        <p class="font-bold text-xs">ノーマル</p>
                        <p class="text-[9px] opacity-60">標準タイヤ</p>
                    </div>
                    <div class="selection-card" data-tire="slick">
                        <div class="text-xl mb-1">⚫</div>
                        <p class="font-bold text-xs">スリック</p>
                        <p class="text-[9px] opacity-60">最高速UP / スライド</p>
                    </div>
                    <div class="selection-card" data-tire="big">
                        <div class="text-xl mb-1">🔘</div>
                        <p class="font-bold text-xs">ビッグタイヤ</p>
                        <p class="text-[9px] opacity-60">曲がりやすさ・グリップUP</p>
                    </div>
                </div>
            </div>

            <button id="start-btn" class="mt-2 bg-yellow-400 hover:bg-yellow-300 active:scale-95 text-black font-black py-3 rounded-2xl text-xl md:text-2xl transition shadow-[0_0_20px_rgba(250,204,21,0.5)] uppercase italic">
                レーススタート!
            </button>
        </div>
        <p class="mt-4 text-xs opacity-50">操作: 矢印キー/WASD(移動) | スペース(アイテム使用)</p>
    </div>

    <!-- ゴール画面 -->
    <div id="goal-screen">
        <h1 class="text-6xl md:text-8xl font-black mb-4 text-yellow-400 italic" id="finish-rank">GOAL!!</h1>
        <p class="text-2xl mb-8" id="finish-msg">お疲れ様でした!</p>
        <button onclick="location.reload()" class="bg-blue-500 hover:bg-blue-400 text-white font-bold py-3 px-8 rounded-full text-xl transition shadow-lg">
            タイトルへ戻る
        </button>
    </div>

    <!-- ゲームプレイHUD -->
    <div id="ui-layer">
        <div class="bg-black/50 backdrop-blur p-3 rounded-xl border border-white/20">
            <h1 class="text-2xl md:text-3xl font-black text-yellow-400 italic">時速: <span id="speed-val">0</span> km/h</h1>
            <p class="text-lg font-bold mt-1">アイテム: <span id="item-val" class="text-blue-300">なし</span></p>
        </div>
        <div class="bg-black/50 backdrop-blur p-3 rounded-xl border border-white/20 text-right">
            <h1 class="text-3xl md:text-4xl font-black text-white italic"><span id="lap-val">1</span> / 3 LAP</h1>
            <p class="text-sm font-bold mt-1 text-gray-300">順位: <span id="rank-val" class="text-yellow-400">1</span> / 4位</p>
        </div>
    </div>

    <!-- モバイル用操作UI -->
    <div id="mobile-controls" class="controls">
        <div class="btn" id="btn-left">←</div>
        <div class="flex flex-col gap-2">
            <div class="btn" id="btn-up">↑</div>
            <div class="btn" id="btn-down">↓</div>
        </div>
        <div class="btn" id="btn-right">→</div>
        <div class="btn bg-red-600/80" id="btn-item">🚀</div>
    </div>

    <script>
        let scene, camera, renderer, kart;
        let selectedColor = "#ff0000";
        let selectedType = "speed";
        let selectedChar = "M";
        let selectedTrack = "circle";
        let selectedBody = "standard";
        let selectedTire = "normal";
        let charStats = { speed: 0, accel: 0, turn: 0 };

        let previewScene, previewCamera, previewRenderer, previewMesh, previewGroundDisc;
        let isDraggingPreview = false;
        let previousMousePosition = { x: 0, y: 0 };
        let previewRotationY = 0;

        // レース進行管理
        let currentLap = 1;
        const maxLaps = 3;
        let passedCheckpoint = false;
        let isFinished = false;
        const TRACK_WIDTH = 36; // コース幅

        const player = {
            speed: 0,
            maxSpeed: 1.0,
            acceleration: 0.015,
            deceleration: 0.006,
            turnSpeed: 0.045,
            rotation: 0,
            item: null,
            isBoosting: false,
            isStar: false,
            spinTime: 0,
            rank: 1,
            progress: 0,
            isFalling: false,
            fallVelocity: 0
        };

        const enemies = [];
        const waypoints = [];
        const keys = { up: false, down: false, left: false, right: false };
        const items = [];
        let missiles = [];
        let bananas = [];
        let rainbowMesh = null;
        let starField = null;

        const setupSelection = (containerId, variableSetter) => {
            document.querySelectorAll(`#${containerId} .selection-card`).forEach(card => {
                card.onclick = () => {
                    document.querySelectorAll(`#${containerId} .selection-card`).forEach(c => c.classList.remove('active'));
                    card.classList.add('active');
                    variableSetter(card);
                    updatePreviewMesh();
                };
            });
        };

        // コースカップ切り替え処理
        document.querySelectorAll('#cup-tabs .cup-tab').forEach(tab => {
            tab.onclick = () => {
                document.querySelectorAll('#cup-tabs .cup-tab').forEach(t => t.classList.remove('active'));
                tab.classList.add('active');
                const cup = tab.dataset.cup;
                document.querySelectorAll('#track-options .selection-card').forEach(card => {
                    if (cup === 'all' || card.dataset.cup === cup) {
                        card.style.display = 'block';
                    } else {
                        card.style.display = 'none';
                    }
                });
            };
        });

        setupSelection('track-options', (card) => { 
            selectedTrack = card.dataset.track; 
            const name = card.querySelector('p')?.innerText || selectedTrack;
            const badge = document.getElementById('preview-track-badge');
            if (badge) badge.innerText = "コース: " + name;
        });

        setupSelection('color-options', (card) => { 
            selectedColor = card.dataset.color; 
            selectedChar = card.dataset.char; 
            charStats.speed = parseFloat(card.dataset.speed || 0);
            charStats.accel = parseFloat(card.dataset.accel || 0);
            charStats.turn = parseFloat(card.dataset.turn || 0);
        });
        setupSelection('type-options', (card) => { selectedType = card.dataset.type; });
        setupSelection('body-options', (card) => { selectedBody = card.dataset.body; });
        setupSelection('tire-options', (card) => { selectedTire = card.dataset.tire; });

        function initPreview() {
            const canvas = document.getElementById('preview-canvas');
            const container = document.getElementById('preview-container') || canvas?.parentElement;
            if (!canvas || !container) return;

            const width = container.clientWidth || 340;
            const height = container.clientHeight || 200;

            previewScene = new THREE.Scene();
            previewCamera = new THREE.PerspectiveCamera(40, width / height, 0.1, 100);
            previewCamera.position.set(0, 1.8, 4.5);
            previewCamera.lookAt(0, 0.4, 0);

            previewRenderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true, powerPreference: "high-performance" });
            previewRenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            previewRenderer.setSize(width, height, false);

            // ライティングの強化
            const sunLight = new THREE.DirectionalLight(0xffffff, 1.3);
            sunLight.position.set(5, 10, 7);
            previewScene.add(sunLight);

            const fillLight = new THREE.DirectionalLight(0x88bbff, 0.6);
            fillLight.position.set(-5, 5, -5);
            previewScene.add(fillLight);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
            previewScene.add(ambientLight);

            // コーステーマ連動型の地面ディスク
            previewGroundDisc = new THREE.Mesh(
                new THREE.CircleGeometry(2.4, 32),
                new THREE.MeshBasicMaterial({ color: 0x333333 })
            );
            previewGroundDisc.rotation.x = -Math.PI / 2;
            previewGroundDisc.position.y = -0.01;
            previewScene.add(previewGroundDisc);

            // マウス・タッチでの360度インタラクティブ回転処理
            const startDrag = (x, y) => {
                isDraggingPreview = true;
                previousMousePosition = { x, y };
            };
            const moveDrag = (x, y) => {
                if (!isDraggingPreview || !previewMesh) return;
                const deltaX = x - previousMousePosition.x;
                previewRotationY += deltaX * 0.012;
                previewMesh.rotation.y = previewRotationY;
                previousMousePosition = { x, y };
            };
            const stopDrag = () => { isDraggingPreview = false; };

            container.addEventListener('mousedown', e => startDrag(e.clientX, e.clientY));
            window.addEventListener('mousemove', e => moveDrag(e.clientX, e.clientY));
            window.addEventListener('mouseup', stopDrag);

            container.addEventListener('touchstart', e => {
                if (e.touches.length > 0) startDrag(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: true });
            window.addEventListener('touchmove', e => {
                if (e.touches.length > 0) moveDrag(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: true });
            window.addEventListener('touchend', stopDrag);

            // 自動サイズ調整(ResizeObserver)
            const resizeObserver = new ResizeObserver(() => {
                if (!container || !previewRenderer || !previewCamera) return;
                const w = container.clientWidth;
                const h = container.clientHeight;
                if (w > 0 && h > 0) {
                    previewCamera.aspect = w / h;
                    previewCamera.updateProjectionMatrix();
                    previewRenderer.setSize(w, h, false);
                }
            });
            resizeObserver.observe(container);

            updatePreviewMesh();
            animatePreview();
        }

        function updatePreviewMesh() {
            if (!previewScene) return;

            // コーステーマに合わせて3Dプレビューの地面・背景色を更新
            let bgHex = 0x090d16;
            let groundHex = 0x333333;
            if (selectedTrack === 'eight') { bgHex = 0x3d2200; groundHex = 0xddaa55; }
            else if (selectedTrack === 'ocean') { bgHex = 0x002244; groundHex = 0x0077be; }
            else if (selectedTrack === 'castle') { bgHex = 0x220505; groundHex = 0xff2200; }
            else if (selectedTrack === 'rainbow') { bgHex = 0x110022; groundHex = 0xff00ff; }
            else if (selectedTrack === 'jungle') { bgHex = 0x051a05; groundHex = 0x0f4010; }
            else if (selectedTrack === 'snow') { bgHex = 0x0f2233; groundHex = 0x99ccff; }
            else if (selectedTrack === 'volcano') { bgHex = 0x2a0500; groundHex = 0xaa1100; }
            else if (selectedTrack === 'sunset') { bgHex = 0x331505; groundHex = 0xe69d45; }
            else if (selectedTrack === 'cyber') { bgHex = 0x050015; groundHex = 0x00ffff; }

            previewScene.background = new THREE.Color(bgHex);
            if (previewGroundDisc) previewGroundDisc.material.color.setHex(groundHex);

            if (previewMesh) {
                previewScene.remove(previewMesh);
                previewMesh.traverse(child => {
                    if (child.geometry) child.geometry.dispose();
                    if (child.material) {
                        if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
                        else child.material.dispose();
                    }
                });
            }
            previewMesh = createKartMesh(selectedColor, selectedBody, selectedTire);
            previewMesh.rotation.y = previewRotationY;
            previewScene.add(previewMesh);
        }

        function animatePreview() {
            const selectScreen = document.getElementById('select-screen');
            if (selectScreen && getComputedStyle(selectScreen).display !== 'none') {
                requestAnimationFrame(animatePreview);
                if (previewMesh && !isDraggingPreview) {
                    previewRotationY += 0.012;
                    previewMesh.rotation.y = previewRotationY;
                }
                if (previewRenderer && previewScene && previewCamera) {
                    previewRenderer.render(previewScene, previewCamera);
                }
            }
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initPreview);
        } else {
            initPreview();
        }
        window.addEventListener('load', initPreview);

        document.getElementById('start-btn').onclick = () => {
            document.getElementById('select-screen').style.display = 'none';
            document.getElementById('ui-layer').style.display = 'flex';
            if ('ontouchstart' in window || window.innerWidth < 768) {
                document.getElementById('mobile-controls').style.display = 'flex';
            }
            
            // タイプ・カスタムパーツ・キャラクターによる最終性能の計算
            let maxSpd = 1.0 + charStats.speed;
            let accel = 0.018 + charStats.accel;
            let turnSpd = 0.045 + charStats.turn;

            if (selectedType === 'speed') { maxSpd += 0.25; accel -= 0.003; }
            else { maxSpd -= 0.1; accel += 0.01; turnSpd += 0.008; }

            if (selectedBody === 'sports') { maxSpd += 0.15; turnSpd -= 0.005; }
            else if (selectedBody === 'buggy') { maxSpd -= 0.08; accel += 0.008; turnSpd += 0.008; }
            else if (selectedBody === 'formula') { maxSpd += 0.22; accel -= 0.006; turnSpd -= 0.008; }
            else if (selectedBody === 'hover') { maxSpd += 0.1; accel += 0.006; turnSpd += 0.006; }
            else if (selectedBody === 'monster') { maxSpd += 0.08; accel -= 0.005; turnSpd += 0.01; }

            if (selectedTire === 'slick') { maxSpd += 0.1; turnSpd -= 0.003; }
            else if (selectedTire === 'big') { accel += 0.005; turnSpd += 0.01; }

            player.maxSpeed = maxSpd;
            player.acceleration = accel;
            player.turnSpeed = turnSpd;

            init();
        };

        function init() {
            scene = new THREE.Scene();
            
            // コース別背景&フォグ設定
            let bgHex = 0x1a237e;
            let fogHex = 0x1a237e;
            if (selectedTrack === 'eight') { bgHex = 0xffaa44; fogHex = 0xffaa44; }
            else if (selectedTrack === 'ocean') { bgHex = 0x87ceeb; fogHex = 0x87ceeb; }
            else if (selectedTrack === 'castle') { bgHex = 0x1a0505; fogHex = 0x1a0505; }
            else if (selectedTrack === 'rainbow') { bgHex = 0x070014; fogHex = 0x070014; }
            else if (selectedTrack === 'jungle') { bgHex = 0x113311; fogHex = 0x113311; }
            else if (selectedTrack === 'snow') { bgHex = 0xddeeff; fogHex = 0xbbddff; }
            else if (selectedTrack === 'volcano') { bgHex = 0x330500; fogHex = 0x330500; }
            else if (selectedTrack === 'sunset') { bgHex = 0xff7744; fogHex = 0xff7744; }
            else if (selectedTrack === 'cyber') { bgHex = 0x050015; fogHex = 0x100025; }

            scene.background = new THREE.Color(bgHex);
            scene.fog = new THREE.Fog(fogHex, 70, 320);

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.shadowMap.enabled = true;
            document.body.appendChild(renderer.domElement);

            const ambientLight = new THREE.AmbientLight(0xffffff, (selectedTrack === 'rainbow' || selectedTrack === 'cyber') ? 0.9 : 0.7);
            scene.add(ambientLight);
            const sunLight = new THREE.DirectionalLight(0xffffff, 0.8);
            sunLight.position.set(50, 100, 50);
            sunLight.castShadow = true;
            scene.add(sunLight);

            generateWaypoints();
            createTrackMesh();
            createKart();
            createEnemies();
            createEnvironment();

            window.addEventListener('keydown', e => handleKey(e, true));
            window.addEventListener('keyup', e => handleKey(e, false));
            setupTouchEvents();
            window.addEventListener('resize', onWindowResize);

            animate();
        }

        function generateWaypoints() {
            waypoints.length = 0;
            const count = 72; // 高解像度ウェイポイント

            if (selectedTrack === 'circle') {
                for (let i = 0; i < count; i++) {
                    const rad = (i / count) * Math.PI * 2;
                    waypoints.push(new THREE.Vector3(Math.cos(rad) * 45, 0, Math.sin(rad) * 45));
                }
            } else if (selectedTrack === 'eight') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const scale = 60;
                    const x = (scale * Math.cos(t)) / (1 + Math.sin(t)**2);
                    const z = (scale * Math.sin(t) * Math.cos(t)) / (1 + Math.sin(t)**2);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'ocean') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const x = 55 * Math.cos(t) + 12 * Math.cos(2 * t);
                    const z = 36 * Math.sin(t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'castle') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const rx = 50 * Math.sign(Math.cos(t)) * Math.pow(Math.abs(Math.cos(t)), 0.4);
                    const rz = 35 * Math.sign(Math.sin(t)) * Math.pow(Math.abs(Math.sin(t)), 0.4);
                    waypoints.push(new THREE.Vector3(rx, 0, rz));
                }
            } else if (selectedTrack === 'rainbow') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const r = 46 + 12 * Math.sin(3 * t);
                    const x = r * Math.cos(t);
                    const z = r * Math.sin(t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'jungle') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const x = 50 * Math.cos(t) + 18 * Math.sin(3 * t);
                    const z = 40 * Math.sin(t) + 12 * Math.cos(2 * t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'snow') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const x = 65 * Math.cos(t);
                    const z = 32 * Math.sin(t) + (i > count/2 ? 15 * Math.sin(6 * t) : 0);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'volcano') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const r = 48 + 15 * Math.cos(2 * t);
                    const x = r * Math.cos(t);
                    const z = r * Math.sin(t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'sunset') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const x = 58 * Math.cos(t) + 10 * Math.cos(4 * t);
                    const z = 42 * Math.sin(t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            } else if (selectedTrack === 'cyber') {
                for (let i = 0; i < count; i++) {
                    const t = (i / count) * Math.PI * 2;
                    const x = 60 * Math.sin(t);
                    const z = 40 * Math.sin(2 * t);
                    waypoints.push(new THREE.Vector3(x, 0, z));
                }
            }
        }

        function createTrackMesh() {
            let groundMatColor = 0x2e7d32;
            if (selectedTrack === 'eight') groundMatColor = 0xddaa55;
            else if (selectedTrack === 'ocean') groundMatColor = 0x0077be;
            else if (selectedTrack === 'castle') groundMatColor = 0xff2200;
            else if (selectedTrack === 'jungle') groundMatColor = 0x0f4010;
            else if (selectedTrack === 'snow') groundMatColor = 0xe8f4f8;
            else if (selectedTrack === 'volcano') groundMatColor = 0xaa1100;
            else if (selectedTrack === 'sunset') groundMatColor = 0xe69d45;
            else if (selectedTrack === 'cyber') groundMatColor = 0x090520;

            if (selectedTrack !== 'rainbow') {
                const groundMat = new THREE.MeshPhongMaterial({
                    color: groundMatColor,
                    emissive: (selectedTrack === 'castle' || selectedTrack === 'volcano') ? 0x660000 : (selectedTrack === 'cyber' ? 0x110033 : 0x000000)
                });
                const ground = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000), groundMat);
                ground.rotation.x = -Math.PI / 2;
                if (selectedTrack === 'castle' || selectedTrack === 'ocean' || selectedTrack === 'volcano') {
                    ground.position.y = -12;
                }
                ground.receiveShadow = true;
                scene.add(ground);
            }

            // リボン幾何学によるコース走路生成
            const trackGeo = new THREE.BufferGeometry();
            const positions = [];
            const colors = [];
            const uvs = [];
            const count = waypoints.length;

            let trackColor = new THREE.Color(0x333333);
            if (selectedTrack === 'ocean') trackColor = new THREE.Color(0xeeeeee);
            else if (selectedTrack === 'castle' || selectedTrack === 'volcano') trackColor = new THREE.Color(0x222228);
            else if (selectedTrack === 'snow') trackColor = new THREE.Color(0x99ccff);
            else if (selectedTrack === 'cyber') trackColor = new THREE.Color(0x111122);

            for (let i = 0; i < count; i++) {
                const current = waypoints[i];
                const next = waypoints[(i + 1) % count];
                const prev = waypoints[(i - 1 + count) % count];

                const dir = new THREE.Vector3().subVectors(next, prev).normalize();
                const perp = new THREE.Vector3(-dir.z, 0, dir.x).normalize();

                const pLeft = current.clone().add(perp.clone().multiplyScalar(TRACK_WIDTH / 2));
                const pRight = current.clone().sub(perp.clone().multiplyScalar(TRACK_WIDTH / 2));

                positions.push(pLeft.x, pLeft.y + 0.06, pLeft.z);
                positions.push(pRight.x, pRight.y + 0.06, pRight.z);

                if (selectedTrack === 'rainbow' || selectedTrack === 'cyber') {
                    const hue = i / count;
                    const c = new THREE.Color().setHSL(hue, selectedTrack === 'cyber' ? 0.8 : 1.0, 0.5);
                    colors.push(c.r, c.g, c.b, c.r, c.g, c.b);
                }

                uvs.push(0, i / count * 10);
                uvs.push(1, i / count * 10);
            }

            const indices = [];
            for (let i = 0; i < count; i++) {
                const nextI = (i + 1) % count;
                const i0 = i * 2;
                const i1 = i * 2 + 1;
                const i2 = nextI * 2;
                const i3 = nextI * 2 + 1;

                indices.push(i0, i1, i2);
                indices.push(i1, i3, i2);
            }

            trackGeo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            if (selectedTrack === 'rainbow' || selectedTrack === 'cyber') {
                trackGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
            }
            trackGeo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
            trackGeo.setIndex(indices);
            trackGeo.computeVertexNormals();

            let trackMaterial;
            if (selectedTrack === 'rainbow' || selectedTrack === 'cyber') {
                trackMaterial = new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide });
            } else {
                trackMaterial = new THREE.MeshPhongMaterial({ color: trackColor, side: THREE.DoubleSide });
            }

            rainbowMesh = new THREE.Mesh(trackGeo, trackMaterial);
            rainbowMesh.receiveShadow = true;
            scene.add(rainbowMesh);

            // スタート&ゴールライン
            const startPos = waypoints[0];
            const nextPos = waypoints[1];
            const startDir = new THREE.Vector3().subVectors(nextPos, startPos).normalize();
            const startAngle = Math.atan2(startDir.x, startDir.z);

            const finishLine = new THREE.Mesh(
                new THREE.BoxGeometry(TRACK_WIDTH, 0.1, 2),
                new THREE.MeshPhongMaterial({ color: 0xffffff })
            );
            finishLine.position.copy(startPos);
            finishLine.position.y = 0.12;
            finishLine.rotation.y = startAngle;
            scene.add(finishLine);

            // アイテムボックスの設置
            const setsCount = 10;
            for (let i = 0; i < setsCount; i++) {
                const wpIdx = Math.floor(i * count / setsCount);
                const current = waypoints[wpIdx];
                const next = waypoints[(wpIdx + 1) % count];
                const prev = waypoints[(wpIdx - 1 + count) % count];
                const dir = new THREE.Vector3().subVectors(next, prev).normalize();
                const perp = new THREE.Vector3(-dir.z, 0, dir.x).normalize();

                [-0.35, 0, 0.35].forEach(offsetRatio => {
                    const boxPos = current.clone().add(perp.clone().multiplyScalar((TRACK_WIDTH / 2) * offsetRatio));
                    spawnItemBox(boxPos.x, boxPos.z);
                });
            }
        }

        function spawnItemBox(x, z) {
            const box = new THREE.Mesh(
                new THREE.BoxGeometry(1.8, 1.8, 1.8),
                new THREE.MeshPhongMaterial({ color: 0x00ffff, transparent: true, opacity: 0.7 })
            );
            box.position.set(x, 1.5, z);
            scene.add(box);
            items.push(box);
        }

        function createKartMesh(color, bodyType = 'standard', tireType = 'normal') {
            const group = new THREE.Group();
            const bodyMat = new THREE.MeshPhongMaterial({ color: color, shininess: 60 });
            
            if (bodyType === 'sports') {
                const body = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.35, 2.6), bodyMat);
                body.position.y = 0.35;
                body.castShadow = true;
                group.add(body);

                const nose = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.15, 0.6), bodyMat);
                nose.position.set(0, 0.2, 1.4);
                group.add(nose);

                const wingMat = new THREE.MeshPhongMaterial({ color: 0x111111 });
                const wingPillar1 = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.5, 0.1), wingMat);
                wingPillar1.position.set(-0.5, 0.6, -1.1);
                const wingPillar2 = wingPillar1.clone();
                wingPillar2.position.x = 0.5;
                const wingBlade = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.08, 0.5), wingMat);
                wingBlade.position.set(0, 0.85, -1.1);
                group.add(wingPillar1, wingPillar2, wingBlade);
            } else if (bodyType === 'buggy') {
                const body = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.6, 2.2), bodyMat);
                body.position.y = 0.6;
                body.castShadow = true;
                group.add(body);

                const pipeMat = new THREE.MeshPhongMaterial({ color: 0x222222 });
                const cage = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.2), pipeMat);
                cage.rotation.z = Math.PI / 2;
                cage.position.set(0, 1.25, 0);
                group.add(cage);
            } else if (bodyType === 'formula') {
                const body = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.35, 3.0), bodyMat);
                body.position.y = 0.3;
                group.add(body);

                const nose = new THREE.Mesh(new THREE.ConeGeometry(0.45, 1.2, 4), bodyMat);
                nose.rotation.x = Math.PI / 2;
                nose.rotation.y = Math.PI / 4;
                nose.position.set(0, 0.25, 1.8);
                group.add(nose);

                const frontWing = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.06, 0.4), new THREE.MeshPhongMaterial({ color: 0x222222 }));
                frontWing.position.set(0, 0.18, 2.0);
                group.add(frontWing);

                const rearWingMat = new THREE.MeshPhongMaterial({ color: 0x111111 });
                const rWing = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.08, 0.5), rearWingMat);
                rWing.position.set(0, 0.95, -1.2);
                const pillar1 = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.6, 0.2), rearWingMat);
                pillar1.position.set(-0.6, 0.65, -1.2);
                const pillar2 = pillar1.clone();
                pillar2.position.x = 0.6;
                group.add(rWing, pillar1, pillar2);

                const leftPod = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.3, 1.2), bodyMat);
                leftPod.position.set(-0.65, 0.3, 0);
                const rightPod = leftPod.clone();
                rightPod.position.x = 0.65;
                group.add(leftPod, rightPod);
            } else if (bodyType === 'hover') {
                const body = new THREE.Mesh(new THREE.ConeGeometry(0.85, 3.0, 8), bodyMat);
                body.rotation.x = -Math.PI / 2;
                body.position.set(0, 0.45, 0.2);
                group.add(body);

                const jetMat = new THREE.MeshPhongMaterial({ color: 0x333344, emissive: 0x00ffff, emissiveIntensity: 0.3 });
                const jet1 = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.3, 0.8, 12), jetMat);
                jet1.rotation.x = Math.PI / 2;
                jet1.position.set(-0.45, 0.45, -1.2);
                const jet2 = jet1.clone();
                jet2.position.x = 0.45;
                group.add(jet1, jet2);

                const finMat = new THREE.MeshPhongMaterial({ color: 0x111122 });
                const leftFin = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.6, 0.8), finMat);
                leftFin.position.set(-0.8, 0.5, -0.6);
                leftFin.rotation.z = -0.3;
                const rightFin = leftFin.clone();
                rightFin.position.x = 0.8;
                rightFin.rotation.z = 0.3;
                group.add(leftFin, rightFin);
            } else if (bodyType === 'monster') {
                const body = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.75, 2.5), bodyMat);
                body.position.y = 0.75;
                group.add(body);

                const engineBlock = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 0.8), new THREE.MeshPhongMaterial({ color: 0xaaaaaa, shininess: 100 }));
                engineBlock.position.set(0, 1.2, 0.6);
                group.add(engineBlock);

                const intake = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.3, 8), new THREE.MeshPhongMaterial({ color: 0xff3300 }));
                intake.position.set(0, 1.45, 0.6);
                group.add(intake);

                const pipeMat = new THREE.MeshPhongMaterial({ color: 0x555555 });
                const pipe1 = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 1.2, 8), pipeMat);
                pipe1.position.set(-0.85, 1.2, -1.0);
                const pipe2 = pipe1.clone();
                pipe2.position.x = 0.85;
                group.add(pipe1, pipe2);
            } else {
                const body = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.5, 2.4), bodyMat);
                body.position.y = 0.5;
                body.castShadow = true;
                group.add(body);
            }

            // ドライバー
            let headY = 1.4;
            if (bodyType === 'sports') headY = 1.05;
            else if (bodyType === 'buggy') headY = 1.45;
            else if (bodyType === 'formula') headY = 0.95;
            else if (bodyType === 'hover') headY = 1.1;
            else if (bodyType === 'monster') headY = 1.6;

            const driverGroup = new THREE.Group();
            driverGroup.position.y = headY;
            if (bodyType === 'formula') driverGroup.position.z = -0.2;

            const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.45, 16, 16), bodyMat);
            driverGroup.add(helmet);

            const visorMat = new THREE.MeshPhongMaterial({ color: 0x111122, shininess: 100 });
            const visor = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.22, 0.3), visorMat);
            visor.position.set(0, 0.05, 0.22);
            driverGroup.add(visor);

            group.add(driverGroup);

            // タイヤ
            let radius = 0.4;
            let width = 0.6;
            let wheelMat = new THREE.MeshPhongMaterial({ color: 0x111111 });

            if (tireType === 'slick') {
                radius = 0.38;
                width = 0.85;
                wheelMat = new THREE.MeshPhongMaterial({ color: 0x222222, shininess: 80 });
            } else if (tireType === 'big') {
                radius = 0.62;
                width = 0.7;
                wheelMat = new THREE.MeshPhongMaterial({ color: 0x080808 });
            }

            const wheelGeo = new THREE.CylinderGeometry(radius, radius, width, 14);
            const wheelOffsetY = radius;
            const sideOffset = tireType === 'big' ? 0.95 : 0.85;

            [[-sideOffset, wheelOffsetY, 0.9], [sideOffset, wheelOffsetY, 0.9], [-sideOffset, wheelOffsetY, -0.9], [sideOffset, wheelOffsetY, -0.9]].forEach(pos => {
                const w = new THREE.Mesh(wheelGeo, wheelMat);
                w.rotation.z = Math.PI / 2;
                w.position.set(...pos);
                group.add(w);
            });

            group.userData.bodyMat = bodyMat;
            return group;
        }

        function createKart() {
            kart = createKartMesh(selectedColor, selectedBody, selectedTire);
            const startPos = waypoints[0];
            const nextPos = waypoints[1];
            const startDir = new THREE.Vector3().subVectors(nextPos, startPos).normalize();
            player.rotation = Math.atan2(startDir.x, startDir.z);

            kart.position.copy(startPos);
            kart.rotation.y = player.rotation;
            scene.add(kart);
        }

        function createEnemies() {
            const allColors = [
                '#ff0000', '#00dd00', '#ff88cc', '#ffcc00', 
                '#1e90ff', '#9d4edd', '#ff6b00', '#f1f5f9', 
                '#222222', '#06b6d4', '#8b4513', '#ffd700'
            ].filter(c => c !== selectedColor);

            for (let i = allColors.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [allColors[i], allColors[j]] = [allColors[j], allColors[i]];
            }

            const startPos = waypoints[0];
            const nextPos = waypoints[1];
            const startDir = new THREE.Vector3().subVectors(nextPos, startPos).normalize();
            const perp = new THREE.Vector3(-startDir.z, 0, startDir.x).normalize();
            const initAngle = Math.atan2(startDir.x, startDir.z);

            const bodies = ['standard', 'sports', 'buggy'];
            const tires = ['normal', 'slick', 'big'];

            for (let i = 0; i < 3; i++) {
                const color = allColors[i];
                const eMesh = createKartMesh(color, bodies[i % 3], tires[(i + 1) % 3]);
                const offsetSide = (i % 2 === 0 ? 3.5 : -3.5);
                const offsetBack = -6 - (i * 4);
                
                const pos = startPos.clone()
                    .add(perp.clone().multiplyScalar(offsetSide))
                    .add(startDir.clone().multiplyScalar(offsetBack));

                eMesh.position.copy(pos);
                eMesh.rotation.y = initAngle;
                scene.add(eMesh);

                enemies.push({
                    mesh: eMesh,
                    speed: 0,
                    maxSpeed: 0.85 + Math.random() * 0.18,
                    targetIdx: 1,
                    spinTime: 0,
                    lap: 1,
                    progress: 0,
                    isFalling: false,
                    fallVelocity: 0
                });
            }
        }

        function createBuilding(x, z, type) {
            const building = new THREE.Group();
            if (type === 'skyscraper') {
                const h = 12 + Math.random() * 22;
                const b = new THREE.Mesh(new THREE.BoxGeometry(6, h, 6), new THREE.MeshPhongMaterial({ color: 0x334466 }));
                b.position.y = h / 2;
                building.add(b);
            } else if (type === 'pyramid') {
                const p = new THREE.Mesh(new THREE.ConeGeometry(12, 16, 4), new THREE.MeshPhongMaterial({ color: 0xedc9af }));
                p.position.y = 8;
                p.rotation.y = Math.PI / 4;
                building.add(p);
            } else if (type === 'palm') {
                const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.6, 8), new THREE.MeshPhongMaterial({ color: 0x8b5a2b }));
                trunk.position.y = 4;
                const top = new THREE.Mesh(new THREE.ConeGeometry(4, 3, 6), new THREE.MeshPhongMaterial({ color: 0x2e8b57 }));
                top.position.y = 8;
                building.add(trunk, top);
            } else if (type === 'castle') {
                const tower = new THREE.Mesh(new THREE.CylinderGeometry(3, 3, 14), new THREE.MeshPhongMaterial({ color: 0x444444 }));
                tower.position.y = 7;
                const roof = new THREE.Mesh(new THREE.ConeGeometry(4, 5, 8), new THREE.MeshPhongMaterial({ color: 0x880000 }));
                roof.position.y = 16.5;
                building.add(tower, roof);
            } else if (type === 'planet') {
                const p = new THREE.Mesh(new THREE.SphereGeometry(6 + Math.random() * 6, 16, 16), new THREE.MeshPhongMaterial({ color: Math.random() * 0xffffff }));
                p.position.y = 15 + Math.random() * 15;
                building.add(p);
            } else if (type === 'tree') {
                const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 1.2, 7), new THREE.MeshPhongMaterial({ color: 0x5c4033 }));
                trunk.position.y = 3.5;
                const leaves = new THREE.Mesh(new THREE.SphereGeometry(3.5, 8, 8), new THREE.MeshPhongMaterial({ color: 0x1e5631 }));
                leaves.position.y = 7.5;
                building.add(trunk, leaves);
            } else if (type === 'iceberg') {
                const ice = new THREE.Mesh(new THREE.ConeGeometry(7, 12, 5), new THREE.MeshPhongMaterial({ color: 0xcceeff, transparent: true, opacity: 0.85 }));
                ice.position.y = 6;
                building.add(ice);
            } else if (type === 'volcano_rock') {
                const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(5), new THREE.MeshPhongMaterial({ color: 0x331111 }));
                rock.position.y = 3.5;
                building.add(rock);
            } else if (type === 'cyber_tower') {
                const h = 20 + Math.random() * 25;
                const tower = new THREE.Mesh(new THREE.BoxGeometry(4, h, 4), new THREE.MeshPhongMaterial({ color: 0x050515, emissive: 0x00ffff, wireframe: true }));
                tower.position.y = h / 2;
                building.add(tower);
            }
            building.position.set(x, 0, z);
            scene.add(building);
        }

        function createEnvironment() {
            if (selectedTrack === 'rainbow' || selectedTrack === 'cyber') {
                const starGeo = new THREE.BufferGeometry();
                const starPositions = [];
                for (let i = 0; i < 800; i++) {
                    starPositions.push((Math.random() - 0.5) * 600, (Math.random() - 0.5) * 300 + 50, (Math.random() - 0.5) * 600);
                }
                starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
                const starMat = new THREE.PointsMaterial({ color: selectedTrack === 'cyber' ? 0x00ffff : 0xffffff, size: 1.5 });
                starField = new THREE.Points(starGeo, starMat);
                scene.add(starField);
            }

            for (let i = 0; i < 45; i++) {
                const angle = Math.random() * Math.PI * 2;
                const dist = 75 + Math.random() * 60;
                let type = 'skyscraper';
                if (selectedTrack === 'eight') type = 'pyramid';
                else if (selectedTrack === 'ocean') type = 'palm';
                else if (selectedTrack === 'castle') type = 'castle';
                else if (selectedTrack === 'rainbow') type = 'planet';
                else if (selectedTrack === 'jungle') type = 'tree';
                else if (selectedTrack === 'snow') type = 'iceberg';
                else if (selectedTrack === 'volcano') type = 'volcano_rock';
                else if (selectedTrack === 'sunset') type = 'palm';
                else if (selectedTrack === 'cyber') type = 'cyber_tower';

                createBuilding(Math.cos(angle) * dist, Math.sin(angle) * dist, type);
            }
        }

        function handleKey(e, isPressed) {
            if (isFinished) return;
            if (e.code === 'Space') { e.preventDefault(); if (isPressed) useItem(); return; }
            switch (e.code) {
                case 'ArrowUp': case 'KeyW': keys.up = isPressed; break;
                case 'ArrowDown': case 'KeyS': keys.down = isPressed; break;
                case 'ArrowLeft': case 'KeyA': keys.left = isPressed; break;
                case 'ArrowRight': case 'KeyD': keys.right = isPressed; break;
            }
        }

        function setupTouchEvents() {
            const bind = (id, key) => {
                const el = document.getElementById(id);
                if (!el) return;
                el.addEventListener('touchstart', (e) => { e.preventDefault(); keys[key] = true; });
                el.addEventListener('touchend', (e) => { e.preventDefault(); keys[key] = false; });
            };
            bind('btn-left', 'left'); bind('btn-right', 'right'); bind('btn-up', 'up'); bind('btn-down', 'down');
            document.getElementById('btn-item')?.addEventListener('touchstart', (e) => { e.preventDefault(); useItem(); });
        }

        function useItem() {
            if (!player.item || isFinished || player.spinTime > 0) return;
            const itemType = player.item;
            player.item = null;
            document.getElementById('item-val').innerText = "なし";

            if (itemType === 'ダッシュ') {
                player.isBoosting = true;
                player.speed = player.maxSpeed * 1.8;
                setTimeout(() => { player.isBoosting = false; }, 1500);
            } else if (itemType === 'ミサイル') {
                const m = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 8), new THREE.MeshPhongMaterial({ color: 0xff2222 }));
                m.position.copy(kart.position).add(new THREE.Vector3(0, 1, 0));
                m.userData = { dir: new THREE.Vector3(0, 0, 1).applyQuaternion(kart.quaternion), life: 120 };
                scene.add(m);
                missiles.push(m);
            } else if (itemType === 'バナナ') {
                const backOffset = new THREE.Vector3(0, 0.3, -2.5).applyQuaternion(kart.quaternion);
                const b = new THREE.Mesh(new THREE.ConeGeometry(0.6, 0.8, 6), new THREE.MeshPhongMaterial({ color: 0xffea00 }));
                b.position.copy(kart.position).add(backOffset);
                b.rotation.x = Math.PI / 6;
                scene.add(b);
                bananas.push(b);
            } else if (itemType === 'サンダー') {
                const origBg = scene.background.clone();
                scene.background = new THREE.Color(0xffffff);
                setTimeout(() => { scene.background = origBg; }, 150);
                enemies.forEach(e => { e.spinTime = 70; e.speed = 0; });
            } else if (itemType === 'スター') {
                player.isStar = true;
                player.isBoosting = true;
                setTimeout(() => {
                    player.isStar = false;
                    player.isBoosting = false;
                    if (kart.userData.bodyMat) kart.userData.bodyMat.color.setStyle(selectedColor);
                }, 5000);
            }
        }

        function animate() {
            requestAnimationFrame(animate);
            if (!isFinished) {
                updatePlayer();
                updateEnemies();
                checkRaceProgress();
                updateMissiles();
                updateBananas();
                updateStarEffect();
            }
            if (starField) starField.rotation.y += 0.0005;
            updateCamera();
            updateItems();
            renderer.render(scene, camera);
        }

        function updatePlayer() {
            if (player.isFalling) {
                player.fallVelocity += 0.02;
                kart.position.y -= player.fallVelocity;
                kart.rotation.x += 0.1;
                kart.rotation.z += 0.1;
                player.speed *= 0.9;

                if (kart.position.y < -35) {
                    player.isFalling = false;
                    player.fallVelocity = 0;
                    player.speed = 0;

                    let nearestWp = waypoints[0];
                    let minD = Infinity;
                    waypoints.forEach(wp => {
                        const d = kart.position.distanceTo(wp);
                        if (d < minD) { minD = d; nearestWp = wp; }
                    });

                    kart.position.copy(nearestWp);
                    kart.position.y = 0;
                    kart.rotation.set(0, player.rotation, 0);
                }
                document.getElementById('speed-val').innerText = Math.round(player.speed * 180);
                return;
            }

            if (player.spinTime > 0) {
                kart.rotation.y += 0.3;
                player.spinTime--;
                player.speed *= 0.8;
                if (player.spinTime === 0) kart.rotation.y = player.rotation;
                document.getElementById('speed-val').innerText = Math.round(player.speed * 180);
                return;
            }

            if (keys.up) player.speed += player.acceleration;
            else if (keys.down) player.speed -= player.acceleration;
            else player.speed *= (1 - player.deceleration);

            const max = player.isBoosting ? player.maxSpeed * 1.8 : player.maxSpeed;
            player.speed = Math.min(max, Math.max(-max / 2, player.speed));

            if (Math.abs(player.speed) > 0.01) {
                const d = player.speed > 0 ? 1 : -1;
                if (keys.left) player.rotation += player.turnSpeed * d;
                if (keys.right) player.rotation -= player.turnSpeed * d;
            }

            kart.rotation.y = player.rotation;
            const vel = new THREE.Vector3(0, 0, player.speed).applyQuaternion(kart.quaternion);
            kart.position.add(vel);

            if (player.isStar) {
                enemies.forEach(e => {
                    if (kart.position.distanceTo(e.mesh.position) < 2.8) {
                        e.spinTime = 60;
                        e.speed = 0;
                    }
                });
            }

            document.getElementById('speed-val').innerText = Math.round(player.speed * 180);
        }

        function updateEnemies() {
            enemies.forEach(e => {
                if (e.isFalling) {
                    e.fallVelocity += 0.02;
                    e.mesh.position.y -= e.fallVelocity;
                    e.mesh.rotation.x += 0.1;
                    e.mesh.rotation.z += 0.1;
                    e.speed = 0;

                    if (e.mesh.position.y < -35) {
                        e.isFalling = false;
                        e.fallVelocity = 0;
                        const respawnTarget = waypoints[e.targetIdx];
                        e.mesh.position.copy(respawnTarget);
                        e.mesh.position.y = 0;
                        e.mesh.rotation.set(0, Math.atan2(respawnTarget.x, respawnTarget.z), 0);
                    }
                    return;
                }

                if (e.spinTime > 0) {
                    e.mesh.rotation.y += 0.3;
                    e.spinTime--;
                    if (e.spinTime === 0) {
                        e.mesh.rotation.y = Math.atan2(Math.sin(e.mesh.rotation.y), Math.cos(e.mesh.rotation.y));
                    }
                    return;
                }

                const target = waypoints[e.targetIdx];
                const dx = target.x - e.mesh.position.x;
                const dz = target.z - e.mesh.position.z;
                const dist = Math.sqrt(dx * dx + dz * dz);

                if (dist > 120) {
                    e.mesh.position.copy(target).add(new THREE.Vector3((Math.random() - 0.5) * 3, 0, (Math.random() - 0.5) * 3));
                    e.speed = 0;
                }

                if (dist < 8) {
                    e.targetIdx = (e.targetIdx + 1) % waypoints.length;
                    if (e.targetIdx === 0) e.lap++;
                }

                const targetAngle = Math.atan2(dx, dz);
                let angleDiff = targetAngle - e.mesh.rotation.y;
                angleDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));

                e.mesh.rotation.y += angleDiff * 0.12;
                e.speed = Math.min(e.maxSpeed, e.speed + 0.012);

                const vel = new THREE.Vector3(0, 0, e.speed).applyQuaternion(e.mesh.quaternion);
                e.mesh.position.add(vel);

                e.progress = e.lap * 1000 + e.targetIdx;
            });
        }

        function updateBananas() {
            for (let i = bananas.length - 1; i >= 0; i--) {
                const b = bananas[i];
                b.rotation.y += 0.02;

                if (!player.isStar && player.spinTime <= 0 && kart.position.distanceTo(b.position) < 1.8) {
                    player.spinTime = 50;
                    player.speed = 0;
                    scene.remove(b);
                    bananas.splice(i, 1);
                    continue;
                }

                for (let e of enemies) {
                    if (e.spinTime <= 0 && e.mesh.position.distanceTo(b.position) < 2.0) {
                        e.spinTime = 60;
                        e.speed = 0;
                        scene.remove(b);
                        bananas.splice(i, 1);
                        break;
                    }
                }
            }
        }

        function updateStarEffect() {
            if (player.isStar && kart.userData.bodyMat) {
                const hue = (Date.now() % 1000) / 1000;
                kart.userData.bodyMat.color.setHSL(hue, 1, 0.5);
            }
        }

        function updateMissiles() {
            for (let i = missiles.length - 1; i >= 0; i--) {
                const m = missiles[i];
                m.position.add(m.userData.dir.clone().multiplyScalar(1.5));
                m.userData.life--;

                enemies.forEach(e => {
                    if (m.position.distanceTo(e.mesh.position) < 3) {
                        e.spinTime = 60; e.speed = 0;
                        m.userData.life = 0;
                    }
                });

                if (m.userData.life <= 0) { scene.remove(m); missiles.splice(i, 1); }
            }
        }

        function checkRaceProgress() {
            const startWp = waypoints[0];
            const halfWp = waypoints[Math.floor(waypoints.length / 2)];

            if (!passedCheckpoint && kart.position.distanceTo(halfWp) < 18) {
                passedCheckpoint = true;
            }

            if (passedCheckpoint && kart.position.distanceTo(startWp) < 14) {
                if (currentLap < maxLaps) {
                    currentLap++;
                    passedCheckpoint = false;
                    document.getElementById('lap-val').innerText = currentLap;
                } else {
                    isFinished = true;
                    document.getElementById('finish-rank').innerText = player.rank + "位でゴール!";
                    document.getElementById('goal-screen').style.display = 'flex';
                }
            }

            let myWpIdx = 0;
            let minDist = Infinity;
            waypoints.forEach((wp, i) => {
                const d = kart.position.distanceTo(wp);
                if (d < minDist) { minDist = d; myWpIdx = i; }
            });
            player.progress = currentLap * 1000 + myWpIdx;

            const fallTracks = ['rainbow', 'castle', 'ocean', 'volcano', 'cyber'];
            if (minDist > TRACK_WIDTH / 2 + 1.2) {
                if (fallTracks.includes(selectedTrack)) {
                    if (!player.isFalling && !player.isStar) {
                        player.isFalling = true;
                        player.fallVelocity = 0.1;
                    }
                } else if (!player.isStar) {
                    player.speed *= selectedTrack === 'snow' ? 0.97 : 0.94;
                }
            }

            if (fallTracks.includes(selectedTrack)) {
                enemies.forEach(e => {
                    let eMinDist = Infinity;
                    waypoints.forEach(wp => {
                        const d = e.mesh.position.distanceTo(wp);
                        if (d < eMinDist) eMinDist = d;
                    });
                    if (eMinDist > TRACK_WIDTH / 2 + 2.5 && !e.isFalling) {
                        e.isFalling = true;
                        e.fallVelocity = 0.1;
                    }
                });
            }

            const allRacers = [{ p: player, id: 'player' }, ...enemies.map(e => ({ p: e, id: 'enemy' }))];
            allRacers.sort((a, b) => b.p.progress - a.p.progress);
            player.rank = allRacers.findIndex(r => r.id === 'player') + 1;
            document.getElementById('rank-val').innerText = player.rank;
        }

        function updateCamera() {
            const offset = new THREE.Vector3(0, 4, -10).applyQuaternion(kart.quaternion);
            camera.position.lerp(kart.position.clone().add(offset), 0.1);
            camera.lookAt(kart.position.clone().add(new THREE.Vector3(0, 1, 2)));
        }

        function updateItems() {
            const itemList = ['ダッシュ', 'ミサイル', 'バナナ', 'サンダー', 'スター'];
            items.forEach(box => {
                box.rotation.y += 0.05;
                if (!isFinished && box.visible && kart.position.distanceTo(box.position) < 3) {
                    if (!player.item) {
                        const randomIndex = Math.floor(Math.random() * itemList.length);
                        player.item = itemList[randomIndex];
                        document.getElementById('item-val').innerText = player.item;
                        box.visible = false;
                        setTimeout(() => box.visible = true, 5000);
                    }
                }
            });
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
    </script>
</body>
</html>

スペシャルレースゲーム

by kizu

レースゲーム

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