(() => { // Game Config const WORLD = { width: 4200, height: 180 }; const GRAVITY = 0.35; const FRICTION = 0.80; const MAX_FALL = 8.5; const PLAYER = { w: 14, h: 16, speed: 1.25, jump: 6.5, fireCooldown: 280 }; const CAMERA = { lerp: 0.12, shake: 0 }; const TILE = 16; // Colors (choose pleasing 16-bit palette) const COLORS = { sky: "#14213d", sky2: "#0f1a35", cloud: "#e9f0ff", cloud2: "#d6e4ff", hill: "#274c77", hill2: "#1d3a5b", grass: "#41a55d", dirt: "#6b4f3a", dirt2: "#5a4130", pipe: "#2f9e44", pipeDark: "#237a34", enemy: "#8b5e34", enemyDark: "#704a2a", playerHat: "#e63946", playerBody: "#1d3557", playerFace: "#ffe0bd", fire1: "#ff9f1c", fire2: "#ffd166", coin1: "#ffd166", coin2: "#fca311", uiText: "#ffffff", overlay: "rgba(0,0,0,0.55)" }; // DOM const bgCanvas = document.getElementById("bg"); const gameCanvas = document.getElementById("game"); const bgCtx = bgCanvas.getContext("2d"); const ctx = gameCanvas.getContext("2d"); ctx.imageSmoothingEnabled = false; bgCtx.imageSmoothingEnabled = false; const scoreEl = document.getElementById("score"); const coinsEl = document.getElementById("coins"); const livesEl = document.getElementById("lives"); const messageBox = document.getElementById("messageBox"); const messageTitle = document.getElementById("messageTitle"); const messageText = document.getElementById("messageText"); // Input const keys = { left: false, right: false, up: false, fire: false, pause: false, mute: false }; // Touch controls const btnLeft = document.getElementById("btnLeft"); const btnRight = document.getElementById("btnRight"); const btnJump = document.getElementById("btnJump"); const btnFire = document.getElementById("btnFire"); // Camera const camera = { x: 0, y: 0, w: 320, h: 180 }; // Game State let state = "playing"; // playing, paused, gameover, win let score = 0; let coins = 0; let lives = 3; let time = 0; let muted = false; // Entities const player = { x: 32, y: 0, w: PLAYER.w, h: PLAYER.h, vx: 0, vy: 0, dir: 1, onGround: false, canFireAt: 0, spawnX: 32, spawnY: 0 }; /** Platforms and Pipes (simple AABB rectangles) */ const solids = [ // Ground { x: 0, y: 150, w: WORLD.width, h: 60 }, // Small hills / platforms { x: 240, y: 136, w: 90, h: 8 }, { x: 420, y: 122, w: 90, h: 8 }, { x: 650, y: 142, w: 80, h: 8 }, { x: 980, y: 120, w: 120, h: 8 }, { x: 1270, y: 128, w: 100, h: 8 }, { x: 1550, y: 110, w: 80, h: 8 }, { x: 1800, y: 140, w: 140, h: 8 }, { x: 2150, y: 118, w: 100, h: 8 }, { x: 2450, y: 130, w: 90, h: 8 }, { x: 2780, y: 115, w: 120, h: 8 }, { x: 3120, y: 140, w: 120, h: 8 }, { x: 3450, y: 120, w: 100, h: 8 }, { x: 3720, y: 135, w: 90, h: 8 }, ]; // Pipes const pipes = [ { x: 600, y: 150 - 24, w: 24, h: 24 }, { x: 1320, y: 150 - 32, w: 24, h: 32 }, { x: 2000, y: 150 - 40, w: 24, h: 40 }, { x: 2680, y: 150 - 28, w: 24, h: 28 }, { x: 3300, y: 150 - 36, w: 24, h: 36 }, ]; solids.push(...pipes); /** Enemies */ const enemies = []; const enemySpawn = [300, 480, 700, 1060, 1300, 1700, 2100, 2450, 2890, 3230, 3600, 3920]; enemySpawn.forEach((x, i) => { enemies.push({ x, y: 0, w: 14, h: 12, vx: (i % 2 === 0 ? -0.6 : -0.8), vy: 0, dir: -1, onGround: false, dead: false, squash: 0 }); }); /** Fireballs */ const fireballs = []; /** Coins */ const coinsArr = []; const coinSpawn = [ [260, 120], [440, 108], [680, 128], [1000, 100], [1280, 110], [1560, 95], [1810, 120], [2160, 100], [2460, 110], [2790, 95], [3140, 120], [3460, 100], [3740, 116] ]; coinSpawn.forEach(([x, y]) => { coinsArr.push({ x, y, w: 6, h: 8, t: Math.random() * Math.PI * 2, collected: false }); }); // Resize handling function resize() { bgCanvas.width = window.innerWidth; bgCanvas.height = window.innerHeight; gameCanvas.width = window.innerWidth; gameCanvas.height = window.innerHeight; camera.w = gameCanvas.width; camera.h = gameCanvas.height; } window.addEventListener("resize", resize); resize(); // Input: Keyboard const onKey = (e, down) => { const k = e.key.toLowerCase(); if (["arrowleft","arrowright","arrowup"," "].includes(e.key.toLowerCase())) e.preventDefault(); if (k === "arrowleft" || k === "a") keys.left = down; if (k === "arrowright" || k === "d") keys.right = down; if (k === "arrowup" || k === "w" || e.key === " ") keys.up = down; if (k === "f") keys.fire = down; if (k === "p" && down) togglePause(); if (k === "m" && down) muted = !muted; if (k === "r" && down) resetGame(); }; window.addEventListener("keydown", (e)=>onKey(e, true)); window.addEventListener("keyup", (e)=>onKey(e, false)); // Touch helpers const press = (setter) => (down) => { setter(down); }; btnLeft.addEventListener("pointerdown", press(v => keys.left = v)); btnLeft.addEventListener("pointerup", press(v => keys.left = v)); btnLeft.addEventListener("pointerleave", press(v => keys.left = v)); btnRight.addEventListener("pointerdown", press(v => keys.right = v)); btnRight.addEventListener("pointerup", press(v => keys.right = v)); btnRight.addEventListener("pointerleave", press(v => keys.right = v)); btnJump.addEventListener("pointerdown", press(v => keys.up = v)); btnJump.addEventListener("pointerup", press(v => keys.up = v)); btnJump.addEventListener("pointerleave", press(v => keys.up = v)); btnFire.addEventListener("pointerdown", press(v => keys.fire = v)); btnFire.addEventListener("pointerup", press(v => keys.fire = v)); btnFire.addEventListener("pointerleave", press(v => keys.fire = v)); // Audio (simple blips) const AudioCtx = window.AudioContext || window.webkitAudioContext; const actx = new AudioCtx(); function beep(freq = 440, dur = 0.06, type = "square", vol = 0.05) { if (muted) return; const o = actx.createOscillator(); const g = actx.createGain(); o.type = type; o.frequency.value = freq; g.gain.value = vol; o.connect(g).connect(actx.destination); o.start(); o.stop(actx.currentTime + dur); } // Utils const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); function rectsOverlap(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; } function collideAxis(entity, dx, dy) { // Move X if (dx !== 0) { entity.x += dx; for (const s of solids) { if (rectsOverlap(entity, s)) { if (dx > 0) entity.x = s.x - entity.w; else entity.x = s.x + s.w; entity.vx = 0; } } } // Move Y let onGround = false; if (dy !== 0) { entity.y += dy; for (const s of solids) { if (rectsOverlap(entity, s)) { if (dy > 0) { // falling entity.y = s.y - entity.h; entity.vy = 0; onGround = true; } else { // jumping entity.y = s.y + s.h; entity.vy = 0; } } } } return onGround; } // Update player function updatePlayer(dt) { // Horizontal if (keys.left) player.vx -= PLAYER.speed * dt; if (keys.right) player.vx += PLAYER.speed * dt; if (!(keys.left || keys.right)) player.vx *= FRICTION; player.vx = clamp(player.vx, -2.4, 2.4); if (Math.abs(player.vx) > 0.05) player.dir = player.vx > 0 ? 1 : -1; // Jump if (keys.up && player.onGround) { player.vy = -PLAYER.jump; player.onGround = false; beep(600, 0.07, "square", 0.05); CAMERA.shake = Math.max(CAMERA.shake, 2); } // Gravity player.vy += GRAVITY * dt; player.vy = clamp(player.vy, -99, MAX_FALL); // Move and collide const onGroundNow = collideAxis(player, player.vx * dt, 0) === true; const prevVy = player.vy; const landed = collideAxis(player, 0, player.vy * dt) === true; if (landed) { player.onGround = true; if (prevVy > 4.5) { CAMERA.shake = Math.max(CAMERA.shake, prevVy * 0.3); beep(120, 0.05, "sawtooth", 0.03); } } else { player.onGround = onGroundNow; } // Fire if (keys.fire && performance.now() > player.canFireAt) { shootFireball(); player.canFireAt = performance.now() + PLAYER.fireCooldown; } // Clamp world player.x = clamp(player.x, 0, WORLD.width - player.w); if (player.y > WORLD.height + 100) { // fell off world loseLife(true); } } function shootFireball() { const fb = { x: player.x + player.w / 2 + player.dir * 8, y: player.y + 6, w: 5, h: 5, vx: 3.2 * player.dir, vy: -0.2, life: 1400 }; fireballs.push(fb); beep(900, 0.05, "square", 0.04); } function updateFireballs(dt) { for (let i = fireballs.length - 1; i >= 0; i--) { const f = fireballs[i]; f.vy += 0.1 * dt; f.x += f.vx * dt; f.y += f.vy * dt; f.life -= dt; let remove = false; // Collide with solids for (const s of solids) { if (rectsOverlap(f, s)) { remove = true; break; } } // Hit enemies if (!remove) { for (const e of enemies) { if (!e.dead && rectsOverlap(f, e)) { e.dead = true; e.squash = 0.3; addScore(100); beep(300, 0.06, "triangle", 0.05); remove = true; break; } } } if (remove || f.life <= 0) fireballs.splice(i, 1); } } function updateEnemies(dt) { for (const e of enemies) { if (e.dead) { e.squash -= 0.02 * dt; if (e.squash <= -0.2) e.y += 0.2 * dt; continue; } // Basic AI: walk, turn on edge or hit wall const ahead = { x: e.x + (e.vx > 0 ? e.w + 1 : -1), y: e.y + e.h + 1, w: 1, h: 1 }; // Edge detection let onGround = false; for (const s of solids) { if (rectsOverlap(ahead, s)) { onGround = true; break; } } if (!onGround) e.vx = -e.vx; // Gravity e.vy += GRAVITY * dt; e.vy = clamp(e.vy, -99, MAX_FALL); // Move with collision collideAxis(e, e.vx * dt, 0); const landed = collideAxis(e, 0, e.vy * dt); if (landed) e.onGround = true; // Reverse on solid hit for (const s of solids) { if (rectsOverlap(e, s)) { if (e.x + e.w > s.x && e.x < s.x) e.vx = Math.abs(e.vx); else if (e.x < s.x + s.w && e.x + e.w > s.x + s.w) e.vx = -Math.abs(e.vx); } } // Player interaction if (rectsOverlap(player, e) && !e.dead) { // Stomp if falling and above enemy const playerBottom = player.y + player.h; const enemyTop = e.y; if (player.vy > 0.2 && playerBottom - enemyTop < 10) { e.dead = true; e.squash = 0.3; player.vy = -4.8; // bounce addScore(100); beep(340, 0.07, "triangle", 0.05); } else { // Player hit loseLife(false); } } } } function updateCoins(dt) { for (const c of coinsArr) { c.t += 0.05 * dt; if (!c.collected && rectsOverlap(player, c)) { c.collected = true; coins += 1; addScore(200); beep(880, 0.05, "sine", 0.05); } } } function addScore(n) { score += n; scoreEl.textContent = score.toString().padStart(6, "0"); } function loseLife(fell) { if (state !== "playing") return; lives -= 1; livesEl.textContent = lives.toString(); beep(160, 0.25, "sawtooth", 0.08); CAMERA.shake = 10; if (lives < 0) { state = "gameover"; showMessage("GAME OVER", "Press R to Restart"); } else { // Respawn player.x = player.spawnX; player.y = player.spawnY; player.vx = 0; player.vy = 0; } } function checkWin() { if (player.x > WORLD.width - 220 && state === "playing") { state = "win"; showMessage("YOU WIN!", "Press R to Play Again"); addScore(1000); } } function showMessage(title, text) { messageTitle.textContent = title; messageText.textContent = text; messageBox.classList.remove("hidden"); } function hideMessage() { messageBox.classList.add("hidden"); } function togglePause() { if (state === "playing") { state = "paused"; showMessage("PAUSED", "Press P to Resume"); } else if (state === "paused") { state = "playing"; hideMessage(); } } function resetGame() { // Reset state score = 0; coins = 0; lives = 3; time = 0; player.x = 32; player.y = 0; player.vx = 0; player.vy = 0; enemies.forEach((e, i) => { e.dead = false; e.squash = 0; e.x = enemySpawn[i]; e.y = 0; e.vx = (i % 2 === 0 ? -0.6 : -0.8); e.vy = 0; }); coinsArr.forEach(c => c.collected = false); fireballs.length = 0; scoreEl.textContent = "000000"; coinsEl.textContent = "0"; livesEl.textContent = "3"; state = "playing"; hideMessage(); } // Camera follows player with easing function updateCamera(dt) { const targetX = clamp(player.x + player.w / 2 - camera.w / 2, 0, WORLD.width - camera.w); camera.x += (targetX - camera.x) * CAMERA.lerp * dt; // Parallax background offset } // Background drawing (parallax) function drawBackground() { // Sky gradient is via CSS; draw clouds/hills on bg canvas const w = bgCanvas.width, h = bgCanvas.height; bgCtx.clearRect(0, 0, w, h); // Parallax offsets const camX = camera.x * 0.3; const hillX = camera.x * 0.5; // Clouds function cloud(x, y, s, c1, c2) { bgCtx.fillStyle = c1; bgCtx.beginPath(); bgCtx.ellipse(x, y, 18*s, 12*s, 0, 0, Math.PI*2); bgCtx.ellipse(x+16*s, y+2*s, 16*s, 10*s, 0, 0, Math.PI*2); bgCtx.ellipse(x-16*s, y+3*s, 14*s, 9*s, 0, 0, Math.PI*2); bgCtx.fill(); bgCtx.fillStyle = c2; bgCtx.beginPath(); bgCtx.ellipse(x+2, y+5, 14*s, 8*s, 0, 0, Math.PI*2); bgCtx.fill(); } for (let i=0;i<6;i++){ const cx = ((i*280 - camX*0.8) % (w+300)) - 150; cloud(cx, 80 + (i%2)*12, 1 + (i%3)*0.2, COLORS.cloud, COLORS.cloud2); } // Hills function hill(x, baseY, r, color) { bgCtx.fillStyle = color; bgCtx.beginPath(); bgCtx.arc(x, baseY, r, Math.PI, 0); bgCtx.lineTo(x+r, baseY+200); bgCtx.lineTo(x-r, baseY+200); bgCtx.closePath(); bgCtx.fill(); } for (let i=0;i<8;i++) { const hx = ((i*280 - hillX) % (w+400)) - 200; hill(hx, h-40, 140 + (i%3)*20, COLORS.hill); hill(hx+140, h-36, 120 + ((i+1)%3)*16, COLORS.hill2); } } // Draw entities and tiles function draw() { // Camera shake if (CAMERA.shake > 0) CAMERA.shake *= 0.9; const shakeX = (Math.random() - 0.5) * CAMERA.shake; const shakeY = (Math.random() - 0.5) * CAMERA.shake; // Clear ctx.clearRect(0, 0, gameCanvas.width, gameCanvas.height); ctx.save(); ctx.translate(-Math.floor(camera.x) + shakeX, -Math.floor(camera.y) + shakeY); // Ground tiles for (let x = 0; x < WORLD.width; x += TILE) { const baseY = 150; ctx.fillStyle = COLORS.dirt; ctx.fillRect(x, baseY, TILE, WORLD.height - baseY); // top grass ctx.fillStyle = COLORS.grass; ctx.fillRect(x, baseY - 2, TILE, 2); // speckles ctx.fillStyle = COLORS.dirt2; for (let i = 0; i < 3; i++) { const sx = x + (i * 5 % 12) + 2; const sy = baseY + 6 + (i * 7 % 10); ctx.fillRect(sx, sy, 2, 2); } } // Platforms for (const s of solids) { if (s.y < 150) { ctx.fillStyle = COLORS.dirt; ctx.fillRect(s.x, s.y, s.w, s.h); ctx.fillStyle = COLORS.grass; ctx.fillRect(s.x, s.y - 2, s.w, 2); } } // Pipes for (const p of pipes) { ctx.fillStyle = COLORS.pipe; ctx.fillRect(p.x, p.y, p.w, p.h); ctx.fillStyle = COLORS.pipeDark; ctx.fillRect(p.x + 2, p.y + 2, p.w - 4, p.h - 4); // lip ctx.fillStyle = COLORS.pipe; ctx.fillRect(p.x - 4, p.y - 4, p.w + 8, 4); } // Coins for (const c of coinsArr) { if (c.collected) continue; const bob = Math.sin(c.t) * 2; ctx.fillStyle = COLORS.coin2; ctx.fillRect(c.x, c.y + bob, c.w, c.h); ctx.fillStyle = COLORS.coin1; ctx.fillRect(c.x + 1, c.y + 1 + bob, c.w - 2, c.h - 2); // highlight ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillRect(c.x + 1, c.y + 2 + bob, 1, c.h - 4); } // Enemies for (const e of enemies) { if (e.dead) { // Squash effect ctx.fillStyle = COLORS.enemyDark; ctx.fillRect(e.x, e.y + e.h - 4, e.w, 4); continue; } ctx.fillStyle = COLORS.enemy; ctx.fillRect(e.x, e.y, e.w, e.h); ctx.fillStyle = COLORS.enemyDark; ctx.fillRect(e.x + 2, e.y + e.h - 3, e.w - 4, 3); // Eyes ctx.fillStyle = "#111"; ctx.fillRect(e.x + 3, e.y + 3, 2, 2); ctx.fillRect(e.x + e.w - 5, e.y + 3, 2, 2); } // Fireballs for (const f of fireballs) { ctx.fillStyle = COLORS.fire1; ctx.fillRect(f.x - 2, f.y - 2, 4, 4); ctx.fillStyle = COLORS.fire2; ctx.fillRect(f.x - 1, f.y - 1, 2, 2); } // Player drawPlayer(); // Goal flag near end const flagX = WORLD.width - 160, flagY = 110; ctx.fillStyle = "#444"; ctx.fillRect(flagX, flagY - 40, 3, 60); ctx.fillStyle = "#e63946"; ctx.fillRect(flagX + 3, flagY - 38, 18, 12); ctx.fillStyle = "#fff"; ctx.fillRect(flagX + 9, flagY - 35, 6, 3); ctx.restore(); // HUD update coinsEl.textContent = coins.toString(); } function drawPlayer() { const p = player; // Shadow ctx.fillStyle = "rgba(0,0,0,0.25)"; ctx.fillRect(p.x - 2, p.y + p.h - 2, p.w + 4, 3); // Body ctx.fillStyle = COLORS.playerBody; ctx.fillRect(p.x, p.y + 4, p.w, p.h - 4); // Hat ctx.fillStyle = COLORS.playerHat; ctx.fillRect(p.x, p.y, p.w, 4); // Face ctx.fillStyle = COLORS.playerFace; ctx.fillRect(p.x + 3, p.y + 5, 8, 6); // Eye ctx.fillStyle = "#111"; if (p.dir >= 0) ctx.fillRect(p.x + 8, p.y + 6, 2, 2); else ctx.fillRect(p.x + 4, p.y + 6, 2, 2); // Shoes ctx.fillStyle = "#3d3d3d"; ctx.fillRect(p.x + 2, p.y + p.h - 2, 4, 2); ctx.fillRect(p.x + p.w - 6, p.y + p.h - 2, 4, 2); } // Main loop let last = performance.now(); function loop(now) { const dtMs = now - last; last = now; const dt = clamp(dtMs / (1000 / 60), 0.2, 3); // normalize to 60fps units time += dtMs; if (state === "playing") { updatePlayer(dt); updateFireballs(dt); updateEnemies(dt); updateCoins(dt); checkWin(); updateCamera(dt); } // Redraw bg and fg drawBackground(); draw(); requestAnimationFrame(loop); } requestAnimationFrame(loop); // Initialize resetGame(); // Prevent context menu on touch window.addEventListener("contextmenu", e => e.preventDefault(), { passive: false }); })();