Geordie Lander - Newcastle Arcade
by PhantomPanda28657 lines19.2 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geordie Lander - Newcastle Arcade</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #0b0f19;
color: #f3f4f6;
font-family: 'Courier New', Courier, monospace;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
user-select: none;
}
#game-container {
position: relative;
box-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
border: 4px solid #1f2937;
border-radius: 8px;
background: #050508;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(to bottom, #020617 0%, #0f172a 70%, #1e1b4b 100%);
}
/* Very subtle CRT scanline effect */
#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.05) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.005), rgba(0, 255, 0, 0.002), rgba(0, 0, 255, 0.005));
z-index: 10;
background-size: 100% 4px, 6px 100%;
pointer-events: none;
}
.ui-overlay {
position: absolute;
top: 15px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
pointer-events: none;
z-index: 20;
font-size: 14px;
font-weight: bold;
text-shadow: 2px 2px #000;
color: #38bdf8;
letter-spacing: 1px;
}
.hud-box {
background: rgba(15, 23, 42, 0.85);
padding: 8px 14px;
border: 2px solid #38bdf8;
border-radius: 4px;
}
.fuel-bar-container {
width: 100px;
height: 12px;
background: #334155;
border: 1px solid #94a3b8;
margin-top: 4px;
border-radius: 2px;
overflow: hidden;
}
.fuel-bar-fill {
height: 100%;
background: #22c55e;
width: 100%;
transition: width 0.1s linear;
}
#mobile-controls {
display: none;
position: absolute;
bottom: 15px;
left: 0;
right: 0;
justify-content: space-between;
padding: 0 20px;
z-index: 20;
pointer-events: auto;
}
.mob-btn {
background: rgba(30, 41, 59, 0.85);
border: 2px solid #38bdf8;
color: #38bdf8;
font-size: 20px;
padding: 15px 22px;
border-radius: 8px;
font-family: inherit;
font-weight: bold;
cursor: pointer;
touch-action: manipulation;
}
.mob-btn:active {
background: #38bdf8;
color: #0f172a;
}
@media (max-width: 820px) {
#mobile-controls { display: flex; }
}
</style>
</head>
<body>
<div id="game-container">
<div class="ui-overlay">
<div class="hud-box">
<div>SCORE: <span id="score-val">0</span></div>
<div>PILOT: <span id="pilot-val" style="color: #facc15;">PJ (Byker Grove)</span></div>
</div>
<div class="hud-box" style="text-align: right;">
<div>TOON FUEL (BROWN ALE)</div>
<div class="fuel-bar-container">
<div id="fuel-fill" class="fuel-bar-fill"></div>
</div>
</div>
</div>
<canvas id="canvas" width="800" height="600"></canvas>
<div id="mobile-controls">
<div>
<button class="mob-btn" id="btn-left">◀</button>
<button class="mob-btn" id="btn-right">▶</button>
</div>
<button class="mob-btn" id="btn-thrust">🚀 THRUST</button>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const scoreEl = document.getElementById('score-val');
const pilotEl = document.getElementById('pilot-val');
const fuelFillEl = document.getElementById('fuel-fill');
// Audio Context for sound effects
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playSound(type) {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'thrust') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 0.1);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
} else if (type === 'success') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(600, now + 0.3);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
} else if (type === 'crash') {
osc.type = 'square';
osc.frequency.setValueAtTime(120, now);
osc.frequency.linearRampToValueAtTime(30, now + 0.5);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
}
}
// Game States
const STATE_START = 0;
const STATE_PLAYING = 1;
const STATE_LANDED = 2;
const STATE_CRASHED = 3;
let gameState = STATE_START;
let score = 0;
let currentPilot = 0; // 0 = PJ, 1 = Duncan
const pilotNames = ["PJ (Byker Grove)", "Duncan (Byker Grove)"];
// Lander physics
let lander = {
x: 400,
y: 80,
vx: 0.5,
vy: 0,
angle: 0,
fuel: 1000,
maxFuel: 1000,
width: 36,
height: 44
};
// Alan Shearer Encouragement Coach
let shearer = {
active: false,
text: "",
timer: 0,
x: -360
};
let shearerCooldown = 0;
const shearerQuotes = [
"Haway man! Keep her steady, son!",
"Absolute belta! That's how we do it in Newcastle!",
"Even Kevin Keegan would be proud of that!",
"Show them how number 9 does it!",
"Divvent crash into the Tyne Bridge, pet!",
"Why aye! Champion flying!"
];
function triggerShearer(msg = null) {
shearer.text = msg || shearerQuotes[Math.floor(Math.random() * shearerQuotes.length)];
shearer.active = true;
shearer.timer = 240; // 4 seconds visible
}
// Landing Pads definitions
const pads = [
{ x: 120, y: 530, w: 80, multiplier: 4, name: "St. James' Park Roof (Hard)" },
{ x: 340, y: 550, w: 120, multiplier: 2, name: "Grey Street (Medium)" },
{ x: 580, y: 540, w: 140, multiplier: 1, name: "The Quayside (Easy)" }
];
// Controls state
const keys = { left: false, right: false, thrust: false };
window.addEventListener('keydown', e => {
initAudio();
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' || e.code === 'Space') {
keys.thrust = true;
e.preventDefault();
}
if (e.code === 'Enter' && (gameState === STATE_START || gameState === STATE_LANDED || gameState === STATE_CRASHED)) {
resetGame(gameState === STATE_CRASHED);
}
});
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' || e.code === 'Space') keys.thrust = false;
});
// Mobile button bindings
function bindTouch(id, keyName) {
const el = document.getElementById(id);
el.addEventListener('touchstart', (e) => { initAudio(); keys[keyName] = true; e.preventDefault(); });
el.addEventListener('touchend', (e) => { keys[keyName] = false; e.preventDefault(); });
el.addEventListener('mousedown', (e) => { initAudio(); keys[keyName] = true; });
el.addEventListener('mouseup', (e) => { keys[keyName] = false; });
}
bindTouch('btn-left', 'left');
bindTouch('btn-right', 'right');
bindTouch('btn-thrust', 'thrust');
canvas.addEventListener('click', () => {
initAudio();
if (gameState === STATE_START || gameState === STATE_LANDED || gameState === STATE_CRASHED) {
resetGame(gameState === STATE_CRASHED);
}
});
function resetGame(hardReset = false) {
if (hardReset) {
score = 0;
currentPilot = 0;
} else if (gameState === STATE_LANDED) {
currentPilot = 1 - currentPilot;
}
lander.x = 100 + Math.random() * 600;
lander.y = 60;
lander.vx = (Math.random() - 0.5) * 1.5;
lander.vy = 0.5;
lander.angle = 0;
lander.fuel = lander.maxFuel;
gameState = STATE_PLAYING;
shearerCooldown = 0;
pilotEl.innerText = pilotNames[currentPilot];
triggerShearer("Right then, let's fly over the Toon!");
}
// Draw Newcastle Background Landmarks
function drawBackground() {
// Stars
ctx.fillStyle = '#ffffff';
ctx.fillRect(100, 50, 2, 2);
ctx.fillRect(250, 80, 2, 2);
ctx.fillRect(450, 40, 3, 3);
ctx.fillRect(680, 90, 2, 2);
ctx.fillRect(750, 50, 2, 2);
// Grey's Monument (Left background)
ctx.fillStyle = '#334155';
ctx.fillRect(45, 380, 25, 170);
ctx.fillStyle = '#475569';
ctx.fillRect(35, 365, 45, 20);
ctx.fillStyle = '#facc15';
ctx.fillRect(50, 345, 15, 20);
// Tyne Bridge Silhouette (Center-Left)
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(300, 540, 180, Math.PI, 0, false);
ctx.stroke();
ctx.fillStyle = '#1e293b';
ctx.fillRect(160, 480, 280, 10);
ctx.fillRect(180, 490, 16, 60);
ctx.fillRect(404, 490, 16, 60);
// Millennium Bridge (Right background)
ctx.beginPath();
ctx.moveTo(600, 550);
ctx.quadraticCurveTo(680, 380, 780, 550);
ctx.lineWidth = 4;
ctx.strokeStyle = '#334155';
ctx.stroke();
// River Tyne at bottom
const grad = ctx.createLinearGradient(0, 550, 0, 600);
grad.addColorStop(0, '#0284c7');
grad.addColorStop(1, '#0369a1');
ctx.fillStyle = grad;
ctx.fillRect(0, 550, 800, 50);
// Draw Landing Pads
pads.forEach(pad => {
ctx.fillStyle = '#facc15';
ctx.fillRect(pad.x, pad.y, pad.w, 10);
ctx.fillStyle = '#ef4444';
ctx.fillRect(pad.x - 4, pad.y + 4, 4, 6);
ctx.fillRect(pad.x + pad.w, pad.y + 4, 4, 6);
ctx.fillStyle = '#ffffff';
ctx.font = '11px Courier New';
ctx.textAlign = 'center';
ctx.fillText(`${pad.multiplier}x`, pad.x + pad.w/2, pad.y - 5);
});
}
// Draw the Lander Ship
function drawLander() {
ctx.save();
ctx.translate(lander.x, lander.y);
ctx.rotate(lander.angle);
if (keys.thrust && lander.fuel > 0) {
ctx.fillStyle = '#f97316';
ctx.beginPath();
ctx.moveTo(-8, 22);
ctx.lineTo(0, 38 + Math.random() * 10);
ctx.lineTo(8, 22);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.moveTo(-4, 22);
ctx.lineTo(0, 30 + Math.random() * 6);
ctx.lineTo(4, 22);
ctx.closePath();
ctx.fill();
}
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-16, 15);
ctx.lineTo(-24, 25);
ctx.moveTo(16, 15);
ctx.lineTo(24, 25);
ctx.stroke();
ctx.fillStyle = '#e2e8f0';
ctx.fillRect(-16, -14, 32, 30);
ctx.strokeStyle = '#64748b';
ctx.lineWidth = 2;
ctx.strokeRect(-16, -14, 32, 30);
ctx.fillStyle = '#0284c7';
ctx.beginPath();
ctx.arc(0, -10, 11, Math.PI, 0, false);
ctx.fill();
ctx.stroke();
if (currentPilot === 0) {
ctx.fillStyle = '#854d0e';
ctx.fillRect(-6, -18, 12, 6);
ctx.fillStyle = '#fde047';
ctx.fillRect(-8, -10, 16, 4);
} else {
ctx.fillStyle = '#1e293b';
ctx.fillRect(-6, -18, 12, 5);
ctx.fillStyle = '#ec4899';
ctx.fillRect(-8, -10, 16, 4);
}
ctx.fillStyle = '#000000';
ctx.fillRect(-4, 2, 8, 8);
ctx.fillStyle = '#ffffff';
ctx.fillRect(-2, 4, 4, 4);
ctx.restore();
}
// Draw Alan Shearer Pixel Art Portrait & Speech Bubble
function drawShearer() {
if (!shearer.active) return;
if (shearer.x < 20) {
shearer.x += 8;
}
const boxX = shearer.x;
const boxY = 68; // Neatly under HUD
const boxW = 340;
const boxH = 75;
// Speech Bubble Box
ctx.fillStyle = 'rgba(15, 23, 42, 0.95)';
ctx.strokeStyle = '#facc15';
ctx.lineWidth = 2;
ctx.fillRect(boxX, boxY, boxW, boxH);
ctx.strokeRect(boxX, boxY, boxW, boxH);
// Alan Shearer Pixel Art Avatar
const avX = boxX + 10;
const avY = boxY + 10;
// Black & White Striped Shoulders / Kit
ctx.fillStyle = '#000000';
ctx.fillRect(avX, avY + 35, 50, 20);
ctx.fillStyle = '#ffffff';
ctx.fillRect(avX + 10, avY + 35, 10, 20);
ctx.fillRect(avX + 30, avY + 35, 10, 20);
// #9 Number on Kit
ctx.fillStyle = '#facc15';
ctx.font = 'bold 10px Courier New';
ctx.textAlign = 'center';
ctx.fillText("9", avX + 25, avY + 48);
// Skin Tone Head
ctx.fillStyle = '#f5d0b1';
ctx.fillRect(avX + 12, avY + 10, 26, 28);
// Short Buzz-Cut Hair
ctx.fillStyle = '#d4d4d4';
ctx.fillRect(avX + 12, avY + 8, 26, 7);
// Determined Eyes
ctx.fillStyle = '#0f172a';
ctx.fillRect(avX + 17, avY + 18, 5, 3);
ctx.fillRect(avX + 28, avY + 18, 5, 3);
// Serious Mouth
ctx.fillRect(avX + 20, avY + 28, 10, 2);
// Text Content (Cleanly aligned left with vertical centering)
ctx.textAlign = 'left';
ctx.fillStyle = '#facc15';
ctx.font = 'bold 11px Courier New';
ctx.fillText("ALAN SHEARER (#9)", boxX + 72, boxY + 24);
ctx.fillStyle = '#f3f4f6';
ctx.font = '11px Courier New';
ctx.fillText(shearer.text, boxX + 72, boxY + 48, 255);
shearer.timer--;
if (shearer.timer <= 0) {
shearer.x -= 8;
if (shearer.x <= -360) {
shearer.active = false;
}
}
}
// Update game physics and collision detection
function update() {
if (gameState !== STATE_PLAYING) return;
// Rotation controls
if (keys.left) lander.angle -= 0.04;
if (keys.right) lander.angle += 0.04;
// Thrust controls
if (keys.thrust && lander.fuel > 0) {
const power = 0.12;
lander.vx += Math.sin(lander.angle) * power;
lander.vy -= Math.cos(lander.angle) * power;
lander.fuel -= 1.5;
if (Math.random() < 0.3) playSound('thrust');
}
// Gravity
lander.vy += 0.035;
// Apply velocity
lander.x += lander.vx;
lander.y += lander.vy;
// Screen bounds wrap horizontal
if (lander.x < 0) lander.x = 800;
if (lander.x > 800) lander.x = 0;
// Reliable random mid-flight Shearer encouragement (every ~10 seconds)
shearerCooldown++;
if (!shearer.active && shearerCooldown > 600) {
shearerCooldown = 0;
triggerShearer();
}
// Update UI
scoreEl.innerText = score;
fuelFillEl.style.width = Math.max(0, (lander.fuel / lander.maxFuel) * 100) + '%';
// Collision with ground / water (y = 550)
if (lander.y + 22 >= 550) {
let landedPad = null;
pads.forEach(pad => {
if (lander.x >= pad.x && lander.x <= pad.x + pad.w) {
landedPad = pad;
}
});
const safeSpeed = lander.vy < 2.2 && Math.abs(lander.vx) < 1.2;
const safeAngle = Math.abs(lander.angle % (Math.PI * 2)) < 0.25;
if (landedPad && safeSpeed && safeAngle) {
gameState = STATE_LANDED;
const points = landedPad.multiplier * 150;
score += points;
playSound('success');
triggerShearer(`Howay! Cracking landing on ${landedPad.name}! (+${points} pts)`);
} else {
gameState = STATE_CRASHED;
playSound('crash');
triggerShearer("Divvent do that again! Total write-off!");
}
}
}
// Render screens & game objects
function draw() {
ctx.clearRect(0, 0, 800, 600);
drawBackground();
drawLander();
drawShearer();
if (gameState === STATE_START) {
ctx.fillStyle = 'rgba(2, 6, 23, 0.85)';
ctx.fillRect(0, 0, 800, 600);
ctx.fillStyle = '#38bdf8';
ctx.font = 'bold 36px Courier New';
ctx.textAlign = 'center';
ctx.fillText("GEORDIE LANDER", 400, 180);
ctx.fillStyle = '#facc15';
ctx.font = '16px Courier New';
ctx.fillText("Pilot PJ & Duncan with Toon Fuel (Brown Ale)", 400, 225);
ctx.fillStyle = '#94a3b8';
ctx.font = '14px Courier New';
ctx.fillText("Controls: Arrow Keys / WASD to Rotate & Thrust", 400, 280);
ctx.fillText("Land softly on Quayside, Grey Street, or St. James' Park!", 400, 310);
ctx.fillStyle = '#22c55e';
ctx.font = 'bold 18px Courier New';
ctx.fillText(">> CLICK OR PRESS ENTER TO START HAWAY MAN! <<", 400, 400);
}
else if (gameState === STATE_LANDED) {
ctx.fillStyle = 'rgba(15, 23, 42, 0.8)';
ctx.fillRect(150, 180, 500, 240);
ctx.strokeStyle = '#22c55e';
ctx.lineWidth = 3;
ctx.strokeRect(150, 180, 500, 240);
ctx.fillStyle = '#22c55e';
ctx.font = 'bold 26px Courier New';
ctx.textAlign = 'center';
ctx.fillText("BELTA! SUCCESSFUL LANDING!", 400, 230);
ctx.fillStyle = '#f3f4f6';
ctx.font = '16px Courier New';
ctx.fillText(`Pilot ${pilotNames[currentPilot]} secured the ship!`, 400, 270);
ctx.fillText("Next pilot stepping into the cockpit...", 400, 305);
ctx.fillStyle = '#facc15';
ctx.font = 'bold 16px Courier New';
ctx.fillText("CLICK OR PRESS ENTER FOR NEXT LAUNCH", 400, 370);
}
else if (gameState === STATE_CRASHED) {
ctx.fillStyle = 'rgba(15, 23, 42, 0.85)';
ctx.fillRect(150, 180, 500, 240);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 3;
ctx.strokeRect(150, 180, 500, 240);
ctx.fillStyle = '#ef4444';
ctx.font = 'bold 28px Courier New';
ctx.textAlign = 'center';
ctx.fillText("HOWAY MAN! YOU CRASHED!", 400, 240);
ctx.fillStyle = '#f3f4f6';
ctx.font = '16px Courier New';
ctx.fillText(`Final Score: ${score}`, 400, 290);
ctx.fillStyle = '#facc15';
ctx.font = 'bold 16px Courier New';
ctx.fillText("CLICK OR PRESS ENTER TO TRY AGAIN", 400, 360);
}
}
// Game Loop
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
Game Source: Geordie Lander - Newcastle Arcade
Creator: PhantomPanda28
Libraries: none
Complexity: complex (657 lines, 19.2 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: geordie-lander-newcastle-arcade-phantompanda28" to link back to the original. Then publish at arcadelab.ai/publish.