sábado, 7 de noviembre de 2026

Juego 3d echo en IA;

https://marvelous-kitsune-cc381f.netlify.app 

 Este videojuego esta echo en menos de 3 minutos por una IA en html y esta fenomenal, no se que pasara en un futuro ya muy proximo con el desarrollo de videojuegos, pero estoy alucinando en colores, sim plemente de  escribi a la  IA  haz un juego de disparos en 3d en html.  Y el resultado fue sorprendente, no sabia como  mostrarlo en mi blog y tambien me soluciono el tema  muy rapidamente, . Añado aqui el codigo completo.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CYBERSTRIKE 3D FPS</title>
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Three.js CDN -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #050508;
            font-family: 'Inter', system-ui, -apple-system, sans-serif;
            user-select: none;
            -webkit-user-select: none;
        }
        #crosshair {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            pointer-events: none;
            z-index: 10;
        }
        .hud-glass {
            background: rgba(15, 23, 42, 0.65);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        .glow-cyan {
            box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);
        }
        .glow-red {
            box-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
        }
        #damage-overlay {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            pointer-events: none;
            background: radial-gradient(circle, transparent 60%, rgba(239, 68, 68, 0.5) 100%);
            opacity: 0;
            transition: opacity 0.15s ease-out;
            z-index: 5;
        }
    </style>
</head>
<body class="w-screen h-screen overflow-hidden text-white">

    <!-- Damage Screen Flash -->
    <div id="damage-overlay"></div>

    <!-- Crosshair -->
    <div id="crosshair" class="hidden">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="opacity-80">
            <circle cx="12" cy="12" r="3" fill="#00f0ff"/>
            <line x1="12" y1="2" x2="12" y2="7" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
            <line x1="12" y1="17" x2="12" y2="22" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
            <line x1="2" y1="12" x2="7" y2="12" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
            <line x1="17" y1="12" x2="22" y2="12" stroke="#00f0ff" stroke-width="1.5" stroke-linecap="round"/>
        </svg>
    </div>

    <!-- HUD Overlay -->
    <div id="hud" class="absolute inset-0 pointer-events-none hidden flex-col justify-between p-6 z-10">
        <!-- Top Bar: Score & Wave -->
        <div class="flex justify-between items-center w-full max-w-4xl mx-auto">
            <div class="hud-glass px-5 py-2.5 rounded-2xl flex items-center gap-4 glow-cyan">
                <div>
                    <div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Score</div>
                    <div id="hud-score" class="text-2xl font-black tracking-tight text-white">0</div>
                </div>
            </div>

            <div class="hud-glass px-6 py-2.5 rounded-2xl text-center glow-cyan">
                <div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Wave</div>
                <div id="hud-wave" class="text-2xl font-black text-white">1</div>
            </div>

            <div class="hud-glass px-5 py-2.5 rounded-2xl flex items-center gap-4 glow-cyan">
                <div class="text-right">
                    <div class="text-xs text-cyan-400 font-semibold uppercase tracking-wider">Enemies Left</div>
                    <div id="hud-enemies" class="text-2xl font-black text-white">0</div>
                </div>
            </div>
        </div>

        <!-- Bottom Bar: Health & Ammo -->
        <div class="flex justify-between items-end w-full max-w-4xl mx-auto">
            <!-- Health Bar -->
            <div class="hud-glass p-4 rounded-2xl w-64 glow-red">
                <div class="flex justify-between mb-1.5 items-center">
                    <span class="text-xs text-red-400 font-bold uppercase tracking-wider">Health</span>
                    <span id="hud-health-num" class="text-sm font-bold text-red-200">100 HP</span>
                </div>
                <div class="w-full h-3 bg-gray-900/80 rounded-full overflow-hidden p-0.5 border border-red-500/30">
                    <div id="hud-health-bar" class="h-full bg-gradient-to-r from-red-600 to-rose-400 rounded-full transition-all duration-200" style="width: 100%;"></div>
                </div>
            </div>

            <!-- Reloading Notification Indicator -->
            <div id="reload-msg" class="text-amber-400 font-black tracking-widest uppercase text-xl animate-pulse hidden mb-4">
                [ Reloading... ]
            </div>

            <!-- Ammo Indicator -->
            <div class="hud-glass p-4 rounded-2xl text-right w-44 glow-cyan">
                <div class="text-xs text-cyan-400 font-bold uppercase tracking-wider mb-0.5">Ammo</div>
                <div class="flex items-baseline justify-end gap-1">
                    <span id="hud-ammo" class="text-3xl font-black text-cyan-300">30</span>
                    <span class="text-gray-400 text-sm font-bold">/ <span id="hud-ammo-max">30</span></span>
                </div>
            </div>
        </div>
    </div>

    <!-- Start / Title Screen Overlay -->
    <div id="start-screen" class="absolute inset-0 bg-slate-950/90 backdrop-blur-md flex flex-col items-center justify-center z-20">
        <div class="max-w-md w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-cyan-500/30">
            <h1 class="text-4xl md:text-5xl font-black tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-sky-300 to-indigo-400 mb-2">
                CYBERSTRIKE 3D
            </h1>
            <p class="text-slate-400 text-sm mb-8">Defend the arena against cybernetic rogue drones.</p>

            <div class="bg-slate-900/60 p-4 rounded-xl text-left text-xs text-slate-300 space-y-2 mb-8 border border-white/5">
                <div class="flex justify-between"><span class="text-cyan-400 font-bold">WASD / Arrows</span> Move</div>
                <div class="flex justify-between"><span class="text-cyan-400 font-bold">Mouse Move</span> Look Around</div>
                <div class="flex justify-between"><span class="text-cyan-400 font-bold">Left Click</span> Shoot</div>
                <div class="flex justify-between"><span class="text-cyan-400 font-bold">R</span> Reload Weapon</div>
                <div class="flex justify-between"><span class="text-cyan-400 font-bold">Space</span> Jump</div>
            </div>

            <button id="btn-start" class="w-full py-4 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-bold rounded-xl tracking-wider uppercase transition transform hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-cyan-500/30">
                Deploy to Arena
            </button>
        </div>
    </div>

    <!-- Pause / Lock Screen Overlay -->
    <div id="pause-screen" class="absolute inset-0 bg-slate-950/80 backdrop-blur-md flex flex-col items-center justify-center z-20 hidden">
        <div class="max-w-sm w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-white/10">
            <h2 class="text-3xl font-bold text-cyan-400 mb-4">GAME PAUSED</h2>
            <p class="text-slate-300 text-sm mb-6">Click anywhere to resume target engagement.</p>
            <button id="btn-resume" class="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-bold rounded-xl tracking-wider uppercase transition">
                Resume Game
            </button>
        </div>
    </div>

    <!-- Game Over Overlay -->
    <div id="gameover-screen" class="absolute inset-0 bg-slate-950/95 backdrop-blur-md flex flex-col items-center justify-center z-20 hidden">
        <div class="max-w-md w-full p-8 rounded-3xl hud-glass text-center shadow-2xl border border-red-500/30">
            <h2 class="text-4xl font-black text-red-500 tracking-wider mb-2">SYSTEM FAILURE</h2>
            <p class="text-slate-400 text-sm mb-6">Your chassis was overwhelmed by hostile forces.</p>

            <div class="bg-slate-900/60 p-4 rounded-xl text-center space-y-2 mb-8 border border-white/5">
                <div class="text-xs text-slate-400 uppercase font-bold">Final Score</div>
                <div id="final-score" class="text-4xl font-black text-cyan-400">0</div>
                <div id="final-wave" class="text-xs text-slate-400 font-medium">Survived until Wave 1</div>
            </div>

            <button id="btn-restart" class="w-full py-4 bg-gradient-to-r from-red-600 to-rose-500 hover:from-red-500 hover:to-rose-400 text-white font-bold rounded-xl tracking-wider uppercase transition transform hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-red-500/30">
                Re-initialize Core
            </button>
        </div>
    </div>

    <!-- Three.js Canvas Container -->
    <div id="game-container" class="w-full h-full"></div>

    <script>
        /**
         * CYBERSTRIKE 3D FPS - Sound Synthesizer (Web Audio API)
         * Generates sound effects without external assets.
         */
        class SoundFX {
            constructor() {
                this.ctx = null;
            }
            init() {
                if (!this.ctx) {
                    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
                }
            }
            playGunshot() {
                if (!this.ctx) return;
                const now = this.ctx.currentTime;
                // Noise buffer for blast
                const bufferSize = this.ctx.sampleRate * 0.15;
                const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
                const output = buffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) {
                    output[i] = Math.random() * 2 - 1;
                }
                const whiteNoise = this.ctx.createBufferSource();
                whiteNoise.buffer = buffer;

                const filter = this.ctx.createBiquadFilter();
                filter.type = 'lowpass';
                filter.frequency.setValueAtTime(1200, now);
                filter.frequency.exponentialRampToValueAtTime(80, now + 0.15);

                const gain = this.ctx.createGain();
                gain.gain.setValueAtTime(0.4, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);

                whiteNoise.connect(filter);
                filter.connect(gain);
                gain.connect(this.ctx.destination);
                whiteNoise.start(now);

                // Transient punch oscillator
                const osc = this.ctx.createOscillator();
                const oscGain = this.ctx.createGain();
                osc.type = 'triangle';
                osc.frequency.setValueAtTime(150, now);
                osc.frequency.exponentialRampToValueAtTime(30, now + 0.1);
                oscGain.gain.setValueAtTime(0.5, now);
                oscGain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
                osc.connect(oscGain);
                oscGain.connect(this.ctx.destination);
                osc.start(now);
                osc.stop(now + 0.1);
            }

            playHit() {
                if (!this.ctx) return;
                const now = this.ctx.currentTime;
                const osc = this.ctx.createOscillator();
                const gain = this.ctx.createGain();
                osc.type = 'sine';
                osc.frequency.setValueAtTime(800, now);
                osc.frequency.exponentialRampToValueAtTime(400, now + 0.05);
                gain.gain.setValueAtTime(0.2, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
                osc.connect(gain);
                gain.connect(this.ctx.destination);
                osc.start(now);
                osc.stop(now + 0.05);
            }

            playExplosion() {
                if (!this.ctx) return;
                const now = this.ctx.currentTime;
                const bufferSize = this.ctx.sampleRate * 0.4;
                const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
                const output = buffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) {
                    output[i] = Math.random() * 2 - 1;
                }
                const noise = this.ctx.createBufferSource();
                noise.buffer = buffer;

                const filter = this.ctx.createBiquadFilter();
                filter.type = 'lowpass';
                filter.frequency.setValueAtTime(400, now);
                filter.frequency.linearRampToValueAtTime(30, now + 0.4);

                const gain = this.ctx.createGain();
                gain.gain.setValueAtTime(0.6, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);

                noise.connect(filter);
                filter.connect(gain);
                gain.connect(this.ctx.destination);
                noise.start(now);
            }

            playPlayerHit() {
                if (!this.ctx) return;
                const now = this.ctx.currentTime;
                const osc = this.ctx.createOscillator();
                const gain = this.ctx.createGain();
                osc.type = 'sawtooth';
                osc.frequency.setValueAtTime(120, now);
                osc.frequency.exponentialRampToValueAtTime(40, now + 0.2);
                gain.gain.setValueAtTime(0.4, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
                osc.connect(gain);
                gain.connect(this.ctx.destination);
                osc.start(now);
                osc.stop(now + 0.2);
            }

            playReload() {
                if (!this.ctx) return;
                const now = this.ctx.currentTime;
                const osc = this.ctx.createOscillator();
                const gain = this.ctx.createGain();
                osc.type = 'sine';
                osc.frequency.setValueAtTime(300, now);
                osc.frequency.linearRampToValueAtTime(600, now + 0.15);
                gain.gain.setValueAtTime(0.15, now);
                gain.gain.linearRampToValueAtTime(0.01, now + 0.15);
                osc.connect(gain);
                gain.connect(this.ctx.destination);
                osc.start(now);
                osc.stop(now + 0.15);
            }
        }

        const sfx = new SoundFX();

        /**
         * GAME ENGINE & LOGIC
         */
        const Game = {
            // State Flags
            started: false,
            paused: false,
            gameOver: false,

            // Stats
            score: 0,
            wave: 1,
            health: 100,
            maxHealth: 100,
            ammo: 30,
            maxAmmo: 30,
            isReloading: false,

            // Spatial Constants
            ARENA_SIZE: 100,
            PLAYER_RADIUS: 1.2,

            // Three.js Core Setup
            scene: null,
            camera: null,
            renderer: null,
            raycaster: null,

            // Game Objects & Physics
            obstacles: [],
            enemies: [],
            particles: [],
            bullets: [],

            // Player State
            player: {
                position: new THREE.Vector3(0, 1.7, 0),
                velocity: new THREE.Vector3(),
                pitch: 0,
                yaw: 0,
                onGround: true,
                speed: 12,
                jumpForce: 10
            },
            keys: {},

            // Weapon Model & Rig
            gunGroup: null,
            recoilAmount: 0,
            muzzleFlashLight: null,

            // Clock & Loop Controls
            clock: new THREE.Clock(),

            // Initialize Game engine components
            init() {
                const container = document.getElementById('game-container');

                // Scene Setup
                this.scene = new THREE.Scene();
                this.scene.background = new THREE.Color(0x05070f);
                this.scene.fog = new THREE.FogExp2(0x05070f, 0.015);

                // Camera Setup
                this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
                this.camera.rotation.order = 'YXZ';

                // Renderer Setup
                this.renderer = new THREE.WebGLRenderer({ antialias: true });
                this.renderer.setSize(window.innerWidth, window.innerHeight);
                this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
                this.renderer.shadowMap.enabled = true;
                this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
                container.appendChild(this.renderer.domElement);

                // Raycaster for shooting logic
                this.raycaster = new THREE.Raycaster();

                // Build World Environment
                this.setupLighting();
                this.buildArena();
                this.createGunModel();

                // Setup Input Event Listeners
                this.setupEvents();

                // Window Resize Handler
                window.addEventListener('resize', () => this.onWindowResize());
            },

            setupLighting() {
                // Ambient Light
                const ambientLight = new THREE.AmbientLight(0x1a2638, 1.2);
                this.scene.add(ambientLight);

                // Directional Sun Light with Shadows
                const dirLight = new THREE.DirectionalLight(0x38bdf8, 1.5);
                dirLight.position.set(30, 50, 20);
                dirLight.castShadow = true;
                dirLight.shadow.mapSize.width = 2048;
                dirLight.shadow.mapSize.height = 2048;
                dirLight.shadow.camera.near = 0.5;
                dirLight.shadow.camera.far = 150;
                const d = 60;
                dirLight.shadow.camera.left = -d;
                dirLight.shadow.camera.right = d;
                dirLight.shadow.camera.top = d;
                dirLight.shadow.camera.bottom = -d;
                this.scene.add(dirLight);

                // Accent Point Lights in corners
                const colors = [0x00f0ff, 0xff0055, 0x00ff88, 0x7928ca];
                const corners = [
                    [40, 40], [-40, 40], [-40, -40], [40, -40]
                ];
                corners.forEach((pos, idx) => {
                    const ptLight = new THREE.PointLight(colors[idx], 2, 35);
                    ptLight.position.set(pos[0], 6, pos[1]);
                    this.scene.add(ptLight);

                    // Light source pillar visual
                    const pillarGeo = new THREE.CylinderGeometry(0.2, 0.2, 12, 16);
                    const pillarMat = new THREE.MeshStandardMaterial({
                        color: colors[idx],
                        emissive: colors[idx],
                        emissiveIntensity: 0.8
                    });
                    const pillar = new THREE.Mesh(pillarGeo, pillarMat);
                    pillar.position.set(pos[0], 6, pos[1]);
                    this.scene.add(pillar);
                });
            },

            buildArena() {
                // Grid Floor Texture (Procedural Canvas)
                const canvas = document.createElement('canvas');
                canvas.width = 512;
                canvas.height = 512;
                const ctx = canvas.getContext('2d');
                ctx.fillStyle = '#090d16';
                ctx.fillRect(0, 0, 512, 512);
                ctx.strokeStyle = '#1e293b';
                ctx.lineWidth = 4;
                ctx.strokeRect(0, 0, 512, 512);
                ctx.strokeStyle = '#0f172a';
                ctx.lineWidth = 2;
                for (let i = 0; i < 512; i += 32) {
                    ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, 512); ctx.stroke();
                    ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(512, i); ctx.stroke();
                }
                const floorTexture = new THREE.CanvasTexture(canvas);
                floorTexture.wrapS = THREE.RepeatWrapping;
                floorTexture.wrapT = THREE.RepeatWrapping;
                floorTexture.repeat.set(25, 25);

                // Floor Mesh
                const floorGeo = new THREE.PlaneGeometry(this.ARENA_SIZE, this.ARENA_SIZE);
                const floorMat = new THREE.MeshStandardMaterial({
                    map: floorTexture,
                    roughness: 0.4,
                    metalness: 0.2
                });
                const floor = new THREE.Mesh(floorGeo, floorMat);
                floor.rotation.x = -Math.PI / 2;
                floor.receiveShadow = true;
                this.scene.add(floor);

                // Perimeter Outer Walls
                const wallMat = new THREE.MeshStandardMaterial({
                    color: 0x0f172a,
                    roughness: 0.6,
                    metalness: 0.5
                });
                const wallHeight = 12;
                const wallGeoHorizontal = new THREE.BoxGeometry(this.ARENA_SIZE, wallHeight, 2);
                const wallGeoVertical = new THREE.BoxGeometry(2, wallHeight, this.ARENA_SIZE);

                // Wall 1 (North)
                const w1 = new THREE.Mesh(wallGeoHorizontal, wallMat);
                w1.position.set(0, wallHeight / 2, -this.ARENA_SIZE / 2);
                // Wall 2 (South)
                const w2 = new THREE.Mesh(wallGeoHorizontal, wallMat);
                w2.position.set(0, wallHeight / 2, this.ARENA_SIZE / 2);
                // Wall 3 (West)
                const w3 = new THREE.Mesh(wallGeoVertical, wallMat);
                w3.position.set(-this.ARENA_SIZE / 2, wallHeight / 2, 0);
                // Wall 4 (East)
                const w4 = new THREE.Mesh(wallGeoVertical, wallMat);
                w4.position.set(this.ARENA_SIZE / 2, wallHeight / 2, 0);

                [w1, w2, w3, w4].forEach(w => {
                    w.receiveShadow = true;
                    w.castShadow = true;
                    this.scene.add(w);
                    this.obstacles.push(new THREE.Box3().setFromObject(w));
                });

                // Spawn Internal Obstacles / Cover Blocks
                const boxGeo = new THREE.BoxGeometry(4, 4, 4);
                const boxMat = new THREE.MeshStandardMaterial({
                    color: 0x1e293b,
                    roughness: 0.3,
                    metalness: 0.8
                });

                // Structured placement layout for obstacles
                const layoutPositions = [
                    {x: -15, z: -15}, {x: 15, z: -15}, {x: -15, z: 15}, {x: 15, z: 15},
                    {x: 0, z: -25}, {x: 0, z: 25}, {x: -25, z: 0}, {x: 25, z: 0},
                    {x: -30, z: -30}, {x: 30, z: -30}, {x: -30, z: 30}, {x: 30, z: 30}
                ];

                layoutPositions.forEach(pos => {
                    const block = new THREE.Mesh(boxGeo, boxMat);
                    // Add random height stacking for dynamic cover
                    const scaleY = 1 + Math.floor(Math.random() * 2);
                    block.scale.set(1, scaleY, 1);
                    block.position.set(pos.x, (4 * scaleY) / 2, pos.z);
                    block.castShadow = true;
                    block.receiveShadow = true;
                    this.scene.add(block);

                    // Add cyan accent stripe on blocks
                    const stripeGeo = new THREE.BoxGeometry(4.1, 0.2, 4.1);
                    const stripeMat = new THREE.MeshBasicMaterial({ color: 0x00f0ff });
                    const stripe = new THREE.Mesh(stripeGeo, stripeMat);
                    stripe.position.set(0, 0, 0);
                    block.add(stripe);

                    // Store bounding box for physics collision
                    this.obstacles.push(new THREE.Box3().setFromObject(block));
                });
            },

            createGunModel() {
                // HUD Camera Setup (Separate rendering view layer for FPS weapon)
                this.gunGroup = new THREE.Group();

                // Futuristic Blaster Geometry Assembly
                const bodyMat = new THREE.MeshStandardMaterial({ color: 0x111827, metalness: 0.9, roughness: 0.2 });
                const accentMat = new THREE.MeshStandardMaterial({ color: 0x00f0ff, emissive: 0x00f0ff, emissiveIntensity: 0.5 });
                const metalMat = new THREE.MeshStandardMaterial({ color: 0x374151, metalness: 0.95, roughness: 0.1 });

                // Gun Main Body
                const bodyGeo = new THREE.BoxGeometry(0.12, 0.18, 0.6);
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.position.set(0, 0, 0);

                // Barrel
                const barrelGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.5, 16);
                const barrel = new THREE.Mesh(barrelGeo, metalMat);
                barrel.rotation.x = Math.PI / 2;
                barrel.position.set(0, 0.04, -0.4);

                // Energy Cell / Scope Accent
                const scopeGeo = new THREE.BoxGeometry(0.08, 0.06, 0.25);
                const scope = new THREE.Mesh(scopeGeo, accentMat);
                scope.position.set(0, 0.12, -0.05);

                // Muzzle Flash Point Light
                this.muzzleFlashLight = new THREE.PointLight(0x00f0ff, 0, 8);
                this.muzzleFlashLight.position.set(0, 0.04, -0.7);

                this.gunGroup.add(body, barrel, scope, this.muzzleFlashLight);

                // Offset Gun to bottom-right of player view camera
                this.gunGroup.position.set(0.28, -0.24, -0.45);
                this.camera.add(this.gunGroup);
                this.scene.add(this.camera);
            },

            setupEvents() {
                // Key state trackers
                window.addEventListener('keydown', (e) => {
                    this.keys[e.code] = true;
                    if (e.code === 'KeyR') this.reloadWeapon();
                });
                window.addEventListener('keyup', (e) => {
                    this.keys[e.code] = false;
                });

                // Mouse Movement for Pointer Lock Controls
                window.addEventListener('mousemove', (e) => {
                    if (document.pointerLockElement !== document.body) return;
                    const sensitivity = 0.0022;
                    this.player.yaw -= e.movementX * sensitivity;
                    this.player.pitch -= e.movementY * sensitivity;

                    // Clamp pitch vertical look angle (-89 deg to +89 deg)
                    const maxPitch = Math.PI / 2 - 0.02;
                    this.player.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.player.pitch));
                });

                // Shooting Mouse Click
                window.addEventListener('mousedown', (e) => {
                    if (document.pointerLockElement === document.body && e.button === 0) {
                        this.shoot();
                    }
                });

                // Pointer Lock State Change Listeners
                document.addEventListener('pointerlockchange', () => {
                    if (document.pointerLockElement === document.body) {
                        if (this.started && !this.gameOver) {
                            this.paused = false;
                            document.getElementById('pause-screen').classList.add('hidden');
                            document.getElementById('hud').classList.remove('hidden');
                            document.getElementById('crosshair').classList.remove('hidden');
                        }
                    } else {
                        if (this.started && !this.gameOver) {
                            this.paused = true;
                            document.getElementById('pause-screen').classList.remove('hidden');
                            document.getElementById('hud').classList.add('hidden');
                            document.getElementById('crosshair').classList.add('hidden');
                        }
                    }
                });

                // UI Button Actions
                document.getElementById('btn-start').addEventListener('click', () => {
                    sfx.init();
                    this.startGame();
                });
                document.getElementById('btn-resume').addEventListener('click', () => {
                    document.body.requestPointerLock();
                });
                document.getElementById('btn-restart').addEventListener('click', () => {
                    this.restartGame();
                });
            },

            startGame() {
                this.started = true;
                this.paused = false;
                this.gameOver = false;
                this.score = 0;
                this.wave = 1;
                this.health = 100;
                this.ammo = 30;

                this.updateHUD();
                document.getElementById('start-screen').classList.add('hidden');
                document.getElementById('gameover-screen').classList.add('hidden');
                document.getElementById('hud').classList.remove('hidden');
                document.getElementById('crosshair').classList.remove('hidden');

                document.body.requestPointerLock();
                this.spawnWave();
            },

            restartGame() {
                // Clear existing active enemies
                this.enemies.forEach(e => this.scene.remove(e.mesh));
                this.enemies = [];
                this.bullets.forEach(b => this.scene.remove(b.mesh));
                this.bullets = [];
                this.particles.forEach(p => this.scene.remove(p.mesh));
                this.particles = [];

                // Reset player transform
                this.player.position.set(0, 1.7, 0);
                this.player.velocity.set(0, 0, 0);
                this.player.yaw = 0;
                this.player.pitch = 0;

                this.startGame();
            },

            spawnWave() {
                const count = 4 + (this.wave - 1) * 3;
                document.getElementById('hud-wave').innerText = this.wave;

                for (let i = 0; i < count; i++) {
                    this.spawnEnemy();
                }
                this.updateHUD();
            },

            spawnEnemy() {
                // Enemy Robot Model Construction
                const group = new THREE.Group();

                // Core Torso Body
                const bodyGeo = new THREE.BoxGeometry(1.2, 1.5, 0.8);
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: 0x991b1b,
                    roughness: 0.3,
                    metalness: 0.8
                });
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.position.y = 1.25;
                body.castShadow = true;
                group.add(body);

                // Glowing Head Visor Eye
                const headGeo = new THREE.BoxGeometry(0.8, 0.6, 0.7);
                const headMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
                const head = new THREE.Mesh(headGeo, headMat);
                head.position.y = 2.3;
                group.add(head);

                const eyeGeo = new THREE.BoxGeometry(0.6, 0.15, 0.1);
                const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0044 });
                const eye = new THREE.Mesh(eyeGeo, eyeMat);
                eye.position.set(0, 2.35, 0.36);
                group.add(eye);

                // Floating Jet / Hover Legs
                const baseGeo = new THREE.CylinderGeometry(0.5, 0.1, 0.8, 8);
                const baseMat = new THREE.MeshStandardMaterial({ color: 0x374151, metalness: 0.9 });
                const base = new THREE.Mesh(baseGeo, baseMat);
                base.position.y = 0.4;
                group.add(base);

                // Position enemy at safe distance away from player
                let spawnX, spawnZ;
                do {
                    spawnX = (Math.random() - 0.5) * (this.ARENA_SIZE - 12);
                    spawnZ = (Math.random() - 0.5) * (this.ARENA_SIZE - 12);
                } while (Math.hypot(spawnX - this.player.position.x, spawnZ - this.player.position.z) < 15);

                group.position.set(spawnX, 0, spawnZ);
                this.scene.add(group);

                // Enemy data record
                const hp = 80 + (this.wave * 20);
                this.enemies.push({
                    mesh: group,
                    bodyMesh: body,
                    health: hp,
                    maxHealth: hp,
                    speed: 3.5 + Math.min(this.wave * 0.4, 4),
                    attackCooldown: 0,
                    hitTimer: 0
                });
            },

            shoot() {
                if (this.isReloading) return;
                if (this.ammo <= 0) {
                    this.reloadWeapon();
                    return;
                }

                this.ammo--;
                this.updateHUD();
                sfx.playGunshot();

                // Muzzle Flash & Recoil Trigger
                this.recoilAmount = 0.08;
                this.muzzleFlashLight.intensity = 4;
                setTimeout(() => { this.muzzleFlashLight.intensity = 0; }, 40);

                // Calculate Bullet Trajectory Raycast
                const rayDir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion).normalize();
                const rayOrigin = this.camera.position.clone();

                this.raycaster.set(rayOrigin, rayDir);

                // Spawn Visual Tracer Line Beam
                this.createTracer(rayOrigin, rayDir);

                // Check Collisions with Enemies
                const enemyMeshes = this.enemies.map(e => e.mesh);
                const intersects = this.raycaster.intersectObjects(enemyMeshes, true);

                if (intersects.length > 0) {
                    const hitObj = intersects[0].object;
                    let hitEnemy = this.enemies.find(e => e.mesh === hitObj || e.mesh.children.includes(hitObj));

                    if (!hitEnemy && hitObj.parent) {
                        hitEnemy = this.enemies.find(e => e.mesh === hitObj.parent);
                    }

                    if (hitEnemy) {
                        const damage = 35;
                        hitEnemy.health -= damage;
                        hitEnemy.hitTimer = 0.1; // Flash red/white duration
                        sfx.playHit();

                        // Spawn impact spark particles
                        this.createSparks(intersects[0].point, intersects[0].face.normal);

                        // Enemy Death Handling
                        if (hitEnemy.health <= 0) {
                            sfx.playExplosion();
                            this.createExplosion(hitEnemy.mesh.position.clone());
                            this.scene.remove(hitEnemy.mesh);
                            this.enemies = this.enemies.filter(e => e !== hitEnemy);

                            this.score += 150;
                            this.updateHUD();

                            // Check Wave Cleared Condition
                            if (this.enemies.length === 0) {
                                this.wave++;
                                setTimeout(() => this.spawnWave(), 1500);
                            }
                        }
                    }
                }
            },

            reloadWeapon() {
                if (this.isReloading || this.ammo === this.maxAmmo) return;
                this.isReloading = true;
                sfx.playReload();
                document.getElementById('reload-msg').classList.remove('hidden');

                setTimeout(() => {
                    this.ammo = this.maxAmmo;
                    this.isReloading = false;
                    document.getElementById('reload-msg').classList.add('hidden');
                    this.updateHUD();
                }, 1200);
            },

            createTracer(origin, direction) {
                const tracerMat = new THREE.LineBasicMaterial({ color: 0x00f0ff, transparent: true, opacity: 0.8 });
                const points = [];
                // Start slightly in front of camera
                const start = origin.clone().add(direction.clone().multiplyScalar(0.5));
                const end = origin.clone().add(direction.clone().multiplyScalar(60));
                points.push(start, end);

                const tracerGeo = new THREE.BufferGeometry().setFromPoints(points);
                const line = new THREE.Line(tracerGeo, tracerMat);
                this.scene.add(line);

                // Fade and destroy tracer
                let alpha = 0.8;
                const fade = () => {
                    alpha -= 0.1;
                    if (alpha <= 0) {
                        this.scene.remove(line);
                    } else {
                        tracerMat.opacity = alpha;
                        requestAnimationFrame(fade);
                    }
                };
                fade();
            },

            createSparks(position, normal) {
                const particleCount = 8;
                const geo = new THREE.BufferGeometry();
                const positions = [];
                const velocities = [];

                for (let i = 0; i < particleCount; i++) {
                    positions.push(position.x, position.y, position.z);
                    const vel = normal.clone().add(new THREE.Vector3(
                        (Math.random() - 0.5) * 1.5,
                        (Math.random() - 0.5) * 1.5,
                        (Math.random() - 0.5) * 1.5
                    )).normalize().multiplyScalar(5 + Math.random() * 5);
                    velocities.push(vel);
                }

                geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
                const mat = new THREE.PointsMaterial({ color: 0x00f0ff, size: 0.15, transparent: true });
                const pSystem = new THREE.Points(geo, mat);
                this.scene.add(pSystem);

                this.particles.push({ mesh: pSystem, velocities: velocities, life: 0.25 });
            },

            createExplosion(position) {
                const count = 25;
                const geo = new THREE.BufferGeometry();
                const positions = [];
                const velocities = [];

                for (let i = 0; i < count; i++) {
                    positions.push(position.x, position.y + 1, position.z);
                    const vel = new THREE.Vector3(
                        (Math.random() - 0.5) * 8,
                        Math.random() * 8,
                        (Math.random() - 0.5) * 8
                    );
                    velocities.push(vel);
                }

                geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
                const mat = new THREE.PointsMaterial({ color: 0xef4444, size: 0.35, transparent: true });
                const pSystem = new THREE.Points(geo, mat);
                this.scene.add(pSystem);

                this.particles.push({ mesh: pSystem, velocities: velocities, life: 0.5 });
            },

            takeDamage(amount) {
                this.health = Math.max(0, this.health - amount);
                this.updateHUD();
                sfx.playPlayerHit();

                // Flash Red Vignette HUD overlay
                const overlay = document.getElementById('damage-overlay');
                overlay.style.opacity = '1';
                setTimeout(() => { overlay.style.opacity = '0'; }, 200);

                if (this.health <= 0 && !this.gameOver) {
                    this.triggerGameOver();
                }
            },

            triggerGameOver() {
                this.gameOver = true;
                document.exitPointerLock();
                document.getElementById('hud').classList.add('hidden');
                document.getElementById('crosshair').classList.add('hidden');
                document.getElementById('final-score').innerText = this.score;
                document.getElementById('final-wave').innerText = `Survived until Wave ${this.wave}`;
                document.getElementById('gameover-screen').classList.remove('hidden');
            },

            updateHUD() {
                document.getElementById('hud-score').innerText = this.score;
                document.getElementById('hud-health-num').innerText = `${Math.ceil(this.health)} HP`;
                document.getElementById('hud-health-bar').style.width = `${(this.health / this.maxHealth) * 100}%`;
                document.getElementById('hud-ammo').innerText = this.ammo;
                document.getElementById('hud-enemies').innerText = this.enemies.length;
            },

            updatePlayer(delta) {
                // Apply Camera Pitch / Yaw Rotation
                this.camera.rotation.x = this.player.pitch;
                this.camera.rotation.y = this.player.yaw;

                // Ground Check & Gravity Acceleration
                if (!this.player.onGround) {
                    this.player.velocity.y -= 25 * delta; // Gravity
                }

                // Input Movement Calculation
                const moveVector = new THREE.Vector3();
                if (this.keys['KeyW'] || this.keys['ArrowUp']) moveVector.z -= 1;
                if (this.keys['KeyS'] || this.keys['ArrowDown']) moveVector.z += 1;
                if (this.keys['KeyA'] || this.keys['ArrowLeft']) moveVector.x -= 1;
                if (this.keys['KeyD'] || this.keys['ArrowRight']) moveVector.x += 1;

                moveVector.normalize();

                // Transform movement vector into camera relative direction (Horizontal plane)
                const euler = new THREE.Euler(0, this.player.yaw, 0, 'YXZ');
                moveVector.applyEuler(euler);

                // Velocity update
                this.player.velocity.x = moveVector.x * this.player.speed;
                this.player.velocity.z = moveVector.z * this.player.speed;

                // Jump Action
                if (this.keys['Space'] && this.player.onGround) {
                    this.player.velocity.y = this.player.jumpForce;
                    this.player.onGround = false;
                }

                // Potential Future Position
                const nextPosition = this.player.position.clone().add(
                    new THREE.Vector3(this.player.velocity.x * delta, this.player.velocity.y * delta, this.player.velocity.z * delta)
                );

                // Collision Detection with Obstacles & Arena Boundaries
                const playerBoundingBox = new THREE.Box3(
                    new THREE.Vector3(nextPosition.x - this.PLAYER_RADIUS, nextPosition.y - 1.7, nextPosition.z - this.PLAYER_RADIUS),
                    new THREE.Vector3(nextPosition.x + this.PLAYER_RADIUS, nextPosition.y + 0.3, nextPosition.z + this.PLAYER_RADIUS)
                );

                let collided = false;
                for (const obstacleBox of this.obstacles) {
                    if (playerBoundingBox.intersectsBox(obstacleBox)) {
                        collided = true;
                        break;
                    }
                }

                if (!collided) {
                    this.player.position.copy(nextPosition);
                } else {
                    // Slide along axes if full collision blocked
                    this.player.position.y += this.player.velocity.y * delta;
                }

                // Floor height clamp
                if (this.player.position.y <= 1.7) {
                    this.player.position.y = 1.7;
                    this.player.velocity.y = 0;
                    this.player.onGround = true;
                }

                // Sync Three.js Camera Position
                this.camera.position.copy(this.player.position);

                // Weapon Recoil Animation Spring Return
                if (this.recoilAmount > 0) {
                    this.gunGroup.position.z = -0.45 + this.recoilAmount;
                    this.recoilAmount = Math.max(0, this.recoilAmount - delta * 0.5);
                } else {
                    this.gunGroup.position.z = -0.45;
                }

                // Gun Bobbing effect while moving
                if (moveVector.lengthSq() > 0 && this.player.onGround) {
                    const time = this.clock.getElapsedTime() * 10;
                    this.gunGroup.position.y = -0.24 + Math.sin(time) * 0.015;
                    this.gunGroup.position.x = 0.28 + Math.cos(time * 0.5) * 0.015;
                }
            },

            updateEnemies(delta) {
                this.enemies.forEach(enemy => {
                    // Flash hit animation material restore
                    if (enemy.hitTimer > 0) {
                        enemy.hitTimer -= delta;
                        enemy.bodyMesh.material.color.setHex(0xffffff);
                    } else {
                        enemy.bodyMesh.material.color.setHex(0x991b1b);
                    }

                    // Move enemy vector towards player
                    const dir = new THREE.Vector3().subVectors(this.player.position, enemy.mesh.position);
                    dir.y = 0; // Lock to floor plane
                    const dist = dir.length();

                    dir.normalize();
                    enemy.mesh.rotation.y = Math.atan2(dir.x, dir.z);

                    // Move if not right inside attack range
                    if (dist > 1.8) {
                        enemy.mesh.position.add(dir.multiplyScalar(enemy.speed * delta));
                    } else {
                        // Melee Attack Player
                        if (enemy.attackCooldown <= 0) {
                            this.takeDamage(15);
                            enemy.attackCooldown = 1.2; // Attack delay seconds
                        }
                    }

                    if (enemy.attackCooldown > 0) {
                        enemy.attackCooldown -= delta;
                    }
                });
            },

            updateParticles(delta) {
                for (let i = this.particles.length - 1; i >= 0; i--) {
                    const p = this.particles[i];
                    p.life -= delta;

                    if (p.life <= 0) {
                        this.scene.remove(p.mesh);
                        this.particles.splice(i, 1);
                        continue;
                    }

                    const positions = p.mesh.geometry.attributes.position.array;
                    for (let j = 0; j < p.velocities.length; j++) {
                        positions[j * 3] += p.velocities[j].x * delta;
                        positions[j * 3 + 1] += p.velocities[j].y * delta;
                        positions[j * 3 + 2] += p.velocities[j].z * delta;
                    }
                    p.mesh.geometry.attributes.position.needsUpdate = true;
                }
            },

            onWindowResize() {
                this.camera.aspect = window.innerWidth / window.innerHeight;
                this.camera.updateProjectionMatrix();
                this.renderer.setSize(window.innerWidth, window.innerHeight);
            },

            // Main Animation Loop
            animate() {
                requestAnimationFrame(() => this.animate());

                const delta = Math.min(this.clock.getDelta(), 0.1);

                if (this.started && !this.paused && !this.gameOver) {
                    this.updatePlayer(delta);
                    this.updateEnemies(delta);
                    this.updateParticles(delta);
                }

                this.renderer.render(this.scene, this.camera);
            }
        };

        // Launch Game on window initialization
        window.onload = () => {
            Game.init();
            Game.animate();
        };
    </script>
</body>
</html>


No hay comentarios:

Publicar un comentario