|
|
(() => { |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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)" |
|
|
}; |
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
const keys = { |
|
|
left: false, right: false, up: false, fire: false, |
|
|
pause: false, mute: false |
|
|
}; |
|
|
|
|
|
|
|
|
const btnLeft = document.getElementById("btnLeft"); |
|
|
const btnRight = document.getElementById("btnRight"); |
|
|
const btnJump = document.getElementById("btnJump"); |
|
|
const btnFire = document.getElementById("btnFire"); |
|
|
|
|
|
|
|
|
const camera = { x: 0, y: 0, w: 320, h: 180 }; |
|
|
|
|
|
|
|
|
let state = "playing"; |
|
|
let score = 0; |
|
|
let coins = 0; |
|
|
let lives = 3; |
|
|
let time = 0; |
|
|
let muted = false; |
|
|
|
|
|
|
|
|
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 |
|
|
}; |
|
|
|
|
|
|
|
|
const solids = [ |
|
|
|
|
|
{ x: 0, y: 150, w: WORLD.width, h: 60 }, |
|
|
|
|
|
{ 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 }, |
|
|
]; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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 |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const fireballs = []; |
|
|
|
|
|
|
|
|
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 }); |
|
|
}); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
let onGround = false; |
|
|
if (dy !== 0) { |
|
|
entity.y += dy; |
|
|
for (const s of solids) { |
|
|
if (rectsOverlap(entity, s)) { |
|
|
if (dy > 0) { |
|
|
entity.y = s.y - entity.h; |
|
|
entity.vy = 0; |
|
|
onGround = true; |
|
|
} else { |
|
|
entity.y = s.y + s.h; |
|
|
entity.vy = 0; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
return onGround; |
|
|
} |
|
|
|
|
|
|
|
|
function updatePlayer(dt) { |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
player.vy += GRAVITY * dt; |
|
|
player.vy = clamp(player.vy, -99, MAX_FALL); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
if (keys.fire && performance.now() > player.canFireAt) { |
|
|
shootFireball(); |
|
|
player.canFireAt = performance.now() + PLAYER.fireCooldown; |
|
|
} |
|
|
|
|
|
|
|
|
player.x = clamp(player.x, 0, WORLD.width - player.w); |
|
|
if (player.y > WORLD.height + 100) { |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
for (const s of solids) { |
|
|
if (rectsOverlap(f, s)) { remove = true; break; } |
|
|
} |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
const ahead = { x: e.x + (e.vx > 0 ? e.w + 1 : -1), y: e.y + e.h + 1, w: 1, h: 1 }; |
|
|
|
|
|
let onGround = false; |
|
|
for (const s of solids) { |
|
|
if (rectsOverlap(ahead, s)) { onGround = true; break; } |
|
|
} |
|
|
if (!onGround) e.vx = -e.vx; |
|
|
|
|
|
|
|
|
e.vy += GRAVITY * dt; |
|
|
e.vy = clamp(e.vy, -99, MAX_FALL); |
|
|
|
|
|
|
|
|
collideAxis(e, e.vx * dt, 0); |
|
|
const landed = collideAxis(e, 0, e.vy * dt); |
|
|
if (landed) e.onGround = true; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (rectsOverlap(player, e) && !e.dead) { |
|
|
|
|
|
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; |
|
|
addScore(100); |
|
|
beep(340, 0.07, "triangle", 0.05); |
|
|
} else { |
|
|
|
|
|
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 { |
|
|
|
|
|
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() { |
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function drawBackground() { |
|
|
|
|
|
const w = bgCanvas.width, h = bgCanvas.height; |
|
|
bgCtx.clearRect(0, 0, w, h); |
|
|
|
|
|
|
|
|
const camX = camera.x * 0.3; |
|
|
const hillX = camera.x * 0.5; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function draw() { |
|
|
|
|
|
if (CAMERA.shake > 0) CAMERA.shake *= 0.9; |
|
|
|
|
|
const shakeX = (Math.random() - 0.5) * CAMERA.shake; |
|
|
const shakeY = (Math.random() - 0.5) * CAMERA.shake; |
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, gameCanvas.width, gameCanvas.height); |
|
|
|
|
|
ctx.save(); |
|
|
ctx.translate(-Math.floor(camera.x) + shakeX, -Math.floor(camera.y) + shakeY); |
|
|
|
|
|
|
|
|
for (let x = 0; x < WORLD.width; x += TILE) { |
|
|
const baseY = 150; |
|
|
ctx.fillStyle = COLORS.dirt; |
|
|
ctx.fillRect(x, baseY, TILE, WORLD.height - baseY); |
|
|
|
|
|
ctx.fillStyle = COLORS.grass; |
|
|
ctx.fillRect(x, baseY - 2, TILE, 2); |
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
ctx.fillStyle = COLORS.pipe; |
|
|
ctx.fillRect(p.x - 4, p.y - 4, p.w + 8, 4); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
ctx.fillStyle = "rgba(255,255,255,0.5)"; |
|
|
ctx.fillRect(c.x + 1, c.y + 2 + bob, 1, c.h - 4); |
|
|
} |
|
|
|
|
|
|
|
|
for (const e of enemies) { |
|
|
if (e.dead) { |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
drawPlayer(); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
coinsEl.textContent = coins.toString(); |
|
|
} |
|
|
|
|
|
function drawPlayer() { |
|
|
const p = player; |
|
|
|
|
|
ctx.fillStyle = "rgba(0,0,0,0.25)"; |
|
|
ctx.fillRect(p.x - 2, p.y + p.h - 2, p.w + 4, 3); |
|
|
|
|
|
|
|
|
ctx.fillStyle = COLORS.playerBody; |
|
|
ctx.fillRect(p.x, p.y + 4, p.w, p.h - 4); |
|
|
|
|
|
ctx.fillStyle = COLORS.playerHat; |
|
|
ctx.fillRect(p.x, p.y, p.w, 4); |
|
|
|
|
|
ctx.fillStyle = COLORS.playerFace; |
|
|
ctx.fillRect(p.x + 3, p.y + 5, 8, 6); |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
let last = performance.now(); |
|
|
function loop(now) { |
|
|
const dtMs = now - last; |
|
|
last = now; |
|
|
const dt = clamp(dtMs / (1000 / 60), 0.2, 3); |
|
|
|
|
|
time += dtMs; |
|
|
if (state === "playing") { |
|
|
updatePlayer(dt); |
|
|
updateFireballs(dt); |
|
|
updateEnemies(dt); |
|
|
updateCoins(dt); |
|
|
checkWin(); |
|
|
updateCamera(dt); |
|
|
} |
|
|
|
|
|
|
|
|
drawBackground(); |
|
|
draw(); |
|
|
|
|
|
requestAnimationFrame(loop); |
|
|
} |
|
|
requestAnimationFrame(loop); |
|
|
|
|
|
|
|
|
resetGame(); |
|
|
|
|
|
|
|
|
window.addEventListener("contextmenu", e => e.preventDefault(), { passive: false }); |
|
|
|
|
|
})(); |