Skip to content

Instantly share code, notes, and snippets.

@crusaderky
Created July 2, 2026 18:23
Show Gist options
  • Select an option

  • Save crusaderky/d6772b634bce09bf276e00e568300a37 to your computer and use it in GitHub Desktop.

Select an option

Save crusaderky/d6772b634bce09bf276e00e568300a37 to your computer and use it in GitHub Desktop.
Neon Arena
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Neon Arena</title>
<link rel="icon" href="data:image/x-icon;base64,">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
#hud {
position: fixed; top: 0; left: 0; width: 100%; padding: 20px 30px;
display: flex; justify-content: space-between; align-items: center;
pointer-events: none; z-index: 10;
font-size: 22px; color: #fff; text-shadow: 0 0 10px rgba(0,200,255,0.8);
}
#score-display { font-weight: 700; letter-spacing: 2px; }
#lives-display { font-weight: 700; letter-spacing: 2px; }
#lives-display .heart { color: #ff3366; margin: 0 2px; text-shadow: 0 0 12px #ff3366; }
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
display: none; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0,0,0,0.85); z-index: 20;
}
#game-over h1 {
font-size: 64px; color: #ff3366; margin-bottom: 10px;
text-shadow: 0 0 30px #ff3366, 0 0 60px rgba(255,51,102,0.5);
letter-spacing: 6px;
}
#game-over .final-score {
font-size: 28px; color: #fff; margin-bottom: 40px;
text-shadow: 0 0 10px rgba(255,255,255,0.5);
}
#game-over .restart-hint {
font-size: 18px; color: #88aacc; animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
#damage-flash {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 15;
background: radial-gradient(ellipse at center, transparent 40%, rgba(255,0,0,0.4) 100%);
opacity: 0; transition: opacity 0.1s;
}
#combo {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-size: 48px; font-weight: 900; color: #ffcc00;
text-shadow: 0 0 20px #ffcc00; pointer-events: none; z-index: 12;
opacity: 0; transition: opacity 0.3s;
}
</style>
</head>
<body>
<div id="hud">
<div id="score-display">SCORE: 0</div>
<div id="lives-display"></div>
</div>
<div id="game-over">
<h1>GAME OVER</h1>
<div class="final-score" id="final-score-display">Final Score: 0</div>
<div class="restart-hint">Press any key to restart</div>
</div>
<div id="damage-flash"></div>
<div id="combo"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function() {
'use strict';
// === CONSTANTS ===
const ARENA_SIZE = 40;
const HALF_ARENA = ARENA_SIZE / 2;
const PLAYER_SPEED = 30;
const PLAYER_ACCEL = 55;
const PLAYER_FRICTION = 6;
const PLAYER_RADIUS = 0.8;
const ORB_RADIUS = 0.5;
const ORB_SPEED = 3;
const ORB_SPAWN_RATE = 2000;
const ENEMY_BASE_SPEED = 6;
const ENEMY_SPAWN_INTERVAL_BASE = 2000;
const ENEMY_SPAWN_INTERVAL_MIN = 400;
const ENEMY_RADIUS = 0.7;
const LIVES_START = 3;
const CAMERA_HEIGHT = 8;
const CAMERA_DISTANCE = 10;
const CAMERA_LERP = 0.06;
// === STATE ===
let score = 0;
let lives = LIVES_START;
let gameRunning = true;
let gameTime = 0;
let difficulty = 1;
let lastEnemySpawn = 0;
let lastOrbSpawn = 0;
let comboCount = 0;
let lastCollectTime = 0;
let playerInvincible = 0;
let screenShake = 0;
const keys = {};
const playerVelocity = new THREE.Vector3();
const cameraTarget = new THREE.Vector3();
const cameraPosition = new THREE.Vector3();
// === SCENE SETUP ===
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.FogExp2(0x050510, 0.018);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, CAMERA_HEIGHT, CAMERA_DISTANCE);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
// FPS meter
const fpsEl = document.createElement('div');
fpsEl.style.cssText = 'position:fixed;bottom:10px;left:10px;color:#0f0;font:12px monospace;z-index:100;pointer-events:none;text-shadow:0 0 4px #0f0;';
document.body.appendChild(fpsEl);
// === LIGHTING ===
const ambientLight = new THREE.AmbientLight(0x222244, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0x88aaff, 0.6);
dirLight.position.set(15, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 60;
dirLight.shadow.camera.left = -25;
dirLight.shadow.camera.right = 25;
dirLight.shadow.camera.top = 25;
dirLight.shadow.camera.bottom = -25;
scene.add(dirLight);
const pointLight1 = new THREE.PointLight(0x00ccff, 1.5, 30);
pointLight1.position.set(0, 5, 0);
scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0xff3366, 0.8, 20);
pointLight2.position.set(-15, 4, -15);
scene.add(pointLight2);
const pointLight3 = new THREE.PointLight(0xffcc00, 0.8, 20);
pointLight3.position.set(15, 4, 15);
scene.add(pointLight3);
// === ARENA FLOOR ===
const floorGeo = new THREE.PlaneGeometry(ARENA_SIZE, ARENA_SIZE, 20, 20);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x111122,
roughness: 0.8,
metalness: 0.2,
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// Grid lines on floor
const gridHelper = new THREE.GridHelper(ARENA_SIZE, 20, 0x1a1a3a, 0x111133);
gridHelper.position.y = 0.01;
scene.add(gridHelper);
// === ARENA WALLS (glowing borders) ===
const wallMat = new THREE.MeshStandardMaterial({
color: 0x00aaff,
emissive: 0x004488,
emissiveIntensity: 0.5,
transparent: true,
opacity: 0.3,
});
const wallHeight = 2;
const wallPositions = [
{ pos: [0, wallHeight/2, -HALF_ARENA], rot: [0, 0, 0], scale: [ARENA_SIZE, wallHeight, 0.3] },
{ pos: [0, wallHeight/2, HALF_ARENA], rot: [0, 0, 0], scale: [ARENA_SIZE, wallHeight, 0.3] },
{ pos: [-HALF_ARENA, wallHeight/2, 0], rot: [0, 0, 0], scale: [0.3, wallHeight, ARENA_SIZE] },
{ pos: [HALF_ARENA, wallHeight/2, 0], rot: [0, 0, 0], scale: [0.3, wallHeight, ARENA_SIZE] },
];
wallPositions.forEach(w => {
const geo = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geo, wallMat);
mesh.position.set(...w.pos);
mesh.scale.set(...w.scale);
mesh.castShadow = true;
scene.add(mesh);
});
// === PLAYER ===
const playerGroup = new THREE.Group();
const playerBodyGeo = new THREE.SphereGeometry(PLAYER_RADIUS, 16, 16);
const playerBodyMat = new THREE.MeshStandardMaterial({
color: 0x00ddff,
emissive: 0x0066aa,
emissiveIntensity: 0.8,
roughness: 0.2,
metalness: 0.8,
});
const playerBody = new THREE.Mesh(playerBodyGeo, playerBodyMat);
playerBody.castShadow = true;
playerGroup.add(playerBody);
// Player glow ring
const ringGeo = new THREE.TorusGeometry(PLAYER_RADIUS + 0.15, 0.05, 8, 32);
const ringMat = new THREE.MeshStandardMaterial({
color: 0x00ffff,
emissive: 0x00ffff,
emissiveIntensity: 1.5,
});
const playerRing = new THREE.Mesh(ringGeo, ringMat);
playerRing.rotation.x = Math.PI / 2;
playerGroup.add(playerRing);
// Player point light
const playerLight = new THREE.PointLight(0x00ccff, 2, 8);
playerLight.position.y = 0.5;
playerGroup.add(playerLight);
scene.add(playerGroup);
// === ORBS ===
const orbs = [];
const orbColors = [0xffcc00, 0xff6600, 0xff0066, 0x66ff00, 0x00ffcc];
function spawnOrb() {
const color = orbColors[Math.floor(Math.random() * orbColors.length)];
const geo = new THREE.SphereGeometry(ORB_RADIUS, 12, 12);
const mat = new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 1.2,
transparent: true,
opacity: 0.9,
});
const orb = new THREE.Mesh(geo, mat);
// Random position within arena bounds
const margin = 3;
orb.position.x = (Math.random() - 0.5) * (ARENA_SIZE - margin * 2);
orb.position.z = (Math.random() - 0.5) * (ARENA_SIZE - margin * 2);
orb.position.y = 1.2 + Math.random() * 0.5;
// Glow light for orb
const orbLight = new THREE.PointLight(color, 0.8, 5);
orb.add(orbLight);
orb.userData = {
baseY: orb.position.y,
phase: Math.random() * Math.PI * 2,
collected: false,
};
scene.add(orb);
orbs.push(orb);
}
for (let i = 0; i < 8; i++) spawnOrb();
// === ENEMIES ===
const enemies = [];
const enemyColors = [0xff0044, 0xff3300, 0xcc0066];
function spawnEnemy() {
const color = enemyColors[Math.floor(Math.random() * enemyColors.length)];
const geo = new THREE.OctahedronGeometry(ENEMY_RADIUS, 0);
const mat = new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.8,
roughness: 0.3,
metalness: 0.6,
});
const enemy = new THREE.Mesh(geo, mat);
// Spawn at random edge position
const side = Math.floor(Math.random() * 4);
const margin = 2;
switch (side) {
case 0: enemy.position.set((Math.random()-0.5)*ARENA_SIZE, 1.2, -HALF_ARENA - margin); break;
case 1: enemy.position.set((Math.random()-0.5)*ARENA_SIZE, 1.2, HALF_ARENA + margin); break;
case 2: enemy.position.set(-HALF_ARENA - margin, 1.2, (Math.random()-0.5)*ARENA_SIZE); break;
case 3: enemy.position.set(HALF_ARENA + margin, 1.2, (Math.random()-0.5)*ARENA_SIZE); break;
}
// Enemy glow
const enemyLight = new THREE.PointLight(color, 0.5, 4);
enemy.add(enemyLight);
enemy.userData = {
speed: ENEMY_BASE_SPEED + Math.random() * 2,
rotationSpeed: new THREE.Vector3(
(Math.random()-0.5)*3, (Math.random()-0.5)*3, (Math.random()-0.5)*3
),
phase: Math.random() * Math.PI * 2,
};
scene.add(enemy);
enemies.push(enemy);
}
// === PARTICLES ===
const particles = [];
const particlePool = [];
function spawnParticles(position, color, count, spread, speed) {
for (let i = 0; i < count; i++) {
let p = particlePool.length > 0 ? particlePool.pop() : null;
if (!p) {
p = {
mesh: new THREE.Mesh(
new THREE.SphereGeometry(0.08 + Math.random() * 0.12, 6, 6),
new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1 })
),
velocity: new THREE.Vector3(),
life: 0,
maxLife: 0.5 + Math.random() * 0.5,
};
scene.add(p.mesh);
}
p.mesh.position.copy(position);
p.mesh.material.opacity = 1;
p.mesh.scale.setScalar(0.5 + Math.random());
const angle = Math.random() * Math.PI * 2;
const vertAngle = (Math.random() - 0.5) * Math.PI;
p.velocity.set(
Math.cos(angle) * Math.cos(vertAngle),
Math.sin(vertAngle) * 0.5 + 0.5,
Math.sin(angle) * Math.cos(vertAngle)
).multiplyScalar(speed * (0.5 + Math.random()));
p.life = p.maxLife;
particles.push(p);
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) {
scene.remove(p.mesh);
p.mesh.material.dispose();
particlePool.push(p);
particles.splice(i, 1);
continue;
}
p.velocity.y -= 9.8 * dt;
p.mesh.position.add(p.velocity.clone().multiplyScalar(dt));
p.mesh.material.opacity = Math.max(0, p.life / p.maxLife);
p.mesh.scale.setScalar((p.life / p.maxLife) * (0.5 + Math.random() * 0.3));
}
}
// === HUD ===
function updateHUD() {
document.getElementById('score-display').textContent = `SCORE: ${score}`;
let heartsHTML = '';
for (let i = 0; i < LIVES_START; i++) {
heartsHTML += i < lives ? '<span class="heart">♥</span>' : '<span style="color:#333;margin:0 2px;">♥</span>';
}
document.getElementById('lives-display').innerHTML = heartsHTML;
}
// === GAME OVER ===
function showGameOver() {
gameRunning = false;
document.getElementById('final-score-display').textContent = `Final Score: ${score}`;
document.getElementById('game-over').style.display = 'flex';
}
function restartGame() {
score = 0;
lives = LIVES_START;
gameTime = 0;
difficulty = 1;
comboCount = 0;
playerInvincible = 0;
screenShake = 0;
playerVelocity.set(0, 0, 0);
playerGroup.position.set(0, 0.8, 0);
// Clear enemies
enemies.forEach(e => { scene.remove(e); });
enemies.length = 0;
// Clear orbs
orbs.forEach(o => { scene.remove(o); });
orbs.length = 0;
for (let i = 0; i < 8; i++) spawnOrb();
lastEnemySpawn = performance.now();
lastOrbSpawn = performance.now();
document.getElementById('game-over').style.display = 'none';
gameRunning = true;
updateHUD();
}
// === INPUT ===
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (!gameRunning && document.getElementById('game-over').style.display === 'flex') {
restartGame();
}
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// === COLLISION ===
function checkCollision(a, b, radiusA, radiusB) {
const dx = a.position.x - b.position.x;
const dz = a.position.z - b.position.z;
const dist = Math.sqrt(dx * dx + dz * dz);
return dist < radiusA + radiusB;
}
// === MAIN LOOP ===
let prevTime = performance.now();
let fpsFrames = 0;
let fpsTime = 0;
let currentFps = 0;
function update() {
const now = performance.now();
const dt = Math.min((now - prevTime) / 1000, 0.05);
prevTime = now;
// FPS calculation
fpsFrames++;
fpsTime += dt;
if (fpsTime >= 0.5) {
currentFps = Math.round(fpsFrames / fpsTime);
fpsFrames = 0;
fpsTime = 0;
fpsEl.textContent = `${currentFps} FPS`;
}
if (!gameRunning) return;
gameTime += dt;
difficulty = 1 + gameTime / 30; // ramps over 30 seconds
// === PLAYER MOVEMENT ===
let moveX = 0, moveZ = 0;
if (keys['KeyW'] || keys['ArrowUp']) moveZ -= 1;
if (keys['KeyS'] || keys['ArrowDown']) moveZ += 1;
if (keys['KeyA'] || keys['ArrowLeft']) moveX -= 1;
if (keys['KeyD'] || keys['ArrowRight']) moveX += 1;
// Normalize diagonal
if (moveX !== 0 && moveZ !== 0) {
const inv = 1 / Math.sqrt(2);
moveX *= inv;
moveZ *= inv;
}
// Apply acceleration
playerVelocity.x += moveX * PLAYER_ACCEL * dt;
playerVelocity.z += moveZ * PLAYER_ACCEL * dt;
// Apply friction
playerVelocity.x -= playerVelocity.x * PLAYER_FRICTION * dt;
playerVelocity.z -= playerVelocity.z * PLAYER_FRICTION * dt;
// Clamp speed
const speed = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);
if (speed > PLAYER_SPEED) {
playerVelocity.x *= PLAYER_SPEED / speed;
playerVelocity.z *= PLAYER_SPEED / speed;
}
// Move player
playerGroup.position.x += playerVelocity.x * dt;
playerGroup.position.z += playerVelocity.z * dt;
// Arena bounds
playerGroup.position.x = Math.max(-HALF_ARENA + PLAYER_RADIUS, Math.min(HALF_ARENA - PLAYER_RADIUS, playerGroup.position.x));
playerGroup.position.z = Math.max(-HALF_ARENA + PLAYER_RADIUS, Math.min(HALF_ARENA - PLAYER_RADIUS, playerGroup.position.z));
// Rotate player based on movement
if (speed > 0.5) {
const targetRot = Math.atan2(playerVelocity.x, playerVelocity.z);
playerGroup.rotation.y += (targetRot - playerGroup.rotation.y) * 10 * dt;
}
// Player bob animation
playerBody.position.y = Math.sin(now * 0.003) * 0.1;
playerRing.rotation.z = now * 0.002;
// Invincibility flash - smooth toggle
if (playerInvincible > 0) {
playerInvincible -= dt;
const flashPhase = Math.floor((2 - playerInvincible) * 8) % 2;
playerBody.visible = flashPhase === 0;
} else {
playerBody.visible = true;
}
// === CAMERA ===
const camOffset = new THREE.Vector3(0, CAMERA_HEIGHT, CAMERA_DISTANCE);
// Add slight offset based on player velocity for dynamic feel
camOffset.x += playerVelocity.x * 0.3;
camOffset.z += playerVelocity.z * 0.2;
const desiredCamPos = playerGroup.position.clone().add(camOffset);
cameraPosition.lerp(desiredCamPos, CAMERA_LERP);
camera.position.copy(cameraPosition);
camera.lookAt(playerGroup.position);
// === ORBS ===
for (let i = orbs.length - 1; i >= 0; i--) {
const orb = orbs[i];
orb.userData.phase += dt * ORB_SPEED;
orb.position.y = orb.userData.baseY + Math.sin(orb.userData.phase) * 0.3;
orb.rotation.y += dt * 2;
if (checkCollision(playerGroup, orb, PLAYER_RADIUS, ORB_RADIUS)) {
// Collect!
const now2 = performance.now();
if (now2 - lastCollectTime < 2000) {
comboCount++;
} else {
comboCount = 1;
}
lastCollectTime = now2;
const points = 10 * (1 + Math.floor(comboCount / 3));
score += points;
// Particles
spawnParticles(orb.position, orb.material.color.getHex(), 12, 3, 4);
scene.remove(orb);
orbs.splice(i, 1);
// Spawn new orb
spawnOrb();
updateHUD();
// Combo display
if (comboCount >= 3) {
const comboEl = document.getElementById('combo');
comboEl.textContent = `${comboCount}x COMBO!`;
comboEl.style.opacity = '1';
setTimeout(() => { comboEl.style.opacity = '0'; }, 800);
}
}
}
// === ENEMIES ===
const spawnInterval = Math.max(ENEMY_SPAWN_INTERVAL_MIN, ENEMY_SPAWN_INTERVAL_BASE / difficulty);
if (now - lastEnemySpawn > spawnInterval) {
spawnEnemy();
lastEnemySpawn = now;
}
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
// Move toward player
const dir = new THREE.Vector3().subVectors(playerGroup.position, enemy.position);
dir.y = 0;
dir.normalize();
const enemySpeed = (ENEMY_BASE_SPEED + (difficulty - 1) * 2) * (0.8 + Math.random() * 0.4);
enemy.position.x += dir.x * enemySpeed * dt;
enemy.position.z += dir.z * enemySpeed * dt;
enemy.position.y = 1.2 + Math.sin(now * 0.004 + enemy.userData.phase) * 0.2;
// Rotate
enemy.rotation.x += enemy.userData.rotationSpeed.x * dt;
enemy.rotation.y += enemy.userData.rotationSpeed.y * dt;
enemy.rotation.z += enemy.userData.rotationSpeed.z * dt;
// Collision with player
if (playerInvincible <= 0 && checkCollision(playerGroup, enemy, PLAYER_RADIUS, ENEMY_RADIUS)) {
lives--;
playerInvincible = 2;
screenShake = 0.5;
// Damage flash
const flash = document.getElementById('damage-flash');
flash.style.opacity = '1';
setTimeout(() => { flash.style.opacity = '0'; }, 300);
// Particles
spawnParticles(playerGroup.position, 0xff0000, 15, 4, 5);
// Remove enemy
spawnParticles(enemy.position, enemy.material.color.getHex(), 8, 2, 3);
scene.remove(enemy);
enemies.splice(i, 1);
updateHUD();
if (lives <= 0) {
showGameOver();
return;
}
}
// Remove if too far from arena
if (Math.abs(enemy.position.x) > HALF_ARENA + 10 || Math.abs(enemy.position.z) > HALF_ARENA + 10) {
scene.remove(enemy);
enemies.splice(i, 1);
}
}
// === PARTICLES ===
updateParticles(dt);
// === SCREEN SHAKE ===
if (screenShake > 0) {
screenShake -= dt * 2;
camera.position.x += (Math.random() - 0.5) * screenShake * 0.3;
camera.position.y += (Math.random() - 0.5) * screenShake * 0.2;
}
// === ANIMATE LIGHTS ===
pointLight1.intensity = 1.5 + Math.sin(now * 0.002) * 0.3;
pointLight2.position.x = -15 + Math.sin(now * 0.001) * 3;
pointLight3.position.x = 15 + Math.cos(now * 0.001) * 3;
}
function animate() {
requestAnimationFrame(animate);
update();
renderer.render(scene, camera);
}
updateHUD();
animate();
})();
</script>
</body>
</html>

Build a 3D arena game as a SINGLE self-contained .html file.

STACK (mandatory):

  • Three.js loaded from a CDN (one <script> tag). No other JS libraries, no build step.
  • All HTML, CSS, and JS in this one file. It must run by opening it directly in a browser.

CORE SPEC (mandatory — implement all of this exactly):

  1. A flat ground plane forming a bounded arena. The player cannot leave its bounds.
  2. A player object on the ground. WASD moves it (camera-relative); movement has momentum, not instant stop/start.
  3. A third-person camera that smoothly follows behind the player.
  4. Collectible glowing orbs spawn at random positions. Touching one collects it (+10 score) and spawns a new one.
  5. Enemy objects spawn at the arena edges and move toward the player. Contact with the player costs 1 life.
  6. Player starts with 3 lives. A HUD shows score and lives at all times.
  7. At 0 lives: a game-over screen showing final score, with a key press to restart.
  8. Difficulty ramps over time (enemies spawn faster and/or move faster).

STRETCH (strongly encouraged — you will be judged on this): Beyond the core, make it feel PREMIUM. Lighting, shadows, particles, juice, smooth camera, satisfying feedback, polished HUD, atmosphere. Add depth or complexity if it improves the experience. Aim to genuinely impress — this is evaluated on visual quality and feel, not just correctness.

RULES:

  • Implement the full core before adding stretch features.
  • Output the complete, ready-to-run .html file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment