Drawaria DOOM Full Game 3D

Transform Drawaria into a retro first-person maze shooter with procedural graphics and sound!

// ==UserScript==
// @name         Drawaria DOOM Full Game 3D
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Transform Drawaria into a retro first-person maze shooter with procedural graphics and sound!
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Game Constants ---
    const MAP_WIDTH = 24;
    const MAP_HEIGHT = 24;
    const TILE_SIZE = 64; // Logical tile size for calculations, not pixel size
    const WALL_HEIGHT = TILE_SIZE; // All walls have same logical height for simple raycasting
    const FOV_DEGREES = 60; // Field of View in degrees
    const FOV_RADIANS = FOV_DEGREES * Math.PI / 180;

    // --- Game State ---
    let player = {
        x: MAP_WIDTH * TILE_SIZE / 2,
        y: MAP_HEIGHT * TILE_SIZE / 2,
        angle: Math.PI / 2, // Looking right initially
        speed: 80, // Pixels per second
        rotationSpeed: 1.5, // Radians per second
        health: 100,
        ammo: 100,
        fireCooldown: 0,
        maxFireCooldown: 0.2 // seconds
    };

    // Maze map: 0 = empty, 1 = wall. Define walls using 1s.
    // This creates a basic maze structure.
    const worldMap = [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ];

    // Enemies: id, position, state, animation frame, health, type (for drawing)
    const enemies = [
        { id: 'imp1', x: 5.5 * TILE_SIZE, y: 5.5 * TILE_SIZE, alive: true, frame: 0, animSpeed: 0.1, health: 30, type: 'imp' },
        { id: 'zombie1', x: 10.5 * TILE_SIZE, y: 15.5 * TILE_SIZE, alive: true, frame: 0, animSpeed: 0.05, health: 50, type: 'zombie' },
        { id: 'imp2', x: 18.5 * TILE_SIZE, y: 8.5 * TILE_SIZE, alive: true, frame: 0, animSpeed: 0.1, health: 30, type: 'imp' },
        { id: 'imp3', x: 20.5 * TILE_SIZE, y: 18.5 * TILE_SIZE, alive: true, frame: 0, animSpeed: 0.1, health: 30, type: 'imp' },
        { id: 'zombie2', x: 3.5 * TILE_SIZE, y: 18.5 * TILE_SIZE, alive: true, frame: 0, animSpeed: 0.05, health: 50, type: 'zombie' }
    ];

    const particles = []; // For muzzle flash, impact, etc.

    // --- Canvas Setup ---
    let gameCanvas;
    let ctx;
    let screenWidth, screenHeight;

    // --- Input State ---
    const keys = {};

    // --- Audio Context & Sounds ---
    let audioContext;
    let backgroundMusicOscillator;
    let backgroundMusicGain;
    let backgroundMusicNotes = [100, 110, 120, 110, 100, 90, 80, 90]; // Simple "melody"
    let backgroundMusicNoteIndex = 0;
    let backgroundMusicNextNoteTime = 0;

    // --- Game Loop Timing ---
    let lastFrameTime = 0;

    // --- Utility Functions (Colors, Drawing Primitives) ---

    // Convert hex to RGB for procedural shading
    function hexToRgb(hex) {
        let r = 0, g = 0, b = 0;
        // Handle #RGB format
        if (hex.length === 4) {
            r = parseInt(hex[1] + hex[1], 16);
            g = parseInt(hex[2] + hex[2], 16);
            b = parseInt(hex[3] + hex[3], 16);
        }
        // Handle #RRGGBB format
        else if (hex.length === 7) {
            r = parseInt(hex.substring(1, 3), 16);
            g = parseInt(hex.substring(3, 5), 16);
            b = parseInt(hex.substring(5, 7), 16);
        }
        return { r, g, b };
    }

    // Shade color based on a percentage (for distance shading and wall orientation)
    function shadeColor(color, percent) {
        let { r, g, b } = typeof color === 'string' ? hexToRgb(color) : color;
        r = Math.floor(Math.min(255, Math.max(0, r * (1 + percent))));
        g = Math.floor(Math.min(255, Math.max(0, g * (1 + percent))));
        b = Math.floor(Math.min(255, Math.max(0, b * (1 + percent))));
        return `rgb(${r},${g},${b})`;
    }

    // Procedural sprite drawing (simplified to rectangles with pixel-art-like features)
    function drawSprite(context, spriteData, currentTime) {
        context.save();
        context.translate(spriteData.x, spriteData.y + spriteData.height / 2); // Translate to sprite's screen position, centered vertically

        let fillColor;
        let glowColor = null;

        // Determine base color and glow based on sprite type
        if (spriteData.type === 'imp') {
            fillColor = '#800000'; // Dark red for imp
            glowColor = '#ff0000'; // Red glow
        } else if (spriteData.type === 'zombie') {
            fillColor = '#006400'; // Dark green for zombie
            glowColor = '#00ff00'; // Green glow
        } else if (spriteData.type === 'muzzle') {
            fillColor = '#FFFF00'; // Yellow for muzzle flash
            glowColor = '#FF8C00'; // Orange glow
            context.globalAlpha = spriteData.life; // Muzzle flash fades
        }


        // Simple animation: pulsating effect using current time
        const pulse = Math.sin(currentTime * 0.005) * 0.1 + 0.9; // Oscillates between 0.9 and 1.0
        const shadedFillColor = shadeColor(fillColor, (1 - spriteData.distance / 500) * 0.2); // Shade based on distance
        context.fillStyle = shadedFillColor;
        context.fillRect(-spriteData.width * pulse / 2, -spriteData.height * pulse / 2, spriteData.width * pulse, spriteData.height * pulse);

        // Add "Doom-like" elements for specific types
        if (spriteData.type === 'imp') {
            // Draw eyes (small rectangles)
            const eyeWidth = spriteData.width * 0.1;
            const eyeHeight = spriteData.height * 0.05;
            context.fillStyle = '#FFA500'; // Orange eyes
            context.fillRect(-spriteData.width * 0.2, -spriteData.height * 0.2, eyeWidth, eyeHeight);
            context.fillRect(spriteData.width * 0.1, -spriteData.height * 0.2, eyeWidth, eyeHeight);

            // Simple horns (triangles)
            context.fillStyle = '#4B0082'; // Indigo horns
            context.beginPath();
            context.moveTo(-spriteData.width * 0.3, -spriteData.height * 0.4);
            context.lineTo(-spriteData.width * 0.1, -spriteData.height * 0.2);
            context.lineTo(-spriteData.width * 0.2, -spriteData.height * 0.5);
            context.fill();
            context.beginPath();
            context.moveTo(spriteData.width * 0.3, -spriteData.height * 0.4);
            context.lineTo(spriteData.width * 0.1, -spriteData.height * 0.2);
            context.lineTo(spriteData.width * 0.2, -spriteData.height * 0.5);
            context.fill();
        } else if (spriteData.type === 'zombie') {
            // Draw a simplified "face" on the zombie
            context.fillStyle = '#8B4513'; // Brown for face details
            context.fillRect(-spriteData.width * 0.2, -spriteData.height * 0.2, spriteData.width * 0.4, spriteData.height * 0.1); // Mouth
            context.fillStyle = '#000000'; // Black eyes
            context.fillRect(-spriteData.width * 0.15, -spriteData.height * 0.3, spriteData.width * 0.05, spriteData.height * 0.05);
            context.fillRect(spriteData.width * 0.1, -spriteData.height * 0.3, spriteData.width * 0.05, spriteData.height * 0.05);
        }

        // Add glow effect for enemies (pulsating)
        if (glowColor) {
            for (let j = 0; j < 3; j++) {
                context.globalAlpha = 0.05 * (3 - j) * pulse; // Fade out effect with pulse
                context.fillStyle = glowColor;
                context.fillRect(-spriteData.width * (0.5 + j * 0.05), -spriteData.height * (0.5 + j * 0.05), spriteData.width * (1 + j * 0.1), spriteData.height * (1 + j * 0.1));
            }
        }
        context.globalAlpha = 1; // Reset alpha
        context.restore();
    }

    // --- Audio Functions ---
    function initAudio() {
        if (!audioContext) {
            audioContext = new (window.AudioContext || window.webkitAudioContext)();
            backgroundMusicGain = audioContext.createGain();
            backgroundMusicGain.gain.value = 0.05; // Low volume for background music
            backgroundMusicGain.connect(audioContext.destination);

            startBackgroundMusic();
        }
    }

    // Background music (simple oscillating tone for retro feel)
    function startBackgroundMusic() {
        if (backgroundMusicOscillator) {
            backgroundMusicOscillator.stop();
            backgroundMusicOscillator.disconnect();
        }
        backgroundMusicOscillator = audioContext.createOscillator();
        backgroundMusicOscillator.type = 'sawtooth'; // Aggressive waveform for DOOM-like sound

        backgroundMusicOscillator.connect(backgroundMusicGain);
        backgroundMusicOscillator.start();

        backgroundMusicNextNoteTime = audioContext.currentTime;
        scheduleNextMusicNote();
    }

    // Schedule the next note in the background music sequence
    function scheduleNextMusicNote() {
        if (audioContext && backgroundMusicOscillator) {
            backgroundMusicOscillator.frequency.setValueAtTime(backgroundMusicNotes[backgroundMusicNoteIndex], backgroundMusicNextNoteTime);
            backgroundMusicNoteIndex = (backgroundMusicNoteIndex + 1) % backgroundMusicNotes.length;
            backgroundMusicNextNoteTime += 0.25; // Each note lasts 0.25 seconds
            // Recursive call to schedule the next note, ensuring smooth playback
            const delay = (backgroundMusicNextNoteTime - audioContext.currentTime) * 1000;
            setTimeout(scheduleNextMusicNote, Math.max(0, delay));
        }
    }

    // Play a short sound effect
    function playSoundEffect(frequency, duration, type = 'square', volume = 0.5) {
        initAudio(); // Ensure audio context is initialized
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();

        oscillator.type = type;
        oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
        gainNode.gain.setValueAtTime(volume, audioContext.currentTime);

        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);

        oscillator.start();
        oscillator.stop(audioContext.currentTime + duration);
        gainNode.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + duration); // Fade out to prevent clicks
    }

    // --- Game Initialization ---
    function initGame() {
        // Find existing Drawaria canvas and hide its parent to make space
        const drawariaCanvas = document.getElementById('canvas');
        if (drawariaCanvas) {
            let parentContainer = drawariaCanvas.closest('#main');
            if (parentContainer) {
                parentContainer.style.display = 'none'; // Hide main drawing area
            }
            // Hide other Drawaria UI elements that might interfere
            const uiElementsToHide = [
                '#leftbar', '#rightbar', '#chatbox_messages',
                '#chatbox_textinput', '#roomcontrols', '#playerlist',
                '#discordprombox', '.footer', '#howtoplaybox', '.roomlist',
                '#login-midcol', '#login-rightcol', '#login-leftcol', '#login'
            ];
            uiElementsToHide.forEach(selector => {
                const el = document.querySelector(selector);
                if (el) el.style.display = 'none';
            });
        }

        // Create our own game canvas
        gameCanvas = document.createElement('canvas');
        gameCanvas.id = 'doomaria-game-canvas';
        document.body.appendChild(gameCanvas);

        // Style the game canvas to fill the screen
        gameCanvas.style.position = 'fixed';
        gameCanvas.style.top = '0';
        gameCanvas.style.left = '0';
        gameCanvas.style.width = '100vw';
        gameCanvas.style.height = '100vh';
        gameCanvas.style.zIndex = '99999'; // Ensure it's on top of everything
        gameCanvas.style.backgroundColor = 'black'; // Background outside game view

        ctx = gameCanvas.getContext('2d');
        resizeCanvas();

        // Add event listeners for input
        window.addEventListener('keydown', (e) => { keys[e.key] = true; });
        window.addEventListener('keyup', (e) => { keys[e.key] = false; });
        window.addEventListener('resize', resizeCanvas);
        window.addEventListener('click', handleMouseClick); // For firing

        // Start game loop
        requestAnimationFrame(gameLoop);

        // Initialize audio
        initAudio();
    }

    function resizeCanvas() {
        screenWidth = window.innerWidth;
        screenHeight = window.innerHeight;
        gameCanvas.width = screenWidth;
        gameCanvas.height = screenHeight;
    }

    // --- Game Loop ---
    function gameLoop(currentTime) {
        const deltaTime = (currentTime - lastFrameTime) / 1000; // seconds
        lastFrameTime = currentTime;

        updateGame(deltaTime, currentTime);
        renderGame(currentTime);

        requestAnimationFrame(gameLoop);
    }

    // --- Game State Update ---
    function updateGame(deltaTime, currentTime) {
        // Player movement
        let moveStep = player.speed * deltaTime;
        let rotStep = player.rotationSpeed * deltaTime;

        if (keys['w'] || keys['W']) { // Move forward
            let newX = player.x + Math.cos(player.angle) * moveStep;
            let newY = player.y + Math.sin(player.angle) * moveStep;
            if (worldMap[Math.floor(newY / TILE_SIZE)][Math.floor(newX / TILE_SIZE)] === 0) {
                player.x = newX;
                player.y = newY;
            } else {
                playSoundEffect(100, 0.1, 'triangle', 0.8); // Wall hit sound
            }
        }
        if (keys['s'] || keys['S']) { // Move backward
            let newX = player.x - Math.cos(player.angle) * moveStep;
            let newY = player.y - Math.sin(player.angle) * moveStep;
            if (worldMap[Math.floor(newY / TILE_SIZE)][Math.floor(newX / TILE_SIZE)] === 0) {
                player.x = newX;
                player.y = newY;
            } else {
                playSoundEffect(100, 0.1, 'triangle', 0.8); // Wall hit sound
            }
        }
        if (keys['a'] || keys['A']) { // Rotate left
            player.angle -= rotStep;
        }
        if (keys['d'] || keys['D']) { // Rotate right
            player.angle += rotStep;
        }

        // Keep angle within 0 to 2*PI
        player.angle = player.angle % (2 * Math.PI);
        if (player.angle < 0) player.angle += (2 * Math.PI);

        // Update enemies (simple movement)
        enemies.forEach(enemy => {
            if (enemy.alive) {
                // Simple oscillating movement for 'imp' type
                if (enemy.type === 'imp') {
                    const movementFactor = Math.sin(currentTime * enemy.animSpeed * 0.005);
                    enemy.x += Math.cos(enemy.frame * 0.1) * movementFactor;
                    enemy.y += Math.sin(enemy.frame * 0.1) * movementFactor;
                }
                enemy.frame += enemy.animSpeed * deltaTime;
            }
        });

        // Update particles (move and fade)
        for (let i = particles.length - 1; i >= 0; i--) {
            const p = particles[i];
            p.x += p.vx * deltaTime;
            p.y += p.vy * deltaTime;
            p.life -= deltaTime;
            if (p.life <= 0) {
                particles.splice(i, 1);
            }
        }

        // Update fire cooldown
        if (player.fireCooldown > 0) {
            player.fireCooldown -= deltaTime;
        }
    }

    // Handle mouse click for firing
    function handleMouseClick(event) {
        if (event.button === 0 && player.fireCooldown <= 0 && player.ammo > 0) { // Left click
            playSoundEffect(600, 0.1, 'sawtooth', 0.8); // Shoot sound
            player.ammo--;
            player.fireCooldown = player.maxFireCooldown;

            // Spawn muzzle flash particles
            const muzzleFlashX = screenWidth / 2;
            const muzzleFlashY = screenHeight / 2 + 50; // Below crosshair
            addParticleEffect(muzzleFlashX, muzzleFlashY, 15, {r:255,g:200,b:0}, 5, 200, 0.5);

            // Raycast for hit detection
            const hitRayAngle = player.angle; // Ray directly from player's view center
            const hitRayX = player.x;
            const hitRayY = player.y;

            const dx = Math.cos(hitRayAngle);
            const dy = Math.sin(hitRayAngle);

            let closestEnemyHit = null;
            let minDistance = Infinity;

            enemies.forEach(enemy => {
                if (!enemy.alive) return;

                // Simple AABB collision detection for enemy sprites
                const enemySize = (WALL_HEIGHT / (TILE_SIZE * 0.8)) * screenHeight; // Approximate sprite size
                const enemyHalfSize = enemySize * 0.5;

                // Create a simplified ray-rectangle intersection test in world space
                const enemyWorldMinX = enemy.x - enemyHalfSize;
                const enemyWorldMaxX = enemy.x + enemyHalfSize;
                const enemyWorldMinY = enemy.y - enemyHalfSize;
                const enemyWorldMaxY = enemy.y + enemyHalfSize;

                // Test if ray intersects the enemy's bounding box in world coordinates
                // This is a simplified test, not perfect for arbitrary rotated rays or proper 3D bounding box
                // A more accurate test would involve projecting the ray to 2D screen space after raycasting walls.
                // For a 2.5D setup, we fire a ray from player center and check if it passes through enemy.
                const rayLength = 500; // Max effective range of shot
                for (let step = 0; step < rayLength; step++) {
                    const currentRayWorldX = hitRayX + dx * step;
                    const currentRayWorldY = hitRayY + dy * step;

                    if (currentRayWorldX >= enemyWorldMinX && currentRayWorldX <= enemyWorldMaxX &&
                        currentRayWorldY >= enemyWorldMinY && currentRayWorldY <= enemyWorldMaxY) {

                        const distToEnemy = Math.sqrt(Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.y - player.y, 2));
                        if (distToEnemy < minDistance) {
                            closestEnemyHit = enemy;
                            minDistance = distToEnemy;
                            break; // Hit, no need to check further along this ray for this enemy
                        }
                    }
                }
            });

            if (closestEnemyHit) {
                closestEnemyHit.health -= 25; // Damage enemy
                playSoundEffect(200, 0.1, 'sine', 0.8); // Enemy hit sound
                addParticleEffect(screenWidth/2, screenHeight/2, 20, {r:255,g:0,b:0}, 10, 300, 0.8); // Blood effect

                if (closestEnemyHit.health <= 0) {
                    closestEnemyHit.alive = false;
                    playSoundEffect(50, 0.5, 'triangle', 0.8); // Enemy death sound
                    addParticleEffect(screenWidth/2, screenHeight/2, 50, {r:255,g:100,b:0}, 20, 500, 1.0); // Explosion
                }
            }
        }
    }


    // Add a particle effect (like explosions, blood, muzzle flash)
    function addParticleEffect(centerX, centerY, count, color, sizeRange, speedRange, lifeRange) {
        for (let i = 0; i < count; i++) {
            particles.push({
                x: centerX,
                y: centerY,
                vx: (Math.random() - 0.5) * speedRange,
                vy: (Math.random() - 0.5) * speedRange,
                life: Math.random() * lifeRange + 0.1,
                maxLifeAlpha: 1, // Store initial alpha for fading
                size: Math.random() * sizeRange + 1,
                color: color
            });
        }
    }


    // --- Game Rendering ---
    function renderGame(currentTime) {
        // Clear canvas
        ctx.clearRect(0, 0, screenWidth, screenHeight);

        // Sky and Floor (simple gradients for atmospheric perspective)
        let skyGradient = ctx.createLinearGradient(0, 0, 0, screenHeight / 2);
        skyGradient.addColorStop(0, '#000000'); // Black (space)
        skyGradient.addColorStop(0.5, '#1A1A1A'); // Dark gray
        skyGradient.addColorStop(1, '#333333'); // Lighter gray (distant fog)
        ctx.fillStyle = skyGradient;
        ctx.fillRect(0, 0, screenWidth, screenHeight / 2);

        let floorGradient = ctx.createLinearGradient(0, screenHeight / 2, 0, screenHeight);
        floorGradient.addColorStop(0, '#444444'); // Lighter gray (near floor)
        floorGradient.addColorStop(1, '#111111'); // Darker gray (distant floor)
        ctx.fillStyle = floorGradient;
        ctx.fillRect(0, screenHeight / 2, screenWidth, screenHeight / 2);

        // Raycasting for walls
        const numRays = screenWidth; // One ray per pixel column for resolution
        const angleStep = FOV_RADIANS / numRays;

        for (let i = 0; i < numRays; i++) {
            let rayAngle = (player.angle - FOV_RADIANS / 2) + (i * angleStep);

            let currentMapX = Math.floor(player.x / TILE_SIZE);
            let currentMapY = Math.floor(player.y / TILE_SIZE);

            let deltaDistX = Math.abs(TILE_SIZE / Math.cos(rayAngle));
            let deltaDistY = Math.abs(TILE_SIZE / Math.sin(rayAngle));

            let stepX, stepY;
            let sideDistX, sideDistY;

            if (Math.cos(rayAngle) < 0) {
                stepX = -1;
                sideDistX = (player.x - currentMapX * TILE_SIZE) / Math.abs(Math.cos(rayAngle));
            } else {
                stepX = 1;
                sideDistX = ((currentMapX + 1) * TILE_SIZE - player.x) / Math.abs(Math.cos(rayAngle));
            }
            if (Math.sin(rayAngle) < 0) {
                stepY = -1;
                sideDistY = (player.y - currentMapY * TILE_SIZE) / Math.abs(Math.sin(rayAngle));
            } else {
                stepY = 1;
                sideDistY = ((currentMapY + 1) * TILE_SIZE - player.y) / Math.abs(Math.sin(rayAngle));
            }

            let hitWall = false;
            let wallSide = 0; // 0 = vertical wall hit (X-axis parallel), 1 = horizontal wall hit (Y-axis parallel)
            let wallDist = 0;

            while (!hitWall && wallDist < 10000) { // Max distance to prevent infinite loop
                if (sideDistX < sideDistY) {
                    sideDistX += deltaDistX;
                    currentMapX += stepX;
                    wallSide = 0;
                } else {
                    sideDistY += deltaDistY;
                    currentMapY += stepY;
                    wallSide = 1;
                }

                if (currentMapX >= 0 && currentMapX < MAP_WIDTH && currentMapY >= 0 && currentMapY < MAP_HEIGHT) {
                    if (worldMap[currentMapY][currentMapX] > 0) {
                        hitWall = true;
                    }
                } else {
                    // Ray went out of bounds, treat as hit at max distance
                    hitWall = true;
                    wallDist = 10000;
                }
            }

            // Calculate distance to wall
            if (wallSide === 0) {
                wallDist = (currentMapX * TILE_SIZE - player.x + (1 - stepX) / 2 * TILE_SIZE) / Math.cos(rayAngle);
            } else {
                wallDist = (currentMapY * TILE_SIZE - player.y + (1 - stepY) / 2 * TILE_SIZE) / Math.sin(rayAngle);
            }

            // Correct fisheye lens distortion
            wallDist *= Math.cos(player.angle - rayAngle);

            // Calculate wall height on screen
            let wallSliceHeight = (WALL_HEIGHT / wallDist) * screenHeight;

            let drawStart = (screenHeight / 2) - (wallSliceHeight / 2);
            let drawEnd = (screenHeight / 2) + (wallSliceHeight / 2);

            // Apply shading based on distance and wall orientation for depth
            let baseColor = '#777777'; // Gray wall
            if (wallSide === 1) { // Horizontal walls are darker (common raycaster trick)
                baseColor = '#555555';
            }

            // Shade more based on distance
            const shadeFactor = Math.min(1, Math.max(0, wallDist / (TILE_SIZE * 8))); // Max shade at 8 tiles distance
            const finalColor = shadeColor(baseColor, -shadeFactor * 0.7); // Darken by up to 70%

            ctx.fillStyle = finalColor;
            ctx.fillRect(i, drawStart, 1, wallSliceHeight);
        }

        // Sort and draw sprites (enemies & muzzle flash)
        const visibleSprites = [];

        // Add enemies to visible sprites list
        enemies.forEach(enemy => {
            if (enemy.alive) {
                // Calculate distance from player to enemy
                const distToEnemy = Math.sqrt(Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.y - player.y, 2));

                // If enemy is too close or too far, don't draw (optimization/clipping)
                if (distToEnemy < 10 || distToEnemy > 1000) return;

                // Calculate angle of enemy relative to player
                let angleToEnemy = Math.atan2(enemy.y - player.y, enemy.x - player.x);
                let relativeAngle = angleToEnemy - player.angle;

                // Normalize relative angle to be within -PI to PI
                if (relativeAngle > Math.PI) relativeAngle -= 2 * Math.PI;
                if (relativeAngle < -Math.PI) relativeAngle += 2 * Math.PI;

                // Check if enemy is within FOV
                if (Math.abs(relativeAngle) < FOV_RADIANS / 2) {
                    // Project enemy onto screen
                    const spriteScreenX = screenWidth / 2 + (relativeAngle / (FOV_RADIANS / 2)) * (screenWidth / 2);
                    const spriteHeight = (WALL_HEIGHT / distToEnemy) * screenHeight;
                    const spriteWidth = spriteHeight * 0.8; // Adjust aspect ratio if desired

                    visibleSprites.push({
                        id: enemy.id,
                        x: spriteScreenX,
                        y: (screenHeight / 2) - (spriteHeight / 2),
                        width: spriteWidth,
                        height: spriteHeight,
                        distance: distToEnemy,
                        type: enemy.type,
                        frame: enemy.frame,
                        health: enemy.health
                    });
                }
            }
        });

        // Sort sprites by distance (farthest to nearest) for proper drawing order
        visibleSprites.sort((a, b) => b.distance - a.distance);

        visibleSprites.forEach(sprite => {
            drawSprite(ctx, sprite, currentTime);
        });

        // Render particles
        ctx.globalAlpha = 0.8;
        particles.forEach(p => {
            ctx.fillStyle = `rgba(${p.color.r},${p.color.g},${p.color.b},${p.life * p.maxLifeAlpha})`;
            ctx.fillRect(p.x, p.y, p.size, p.size);
        });
        ctx.globalAlpha = 1;


        // --- HUD / UI Elements ---
        ctx.font = 'bold 20px "Press Start 2P", monospace'; // Use monospace as fallback
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';

        // Doom-style health bar
        ctx.fillStyle = '#6a0a0a'; // Dark red for health bar background
        ctx.fillRect(screenWidth * 0.02, screenHeight * 0.88, screenWidth * 0.25, screenHeight * 0.04);
        ctx.fillStyle = '#ff0000'; // Bright red for health value
        ctx.fillRect(screenWidth * 0.02, screenHeight * 0.88, screenWidth * 0.25 * (player.health / 100), screenHeight * 0.04);
        ctx.strokeStyle = '#FFFFFF';
        ctx.lineWidth = 2;
        ctx.strokeRect(screenWidth * 0.02, screenHeight * 0.88, screenWidth * 0.25, screenHeight * 0.04);
        ctx.fillStyle = 'white';
        ctx.fillText(`HEALTH: ${player.health}%`, screenWidth * 0.02 + 10, screenHeight * 0.88 + 8);


        // Doom-style ammo bar
        ctx.fillStyle = '#0a0a6a'; // Dark blue for ammo bar background
        ctx.fillRect(screenWidth * 0.73, screenHeight * 0.88, screenWidth * 0.25, screenHeight * 0.04);
        ctx.fillStyle = '#0000ff'; // Bright blue for ammo value
        ctx.fillRect(screenWidth * 0.73, screenHeight * 0.88, screenWidth * 0.25 * (player.ammo / 100), screenHeight * 0.04);
        ctx.strokeRect(screenWidth * 0.73, screenHeight * 0.88, screenWidth * 0.25, screenHeight * 0.04);
        ctx.fillStyle = 'white';
        ctx.fillText(`AMMO: ${player.ammo}`, screenWidth * 0.73 + 10, screenHeight * 0.88 + 8);

        // Crosshair
        ctx.strokeStyle = 'lime';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(screenWidth / 2 - 15, screenHeight / 2);
        ctx.lineTo(screenWidth / 2 + 15, screenHeight / 2);
        ctx.moveTo(screenWidth / 2, screenHeight / 2 - 15);
        ctx.lineTo(screenWidth / 2, screenHeight / 2 + 15);
        ctx.stroke();

        // Doom-style "face" (simplified: just a yellow square with pulsating glow)
        const faceSize = screenHeight * 0.08;
        const faceX = screenWidth / 2 - faceSize / 2;
        const faceY = screenHeight * 0.9 - faceSize / 2; // Position it lower
        ctx.fillStyle = '#FFD700'; // Gold for face
        ctx.fillRect(faceX, faceY, faceSize, faceSize);

        // Face glow (pulsating effect)
        const glowFactor = Math.sin(currentTime * 0.005) * 0.5 + 0.5; // Oscillates between 0.5 and 1
        for (let j = 0; j < 3; j++) {
            ctx.globalAlpha = 0.05 * (3 - j) * glowFactor;
            ctx.fillStyle = '#FFD700'; // Gold glow
            ctx.beginPath();
            ctx.arc(faceX + faceSize / 2, faceY + faceSize / 2, faceSize * (0.6 + j * 0.1), 0, Math.PI * 2);
            ctx.fill();
        }
        ctx.globalAlpha = 1; // Reset alpha

        // "DOOMARIA" Title (top center, animated glow)
        ctx.font = 'bold 48px "Press Start 2P", monospace'; // Large title font
        ctx.textAlign = 'center';
        ctx.textBaseline = 'top';
        ctx.fillStyle = 'orange';
        const titleText = "DOOMARIA";
        ctx.fillText(titleText, screenWidth / 2, screenHeight * 0.02);

        // Title glow (pulsating effect)
        const titleGlowFactor = Math.sin(currentTime * 0.003) * 0.5 + 0.5; // Slower oscillation
        for (let j = 0; j < 5; j++) {
            ctx.globalAlpha = 0.02 * (5 - j) * titleGlowFactor;
            ctx.fillStyle = 'gold';
            // Offset for blurry glow
            ctx.fillText(titleText, screenWidth / 2 + j * 0.5, screenHeight * 0.02 + j * 0.5);
            ctx.fillText(titleText, screenWidth / 2 - j * 0.5, screenHeight * 0.02 - j * 0.5);
        }
        ctx.globalAlpha = 1;
    }

    // --- Entry Point ---
    // Wait for the DOM to be fully loaded, including Drawaria's scripts and canvas
    window.addEventListener('load', () => {
        // Delay initialization slightly to ensure Drawaria's canvas is ready
        setTimeout(initGame, 500); // Increased delay for robustness
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址