Skip to content
McStay Designs

Wordpress tech Development

McStay Designs

Wordpress tech Development

  • Network benchmark
  • Get a Quote
  • Free code Projects
  • Free plugins
  • About
  • Network benchmark
  • Get a Quote
  • Free code Projects
  • Free plugins
  • About
McStay Designs

Wordpress tech Development

McStay Designs

Wordpress tech Development

  • Network benchmark
  • Get a Quote
  • Free code Projects
  • Free plugins
  • About
  • Network benchmark
  • Get a Quote
  • Free code Projects
  • Free plugins
  • About
Home/Free code Projects/WP Retro Asteroids Game Pro Master file
Free code Projects

WP Retro Asteroids Game Pro Master file

By mkmcst
September 12, 2026 12 Min Read
0
<?php
/**
 * Plugin Name:       WP Retro Asteroids Game Pro
 * Plugin URI:        https://mcstaydesigns.cc
 * Description:       Pro-grade, fully responsive retro vector Asteroids game with built-in Web Audio retro sound effects, custom branding, progressive difficulty scaling, high-DPI canvas rendering, and shortcode support.
 * Version:           1.4.1
 * Author:            McStay Designs
 * Author URI:        https://mcstaydesigns.cc
 * License:           GPLv2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       wp-retro-asteroids
 * Requires at least: 5.8
 * Requires PHP:      7.4
 */
if (!defined('ABSPATH')) exit; // Exit if accessed directly

/**
 * Register plugin assets properly via WordPress enqueue system.
 */
function wp_retro_asteroids_register_assets() {
    wp_register_style('wp-retro-asteroids-fonts', 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
    wp_register_style('wp-retro-asteroids-css', false, array('wp-retro-asteroids-fonts'));
    wp_register_script('wp-retro-asteroids-js', '', array(), '1.4.1', true);
}
add_action('wp_enqueue_scripts', 'wp_retro_asteroids_register_assets');

/**
 * Render the Asteroids Arcade shortcode.
 */
function wp_retro_asteroids_shortcode() {
    $instance_id = 'astGame_' . wp_generate_password(6, false, false);

    wp_enqueue_style('wp-retro-asteroids-fonts');
    wp_enqueue_style('wp-retro-asteroids-css');
    wp_add_inline_style('wp-retro-asteroids-css', "
        .asteroids-arcade-container {
            position: relative;
            width: 100%;
            max-width: 900px;
            margin: 20px auto;
            background: #030712;
            border: 3px solid #00f0ff;
            border-radius: 12px;
            box-shadow: 0 0 35px rgba(0, 240, 255, 0.45), inset 0 0 20px rgba(0, 240, 255, 0.25);
            font-family: 'Press Start 2P', 'Courier New', monospace;
            overflow: hidden;
            user-select: none;
            touch-action: manipulation;
            transition: all 0.2s ease-in-out;
            outline: none;
            box-sizing: border-box;
        }

        .asteroids-arcade-container.is-fullscreen {
            position: fixed !important;
            top: 0 !important;
            left: 0 !important;
            width: 100vw !important;
            height: 100vh !important;
            max-width: 100vw !important;
            margin: 0 !important;
            border: none !important;
            border-radius: 0 !important;
            z-index: 999999 !important;
            background: #000;
        }
        
        .asteroids-canvas-wrapper {
            position: relative;
            width: 100%;
            padding-top: 75%; /* 4:3 Aspect Ratio */
            background: #000;
        }

        .asteroids-arcade-container canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: block;
        }
        
        /* CRT Scanline Shader Effect */
        .asteroids-arcade-container::after {
            content: ' '; 
            position: absolute; 
            top: 0; left: 0; bottom: 0; right: 0;
            background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.35) 50%);
            background-size: 100% 4px; 
            pointer-events: none; 
            z-index: 5;
        }

        /* Scaled Arcade HUD */
        .arcade-hud {
            position: absolute; top: 18px; left: 20px; right: 20px;
            display: flex; justify-content: space-between; align-items: center; color: #00f0ff;
            font-size: clamp(0.75rem, 2vw, 1.15rem); 
            font-weight: bold; 
            text-shadow: 0 0 10px #00f0ff; 
            z-index: 10;
        }
        .hud-left { display: flex; gap: 20px; flex-wrap: wrap; }
        .hud-controls { display: flex; gap: 8px; }

        .hud-btn {
            background: rgba(0, 240, 255, 0.15);
            border: 2px solid #00f0ff;
            color: #00f0ff;
            padding: 8px 12px;
            font-family: inherit;
            font-size: clamp(0.6rem, 1.5vw, 0.85rem);
            font-weight: bold;
            cursor: pointer;
            border-radius: 6px;
            box-shadow: 0 0 10px rgba(0, 240, 255, 0.3);
            transition: all 0.2s ease;
        }
        .hud-btn:hover {
            background: #00f0ff;
            color: #000;
            box-shadow: 0 0 16px #00f0ff;
        }

        .level-banner {
            position: absolute;
            top: 40%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #00f0ff;
            font-size: clamp(1.4rem, 4vw, 2.5rem);
            text-shadow: 0 0 20px #00f0ff;
            z-index: 18;
            pointer-events: none;
            transition: opacity 0.5s ease;
        }

        .arcade-overlay {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0, 0, 0, 0.92); display: flex; flex-direction: column;
            justify-content: center; align-items: center; color: #ff0055; z-index: 20; text-align: center;
            padding: 24px; box-sizing: border-box;
        }
        .arcade-overlay h2 { 
            font-size: clamp(2rem, 5vw, 3.8rem); 
            margin: 0 0 10px 0; 
            color: #00f0ff; 
            text-shadow: 0 0 20px #00f0ff, 0 0 40px #00f0ff; 
        }
        .arcade-overlay .brand-credits {
            font-size: clamp(0.65rem, 1.8vw, 0.95rem);
            color: #ff0055;
            margin-bottom: 24px;
            letter-spacing: 1px;
            text-shadow: 0 0 10px #ff0055;
        }
        .arcade-overlay .brand-credits a {
            color: #ff0055;
            text-decoration: underline;
            transition: color 0.2s ease;
        }
        .arcade-overlay .brand-credits a:hover {
            color: #ffffff;
            text-shadow: 0 0 15px #ffffff;
        }
        .arcade-overlay p { 
            color: #fff; 
            font-size: clamp(0.75rem, 2vw, 1.1rem); 
            letter-spacing: 2px; 
            line-height: 1.6;
        }

        /* Large Flight Manual Overlay */
        .overlay-card {
            background: rgba(5, 12, 25, 0.96);
            border: 2px solid #00f0ff;
            box-shadow: 0 0 25px rgba(0, 240, 255, 0.4);
            padding: 28px;
            border-radius: 8px;
            max-width: 650px;
            width: 100%;
            color: #e0f7fc;
            text-shadow: none;
        }
        .card-title {
            color: #ff0055;
            margin: 0 0 20px 0;
            font-size: clamp(0.9rem, 2.5vw, 1.4rem);
            letter-spacing: 2px;
            text-shadow: 0 0 10px #ff0055;
        }
        .help-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            text-align: left;
            margin-bottom: 20px;
        }
        .help-section h4 { 
            color: #00f0ff; 
            margin: 0 0 12px 0; 
            font-size: clamp(0.7rem, 1.8vw, 0.95rem); 
        }
        .help-section ul { list-style: none; padding: 0; margin: 0; font-size: clamp(0.55rem, 1.5vw, 0.75rem); line-height: 1.8; color: #a0c0d0; }
        .help-section ul li span { color: #fff; font-weight: bold; }
        .help-rules {
            border-top: 1px dashed rgba(0, 240, 255, 0.4);
            padding-top: 14px;
            font-size: clamp(0.6rem, 1.5vw, 0.8rem);
            line-height: 1.6;
            color: #d0e0f0;
        }
        .overlay-close-btn {
            margin-top: 20px;
            background: #00f0ff;
            color: #000;
            border: none;
            padding: 12px 24px;
            font-size: clamp(0.7rem, 1.8vw, 0.9rem);
            font-weight: bold;
            cursor: pointer;
            font-family: inherit;
            border-radius: 6px;
            box-shadow: 0 0 14px #00f0ff;
            transition: transform 0.1s ease;
        }
        .overlay-close-btn:active { transform: scale(0.96); }

        .hidden { display: none !important; }
        
        /* Pro Mobile Touch Controls */
        .touch-controls {
            display: none; 
            position: absolute; 
            bottom: 20px; 
            left: 15px; 
            right: 15px;
            justify-content: space-between; 
            z-index: 15;
        }
        @media (max-width: 768px) {
            .touch-controls { display: flex; }
            .help-grid { grid-template-columns: 1fr; }
        }
        .touch-group { display: flex; gap: 10px; }
        .t-btn {
            background: rgba(0, 240, 255, 0.2); 
            border: 2px solid #00f0ff; 
            color: #00f0ff;
            padding: 16px 22px; 
            font-size: 1.3rem; 
            font-weight: bold; 
            border-radius: 8px;
            box-shadow: 0 0 12px rgba(0, 240, 255, 0.4);
            user-select: none;
            touch-action: manipulation;
        }
        .t-btn:active { background: rgba(0, 240, 255, 0.5); }
        .fire-btn { 
            background: rgba(255, 0, 85, 0.25); 
            border-color: #ff0055; 
            color: #ff0055; 
            box-shadow: 0 0 12px rgba(255, 0, 85, 0.5); 
            padding-left: 28px;
            padding-right: 28px;
        }
        .fire-btn:active { background: rgba(255, 0, 85, 0.6); }
    ");

    wp_enqueue_script('wp-retro-asteroids-js');
    wp_add_inline_script('wp-retro-asteroids-js', "
    (function() {
        function initAsteroidsGame(wrapperId) {
            const container = document.getElementById(wrapperId);
            if (!container) return;
            const canvas = container.querySelector('canvas');
            if (!canvas) return;
            const ctx = canvas.getContext('2d');

            const scoreEl = container.querySelector('.astScore');
            const levelEl = container.querySelector('.astLevel');
            const livesEl = container.querySelector('.astLives');
            const highScoreEl = container.querySelector('.astHighScore');
            const levelBanner = container.querySelector('.astLevelBanner');
            const startScreen = container.querySelector('.astStartScreen');
            const gameOverScreen = container.querySelector('.astGameOverScreen');
            const helpOverlay = container.querySelector('.astHelpOverlay');
            const btnStartGame = container.querySelector('.btnStartGame');
            
            const btnToggleFS = container.querySelector('.btnToggleFS');
            const btnToggleAudio = container.querySelector('.btnToggleAudio');
            const btnToggleHelp = container.querySelector('.btnToggleHelp');
            const btnCloseHelp = container.querySelector('.btnCloseHelp');

            // --- RETRO SOUND SYNTHESIZER (WEB AUDIO API) ---
            let audioCtx = null;
            let audioMuted = false;
            let thrustOsc = null;
            let thrustGain = null;

            function initAudio() {
                if (!audioCtx) {
                    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                }
                if (audioCtx.state === 'suspended') {
                    audioCtx.resume();
                }
            }

            btnToggleAudio.addEventListener('click', () => {
                audioMuted = !audioMuted;
                btnToggleAudio.innerText = audioMuted ? '🔇 MUTE' : '🔊 SOUND';
                if (audioMuted && thrustGain) {
                    thrustGain.gain.setValueAtTime(0, audioCtx ? audioCtx.currentTime : 0);
                }
            });

            function playLaserSound() {
                if (audioMuted) return;
                initAudio();
                const osc = audioCtx.createOscillator();
                const gain = audioCtx.createGain();
                osc.type = 'sawtooth';
                osc.frequency.setValueAtTime(880, audioCtx.currentTime);
                osc.frequency.exponentialRampToValueAtTime(110, audioCtx.currentTime + 0.12);
                gain.gain.setValueAtTime(0.18, audioCtx.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.12);
                osc.connect(gain);
                gain.connect(audioCtx.destination);
                osc.start();
                osc.stop(audioCtx.currentTime + 0.12);
            }

            function playExplosionSound(tier) {
                if (audioMuted) return;
                initAudio();
                const bufferSize = audioCtx.sampleRate * 0.25;
                const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
                const data = buffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) {
                    data[i] = Math.random() * 2 - 1;
                }
                const noise = audioCtx.createBufferSource();
                noise.buffer = buffer;

                const filter = audioCtx.createBiquadFilter();
                filter.type = 'lowpass';
                const freq = tier === 3 ? 200 : tier === 2 ? 400 : 800;
                filter.frequency.setValueAtTime(freq, audioCtx.currentTime);
                filter.frequency.linearRampToValueAtTime(30, audioCtx.currentTime + 0.25);

                const gain = audioCtx.createGain();
                gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.25);

                noise.connect(filter);
                filter.connect(gain);
                gain.connect(audioCtx.destination);
                noise.start();
            }

            function playShipHitSound() {
                if (audioMuted) return;
                initAudio();
                const osc = audioCtx.createOscillator();
                const gain = audioCtx.createGain();
                osc.type = 'square';
                osc.frequency.setValueAtTime(150, audioCtx.currentTime);
                osc.frequency.setValueAtTime(90, audioCtx.currentTime + 0.1);
                osc.frequency.setValueAtTime(50, audioCtx.currentTime + 0.2);
                gain.gain.setValueAtTime(0.35, audioCtx.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.45);
                osc.connect(gain);
                gain.connect(audioCtx.destination);
                osc.start();
                osc.stop(audioCtx.currentTime + 0.45);
            }

            function updateThrustSound(isThrusting) {
                if (audioMuted || !audioCtx) return;
                if (isThrusting) {
                    if (!thrustOsc) {
                        thrustOsc = audioCtx.createOscillator();
                        thrustGain = audioCtx.createGain();
                        thrustOsc.type = 'sawtooth';
                        thrustOsc.frequency.setValueAtTime(65, audioCtx.currentTime);
                        thrustGain.gain.setValueAtTime(0.08, audioCtx.currentTime);
                        thrustOsc.connect(thrustGain);
                        thrustGain.connect(audioCtx.destination);
                        thrustOsc.start();
                    }
                } else if (thrustOsc) {
                    thrustGain.gain.setValueAtTime(0, audioCtx.currentTime);
                    thrustOsc.stop();
                    thrustOsc.disconnect();
                    thrustOsc = null;
                }
            }

            // --- GAME PARAMETERS ---
            const SHIP_SIZE = 24;
            const ROTATION_SPEED = Math.PI * 1.6;
            const THRUST = 220;
            const FRICTION = 0.5;
            const LASER_SPEED = 500;
            const LASER_DIST_LIMIT = 0.6;
            const BASE_ASTEROID_SPEED = 12; // Tuned down for Level 1 drift

            let score = 0;
            let currentLevel = 1;
            let highScore = localStorage.getItem('ast_highscore') || 0;
            let lives = 3;
            let gameStarted = false;
            let gameOver = false;
            let isPaused = false;
            highScoreEl.innerText = String(highScore).padStart(4, '0');

            const keys = { left: false, right: false, thrust: false, fire: false };

            function resizeCanvas() {
                const rect = canvas.getBoundingClientRect();
                canvas.width = rect.width;
                canvas.height = rect.height;
            }
            window.addEventListener('resize', resizeCanvas);
            resizeCanvas();

            // Fullscreen Controller
            btnToggleFS.addEventListener('click', () => {
                if (!document.fullscreenElement && !container.classList.contains('is-fullscreen')) {
                    if (container.requestFullscreen) container.requestFullscreen();
                    else container.classList.add('is-fullscreen');
                    btnToggleFS.innerText = '🗗 MINIMIZE';
                } else {
                    if (document.exitFullscreen && document.fullscreenElement) document.exitFullscreen();
                    container.classList.remove('is-fullscreen');
                    btnToggleFS.innerText = '⛶ FULLSCREEN';
                }
                setTimeout(resizeCanvas, 100);
            });

            document.addEventListener('fullscreenchange', () => {
                if (!document.fullscreenElement) {
                    container.classList.remove('is-fullscreen');
                    btnToggleFS.innerText = '⛶ FULLSCREEN';
                }
                setTimeout(resizeCanvas, 100);
            });

            btnToggleHelp.addEventListener('click', () => { isPaused = true; helpOverlay.classList.remove('hidden'); });
            btnCloseHelp.addEventListener('click', () => { isPaused = false; helpOverlay.classList.add('hidden'); });

            function showLevelBanner(level) {
                levelBanner.innerText = 'STAGE ' + level;
                levelBanner.classList.remove('hidden');
                setTimeout(() => levelBanner.classList.add('hidden'), 1800);
            }

            function createShip() {
                return {
                    x: canvas.width / 2, y: canvas.height / 2, r: SHIP_SIZE / 2,
                    angle: 90 * Math.PI / 180, thrust: { x: 0, y: 0 },
                    lasers: [], canShoot: true, explodeTime: 0
                };
            }

            let ship = createShip();
            let asteroids = [];
            let particles = [];

            function spawnAsteroids(count, level = 3) {
                for (let i = 0; i < count; i++) {
                    let x, y;
                    do {
                        x = Math.floor(Math.random() * canvas.width);
                        y = Math.floor(Math.random() * canvas.height);
                    } while (distBetweenPoints(ship.x, ship.y, x, y) < 160);
                    asteroids.push(createAsteroid(x, y, level));
                }
            }

            function createAsteroid(x, y, tier) {
                // Slower baseline speed scaling per level
                const speedMultiplier = Math.min(1 + (currentLevel - 1) * 0.2, 2.8);
                const currentSpeed = BASE_ASTEROID_SPEED * speedMultiplier;
                const radius = tier === 3 ? 42 : tier === 2 ? 22 : 12;
                const vertices = 12;
                const offsets = [];
                for (let i = 0; i < vertices; i++) offsets.push(Math.random() * 0.4 + 0.8);

                return {
                    x, y,
                    vx: (Math.random() * currentSpeed - currentSpeed / 2) / (tier === 3 ? 1 : tier === 2 ? 1.3 : 1.7),
                    vy: (Math.random() * currentSpeed - currentSpeed / 2) / (tier === 3 ? 1 : tier === 2 ? 1.3 : 1.7),
                    r: radius, level: tier, angle: Math.random() * Math.PI * 2,
                    rotSpeed: (Math.random() - 0.5) * 0.015 * speedMultiplier,
                    vertices, offsets
                };
            }

            function distBetweenPoints(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); }

            function createExplosion(x, y, color = '#00f0ff', count = 25) {
                for (let i = 0; i < count; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = Math.random() * 2.5 + 0.5;
                    particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, alpha: 1, color });
                }
            }

            // Keyboard Controls
            window.addEventListener('keydown', (e) => {
                if (!container.contains(document.activeElement)) return;
                initAudio();

                const handledKeys = ['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
                if (handledKeys.includes(e.code)) e.preventDefault();

                if (!gameStarted && (e.code === 'Enter' || e.code === 'Space')) return startGame();
                if (gameOver && e.code === 'Enter') return restartGame();
                if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
                if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
                if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.thrust = true;
                if (e.code === 'Space') keys.fire = true;
            }, { passive: false });

            window.addEventListener('keyup', (e) => {
                if (!container.contains(document.activeElement)) return;

                const handledKeys = ['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
                if (handledKeys.includes(e.code)) e.preventDefault();

                if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
                if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
                if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.thrust = false;
                if (e.code === 'Space') { keys.fire = false; ship.canShoot = true; }
            }, { passive: false });

            function bindTouch(selector, keyProp) {
                const btn = container.querySelector(selector);
                if (!btn) return;
                btn.addEventListener('touchstart', (e) => { e.preventDefault(); initAudio(); keys[keyProp] = true; });
                btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyProp] = false; if (keyProp === 'fire') ship.canShoot = true; });
            }
            bindTouch('.btnLeft', 'left'); 
            bindTouch('.btnRight', 'right'); 
            bindTouch('.btnThrust', 'thrust'); 
            bindTouch('.btnFire', 'fire');

            btnStartGame.addEventListener('click', startGame);
            gameOverScreen.addEventListener('click', restartGame);

            function startGame() {
                initAudio();
                gameStarted = true;
                startScreen.classList.add('hidden');
                showLevelBanner(1);
                spawnAsteroids(4);
            }

            function shootLaser() {
                if (ship.canShoot && ship.lasers.length < 10 && ship.explodeTime === 0) {
                    ship.lasers.push({
                        x: ship.x + 4 / 3 * ship.r * Math.cos(ship.angle),
                        y: ship.y - 4 / 3 * ship.r * Math.sin(ship.angle),
                        vx: LASER_SPEED * Math.cos(ship.angle),
                        vy: -LASER_SPEED * Math.sin(ship.angle),
                        dist: 0
                    });
                    ship.canShoot = false;
                    playLaserSound();
                }
            }

            function destroyAsteroid(index) {
                const a = asteroids[index];
                playExplosionSound(a.level);
                createExplosion(a.x, a.y, a.level === 3 ? '#ff0055' : a.level === 2 ? '#00f0ff' : '#ffffff');
                if (a.level > 1) {
                    asteroids.push(createAsteroid(a.x, a.y, a.level - 1));
                    asteroids.push(createAsteroid(a.x, a.y, a.level - 1));
                }
                score += a.level === 3 ? 20 : a.level === 2 ? 50 : 100;
                scoreEl.innerText = String(score).padStart(4, '0');
                if (score > highScore) {
                    highScore = score; localStorage.setItem('ast_highscore', highScore);
                    highScoreEl.innerText = String(highScore).padStart(4, '0');
                }
                asteroids.splice(index, 1);
                
                if (asteroids.length === 0) {
                    currentLevel++;
                    levelEl.innerText = currentLevel;
                    showLevelBanner(currentLevel);
                    spawnAsteroids(Math.min(3 + currentLevel, 9));
                }
            }

            function killShip() {
                ship.explodeTime = 30;
                playShipHitSound();
                createExplosion(ship.x, ship.y, '#ff0055', 45);
                lives--; livesEl.innerText = lives;
                if (lives <= 0) { gameOver = true; gameOverScreen.classList.remove('hidden'); }
            }

            function restartGame() {
                score = 0; currentLevel = 1; lives = 3; gameOver = false;
                scoreEl.innerText = '0000'; levelEl.innerText = '1'; livesEl.innerText = lives;
                gameOverScreen.classList.add('hidden');
                ship = createShip(); asteroids = []; 
                showLevelBanner(1);
                spawnAsteroids(4);
            }

            let lastTime = performance.now();
            function update() {
                const now = performance.now();
                const dt = Math.min((now - lastTime) / 1000, 0.1);
                lastTime = now;

                if (gameStarted && !gameOver && !isPaused) {
                    if (keys.left) ship.angle += ROTATION_SPEED * dt;
                    if (keys.right) ship.angle -= ROTATION_SPEED * dt;
                    
                    updateThrustSound(keys.thrust && ship.explodeTime === 0);

                    if (keys.thrust && ship.explodeTime === 0) {
                        ship.thrust.x += THRUST * Math.cos(ship.angle) * dt;
                        ship.thrust.y -= THRUST * Math.sin(ship.angle) * dt;
                        
                        if (Math.random() < 0.6) {
                            particles.push({
                                x: ship.x - ship.r * Math.cos(ship.angle),
                                y: ship.y + ship.r * Math.sin(ship.angle),
                                vx: -ship.thrust.x * 0.2 + (Math.random() - 0.5) * 20,
                                vy: -ship.thrust.y * 0.2 + (Math.random() - 0.5) * 20,
                                alpha: 0.8, color: '#ff0055'
                            });
                        }
                    } else {
                        ship.thrust.x -= FRICTION * ship.thrust.x * dt;
                        ship.thrust.y -= FRICTION * ship.thrust.y * dt;
                    }
                    if (keys.fire) shootLaser();

                    if (ship.explodeTime === 0) {
                        ship.x += ship.thrust.x * dt; ship.y += ship.thrust.y * dt;
                        if (ship.x < -ship.r) ship.x = canvas.width + ship.r;
                        if (ship.x > canvas.width + ship.r) ship.x = -ship.r;
                        if (ship.y < -ship.r) ship.y = canvas.height + ship.r;
                        if (ship.y > canvas.height + ship.r) ship.y = -ship.r;
                    } else {
                        ship.explodeTime--;
                        if (ship.explodeTime === 0 && lives > 0) ship = createShip();
                    }

                    for (let i = ship.lasers.length - 1; i >= 0; i--) {
                        const l = ship.lasers[i];
                        l.x += l.vx * dt; 
                        l.y += l.vy * dt;
                        if (l.x < 0) l.x = canvas.width; if (l.x > canvas.width) l.x = 0;
                        if (l.y < 0) l.y = canvas.height; if (l.y > canvas.height) l.y = 0;
                        l.dist += Math.sqrt(Math.pow(l.vx * dt, 2) + Math.pow(l.vy * dt, 2));
                        if (l.dist > canvas.width * LASER_DIST_LIMIT) ship.lasers.splice(i, 1);
                    }

                    asteroids.forEach(a => {
                        a.x += a.vx * dt * 60; a.y += a.vy * dt * 60; a.angle += a.rotSpeed;
                        if (a.x < -a.r) a.x = canvas.width + a.r; if (a.x > canvas.width + a.r) a.x = -a.r;
                        if (a.y < -a.r) a.y = canvas.height + a.r; if (a.y > canvas.height + a.r) a.y = -a.r;
                    });

                    for (let i = asteroids.length - 1; i >= 0; i--) {
                        const a = asteroids[i];
                        for (let j = ship.lasers.length - 1; j >= 0; j--) {
                            const l = ship.lasers[j];
                            if (distBetweenPoints(a.x, a.y, l.x, l.y) < a.r) {
                                ship.lasers.splice(j, 1); destroyAsteroid(i); break;
                            }
                        }
                    }

                    if (ship.explodeTime === 0) {
                        for (let i = 0; i < asteroids.length; i++) {
                            const a = asteroids[i];
                            if (distBetweenPoints(ship.x, ship.y, a.x, a.y) < ship.r + a.r) {
                                killShip(); destroyAsteroid(i); break;
                            }
                        }
                    }
                }

                render();
                requestAnimationFrame(update);
            }

            function render() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                asteroids.forEach(a => {
                    ctx.save(); ctx.translate(a.x, a.y); ctx.rotate(a.angle);
                    ctx.strokeStyle = '#00f0ff'; ctx.lineWidth = 2; ctx.shadowColor = '#00f0ff'; ctx.shadowBlur = 10;
                    ctx.beginPath();
                    for (let j = 0; j < a.vertices; j++) {
                        const angle = (j / a.vertices) * Math.PI * 2;
                        const r = a.r * a.offsets[j];
                        const px = Math.cos(angle) * r; const py = Math.sin(angle) * r;
                        if (j === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
                    }
                    ctx.closePath(); ctx.stroke(); ctx.restore();
                });

                if (gameStarted && ship.explodeTime === 0) {
                    ctx.save(); ctx.translate(ship.x, ship.y); ctx.rotate(-ship.angle + Math.PI / 2);
                    ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2.2; ctx.shadowColor = '#00f0ff'; ctx.shadowBlur = 12;
                    ctx.beginPath(); ctx.moveTo(0, -ship.r); ctx.lineTo(-ship.r * 0.7, ship.r);
                    ctx.lineTo(0, ship.r * 0.5); ctx.lineTo(ship.r * 0.7, ship.r); ctx.closePath(); ctx.stroke();
                    if (keys.thrust) {
                        ctx.strokeStyle = '#ff0055'; ctx.shadowColor = '#ff0055'; ctx.beginPath();
                        ctx.moveTo(-ship.r * 0.4, ship.r * 0.7); ctx.lineTo(0, ship.r * 1.5); ctx.lineTo(ship.r * 0.4, ship.r * 0.7); ctx.stroke();
                    }
                    ctx.restore();
                }

                ship.lasers.forEach(l => {
                    ctx.fillStyle = '#ff0055'; ctx.shadowColor = '#ff0055'; ctx.shadowBlur = 12;
                    ctx.beginPath(); ctx.arc(l.x, l.y, 3.5, 0, Math.PI * 2); ctx.fill();
                });

                for (let i = particles.length - 1; i >= 0; i--) {
                    const p = particles[i]; p.x += p.vx; p.y += p.vy; p.alpha -= 0.02;
                    if (p.alpha <= 0) { particles.splice(i, 1); continue; }
                    ctx.save(); ctx.globalAlpha = p.alpha; ctx.fillStyle = p.color;
                    ctx.shadowColor = p.color; ctx.shadowBlur = 8; ctx.beginPath();
                    ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.restore();
                }
            }

            requestAnimationFrame(update);
        }

        document.addEventListener('DOMContentLoaded', function() {
            const containers = document.querySelectorAll('.asteroids-arcade-container');
            containers.forEach(c => initAsteroidsGame(c.id));
        });
    })();
    ");

    ob_start();
    ?>
    <div id="<?php echo esc_attr($instance_id); ?>" class="asteroids-arcade-container" tabindex="0">
        <div class="arcade-hud">
            <div class="hud-left">
                <div>SCORE: <span class="astScore">0000</span></div>
                <div>HI: <span class="astHighScore">0000</span></div>
                <div>STAGE: <span class="astLevel">1</span></div>
                <div>LIVES: <span class="astLives">3</span></div>
            </div>
            <div class="hud-controls">
                <button class="hud-btn btnToggleFS">⛶ FULLSCREEN</button>
                <button class="hud-btn btnToggleAudio">🔊 SOUND</button>
                <button class="hud-btn btnToggleHelp">❓ MANUAL</button>
            </div>
        </div>

        <div class="level-banner astLevelBanner hidden">STAGE 1</div>

        <div class="asteroids-canvas-wrapper">
            <canvas></canvas>
        </div>

        <div class="arcade-overlay astStartScreen">
            <h2>ASTEROIDS</h2>
            <div class="brand-credits">DEVELOPED BY <a href="https://mcstaydesigns.cc" target="_blank" rel="noopener">MCSTAY DESIGNS</a></div>
            <p>PRESS ENTER / SPACE OR CLICK BELOW TO LAUNCH</p>
            <button class="hud-btn btnStartGame" style="margin-top: 20px; font-size: 1rem; padding: 12px 24px;">START GAME</button>
        </div>

        <div class="arcade-overlay astGameOverScreen hidden" style="cursor: pointer;">
            <h2>GAME OVER</h2>
            <p>CLICK OR PRESS ENTER TO RESTART</p>
        </div>

        <div class="arcade-overlay astHelpOverlay hidden">
            <div class="overlay-card">
                <div class="card-title">FLIGHT MANUAL & CONTROLS</div>
                <div class="help-grid">
                    <div class="help-section">
                        <h4>KEYBOARD CONTROLS</h4>
                        <ul>
                            <li><span>[A] / [LEFT]</span> : ROTATE LEFT</li>
                            <li><span>[D] / [RIGHT]</span> : ROTATE RIGHT</li>
                            <li><span>[W] / [UP]</span> : THRUST ENGINE</li>
                            <li><span>[SPACE]</span> : FIRE CANNON</li>
                        </ul>
                    </div>
                    <div class="help-section">
                        <h4>TACTICAL BRIEFING</h4>
                        <ul>
                            <li>LARGE TARGET: <span>20 PTS</span></li>
                            <li>MEDIUM TARGET: <span>50 PTS</span></li>
                            <li>SMALL TARGET: <span>100 PTS</span></li>
                        </ul>
                    </div>
                </div>
                <div class="help-rules">
                    Destroy all vector field debris to clear the stage. Vessel speed increases with each subsequent sector. High scores are saved to local terminal storage.
                </div>
                <button class="hud-btn overlay-close-btn btnCloseHelp">CLOSE MANUAL</button>
            </div>
        </div>

        <div class="touch-controls">
            <div class="touch-group">
                <button class="t-btn btnLeft">◄</button>
                <button class="t-btn btnRight">►</button>
            </div>
            <div class="touch-group">
                <button class="t-btn btnThrust">▲</button>
                <button class="t-btn fire-btn btnFire">●</button>
            </div>
        </div>
    </div>
    <?php
    return ob_get_clean();
}
add_shortcode('retro_asteroids', 'wp_retro_asteroids_shortcode');
Author

mkmcst

test test test

Follow Me
Other Articles
Previous

Retro Asteroids Game

Next

Dear Pilgrim: Final Integrated System with Theme-Adaptive Styling

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Legal

  • Terms of Service
  • Privacy Policy
Copyright 2026 — McStay Designs. All rights reserved.McStay Media
🚀 Upgrade Your Site: 20% Off Custom WordPress Plugins, Security Cleanup & Performance Tuning!Get a Free Quote →