{
keys[e.key] = true;
if (["ArrowLeft","ArrowRight","ArrowUp"," ","z","x","f","w","a","d"].includes(e.key)){
e.preventDefault();
}
});
window.addEventListener("keyup", e=>{
keys[e.key] = false;
});
function bindControl(id, key){
const el = document.getElementById(id);
if (!el) return;
const down = (e) => {
e.preventDefault();
keys[key] = true;
};
const up = (e) => {
e.preventDefault();
keys[key] = false;
};
el.addEventListener("pointerdown", down, { passive:false });
el.addEventListener("pointerup", up, { passive:false });
el.addEventListener("pointercancel", up, { passive:false });
el.addEventListener("touchstart", down, { passive:false });
el.addEventListener("touchend", up, { passive:false });
el.addEventListener("touchcancel", up, { passive:false });
el.addEventListener("mousedown", down);
el.addEventListener("mouseup", up);
el.addEventListener("mouseleave", up);
}
bindControl("btn-left", "ArrowLeft");
bindControl("btn-right", "ArrowRight");
bindControl("btn-jump", "jump");
bindControl("btn-tramp", "tramp");
bindControl("btn-attack", "attack");
/* Global tap = jump (after game starts) */
document.addEventListener("touchstart", (e)=>{
if (!gameStarted) return;
keys["jump"] = true;
}, {passive:false});
document.addEventListener("touchend", ()=>{
keys["jump"] = false;
}, {passive:false});
/* ============================================================
TITLE + MESSAGE HANDLING
============================================================ */
const titleLogoEl = document.getElementById("title-logo");
const titleChoices = [
"picture1.png"+CB,
"pens1.png"+CB, "pens2.jpeg"+CB, "pens3.jpg"+CB, "pens4.jpeg"+CB, "pens5.jpeg"+CB,
"pens6.png"+CB, "pens7.jpeg"+CB, "pens8.jpeg"+CB, "pens9.png"+CB, "pens10.png"+CB,
"pens11.png"+CB,"pens12.png"+CB,"logo.jpeg"+CB
];
function setTitleImage(){
const pick = titleChoices[Math.floor(Math.random()*titleChoices.length)];
titleLogoEl.src = pick;
}
setTitleImage();
function showTempMessage(text, ms=1600){
msgEl.textContent = text;
msgEl.classList.remove("hidden");
setTimeout(()=>{
if (state === "playing") msgEl.classList.add("hidden");
}, ms);
}
function startGame(){
if (gameStarted) return;
// TEMP TEST — SHOW DRAG2.JPEG BIG ON SCREEN
setTimeout(()=>{
console.log("Testing drag2.jpeg =", imgPlayer);
// Draw a big visible test box
ctx.save();
ctx.fillStyle = "rgba(0,255,0,0.3)";
ctx.fillRect(100,60,120,120);
// Try to draw drag2.jpeg
if (imgPlayer && (imgPlayer.naturalWidth || imgPlayer.width)){
ctx.drawImage(imgPlayer, 100, 60, 120, 120);
console.log("drag2.jpeg DRAWN SUCCESSFULLY");
} else {
console.log("drag2.jpeg NOT LOADED");
ctx.fillStyle = "#ff0000";
ctx.fillRect(100,60,120,120);
}
ctx.restore();
},1200);
ensureAudio();
if (audioCtx && audioCtx.state === "suspended"){
audioCtx.resume();
}
playRoar();
gameStarted = true;
document.getElementById("title-screen").style.display = "none";
introFireTimer = 60;
showTempMessage(
"Collect 12 pens.\nGate opens.\nBeat both dragons.\nUse ⚡ near bosses for Mega Blast."
);
}
window.startGame = startGame;
document.getElementById("start-button").addEventListener("click", startGame);
document.addEventListener("click", ()=>{
if (!gameStarted){
startGame();
return;
}
if (state==="dead" || state==="won"){
location.reload();
}
});
/* ============================================================
SPRITES + HELPERS
============================================================ */
function img(src){
const i = new Image();
i.src = src + CB;
return i;
}
/* Draw an image maintaining square ratio */
function drawSquareImage(image, x, y, size){
if (!image) return;
const iw = image.naturalWidth || image.width;
const ih = image.naturalHeight || image.height;
if (!iw || !ih){
ctx.drawImage(image, x, y, size, size);
return;
}
const scale = Math.min(size/iw, size/ih);
const dw = iw * scale;
const dh = ih * scale;
const dx = x + (size - dw)/2;
const dy = y + (size - dh)/2;
ctx.drawImage(image, dx, dy, dw, dh);
}
/* LOAD ALL GAME IMAGES */
const imgPlayer = img("drag2.jpeg");
const imgDragon = img("dragon.jpeg");
const imgPendragon = img("pendragon.jpeg");
const imgPendShot = img("pendshot.jpeg");
const imgSymbol = img("symbol.png");
const imgWings = img("wings.jpeg");
const imgBG = img("pens2.jpeg");
const imgFire = img("fire.jpeg");
const imgPPAPStaff = img("ppap.jpeg");
const imgPPAPMan = img("ppapman3.jpeg");
/* Pens */
const penImgs = [
img("pens1.png"), img("pens2.jpeg"), img("pens3.jpg"), img("pens4.jpeg"),
img("pens5.jpeg"), img("pens6.png"), img("pens7.jpeg"), img("pens8.jpeg"),
img("pens9.png"), img("pens10.png"), img("pens11.png"), img("pens12.png")
];
/* Enemies */
const enemyImgs = [
img("enemy1.png"), img("enemy2.png"),
img("enemy3.png"), img("enemy4.png")
];
/* ============================================================
WORLD / OBJECTS / PLAYER SETUP
============================================================ */
let cameraX = 0;
const worldWidth = 5200;
/* PLATFORMS */
const platforms = [
{x:0, y:H-80, w:worldWidth, h:80},
{x:600, y:H-150, w:140, h:12},
{x:1100, y:H-180, w:160, h:12},
{x:1600, y:H-160, w:160, h:12},
{x:2100, y:H-190, w:160, h:12},
{x:2600, y:H-210, w:180, h:12},
{x:3100, y:H-170, w:200, h:12},
{x:4400, y:H-170, w:240, h:12}
];
/* PLAYER */
const player = {
x: 40,
y: H-160,
w: 40,
h: 40,
vx: 0,
vy: 0,
baseSpeed: 0.16,
baseMaxSpeed: 2.0,
baseJump: 6.5,
speed: 0.16,
maxSpeed: 2.0,
jumpPower:6.5,
onGround:false,
facing: 1,
scale: 1,
wingTimer: 0,
hasSymbol:false,
invuln: 0,
hp:5,
maxHp:5
};
/* Wings pickup */
const wingsPickup = {
x:1800, y:H-260, w:40, h:40,
taken:false
};
/* Pens */
let pens = [];
for (let i=0;i<12;i++){
pens.push({
x:450 + i*260,
y:H-240 - (i%3)*40,
w:32,
h:32,
collected:false,
img:penImgs[i]
});
}
const TOTAL_PENS = pens.length;
let pensCollected = 0;
let ppapTriggers = 0;
/* Symbol pickup */
const symbol = {
x:1150, y:H-250, w:40, h:40,
taken:false
};
/* Enemies */
let enemies = [];
for (let i=0;i<6;i++){
enemies.push({
x:900 + i*500,
y:H-120,
w:40,
h:40,
alive:true,
vx: i%2===0 ? 0.6 : -0.6,
img: enemyImgs[i % enemyImgs.length]
});
}
/* ============================================================
BOSSES, GATE, SUPER PEN, STATE
============================================================ */
/* Boss 1 – dragon.jpeg */
let boss1 = {
x: 3500,
y: H-280,
w: 160,
h: 120,
hp: 12,
maxHp: 12,
active: false,
defeated: false,
attackCooldown: 100
};
/* Boss 2 – pendragon.jpeg */
let boss2 = {
x: 4800,
y: H-320,
w: 200,
h: 160,
hp: 16,
maxHp: 16,
active: false,
defeated: false,
attackCooldown: 110
};
let boss1Shots = [];
let pendShots = [];
/* Fire gate opens after all pens collected */
const fireGate = {
x: 3200,
y: H-250,
w: 40,
h: 250,
unlocked: false,
opening: false,
timer: 60
};
/* SUPER PPAP pen object */
const SUPER_EXPLOSION_MAX = 260;
let superPen = {
active: false,
exploding: false,
x: 0,
y: 0,
w: 40,
h: 40,
vy: 0,
explosionTimer: 0
};
/* Buffs / overlays / finisher */
let penPowerName = "None";
let penPowerTimer = 0;
let penShieldActive = false;
let dragonOverlayTimer = 0;
const DRAGON_OVERLAY_MAX = 140;
let finisherActive = false;
let finisherTimer = 0;
let ppapKillTimer = 0;
/* ============================================================
HELPERS
============================================================ */
function rectOverlap(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 getActiveBoss(){
if (boss1.active && !boss1.defeated) return boss1;
if (boss2.active && !boss2.defeated) return boss2;
return null;
}
function updateHUD(){
const buff = penPowerName;
powerEl.textContent =
`Pens: ${pensCollected}/${TOTAL_PENS} | PPAPs: ${ppapTriggers} | Buff: ${buff}`;
const hpPct = Math.max(0, Math.min(1, player.hp / player.maxHp));
playerHealthFill.style.width = (hpPct * 100) + "%";
const boss = getActiveBoss();
let bossPct = 0;
if (boss) bossPct = Math.max(0, Math.min(1, boss.hp / boss.maxHp));
bossHealthFill.style.width = (bossPct * 100) + "%";
}
function damagePlayer(amount=1){
if (player.invuln > 0 || state !== "playing") return;
player.hp -= amount;
if (player.hp < 0) player.hp = 0;
player.invuln = 45;
damageFlashTimer = 18;
shakeTimer = 16;
shakeMag = 3.5;
playHitSound();
updateHUD();
if (player.hp <= 0){
state = "dead";
msgEl.textContent = "You were defeated.\nTap to restart.";
msgEl.classList.remove("hidden");
}
}
function givePenPower(idx){
penPowerName = "Pen " + idx;
penPowerTimer = 6 * 60; // 6 seconds at 60 fps
playBeep(900, 0.12);
if (idx % 4 === 0){
ppapTriggers++;
spawnSuperPen();
}
updateHUD();
}
/* ============================================================
UPDATE LOOP LOGIC
============================================================ */
function update(dt){
if (!gameStarted || state !== "playing") return;
const d = dt / 16.666; // normalize to ~60fps
/* Timers */
if (damageFlashTimer > 0) damageFlashTimer -= d;
if (introFireTimer > 0) introFireTimer -= d;
if (player.invuln > 0) player.invuln -= d;
if (shakeTimer > 0) shakeTimer -= d;
if (ppapKillTimer > 0){
ppapKillTimer -= d * 60;
if (ppapKillTimer < 0) ppapKillTimer = 0;
}
if (penPowerTimer > 0){
penPowerTimer -= d;
if (penPowerTimer <= 0){
penPowerTimer = 0;
penPowerName = "None";
penShieldActive = false;
}
}
if (dragonOverlayTimer > 0){
dragonOverlayTimer -= d;
if (dragonOverlayTimer < 0) dragonOverlayTimer = 0;
}
if (finisherActive){
finisherTimer -= d;
if (finisherTimer <= 0){
finisherActive = false;
const b = getActiveBoss();
if (!b && boss1.defeated && boss2.defeated){
state = "won";
msgEl.textContent = "Both dragons fall.\nThe Vault is yours.\nTap to play again.";
msgEl.classList.remove("hidden");
}
}
}
/* Reset player to base stats */
player.speed = player.baseSpeed;
player.maxSpeed = player.baseMaxSpeed;
player.jumpPower = player.baseJump;
/* Wings buff */
if (player.wingTimer > 0){
player.wingTimer -= d;
if (player.wingTimer < 0) player.wingTimer = 0;
player.maxSpeed *= 1.8;
player.jumpPower *= 1.6;
}
/* Horizontal movement */
let move = 0;
if (keys["ArrowLeft"] || keys["a"]) move -= 1;
if (keys["ArrowRight"] || keys["d"]) move += 1;
if (move !== 0){
player.facing = move;
player.vx += move * player.speed * 4 * d;
if (player.vx > player.maxSpeed) player.vx = player.maxSpeed;
if (player.vx < -player.maxSpeed) player.vx = -player.maxSpeed;
} else {
player.vx *= 0.84;
}
/* Jump + tramp jump */
const jumpPressed =
keys["jump"] || keys["ArrowUp"] || keys["w"] || keys[" "];
if (jumpPressed && player.onGround){
player.vy = -player.jumpPower;
player.onGround = false;
playBeep(1100, 0.12);
}
const trampPressed = keys["tramp"];
if (trampPressed && !keys._tramp && player.onGround){
keys._tramp = true;
player.vy = -player.jumpPower * 1.9;
player.onGround = false;
playBeep(1400, 0.12);
}
if (!trampPressed) keys._tramp = false;
/* Gravity */
player.vy += 0.45 * d * 16.666;
/* Apply velocity */
player.x += player.vx * d * 16.666 * 0.08;
player.y += player.vy * d * 16.666 * 0.16;
/* Clamp to world */
const pw = player.w * player.scale;
const ph = player.h * player.scale;
if (player.x < 0) player.x = 0;
if (player.x > worldWidth - pw){
player.x = worldWidth - pw;
}
/* Platform collisions */
player.onGround = false;
let pr = { x: player.x, y: player.y, w: pw, h: ph };
for (const p of platforms){
const rP = { x:p.x, y:p.y, w:p.w, h:p.h };
if (rectOverlap(pr, rP)){
if (player.vy > 0 && pr.y + pr.h - 10 < rP.y + rP.h){
player.y = p.y - ph;
player.vy = 0;
player.onGround = true;
pr.y = player.y;
}
}
}
if (!player.onGround){
player.vx *= 0.97;
}
/* Camera follow */
cameraX = player.x + pw/2 - W/2;
if (cameraX < 0) cameraX = 0;
if (cameraX > worldWidth - W) cameraX = worldWidth - W;
/* Wings pickup */
if (!wingsPickup.taken && rectOverlap(pr, wingsPickup)){
wingsPickup.taken = true;
player.wingTimer = 4 * 60;
playBeep(1300, 0.16);
showTempMessage("Wings!\n4s speed + superjump.");
}
/* Symbol pickup (dragon power) */
if (!symbol.taken && rectOverlap(pr, symbol)){
symbol.taken = true;
player.hasSymbol = true;
player.scale = 2;
dragonOverlayTimer = DRAGON_OVERLAY_MAX;
playRoar();
showTempMessage("Dragon symbol!\nYou grow with power.");
}
/* Pens collection */
for (const pen of pens){
if (pen.collected) continue;
if (rectOverlap(pr, pen)){
pen.collected = true;
pensCollected++;
givePenPower(pensCollected);
}
}
/* Fire gate unlocking */
if (!fireGate.unlocked && pensCollected >= TOTAL_PENS){
fireGate.unlocked = true;
fireGate.opening = true;
fireGate.timer = 60;
playBlastSound();
showTempMessage("The fire gate is opening...");
}
if (fireGate.opening){
fireGate.timer -= d * 60;
if (fireGate.timer <= 0){
fireGate.opening = false;
}
}
/* Block player if gate still closed */
if (!fireGate.unlocked){
const gr = { x:fireGate.x, y:fireGate.y, w:fireGate.w, h:fireGate.h };
if (rectOverlap(pr, gr)){
const center = fireGate.x + fireGate.w/2;
const pc = player.x + pw/2;
if (pc < center){
player.x = fireGate.x - pw - 2;
} else {
player.x = fireGate.x + fireGate.w + 2;
}
pr.x = player.x;
}
}
/* Enemies */
for (const e of enemies){
if (!e.alive) continue;
if (ppapKillTimer > 0 &&
e.x + e.w > cameraX - 40 &&
e.x < cameraX + W + 40){
e.alive = false;
continue;
}
e.x += e.vx * d * 16.666 * 0.09;
if (e.x < 200 || e.x > worldWidth - 200){
e.vx *= -1;
}
const er = { x:e.x, y:e.y, w:e.w, h:e.h };
if (rectOverlap(pr, er)){
if (penShieldActive){
e.alive = false;
playBlastSound();
} else {
damagePlayer(1);
}
}
}
/* Activate bosses as you move right */
if (!boss1.defeated && player.x > 3000){
boss1.active = true;
}
if (!boss2.defeated && player.x > 4300){
boss2.active = true;
}
/* Boss AI + Super Pen */
updateBoss1(d, pr);
updateBoss2(d, pr);
updateSuperPen(d);
/* Attack / finisher (⚡ button) */
const atk = keys["attack"] || keys["z"] || keys["x"] || keys["f"];
if (atk && !keys._attack){
keys._attack = true;
if (pensCollected >= TOTAL_PENS){
triggerFinisher(pr);
} else {
showTempMessage("Collect all 12 pens\nthen use ⚡ near a dragon.");
}
}
if (!atk) keys._attack = false;
updateHUD();
}
/* ============================================================
BOSS 1 (dragon.jpeg) AI
============================================================ */
function updateBoss1(d, pr){
if (!boss1.active || boss1.defeated) return;
// subtle float
boss1.x += Math.sin(performance.now()/600) * 0.4 * d * 16.666;
const b = boss1;
const br = { x: b.x, y: b.y, w: b.w, h: b.h };
// contact damage or shield damage
if (rectOverlap(pr, br)){
if (penShieldActive){
b.hp = Math.max(0, b.hp - 1);
playBlastSound();
shakeTimer = 20;
shakeMag = 5;
if (b.hp === 0){
b.defeated = true;
b.active = false;
showTempMessage("First dragon defeated!\nPendragon awaits.");
}
} else {
damagePlayer(2);
}
}
// fireballs
b.attackCooldown -= d * 60;
if (b.attackCooldown <= 0){
b.attackCooldown = 80 + Math.random()*40;
const dir = (pr.x + pr.w/2) < (b.x + b.w/2) ? -1 : 1;
const sx = b.x + (dir < 0 ? 10 : b.w - 26);
const sy = b.y + b.h * 0.4;
boss1Shots.push({
x: sx,
y: sy,
w: 32,
h: 24,
vx: dir * 3.2,
active: true
});
playBeep(300, 0.12);
}
const newShots = [];
for (const s of boss1Shots){
if (!s.active) continue;
s.x += s.vx * d * 16.666 * 0.18;
if (s.x + s.w < cameraX - 200 || s.x > cameraX + W + 200){
continue; // offscreen
}
const sr = { x:s.x, y:s.y, w:s.w, h:s.h };
if (rectOverlap(pr, sr)){
s.active = false;
damagePlayer(1);
continue;
}
newShots.push(s);
}
boss1Shots = newShots;
}
/* ============================================================
BOSS 2 (pendragon.jpeg) AI
============================================================ */
function updateBoss2(d, pr){
if (!boss2.active || boss2.defeated) return;
boss2.x += Math.cos(performance.now()/700) * 0.3 * d * 16.666;
const b = boss2;
const br = { x: b.x, y: b.y, w: b.w, h: b.h };
// contact
if (rectOverlap(pr, br)){
if (penShieldActive){
b.hp = Math.max(0, b.hp - 1);
playBlastSound();
shakeTimer = 22;
shakeMag = 6;
if (b.hp === 0){
b.defeated = true;
b.active = false;
state = "won";
msgEl.textContent = "Pendragon falls.\nThe Vault is yours.\nTap to play again.";
msgEl.classList.remove("hidden");
}
} else {
damagePlayer(2);
}
}
// homing-ish shots
b.attackCooldown -= d * 60;
const MAX_PEND_SHOTS = 8;
if (b.attackCooldown <= 0 && pendShots.length < MAX_PEND_SHOTS){
b.attackCooldown = 110 + Math.random()*70;
const cx = b.x + b.w/2;
const cy = b.y + b.h/2;
const px = pr.x + pr.w/2;
const py = pr.y + pr.h/2;
const dx = px - cx;
const dy = py - cy;
const len = Math.max(0.01, Math.hypot(dx,dy));
const speed = 2.2;
pendShots.push({
x: cx - 18,
y: cy - 18,
w: 36,
h: 36,
vx: dx/len * speed,
vy: dy/len * speed,
active: true
});
playBeep(500, 0.1);
}
const newShots = [];
for (const s of pendShots){
if (!s.active) continue;
s.x += s.vx * d * 16.666 * 0.16;
s.y += s.vy * d * 16.666 * 0.16;
if (s.x + s.w < cameraX - 200 || s.x > cameraX + W + 200 ||
s.y < -200 || s.y > H + 200){
continue;
}
const sr = { x:s.x, y:s.y, w:s.w, h:s.h };
if (rectOverlap(pr, sr)){
s.active = false;
damagePlayer(1);
continue;
}
newShots.push(s);
}
pendShots = newShots;
}
/* ============================================================
SUPER PPAP PEN – spawn, explode, update
============================================================ */
function spawnSuperPen(){
if (superPen.active) return;
superPen.active = true;
superPen.exploding = false;
superPen.vy = 0;
superPen.x = player.x + (player.w * player.scale)/2 - superPen.w/2;
superPen.y = -60;
superPen.explosionTimer = SUPER_EXPLOSION_MAX;
penPowerName = "PPAP";
penPowerTimer = 7 * 60;
penShieldActive = true;
showTempMessage("PPAP power!\nStaffs are falling...");
playBeep(1100, 0.18);
}
function triggerSuperPenExplosion(hitBoss=false){
if (!superPen.active) return;
superPen.exploding = true;
superPen.explosionTimer = SUPER_EXPLOSION_MAX;
const cx = superPen.x + superPen.w/2;
const cy = superPen.y + superPen.h/2;
const radiusSq = 260 * 260;
// kill enemies inside radius
for (const e of enemies){
if (!e.alive) continue;
const ex = e.x + e.w/2;
const ey = e.y + e.h/2;
const dx = ex - cx;
const dy = ey - cy;
if (dx*dx + dy*dy <= radiusSq){
e.alive = false;
}
}
// damage active boss
const b = getActiveBoss();
if (b && !b.defeated){
const dmg = hitBoss ? 5 : 3;
b.hp = Math.max(0, b.hp - dmg);
if (b.hp === 0){
b.defeated = true;
b.active = false;
}
}
// small kill window for on-screen enemies
ppapKillTimer = 180;
shakeTimer = 24;
shakeMag = 4;
playBlastSound();
playRoar();
updateHUD();
}
function updateSuperPen(d){
if (!superPen.active) return;
if (!superPen.exploding){
// falling from sky
superPen.vy += 0.04 * d * 16.666;
superPen.y += superPen.vy * 2.5 * d;
const spr = {
x: superPen.x,
y: superPen.y,
w: superPen.w,
h: superPen.h
};
// ground
if (superPen.y + superPen.h >= H - 80){
superPen.y = H - 80 - superPen.h;
triggerSuperPenExplosion(false);
return;
}
// platforms
for (const p of platforms){
const rP = {x:p.x, y:p.y, w:p.w, h:p.h};
if (rectOverlap(spr, rP)){
triggerSuperPenExplosion(false);
return;
}
}
// boss hit
const b = getActiveBoss();
if (b && !b.defeated){
const br = {x:b.x, y:b.y, w:b.w, h:b.h};
if (rectOverlap(spr, br)){
triggerSuperPenExplosion(true);
return;
}
}
} else {
// explosion timer
superPen.explosionTimer -= d * 60;
if (superPen.explosionTimer <= 0){
superPen.active = false;
superPen.exploding = false;
}
}
}
/* ============================================================
FINISHER – ⚡ blast near boss
============================================================ */
function triggerFinisher(pr){
const b = getActiveBoss();
if (!b || b.defeated) return;
const bc = b.x + b.w/2;
const pc = pr.x + pr.w/2;
if (Math.abs(bc - pc) > 260){
showTempMessage("Get closer to the dragon\nthen use ⚡ again.");
return;
}
finisherActive = true;
finisherTimer = 120;
b.hp = 0;
b.defeated = true;
b.active = false;
boss1Shots = [];
pendShots = [];
shakeTimer = 40;
shakeMag = 8;
playBlastSound();
playRoar();
updateHUD();
if (boss1.defeated && boss2.defeated){
state = "won";
msgEl.textContent = "Both dragons fall.\nThe Vault is yours.\nTap to play again.";
msgEl.classList.remove("hidden");
}
}
/* ============================================================
DRAW
============================================================ */
function draw(){
let offX = 0, offY = 0;
if (shakeTimer > 0){
const t = performance.now() / 40;
offX = Math.sin(t*3) * shakeMag;
offY = Math.cos(t*4) * shakeMag;
}
ctx.save();
ctx.clearRect(0,0,W,H);
ctx.translate(Math.round(offX), Math.round(offY));
/* Background gradient */
const bgGrad = ctx.createLinearGradient(0,0,0,H);
bgGrad.addColorStop(0, "#02040a");
bgGrad.addColorStop(0.4, "#04101f");
bgGrad.addColorStop(1, "#050406");
ctx.fillStyle = bgGrad;
ctx.fillRect(0,0,W,H);
/* DEBUG: tiny drag2.jpeg preview top-left so you KNOW it loaded */
if (imgPlayer && (imgPlayer.naturalWidth || imgPlayer.width)) {
drawSquareImage(imgPlayer, 8, 8, 40);
}
/* pens2.jpeg along bottom */
if (imgBG && (imgBG.naturalWidth || imgBG.width)){
const tileW = 140;
const tileH = 140;
const startX = - (cameraX % tileW);
ctx.globalAlpha = 0.22;
for (let x = startX; x < W; x += tileW){
ctx.drawImage(imgBG, x, H - tileH - 32, tileW, tileH);
}
ctx.globalAlpha = 1;
}
/* Platforms + Fire Gate */
ctx.save();
ctx.translate(-cameraX, 0);
for (const p of platforms){
const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y+p.h);
grad.addColorStop(0, "#30363f");
grad.addColorStop(1, "#17181b");
ctx.fillStyle = grad;
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.strokeStyle = "rgba(0,255,120,0.25)";
ctx.lineWidth = 2;
ctx.strokeRect(p.x, p.y, p.w, p.h);
}
if (!fireGate.unlocked || fireGate.opening){
const g = fireGate;
const openProgress = fireGate.unlocked
? Math.max(0, 1 - g.timer/60)
: 0;
const visibleHeight = g.h * (1 - openProgress);
const gx = g.x;
const gy = g.y + (g.h - visibleHeight);
for (let i=0; i 0){
const t = dragonOverlayTimer / DRAGON_OVERLAY_MAX;
ctx.save();
ctx.globalAlpha = t * 0.8;
const grad = ctx.createRadialGradient(
W/2, H*0.4, 10,
W/2, H*0.4, Math.max(W,H)*0.7
);
grad.addColorStop(0,"rgba(0,255,120,0.8)");
grad.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle = grad;
ctx.fillRect(0,0,W,H);
ctx.restore();
}
if (finisherActive){
const t = finisherTimer / 120;
const alpha = Math.max(0, Math.min(0.7, t));
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = "rgba(255,255,255,0.9)";
ctx.fillRect(0,0,W,H);
ctx.restore();
}
if (damageFlashTimer > 0){
const a = Math.min(0.5, damageFlashTimer / 18);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = "rgba(255,0,0,0.7)";
ctx.fillRect(0,0,W,H);
ctx.restore();
}
if (introFireTimer > 0){
const t = introFireTimer / 60;
ctx.save();
ctx.globalAlpha = t * 0.9;
const grad = ctx.createLinearGradient(0,H,0,H*0.3);
grad.addColorStop(0,"rgba(255,100,0,1)");
grad.addColorStop(0.4,"rgba(255,220,0,0.9)");
grad.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle = grad;
ctx.fillRect(0,0,W,H);
ctx.restore();
}
ctx.restore(); // end shake translate
}
/* ============================================================
MAIN LOOP
============================================================ */
function loop(timestamp){
if (!lastTime) lastTime = timestamp;
const dt = timestamp - lastTime;
lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(loop);
}
/* Start HUD + loop */
updateHUD();
requestAnimationFrame(loop);