MikeChandler/HermesModel / tron-arcanoid.html
MikeChandler's picture
download
raw
22.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Arcanoid</title>
<style>
body {
margin: 0;
padding: 0;
background: #050505;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: 'Courier New', monospace;
user-select: none;
}
canvas {
display: block;
image-rendering: pixelated;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.1);
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// ==========================================
// CONFIGURATION & STATE
// ==========================================
const CANVAS_W = 400;
const CANVAS_H = 600;
const FPS = 60;
const DT = 1 / FPS;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Neon palette
const NEON = {
CYAN: '#00FFFF', MAGENTA: '#FF00FF', LIME: '#39FF14',
BLUE: '#1E90FF', PINK: '#FF0066', ORANGE: '#FF6600',
YELLOW: '#FFFF00', WHITE: '#FFFFFF'
};
let scale = 1;
let state = 'START'; // START, PLAYING, GAMEOVER, LEVEL_TRANS
let score = 0;
let lives = 3;
let level = 1;
let lastTime = 0;
let audioCtx = null;
let audioInitialized = false;
// Input
const keys = { left: false, right: false };
let mouseX = CANVAS_W / 2;
let mouseDown = false;
// Entities
let paddle = { x: CANVAS_W/2, y: CANVAS_H - 40, w: 70, h: 12, color: NEON.CYAN };
let balls = [];
let bricks = [];
let powerups = [];
let lasers = [];
let particles = [];
let bgParticles = [];
let gridOffset = 0;
// Power-up state
let activePowerUp = { type: null, endTime: 0 };
const POWERUP_TYPES = ['WIDE', 'MULTI', 'SLOW', 'STICKY', 'LASER', 'EXTRA_LIFE'];
const POWERUP_ICONS = {
WIDE: 'W', MULTI: 'M', SLOW: 'S', STICKY: 'T', LASER: 'L', EXTRA_LIFE: '+'
};
const POWERUP_COLORS = {
WIDE: NEON.CYAN, MULTI: NEON.MAGENTA, SLOW: NEON.BLUE,
STICKY: NEON.ORANGE, LASER: NEON.YELLOW, EXTRA_LIFE: NEON.LIME
};
// ==========================================
// AUDIO SYSTEM (Web Audio API)
// ==========================================
function initAudio() {
if (audioInitialized) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
audioInitialized = true;
}
function playSound(type) {
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
osc.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
switch (type) {
case 'WALL':
osc.type = 'sine'; osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(100, t + 0.05);
gain.gain.setValueAtTime(0.15, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.05);
filter.type = 'lowpass'; filter.frequency.value = 1000;
break;
case 'PADDLE':
osc.type = 'triangle'; osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(200, t + 0.08);
gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
filter.type = 'lowpass'; filter.frequency.value = 1500;
break;
case 'BRICK':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(500, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.15);
gain.gain.setValueAtTime(0.12, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
filter.type = 'highpass'; filter.frequency.value = 800;
break;
case 'POWERUP':
osc.type = 'sine'; osc.frequency.setValueAtTime(300, t);
osc.frequency.linearRampToValueAtTime(900, t + 0.2);
gain.gain.setValueAtTime(0.15, t); gain.gain.linearRampToValueAtTime(0.01, t + 0.2);
filter.type = 'bandpass'; filter.Q.value = 5; filter.frequency.value = 600;
break;
case 'LOSE_LIFE':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(250, t);
osc.frequency.exponentialRampToValueAtTime(80, t + 0.4);
gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
filter.type = 'lowpass'; filter.frequency.value = 600;
break;
case 'GAME_OVER':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, t);
osc.frequency.exponentialRampToValueAtTime(40, t + 0.8);
gain.gain.setValueAtTime(0.25, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.8);
filter.type = 'lowpass'; filter.frequency.value = 300;
break;
}
osc.start(t);
osc.stop(t + (type === 'GAME_OVER' ? 0.9 : 0.3));
}
// ==========================================
// INITIALIZATION & RESIZING
// ==========================================
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
scale = Math.min(w / CANVAS_W, h / CANVAS_H);
canvas.width = CANVAS_W * scale;
canvas.height = CANVAS_H * scale;
canvas.style.width = `${canvas.width}px`;
canvas.style.height = `${canvas.height}px`;
}
window.addEventListener('resize', resize);
resize();
function initGame() {
score = 0; lives = 3; level = 1;
resetLevel();
initBgParticles();
}
function resetLevel() {
balls = [];
powerups = [];
lasers = [];
activePowerUp = { type: null, endTime: 0 };
spawnBall();
generateBricks();
}
function spawnBall(sticky = false) {
balls.push({
x: paddle.x, y: paddle.y - 15,
dx: (Math.random() > 0.5 ? 1 : -1) * 3 * (level * 0.2 + 1),
dy: -3 * (level * 0.2 + 1),
r: 6, speed: 3 * (level * 0.2 + 1),
sticky: sticky, active: true
});
}
function generateBricks() {
bricks = [];
const rows = Math.min(4 + level, 10);
const cols = 8;
const bw = 40, bh = 18, gap = 4;
const startX = (CANVAS_W - cols * (bw + gap)) / 2;
const startY = 60;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const hits = (level > 2 && Math.random() < 0.3) ? 2 : 1;
const color = Object.values(NEON)[(r + c) % Object.values(NEON).length];
bricks.push({
x: startX + c * (bw + gap), y: startY + r * (bh + gap),
w: bw, h: bh, color, hits, maxHits: hits, active: true
});
}
}
}
function initBgParticles() {
bgParticles = [];
for (let i = 0; i < 30; i++) {
bgParticles.push({
x: Math.random() * CANVAS_W, y: Math.random() * CANVAS_H,
vx: (Math.random() - 0.5) * 0.2, vy: (Math.random() - 0.5) * 0.2,
size: Math.random() * 2 + 1, color: Object.values(NEON)[Math.floor(Math.random() * Object.values(NEON).length)],
pulse: Math.random() * Math.PI * 2
});
}
}
// ==========================================
// INPUT HANDLING
// ==========================================
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseX = ((e.clientX - rect.left) / scale);
});
canvas.addEventListener('mousedown', () => { mouseDown = true; handleInput(); });
canvas.addEventListener('mouseup', () => { mouseDown = false; });
canvas.addEventListener('touchstart', e => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
mouseX = ((e.touches[0].clientX - rect.left) / scale);
mouseDown = true; handleInput();
}, { passive: false });
canvas.addEventListener('touchend', () => { mouseDown = false; });
window.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft') keys.left = true;
if (e.key === 'ArrowRight') keys.right = true;
});
window.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft') keys.left = false;
if (e.key === 'ArrowRight') keys.right = false;
});
function handleInput() {
if (state === 'START' || state === 'GAMEOVER') {
initAudio();
initGame();
state = 'PLAYING';
return;
}
// Launch sticky balls
balls.forEach(b => {
if (b.sticky && b.active) {
b.sticky = false;
b.dy = -b.speed;
b.dx = (Math.random() - 0.5) * 2;
}
});
}
// ==========================================
// PHYSICS & COLLISIONS
// ==========================================
function updateEntities() {
// Paddle movement
let targetX = mouseX;
if (keys.left) targetX -= 50 * DT * 60;
if (keys.right) targetX += 50 * DT * 60;
paddle.x = Math.max(paddle.w / 2, Math.min(CANVAS_W - paddle.w / 2, targetX));
// Active power-up timers
if (activePowerUp.type && performance.now() > activePowerUp.endTime) {
activePowerUp.type = null;
}
// Balls
balls.forEach(b => {
if (!b.active) return;
if (b.sticky) {
b.x = paddle.x;
b.y = paddle.y - b.r - 1;
return;
}
b.x += b.dx;
b.y += b.dy;
// Wall collisions
if (b.x - b.r < 0 || b.x + b.r > CANVAS_W) { b.dx *= -1; playSound('WALL'); }
if (b.y - b.r < 0) { b.dy *= -1; playSound('WALL'); }
if (b.y + b.r > CANVAS_H) {
b.active = false;
if (balls.filter(x => x.active).length === 0) {
lives--;
playSound('LOSE_LIFE');
if (lives <= 0) {
state = 'GAMEOVER';
playSound('GAME_OVER');
} else {
spawnBall();
}
}
}
// Paddle collision
if (b.y + b.r > paddle.y - paddle.h/2 && b.y - b.r < paddle.y + paddle.h/2 &&
b.x > paddle.x - paddle.w/2 && b.x < paddle.x + paddle.w/2 && b.dy > 0) {
b.dy *= -1;
const hitPos = (b.x - paddle.x) / (paddle.w / 2);
b.dx = hitPos * b.speed * 1.2;
b.y = paddle.y - paddle.h/2 - b.r - 1;
playSound('PADDLE');
}
// Brick collision
for (let brick of bricks) {
if (!brick.active) continue;
if (b.x + b.r > brick.x && b.x - b.r < brick.x + brick.w &&
b.y + b.r > brick.y && b.y - b.r < brick.y + brick.h) {
b.dx *= -1; b.dy *= -1;
brick.hits--;
if (brick.hits <= 0) {
brick.active = false;
score += 10 * level;
playSound('BRICK');
spawnParticles(brick.x + brick.w/2, brick.y + brick.h/2, brick.color);
if (Math.random() < 0.2) spawnPowerUp(brick.x + brick.w/2, brick.y + brick.h/2);
}
break;
}
}
});
// Remove dead balls
balls = balls.filter(b => b.active);
// Power-ups
powerups.forEach(p => {
p.y += p.dy;
if (p.y > CANVAS_H) p.active = false;
// Catch
if (p.active && p.y + 10 > paddle.y - paddle.h/2 && p.y - 10 < paddle.y + paddle.h/2 &&
p.x > paddle.x - paddle.w/2 && p.x < paddle.x + paddle.w/2) {
p.active = false;
applyPowerUp(p.type);
playSound('POWERUP');
}
});
powerups = powerups.filter(p => p.active);
// Lasers
if (activePowerUp.type === 'LASER' && Math.random() < 0.05) {
lasers.push({ x: paddle.x - 10, y: paddle.y - 10, dy: -8, active: true });
lasers.push({ x: paddle.x + 10, y: paddle.y - 10, dy: -8, active: true });
}
lasers.forEach(l => {
if (!l.active) return;
l.y += l.dy;
if (l.y < 0) l.active = false;
for (let brick of bricks) {
if (!brick.active) continue;
if (l.x > brick.x && l.x < brick.x + brick.w && l.y > brick.y && l.y < brick.y + brick.h) {
brick.hits--;
l.active = false;
if (brick.hits <= 0) {
brick.active = false;
score += 10 * level;
playSound('BRICK');
spawnParticles(brick.x + brick.w/2, brick.y + brick.h/2, brick.color);
if (Math.random() < 0.2) spawnPowerUp(brick.x + brick.w/2, brick.y + brick.h/2);
}
break;
}
}
});
lasers = lasers.filter(l => l.active);
// Particles
particles.forEach(p => {
p.x += p.vx; p.y += p.vy; p.life -= 0.02;
});
particles = particles.filter(p => p.life > 0);
// Level clear
if (bricks.every(b => !b.active)) {
level++;
state = 'LEVEL_TRANS';
setTimeout(() => { resetLevel(); state = 'PLAYING'; }, 1500);
}
}
function applyPowerUp(type) {
const now = performance.now();
activePowerUp.type = type;
activePowerUp.endTime = now + 8000; // 8 seconds default
if (type === 'WIDE') {
paddle.w = Math.min(140, paddle.w + 30);
activePowerUp.endTime = now + 8000;
} else if (type === 'MULTI') {
const current = balls.filter(b => b.active);
if (current.length > 0) {
for (let i = 0; i < 2; i++) {
const b = current[0];
balls.push({
x: b.x, y: b.y, dx: b.dx * (i === 0 ? 0.8 : 1.2), dy: b.dy,
r: b.r, speed: b.speed, sticky: false, active: true
});
}
}
} else if (type === 'SLOW') {
balls.forEach(b => { b.dx *= 0.7; b.dy *= 0.7; b.speed *= 0.7; });
} else if (type === 'STICKY') {
balls.forEach(b => { b.sticky = true; b.dy = 0; b.dx = 0; });
} else if (type === 'LASER') {
// Auto-fire handled in update
} else if (type === 'EXTRA_LIFE') {
lives = Math.min(lives + 1, 5);
}
}
function spawnPowerUp(x, y) {
const type = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
powerups.push({ x, y, dy: 1.5, type, active: true });
}
function spawnParticles(x, y, color) {
for (let i = 0; i < 8; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 3 + 1;
particles.push({
x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
color, life: 1
});
}
}
// ==========================================
// RENDERING
// ==========================================
function drawNeonRect(x, y, w, h, color, glow = 15) {
ctx.save();
ctx.shadowBlur = glow;
ctx.shadowColor = color;
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
ctx.restore();
}
function drawNeonCircle(cx, cy, r, color, glow = 15) {
ctx.save();
ctx.shadowBlur = glow;
ctx.shadowColor = color;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawBackground() {
// Dark base
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// Animated grid
ctx.save();
ctx.strokeStyle = 'rgba(0, 255, 255, 0.15)';
ctx.lineWidth = 1;
gridOffset = (gridOffset + 0.5) % 40;
for (let y = gridOffset; y < CANVAS_H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(CANVAS_W, y); ctx.stroke();
}
for (let x = 0; x < CANVAS_W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CANVAS_H); ctx.stroke();
}
ctx.restore();
// Floating particles
bgParticles.forEach(p => {
p.x += p.vx; p.y += p.vy;
p.pulse += 0.05;
if (p.x < 0) p.x = CANVAS_W; if (p.x > CANVAS_W) p.x = 0;
if (p.y < 0) p.y = CANVAS_H; if (p.y > CANVAS_H) p.y = 0;
const alpha = 0.3 + Math.sin(p.pulse) * 0.2;
ctx.save();
ctx.globalAlpha = alpha;
ctx.shadowBlur = 8; ctx.shadowColor = p.color;
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
ctx.restore();
});
}
function drawGame() {
drawBackground();
// Bricks
bricks.forEach(b => {
if (!b.active) return;
const alpha = b.hits / b.maxHits;
ctx.save();
ctx.shadowBlur = 12; ctx.shadowColor = b.color;
ctx.fillStyle = b.color;
ctx.globalAlpha = 0.4 + alpha * 0.6;
ctx.fillRect(b.x, b.y, b.w, b.h);
ctx.strokeStyle = b.color;
ctx.lineWidth = 2;
ctx.strokeRect(b.x, b.y, b.w, b.h);
if (b.hits < b.maxHits) {
ctx.fillStyle = '#000';
ctx.globalAlpha = 1;
ctx.fillRect(b.x + 2, b.y + 2, b.w - 4, b.h - 4);
ctx.globalAlpha = 0.3;
ctx.fillRect(b.x + 4, b.y + 4, b.w - 8, b.h - 8);
}
ctx.restore();
});
// Paddle
ctx.save();
ctx.shadowBlur = 20; ctx.shadowColor = activePowerUp.type === 'WIDE' ? NEON.MAGENTA : paddle.color;
ctx.fillStyle = activePowerUp.type === 'WIDE' ? NEON.MAGENTA : paddle.color;
ctx.fillRect(paddle.x - paddle.w / 2, paddle.y - paddle.h / 2, paddle.w, paddle.h);
ctx.restore();
// Balls
balls.forEach(b => {
if (!b.active) return;
ctx.save();
ctx.shadowBlur = 15; ctx.shadowColor = NEON.WHITE;
ctx.fillStyle = NEON.WHITE;
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill();
if (b.sticky) {
ctx.strokeStyle = NEON.ORANGE; ctx.lineWidth = 2; ctx.stroke();
}
ctx.restore();
});
// Power-ups
powerups.forEach(p => {
ctx.save();
ctx.shadowBlur = 15; ctx.shadowColor = POWERUP_COLORS[p.type];
ctx.fillStyle = POWERUP_COLORS[p.type];
ctx.beginPath(); ctx.arc(p.x, p.y, 8, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(POWERUP_ICONS[p.type], p.x, p.y);
ctx.restore();
});
// Lasers
ctx.save();
ctx.shadowBlur = 10; ctx.shadowColor = NEON.YELLOW;
ctx.fillStyle = NEON.YELLOW;
lasers.forEach(l => ctx.fillRect(l.x - 1, l.y, 2, 6));
ctx.restore();
// Particles
particles.forEach(p => {
ctx.save();
ctx.globalAlpha = p.life;
ctx.shadowBlur = 8; ctx.shadowColor = p.color;
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, 3, 3);
ctx.restore();
});
// HUD
ctx.save();
ctx.fillStyle = '#FFF'; ctx.font = '14px monospace'; ctx.textAlign = 'left';
ctx.fillText(`SCORE: ${score}`, 10, 25);
ctx.textAlign = 'right';
ctx.fillText(`LIVES: ${lives}`, CANVAS_W - 10, 25);
ctx.textAlign = 'center';
ctx.fillText(`LEVEL ${level}`, CANVAS_W / 2, 25);
// Active power-up indicator
if (activePowerUp.type) {
const remaining = Math.max(0, ((activePowerUp.endTime - performance.now()) / 1000)).toFixed(1);
ctx.fillStyle = POWERUP_COLORS[activePowerUp.type];
ctx.fillText(`${activePowerUp.type} [${remaining}s]`, CANVAS_W / 2, 45);
}
ctx.restore();
// Overlays
if (state === 'START') {
ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.save(); ctx.shadowBlur = 20; ctx.shadowColor = NEON.CYAN;
ctx.fillStyle = NEON.CYAN; ctx.font = 'bold 30px monospace'; ctx.textAlign = 'center';
ctx.fillText('NEON ARKANOID', CANVAS_W/2, CANVAS_H/2 - 20);
ctx.fillStyle = '#FFF'; ctx.font = '16px monospace';
ctx.fillText('CLICK OR TAP TO START', CANVAS_W/2, CANVAS_H/2 + 20);
ctx.restore();
} else if (state === 'GAMEOVER') {
ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.save(); ctx.shadowBlur = 20; ctx.shadowColor = NEON.PINK;
ctx.fillStyle = NEON.PINK; ctx.font = 'bold 30px monospace'; ctx.textAlign = 'center';
ctx.fillText('GAME OVER', CANVAS_W/2, CANVAS_H/2 - 20);
ctx.fillStyle = '#FFF'; ctx.font = '16px monospace';
ctx.fillText(`FINAL SCORE: ${score}`, CANVAS_W/2, CANVAS_H/2 + 10);
ctx.fillText('CLICK TO RESTART', CANVAS_W/2, CANVAS_H/2 + 40);
ctx.restore();
} else if (state === 'LEVEL_TRANS') {
ctx.save(); ctx.shadowBlur = 15; ctx.shadowColor = NEON.LIME;
ctx.fillStyle = NEON.LIME; ctx.font = 'bold 24px monospace'; ctx.textAlign = 'center';
ctx.fillText(`LEVEL ${level} CLEAR!`, CANVAS_W/2, CANVAS_H/2);
ctx.restore();
}
}
// ==========================================
// GAME LOOP
// ==========================================
function loop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.1);
lastTime = timestamp;
if (state === 'PLAYING') {
updateEntities();
}
ctx.save();
ctx.scale(scale, scale);
drawGame();
ctx.restore();
requestAnimationFrame(loop);
}
// Start
initGame();
requestAnimationFrame(loop);
</script>
</body>
</html>

Xet Storage Details

Size:
22.4 kB
·
Xet hash:
479f48ed7387b3aa9d2fffc80583e71d731f264fd73082b6bb09cc210e261b44

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.