🔬 AI発明ギャラリー syunsuke ビオランテ
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
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 Biollante Laboratory Sandbox</title>
    <!-- Tailwind CSS (Aesthetics and layouts) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- FontAwesome for matching bio-hazard style icons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        html, body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-color: #010301;
            font-family: 'Courier New', Courier, monospace;
            user-select: none;
            /* モバイル端末でのバウンススクロールやリフレッシュ動作を完全に無効化 */
            touch-action: none; 
        }
        /* スキャンライン効果 */
        .scanlines {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background: linear-gradient(
                rgba(18, 16, 16, 0) 50%, 
                rgba(0, 0, 0, 0.3) 50%
            ), linear-gradient(
                90deg,
                rgba(255, 0, 0, 0.04),
                rgba(0, 255, 0, 0.01),
                rgba(0, 0, 255, 0.04)
            );
            background-size: 100% 4px, 6px 100%;
            z-index: 10;
            pointer-events: none;
        }
        /* サイバーホログラム風グリッド */
        .glitch-text {
            text-shadow: 0 0 8px rgba(74, 242, 74, 0.8);
            animation: pulse 2s infinite alternate;
        }
        @keyframes pulse {
            0% { opacity: 0.8; text-shadow: 0 0 4px rgba(74, 242, 74, 0.5); }
            100% { opacity: 1; text-shadow: 0 0 15px rgba(74, 242, 74, 1); }
        }
    </style>
    <!-- 安定して動作する同一バージョンの Three.js と OrbitControls を読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
</head>
<body class="relative w-full h-full">

    <div class="scanlines"></div>

    <!-- 万が一のエラーを画面に表示するためのデバッグ用コンソール (初期状態は隠し) -->
    <div id="debug-log" class="absolute top-24 left-4 z-50 bg-red-950/90 border border-red-500 text-red-200 p-3 rounded font-mono text-xs max-w-sm hidden">
        <div class="font-bold border-b border-red-500 pb-1 mb-1 text-red-400"><i class="fa-solid fa-triangle-exclamation"></i> SYSTEM ERROR LOG:</div>
        <p id="debug-text" class="break-all"></p>
    </div>

    <!-- UIトグル用ボタン (右上・タッチしやすい適切なパディングの極小ボタン z-30) -->
    <div class="absolute top-3 right-3 md:top-4 right-4 z-30 pointer-events-auto">
        <button id="btn-toggle-hud" class="bg-black/90 hover:bg-green-950/80 border border-green-500/50 text-green-400 py-1.5 px-2.5 rounded text-[10px] font-bold flex items-center gap-1.5 shadow-[0_0_10px_rgba(74,242,74,0.2)] transition-all duration-200 active:scale-95">
            <i class="fa-solid fa-eye-slash text-[11px]" id="hud-icon"></i>
            <span id="hud-text">UI非表示</span>
        </button>
    </div>

    <!-- UI Overlay (メインコントロール群 z-20) -->
    <div class="absolute inset-0 z-20 pointer-events-none flex flex-col justify-between p-3 md:p-4">
        
        <!-- TOP NAV: Status Monitor (高さを抑えてスリム化、右上ボタンとの干渉を避ける右パディング) -->
        <div id="panel-top" class="w-full flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 bg-black/75 border border-green-900/50 p-2 md:p-2.5 rounded backdrop-blur-md pointer-events-auto transition-all duration-500 ease-in-out opacity-100 pr-24">
            <div>
                <div class="flex items-center gap-2">
                    <span class="relative flex h-2 w-2">
                        <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"></span>
                        <span class="relative inline-flex rounded-full h-2 w-2 bg-red-600"></span>
                    </span>
                    <h1 class="text-sm md:text-lg font-bold text-green-400 tracking-wider glitch-text">
                        G-CELL HYBRID: <span class="text-orange-500">BIOLLANTE</span>
                    </h1>
                </div>
                <p class="text-[8px] md:text-[9px] text-green-600/70">
                    SUBJECT ID: #1989-AR2 | CODENAME: 植獣形態 (PLANT BEAST)
                </p>
            </div>
            <div class="flex gap-2.5 text-[9px] md:text-xs text-green-500 font-mono">
                <div class="border-l border-green-800/80 pl-2">
                    <span class="text-green-700 block text-[7px] md:text-[8px]">CORE TEMPERATURE</span>
                    <span id="core-status" class="font-bold text-orange-400">1200°C CRITICAL</span>
                </div>
                <div class="border-l border-green-800/80 pl-2">
                    <span class="text-green-700 block text-[7px] md:text-[8px]">TENTACLE FORESTS</span>
                    <span id="tentacle-status" class="font-bold text-red-500">8 VINES ACTIVE</span>
                </div>
                <div class="border-l border-green-800/80 pl-2 hidden lg:block">
                    <span class="text-green-700 block text-[8px]">GENOME ANALYSIS</span>
                    <span class="text-cyan-400">G-Cells 50% / Rose 30%</span>
                </div>
            </div>
        </div>

        <!-- CENTER: Custom notifications -->
        <div class="self-center flex flex-col items-center justify-center">
            <div id="action-alert" class="hidden bg-red-950/90 border border-red-500 text-red-400 px-4 py-1.5 rounded font-bold text-xs md:text-sm tracking-widest uppercase animate-bounce shadow-[0_0_10px_rgba(239,68,68,0.4)]">
                WARNING: ACID SPITTING ACTIVE!
            </div>
        </div>

        <!-- BOTTOM CONTROLS & MONITOR (スマホではタブ切り替え、PCでは3カラム並列) -->
        <div id="panel-bottom" class="w-full flex flex-col gap-2 transition-all duration-500 ease-in-out opacity-100 pointer-events-auto">
            
            <!-- Mobile-only Tabs Navigation (スマホの時だけ出現するスリムなタブボタン群) -->
            <div class="flex md:hidden w-full bg-black/90 border border-green-950 p-0.5 rounded gap-0.5">
                <button id="tab-btn-bio" class="flex-1 text-center py-1.5 rounded text-[10px] font-bold transition-all bg-green-950/60 text-green-400 border border-green-500/30">
                    <i class="fa-solid fa-flask mr-1"></i>制御 (CTRL)
                </button>
                <button id="tab-btn-light" class="flex-1 text-center py-1.5 rounded text-[10px] font-bold transition-all bg-black/40 text-gray-500">
                    <i class="fa-solid fa-sliders mr-1"></i>環境 (LIGHT)
                </button>
                <button id="tab-btn-telemetry" class="flex-1 text-center py-1.5 rounded text-[10px] font-bold transition-all bg-black/40 text-gray-500">
                    <i class="fa-solid fa-chart-line mr-1"></i>生体 (DATA)
                </button>
            </div>

            <!-- Panels Wrapper (スマホでは表示/非表示を動的制御、PC(md:)では横並び) -->
            <div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-end">
                
                <!-- 1. LEFT PANEL: Bio-commands -->
                <div id="panel-bio" class="bg-black/80 border border-green-950 p-2 md:p-2.5 rounded flex flex-col gap-2 shadow-[0_0_15px_rgba(0,0,0,0.5)]">
                    <span class="text-green-400 font-bold text-[9px] md:text-[10px] border-b border-green-900/40 pb-1 flex items-center gap-1">
                        <i class="fa-solid fa-flask"></i> BIO-CHEMICAL CONTROL
                    </span>
                    <div class="grid grid-cols-2 gap-2">
                        <button id="btn-spit" class="bg-emerald-950/80 border border-emerald-500/60 text-emerald-400 hover:bg-emerald-900/80 active:scale-95 transition-all py-2 px-2 rounded text-[10px] md:text-[11px] font-bold flex items-center justify-center gap-1.5">
                            <i class="fa-solid fa-skull-crossbones text-xs"></i>
                            <span>酸性樹液 (Acid Spit)</span>
                        </button>
                        <button id="btn-roar" class="bg-red-950/80 border border-red-500/60 text-red-400 hover:bg-red-900/80 active:scale-95 transition-all py-2 px-2 rounded text-[10px] md:text-[11px] font-bold flex items-center justify-center gap-1.5">
                            <i class="fa-solid fa-volume-high text-xs"></i>
                            <span>大咆哮 (Roar)</span>
                        </button>
                    </div>
                    <button id="btn-combat" class="bg-orange-950/80 border border-orange-500/60 text-orange-400 hover:bg-orange-900/80 active:scale-95 transition-all py-1.5 px-2 rounded text-[9px] md:text-[10px] font-bold flex items-center justify-center gap-1.5">
                        <i class="fa-solid fa-bolt"></i>
                        <span>戦闘モード切替 (Combat Mode)</span>
                    </button>
                </div>

                <!-- 2. MIDDLE PANEL: Environmental Chamber Config -->
                <div id="panel-light" class="hidden md:flex bg-black/80 border border-green-950 p-2 md:p-2.5 rounded flex-col gap-2 shadow-[0_0_15px_rgba(0,0,0,0.5)]">
                    <span class="text-green-400 font-bold text-[9px] md:text-[10px] border-b border-green-900/40 pb-1 flex items-center gap-1">
                        <i class="fa-solid fa-sliders"></i> CHAMBER LIGHTING
                    </span>
                    <div class="flex flex-col gap-1">
                        <span class="text-[8px] md:text-[9px] text-green-600/80 font-mono">CHAMBER ENVIRONMENT PRESET</span>
                        <div class="grid grid-cols-3 gap-1">
                            <button id="env-swamp" class="bg-green-900/40 text-green-300 border border-green-500/50 text-[9px] md:text-[10px] py-1 px-1 rounded hover:bg-green-900/80 transition-all font-mono active">
                                SWAMP
                            </button>
                            <button id="env-alert" class="bg-black/50 text-red-500/60 border border-red-950 text-[9px] md:text-[10px] py-1 px-1 rounded hover:bg-red-950/50 transition-all font-mono">
                                ALERT
                            </button>
                            <button id="env-glow" class="bg-black/50 text-amber-500/60 border border-amber-950 text-[9px] md:text-[10px] py-1 px-1 rounded hover:bg-amber-950/50 transition-all font-mono">
                                GLOW
                            </button>
                        </div>
                    </div>
                    <div class="flex justify-between text-[8px] md:text-[9px] text-green-600/70 font-mono">
                        <span>Cam Dist: <span id="val-dist">0.00</span>m</span>
                        <span>Fog: Adaptive auto</span>
                    </div>
                </div>

                <!-- 3. RIGHT PANEL: Biotech Telemetry readout -->
                <div id="panel-telemetry" class="hidden md:flex bg-black/80 border border-green-950 p-2 md:p-2.5 rounded flex-col justify-between shadow-[0_0_15px_rgba(0,0,0,0.5)] text-green-500 font-mono">
                    <div>
                        <span class="text-green-400 font-bold text-[9px] md:text-[10px] border-b border-green-900/40 pb-1 block mb-1">
                            <i class="fa-solid fa-chart-line"></i> BIOMETRIC READOUT
                        </span>
                        <div class="grid grid-cols-2 gap-y-0.5 gap-x-2 text-[8px] md:text-[9px]">
                            <span class="text-green-700">TEMPERATURE:</span><span id="tel-temp">36.8 °C</span>
                            <span class="text-green-700">BIO-EMISSION:</span><span id="tel-em">284.1 mSv/h</span>
                            <span class="text-green-700">REGENERATION:</span><span class="text-green-400 animate-pulse">MAXIMUM</span>
                            <span class="text-green-700">THREAT LEVEL:</span><span class="text-red-500 font-bold">CRITICAL</span>
                        </div>
                    </div>
                    <div class="mt-1 border-t border-green-950/60 pt-1 text-[8px] text-green-600/80 flex justify-between">
                        <span>ROTATION: <span id="tel-rot" class="text-green-400">AUTO</span></span>
                        <span>DRAG / SCROLL TO ZOOM</span>
                    </div>
                </div>

            </div>

        </div>

    </div>

    <!-- MAIN THREE.JS CANVAS CONTAINER (背面に絶対配置 z-0) -->
    <div id="canvas-container" class="absolute inset-0 w-full h-full z-0"></div>

    <script>
        // エラーハンドラー
        window.onerror = function(message, source, lineno, colno, error) {
            const debugPanel = document.getElementById('debug-log');
            const debugText = document.getElementById('debug-text');
            debugPanel.classList.remove('hidden');
            debugText.innerText = `${message}\nLine: ${lineno}:${colno}`;
            console.error(error);
            return false;
        };

        // Global variables & state
        let scene, camera, renderer, controls;
        let biollanteGroup;
        let coreMesh, coreLight, ambientLight, spotLight, floor;
        let tentacles = [];
        let acidParticles = [];
        let slimeParticles = []; // ★ 滴り落ちる生々しい粘液パーティクル用
        let sporeParticles;
        let mouthUpper, mouthLower;
        let sideMouths = []; 
        let webL, webR; // ★ 伸縮追従する口角肉膜用
        let chestWallL, chestWallR, centerMound; // 体のアニメーション連動用
        let isCombatMode = false;
        let isSpitting = false;
        let spitTimer = 0; // ★ 口の開閉時間を確実に制御する自立タイマー
        let isRoaring = false;
        let roarTimer = 0;
        let isHUDVisible = true; // HUD表示フラグ

        // 劇中スチルの空気感を完全に再現するライティング&フォグ環境設定
        const environments = {
            swamp: {
                bg: 0x000602,          // 真夜中の静まり返った闇
                ambient: 0x0a140a,     // 極限まで抑えた環境光で、明暗のコントラストを引き立てる
                spot: 0x66ff66,        
                spotIntensity: 2.8,    // 上部からのサーチライト風月光
                coreColor: 0xff4500,   // 黄金色〜オレンジに燃え盛る溶岩コア
                coreLightColor: 0xffaa00,
                fogNear: 10,           
                fogFar: 36             
            },
            alert: {
                bg: 0x0a0101,
                ambient: 0x240a0a,     
                spot: 0xff0000,
                spotIntensity: 3.5,
                coreColor: 0xff2200,
                coreLightColor: 0xff5500,
                fogNear: 10,
                fogFar: 36
            },
            glow: {
                bg: 0x01080d,
                ambient: 0x0a2024,
                spot: 0x00ffcc,
                spotIntensity: 2.5,
                coreColor: 0xff8800,
                coreLightColor: 0xff3300,
                fogNear: 10,
                fogFar: 36
            }
        };
        let currentEnv = 'swamp';

        // システム通知表示
        function showAlert(msg, duration = 2000) {
            const el = document.getElementById('action-alert');
            el.innerText = msg;
            el.classList.remove('hidden');
            setTimeout(() => {
                el.classList.add('hidden');
            }, duration);
        }

        // アプリケーション初期化
        function init() {
            const container = document.getElementById('canvas-container');

            const width = container.clientWidth || window.innerWidth;
            const height = container.clientHeight || window.innerHeight;

            // 1. Scene setup
            scene = new THREE.Scene();
            const config = environments[currentEnv];
            scene.background = new THREE.Color(config.bg);
            scene.fog = new THREE.Fog(config.bg, config.fogNear, config.fogFar);

            // 2. Camera setup (劇中スチルのド迫力のアングルを意識)
            camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000);
            camera.position.set(-6.5, 3.8, 11.5); 

            // 3. Renderer setup (モバイルデバイス向けにPixelRatioの天井値を1.5に抑えて動作を劇的に最適化)
            renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
            renderer.setSize(width, height);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            container.appendChild(renderer.domElement);

            // 4. OrbitControls setup
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.maxPolarAngle = Math.PI / 2 - 0.05; 
            controls.minDistance = 3.5;
            controls.maxDistance = 20;
            controls.target.set(0, 1.3, 0.5); // ビオランテの咆哮する顔を常に見つめる
            
            // 自動回転を設定
            controls.autoRotate = true;
            controls.autoRotateSpeed = 0.35; // 劇中のスケール感を出すため非常に重厚にゆっくり回転

            // 5. Lighting Setup (劇中の妖艶なエッジライティング&ナイトシーンを形成)
            ambientLight = new THREE.AmbientLight(config.ambient, 1.2);
            scene.add(ambientLight);

            // メインスポットライト(上部から照らし、濡れた皮膚に美しいハイライトを走らせる)
            spotLight = new THREE.SpotLight(config.spot, config.spotIntensity);
            spotLight.position.set(4, 18, 4);
            spotLight.angle = Math.PI / 4.2;
            spotLight.penumbra = 0.7;
            spotLight.castShadow = true;
            spotLight.shadow.mapSize.width = 1024;
            spotLight.shadow.mapSize.height = 1024;
            spotLight.shadow.bias = -0.001;
            scene.add(spotLight);

            // 劇中特有の超強力な「グリーン/シアンの逆光エッジライト」
            const rimLight = new THREE.DirectionalLight(0x22ee44, 3.2); // インテンシティを極限まで強化
            rimLight.position.set(8, 6, -10);
            scene.add(rimLight);

            // 前面補助の寒色系ローライト(ディテールを完全に潰さないための暗視風ライト)
            const fillLight = new THREE.DirectionalLight(0x1a2e3a, 1.1);
            fillLight.position.set(-8, 3, 8);
            scene.add(fillLight);

            // 6. ビオランテモデル構築
            buildBiollante();

            // 7. 胞子エフェクト
            buildSpores();

            // 8. 湿地の地面
            buildFloor();

            // コントロールリスナー登録
            setupControls();

            // 画面リサイズ対応
            window.addEventListener('resize', onWindowResize);
            
            // 初回強制サイズチェック
            onWindowResize();

            // アニメーションループ開始 (自律高精度タイマーを使用するため、requestAnimationFrameを呼びます)
            requestAnimationFrame(animate);
        }

        // ゴツゴツした不規則な形状を高精度で生成する関数
        function createOrganicGeometry(radius, detail, noiseScale = 0.16) {
            const geo = new THREE.DodecahedronGeometry(radius, detail);
            const pos = geo.attributes.position;
            // 頂点ノイズを何重にも適用して、劇中の生々しい樹皮と肉腫の起伏を作る
            for (let i = 0; i < pos.count; i++) {
                const x = pos.getX(i);
                const y = pos.getY(i);
                const z = pos.getZ(i);

                const noise = (Math.sin(x*3) * Math.cos(y*3) * Math.sin(z*3) + 
                               Math.sin(x*8) * 0.4 +
                               Math.cos(y*12) * 0.15) * noiseScale;
                pos.setXYZ(i, x + noise, y + noise, z + noise);
            }
            geo.computeVertexNormals();
            return geo;
        }

        // ★ 高密度Sphereベース:下部ほどラッパ状に広がる、ずっしりと山型(円錐台)ボディを作る特殊幾何学生成
        function createMountainGeometry(radius, widthSeg, heightSeg, noiseScale = 0.16) {
            const geo = new THREE.SphereGeometry(radius, widthSeg, heightSeg);
            const pos = geo.attributes.position;
            
            for (let i = 0; i < pos.count; i++) {
                let x = pos.getX(i);
                let y = pos.getY(i);
                let z = pos.getZ(i);

                // 地面に近づく(Yが低くなる)ほど、外周への広がりを大きくして「丸み」を「末広がりの山」に変える
                const heightFactor = (radius - y) / (radius * 2); // 0 から 1.0
                const spread = 1.0 + heightFactor * 1.6;          // 最下部を約2.6倍に広げる
                x *= spread;
                z *= spread;

                // 樹木のような縦のうねり(幹テクスチャ)をサイン波でシミュレート
                const noise = (Math.sin(x * 1.5) * Math.cos(y * 1.5) * Math.sin(z * 1.5) + 
                               Math.sin(y * 4.0) * 0.35 + 
                               Math.cos(x * 6.0) * 0.1) * noiseScale;

                pos.setXYZ(i, x + noise, y + noise, z + noise);
            }
            geo.computeVertexNormals();
            return geo;
        }

        // ビオランテモデルの構築 (劇中スチルの完全再現)
        function buildBiollante() {
            // 地面に埋没しすぎず、湖面から美しく生えるように、グループ自体を少しY方向に引き上げ
            biollanteGroup = new THREE.Group();
            biollanteGroup.position.set(0, 0.22, 0); 
            scene.add(biollanteGroup);

            // ★ 劇中スチルの「濡れた漆黒の肉体」を完璧にシミュレートするマテリアル設定(荒さをさらに下げてテカリを強調)
            const wetSkinMat = new THREE.MeshStandardMaterial({
                color: 0x080e07,      // 極めて深い漆黒オリーブグリーン
                roughness: 0.25,      // 粘液をまとい、濡れ光る質感を高めるために粗さをさらに低減
                metalness: 0.35,      // サーチライトをヌラヌラと反射させる光沢
                flatShading: true     // 面の角を残してゴツゴツ感を強調
            });

            const vineMat = new THREE.MeshStandardMaterial({
                color: 0x0a160b,
                roughness: 0.30,      // 触手も粘液に濡れた質感に変更
                metalness: 0.30,
                flatShading: true
            });

            // 喉奥の共通赤肉マテリアル (より湿った濡れ質感に)
            const throatMat = new THREE.MeshStandardMaterial({ 
                color: 0x6e0606,
                roughness: 0.2,
                flatShading: true
            });

            // ★ 顎開閉時の隙間を埋める「口角の肉膜ウェブ」用マテリアル
            const webMat = new THREE.MeshStandardMaterial({ 
                color: 0x3d0303, // 深い血の色
                roughness: 0.3, 
                flatShading: true 
            });

            // ★ 歯・牙用マテリアル (ReferenceErrorを防ぐため、一番初めに定義を統一します)
            const toothMat = new THREE.MeshStandardMaterial({
                color: 0xe5dfd0, 
                roughness: 0.3,
                flatShading: true
            });

            // A. 本体(createMountainGeometryによって破綻のない美しい火山型シルエットを構築)
            const bodyRoot = new THREE.Group();
            biollanteGroup.add(bodyRoot);

            // 細分化された高密度Sphereを用いて、美しくゴツゴツした「植物の山」を形成。
            const centerMoundGeo = createMountainGeometry(2.3, 32, 24, 0.25);
            centerMound = new THREE.Mesh(centerMoundGeo, wetSkinMat);
            centerMound.scale.set(1.4, 0.65, 1.4); 
            centerMound.position.set(0, 0.45, -0.4); 
            centerMound.castShadow = true;
            centerMound.receiveShadow = true;
            bodyRoot.add(centerMound);

            // ★ 劇中スチルのような「心臓を抱く胸 of 割れ目」を演出する肉厚な胸壁セグメント(左右)
            chestWallL = new THREE.Mesh(createOrganicGeometry(0.9, 1, 0.1), wetSkinMat);
            chestWallL.scale.set(1.0, 1.4, 1.8);
            chestWallL.position.set(0.9, 0.5, 0.9);
            chestWallL.rotation.y = -Math.PI / 4;
            bodyRoot.add(chestWallL);

            chestWallR = chestWallL.clone();
            chestWallR.position.x = -0.9;
            chestWallR.rotation.y = Math.PI / 4;
            bodyRoot.add(chestWallR);

            // 地面に頑強に広がり這う巨大な根コブ
            const tuberCount = 8;
            for(let i=0; i < tuberCount; i++) {
                const tuber = new THREE.Mesh(createOrganicGeometry(1.2, 1, 0.14), wetSkinMat);
                const angle = (i / tuberCount) * Math.PI * 2;
                const dist = 2.3 + Math.random() * 0.4;
                tuber.position.set(
                    Math.cos(angle) * dist, 
                    -0.5 + Math.random() * 0.08, 
                    Math.sin(angle) * dist
                );
                tuber.scale.set(1.6 + Math.random()*0.4, 0.42, 1.3 + Math.random()*0.3);
                tuber.rotation.set(Math.random() * 0.3, Math.random() * 6.2, Math.random() * 0.3);
                tuber.castShadow = true;
                tuber.receiveShadow = true;
                bodyRoot.add(tuber);

                // 土台コブの棘
                if (Math.random() > 0.3) {
                    const spike = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.45, 4), wetSkinMat);
                    spike.position.set(tuber.position.x * 1.1, 0.1, tuber.position.z * 1.1);
                    spike.rotation.set(Math.random()*0.5, Math.random()*6, Math.random()*0.5);
                    bodyRoot.add(spike);
                }
            }

            // A-1. 首
            const neckGroup = new THREE.Group();
            biollanteGroup.add(neckGroup);
            
            const neckSegmentCount = 7;
            for (let i = 0; i < neckSegmentCount; i++) {
                const t = i / (neckSegmentCount - 1);
                const neckSeg = new THREE.Mesh(createOrganicGeometry(1.35 - t * 0.45, 1, 0.08), wetSkinMat);
                neckSeg.position.set(0, 0.55 + t * 1.45, t * 1.3);
                neckSeg.scale.set(1.25, 0.95, 1.25);
                neckSeg.castShadow = true;
                neckSeg.receiveShadow = true;
                neckGroup.add(neckSeg);

                // 首の棘
                if (i > 1 && i < 6) {
                    const spikeL = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.5, 4), wetSkinMat);
                    spikeL.position.set(0.8 - t * 0.2, 0.55 + t * 1.45, t * 1.3 + 0.2);
                    spikeL.rotation.set(0, -Math.PI/6, Math.PI/3); 
                    neckGroup.add(spikeL);

                    const spikeR = spikeL.clone();
                    spikeR.position.x = -spikeL.position.x;
                    spikeR.rotation.z = -Math.PI/3;
                    neckGroup.add(spikeR);
                }
            }

            // A-2. 首回りの「襟巻き状の外殻装甲」
            const collarGroup = new THREE.Group();
            collarGroup.position.set(0, 1.25, 0.5);
            bodyRoot.add(collarGroup);

            const collarParts = 8;
            for(let i = 0; i < collarParts; i++) {
                const scale = 0.85 + Math.random() * 0.45;
                const part = new THREE.Mesh(createOrganicGeometry(0.8, 1, 0.12), wetSkinMat);
                const angle = (i / (collarParts - 1)) * Math.PI - Math.PI / 2; 
                part.position.set(
                    Math.sin(angle) * 1.45,
                    0.25 + (Math.random() * 0.25),
                    -Math.cos(angle) * 1.25 - 0.25
                );
                part.scale.set(scale * 1.3, scale * 0.75, scale * 1.0);
                part.rotation.set(Math.random()*0.4, Math.random()*3.1, Math.random()*0.4);
                collarGroup.add(part);
            }

            // A-3. 背骨の多重装甲びれ
            const spineGroup = new THREE.Group();
            spineGroup.position.set(0, 1.6, -0.6); 
            bodyRoot.add(spineGroup);

            const spineCount = 9;
            for(let i = 0; i < spineCount; i++) {
                const hFactor = 1.0 - (i / spineCount) * 0.5; 
                const spinePart = new THREE.Mesh(createOrganicGeometry(0.4, 1, 0.15), wetSkinMat);
                spinePart.scale.set(1.5 * hFactor, 1.2 * hFactor, 0.3 * hFactor); 
                
                spinePart.position.set(
                    (Math.random() - 0.5) * 0.25,
                    (i * 0.42),
                    -i * 0.26 + 0.25
                );
                spinePart.rotation.set(
                    -Math.PI / 4 - (Math.random() * 0.1), 
                    (Math.random() - 0.5) * 0.3,
                    (Math.random() - 0.5) * 0.25
                );
                spinePart.castShadow = true;
                spineGroup.add(spinePart);
            }


            // B. 胸の奥深くに脈動する「心臓コア」
            const coreGroup = new THREE.Group();
            coreGroup.position.set(0, 0.48, 1.45); 
            bodyRoot.add(coreGroup);

            const coreGeo = new THREE.SphereGeometry(0.85, 20, 20);
            const coreConfig = environments[currentEnv];
            
            const coreMat = new THREE.MeshStandardMaterial({
                color: coreConfig.coreColor,
                emissive: coreConfig.coreColor,
                emissiveIntensity: 3.8, 
                roughness: 0.1,
                transparent: true,
                opacity: 0.98
            });
            coreMesh = new THREE.Mesh(coreGeo, coreMat);
            coreMesh.scale.set(1.1, 0.82, 1.25); 
            coreGroup.add(coreMesh);

            coreLight = new THREE.PointLight(coreConfig.coreLightColor, 9, 16);
            coreGroup.add(coreLight);

            // B-1. コアを縛る極細根(48本)
            const rootCage = new THREE.Group();
            coreGroup.add(rootCage);

            const rootCount = 48;
            const rootMat = new THREE.MeshStandardMaterial({ color: 0x050a05, roughness: 1.0, flatShading: true });
            for(let i = 0; i < rootCount; i++) {
                const rootGeo = new THREE.CylinderGeometry(0.016, 0.035, 2.1, 4);
                const root = new THREE.Mesh(rootGeo, rootMat);
                
                const angle = (i / rootCount) * Math.PI * 2;
                root.position.set(
                    Math.cos(angle) * 0.88,
                    (Math.random() - 0.5) * 0.4,
                    Math.sin(angle) * 0.45 + 0.12
                );
                root.rotation.set(
                    Math.PI / 4.5 + (Math.random() - 0.5) * 1.1,
                    angle + Math.PI / 2,
                    (Math.random() - 0.5) * 0.7
                );
                rootCage.add(root);
            }


            // C. ★ 鰐口(わにぐち)状の巨大なメイン頭部
            const headRoot = new THREE.Group();
            headRoot.position.set(0, 1.95, 1.45); 
            biollanteGroup.add(headRoot);

            // ★ 伸縮する口角肉膜ウェブ(顎関節ピボットに完全にアライメント調整)
            webL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.65, 0.95), webMat);
            webL.position.set(0.48, 0.0, 0.75); 
            webL.rotation.y = Math.PI / 10;
            headRoot.add(webL);

            webR = webL.clone();
            webR.position.x = -0.48;
            webR.rotation.y = -Math.PI / 10;
            headRoot.add(webR);


            // 1. ★ 上顎 (Upper Jaw)
            // 回転ピボットを顎の根本(関節後端 Z:0)に設定
            mouthUpper = new THREE.Group();
            mouthUpper.position.set(0, 0.2, 0.4); // 後部関節位置アライメント
            headRoot.add(mouthUpper);

            // 長く裂けた獰猛な頭蓋
            const upperSkull = new THREE.Mesh(createOrganicGeometry(0.76, 1, 0.1), wetSkinMat);
            upperSkull.scale.set(1.15, 0.65, 2.75); 
            upperSkull.position.set(0, 0.25, 1.85); // 根本ピボット位置にスライド
            mouthUpper.add(upperSkull);

            // 鼻筋のデコボコ突起 (ピボットシフトに合わせてZ位置を調整)
            const snoutBump = new THREE.Mesh(createOrganicGeometry(0.34, 0, 0.08), wetSkinMat);
            snoutBump.scale.set(1.0, 0.5, 1.55);
            snoutBump.position.set(0, 0.57, 2.8);
            mouthUpper.add(snoutBump);

            // 瞳 (ピボットシフトに連動)
            const eyeGeo = new THREE.SphereGeometry(0.09, 8, 8);
            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffdd00 });
            const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
            eyeL.position.set(0.62, 0.32, 1.35);
            const eyeR = eyeL.clone();
            eyeR.position.x = -0.62;
            mouthUpper.add(eyeL);
            mouthUpper.add(eyeR);

            // 上顎の牙:多重配列構造 (Zアライメント調整)
            const upperToothCount = 24; 
            for(let i=0; i < upperToothCount; i++) {
                const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.32, 4), toothMat);
                tooth.rotation.x = Math.PI; 
                const t = (i / (upperToothCount - 1)); 
                const angle = t * Math.PI - Math.PI/2; 
                const doubleLayer = (i % 2 === 0) ? 1.0 : 0.92;
                
                const zPos = 0.85 + Math.cos(angle * 0.75) * 2.05 * doubleLayer + 1.0; 
                const xPos = Math.sin(angle * 1.1) * 0.46 * doubleLayer;
                
                tooth.position.set(xPos, -0.02, zPos);
                tooth.rotation.set(
                    -Math.PI + (Math.random() - 0.5) * 0.3,
                    (Math.random() - 0.5) * 0.2,
                    -xPos * 0.7 + (Math.random() - 0.5) * 0.2
                );
                mouthUpper.add(tooth);
            }


            // 2. ★ 下顎 (Lower Jaw)
            // 開閉時のめり込みを防ぐため、ピボット位置を十分に下方へ移動
            mouthLower = new THREE.Group();
            mouthLower.position.set(0, -0.2, 0.35); // 顎関節位置を低く最適配置
            headRoot.add(mouthLower);

            // ピボット関節(Z:0)に基づき、メッシュが不自然にせり上がるのを完全に解消
            const lowerSkull = new THREE.Mesh(createOrganicGeometry(0.66, 1, 0.1), wetSkinMat);
            lowerSkull.scale.set(0.98, 0.42, 2.5); 
            lowerSkull.position.set(0, -0.15, 1.7); // ピボットアライメント
            lowerSkull.castShadow = true;
            lowerSkull.receiveShadow = true;
            mouthLower.add(lowerSkull);

            // 下顎の多重牙 (アライメント修正)
            const lowerToothCount = 24;
            for(let i=0; i < lowerToothCount; i++) {
                const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.28, 4), toothMat);
                const t = (i / (lowerToothCount - 1));
                const angle = t * Math.PI - Math.PI/2;
                const doubleLayer = (i % 2 === 0) ? 1.0 : 0.94;
                
                const zPos = 0.85 + Math.cos(angle * 0.75) * 2.0 * doubleLayer + 1.05;
                const xPos = Math.sin(angle * 1.1) * 0.42 * doubleLayer;
                
                tooth.position.set(xPos, 0.04, zPos);
                tooth.rotation.set(
                    (Math.random() - 0.5) * 0.3,
                    (Math.random() - 0.5) * 0.2,
                    xPos * 0.7 + (Math.random() - 0.5) * 0.2
                );
                mouthLower.add(tooth);
            }

            // ★ 左右の巨大犬歯(大犬歯)の咬合めり込み防止アライメント
            const bigToothL = new THREE.Mesh(new THREE.ConeGeometry(0.11, 0.65, 5), toothMat);
            bigToothL.position.set(0.62, 0.05, 1.4); 
            bigToothL.rotation.set(-Math.PI / 5.5, 0.15, Math.PI / 3.6); 
            mouthLower.add(bigToothL);

            const bigToothR = bigToothL.clone();
            bigToothR.position.x = -0.62;
            bigToothR.rotation.y = -0.15;
            bigToothR.rotation.z = -Math.PI / 3.6;
            mouthLower.add(bigToothR);

            // 喉の奥
            const throatGeo = new THREE.CylinderGeometry(0.32, 0.42, 1.8, 6);
            throatGeo.rotateX(Math.PI / 2);
            const throat = new THREE.Mesh(throatGeo, throatMat); 
            throat.position.set(0, 0.05, 0.25);
            headRoot.add(throat);


            // D. ★ うねり蠢く極太の蔦触手(8本)
            const numTentacles = 8;
            for (let i = 0; i < numTentacles; i++) {
                const tGroup = new THREE.Group();
                
                const angle = (i / numTentacles) * Math.PI * 2 + Math.PI / 8;
                
                const isOuter = (i % 2 === 1);
                const rad = isOuter ? 2.95 : 2.15;
                const posY = isOuter ? -0.4 : -0.1;
                
                tGroup.position.set(Math.cos(angle) * rad, posY, Math.sin(angle) * rad);
                tGroup.rotation.y = -angle + Math.PI / 2;

                let parentPart = tGroup;
                const segments = [];
                const segCount = 9; 
                
                for (let j = 0; j < segCount; j++) {
                    const thick = 0.23 * (1 - (j / (segCount + 1))); 
                    const length = 0.8;
                    
                    const segGeo = new THREE.CylinderGeometry(thick * 0.75, thick, length, 6);
                    segGeo.rotateX(Math.PI / 2);      
                    segGeo.translate(0, 0, length / 2); 
                    
                    const seg = new THREE.Mesh(segGeo, vineMat);
                    seg.castShadow = true;
                    seg.receiveShadow = true;
                    
                    parentPart.add(seg);
                    segments.push(seg);
                    
                    const nextAnchor = new THREE.Group();
                    nextAnchor.position.set(0, 0, length);
                    seg.add(nextAnchor);
                    parentPart = nextAnchor;
                }

                // 小頭部
                const headScale = 0.55;
                const smGroup = new THREE.Group();
                smGroup.scale.set(headScale, headScale, headScale);
                parentPart.add(smGroup);

                const smUpper = new THREE.Mesh(createOrganicGeometry(0.55, 0, 0.08), wetSkinMat);
                smUpper.scale.set(1, 0.55, 2.2);
                smUpper.position.set(0, 0.15, 0.5);
                smGroup.add(smUpper);

                const smLower = new THREE.Mesh(createOrganicGeometry(0.53, 0, 0.08), wetSkinMat);
                smLower.scale.set(0.95, 0.5, 2.1);
                smLower.position.set(0, -0.15, 0.5);
                smGroup.add(smLower);

                const smThroat = new THREE.Mesh(new THREE.SphereGeometry(0.25, 4, 4), throatMat);
                smThroat.position.set(0, 0, 0.2);
                smGroup.add(smThroat);

                const smToothGeo = new THREE.ConeGeometry(0.04, 0.22, 4);
                for(let t=0; t<8; t++) {
                    const tooth = new THREE.Mesh(smToothGeo, toothMat);
                    const tAngle = (t / 7) * Math.PI - Math.PI/2;
                    tooth.position.set(Math.sin(tAngle)*0.25, 0.08, 0.4 + Math.cos(tAngle)*0.8);
                    tooth.rotation.x = Math.PI; 
                    smUpper.add(tooth);

                    const toothLow = tooth.clone();
                    toothLow.rotation.x = 0; 
                    toothLow.position.y = 0.08;
                    smLower.add(toothLow);
                }

                sideMouths.push({
                    upper: smUpper,
                    lower: smLower,
                    index: i
                });
                
                biollanteGroup.add(tGroup);
                tentacles.push({ 
                    group: tGroup, 
                    segments: segments, 
                    baseAngle: angle,
                    speedFactor: 0.8 + Math.random() * 0.5, 
                    isOuter: isOuter,
                    huntMode: false,      
                    huntTimer: Math.random() * 4,
                    targetYaw: 0,
                    targetPitch: 0
                });
            }
        }

        // 湖上に漂うバイオ胞子パーティクル
        function buildSpores() {
            const count = 300;
            const geo = new THREE.BufferGeometry();
            const pos = new Float32Array(count * 3);
            const sizes = new Float32Array(count);

            for(let i=0; i<count*3; i+=3) {
                pos[i] = (Math.random() - 0.5) * 35;     
                pos[i+1] = Math.random() * 12 - 1.5;       
                pos[i+2] = (Math.random() - 0.5) * 35;   
                sizes[i/3] = Math.random() * 0.15 + 0.05;
            }

            geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
            geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

            const pMat = new THREE.PointsMaterial({
                color: 0x4df313,
                size: 0.2,
                transparent: true,
                opacity: 0.75,
                blending: THREE.AdditiveBlending,
                depthWrite: false
            });

            sporeParticles = new THREE.Points(geo, pMat);
            scene.add(sporeParticles);
        }

        // 蘆ノ湖の水面
        function buildFloor() {
            const size = 150;
            const floorGeo = new THREE.PlaneGeometry(size, size, 40, 40);
            
            const pos = floorGeo.attributes.position;
            for(let i=0; i<pos.count; i++) {
                const x = pos.getX(i);
                const y = pos.getY(i);
                const wave = Math.sin(x*0.12) * Math.cos(y*0.12) * 0.22;
                pos.setZ(i, wave);
            }
            floorGeo.computeVertexNormals();

            const floorMat = new THREE.MeshStandardMaterial({
                color: 0x011404, 
                roughness: 0.2,   
                metalness: 0.85,
                flatShading: true
            });
            floor = new THREE.Mesh(floorGeo, floorMat);
            floor.rotation.x = -Math.PI / 2;
            floor.position.y = -1.4; 
            floor.receiveShadow = true;
            scene.add(floor);
        }

        // アシッド毒樹液放射
        function triggerAcidSpit() {
            if (isSpitting) return;
            isSpitting = true;
            spitTimer = 1.8; // ★1.8秒間大口を開けて放射し、タイマー終了後に確実に自動で閉じます
            showAlert("WARNING: BIO-ACID TOXIN VENTED!", 2500);

            const particleCount = 120;
            const acidGeo = new THREE.SphereGeometry(0.12, 5, 5);
            const acidMat = new THREE.MeshBasicMaterial({ 
                color: 0x39ff14, 
                transparent: true,
                opacity: 0.95
            });

            const baseDir = new THREE.Vector3(0, 0.15, 3.2); 
            baseDir.applyQuaternion(mouthUpper.parent.quaternion);
            baseDir.add(biollanteGroup.position);

            for (let i = 0; i < particleCount; i++) {
                const mesh = new THREE.Mesh(acidGeo, acidMat);
                mesh.position.set(
                    baseDir.x + (Math.random() - 0.5) * 0.3, 
                    baseDir.y + (Math.random() - 0.5) * 0.2, 
                    baseDir.z + (Math.random() - 0.5) * 0.3
                );
                
                const velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 4,
                    (Math.random() - 0.2) * 4 + 3.5, 
                    9 + Math.random() * 8
                );
                
                scene.add(mesh);
                acidParticles.push({
                    mesh: mesh,
                    vel: velocity,
                    life: 1.0 
                });
            }

            // 唾液スライム
            triggerSlimeDrop(30, new THREE.Vector3(0, 2.0, 2.85));
        }

        // 粘液滴(スライムドロップ)生成関数
        function triggerSlimeDrop(count, basePos) {
            const slimeGeo = new THREE.SphereGeometry(0.06, 4, 4);
            const slimeMat = new THREE.MeshStandardMaterial({
                color: 0x33aa11,
                roughness: 0.1,
                transparent: true,
                opacity: 0.8
            });

            for(let i=0; i<count; i++) {
                const mesh = new THREE.Mesh(slimeGeo, slimeMat);
                mesh.scale.set(0.6 + Math.random()*0.5, 1.5 + Math.random()*1.0, 0.6 + Math.random()*0.5); 
                mesh.position.copy(basePos).add(new THREE.Vector3(
                    (Math.random() - 0.5) * 0.8,
                    (Math.random() - 0.5) * 0.2,
                    (Math.random() - 0.5) * 0.8
                ));
                scene.add(mesh);
                
                slimeParticles.push({
                    mesh: mesh,
                    velY: -1.0 - Math.random()*2.0,
                    life: 1.0 + Math.random()*1.0
                });
            }
        }

        // 大咆哮
        function triggerRoar() {
            if (isRoaring) return;
            isRoaring = true;
            roarTimer = 0;
            showAlert("SEISMIC ROAR: DETECTING ULTRASONIC PULSES", 3000);
            
            coreLight.intensity = 35; 
            coreMesh.scale.set(1.4, 1.15, 1.6);

            // 口元から粘液落下
            triggerSlimeDrop(40, new THREE.Vector3(0, 2.0, 2.85));
        }

        // コントロール・HUDリスナー登録 (レスポンシブ&タブ切替ロジック追加)
        function setupControls() {
            const btnToggleHUD = document.getElementById('btn-toggle-hud');
            const panelTop = document.getElementById('panel-top');
            const panelBottom = document.getElementById('panel-bottom');
            const hudIcon = document.getElementById('hud-icon');
            const hudText = document.getElementById('hud-text');

            // --- モバイル専用タブ切り替えロジック ---
            const tabBtnBio = document.getElementById('tab-btn-bio');
            const tabBtnLight = document.getElementById('tab-btn-light');
            const tabBtnTelemetry = document.getElementById('tab-btn-telemetry');
            
            const panelBio = document.getElementById('panel-bio');
            const panelLight = document.getElementById('panel-light');
            const panelTelemetry = document.getElementById('panel-telemetry');

            function switchMobileTab(activeTab) {
                // すべてのタブのボタンデザインをリセット
                [tabBtnBio, tabBtnLight, tabBtnTelemetry].forEach(btn => {
                    btn.classList.remove('bg-green-950/60', 'text-green-400', 'border-green-500/30');
                    btn.classList.add('bg-black/40', 'text-gray-500', 'border-transparent');
                });
                // すべてのパネルを隠す (PC表示のときはTailwindの md:flex 等で強制横並びになります)
                [panelBio, panelLight, panelTelemetry].forEach(p => {
                    p.classList.add('hidden');
                });

                if (activeTab === 'bio') {
                    tabBtnBio.classList.remove('bg-black/40', 'text-gray-500', 'border-transparent');
                    tabBtnBio.classList.add('bg-green-950/60', 'text-green-400', 'border', 'border-green-500/30');
                    panelBio.classList.remove('hidden');
                } else if (activeTab === 'light') {
                    tabBtnLight.classList.remove('bg-black/40', 'text-gray-500', 'border-transparent');
                    tabBtnLight.classList.add('bg-green-950/60', 'text-green-400', 'border', 'border-green-500/30');
                    panelLight.classList.remove('hidden', 'md:hidden');
                    panelLight.classList.add('flex');
                } else if (activeTab === 'telemetry') {
                    tabBtnTelemetry.classList.remove('bg-black/40', 'text-gray-500', 'border-transparent');
                    tabBtnTelemetry.classList.add('bg-green-950/60', 'text-green-400', 'border', 'border-green-500/30');
                    panelTelemetry.classList.remove('hidden', 'md:hidden');
                    panelTelemetry.classList.add('flex');
                }
            }

            // モバイル用タブイベントハンドラー (clickとtouchstartの両方で軽快に応答)
            const handleTabClick = (tabKey) => (e) => {
                e.preventDefault();
                switchMobileTab(tabKey);
            };
            
            tabBtnBio.addEventListener('click', handleTabClick('bio'));
            tabBtnLight.addEventListener('click', handleTabClick('light'));
            tabBtnTelemetry.addEventListener('click', handleTabClick('telemetry'));

            // 最初はBIOパネルを表示
            switchMobileTab('bio');


            // --- HUD表示/非表示トグルの制御 ---
            btnToggleHUD.addEventListener('click', (e) => {
                e.preventDefault();
                isHUDVisible = !isHUDVisible;
                if (isHUDVisible) {
                    panelTop.classList.remove('opacity-0', 'pointer-events-none');
                    panelTop.classList.add('opacity-100', 'pointer-events-auto');
                    panelBottom.classList.remove('opacity-0', 'pointer-events-none');
                    panelBottom.classList.add('opacity-100', 'pointer-events-auto');
                    hudIcon.className = "fa-solid fa-eye-slash text-sm";
                    hudText.innerText = "UI非表示";
                    showAlert("SYSTEM HUD ACTIVATED", 1500);
                } else {
                    panelTop.classList.remove('opacity-100', 'pointer-events-auto');
                    panelTop.classList.add('opacity-0', 'pointer-events-none');
                    panelBottom.classList.remove('opacity-100', 'pointer-events-auto');
                    panelBottom.classList.add('opacity-0', 'pointer-events-none');
                    hudIcon.className = "fa-solid fa-eye text-sm";
                    hudText.innerText = "UI表示";
                    showAlert("CINEMATIC OBSERVER MODE ACTIVE", 2000);
                }
            });

            document.getElementById('btn-spit').addEventListener('click', (e) => {
                e.preventDefault();
                triggerAcidSpit();
            });

            document.getElementById('btn-roar').addEventListener('click', (e) => {
                e.preventDefault();
                triggerRoar();
            });

            document.getElementById('btn-combat').addEventListener('click', (e) => {
                e.preventDefault();
                isCombatMode = !isCombatMode;
                const modeStr = isCombatMode ? "THRASHING" : "SWAYING";
                document.getElementById('tentacle-status').innerText = isCombatMode ? "COMBAT / ACTIVE" : "8 VINES ACTIVE";
                document.getElementById('tentacle-status').className = isCombatMode ? "font-bold text-red-500 animate-pulse" : "font-bold text-green-500";
                
                const btn = document.getElementById('btn-combat');
                if (isCombatMode) {
                    btn.classList.replace('bg-orange-950/80', 'bg-red-800/90');
                    btn.classList.replace('border-orange-500/60', 'border-red-500/60');
                    showAlert("COMBAT MODE ACTIVE: TENSION AMPLIFIED", 2000);
                } else {
                    btn.classList.replace('bg-red-800/90', 'bg-orange-950/80');
                    btn.classList.replace('border-red-500/60', 'border-orange-500/60');
                    showAlert("STANDBY: DE-ESCALATING BIOMETRICS", 2000);
                }
            });

            const swapEv = (presetKey) => {
                currentEnv = presetKey;
                const cfg = environments[presetKey];
                
                scene.background.setHex(cfg.bg);
                scene.fog.color.setHex(cfg.bg);
                scene.fog.near = cfg.fogNear;
                scene.fog.far = cfg.fogFar;

                ambientLight.color.setHex(cfg.ambient);
                spotLight.color.setHex(cfg.spot);
                spotLight.intensity = cfg.spotIntensity;

                coreMesh.material.color.setHex(cfg.coreColor);
                coreMesh.material.emissive.setHex(cfg.coreColor);
                coreLight.color.setHex(cfg.coreLightColor);

                ['env-swamp', 'env-alert', 'env-glow'].forEach(id => {
                    const el = document.getElementById(id);
                    el.classList.remove('bg-green-900/40', 'bg-red-950/80', 'bg-amber-950/80', 'text-green-300', 'text-red-500', 'text-amber-500', 'border-green-500/50', 'border-red-500', 'border-amber-500');
                    el.classList.add('bg-black/50', 'text-gray-500', 'border-transparent');
                });

                const activeBtn = document.getElementById(`env-${presetKey}`);
                if (presetKey === 'swamp') {
                    activeBtn.className = "bg-green-900/40 text-green-300 border border-green-500/50 text-[10px] py-1 px-1.5 rounded hover:bg-green-900/80 transition-all font-mono";
                } else if (presetKey === 'alert') {
                    activeBtn.className = "bg-red-950/80 text-red-500 border border-red-500 text-[10px] py-1 px-1.5 rounded hover:bg-red-900/80 transition-all font-mono";
                } else if (presetKey === 'glow') {
                    activeBtn.className = "bg-amber-950/80 text-amber-500 border border-amber-500 text-[10px] py-1 px-1.5 rounded hover:bg-amber-950/50 transition-all font-mono";
                }
                
                showAlert("ENVIRONMENT RE-CONFIGURED", 1500);
            };

            document.getElementById('env-swamp').addEventListener('click', () => swapEv('swamp'));
            document.getElementById('env-alert').addEventListener('click', () => swapEv('alert'));
            document.getElementById('env-glow').addEventListener('click', () => swapEv('glow'));
        }

        // アニメーション&物理更新ループ (THREE.Clockに頼らない自律物理演算タイマーへ移行)
        let lastTime = 0;
        let totalTime = 0;

        function animate(timestamp) {
            requestAnimationFrame(animate);

            // タイムスタンプが未定義(初回の直接呼び出し等)の場合のフォールバック
            if (!timestamp) timestamp = performance.now();
            if (!lastTime) lastTime = timestamp;
            
            // タブが非アクティブから復帰した時の、過剰なdelta時間の蓄積(パーティクルの大暴走)を避けるために0.1秒でクランプします
            const delta = Math.min((timestamp - lastTime) / 1000, 0.1); 
            lastTime = timestamp;
            totalTime += delta;

            const time = totalTime; // 既存の time 変数に代入して互換性を維持

            // 1. 重量感ある呼吸(胸腔全体が吸気で膨らみ、呼気で縮み、巨体が前後に小さく揺らぐ)
            const breathingCycle = Math.sin(time * 0.8);
            biollanteGroup.rotation.y = breathingCycle * 0.015;
            biollanteGroup.position.y = 0.22 + breathingCycle * 0.035;

            // 首と胸の連動:呼吸に合わせて胸壁が動的な厚み変化をする
            const breathingScale = 1.0 + breathingCycle * 0.02;
            if (chestWallL && chestWallR) {
                chestWallL.scale.set(breathingScale, 1.4, 1.8 * breathingScale);
                chestWallR.scale.set(breathingScale, 1.4, 1.8 * breathingScale);
            }

            // 2. 心臓コアの生命心電拍動(QRS鼓動)
            const ecgSpeed = isCombatMode ? 6.5 : 2.8;
            const rawECG = Math.sin(time * ecgSpeed);
            const heartBeat = Math.pow(Math.max(0, rawECG), 8.0) * 0.12 + Math.pow(Math.max(0, Math.sin(time * ecgSpeed - 0.45)), 8.0) * 0.06;
            
            const pulseScale = 1.0 + heartBeat;
            
            if (!isRoaring) {
                coreMesh.scale.set(pulseScale * 1.12, pulseScale * 0.82, pulseScale * 1.25);
                coreLight.intensity = (isCombatMode ? 10.0 : 6.0) + heartBeat * 35.0; 
                
                const warmGold = new THREE.Color(0xffaa00);
                const deepRed = new THREE.Color(0x8a0303);
                coreMesh.material.emissive.copy(deepRed).lerp(warmGold, heartBeat * 6.0);
            }
            
            // リアルタイムセンサー情報ゆらぎ
            if (Math.random() < 0.08) {
                document.getElementById('core-status').innerText = isCombatMode ? "TACHYCARDIA" : `HR: ${(60 + heartBeat*40).toFixed(0)} BPM`;
                document.getElementById('tel-temp').innerText = `${(36.6 + Math.random()*1.2).toFixed(1)} °C`;
                document.getElementById('tel-em').innerText = `${(270.0 + Math.random()*60.0).toFixed(1)} mSv/h`;
            }

            // ★ 酸性樹液の自律タイマー更新制御 (パーティクル監視から時間経過方式に完全修正しました)
            if (isSpitting) {
                spitTimer -= delta;
                if (spitTimer <= 0) {
                    isSpitting = false; // 指定時間経過で確実に口がスムーズに閉じるようになります
                }
            }

            // 3. 蔦触手 (S字湾曲をデフォルトポーズに設定。地面への不自然なめり込みを解消)
            const speedScale = isCombatMode ? 4.0 : 1.35;
            const flexScale = isCombatMode ? 0.4 : 0.13;

            tentacles.forEach((t, tIdx) => {
                t.huntTimer -= delta;
                if (t.huntTimer <= 0) {
                    t.huntMode = Math.random() > (isCombatMode ? 0.2 : 0.6);
                    t.huntTimer = 4.0 + Math.random() * 5.0;

                    // ハントモード移行時、狙うターゲット角度を設定
                    t.targetYaw = (Math.random() - 0.5) * 1.2;
                    t.targetPitch = (Math.random() - 0.5) * 0.8;
                }

                t.segments.forEach((seg, sIdx) => {
                    let defaultRotX = 0;
                    if (sIdx < 3) {
                        defaultRotX = t.isOuter ? -Math.PI / 11 : -Math.PI / 18;  
                    } else if (sIdx < 7) {
                        defaultRotX = t.isOuter ? Math.PI / 11 : Math.PI / 7.5;  
                    } else {
                        defaultRotX = t.isOuter ? -Math.PI / 14 : -Math.PI / 9;  
                    }

                    const noiseWave = Math.sin(time * 0.35 + tIdx * 2.3) * 0.06 + Math.cos(time * 2.2 + sIdx * 1.4) * 0.03;
                    let waveX = Math.sin(time * speedScale * t.speedFactor + (sIdx * 0.45) + tIdx) * flexScale + noiseWave;
                    let waveY = Math.cos(time * (speedScale * 0.95) * t.speedFactor + (sIdx * 0.4) + tIdx) * flexScale;
                    
                    if (t.huntMode && sIdx >= 5) {
                        const huntWeight = (sIdx - 4) / 5; 
                        waveX = THREE.MathUtils.lerp(waveX, t.targetPitch, huntWeight * 0.45);
                        waveY = THREE.MathUtils.lerp(waveY, t.targetYaw, huntWeight * 0.45);
                    }

                    seg.rotation.x = defaultRotX + waveX;
                    seg.rotation.y = waveY;
                });
            });

            // 4. 触手の先端(すべての小顎)の自律的・非同期スナップ噛み付き
            sideMouths.forEach((sm, index) => {
                const isHunting = tentacles[index]?.huntMode;
                const snapThresh = isHunting ? 0.0 : 0.3;
                const snapSpeed = isHunting ? 6.0 : 3.5;

                const triggerSnapping = Math.sin(time * snapSpeed + index * 1.8) > snapThresh;
                const targetAngle = triggerSnapping ? (Math.PI / 6.5) : 0.03;
                
                sm.upper.rotation.x = THREE.MathUtils.lerp(sm.upper.rotation.x, targetAngle, 0.15);
                sm.lower.rotation.x = THREE.MathUtils.lerp(sm.lower.rotation.x, -targetAngle, 0.15);

                if (Math.random() < 0.005 && isHUDVisible) {
                    const worldPos = new THREE.Vector3();
                    sm.upper.getWorldPosition(worldPos);
                    if(worldPos.y > -1.0) triggerSlimeDrop(2, worldPos);
                }
            });

            // 5. ★ メイン大顎の生物的ダイナミック開閉
            let targetUpperAngle = 0.0;
            let targetLowerAngle = 0.0;

            if (isRoaring) {
                roarTimer += delta;
                // 上顎はマイナス方向(反り上がる)、下顎はプラス方向(深く大きく落ちる)
                targetUpperAngle = -Math.PI / 16.0; 
                targetLowerAngle = Math.PI / 4.0; 

                camera.position.x += (Math.random() - 0.5) * 0.15;
                camera.position.y += (Math.random() - 0.5) * 0.15;

                if (roarTimer > 2.2) {
                    isRoaring = false;
                    roarTimer = 0;
                }
            } else if (isSpitting) {
                targetUpperAngle = -Math.PI / 24.0;
                targetLowerAngle = Math.PI / 5.5;
            } else {
                // 普段の呼吸(下顎メインの微小なうごめき:上顎僅かに上、下顎僅かに下)
                targetUpperAngle = -0.005 - Math.sin(time * 0.7) * 0.005;
                targetLowerAngle = 0.015 + Math.sin(time * 0.7) * 0.015;
            }

            mouthUpper.rotation.x = THREE.MathUtils.lerp(mouthUpper.rotation.x, targetUpperAngle, 0.12);
            mouthLower.rotation.x = THREE.MathUtils.lerp(mouthLower.rotation.x, targetLowerAngle, 0.12);

            // ★ 顎を開閉させた時の「口角肉膜ウェブ」の伸縮シミュレーション
            if (webL && webR) {
                // 下顎(プラス)から上顎(マイナス)を引くことで、開くほど大きくなる正の「openAngle(開口角度)」を算出
                const openAngle = mouthLower.rotation.x - mouthUpper.rotation.x; 
                
                // 開いたとき、ウェブを縦方向(Y軸)に美しく引き伸ばし、Z方向には少し引き締める
                webL.scale.y = 1.0 + openAngle * 2.2; 
                webR.scale.y = 1.0 + openAngle * 2.2; 
                webL.scale.z = 1.0 - openAngle * 0.2;
                webR.scale.z = 1.0 - openAngle * 0.2;
                
                // 開きにあわせたリアルタイムY・Z位置補正(ピボット円弧移動の追従)
                const midAngle = (mouthUpper.rotation.x + mouthLower.rotation.x) * 0.5;
                webL.position.y = 0.0 + midAngle * 0.4;
                webR.position.y = 0.0 + midAngle * 0.4;
                webL.position.z = 0.75 - openAngle * 0.15;
                webR.position.z = 0.75 - openAngle * 0.15;

                // ウェブ自体の角度も、上顎と下顎の「完全に中間の傾き」へ自動同期
                webL.rotation.x = midAngle;
                webR.rotation.x = midAngle;
            }

            // 6. 浮遊胞子の巡回
            if (sporeParticles) {
                const posArr = sporeParticles.geometry.attributes.position.array;
                for (let i = 1; i < posArr.length; i += 3) {
                    posArr[i] += 0.015; 
                    if (posArr[i] > 10.0) {
                        posArr[i] = -1.5; 
                    }
                }
                sporeParticles.geometry.attributes.position.needsUpdate = true;
            }

            // 7. アシッド強酸液パーティクル
            for (let i = acidParticles.length - 1; i >= 0; i--) {
                const drop = acidParticles[i];
                drop.mesh.position.addScaledVector(drop.vel, delta);
                drop.vel.y -= 13.0 * delta; 
                
                drop.life -= 0.75 * delta;
                drop.mesh.scale.setScalar(drop.life);

                if (drop.life <= 0 || drop.mesh.position.y < -1.1) {
                    scene.remove(drop.mesh);
                    drop.mesh.geometry.dispose();
                    drop.mesh.material.dispose();
                    acidParticles.splice(i, 1);
                }
            }

            // 滴り落ちる生々しい唾液(粘液スライム)の物理演算更新
            for (let i = slimeParticles.length - 1; i >= 0; i--) {
                const sp = slimeParticles[i];
                sp.mesh.position.y += sp.velY * delta;
                sp.velY -= 9.8 * delta; 
                sp.life -= 0.5 * delta;

                if (sp.mesh.position.y < -1.35) {
                    sp.mesh.position.y = -1.35;
                    sp.mesh.scale.x += 2.0 * delta; 
                    sp.mesh.scale.z += 2.0 * delta;
                    sp.mesh.scale.y = 0.01;         
                }

                if (sp.life <= 0) {
                    scene.remove(sp.mesh);
                    sp.mesh.geometry.dispose();
                    sp.mesh.material.dispose();
                    slimeParticles.splice(i, 1);
                }
            }

            // 8. インタラクティブ・ウォーター
            if (floor) {
                const pos = floor.geometry.attributes.position;
                for (let i = 0; i < pos.count; i++) {
                    const x = pos.getX(i);
                    const y = pos.getY(i);
                    
                    const distToCenter = Math.sqrt(x*x + y*y);
                    let ripple = Math.sin(distToCenter * 0.45 - time * 2.8) * 0.09 / (1.0 + distToCenter * 0.08);
                    
                    let baseWave = Math.sin(x*0.12 + time*0.5) * Math.cos(y*0.12 + time*0.4) * 0.12;

                    pos.setZ(i, baseWave + ripple);
                }
                floor.geometry.attributes.position.needsUpdate = true;
                floor.geometry.computeVertexNormals();
            }

            // カメラ・コントロールの更新
            controls.update();
            const distance = camera.position.distanceTo(biollanteGroup.position);
            document.getElementById('val-dist').innerText = distance.toFixed(2);

            renderer.render(scene, camera);
        }

        // 画面リサイズ対応
        function onWindowResize() {
            const container = document.getElementById('canvas-container');
            if (!container) return;
            
            const w = container.clientWidth || window.innerWidth;
            const h = container.clientHeight || window.innerHeight;
            
            camera.aspect = w / h;
            camera.updateProjectionMatrix();
            renderer.setSize(w, h);
        }

        window.onload = function() {
            init();
        };
    </script>
</body>
</html>

ビオランテ

ツルの数を工夫しました。

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