LUNAR LANDER ARCADE
by PrismPhoenix37681 lines23.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>LUNAR LANDER ARCADE</title>
<style>
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
body {
margin: 0;
padding: 0;
background-color: #050505;
color: #00ffcc;
font-family: 'Courier New', Courier, monospace, sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
height: 100%;
max-height: 600px;
display: flex;
flex-direction: column;
background: #000;
border: 4px solid #113322;
box-shadow: 0 0 20px rgba(0, 255, 204, 0.2);
}
canvas {
display: block;
background-color: #000;
width: 100%;
height: 100%;
flex-grow: 1;
}
/* CRT Scanline Overlay */
#game-container::after {
content: " ";
display: block;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
z-index: 10;
background-size: 100% 4px, 6px 100%;
pointer-events: none;
}
.hud-overlay {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
font-weight: bold;
color: #00ffcc;
text-shadow: 0 0 5px #00ffcc;
pointer-events: none;
z-index: 20;
text-transform: uppercase;
}
.hud-group {
display: flex;
gap: 15px;
}
#ui-screen {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 30;
text-align: center;
padding: 20px;
}
h1 {
font-size: 32px;
margin: 0 0 10px 0;
color: #00ffcc;
text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc;
letter-spacing: 2px;
}
p {
font-size: 14px;
color: #88ffcc;
max-width: 400px;
line-height: 1.5;
margin-bottom: 25px;
}
.btn {
background: transparent;
color: #00ffcc;
border: 2px solid #00ffcc;
padding: 12px 30px;
font-size: 16px;
font-family: 'Courier New', Courier, monospace;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
box-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
transition: all 0.2s ease;
}
.btn:hover, .btn:active {
background: #00ffcc;
color: #000;
box-shadow: 0 0 20px #00ffcc;
}
/* Mobile Touch Controls */
#mobile-controls {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 120px;
display: none;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 25;
pointer-events: none;
}
.control-group {
display: flex;
gap: 15px;
pointer-events: auto;
}
.ctrl-btn {
width: 65px;
height: 65px;
background: rgba(0, 255, 204, 0.1);
border: 2px solid #00ffcc;
border-radius: 50%;
color: #00ffcc;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(0, 255, 204, 0.2);
outline: none;
}
.ctrl-btn:active {
background: rgba(0, 255, 204, 0.4);
box-shadow: 0 0 15px #00ffcc;
transform: scale(0.95);
}
@media (pointer: coarse) {
#mobile-controls {
display: flex;
}
}
</style>
</head>
<body>
<div id="game-container">
<!-- HUD Overlay -->
<div class="hud-overlay">
<div class="hud-group">
<span>FUEL: <span id="fuel-val">1000</span></span>
<span>ALT: <span id="alt-val">0</span></span>
</div>
<div class="hud-group">
<span>H-SPD: <span id="hspd-val">0.0</span></span>
<span>V-SPD: <span id="vspd-val">0.0</span></span>
<span>SCORE: <span id="score-val">0</span></span>
</div>
</div>
<!-- Canvas -->
<canvas id="landCanvas"></canvas>
<!-- Mobile Controls -->
<div id="mobile-controls">
<div class="control-group">
<button class="ctrl-btn" id="btn-left">◀</button>
<button class="ctrl-btn" id="btn-right">▶</button>
</div>
<div class="control-group">
<button class="ctrl-btn" id="btn-thrust">▲</button>
</div>
</div>
<!-- Start / Game Over Screen -->
<div id="ui-screen">
<h1 id="screen-title">LUNAR LANDER</h1>
<p id="screen-desc">Land safely on the flat pads (marked with multipliers). Keep vertical speed under 2.0 and horizontal speed under 1.5. Controls: A/D or Left/Right to rotate, W or Up to thrust.</p>
<button class="btn" id="start-btn">INSERT COIN</button>
</div>
</div>
<script>
// Audio Synthesizer using Web Audio API
class SoundFX {
constructor() {
this.ctx = null;
this.thrusterNode = null;
this.thrusterGain = null;
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playThrust(active) {
this.init();
if (active) {
if (!this.thrusterNode) {
// White noise generator for thruster roar
const bufferSize = this.ctx.sampleRate * 2;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
this.thrusterNode = this.ctx.createBufferSource();
this.thrusterNode.buffer = buffer;
this.thrusterNode.loop = true;
// Lowpass filter for deep rocket rumble
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 350;
this.thrusterGain = this.ctx.createGain();
this.thrusterGain.gain.setValueAtTime(0.15, this.ctx.currentTime);
this.thrusterNode.connect(filter);
filter.connect(this.thrusterGain);
this.thrusterGain.connect(this.ctx.destination);
this.thrusterNode.start();
}
} else {
if (this.thrusterNode) {
try {
this.thrusterNode.stop();
this.thrusterNode.disconnect();
} catch(e) {}
this.thrusterNode = null;
this.thrusterGain = null;
}
}
}
playExplosion() {
this.init();
const bufferSize = this.ctx.sampleRate * 1.5;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.3));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, this.ctx.currentTime);
filter.frequency.linearRampToValueAtTime(50, this.ctx.currentTime + 1.5);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 1.5);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
}
playSuccess() {
this.init();
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(554.37, now + 0.1);
osc.frequency.setValueAtTime(659.25, now + 0.2);
osc.frequency.setValueAtTime(880, now + 0.3);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.6);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.6);
}
}
const sfx = new SoundFX();
const canvas = document.getElementById('landCanvas');
const ctx = canvas.getContext('2d');
// Resize Canvas to container
function resize() {
const container = document.getElementById('game-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
window.addEventListener('resize', resize);
resize();
// Game State
let gameState = 'START'; // START, PLAYING, LANDED, CRASHED, GAMEOVER
let score = 0;
let fuel = 1000;
let alt = 0;
let hSpd = 0;
let vSpd = 0;
// Lander Object
let lander = {
x: canvas.width / 2,
y: 50,
vx: 0,
vy: 0,
angle: 0, // radians, 0 is pointing up
width: 16,
height: 24,
rotationSpeed: 0.035,
thrustPower: 0.07,
gravity: 0.02
};
// Terrain & Pads
let terrain = [];
let pads = [];
function generateTerrain() {
terrain = [];
pads = [];
const segments = 24;
const segWidth = canvas.width / segments;
let currentY = canvas.height * 0.65 + Math.random() * (canvas.height * 0.15);
terrain.push({x: 0, y: canvas.height});
terrain.push({x: 0, y: currentY});
let padIndex = Math.floor(Math.random() * 6) + 8; // Pick a segment for landing pad
for (let i = 1; i <= segments; i++) {
let nextX = i * segWidth;
let nextY;
if (i === padIndex) {
// Flat landing pad
nextY = currentY;
pads.push({
x1: (i - 1) * segWidth,
x2: nextX,
y: nextY,
multiplier: Math.random() > 0.6 ? 5 : (Math.random() > 0.3 ? 3 : 2)
});
} else {
nextY = currentY + (Math.random() * 80 - 40);
// Keep bounds safe
nextY = Math.max(canvas.height * 0.4, Math.min(canvas.height * 0.85, nextY));
}
terrain.push({x: nextX, y: nextY});
currentY = nextY;
}
terrain.push({x: canvas.width, y: canvas.height});
}
function resetLander() {
lander.x = Math.random() * (canvas.width - 200) + 100;
lander.y = 40;
lander.vx = (Math.random() - 0.5) * 1.5;
lander.vy = 0.5;
lander.angle = 0;
fuel = 1000;
}
// Input Handling
const keys = {left: false, right: false, thrust: false};
window.addEventListener('keydown', e => {
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;
});
window.addEventListener('keyup', e => {
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;
});
// Mobile Button bindings
const bindTouch = (id, keyName) => {
const btn = document.getElementById(id);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyName] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyName] = false; });
btn.addEventListener('mousedown', (e) => { e.preventDefault(); keys[keyName] = true; });
btn.addEventListener('mouseup', (e) => { e.preventDefault(); keys[keyName] = false; });
};
bindTouch('btn-left', 'left');
bindTouch('btn-right', 'right');
bindTouch('btn-thrust', 'thrust');
// UI triggers
const uiScreen = document.getElementById('ui-screen');
const startBtn = document.getElementById('start-btn');
const screenTitle = document.getElementById('screen-title');
const screenDesc = document.getElementById('screen-desc');
startBtn.addEventListener('click', () => {
sfx.init();
if (gameState === 'GAMEOVER' || gameState === 'START') {
score = 0;
generateTerrain();
resetLander();
}
gameState = 'PLAYING';
uiScreen.style.display = 'none';
});
// Game Loop
let particles = [];
function update() {
if (gameState !== 'PLAYING') return;
// Rotation
if (keys.left) lander.angle -= lander.rotationSpeed;
if (keys.right) lander.angle += lander.rotationSpeed;
// Thrust
let isThrusting = false;
if (keys.thrust && fuel > 0) {
isThrusting = true;
fuel -= 0.8;
lander.vx += Math.sin(lander.angle) * lander.thrustPower;
lander.vy -= Math.cos(lander.angle) * lander.thrustPower;
// Add exhaust particles
particles.push({
x: lander.x - Math.sin(lander.angle) * 12,
y: lander.y + Math.cos(lander.angle) * 12,
vx: -Math.sin(lander.angle) * 2 + (Math.random() - 0.5),
vy: Math.cos(lander.angle) * 2 + (Math.random() - 0.5),
life: 20
});
}
sfx.playThrust(isThrusting && fuel > 0);
// Gravity
lander.vy += lander.gravity;
// Position update
lander.x += lander.vx;
lander.y += lander.vy;
// Screen wrapping horizontally
if (lander.x < 0) lander.x = canvas.width;
if (lander.x > canvas.width) lander.x = 0;
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
// Calculate Altitude above terrain
let terrainYUnderLander = canvas.height;
for (let i = 0; i < terrain.length - 1; i++) {
let p1 = terrain[i];
let p2 = terrain[i+1];
if (lander.x >= p1.x && lander.x <= p2.x) {
let t = (lander.x - p1.x) / (p2.x - p1.x);
terrainYUnderLander = p1.y + t * (p2.y - p1.y);
break;
}
}
alt = Math.max(0, Math.floor(terrainYUnderLander - lander.y - lander.height / 2));
hSpd = lander.vx;
vSpd = lander.vy;
// Collision detection with terrain
if (lander.y + lander.height / 2 >= terrainYUnderLander) {
sfx.playThrust(false);
checkLanding(terrainYUnderLander);
}
}
function checkLanding(terrainY) {
// Check if landed on a pad and within safe velocity and upright angle
let landedOnPad = false;
let padMultiplier = 1;
for (let pad of pads) {
if (lander.x >= pad.x1 && lander.x <= pad.x2 && Math.abs((lander.y + lander.height/2) - pad.y) < 6) {
landedOnPad = true;
padMultiplier = pad.multiplier;
break;
}
}
let safeAngle = Math.abs(lander.angle) < 0.2;
let safeVSpd = vSpd < 2.2;
let safeHSpd = Math.abs(hSpd) < 1.5;
if (landedOnPad && safeAngle && safeVSpd && safeHSpd) {
// SUCCESSFUL LANDING
gameState = 'LANDED';
sfx.playSuccess();
let earned = Math.floor((Math.floor(fuel) + Math.abs(vSpd)*10) * padMultiplier);
score += earned;
screenTitle.innerText = "SUCCESSFUL LANDING!";
screenDesc.innerText = `Lander secured safely.\nBonus multiplier: x${padMultiplier}\nPoints Earned: ${earned}\nTotal Score: ${score}`;
startBtn.innerText = "NEXT MISSION";
uiScreen.style.display = 'flex';
// Generate new terrain for next level after success
setTimeout(() => {
generateTerrain();
resetLander();
}, 500);
} else {
// CRASH
gameState = 'CRASHED';
sfx.playExplosion();
// Create explosion debris particles
for(let i=0; i<40; i++) {
particles.push({
x: lander.x,
y: lander.y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6,
life: 60
});
}
screenTitle.innerText = "CRASH DETECTED";
screenDesc.innerText = `Ship destroyed upon impact!\nSpeed or angle was out of safety parameters.\nFinal Score: ${score}`;
startBtn.innerText = "TRY AGAIN";
uiScreen.style.display = 'flex';
gameState = 'GAMEOVER';
}
}
function draw() {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw Stars / Background
ctx.fillStyle = '#ffffff';
ctx.fillRect(100, 50, 2, 2);
ctx.fillRect(300, 80, 1.5, 1.5);
ctx.fillRect(550, 40, 2, 2);
ctx.fillRect(700, 120, 1, 1);
ctx.fillRect(200, 200, 2, 2);
ctx.fillRect(450, 150, 1, 1);
// Draw Terrain
ctx.strokeStyle = '#00ffcc';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(terrain[0].x, terrain[0].y);
for (let i = 1; i < terrain.length; i++) {
ctx.lineTo(terrain[i].x, terrain[i].y);
}
ctx.stroke();
// Highlight Landing Pads
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 4;
for (let pad of pads) {
ctx.beginPath();
ctx.moveTo(pad.x1, pad.y);
ctx.lineTo(pad.x2, pad.y);
ctx.stroke();
// Draw pad multiplier text
ctx.fillStyle = '#ffff00';
ctx.font = '10px Courier New';
ctx.textAlign = 'center';
ctx.fillText(`[ x${pad.multiplier} ]`, (pad.x1 + pad.x2) / 2, pad.y - 8);
}
// Draw Particles (Exhaust & Explosions)
ctx.fillStyle = '#ff5500';
for (let p of particles) {
ctx.fillRect(p.x, p.y, 2, 2);
}
// Draw Lander (if not crashed/gameover explosion)
if (gameState !== 'GAMEOVER' || particles.length > 0) {
ctx.save();
ctx.translate(lander.x, lander.y);
ctx.rotate(lander.angle);
// Lander Hull Vector Art
ctx.strokeStyle = '#00ffcc';
ctx.lineWidth = 1.5;
ctx.fillStyle = '#001111';
ctx.beginPath();
// Body
ctx.moveTo(0, -12);
ctx.lineTo(8, 6);
ctx.lineTo(-8, 6);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Legs
ctx.beginPath();
ctx.moveTo(-8, 6);
ctx.lineTo(-12, 12);
ctx.moveTo(8, 6);
ctx.lineTo(12, 12);
ctx.stroke();
// Thrust Flame
if (keys.thrust && fuel > 0 && gameState === 'PLAYING') {
ctx.strokeStyle = '#ff3300';
ctx.beginPath();
ctx.moveTo(-4, 6);
ctx.lineTo(0, 18 + Math.random() * 6);
ctx.lineTo(4, 6);
ctx.stroke();
}
ctx.restore();
}
// Update HUD text elements
document.getElementById('fuel-val').innerText = Math.max(0, Math.floor(fuel));
document.getElementById('alt-val').innerText = alt;
document.getElementById('hspd-val').innerText = hSpd.toFixed(1);
document.getElementById('vspd-val').innerText = vSpd.toFixed(1);
document.getElementById('score-val').innerText = score;
}
// Main Loop
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// Initialize terrain first time
generateTerrain();
loop();
</script>
</body>
</html>
Game Source: LUNAR LANDER ARCADE
Creator: PrismPhoenix37
Libraries: none
Complexity: complex (681 lines, 23.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: lunar-lander-arcade-prismphoenix37" to link back to the original. Then publish at arcadelab.ai/publish.