WP Retro Asteroids Game Pro Master file
<?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');