Dragon
{ 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);