top of page

 Erasmus+ Project

estonia-26130_1280.png
poland-26125_1280.png
portugal-162394.png
austria-flag-wave-xs.png
logo 3d adapt.png
full cape.png

This is the official website of the Erasmus+ Project:

ADAPT: Advancing Digital Abilities and Personal Thriving

The project connects four schools:

de La Tour Schule Deutschlandsberg (Austria)

Agrupamento de Escolas de Anadia (Portugal)

Aste Põhikool (Estonia)

II Liceum Ogolnoksztalcace z Oddzialami
Dwujezycznymi im. Andrzeja Frycza Modrzewskiego

w Rybniku (Poland)

Here you can find all the updates from our project,

such as photos, videos and our stories.

Below you can watch the video

announcing the project in 5 languages

and listen to the song made especially for this project.

This is a poster we have created during our workshop in Portugal. It features the participating students, transformed into their favorite superheroes using the AI tool ClipDrop which were then assembled in Canva.

ADAPT Superheroes High Resolution.webp

Here is a podcast that was recorded on September 15, 2024, a few weeks before our meeting in Poland.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Jogging Woman Platform Quest</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> <style> :root { --ui-font: 'Trebuchet MS', 'Segoe UI', Roboto, sans-serif; --purple:#7b2ff7; --pink:#f107a3; --yellow:#ffe34d; --cyan:#26ffe6; --green:#3ddc97; --red:#ff355e; --blue:#2d8bff; --ink:#1d1d27; } html,body { margin:0; padding:0; background:linear-gradient(135deg,#110022,#330a4d,#0d1b4d); min-height:100vh; color:#fff; font-family:var(--ui-font); overflow:hidden; } canvas { image-rendering:pixelated; display:block; margin:0 auto; background:#222; box-shadow:0 0 0 4px #fff2 inset, 0 0 50px -10px #f107a3,0 0 120px -40px #26ffe6; border-radius:18px; } h1 { font-weight:700; letter-spacing:1px; margin:0 0 .4rem; font-size:2.4rem; text-shadow:0 4px 14px rgba(0,0,0,.6),0 0 12px #f107a3; } button { cursor:pointer; border:none; background:linear-gradient(90deg,var(--purple),var(--pink)); color:#fff; font-weight:600; font-size:1.05rem; padding:.85rem 1.8rem; border-radius:40px; box-shadow:0 6px 16px -4px rgba(0,0,0,.6),0 0 0 3px #ffffff10; backdrop-filter:blur(6px); transition:.3s; } button:hover { transform:translateY(-3px) scale(1.04); box-shadow:0 8px 22px -6px #f107a380,0 0 0 3px #ffffff40; } button:active { transform:translateY(1px) scale(.97); } a { color:var(--yellow); } .overlay { position:fixed; inset:0; display:flex; flex-direction:column; justify-content:center; align-items:center; background:radial-gradient(circle at 40% 30%, #442266bb,#110022ee 70%); z-index:10; text-align:center; padding:2rem; } .hidden { display:none !important; } .panel { max-width:880px; backdrop-filter:blur(10px); background:linear-gradient(160deg,#ffffff12,#ffffff05); border:1px solid #ffffff30; padding:2.2rem 2.6rem 2.6rem; border-radius:34px; box-shadow:0 14px 50px -10px #0008, 0 0 0 1px #ffffff20 inset; } .panel p { line-height:1.45; font-size:1.02rem; } .flex { display:flex; gap:1.1rem; flex-wrap:wrap; justify-content:center; } .pill { background:#ffffff10; padding:.35rem .9rem; border-radius:30px; font-size:.85rem; letter-spacing:.5px; box-shadow:0 0 0 1px #ffffff20 inset; } #hud { position:fixed; top:12px; left:50%; transform:translateX(-50%); display:flex; gap:2.5rem; font-weight:600; font-size:1.05rem; letter-spacing:.5px; z-index:5; text-shadow:0 0 6px #000,0 0 14px #000; } #hud .hideLives { display:none; } #hud span.label { opacity:.7; font-weight:500; margin-right:.4rem; } #hud .value { color:var(--yellow); } /* Boss Heart Bar */ #bossBarWrap { position:fixed; top:56px; left:50%; transform:translateX(-50%); display:none; z-index:8; } #bossBarContainer { display:flex; align-items:center; gap:14px; padding:10px 18px 14px; background:linear-gradient(135deg,#22000cdd,#3d0015cc); border:1px solid #ff5a5a70; border-radius:40px; box-shadow:0 10px 28px -10px #000a,0 0 0 2px #ffffff10 inset; backdrop-filter:blur(8px); } .bossHeart { position:relative; width:54px; height:54px; } .bossHeart:before, .bossHeart:after { content:''; position:absolute; top:12px; left:18px; width:18px; height:28px; background:#ff224d; border-radius:12px 12px 8px 8px; box-shadow:0 0 0 2px #990021 inset,0 0 10px #ff224d88; } .bossHeart:after { left:0; } .bossHeart:before { left:18px; } .bossHeart .shine { position:absolute; top:16px; left:6px; width:12px; height:12px; border-radius:50%; background:radial-gradient(circle at 30% 30%,#fff8,#fff0 80%); } #bossHpText { position:absolute; bottom:-12px; left:50%; transform:translateX(-50%); font-size:.65rem; letter-spacing:1px; font-weight:700; color:#fff; text-shadow:0 0 6px #000; font-family:var(--ui-font); } #bossBarOuter { position:relative; width:560px; height:30px; background:#1a0c12; border:2px solid #5d1a2b; border-radius:16px; overflow:hidden; box-shadow:0 0 0 2px #ffffff10 inset, 0 6px 16px -8px #000; } #bossBar { position:absolute; inset:0; width:100%; height:100%; background:linear-gradient(90deg,#28d657,#1ea94e); transition:width .3s, background .4s; box-shadow:0 0 18px -6px #ff5f6daa inset, 0 0 14px -4px #ffe066aa; } #bossBarGlow { pointer-events:none; position:absolute; inset:0; mix-blend-mode:screen; background:radial-gradient(circle at 25% 50%,#ffffff66,transparent 70%); } .toast { position:fixed; bottom:18px; left:50%; transform:translateX(-50%) translateY(50px); padding:.75rem 1.4rem; background:linear-gradient(90deg,#ff5f6d,#ffc371); color:#111; font-weight:600; border-radius:40px; box-shadow:0 8px 30px -10px #000a; opacity:0; transition:.5s; pointer-events:none; font-size:.9rem; letter-spacing:.5px; } .toast.show { opacity:1; transform:translateX(-50%) translateY(0); } .gameover h2 { font-size:2.2rem; margin:0 0 .6rem; text-shadow:0 0 18px #ff355e; } .victory h2 { text-shadow:0 0 18px #3ddc97; } .statline { font-size:1.05rem; margin:.3rem 0 .9rem; } .key { font-family:monospace; background:#00000040; padding:.15rem .45rem; border-radius:7px; margin:0 .15rem; box-shadow:0 0 0 1px #ffffff30 inset; } .credits { margin-top:1.8rem; opacity:.6; font-size:.75rem; } @media (max-width:900px){ h1 { font-size:1.9rem; } .panel { padding:1.7rem 1.4rem 2rem; } #hud{ gap:1.1rem; font-size:.9rem;} } @keyframes pulseBar { 0%{filter:brightness(1);} 50%{filter:brightness(1.7) saturate(1.3);} 100%{filter:brightness(1);} } </style> </head> <body> <div id="hud"> <div><span class="label">Score</span><span id="score" class="value">0</span></div> <div class="hideLives"><span class="label">Lives</span><span id="lives" class="value">3</span></div> <div><span class="label">World</span><span id="world" class="value">1-1</span></div> <div><span class="label">Power</span><span id="power" class="value">Normal</span></div> <div><span class="label">Veggies</span><span id="veggies" class="value">0/3</span></div> <div><span class="label">Junk</span><span id="junk" class="value">0/2</span></div> </div> <div id="bossBarWrap"><div id="bossBarContainer"><div class="bossHeart"><div class="shine"></div><div id="bossHpText"></div></div><div id="bossBarOuter"><div id="bossBar"></div><div id="bossBarGlow"></div></div></div></div> <div id="toast" class="toast">Power Up!</div> <div id="startScreen" class="overlay"> <div class="panel"> <h1>A Jogging Woman<br><span style="font-size:1.05rem;font-weight:500;letter-spacing:3px; display:inline-block; margin-top:.4rem; background:linear-gradient(90deg,var(--yellow),var(--pink)); -webkit-background-clip:text; background-clip:text; color:transparent;">Quest for the Golden Medal</span></h1> <p>Run, jump & stomp junk food enemies. Collect <strong>healthy power-ups</strong> to grow strong and blaze through each world. Reach the <strong>McDonald's</strong> at the end and defeat the <strong>Big Burger Boss</strong> to earn the medal!</p> <div class="flex" style="margin:1.2rem 0 1.1rem;"> <span class="pill">Move: <span class="key">←</span><span class="key">→</span> or <span class="key">A</span><span class="key">D</span></span> <span class="pill">Jump: <span class="key">Space</span>/<span class="key">W</span>/<span class="key">↑</span></span> <span class="pill">Fireball: <span class="key">F</span></span> <span class="pill">Run: <span class="key">Shift</span></span> <span class="pill">Pause: <span class="key">P</span></span> </div> <p style="margin:.2rem 0 1.1rem;">Power-Ups: <strong>Super Mushroom</strong> (Grow & extra hit) · <strong>Fire Flower</strong> (Shoot fireballs) · <strong>Starman</strong> (Invincible!). Hidden blocks & secret areas reward explorers.</p> <button id="btnStart">Start Adventure</button> <div class="credits">All in one HTML file. Sounds generated via the Web Audio API.</div> </div> </div> <div id="gameOver" class="overlay hidden gameover"> <div class="panel"> <h2 id="gameOverTitle">Game Over</h2> <div id="finalStats" class="statline">Score: 0</div> <button id="btnRestart">Restart</button> </div> </div> <!-- Increased game canvas size for a bigger viewport --> <canvas id="game" width="1600" height="900"></canvas> <script> // ============================================================= // CONFIG & CONSTANTS // ============================================================= const CANVAS = document.getElementById('game'); const CTX = CANVAS.getContext('2d'); const W = CANVAS.width, H = CANVAS.height; const TILE = 48; // Base tile size let pixelScale = 1; // dynamic for crispness // Disable smoothing for crisp pixel look CTX.imageSmoothingEnabled = false; // Player states const POWER = { NORMAL:'Normal', BIG:'Big', FIRE:'Fire', STAR:'Star' }; // Colors per world / theme backgrounds const THEMES = [ { name:'School', bg:['#2d006e','#351b8a','#0d225f'], gradient:'linear-gradient(135deg,#4422aa,#110033)', water:false }, { name:'Horse Stable', bg:['#1a3d0a','#165a24','#072b12'], gradient:'linear-gradient(135deg,#0f5c25,#072b12)', water:false }, { name:'Underwater', bg:['#001d3d','#003566','#001845'], gradient:'linear-gradient(135deg,#003566,#001845)', water:true }, { name:"McDonald's", bg:['#4d0011','#6d001e','#330008'], gradient:'linear-gradient(135deg,#6d001e,#1a0006)', water:false } ]; // Mushroom bounce tuning constants (higher values = higher bounce) const MUSHROOM_BOUNCE_MULT = 1.8; // lower mushroom bounce height const MUSHROOM_MAX_FALL_BONUS = 0.0; // no fall bonus (consistent) const MUSHROOM_FALL_DIVISOR = 14; // (unused now) // Sound generator (very small synth) const AudioCtx = window.AudioContext || window.webkitAudioContext; const audio = new AudioCtx(); function playTone(freq=440, dur=0.12, type='square', vol=0.2, sweep=0){ const t = audio.currentTime; const osc = audio.createOscillator(); const gain = audio.createGain(); osc.frequency.setValueAtTime(freq,t); if(sweep) osc.frequency.exponentialRampToValueAtTime(Math.max(40,freq+sweep), t+dur); osc.type = type; gain.gain.setValueAtTime(vol,t); gain.gain.exponentialRampToValueAtTime(0.0001, t+dur); osc.connect(gain).connect(audio.destination); osc.start(t); osc.stop(t+dur); } function chord(f1,f2,f3,d=0.25){ playTone(f1,d,'sawtooth',0.12); playTone(f2,d,'square',0.08); playTone(f3,d,'triangle',0.1); } // Global game state let gameRunning=false, paused=false, frame=0; let currentLevelIndex=0; let worldTimer=0; let score=0; let lives=3; let startLives=3; let showBossBar=false; let boss=null; let medalCollected=false; let win=false; let vegetablesCollected=0; let vegetablesNeeded=5; let veggieAmmo=0; // ammo from vegetables (used in boss level) // Per-level food spawn caps (exact: 3 vegetables + 2 junk) let levelVegSpawned=0, levelJunkSpawned=0; // Player object const player = { x:120, y:0, w:28, h:38, vx:0, vy:0, onGround:false, facing:1, power:POWER.NORMAL, fireCooldown:0, starTimer:0, grown:false, prevPower:null, hurtTimer:0, invincibleTimer:0, runHeld:false, bounceCooldown:0, bounceBoostWindow:0, usedBounceBoost:false, bounceAssistFrames:0 }; // Entities arrays const enemies=[], particles=[], powerUps=[], fireballs=[], veggieShots=[]; // dynamic arrays // Input const keys={}; window.addEventListener('keydown',e=>{ keys[e.key.toLowerCase()]=true; if(e.key==='p'||e.key==='P'){ paused=!paused; } }); window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; }); // Utility const rand=(a,b)=>Math.random()*(b-a)+a; const clamp=(v,a,b)=>v<a?a:v>b?b:v; // HUD elements const elScore=document.getElementById('score'); const elLives=document.getElementById('lives'); const elWorld=document.getElementById('world'); const elPower=document.getElementById('power'); const elVeggies=document.getElementById('veggies'); const elAmmo=document.createElement('span'); // placeholder (will be injected if not already in HUD) const elJunk=document.getElementById('junk'); const toast=document.getElementById('toast'); const bossBarWrap=document.getElementById('bossBarWrap'); const bossBar=document.getElementById('bossBar'); const bossHpText=document.getElementById('bossHpText'); // Insert ammo HUD if missing if(!document.getElementById('ammo')){ const ammoWrap=document.createElement('div'); ammoWrap.innerHTML='<span class="label">Ammo</span><span id="ammo" class="value">0</span>'; document.getElementById('hud').appendChild(ammoWrap); } const elAmmoVal=document.getElementById('ammo'); // UI overlay elements const startScreen=document.getElementById('startScreen'); const gameOverScreen=document.getElementById('gameOver'); const finalStats=document.getElementById('finalStats'); const gameOverTitle=document.getElementById('gameOverTitle'); document.getElementById('btnStart').onclick=()=>{ startScreen.classList.add('hidden'); initGame(); const canvas=document.getElementById('game'); if(canvas.requestFullscreen){canvas.requestFullscreen().catch(err=>console.log('Fullscreen failed:',err));} else if(canvas.webkitRequestFullscreen){canvas.webkitRequestFullscreen();} else if(canvas.mozRequestFullScreen){canvas.mozRequestFullScreen();} else if(canvas.msRequestFullscreen){canvas.msRequestFullscreen();} }; document.getElementById('btnRestart').onclick=()=>{ gameOverScreen.classList.add('hidden'); initGame(); }; function showToast(msg){ toast.textContent=msg; toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'),1500); } // ============================================================= // LEVEL DATA (Tile maps) '.' empty, '#' block, '=' platform, '?' question, 'E' enemy, 'H' hidden, 'P' pipe, 'M' medal, 'B' boss spawn // ============================================================= // Keep widths moderate; we will scroll camera. const LEVELS=[ { theme:0, name:'1-1', map:[ '................................................', '................................................', '.................?..............................', '...............V................................', '.............E..................................', '..........###...............?.......E...........', '....................V...................#####..........', '.....?...............V.................?........', '?###########..####..###########################.' ]}, { theme:1, name:'2-1', map:[ '................................................', '................................................', '..............E.........?.......................', '.....................................H..........', '..........###...................................', '......................E.........V...............', '......?..........................................', '....................####......?..V.........?...', '?########..###################..###############.' ]}, { theme:2, name:'3-1', map:[ // Underwater: slower gravity '................................................', '................................................', '.....E...........?...............E..............', '......................E..........................', '................................................', '............?.................?.......V.........', '..................####..........V..............M', '......................###.......................', '?##############..#############..################' ]}, { theme:3, name:"Closet Boss", map:[ '######################', '#....................#', '#.........B..........#', '#....................#', '#....?........?......#', '#........M...........#', '#....V...........V...#', '#.........E..........#', '######################' ]} ]; // Camera const camera={x:0,y:0}; // Decorative elements (clouds & hills) regenerated per level let clouds=[], hills=[]; // ------------------------------------------------------------- // IMAGE / SPRITES // ------------------------------------------------------------- // Attempt to load user-provided jogging woman image first, then fallback to runner.png, then pixel hero. const runnerImg=new Image(); const bossImg=new Image(); bossImg.src='https://static.wixstatic.com/media/0a0716_41866c4f4d484d4695b20de873d5d048~mv2.webp'; // Load boss image from Wix const logoImg=new Image(); logoImg.src='https://static.wixstatic.com/media/0a0716_f535650c4f524797add9e64460c3afb4~mv2.png'; // Load project logo for background pattern from Wix const runnerSources=['jogging_woman.png','runner.png']; // Place your provided image as jogging_woman.png in the same folder. let runnerFrames=6; // default expected frames if a sheet is used let runnerLoaded=false; let currentRunnerSourceIndex=0; function loadRunner(){ if(currentRunnerSourceIndex>=runnerSources.length){ runnerLoaded=false; return; } runnerImg.src=runnerSources[currentRunnerSourceIndex]; } runnerImg.onload=()=>{ runnerLoaded=true; // Heuristic: if width is not significantly wider than height, assume single frame if(runnerImg.width <= runnerImg.height*1.3) runnerFrames=1; }; runnerImg.onerror=()=>{ currentRunnerSourceIndex++; loadRunner(); }; loadRunner(); // Draw hero with state function drawPlayer(){ const p=player; CTX.save(); CTX.translate(Math.floor(p.x-camera.x), Math.floor(p.y-camera.y)); // Flicker during invincibility / hurt if(p.hurtTimer>0 && (frame%6<3)) { CTX.restore(); return; } let scale= p.power!==POWER.NORMAL?1.2:1; if(p.power===POWER.STAR) { const hue=(frame*6)%360; CTX.filter=`hue-rotate(${hue}deg)`; } if(runnerLoaded){ const fw=runnerImg.width/runnerFrames; const fh=runnerImg.height; let animIndex=0; if(runnerFrames>1){ animIndex = p.onGround? Math.floor(frame/6)%runnerFrames : 0; } else { // Single-frame sprite: fake a subtle bob while running const bob = p.onGround? Math.sin(frame*0.35)*2 : 0; CTX.translate(0,bob); } CTX.scale(p.facing*scale, scale); CTX.drawImage(runnerImg, animIndex*fw,0,fw,fh, -fw/2, -fh+4, fw, fh); } else { // Simple fallback pixel body CTX.scale(p.facing*scale, scale); CTX.fillStyle='#fff'; CTX.fillRect(-14,-40,28,40); CTX.fillStyle='#ff3566'; CTX.fillRect(-14,-14,28,14); CTX.fillStyle='#222'; CTX.fillRect(-10,-34,8,8); CTX.fillRect(2,-34,8,8); } CTX.restore(); // Life bar above head (hearts style) CTX.save(); const barW=120, barH=14; const px = Math.floor(p.x - camera.x - barW/2); const py = Math.floor(p.y - camera.y - p.h - 26); // Outline CTX.fillStyle='#fff'; CTX.fillRect(px-2,py-2,barW+4,barH+4); CTX.fillStyle='#111'; CTX.fillRect(px,py,barW,barH); // Fill based on initial starting lives so bar begins full green const ratio = clamp(lives/startLives,0,1); const grad=CTX.createLinearGradient(px,py,px+barW,py); if(ratio>0.66){ grad.addColorStop(0,'#30d158'); grad.addColorStop(1,'#0b8d2b'); } else if(ratio>0.33){ grad.addColorStop(0,'#ffb347'); grad.addColorStop(1,'#ff8c00'); } else { grad.addColorStop(0,'#ff5f56'); grad.addColorStop(1,'#c91919'); } CTX.fillStyle=grad; CTX.fillRect(px,py, barW*ratio, barH); // Pixel heart icon at left drawHeart(px-26, py-4, 18); CTX.restore(); } // Small pixel heart drawing helper function drawHeart(x,y,size){ const s=size/8; // 8x7 grid shape CTX.save(); CTX.translate(x,y); CTX.scale(s,s); CTX.fillStyle='#ff002b'; const pixels=[ '01100110', '11111111', '11111111', '11111111', '01111110', '00111100', '00011000' ]; for(let r=0;r<pixels.length;r++){ for(let c=0;c<pixels[r].length;c++) if(pixels[r][c]==='1') CTX.fillRect(c,r,1,1); } CTX.restore(); } // Entity spawners ------------------------------------------------ function spawnEnemy(x,y){ // Underwater world (theme 2) spawns aquatic enemies const lvl=LEVELS[currentLevelIndex]; if(lvl && lvl.theme===2){ const aquatic = Math.random()<0.75? 'fish':'shark'; const w = aquatic==='shark'?56:42; const h = aquatic==='shark'?32:26; enemies.push({x,y,w,h,vx:rand(-1.2,1.2)||0.6,vy:0,dead:false,type:aquatic,frameOff:Math.random()*120}); } else { enemies.push({x,y,w:30,h:30,vx:rand(-0.6,0.6),vy:0,dead:false,type:'junk',frameOff:Math.random()*60}); } } function spawnPowerUp(x,y){ // random selection const r=Math.random(); let kind='mushroom'; if(r>0.66) kind='flower'; else if(r>0.33) kind='star'; powerUps.push({x,y,w:28,h:28,vy: -2, kind, bounce:0}); } // Spawn food with fixed per-level limits: exactly 5 vegetables and 3 junk items total. function spawnVegetable(x,y){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const junk = ['🍔','🍩','🍟','🍕','🌭','🧁']; if(levelVegSpawned+levelJunkSpawned >=8) return; // already reached per-level total (5+3) let kind, food; if(levelVegSpawned < 5){ // Prioritize spawning vegetables until we have 5 food = veggies[Math.floor(Math.random()*veggies.length)]; kind='veggie'; levelVegSpawned++; } else if(levelJunkSpawned < 3){ food = junk[Math.floor(Math.random()*junk.length)]; kind='junk'; levelJunkSpawned++; } else return; // safety (should be caught by first check) powerUps.push({x,y,w:28,h:28,vy:-2, kind, veggie:food, bounce:0}); } // Free spawn (no upward velocity). Uses same caps/logic. function spawnFreeVegetable(x,y){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const junk = ['🍔','🍩','🍟','🍕','🌭','🧁']; if(levelVegSpawned+levelJunkSpawned >=8) return; let kind, food; if(levelVegSpawned < 5){ food = veggies[Math.floor(Math.random()*veggies.length)]; kind='veggie'; levelVegSpawned++; } else if(levelJunkSpawned < 3){ food = junk[Math.floor(Math.random()*junk.length)]; kind='junk'; levelJunkSpawned++; } else return; powerUps.push({x,y,w:28,h:28,vy:0, kind, veggie:food, bounce:0, free:true}); } // Ensure each level always has 5 vegetables even if the map had fewer 'V' markers. function ensureVegetableQuota(){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; while(levelVegSpawned < 5){ const food = veggies[Math.floor(Math.random()*veggies.length)]; // Place them near the start area but spaced so they don't overlap const x = 200 + levelVegSpawned * 70; // world coordinates const y = 140; // mid-air so player sees them powerUps.push({x,y,w:28,h:28,vy:0, kind:'veggie', veggie:food, bounce:0, free:true}); levelVegSpawned++; } } // Fireball function shootFireball(){ if(player.power!==POWER.FIRE || player.fireCooldown>0) return; player.fireCooldown=24; playTone(680,0.08,'square',0.15,-300); fireballs.push({x:player.x+player.facing*20, y:player.y-30, vx:player.facing*6, vy:-1.2, life:180}); } // Veggie projectile (unlimited ammo). Uses vegetables as projectiles. function shootVeggie(){ // Only check cooldown (unlimited ammo) if(player.veggieCooldown>0) return; player.veggieCooldown = 16; // short cooldown between veggie throws const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const char = veggies[Math.floor(Math.random()*veggies.length)]; const speed = 6.2; // Slight upward arc start - added w and h for collision detection veggieShots.push({x:player.x+player.facing*22, y:player.y-34, vx:player.facing*speed, vy:-2.4, life:140, char, w:28, h:28}); // Fun throw sound (two quick tones) playTone(520,0.07,'triangle',0.18,160); playTone(760,0.06,'square',0.14,200); // Throw particles (green-ish) for(let i=0;i<6;i++) particles.push({x:player.x+rand(-8,8), y:player.y-38, vx:rand(-1.2,1.2), vy:rand(-2.4,-0.6), life:14, color:'#6aff6a'}); } // ============================================================= // GAME INITIALIZATION / RESET // ============================================================= function initGame(){ score=0; lives=3; startLives=lives; currentLevelIndex=0; win=false; medalCollected=false; boss=null; vegetablesCollected=0; loadLevel(currentLevelIndex); updateHUD(); player.power=POWER.NORMAL; player.starTimer=0; player.prevPower=null; player.hurtTimer=0; player.invincibleTimer=0; gameRunning=true; paused=false; frame=0; veggieAmmo=0; veggieShots.length=0; player.veggieCooldown=0; } function loadLevel(i){ const lvl=LEVELS[i]; enemies.length=0; powerUps.length=0; fireballs.length=0; particles.length=0; boss=null; showBossBar=false; bossBarWrap.style.display='none'; vegetablesCollected=0; // reset vegetable count for new level levelVegSpawned=0; levelJunkSpawned=0; // reset per-level food counters veggieShots.length=0; veggieAmmo=0; const map=lvl.map; // spawn enemies & power-ups from map symbols for(let r=0;r<map.length;r++){ for(let c=0;c<map[r].length;c++){ const ch=map[r][c]; if(ch==='E') spawnEnemy(c*TILE+TILE/2,(r*TILE)+10); if(ch==='B') boss={x:c*TILE+TILE/2,y:r*TILE+10,w:68,h:68,vx:1.4,vy:0,health:7,attackTimer:240,grounded:false,phase:0}; if(ch==='V') spawnFreeVegetable(c*TILE+TILE/2, r*TILE+TILE/2); if(ch==='?') {/* question blocks spawn when hit */} } } player.x=100; player.y=0; camera.x=0; worldTimer=0; elWorld.textContent=lvl.name; document.body.style.background=THEMES[lvl.theme].gradient; // Adjust spawn & camera for compact closet boss room so player isn't hidden in ceiling if(lvl.name.includes('Closet')){ player.x = TILE*4.5; // center-ish horizontally player.y = TILE*5; // below medal / inside room camera.x = Math.max(0, (lvl.map[0].length*TILE - W)/2); // center room if narrower than canvas // If still inside a solid tile, nudge downward until free (safety loop) let safety=0; while(collideSolid(player) && safety<40){ player.y += 2; safety++; } } // After initial spawns, guarantee vegetable quota ensureVegetableQuota(); initDecor(); } // Create clouds & hills to mimic classic platform sky while remaining original assets function initDecor(){ clouds=[]; hills=[]; const lvl=LEVELS[currentLevelIndex]; const width= lvl.map[0].length*TILE; // Hills (large rounded rectangles) for(let i=0;i<8;i++){ const w= rand(180,360); const h= rand(120,240); hills.push({x: rand(0,width-w), y: H - h - 120, w, h, layer: i%2, shade: `hsl(${rand(180,220)},40%,${i%2?55:65}%)`}); } // Clouds for(let i=0;i<14;i++){ clouds.push({x:rand(0,width), y:rand(40,260), speed:rand(0.1,0.35), scale:rand(0.7,1.4)}); } } // ============================================================= // TILE / COLLISION HELPERS // ============================================================= function getTile(x,y){ const lvl=LEVELS[currentLevelIndex]; const map=lvl.map; if(y<0) return '.'; const r=Math.floor(y/TILE); const c=Math.floor(x/TILE); if(r<0||r>=map.length||c<0||c>=map[0].length) return '.'; return map[r][c]; } function isSolid(ch){ return ch==='#' || ch==='=' || ch==='P' || ch==='?'; } function isQuestion(ch){ return ch==='?'; } function isMedal(ch){ return ch==='M'; } function breakQuestion(r,c){ const lvl=LEVELS[currentLevelIndex]; const row=lvl.map[r]; lvl.map[r]= row.substring(0,c)+'#'+row.substring(c+1); spawnVegetable(c*TILE+TILE/2, r*TILE-4); playTone(880,0.15,'square',0.18, -100); score+=50; } function revealHidden(r,c){ const lvl=LEVELS[currentLevelIndex]; const row=lvl.map[r]; lvl.map[r]= row.substring(0,c)+'?'+row.substring(c+1); spawnVegetable(c*TILE+TILE/2, r*TILE-4); playTone(880,0.15,'square',0.18, -100); score+=50; } // ============================================================= // UPDATE LOOP // ============================================================= function update(){ if(!gameRunning||paused) return; frame++; worldTimer++; const lvl=LEVELS[currentLevelIndex]; const theme=THEMES[lvl.theme]; // Player input const left = keys['arrowleft']||keys['a']; const right=keys['arrowright']||keys['d']; const jump = keys['arrowup']||keys['w']||keys[' ']; const runKey=keys['shift']; player.runHeld=runKey; const maxSpeed = runKey?4.2:3.1; const accel= runKey?0.45:0.35; if(left && !right) { player.vx = Math.max(player.vx - accel, -maxSpeed); player.facing=-1; } else if(right && !left) { player.vx = Math.min(player.vx + accel, maxSpeed); player.facing=1; } else { player.vx *=0.75; if(Math.abs(player.vx)<0.05) player.vx=0; } if(player.fireCooldown>0) player.fireCooldown--; if(player.veggieCooldown>0) player.veggieCooldown--; if(player.starTimer>0){ player.starTimer--; if(player.starTimer%15===0) playTone( rand(600,1200),0.05,'square',0.05); if(player.starTimer===0){ if(player.prevPower) player.power=player.prevPower; updateHUD(); } } if(player.hurtTimer>0) player.hurtTimer--; if(player.invincibleTimer>0) player.invincibleTimer--; if(player.power===POWER.STAR) player.invincibleTimer=10; // always invincible while star // Gravity const grav = theme.water? 0.15 : 0.55; const jumpVel = theme.water? -5.6 : -8.9; // slightly higher normal jump player.vy += grav; if(player.vy>16) player.vy=16; if(jump && player.onGround){ player.vy=jumpVel; player.onGround=false; playTone(300,0.22,'square',0.25,300); } // Shooting: // F key = fireball (if Fire power) or veggie (unlimited) // Shift while boss present = veggie (unlimited) if(keys['f']){ if(player.power===POWER.FIRE) shootFireball(); else shootVeggie(); } if(boss && runKey){ shootVeggie(); // unlimited veggie ammo in boss level } // Movement & collision (horizontal then vertical) const lastVy = player.vy; // store fall speed before movement for bounce strength bonus moveEntity(player); // Camera follows camera.x = clamp(player.x - W/2, 0, lvl.map[0].length*TILE - W); // Mushroom bounce (two-stage trampoline) if(player.bounceCooldown>0) player.bounceCooldown--; if(player.onGround){ const belowTile = getTile(player.x, player.y + player.h/2 + 2); if(belowTile==='?' && player.bounceCooldown<=0){ const baseJump = theme.water? -5.6 : -8.9; // match slightly higher normal jump // Higher bounce in world 2-1 const bounceMult = lvl.name==='2-1' ? 2.6 : MUSHROOM_BOUNCE_MULT; player.vy = baseJump * bounceMult; // big bounce player.onGround=false; player.bounceCooldown=16; player.bounceBoostWindow=0; player.usedBounceBoost=true; player.bounceAssistFrames=10; for(let i=0;i<22;i++) particles.push({x:player.x+rand(-18,18), y:player.y+player.h/2-8, vx:rand(-1.6,1.6), vy:rand(-6,-1.5), life:24, color:i%3? '#ffeb7a':'#ffd54f'}); playTone(820,0.22,'square',0.3,340); playTone(1230,0.18,'triangle',0.18,-260); showToast('Super Jump!'); } } // Apply bounce assist (counter gravity a bit to emphasize height) if(player.bounceAssistFrames>0){ player.vy -= 0.35; // small upward push player.bounceAssistFrames--; } // Optional higher boost if jump is pressed within window // (Super bounce removed for simple double height) // Fall death check (below level bounds) if(player.y - player.h/2 > lvl.map.length*TILE + 160){ fallDeath(); return; // skip rest of update this frame } // Fireballs for(let i=fireballs.length-1;i>=0;i--){ const fb=fireballs[i]; fb.life--; fb.x+=fb.vx; fb.y+=fb.vy; fb.vy+=0.25; if(fb.life<=0) fireballs.splice(i,1); else { // collide with tiles if(isSolid(getTile(fb.x, fb.y))) { particles.push({x:fb.x,y:fb.y,life:16,color:'#ffef8d'}); fireballs.splice(i,1); continue; } // collide with enemies for(const e of enemies){ if(!e.dead && aabb(fb,e)){ killEnemy(e,true); fireballs.splice(i,1); break; } } if(boss && aabb(fb,boss)){ damageBoss(); fireballs.splice(i,1); } } } // Veggie shots (projectiles using collected vegetables) for(let i=veggieShots.length-1;i>=0;i--){ const vs=veggieShots[i]; vs.life--; vs.x+=vs.vx; vs.y+=vs.vy; vs.vy+=0.32; // gravity if(vs.life<=0){ veggieShots.splice(i,1); continue; } // Tile collision ends projectile if(isSolid(getTile(vs.x, vs.y))){ particles.push({x:vs.x,y:vs.y,life:16,color:'#32cd32'}); veggieShots.splice(i,1); continue; } // Enemy collision let hit=false; for(const e of enemies){ if(!e.dead && aabb(vs,e)){ killEnemy(e,true); hit=true; break; } } if(hit){ veggieShots.splice(i,1); continue; } // Boss collision if(boss && aabb(vs,boss)){ damageBoss(); particles.push({x:vs.x,y:vs.y-20,life:18,color:'#4aff4a'}); veggieShots.splice(i,1); continue; } } // Enemies update for(const e of enemies){ if(e.dead){ e.vy+=0.6; e.y+=e.vy; continue; } if(LEVELS[currentLevelIndex].theme===2 && (e.type==='fish'||e.type==='shark')){ // Aquatic motion: sinusoidal vertical swim const swimSpeed = e.type==='shark'?0.045:0.07; const amp = e.type==='shark'?10:16; e.y += Math.sin((frame+e.frameOff)*swimSpeed)*0.9; e.x += e.vx * (e.type==='shark'?1.25:0.9); // Turn when near horizontal bounds (camera independent using map width) if(e.x<40){ e.x=40; e.vx=Math.abs(e.vx); } if(e.x>LEVELS[currentLevelIndex].map[0].length*TILE-40){ e.x=LEVELS[currentLevelIndex].map[0].length*TILE-40; e.vx=-Math.abs(e.vx); } } else { // Land enemy physics (burgers don't fall off platforms) e.vy+=0.5; // Check if about to walk off a platform (look ahead) const checkX = e.x + e.vx*8; const checkY = e.y + e.h/2 + 8; // slightly below feet const groundAhead = isSolid(getTile(checkX, checkY)); if(!groundAhead){ e.vx*=-1; } // turn around if no ground ahead e.x+=e.vx; e.y+=e.vy; if(isSolid(getTile(e.x+e.vx*4, e.y))){ e.vx*=-1; } if(isSolid(getTile(e.x, e.y+e.h/2))){ e.y = Math.floor((e.y+e.h/2)/TILE)*TILE - e.h/2; e.vy=0; } } // Player collision / interaction if(player.starTimer>0 && !e.dead && aabb(player,e)){ killEnemy(e,true); continue; } if(!e.dead && aabb(player,e)){ if(player.vy>2 && player.y < e.y-10){ killEnemy(e,false); player.vy=-8; score+=100; playTone(700,0.15,'square',0.22); } else if(player.invincibleTimer<=0){ damagePlayer(); } } } // PowerUps for(let i=powerUps.length-1;i>=0;i--){ const p=powerUps[i]; if(!p.free) p.vy+=0.4; // only apply gravity to non-free vegetables p.y+=p.vy; if(p.y>H+200){ powerUps.splice(i,1); continue; } if(aabb(player,p)){ applyPowerUp(p.kind); powerUps.splice(i,1); continue; } } // Boss logic if(boss){ showBossBar=true; bossBarWrap.style.display='block'; const levelWidthPx = LEVELS[currentLevelIndex].map[0].length * TILE; // Different physics if in closet room: keep boss hovering on floor and never falling off if(LEVELS[currentLevelIndex].name.includes('Closet')){ // Lock vertical motion to floor level (second to last row inside room) const floorY = (LEVELS[currentLevelIndex].map.length-2)*TILE - boss.h/2; // inside boundary boss.vy = 0; boss.y = floorY; boss.grounded=true; boss.x += boss.vx; // horizontal patrol only } else { // Original gravity based motion boss.vy+=0.5; boss.x+=boss.vx; boss.y+=boss.vy; if(isSolid(getTile(boss.x, boss.y+boss.h/2))){ boss.y=Math.floor((boss.y+boss.h/2)/TILE)*TILE - boss.h/2; boss.vy=0; boss.grounded=true; } else boss.grounded=false; } // Clamp inside level so boss (burger) never leaves floor/edges const minX = boss.w/2 + 16; const maxX = levelWidthPx - boss.w/2 - 16; if(boss.x < minX){ boss.x = minX; boss.vx = Math.abs(boss.vx); } if(boss.x > maxX){ boss.x = maxX; boss.vx = -Math.abs(boss.vx); } if(boss.x<camera.x+50 || boss.x>camera.x+W-50) boss.vx*=-1; // Attack pattern boss.attackTimer--; if(boss.attackTimer<=0){ boss.attackTimer=rand(140,220); // spawn minion for(let j=0;j<3;j++) setTimeout(()=>spawnEnemy(boss.x+rand(-30,30), boss.y), j*200); playTone(200,0.4,'sawtooth',0.3,-70); } // collision player if(player.starTimer>0 && aabb(player,boss)){ damageBoss(true); } else if(aabb(player,boss)){ // Allow jumping on boss from above (easier threshold) if(player.vy>1 && player.y < boss.y-10){ player.vy=-10; // bounce up damageBoss(); score+=100; playTone(700,0.15,'square',0.22); } else if(player.invincibleTimer<=0){ damagePlayer(); } } const ratio = boss.health/7; bossBar.style.width=(ratio*100)+'%'; // Color shifts from green -> yellow -> orange -> red let col1='#28d657', col2='#1ea94e'; if(ratio<0.65 && ratio>=0.4){ col1='#ffd53b'; col2='#ffb347'; } else if(ratio<0.4 && ratio>=0.2){ col1='#ff8f2b'; col2='#ff5d00'; } else if(ratio<0.2){ col1='#ff2d2d'; col2='#b80000'; } bossBar.style.background=`linear-gradient(90deg,${col1},${col2})`; if(bossHpText) bossHpText.textContent = boss.health+"/7"; } // Medal detection (Victory) if(!win && !boss && lvl.name.startsWith('4') && medalCollected){ winGame(); } // Cleanup & particle update for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life--; p.x+=p.vx||0; p.y+=p.vy||0; if(p.life<=0) particles.splice(i,1); } // Level end (touch medal tile) or proceed after right edge reached for earlier levels const rightEdge = lvl.map[0].length*TILE - 100; if(player.x> rightEdge && currentLevelIndex < LEVELS.length-1 && !boss){ nextLevel(); // proceed to next level without vegetable requirement } updateHUD(); } function aabb(a,b){ return Math.abs(a.x - b.x) < (a.w + b.w)/2 && Math.abs(a.y - b.y) < (a.h + b.h)/2; } function moveEntity(p){ // horizontal p.x += p.vx; // sample corners horizontally if(collideSolid(p)){ p.x -= p.vx; while(!collideSolid(p)){ p.x += Math.sign(p.vx)*0.5; } p.x -= Math.sign(p.vx)*0.5; p.vx=0; } p.y += p.vy; const wasGround=p.onGround; p.onGround=false; if(collideSolid(p)){ p.y -= p.vy; while(!collideSolid(p)){ p.y += Math.sign(p.vy)*0.5; } p.y -= Math.sign(p.vy)*0.5; if(p.vy>0) { p.onGround=true; } p.vy=0; } // Block head bump (question blocks / hidden) when going upward if(p.vy<0){ const topTileY = p.y - p.h/2 -1; const left = p.x - p.w/2 +4; const right = p.x + p.w/2 -4; for(const tPos of [left,right]){ const r=Math.floor(topTileY/TILE); const c=Math.floor(tPos/TILE); const ch=getTile(tPos,topTileY); if(ch==='H'){ revealHidden(r,c); playTone(520,0.2,'square',0.15); } if(ch==='?'){ breakQuestion(r,c); } // now mushrooms can be hit from below too } } // Medal tile if(isMedal(getTile(p.x,p.y))){ medalCollected=true; showToast('You got the Medal! Defeat the Boss!'); } } function collideSolid(p){ // check a few points const hw=p.w/2, hh=p.h/2; const pts=[[p.x-hw+4,p.y-hh],[p.x+hw-4,p.y-hh],[p.x-hw+4,p.y+hh],[p.x+hw-4,p.y+hh]]; for(const [x,y] of pts){ if(isSolid(getTile(x,y))) return true; } return false; } // ============================================================= // POWER UP & DAMAGE // ============================================================= function applyPowerUp(kind){ if(kind==='veggie'){ // Healthy vegetables: points + life (capped at 9) score+=100; vegetablesCollected++; if(lives < 9){ lives++; showToast(`+1 Life & Healthy! (${vegetablesCollected}/${vegetablesNeeded})`); } else { showToast(`Healthy! (${vegetablesCollected}/${vegetablesNeeded})`); } // If in boss level convert to ammo if(LEVELS[currentLevelIndex].name.includes('Closet') || boss){ veggieAmmo++; } updateHUD(); playTone(650,0.22,'triangle',0.25,220); playTone(980,0.18,'sine',0.22,340); // Visual effect (green + heart shimmer if life gained) for(let i=0;i<10;i++) particles.push({x:player.x+rand(-14,14), y:player.y-26, vx:rand(-1.2,1.2), vy:rand(-3.2,-0.8), life:18, color:i%3? '#32cd32':'#6aff6a'}); if(lives<=9) particles.push({x:player.x, y:player.y-40, vx:0, vy:-1.2, life:26, text:'❤', color:'#ff4d6d'}); } else if(kind==='junk'){ // Junk food gives points but no vegetable progress score+=50; showToast('Tasty junk food! +50 points'); playTone(600,0.15,'triangle',0.15,100); // Orange visual effect for junk food for(let i=0;i<6;i++) particles.push({x:player.x+rand(-12,12), y:player.y-20, vx:rand(-1,1), vy:rand(-3,-1), life:12, color:'#ff8c00'}); } else if(kind==='mushroom'){ if(player.power===POWER.NORMAL){ player.power=POWER.BIG; player.grown=true; player.h=54; showToast('Super!'); playTone(420,0.3,'sawtooth',0.25,200); } else { score+=200; playTone(900,0.12,'square',0.2); } } else if(kind==='flower'){ if(player.power===POWER.NORMAL){ player.power=POWER.BIG; player.h=54; } player.power=POWER.FIRE; showToast('Fire Power!'); chord(660,880,1320); } else if(kind==='star'){ player.prevPower= player.power===POWER.STAR? player.prevPower : player.power; player.power=POWER.STAR; player.starTimer=600; showToast('Invincible!'); chord(880,1170,1560); } if(kind!=='veggie') score+=300; updateHUD(); } function damagePlayer(){ if(player.invincibleTimer>0) return; if(player.power===POWER.STAR) return; player.hurtTimer=40; player.invincibleTimer=80; playTone(160,0.4,'sawtooth',0.3,-80); if(player.power===POWER.FIRE){ player.power=POWER.BIG; showToast('Lost Fire'); } else if(player.power===POWER.BIG){ player.power=POWER.NORMAL; player.h=38; showToast('Shrunk'); } else { lives--; showToast('-1 Life'); if(lives<=0){ gameOver(false); } else { player.x-=40; player.vx=0; } } updateHUD(); } function killEnemy(e,byStar){ e.dead=true; e.vx= (byStar? rand(-5,5):0); e.vy= - (byStar? rand(6,10):8); score+= byStar?200:100; particles.push({x:e.x,y:e.y,life:22,color:'#ffcf33'}); playTone(560,0.12,'square',0.2,140); } function damageBoss(stompStar){ if(!boss) return; boss.health--; particles.push({x:boss.x,y:boss.y-40,life:18,color:'#ff6633'}); playTone(300,0.28,'square',0.25,-140); playTone(160,0.18,'sine',0.18,-200); if(bossHpText) bossHpText.textContent=boss.health+"/7"; if(boss.health<=0){ score+=5000; particles.push({x:boss.x,y:boss.y,life:50,color:'#ffe400'}); showToast('Boss Defeated! Medal Secured!'); setTimeout(()=>{ winGame(); },1200); boss=null; bossBarWrap.style.display='none'; return; } // Pulse effect on low health (when 2 or less remaining) if(boss.health<=2){ bossBar.style.animation='pulseBar .6s linear'; setTimeout(()=>bossBar.style.animation='',600); } } // Player fall death handler function fallDeath(){ if(!gameRunning) return; lives--; showToast('Fell! -1 Life'); playTone(120,0.6,'sawtooth',0.28,-200); if(lives<=0){ gameOver(false); return; } // Respawn at level start, reset basic state player.x=100; player.y=0; player.vx=0; player.vy=0; player.onGround=false; player.power=POWER.NORMAL; player.h=38; player.starTimer=0; player.invincibleTimer=50; // brief grace period updateHUD(); } // ============================================================= // LEVEL PROGRESSION // ============================================================= function nextLevel(){ currentLevelIndex++; if(currentLevelIndex>=LEVELS.length){ winGame(); return; } loadLevel(currentLevelIndex); showToast('World '+LEVELS[currentLevelIndex].name); playTone(520,0.3,'sine',0.25,300); } function winGame(){ win=true; gameOverTitle.textContent='Victory!'; gameOver(true); } function gameOver(victory){ gameRunning=false; finalStats.textContent='Score: '+score; gameOverScreen.classList.remove('hidden'); if(victory){ gameOverScreen.classList.add('victory'); } else { gameOverScreen.classList.remove('victory'); } playTone(victory?880:120,1.0,'sawtooth',0.25, victory?400:-200); } // ============================================================= // HUD // ============================================================= function updateHUD(){ elScore.textContent=score; elLives.textContent=lives; elWorld.textContent=LEVELS[currentLevelIndex].name; elPower.textContent=player.power; elVeggies.textContent=`${vegetablesCollected}/${vegetablesNeeded}`; if(elJunk) elJunk.textContent=`${levelJunkSpawned}/3`; if(elAmmoVal) elAmmoVal.textContent = '∞'; // unlimited ammo } // ============================================================= // RENDERING // ============================================================= function render(){ CTX.clearRect(0,0,W,H); if(!gameRunning){ requestAnimationFrame(render); return; } const lvl=LEVELS[currentLevelIndex]; drawBackground(); drawTiles(lvl); // Entities for(const e of enemies){ const cx=e.x-camera.x, cy=e.y-camera.y; CTX.save(); CTX.translate(cx,cy); if(e.dead){ CTX.rotate(frame/6); } if(e.type==='fish'){ CTX.scale(e.vx<0?-1:1,1); CTX.fillStyle='#34d6ff'; CTX.beginPath(); CTX.ellipse(0,0, e.w*0.45, e.h*0.55, 0, 0, Math.PI*2); CTX.fill(); CTX.fillStyle='#1fa5c7'; CTX.beginPath(); CTX.moveTo(-e.w*0.45,0); CTX.lineTo(-e.w*0.63, e.h*0.28); CTX.lineTo(-e.w*0.63,-e.h*0.28); CTX.closePath(); CTX.fill(); CTX.fillStyle='#fff'; CTX.beginPath(); CTX.arc(e.w*0.18,-e.h*0.08,e.h*0.18,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#000'; CTX.beginPath(); CTX.arc(e.w*0.20,-e.h*0.08,e.h*0.08,0,Math.PI*2); CTX.fill(); // side fin CTX.save(); CTX.translate(-e.w*0.05, e.h*0.12); CTX.rotate(Math.sin((frame+e.frameOff)/12)*0.4); CTX.fillStyle='#26b2d6'; CTX.beginPath(); CTX.ellipse(0,0,e.w*0.18,e.h*0.3,0,0,Math.PI*2); CTX.fill(); CTX.restore(); } else if(e.type==='shark'){ CTX.scale(e.vx<0?-1:1,1); CTX.fillStyle='#70818f'; CTX.beginPath(); CTX.ellipse(0,0, e.w*0.5, e.h*0.55, 0, 0, Math.PI*2); CTX.fill(); CTX.fillStyle='#5c6a74'; CTX.beginPath(); CTX.moveTo(-e.w*0.5,0); CTX.lineTo(-e.w*0.66, e.h*0.3); CTX.lineTo(-e.w*0.66,-e.h*0.3); CTX.closePath(); CTX.fill(); CTX.fillStyle='#5c6a74'; CTX.beginPath(); CTX.moveTo(-6,-e.h*0.25); CTX.lineTo(0,-e.h*0.62); CTX.lineTo(6,-e.h*0.25); CTX.closePath(); CTX.fill(); CTX.fillStyle='#fff'; CTX.beginPath(); CTX.arc(e.w*0.28,-e.h*0.12,e.h*0.16,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#000'; CTX.beginPath(); CTX.arc(e.w*0.30,-e.h*0.12,e.h*0.07,0,Math.PI*2); CTX.fill(); // Mouth CTX.fillStyle='#111'; CTX.beginPath(); CTX.arc(e.w*0.20,e.h*0.08,e.h*0.38,0,Math.PI,true); CTX.fill(); CTX.fillStyle='#fff'; for(let i=0;i<6;i++){ CTX.beginPath(); const ox=-10+i*6; CTX.moveTo(ox,e.h*0.05); CTX.lineTo(ox+3,e.h*0.16); CTX.lineTo(ox+6,e.h*0.05); CTX.fill(); } } else { // Default land enemies: burger icons CTX.font='42px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍔',0,0); } CTX.restore(); } for(const p of powerUps){ CTX.save(); CTX.translate(p.x-camera.x, p.y-camera.y); if(p.kind==='veggie'){ // Draw emoji vegetables CTX.font='36px Arial'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText(p.veggie,0,0); } else { let g=CTX.createLinearGradient(-14,-14,14,14); if(p.kind==='mushroom'){ g.addColorStop(0,'#ff5f6d'); g.addColorStop(1,'#ffc371'); } else if(p.kind==='flower'){ g.addColorStop(0,'#ff8a00'); g.addColorStop(1,'#e52e71'); } else { g.addColorStop(0,'#26ffe6'); g.addColorStop(1,'#f107a3'); } CTX.fillStyle=g; CTX.beginPath(); CTX.roundRect(-14,-14,28,28,8); CTX.fill(); } CTX.restore(); } for(const fb of fireballs){ CTX.save(); CTX.translate(fb.x-camera.x, fb.y-camera.y); CTX.fillStyle='#ffe05c'; CTX.beginPath(); CTX.arc(0,0,8,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#ff3566'; CTX.arc(0,0,5,0,Math.PI*2); CTX.fill(); CTX.restore(); } // Veggie projectiles (draw emoji) for(const vs of veggieShots){ CTX.save(); CTX.translate(vs.x-camera.x, vs.y-camera.y); CTX.font='38px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText(vs.char,0,0); CTX.restore(); } // Boss: draw boss.webp image if(boss){ CTX.save(); CTX.translate(boss.x-camera.x, boss.y-camera.y); if(bossImg.complete && bossImg.naturalWidth > 0){ // Draw the boss image centered CTX.drawImage(bossImg, -boss.w/2, -boss.h/2, boss.w, boss.h); } else { // Fallback if image not loaded: mushroom emoji CTX.font='72px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍄',0,0); } CTX.restore(); } for(const pr of particles){ const alpha=pr.life/ (pr.maxLife||pr.life+1); CTX.globalAlpha=alpha; CTX.fillStyle=pr.color||'#fff'; CTX.fillRect(pr.x-camera.x, pr.y-camera.y, 6,6); CTX.globalAlpha=1; } drawPlayer(); if(paused){ CTX.fillStyle='#ffffff20'; CTX.fillRect(0,0,W,H); CTX.fillStyle='#fff'; CTX.font='bold 42px var(--ui-font)'; CTX.textAlign='center'; CTX.fillText('PAUSED', W/2, H/2); } requestAnimationFrame(render); } function drawBackground(){ const grd=CTX.createLinearGradient(0,0,0,H); grd.addColorStop(0,'#180022'); grd.addColorStop(1,'#050510'); CTX.fillStyle=grd; CTX.fillRect(0,0,W,H); // parallax glows const lvl=LEVELS[currentLevelIndex]; const theme=THEMES[lvl.theme]; // Override for non-underwater worlds with bright sky look if(!theme.water){ CTX.fillStyle='#69c5ff'; CTX.fillRect(0,0,W,H); // Draw ADAPT logo in background (centered, semi-transparent) if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.12; // subtle transparency const logoWidth = 400; // medium size const logoHeight = (logoImg.naturalHeight / logoImg.naturalWidth) * logoWidth; const logoX = (W - logoWidth) / 2; // centered horizontally const logoY = H * 0.3; // positioned in upper-middle area CTX.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight); CTX.restore(); } // Hills (parallax): layer 0 behind, layer 1 mid for(const layer of [0,1]){ for(const h of hills){ if(h.layer!==layer) continue; const par = layer?0.5:0.25; const sx = h.x - camera.x*par; if(sx+h.w < -50 || sx> W+50) continue; CTX.save(); CTX.translate(Math.floor(sx), h.y - 60); // raise a bit // Body CTX.fillStyle=h.shade; CTX.beginPath(); const r = h.h/2; CTX.roundRect(0,60, h.w, h.h, Math.min(80,r)); CTX.fill(); // Dots CTX.globalAlpha=0.25; CTX.fillStyle='#fff'; for(let i=0;i<8;i++){ CTX.beginPath(); CTX.arc( (i/8)*h.w + 14, 60 + (i%2)*40 + 30, 18, 0, Math.PI*2); CTX.fill(); } CTX.restore(); } } // Clouds drifting CTX.save(); CTX.fillStyle='#fff'; for(const c of clouds){ let x = (c.x - camera.x*0.2 + frame*c.speed) % (lvl.map[0].length*TILE); x -= (x<0? - (lvl.map[0].length*TILE):0); const sx = x - camera.x; if(sx<-150||sx>W+150) continue; CTX.save(); CTX.translate(sx, c.y); CTX.scale(c.scale,c.scale); CTX.globalAlpha=0.95; CTX.beginPath(); CTX.arc(0,0,26,0,Math.PI*2); CTX.arc(30,10,22,0,Math.PI*2); CTX.arc(-30,8,18,0,Math.PI*2); CTX.arc(5,18,20,0,Math.PI*2); CTX.fill(); CTX.restore(); } CTX.restore(); } else { // Enhanced underwater background for world 3 // Deep water gradient const waterGrad=CTX.createLinearGradient(0,0,0,H); waterGrad.addColorStop(0,'#023e8a'); waterGrad.addColorStop(0.45,'#035aa6'); waterGrad.addColorStop(0.75,'#02325f'); waterGrad.addColorStop(1,'#011c33'); CTX.fillStyle=waterGrad; CTX.fillRect(0,0,W,H); // Draw ADAPT logo in underwater background (centered, very subtle) if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.08; // more subtle in water const logoWidth = 400; const logoHeight = (logoImg.naturalHeight / logoImg.naturalWidth) * logoWidth; const logoX = (W - logoWidth) / 2; const logoY = H * 0.35; CTX.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight); CTX.restore(); } // Light shafts (caustics) CTX.save(); CTX.globalAlpha=0.16; CTX.fillStyle='#9ad9ff'; for(let i=0;i<6;i++){ const sx = ((i*210) + frame*12) % (W+400) -200; const wS = 120 + Math.sin((frame+i*30)/40)*40; CTX.beginPath(); CTX.moveTo(sx,0); CTX.lineTo(sx+wS*0.4,H*0.4); CTX.lineTo(sx+wS*0.15,H); CTX.lineTo(sx-wS*0.3,H); CTX.closePath(); CTX.fill(); } CTX.restore(); // Floating particles & bubbles CTX.save(); for(let i=0;i<26;i++){ const bx = ( (i*140) + frame* (0.3 + (i%5)*0.12) - camera.x*0.15) % (lvl.map[0].length*TILE); let sx = bx - camera.x; if(sx<-60||sx>W+60) continue; const by = ( (i*57) + frame* (0.15 + (i%7)*0.04) ) % H; CTX.fillStyle = i%3? '#8fd3ff55':'#ffffff30'; CTX.beginPath(); CTX.arc(sx, H - by, 6 + (i%4), 0, Math.PI*2); CTX.fill(); } CTX.restore(); // Sea plants at bottom CTX.save(); CTX.translate(0,H-40); for(let i=0;i<18;i++){ const px = (i* (lvl.map[0].length*TILE)/18) - camera.x*0.25 - camera.x; const sx = px - camera.x; if(sx<-100||sx>W+60) continue; CTX.save(); CTX.translate(sx,0); CTX.scale(1,1+Math.sin((frame+i*10)/50)*0.08); CTX.fillStyle= i%2? '#0c845d':'#0a6f4a'; CTX.beginPath(); CTX.roundRect(-6,-60,12,60,6); CTX.fill(); CTX.fillStyle='#13b384'; CTX.globalAlpha=0.35; CTX.beginPath(); CTX.roundRect(-3,-60,6,60,4); CTX.fill(); CTX.globalAlpha=1; CTX.restore(); } CTX.restore(); } } function drawTiles(lvl){ const startCol=Math.floor(camera.x/TILE); const endCol=Math.ceil((camera.x+W)/TILE); for(let r=0;r<lvl.map.length;r++){ for(let c=startCol;c<endCol;c++){ if(c<0||c>=lvl.map[r].length) continue; const ch=lvl.map[r][c]; if(ch==='.'||ch==='E'||ch==='B') continue; const x=c*TILE-camera.x; const y=r*TILE-camera.y; const above = r>0? lvl.map[r-1][c]:'.'; if(ch==='#') { drawGround(x,y, above!=='#'); continue; } CTX.save(); if(ch==='?'){ drawMushroomTile(x,y); } else if(ch==='H'){ CTX.globalAlpha=0.15; drawQuestion(x,y,true); } else if(ch==='M'){ drawMedal(x,y); } else if(isSolid(ch)) { drawBlock(x,y,'#5a4dff','#262e8e'); } CTX.restore(); } } } function drawGround(x,y,isTop){ // Draw small transparent logo pattern underneath if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.15; // transparent const logoSize = 32; // small size CTX.drawImage(logoImg, x + (TILE-logoSize)/2, y + (TILE-logoSize)/2, logoSize, logoSize); CTX.restore(); } // Dirt body const g=CTX.createLinearGradient(x,y,x,y+TILE); g.addColorStop(0,'#d37a25'); g.addColorStop(1,'#8a400c'); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); // Brick pattern lines CTX.strokeStyle='#632b06aa'; CTX.lineWidth=2; CTX.beginPath(); CTX.moveTo(x,y+TILE/2); CTX.lineTo(x+TILE,y+TILE/2); CTX.moveTo(x+TILE/2,y); CTX.lineTo(x+TILE/2,y+TILE); CTX.stroke(); // Grass top overlay if exposed if(isTop){ CTX.fillStyle='#2ecc40'; CTX.fillRect(x,y-10,TILE,14); CTX.fillStyle='#28b437'; for(let i=0;i<6;i++){ CTX.beginPath(); const px=x+i*(TILE/5); CTX.arc(px+6,y+2,8,0,Math.PI,true); CTX.fill(); } } } function drawQuestion(x,y,hidden){ const g=CTX.createLinearGradient(x,y,x+TILE,y+TILE); g.addColorStop(0,'#ffe18a'); g.addColorStop(1,'#e79807'); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); CTX.strokeStyle='#0005'; CTX.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1); if(!hidden){ CTX.fillStyle='#5e3200'; CTX.font='bold 26px var(--ui-font)'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('?',x+TILE/2,y+TILE/2+2); } } // Replaces question block visual: bouncy mushroom trampoline function drawMushroomTile(x,y){ // Draw mushroom emoji instead of custom drawing CTX.save(); CTX.font='48px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍄', x+TILE/2, y+TILE/2); CTX.restore(); } function drawBlock(x,y,c1,c2){ const g=CTX.createLinearGradient(x,y,x+TILE,y+TILE); g.addColorStop(0,c1); g.addColorStop(1,c2); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); CTX.strokeStyle='#0005'; CTX.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1); } function drawMedal(x,y){ CTX.save(); CTX.translate(x+TILE/2,y+TILE/2); CTX.fillStyle='#ffe34d'; CTX.beginPath(); CTX.arc(0,0,20,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#ffb347'; CTX.font='bold 18px var(--ui-font)'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('★',0,2); CTX.restore(); } // ============================================================= // MAIN LOOP // ============================================================= function loop(){ if(gameRunning){ update(); } requestAnimationFrame(loop); } requestAnimationFrame(loop); requestAnimationFrame(render); // ============================================================= // EXTRA: KEY EVENTS (PREVENT SCROLL) // ============================================================= window.addEventListener('keydown',e=>{ if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); }); // ============================================================= // FIREBALL & PLAYER SHOOT HOTKEY // ============================================================= document.addEventListener('keydown', e=>{ if(e.key==='F'||e.key==='f'){ shootFireball(); } }); // Polyfill roundRect if(!CanvasRenderingContext2D.prototype.roundRect){ CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){ if(!r) r=8; this.beginPath(); this.moveTo(x+r,y); this.lineTo(x+w-r,y); this.quadraticCurveTo(x+w,y,x+w,y+r); this.lineTo(x+w,y+h-r); this.quadraticCurveTo(x+w,y+h,x+w-r,y+h); this.lineTo(x+r,y+h); this.quadraticCurveTo(x,y+h,x,y+h-r); this.lineTo(x,y+r); this.quadraticCurveTo(x,y,x+r,y); return this; } } // ============================================================= // DEBUG SHORTCUTS (REMOVE IF YOU WANT PURE EXPERIENCE) // ============================================================= window.addEventListener('keydown',e=>{ if(e.key==='L'){ score+=500; showToast('+500'); } if(e.key==='N'){ nextLevel(); } }); </script> </body> </html>

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Jogging Woman Platform Quest</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> <style> :root { --ui-font: 'Trebuchet MS', 'Segoe UI', Roboto, sans-serif; --purple:#7b2ff7; --pink:#f107a3; --yellow:#ffe34d; --cyan:#26ffe6; --green:#3ddc97; --red:#ff355e; --blue:#2d8bff; --ink:#1d1d27; } html,body { margin:0; padding:0; background:linear-gradient(135deg,#110022,#330a4d,#0d1b4d); min-height:100vh; color:#fff; font-family:var(--ui-font); overflow:hidden; } canvas { image-rendering:pixelated; display:block; margin:0 auto; background:#222; box-shadow:0 0 0 4px #fff2 inset, 0 0 50px -10px #f107a3,0 0 120px -40px #26ffe6; border-radius:18px; } h1 { font-weight:700; letter-spacing:1px; margin:0 0 .4rem; font-size:2.4rem; text-shadow:0 4px 14px rgba(0,0,0,.6),0 0 12px #f107a3; } button { cursor:pointer; border:none; background:linear-gradient(90deg,var(--purple),var(--pink)); color:#fff; font-weight:600; font-size:1.05rem; padding:.85rem 1.8rem; border-radius:40px; box-shadow:0 6px 16px -4px rgba(0,0,0,.6),0 0 0 3px #ffffff10; backdrop-filter:blur(6px); transition:.3s; } button:hover { transform:translateY(-3px) scale(1.04); box-shadow:0 8px 22px -6px #f107a380,0 0 0 3px #ffffff40; } button:active { transform:translateY(1px) scale(.97); } a { color:var(--yellow); } .overlay { position:fixed; inset:0; display:flex; flex-direction:column; justify-content:center; align-items:center; background:radial-gradient(circle at 40% 30%, #442266bb,#110022ee 70%); z-index:10; text-align:center; padding:2rem; } .hidden { display:none !important; } .panel { max-width:880px; backdrop-filter:blur(10px); background:linear-gradient(160deg,#ffffff12,#ffffff05); border:1px solid #ffffff30; padding:2.2rem 2.6rem 2.6rem; border-radius:34px; box-shadow:0 14px 50px -10px #0008, 0 0 0 1px #ffffff20 inset; } .panel p { line-height:1.45; font-size:1.02rem; } .flex { display:flex; gap:1.1rem; flex-wrap:wrap; justify-content:center; } .pill { background:#ffffff10; padding:.35rem .9rem; border-radius:30px; font-size:.85rem; letter-spacing:.5px; box-shadow:0 0 0 1px #ffffff20 inset; } #hud { position:fixed; top:12px; left:50%; transform:translateX(-50%); display:flex; gap:2.5rem; font-weight:600; font-size:1.05rem; letter-spacing:.5px; z-index:5; text-shadow:0 0 6px #000,0 0 14px #000; } #hud .hideLives { display:none; } #hud span.label { opacity:.7; font-weight:500; margin-right:.4rem; } #hud .value { color:var(--yellow); } /* Boss Heart Bar */ #bossBarWrap { position:fixed; top:56px; left:50%; transform:translateX(-50%); display:none; z-index:8; } #bossBarContainer { display:flex; align-items:center; gap:14px; padding:10px 18px 14px; background:linear-gradient(135deg,#22000cdd,#3d0015cc); border:1px solid #ff5a5a70; border-radius:40px; box-shadow:0 10px 28px -10px #000a,0 0 0 2px #ffffff10 inset; backdrop-filter:blur(8px); } .bossHeart { position:relative; width:54px; height:54px; } .bossHeart:before, .bossHeart:after { content:''; position:absolute; top:12px; left:18px; width:18px; height:28px; background:#ff224d; border-radius:12px 12px 8px 8px; box-shadow:0 0 0 2px #990021 inset,0 0 10px #ff224d88; } .bossHeart:after { left:0; } .bossHeart:before { left:18px; } .bossHeart .shine { position:absolute; top:16px; left:6px; width:12px; height:12px; border-radius:50%; background:radial-gradient(circle at 30% 30%,#fff8,#fff0 80%); } #bossHpText { position:absolute; bottom:-12px; left:50%; transform:translateX(-50%); font-size:.65rem; letter-spacing:1px; font-weight:700; color:#fff; text-shadow:0 0 6px #000; font-family:var(--ui-font); } #bossBarOuter { position:relative; width:560px; height:30px; background:#1a0c12; border:2px solid #5d1a2b; border-radius:16px; overflow:hidden; box-shadow:0 0 0 2px #ffffff10 inset, 0 6px 16px -8px #000; } #bossBar { position:absolute; inset:0; width:100%; height:100%; background:linear-gradient(90deg,#28d657,#1ea94e); transition:width .3s, background .4s; box-shadow:0 0 18px -6px #ff5f6daa inset, 0 0 14px -4px #ffe066aa; } #bossBarGlow { pointer-events:none; position:absolute; inset:0; mix-blend-mode:screen; background:radial-gradient(circle at 25% 50%,#ffffff66,transparent 70%); } .toast { position:fixed; bottom:18px; left:50%; transform:translateX(-50%) translateY(50px); padding:.75rem 1.4rem; background:linear-gradient(90deg,#ff5f6d,#ffc371); color:#111; font-weight:600; border-radius:40px; box-shadow:0 8px 30px -10px #000a; opacity:0; transition:.5s; pointer-events:none; font-size:.9rem; letter-spacing:.5px; } .toast.show { opacity:1; transform:translateX(-50%) translateY(0); } .gameover h2 { font-size:2.2rem; margin:0 0 .6rem; text-shadow:0 0 18px #ff355e; } .victory h2 { text-shadow:0 0 18px #3ddc97; } .statline { font-size:1.05rem; margin:.3rem 0 .9rem; } .key { font-family:monospace; background:#00000040; padding:.15rem .45rem; border-radius:7px; margin:0 .15rem; box-shadow:0 0 0 1px #ffffff30 inset; } .credits { margin-top:1.8rem; opacity:.6; font-size:.75rem; } @media (max-width:900px){ h1 { font-size:1.9rem; } .panel { padding:1.7rem 1.4rem 2rem; } #hud{ gap:1.1rem; font-size:.9rem;} } @keyframes pulseBar { 0%{filter:brightness(1);} 50%{filter:brightness(1.7) saturate(1.3);} 100%{filter:brightness(1);} } </style> </head> <body> <div id="hud"> <div><span class="label">Score</span><span id="score" class="value">0</span></div> <div class="hideLives"><span class="label">Lives</span><span id="lives" class="value">3</span></div> <div><span class="label">World</span><span id="world" class="value">1-1</span></div> <div><span class="label">Power</span><span id="power" class="value">Normal</span></div> <div><span class="label">Veggies</span><span id="veggies" class="value">0/3</span></div> <div><span class="label">Junk</span><span id="junk" class="value">0/2</span></div> </div> <div id="bossBarWrap"><div id="bossBarContainer"><div class="bossHeart"><div class="shine"></div><div id="bossHpText"></div></div><div id="bossBarOuter"><div id="bossBar"></div><div id="bossBarGlow"></div></div></div></div> <div id="toast" class="toast">Power Up!</div> <div id="startScreen" class="overlay"> <div class="panel"> <h1>A Jogging Woman<br><span style="font-size:1.05rem;font-weight:500;letter-spacing:3px; display:inline-block; margin-top:.4rem; background:linear-gradient(90deg,var(--yellow),var(--pink)); -webkit-background-clip:text; background-clip:text; color:transparent;">Quest for the Golden Medal</span></h1> <p>Run, jump & stomp junk food enemies. Collect <strong>healthy power-ups</strong> to grow strong and blaze through each world. Reach the <strong>McDonald's</strong> at the end and defeat the <strong>Big Burger Boss</strong> to earn the medal!</p> <div class="flex" style="margin:1.2rem 0 1.1rem;"> <span class="pill">Move: <span class="key">←</span><span class="key">→</span> or <span class="key">A</span><span class="key">D</span></span> <span class="pill">Jump: <span class="key">Space</span>/<span class="key">W</span>/<span class="key">↑</span></span> <span class="pill">Fireball: <span class="key">F</span></span> <span class="pill">Run: <span class="key">Shift</span></span> <span class="pill">Pause: <span class="key">P</span></span> </div> <p style="margin:.2rem 0 1.1rem;">Power-Ups: <strong>Super Mushroom</strong> (Grow & extra hit) · <strong>Fire Flower</strong> (Shoot fireballs) · <strong>Starman</strong> (Invincible!). Hidden blocks & secret areas reward explorers.</p> <button id="btnStart">Start Adventure</button> <div class="credits">All in one HTML file. Sounds generated via the Web Audio API.</div> </div> </div> <div id="gameOver" class="overlay hidden gameover"> <div class="panel"> <h2 id="gameOverTitle">Game Over</h2> <div id="finalStats" class="statline">Score: 0</div> <button id="btnRestart">Restart</button> </div> </div> <!-- Increased game canvas size for a bigger viewport --> <canvas id="game" width="1600" height="900"></canvas> <script> // ============================================================= // CONFIG & CONSTANTS // ============================================================= const CANVAS = document.getElementById('game'); const CTX = CANVAS.getContext('2d'); const W = CANVAS.width, H = CANVAS.height; const TILE = 48; // Base tile size let pixelScale = 1; // dynamic for crispness // Disable smoothing for crisp pixel look CTX.imageSmoothingEnabled = false; // Player states const POWER = { NORMAL:'Normal', BIG:'Big', FIRE:'Fire', STAR:'Star' }; // Colors per world / theme backgrounds const THEMES = [ { name:'School', bg:['#2d006e','#351b8a','#0d225f'], gradient:'linear-gradient(135deg,#4422aa,#110033)', water:false }, { name:'Horse Stable', bg:['#1a3d0a','#165a24','#072b12'], gradient:'linear-gradient(135deg,#0f5c25,#072b12)', water:false }, { name:'Underwater', bg:['#001d3d','#003566','#001845'], gradient:'linear-gradient(135deg,#003566,#001845)', water:true }, { name:"McDonald's", bg:['#4d0011','#6d001e','#330008'], gradient:'linear-gradient(135deg,#6d001e,#1a0006)', water:false } ]; // Mushroom bounce tuning constants (higher values = higher bounce) const MUSHROOM_BOUNCE_MULT = 1.8; // lower mushroom bounce height const MUSHROOM_MAX_FALL_BONUS = 0.0; // no fall bonus (consistent) const MUSHROOM_FALL_DIVISOR = 14; // (unused now) // Sound generator (very small synth) const AudioCtx = window.AudioContext || window.webkitAudioContext; const audio = new AudioCtx(); function playTone(freq=440, dur=0.12, type='square', vol=0.2, sweep=0){ const t = audio.currentTime; const osc = audio.createOscillator(); const gain = audio.createGain(); osc.frequency.setValueAtTime(freq,t); if(sweep) osc.frequency.exponentialRampToValueAtTime(Math.max(40,freq+sweep), t+dur); osc.type = type; gain.gain.setValueAtTime(vol,t); gain.gain.exponentialRampToValueAtTime(0.0001, t+dur); osc.connect(gain).connect(audio.destination); osc.start(t); osc.stop(t+dur); } function chord(f1,f2,f3,d=0.25){ playTone(f1,d,'sawtooth',0.12); playTone(f2,d,'square',0.08); playTone(f3,d,'triangle',0.1); } // Global game state let gameRunning=false, paused=false, frame=0; let currentLevelIndex=0; let worldTimer=0; let score=0; let lives=3; let startLives=3; let showBossBar=false; let boss=null; let medalCollected=false; let win=false; let vegetablesCollected=0; let vegetablesNeeded=5; let veggieAmmo=0; // ammo from vegetables (used in boss level) // Per-level food spawn caps (exact: 3 vegetables + 2 junk) let levelVegSpawned=0, levelJunkSpawned=0; // Player object const player = { x:120, y:0, w:28, h:38, vx:0, vy:0, onGround:false, facing:1, power:POWER.NORMAL, fireCooldown:0, starTimer:0, grown:false, prevPower:null, hurtTimer:0, invincibleTimer:0, runHeld:false, bounceCooldown:0, bounceBoostWindow:0, usedBounceBoost:false, bounceAssistFrames:0 }; // Entities arrays const enemies=[], particles=[], powerUps=[], fireballs=[], veggieShots=[]; // dynamic arrays // Input const keys={}; window.addEventListener('keydown',e=>{ keys[e.key.toLowerCase()]=true; if(e.key==='p'||e.key==='P'){ paused=!paused; } }); window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; }); // Utility const rand=(a,b)=>Math.random()*(b-a)+a; const clamp=(v,a,b)=>v<a?a:v>b?b:v; // HUD elements const elScore=document.getElementById('score'); const elLives=document.getElementById('lives'); const elWorld=document.getElementById('world'); const elPower=document.getElementById('power'); const elVeggies=document.getElementById('veggies'); const elAmmo=document.createElement('span'); // placeholder (will be injected if not already in HUD) const elJunk=document.getElementById('junk'); const toast=document.getElementById('toast'); const bossBarWrap=document.getElementById('bossBarWrap'); const bossBar=document.getElementById('bossBar'); const bossHpText=document.getElementById('bossHpText'); // Insert ammo HUD if missing if(!document.getElementById('ammo')){ const ammoWrap=document.createElement('div'); ammoWrap.innerHTML='<span class="label">Ammo</span><span id="ammo" class="value">0</span>'; document.getElementById('hud').appendChild(ammoWrap); } const elAmmoVal=document.getElementById('ammo'); // UI overlay elements const startScreen=document.getElementById('startScreen'); const gameOverScreen=document.getElementById('gameOver'); const finalStats=document.getElementById('finalStats'); const gameOverTitle=document.getElementById('gameOverTitle'); document.getElementById('btnStart').onclick=()=>{ startScreen.classList.add('hidden'); initGame(); const canvas=document.getElementById('game'); if(canvas.requestFullscreen){canvas.requestFullscreen().catch(err=>console.log('Fullscreen failed:',err));} else if(canvas.webkitRequestFullscreen){canvas.webkitRequestFullscreen();} else if(canvas.mozRequestFullScreen){canvas.mozRequestFullScreen();} else if(canvas.msRequestFullscreen){canvas.msRequestFullscreen();} }; document.getElementById('btnRestart').onclick=()=>{ gameOverScreen.classList.add('hidden'); initGame(); }; function showToast(msg){ toast.textContent=msg; toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'),1500); } // ============================================================= // LEVEL DATA (Tile maps) '.' empty, '#' block, '=' platform, '?' question, 'E' enemy, 'H' hidden, 'P' pipe, 'M' medal, 'B' boss spawn // ============================================================= // Keep widths moderate; we will scroll camera. const LEVELS=[ { theme:0, name:'1-1', map:[ '................................................', '................................................', '.................?..............................', '...............V................................', '.............E..................................', '..........###...............?.......E...........', '....................V...................#####..........', '.....?...............V.................?........', '?###########..####..###########################.' ]}, { theme:1, name:'2-1', map:[ '................................................', '................................................', '..............E.........?.......................', '.....................................H..........', '..........###...................................', '......................E.........V...............', '......?..........................................', '....................####......?..V.........?...', '?########..###################..###############.' ]}, { theme:2, name:'3-1', map:[ // Underwater: slower gravity '................................................', '................................................', '.....E...........?...............E..............', '......................E..........................', '................................................', '............?.................?.......V.........', '..................####..........V..............M', '......................###.......................', '?##############..#############..################' ]}, { theme:3, name:"Closet Boss", map:[ '######################', '#....................#', '#.........B..........#', '#....................#', '#....?........?......#', '#........M...........#', '#....V...........V...#', '#.........E..........#', '######################' ]} ]; // Camera const camera={x:0,y:0}; // Decorative elements (clouds & hills) regenerated per level let clouds=[], hills=[]; // ------------------------------------------------------------- // IMAGE / SPRITES // ------------------------------------------------------------- // Attempt to load user-provided jogging woman image first, then fallback to runner.png, then pixel hero. const runnerImg=new Image(); const bossImg=new Image(); bossImg.src='https://static.wixstatic.com/media/0a0716_41866c4f4d484d4695b20de873d5d048~mv2.webp'; // Load boss image from Wix const logoImg=new Image(); logoImg.src='https://static.wixstatic.com/media/0a0716_f535650c4f524797add9e64460c3afb4~mv2.png'; // Load project logo for background pattern from Wix const runnerSources=['jogging_woman.png','runner.png']; // Place your provided image as jogging_woman.png in the same folder. let runnerFrames=6; // default expected frames if a sheet is used let runnerLoaded=false; let currentRunnerSourceIndex=0; function loadRunner(){ if(currentRunnerSourceIndex>=runnerSources.length){ runnerLoaded=false; return; } runnerImg.src=runnerSources[currentRunnerSourceIndex]; } runnerImg.onload=()=>{ runnerLoaded=true; // Heuristic: if width is not significantly wider than height, assume single frame if(runnerImg.width <= runnerImg.height*1.3) runnerFrames=1; }; runnerImg.onerror=()=>{ currentRunnerSourceIndex++; loadRunner(); }; loadRunner(); // Draw hero with state function drawPlayer(){ const p=player; CTX.save(); CTX.translate(Math.floor(p.x-camera.x), Math.floor(p.y-camera.y)); // Flicker during invincibility / hurt if(p.hurtTimer>0 && (frame%6<3)) { CTX.restore(); return; } let scale= p.power!==POWER.NORMAL?1.2:1; if(p.power===POWER.STAR) { const hue=(frame*6)%360; CTX.filter=`hue-rotate(${hue}deg)`; } if(runnerLoaded){ const fw=runnerImg.width/runnerFrames; const fh=runnerImg.height; let animIndex=0; if(runnerFrames>1){ animIndex = p.onGround? Math.floor(frame/6)%runnerFrames : 0; } else { // Single-frame sprite: fake a subtle bob while running const bob = p.onGround? Math.sin(frame*0.35)*2 : 0; CTX.translate(0,bob); } CTX.scale(p.facing*scale, scale); CTX.drawImage(runnerImg, animIndex*fw,0,fw,fh, -fw/2, -fh+4, fw, fh); } else { // Simple fallback pixel body CTX.scale(p.facing*scale, scale); CTX.fillStyle='#fff'; CTX.fillRect(-14,-40,28,40); CTX.fillStyle='#ff3566'; CTX.fillRect(-14,-14,28,14); CTX.fillStyle='#222'; CTX.fillRect(-10,-34,8,8); CTX.fillRect(2,-34,8,8); } CTX.restore(); // Life bar above head (hearts style) CTX.save(); const barW=120, barH=14; const px = Math.floor(p.x - camera.x - barW/2); const py = Math.floor(p.y - camera.y - p.h - 26); // Outline CTX.fillStyle='#fff'; CTX.fillRect(px-2,py-2,barW+4,barH+4); CTX.fillStyle='#111'; CTX.fillRect(px,py,barW,barH); // Fill based on initial starting lives so bar begins full green const ratio = clamp(lives/startLives,0,1); const grad=CTX.createLinearGradient(px,py,px+barW,py); if(ratio>0.66){ grad.addColorStop(0,'#30d158'); grad.addColorStop(1,'#0b8d2b'); } else if(ratio>0.33){ grad.addColorStop(0,'#ffb347'); grad.addColorStop(1,'#ff8c00'); } else { grad.addColorStop(0,'#ff5f56'); grad.addColorStop(1,'#c91919'); } CTX.fillStyle=grad; CTX.fillRect(px,py, barW*ratio, barH); // Pixel heart icon at left drawHeart(px-26, py-4, 18); CTX.restore(); } // Small pixel heart drawing helper function drawHeart(x,y,size){ const s=size/8; // 8x7 grid shape CTX.save(); CTX.translate(x,y); CTX.scale(s,s); CTX.fillStyle='#ff002b'; const pixels=[ '01100110', '11111111', '11111111', '11111111', '01111110', '00111100', '00011000' ]; for(let r=0;r<pixels.length;r++){ for(let c=0;c<pixels[r].length;c++) if(pixels[r][c]==='1') CTX.fillRect(c,r,1,1); } CTX.restore(); } // Entity spawners ------------------------------------------------ function spawnEnemy(x,y){ // Underwater world (theme 2) spawns aquatic enemies const lvl=LEVELS[currentLevelIndex]; if(lvl && lvl.theme===2){ const aquatic = Math.random()<0.75? 'fish':'shark'; const w = aquatic==='shark'?56:42; const h = aquatic==='shark'?32:26; enemies.push({x,y,w,h,vx:rand(-1.2,1.2)||0.6,vy:0,dead:false,type:aquatic,frameOff:Math.random()*120}); } else { enemies.push({x,y,w:30,h:30,vx:rand(-0.6,0.6),vy:0,dead:false,type:'junk',frameOff:Math.random()*60}); } } function spawnPowerUp(x,y){ // random selection const r=Math.random(); let kind='mushroom'; if(r>0.66) kind='flower'; else if(r>0.33) kind='star'; powerUps.push({x,y,w:28,h:28,vy: -2, kind, bounce:0}); } // Spawn food with fixed per-level limits: exactly 5 vegetables and 3 junk items total. function spawnVegetable(x,y){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const junk = ['🍔','🍩','🍟','🍕','🌭','🧁']; if(levelVegSpawned+levelJunkSpawned >=8) return; // already reached per-level total (5+3) let kind, food; if(levelVegSpawned < 5){ // Prioritize spawning vegetables until we have 5 food = veggies[Math.floor(Math.random()*veggies.length)]; kind='veggie'; levelVegSpawned++; } else if(levelJunkSpawned < 3){ food = junk[Math.floor(Math.random()*junk.length)]; kind='junk'; levelJunkSpawned++; } else return; // safety (should be caught by first check) powerUps.push({x,y,w:28,h:28,vy:-2, kind, veggie:food, bounce:0}); } // Free spawn (no upward velocity). Uses same caps/logic. function spawnFreeVegetable(x,y){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const junk = ['🍔','🍩','🍟','🍕','🌭','🧁']; if(levelVegSpawned+levelJunkSpawned >=8) return; let kind, food; if(levelVegSpawned < 5){ food = veggies[Math.floor(Math.random()*veggies.length)]; kind='veggie'; levelVegSpawned++; } else if(levelJunkSpawned < 3){ food = junk[Math.floor(Math.random()*junk.length)]; kind='junk'; levelJunkSpawned++; } else return; powerUps.push({x,y,w:28,h:28,vy:0, kind, veggie:food, bounce:0, free:true}); } // Ensure each level always has 5 vegetables even if the map had fewer 'V' markers. function ensureVegetableQuota(){ const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; while(levelVegSpawned < 5){ const food = veggies[Math.floor(Math.random()*veggies.length)]; // Place them near the start area but spaced so they don't overlap const x = 200 + levelVegSpawned * 70; // world coordinates const y = 140; // mid-air so player sees them powerUps.push({x,y,w:28,h:28,vy:0, kind:'veggie', veggie:food, bounce:0, free:true}); levelVegSpawned++; } } // Fireball function shootFireball(){ if(player.power!==POWER.FIRE || player.fireCooldown>0) return; player.fireCooldown=24; playTone(680,0.08,'square',0.15,-300); fireballs.push({x:player.x+player.facing*20, y:player.y-30, vx:player.facing*6, vy:-1.2, life:180}); } // Veggie projectile (unlimited ammo). Uses vegetables as projectiles. function shootVeggie(){ // Only check cooldown (unlimited ammo) if(player.veggieCooldown>0) return; player.veggieCooldown = 16; // short cooldown between veggie throws const veggies = ['🥦','🍎','🥕','🍓','🥑','🍉']; const char = veggies[Math.floor(Math.random()*veggies.length)]; const speed = 6.2; // Slight upward arc start - added w and h for collision detection veggieShots.push({x:player.x+player.facing*22, y:player.y-34, vx:player.facing*speed, vy:-2.4, life:140, char, w:28, h:28}); // Fun throw sound (two quick tones) playTone(520,0.07,'triangle',0.18,160); playTone(760,0.06,'square',0.14,200); // Throw particles (green-ish) for(let i=0;i<6;i++) particles.push({x:player.x+rand(-8,8), y:player.y-38, vx:rand(-1.2,1.2), vy:rand(-2.4,-0.6), life:14, color:'#6aff6a'}); } // ============================================================= // GAME INITIALIZATION / RESET // ============================================================= function initGame(){ score=0; lives=3; startLives=lives; currentLevelIndex=0; win=false; medalCollected=false; boss=null; vegetablesCollected=0; loadLevel(currentLevelIndex); updateHUD(); player.power=POWER.NORMAL; player.starTimer=0; player.prevPower=null; player.hurtTimer=0; player.invincibleTimer=0; gameRunning=true; paused=false; frame=0; veggieAmmo=0; veggieShots.length=0; player.veggieCooldown=0; } function loadLevel(i){ const lvl=LEVELS[i]; enemies.length=0; powerUps.length=0; fireballs.length=0; particles.length=0; boss=null; showBossBar=false; bossBarWrap.style.display='none'; vegetablesCollected=0; // reset vegetable count for new level levelVegSpawned=0; levelJunkSpawned=0; // reset per-level food counters veggieShots.length=0; veggieAmmo=0; const map=lvl.map; // spawn enemies & power-ups from map symbols for(let r=0;r<map.length;r++){ for(let c=0;c<map[r].length;c++){ const ch=map[r][c]; if(ch==='E') spawnEnemy(c*TILE+TILE/2,(r*TILE)+10); if(ch==='B') boss={x:c*TILE+TILE/2,y:r*TILE+10,w:68,h:68,vx:1.4,vy:0,health:7,attackTimer:240,grounded:false,phase:0}; if(ch==='V') spawnFreeVegetable(c*TILE+TILE/2, r*TILE+TILE/2); if(ch==='?') {/* question blocks spawn when hit */} } } player.x=100; player.y=0; camera.x=0; worldTimer=0; elWorld.textContent=lvl.name; document.body.style.background=THEMES[lvl.theme].gradient; // Adjust spawn & camera for compact closet boss room so player isn't hidden in ceiling if(lvl.name.includes('Closet')){ player.x = TILE*4.5; // center-ish horizontally player.y = TILE*5; // below medal / inside room camera.x = Math.max(0, (lvl.map[0].length*TILE - W)/2); // center room if narrower than canvas // If still inside a solid tile, nudge downward until free (safety loop) let safety=0; while(collideSolid(player) && safety<40){ player.y += 2; safety++; } } // After initial spawns, guarantee vegetable quota ensureVegetableQuota(); initDecor(); } // Create clouds & hills to mimic classic platform sky while remaining original assets function initDecor(){ clouds=[]; hills=[]; const lvl=LEVELS[currentLevelIndex]; const width= lvl.map[0].length*TILE; // Hills (large rounded rectangles) for(let i=0;i<8;i++){ const w= rand(180,360); const h= rand(120,240); hills.push({x: rand(0,width-w), y: H - h - 120, w, h, layer: i%2, shade: `hsl(${rand(180,220)},40%,${i%2?55:65}%)`}); } // Clouds for(let i=0;i<14;i++){ clouds.push({x:rand(0,width), y:rand(40,260), speed:rand(0.1,0.35), scale:rand(0.7,1.4)}); } } // ============================================================= // TILE / COLLISION HELPERS // ============================================================= function getTile(x,y){ const lvl=LEVELS[currentLevelIndex]; const map=lvl.map; if(y<0) return '.'; const r=Math.floor(y/TILE); const c=Math.floor(x/TILE); if(r<0||r>=map.length||c<0||c>=map[0].length) return '.'; return map[r][c]; } function isSolid(ch){ return ch==='#' || ch==='=' || ch==='P' || ch==='?'; } function isQuestion(ch){ return ch==='?'; } function isMedal(ch){ return ch==='M'; } function breakQuestion(r,c){ const lvl=LEVELS[currentLevelIndex]; const row=lvl.map[r]; lvl.map[r]= row.substring(0,c)+'#'+row.substring(c+1); spawnVegetable(c*TILE+TILE/2, r*TILE-4); playTone(880,0.15,'square',0.18, -100); score+=50; } function revealHidden(r,c){ const lvl=LEVELS[currentLevelIndex]; const row=lvl.map[r]; lvl.map[r]= row.substring(0,c)+'?'+row.substring(c+1); spawnVegetable(c*TILE+TILE/2, r*TILE-4); playTone(880,0.15,'square',0.18, -100); score+=50; } // ============================================================= // UPDATE LOOP // ============================================================= function update(){ if(!gameRunning||paused) return; frame++; worldTimer++; const lvl=LEVELS[currentLevelIndex]; const theme=THEMES[lvl.theme]; // Player input const left = keys['arrowleft']||keys['a']; const right=keys['arrowright']||keys['d']; const jump = keys['arrowup']||keys['w']||keys[' ']; const runKey=keys['shift']; player.runHeld=runKey; const maxSpeed = runKey?4.2:3.1; const accel= runKey?0.45:0.35; if(left && !right) { player.vx = Math.max(player.vx - accel, -maxSpeed); player.facing=-1; } else if(right && !left) { player.vx = Math.min(player.vx + accel, maxSpeed); player.facing=1; } else { player.vx *=0.75; if(Math.abs(player.vx)<0.05) player.vx=0; } if(player.fireCooldown>0) player.fireCooldown--; if(player.veggieCooldown>0) player.veggieCooldown--; if(player.starTimer>0){ player.starTimer--; if(player.starTimer%15===0) playTone( rand(600,1200),0.05,'square',0.05); if(player.starTimer===0){ if(player.prevPower) player.power=player.prevPower; updateHUD(); } } if(player.hurtTimer>0) player.hurtTimer--; if(player.invincibleTimer>0) player.invincibleTimer--; if(player.power===POWER.STAR) player.invincibleTimer=10; // always invincible while star // Gravity const grav = theme.water? 0.15 : 0.55; const jumpVel = theme.water? -5.6 : -8.9; // slightly higher normal jump player.vy += grav; if(player.vy>16) player.vy=16; if(jump && player.onGround){ player.vy=jumpVel; player.onGround=false; playTone(300,0.22,'square',0.25,300); } // Shooting: // F key = fireball (if Fire power) or veggie (unlimited) // Shift while boss present = veggie (unlimited) if(keys['f']){ if(player.power===POWER.FIRE) shootFireball(); else shootVeggie(); } if(boss && runKey){ shootVeggie(); // unlimited veggie ammo in boss level } // Movement & collision (horizontal then vertical) const lastVy = player.vy; // store fall speed before movement for bounce strength bonus moveEntity(player); // Camera follows camera.x = clamp(player.x - W/2, 0, lvl.map[0].length*TILE - W); // Mushroom bounce (two-stage trampoline) if(player.bounceCooldown>0) player.bounceCooldown--; if(player.onGround){ const belowTile = getTile(player.x, player.y + player.h/2 + 2); if(belowTile==='?' && player.bounceCooldown<=0){ const baseJump = theme.water? -5.6 : -8.9; // match slightly higher normal jump // Higher bounce in world 2-1 const bounceMult = lvl.name==='2-1' ? 2.6 : MUSHROOM_BOUNCE_MULT; player.vy = baseJump * bounceMult; // big bounce player.onGround=false; player.bounceCooldown=16; player.bounceBoostWindow=0; player.usedBounceBoost=true; player.bounceAssistFrames=10; for(let i=0;i<22;i++) particles.push({x:player.x+rand(-18,18), y:player.y+player.h/2-8, vx:rand(-1.6,1.6), vy:rand(-6,-1.5), life:24, color:i%3? '#ffeb7a':'#ffd54f'}); playTone(820,0.22,'square',0.3,340); playTone(1230,0.18,'triangle',0.18,-260); showToast('Super Jump!'); } } // Apply bounce assist (counter gravity a bit to emphasize height) if(player.bounceAssistFrames>0){ player.vy -= 0.35; // small upward push player.bounceAssistFrames--; } // Optional higher boost if jump is pressed within window // (Super bounce removed for simple double height) // Fall death check (below level bounds) if(player.y - player.h/2 > lvl.map.length*TILE + 160){ fallDeath(); return; // skip rest of update this frame } // Fireballs for(let i=fireballs.length-1;i>=0;i--){ const fb=fireballs[i]; fb.life--; fb.x+=fb.vx; fb.y+=fb.vy; fb.vy+=0.25; if(fb.life<=0) fireballs.splice(i,1); else { // collide with tiles if(isSolid(getTile(fb.x, fb.y))) { particles.push({x:fb.x,y:fb.y,life:16,color:'#ffef8d'}); fireballs.splice(i,1); continue; } // collide with enemies for(const e of enemies){ if(!e.dead && aabb(fb,e)){ killEnemy(e,true); fireballs.splice(i,1); break; } } if(boss && aabb(fb,boss)){ damageBoss(); fireballs.splice(i,1); } } } // Veggie shots (projectiles using collected vegetables) for(let i=veggieShots.length-1;i>=0;i--){ const vs=veggieShots[i]; vs.life--; vs.x+=vs.vx; vs.y+=vs.vy; vs.vy+=0.32; // gravity if(vs.life<=0){ veggieShots.splice(i,1); continue; } // Tile collision ends projectile if(isSolid(getTile(vs.x, vs.y))){ particles.push({x:vs.x,y:vs.y,life:16,color:'#32cd32'}); veggieShots.splice(i,1); continue; } // Enemy collision let hit=false; for(const e of enemies){ if(!e.dead && aabb(vs,e)){ killEnemy(e,true); hit=true; break; } } if(hit){ veggieShots.splice(i,1); continue; } // Boss collision if(boss && aabb(vs,boss)){ damageBoss(); particles.push({x:vs.x,y:vs.y-20,life:18,color:'#4aff4a'}); veggieShots.splice(i,1); continue; } } // Enemies update for(const e of enemies){ if(e.dead){ e.vy+=0.6; e.y+=e.vy; continue; } if(LEVELS[currentLevelIndex].theme===2 && (e.type==='fish'||e.type==='shark')){ // Aquatic motion: sinusoidal vertical swim const swimSpeed = e.type==='shark'?0.045:0.07; const amp = e.type==='shark'?10:16; e.y += Math.sin((frame+e.frameOff)*swimSpeed)*0.9; e.x += e.vx * (e.type==='shark'?1.25:0.9); // Turn when near horizontal bounds (camera independent using map width) if(e.x<40){ e.x=40; e.vx=Math.abs(e.vx); } if(e.x>LEVELS[currentLevelIndex].map[0].length*TILE-40){ e.x=LEVELS[currentLevelIndex].map[0].length*TILE-40; e.vx=-Math.abs(e.vx); } } else { // Land enemy physics (burgers don't fall off platforms) e.vy+=0.5; // Check if about to walk off a platform (look ahead) const checkX = e.x + e.vx*8; const checkY = e.y + e.h/2 + 8; // slightly below feet const groundAhead = isSolid(getTile(checkX, checkY)); if(!groundAhead){ e.vx*=-1; } // turn around if no ground ahead e.x+=e.vx; e.y+=e.vy; if(isSolid(getTile(e.x+e.vx*4, e.y))){ e.vx*=-1; } if(isSolid(getTile(e.x, e.y+e.h/2))){ e.y = Math.floor((e.y+e.h/2)/TILE)*TILE - e.h/2; e.vy=0; } } // Player collision / interaction if(player.starTimer>0 && !e.dead && aabb(player,e)){ killEnemy(e,true); continue; } if(!e.dead && aabb(player,e)){ if(player.vy>2 && player.y < e.y-10){ killEnemy(e,false); player.vy=-8; score+=100; playTone(700,0.15,'square',0.22); } else if(player.invincibleTimer<=0){ damagePlayer(); } } } // PowerUps for(let i=powerUps.length-1;i>=0;i--){ const p=powerUps[i]; if(!p.free) p.vy+=0.4; // only apply gravity to non-free vegetables p.y+=p.vy; if(p.y>H+200){ powerUps.splice(i,1); continue; } if(aabb(player,p)){ applyPowerUp(p.kind); powerUps.splice(i,1); continue; } } // Boss logic if(boss){ showBossBar=true; bossBarWrap.style.display='block'; const levelWidthPx = LEVELS[currentLevelIndex].map[0].length * TILE; // Different physics if in closet room: keep boss hovering on floor and never falling off if(LEVELS[currentLevelIndex].name.includes('Closet')){ // Lock vertical motion to floor level (second to last row inside room) const floorY = (LEVELS[currentLevelIndex].map.length-2)*TILE - boss.h/2; // inside boundary boss.vy = 0; boss.y = floorY; boss.grounded=true; boss.x += boss.vx; // horizontal patrol only } else { // Original gravity based motion boss.vy+=0.5; boss.x+=boss.vx; boss.y+=boss.vy; if(isSolid(getTile(boss.x, boss.y+boss.h/2))){ boss.y=Math.floor((boss.y+boss.h/2)/TILE)*TILE - boss.h/2; boss.vy=0; boss.grounded=true; } else boss.grounded=false; } // Clamp inside level so boss (burger) never leaves floor/edges const minX = boss.w/2 + 16; const maxX = levelWidthPx - boss.w/2 - 16; if(boss.x < minX){ boss.x = minX; boss.vx = Math.abs(boss.vx); } if(boss.x > maxX){ boss.x = maxX; boss.vx = -Math.abs(boss.vx); } if(boss.x<camera.x+50 || boss.x>camera.x+W-50) boss.vx*=-1; // Attack pattern boss.attackTimer--; if(boss.attackTimer<=0){ boss.attackTimer=rand(140,220); // spawn minion for(let j=0;j<3;j++) setTimeout(()=>spawnEnemy(boss.x+rand(-30,30), boss.y), j*200); playTone(200,0.4,'sawtooth',0.3,-70); } // collision player if(player.starTimer>0 && aabb(player,boss)){ damageBoss(true); } else if(aabb(player,boss)){ // Allow jumping on boss from above (easier threshold) if(player.vy>1 && player.y < boss.y-10){ player.vy=-10; // bounce up damageBoss(); score+=100; playTone(700,0.15,'square',0.22); } else if(player.invincibleTimer<=0){ damagePlayer(); } } const ratio = boss.health/7; bossBar.style.width=(ratio*100)+'%'; // Color shifts from green -> yellow -> orange -> red let col1='#28d657', col2='#1ea94e'; if(ratio<0.65 && ratio>=0.4){ col1='#ffd53b'; col2='#ffb347'; } else if(ratio<0.4 && ratio>=0.2){ col1='#ff8f2b'; col2='#ff5d00'; } else if(ratio<0.2){ col1='#ff2d2d'; col2='#b80000'; } bossBar.style.background=`linear-gradient(90deg,${col1},${col2})`; if(bossHpText) bossHpText.textContent = boss.health+"/7"; } // Medal detection (Victory) if(!win && !boss && lvl.name.startsWith('4') && medalCollected){ winGame(); } // Cleanup & particle update for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life--; p.x+=p.vx||0; p.y+=p.vy||0; if(p.life<=0) particles.splice(i,1); } // Level end (touch medal tile) or proceed after right edge reached for earlier levels const rightEdge = lvl.map[0].length*TILE - 100; if(player.x> rightEdge && currentLevelIndex < LEVELS.length-1 && !boss){ nextLevel(); // proceed to next level without vegetable requirement } updateHUD(); } function aabb(a,b){ return Math.abs(a.x - b.x) < (a.w + b.w)/2 && Math.abs(a.y - b.y) < (a.h + b.h)/2; } function moveEntity(p){ // horizontal p.x += p.vx; // sample corners horizontally if(collideSolid(p)){ p.x -= p.vx; while(!collideSolid(p)){ p.x += Math.sign(p.vx)*0.5; } p.x -= Math.sign(p.vx)*0.5; p.vx=0; } p.y += p.vy; const wasGround=p.onGround; p.onGround=false; if(collideSolid(p)){ p.y -= p.vy; while(!collideSolid(p)){ p.y += Math.sign(p.vy)*0.5; } p.y -= Math.sign(p.vy)*0.5; if(p.vy>0) { p.onGround=true; } p.vy=0; } // Block head bump (question blocks / hidden) when going upward if(p.vy<0){ const topTileY = p.y - p.h/2 -1; const left = p.x - p.w/2 +4; const right = p.x + p.w/2 -4; for(const tPos of [left,right]){ const r=Math.floor(topTileY/TILE); const c=Math.floor(tPos/TILE); const ch=getTile(tPos,topTileY); if(ch==='H'){ revealHidden(r,c); playTone(520,0.2,'square',0.15); } if(ch==='?'){ breakQuestion(r,c); } // now mushrooms can be hit from below too } } // Medal tile if(isMedal(getTile(p.x,p.y))){ medalCollected=true; showToast('You got the Medal! Defeat the Boss!'); } } function collideSolid(p){ // check a few points const hw=p.w/2, hh=p.h/2; const pts=[[p.x-hw+4,p.y-hh],[p.x+hw-4,p.y-hh],[p.x-hw+4,p.y+hh],[p.x+hw-4,p.y+hh]]; for(const [x,y] of pts){ if(isSolid(getTile(x,y))) return true; } return false; } // ============================================================= // POWER UP & DAMAGE // ============================================================= function applyPowerUp(kind){ if(kind==='veggie'){ // Healthy vegetables: points + life (capped at 9) score+=100; vegetablesCollected++; if(lives < 9){ lives++; showToast(`+1 Life & Healthy! (${vegetablesCollected}/${vegetablesNeeded})`); } else { showToast(`Healthy! (${vegetablesCollected}/${vegetablesNeeded})`); } // If in boss level convert to ammo if(LEVELS[currentLevelIndex].name.includes('Closet') || boss){ veggieAmmo++; } updateHUD(); playTone(650,0.22,'triangle',0.25,220); playTone(980,0.18,'sine',0.22,340); // Visual effect (green + heart shimmer if life gained) for(let i=0;i<10;i++) particles.push({x:player.x+rand(-14,14), y:player.y-26, vx:rand(-1.2,1.2), vy:rand(-3.2,-0.8), life:18, color:i%3? '#32cd32':'#6aff6a'}); if(lives<=9) particles.push({x:player.x, y:player.y-40, vx:0, vy:-1.2, life:26, text:'❤', color:'#ff4d6d'}); } else if(kind==='junk'){ // Junk food gives points but no vegetable progress score+=50; showToast('Tasty junk food! +50 points'); playTone(600,0.15,'triangle',0.15,100); // Orange visual effect for junk food for(let i=0;i<6;i++) particles.push({x:player.x+rand(-12,12), y:player.y-20, vx:rand(-1,1), vy:rand(-3,-1), life:12, color:'#ff8c00'}); } else if(kind==='mushroom'){ if(player.power===POWER.NORMAL){ player.power=POWER.BIG; player.grown=true; player.h=54; showToast('Super!'); playTone(420,0.3,'sawtooth',0.25,200); } else { score+=200; playTone(900,0.12,'square',0.2); } } else if(kind==='flower'){ if(player.power===POWER.NORMAL){ player.power=POWER.BIG; player.h=54; } player.power=POWER.FIRE; showToast('Fire Power!'); chord(660,880,1320); } else if(kind==='star'){ player.prevPower= player.power===POWER.STAR? player.prevPower : player.power; player.power=POWER.STAR; player.starTimer=600; showToast('Invincible!'); chord(880,1170,1560); } if(kind!=='veggie') score+=300; updateHUD(); } function damagePlayer(){ if(player.invincibleTimer>0) return; if(player.power===POWER.STAR) return; player.hurtTimer=40; player.invincibleTimer=80; playTone(160,0.4,'sawtooth',0.3,-80); if(player.power===POWER.FIRE){ player.power=POWER.BIG; showToast('Lost Fire'); } else if(player.power===POWER.BIG){ player.power=POWER.NORMAL; player.h=38; showToast('Shrunk'); } else { lives--; showToast('-1 Life'); if(lives<=0){ gameOver(false); } else { player.x-=40; player.vx=0; } } updateHUD(); } function killEnemy(e,byStar){ e.dead=true; e.vx= (byStar? rand(-5,5):0); e.vy= - (byStar? rand(6,10):8); score+= byStar?200:100; particles.push({x:e.x,y:e.y,life:22,color:'#ffcf33'}); playTone(560,0.12,'square',0.2,140); } function damageBoss(stompStar){ if(!boss) return; boss.health--; particles.push({x:boss.x,y:boss.y-40,life:18,color:'#ff6633'}); playTone(300,0.28,'square',0.25,-140); playTone(160,0.18,'sine',0.18,-200); if(bossHpText) bossHpText.textContent=boss.health+"/7"; if(boss.health<=0){ score+=5000; particles.push({x:boss.x,y:boss.y,life:50,color:'#ffe400'}); showToast('Boss Defeated! Medal Secured!'); setTimeout(()=>{ winGame(); },1200); boss=null; bossBarWrap.style.display='none'; return; } // Pulse effect on low health (when 2 or less remaining) if(boss.health<=2){ bossBar.style.animation='pulseBar .6s linear'; setTimeout(()=>bossBar.style.animation='',600); } } // Player fall death handler function fallDeath(){ if(!gameRunning) return; lives--; showToast('Fell! -1 Life'); playTone(120,0.6,'sawtooth',0.28,-200); if(lives<=0){ gameOver(false); return; } // Respawn at level start, reset basic state player.x=100; player.y=0; player.vx=0; player.vy=0; player.onGround=false; player.power=POWER.NORMAL; player.h=38; player.starTimer=0; player.invincibleTimer=50; // brief grace period updateHUD(); } // ============================================================= // LEVEL PROGRESSION // ============================================================= function nextLevel(){ currentLevelIndex++; if(currentLevelIndex>=LEVELS.length){ winGame(); return; } loadLevel(currentLevelIndex); showToast('World '+LEVELS[currentLevelIndex].name); playTone(520,0.3,'sine',0.25,300); } function winGame(){ win=true; gameOverTitle.textContent='Victory!'; gameOver(true); } function gameOver(victory){ gameRunning=false; finalStats.textContent='Score: '+score; gameOverScreen.classList.remove('hidden'); if(victory){ gameOverScreen.classList.add('victory'); } else { gameOverScreen.classList.remove('victory'); } playTone(victory?880:120,1.0,'sawtooth',0.25, victory?400:-200); } // ============================================================= // HUD // ============================================================= function updateHUD(){ elScore.textContent=score; elLives.textContent=lives; elWorld.textContent=LEVELS[currentLevelIndex].name; elPower.textContent=player.power; elVeggies.textContent=`${vegetablesCollected}/${vegetablesNeeded}`; if(elJunk) elJunk.textContent=`${levelJunkSpawned}/3`; if(elAmmoVal) elAmmoVal.textContent = '∞'; // unlimited ammo } // ============================================================= // RENDERING // ============================================================= function render(){ CTX.clearRect(0,0,W,H); if(!gameRunning){ requestAnimationFrame(render); return; } const lvl=LEVELS[currentLevelIndex]; drawBackground(); drawTiles(lvl); // Entities for(const e of enemies){ const cx=e.x-camera.x, cy=e.y-camera.y; CTX.save(); CTX.translate(cx,cy); if(e.dead){ CTX.rotate(frame/6); } if(e.type==='fish'){ CTX.scale(e.vx<0?-1:1,1); CTX.fillStyle='#34d6ff'; CTX.beginPath(); CTX.ellipse(0,0, e.w*0.45, e.h*0.55, 0, 0, Math.PI*2); CTX.fill(); CTX.fillStyle='#1fa5c7'; CTX.beginPath(); CTX.moveTo(-e.w*0.45,0); CTX.lineTo(-e.w*0.63, e.h*0.28); CTX.lineTo(-e.w*0.63,-e.h*0.28); CTX.closePath(); CTX.fill(); CTX.fillStyle='#fff'; CTX.beginPath(); CTX.arc(e.w*0.18,-e.h*0.08,e.h*0.18,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#000'; CTX.beginPath(); CTX.arc(e.w*0.20,-e.h*0.08,e.h*0.08,0,Math.PI*2); CTX.fill(); // side fin CTX.save(); CTX.translate(-e.w*0.05, e.h*0.12); CTX.rotate(Math.sin((frame+e.frameOff)/12)*0.4); CTX.fillStyle='#26b2d6'; CTX.beginPath(); CTX.ellipse(0,0,e.w*0.18,e.h*0.3,0,0,Math.PI*2); CTX.fill(); CTX.restore(); } else if(e.type==='shark'){ CTX.scale(e.vx<0?-1:1,1); CTX.fillStyle='#70818f'; CTX.beginPath(); CTX.ellipse(0,0, e.w*0.5, e.h*0.55, 0, 0, Math.PI*2); CTX.fill(); CTX.fillStyle='#5c6a74'; CTX.beginPath(); CTX.moveTo(-e.w*0.5,0); CTX.lineTo(-e.w*0.66, e.h*0.3); CTX.lineTo(-e.w*0.66,-e.h*0.3); CTX.closePath(); CTX.fill(); CTX.fillStyle='#5c6a74'; CTX.beginPath(); CTX.moveTo(-6,-e.h*0.25); CTX.lineTo(0,-e.h*0.62); CTX.lineTo(6,-e.h*0.25); CTX.closePath(); CTX.fill(); CTX.fillStyle='#fff'; CTX.beginPath(); CTX.arc(e.w*0.28,-e.h*0.12,e.h*0.16,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#000'; CTX.beginPath(); CTX.arc(e.w*0.30,-e.h*0.12,e.h*0.07,0,Math.PI*2); CTX.fill(); // Mouth CTX.fillStyle='#111'; CTX.beginPath(); CTX.arc(e.w*0.20,e.h*0.08,e.h*0.38,0,Math.PI,true); CTX.fill(); CTX.fillStyle='#fff'; for(let i=0;i<6;i++){ CTX.beginPath(); const ox=-10+i*6; CTX.moveTo(ox,e.h*0.05); CTX.lineTo(ox+3,e.h*0.16); CTX.lineTo(ox+6,e.h*0.05); CTX.fill(); } } else { // Default land enemies: burger icons CTX.font='42px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍔',0,0); } CTX.restore(); } for(const p of powerUps){ CTX.save(); CTX.translate(p.x-camera.x, p.y-camera.y); if(p.kind==='veggie'){ // Draw emoji vegetables CTX.font='36px Arial'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText(p.veggie,0,0); } else { let g=CTX.createLinearGradient(-14,-14,14,14); if(p.kind==='mushroom'){ g.addColorStop(0,'#ff5f6d'); g.addColorStop(1,'#ffc371'); } else if(p.kind==='flower'){ g.addColorStop(0,'#ff8a00'); g.addColorStop(1,'#e52e71'); } else { g.addColorStop(0,'#26ffe6'); g.addColorStop(1,'#f107a3'); } CTX.fillStyle=g; CTX.beginPath(); CTX.roundRect(-14,-14,28,28,8); CTX.fill(); } CTX.restore(); } for(const fb of fireballs){ CTX.save(); CTX.translate(fb.x-camera.x, fb.y-camera.y); CTX.fillStyle='#ffe05c'; CTX.beginPath(); CTX.arc(0,0,8,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#ff3566'; CTX.arc(0,0,5,0,Math.PI*2); CTX.fill(); CTX.restore(); } // Veggie projectiles (draw emoji) for(const vs of veggieShots){ CTX.save(); CTX.translate(vs.x-camera.x, vs.y-camera.y); CTX.font='38px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText(vs.char,0,0); CTX.restore(); } // Boss: draw boss.webp image if(boss){ CTX.save(); CTX.translate(boss.x-camera.x, boss.y-camera.y); if(bossImg.complete && bossImg.naturalWidth > 0){ // Draw the boss image centered CTX.drawImage(bossImg, -boss.w/2, -boss.h/2, boss.w, boss.h); } else { // Fallback if image not loaded: mushroom emoji CTX.font='72px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍄',0,0); } CTX.restore(); } for(const pr of particles){ const alpha=pr.life/ (pr.maxLife||pr.life+1); CTX.globalAlpha=alpha; CTX.fillStyle=pr.color||'#fff'; CTX.fillRect(pr.x-camera.x, pr.y-camera.y, 6,6); CTX.globalAlpha=1; } drawPlayer(); if(paused){ CTX.fillStyle='#ffffff20'; CTX.fillRect(0,0,W,H); CTX.fillStyle='#fff'; CTX.font='bold 42px var(--ui-font)'; CTX.textAlign='center'; CTX.fillText('PAUSED', W/2, H/2); } requestAnimationFrame(render); } function drawBackground(){ const grd=CTX.createLinearGradient(0,0,0,H); grd.addColorStop(0,'#180022'); grd.addColorStop(1,'#050510'); CTX.fillStyle=grd; CTX.fillRect(0,0,W,H); // parallax glows const lvl=LEVELS[currentLevelIndex]; const theme=THEMES[lvl.theme]; // Override for non-underwater worlds with bright sky look if(!theme.water){ CTX.fillStyle='#69c5ff'; CTX.fillRect(0,0,W,H); // Draw ADAPT logo in background (centered, semi-transparent) if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.12; // subtle transparency const logoWidth = 400; // medium size const logoHeight = (logoImg.naturalHeight / logoImg.naturalWidth) * logoWidth; const logoX = (W - logoWidth) / 2; // centered horizontally const logoY = H * 0.3; // positioned in upper-middle area CTX.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight); CTX.restore(); } // Hills (parallax): layer 0 behind, layer 1 mid for(const layer of [0,1]){ for(const h of hills){ if(h.layer!==layer) continue; const par = layer?0.5:0.25; const sx = h.x - camera.x*par; if(sx+h.w < -50 || sx> W+50) continue; CTX.save(); CTX.translate(Math.floor(sx), h.y - 60); // raise a bit // Body CTX.fillStyle=h.shade; CTX.beginPath(); const r = h.h/2; CTX.roundRect(0,60, h.w, h.h, Math.min(80,r)); CTX.fill(); // Dots CTX.globalAlpha=0.25; CTX.fillStyle='#fff'; for(let i=0;i<8;i++){ CTX.beginPath(); CTX.arc( (i/8)*h.w + 14, 60 + (i%2)*40 + 30, 18, 0, Math.PI*2); CTX.fill(); } CTX.restore(); } } // Clouds drifting CTX.save(); CTX.fillStyle='#fff'; for(const c of clouds){ let x = (c.x - camera.x*0.2 + frame*c.speed) % (lvl.map[0].length*TILE); x -= (x<0? - (lvl.map[0].length*TILE):0); const sx = x - camera.x; if(sx<-150||sx>W+150) continue; CTX.save(); CTX.translate(sx, c.y); CTX.scale(c.scale,c.scale); CTX.globalAlpha=0.95; CTX.beginPath(); CTX.arc(0,0,26,0,Math.PI*2); CTX.arc(30,10,22,0,Math.PI*2); CTX.arc(-30,8,18,0,Math.PI*2); CTX.arc(5,18,20,0,Math.PI*2); CTX.fill(); CTX.restore(); } CTX.restore(); } else { // Enhanced underwater background for world 3 // Deep water gradient const waterGrad=CTX.createLinearGradient(0,0,0,H); waterGrad.addColorStop(0,'#023e8a'); waterGrad.addColorStop(0.45,'#035aa6'); waterGrad.addColorStop(0.75,'#02325f'); waterGrad.addColorStop(1,'#011c33'); CTX.fillStyle=waterGrad; CTX.fillRect(0,0,W,H); // Draw ADAPT logo in underwater background (centered, very subtle) if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.08; // more subtle in water const logoWidth = 400; const logoHeight = (logoImg.naturalHeight / logoImg.naturalWidth) * logoWidth; const logoX = (W - logoWidth) / 2; const logoY = H * 0.35; CTX.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight); CTX.restore(); } // Light shafts (caustics) CTX.save(); CTX.globalAlpha=0.16; CTX.fillStyle='#9ad9ff'; for(let i=0;i<6;i++){ const sx = ((i*210) + frame*12) % (W+400) -200; const wS = 120 + Math.sin((frame+i*30)/40)*40; CTX.beginPath(); CTX.moveTo(sx,0); CTX.lineTo(sx+wS*0.4,H*0.4); CTX.lineTo(sx+wS*0.15,H); CTX.lineTo(sx-wS*0.3,H); CTX.closePath(); CTX.fill(); } CTX.restore(); // Floating particles & bubbles CTX.save(); for(let i=0;i<26;i++){ const bx = ( (i*140) + frame* (0.3 + (i%5)*0.12) - camera.x*0.15) % (lvl.map[0].length*TILE); let sx = bx - camera.x; if(sx<-60||sx>W+60) continue; const by = ( (i*57) + frame* (0.15 + (i%7)*0.04) ) % H; CTX.fillStyle = i%3? '#8fd3ff55':'#ffffff30'; CTX.beginPath(); CTX.arc(sx, H - by, 6 + (i%4), 0, Math.PI*2); CTX.fill(); } CTX.restore(); // Sea plants at bottom CTX.save(); CTX.translate(0,H-40); for(let i=0;i<18;i++){ const px = (i* (lvl.map[0].length*TILE)/18) - camera.x*0.25 - camera.x; const sx = px - camera.x; if(sx<-100||sx>W+60) continue; CTX.save(); CTX.translate(sx,0); CTX.scale(1,1+Math.sin((frame+i*10)/50)*0.08); CTX.fillStyle= i%2? '#0c845d':'#0a6f4a'; CTX.beginPath(); CTX.roundRect(-6,-60,12,60,6); CTX.fill(); CTX.fillStyle='#13b384'; CTX.globalAlpha=0.35; CTX.beginPath(); CTX.roundRect(-3,-60,6,60,4); CTX.fill(); CTX.globalAlpha=1; CTX.restore(); } CTX.restore(); } } function drawTiles(lvl){ const startCol=Math.floor(camera.x/TILE); const endCol=Math.ceil((camera.x+W)/TILE); for(let r=0;r<lvl.map.length;r++){ for(let c=startCol;c<endCol;c++){ if(c<0||c>=lvl.map[r].length) continue; const ch=lvl.map[r][c]; if(ch==='.'||ch==='E'||ch==='B') continue; const x=c*TILE-camera.x; const y=r*TILE-camera.y; const above = r>0? lvl.map[r-1][c]:'.'; if(ch==='#') { drawGround(x,y, above!=='#'); continue; } CTX.save(); if(ch==='?'){ drawMushroomTile(x,y); } else if(ch==='H'){ CTX.globalAlpha=0.15; drawQuestion(x,y,true); } else if(ch==='M'){ drawMedal(x,y); } else if(isSolid(ch)) { drawBlock(x,y,'#5a4dff','#262e8e'); } CTX.restore(); } } } function drawGround(x,y,isTop){ // Draw small transparent logo pattern underneath if(logoImg.complete && logoImg.naturalWidth > 0){ CTX.save(); CTX.globalAlpha = 0.15; // transparent const logoSize = 32; // small size CTX.drawImage(logoImg, x + (TILE-logoSize)/2, y + (TILE-logoSize)/2, logoSize, logoSize); CTX.restore(); } // Dirt body const g=CTX.createLinearGradient(x,y,x,y+TILE); g.addColorStop(0,'#d37a25'); g.addColorStop(1,'#8a400c'); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); // Brick pattern lines CTX.strokeStyle='#632b06aa'; CTX.lineWidth=2; CTX.beginPath(); CTX.moveTo(x,y+TILE/2); CTX.lineTo(x+TILE,y+TILE/2); CTX.moveTo(x+TILE/2,y); CTX.lineTo(x+TILE/2,y+TILE); CTX.stroke(); // Grass top overlay if exposed if(isTop){ CTX.fillStyle='#2ecc40'; CTX.fillRect(x,y-10,TILE,14); CTX.fillStyle='#28b437'; for(let i=0;i<6;i++){ CTX.beginPath(); const px=x+i*(TILE/5); CTX.arc(px+6,y+2,8,0,Math.PI,true); CTX.fill(); } } } function drawQuestion(x,y,hidden){ const g=CTX.createLinearGradient(x,y,x+TILE,y+TILE); g.addColorStop(0,'#ffe18a'); g.addColorStop(1,'#e79807'); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); CTX.strokeStyle='#0005'; CTX.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1); if(!hidden){ CTX.fillStyle='#5e3200'; CTX.font='bold 26px var(--ui-font)'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('?',x+TILE/2,y+TILE/2+2); } } // Replaces question block visual: bouncy mushroom trampoline function drawMushroomTile(x,y){ // Draw mushroom emoji instead of custom drawing CTX.save(); CTX.font='48px system-ui'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('🍄', x+TILE/2, y+TILE/2); CTX.restore(); } function drawBlock(x,y,c1,c2){ const g=CTX.createLinearGradient(x,y,x+TILE,y+TILE); g.addColorStop(0,c1); g.addColorStop(1,c2); CTX.fillStyle=g; CTX.fillRect(x,y,TILE,TILE); CTX.strokeStyle='#0005'; CTX.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1); } function drawMedal(x,y){ CTX.save(); CTX.translate(x+TILE/2,y+TILE/2); CTX.fillStyle='#ffe34d'; CTX.beginPath(); CTX.arc(0,0,20,0,Math.PI*2); CTX.fill(); CTX.fillStyle='#ffb347'; CTX.font='bold 18px var(--ui-font)'; CTX.textAlign='center'; CTX.textBaseline='middle'; CTX.fillText('★',0,2); CTX.restore(); } // ============================================================= // MAIN LOOP // ============================================================= function loop(){ if(gameRunning){ update(); } requestAnimationFrame(loop); } requestAnimationFrame(loop); requestAnimationFrame(render); // ============================================================= // EXTRA: KEY EVENTS (PREVENT SCROLL) // ============================================================= window.addEventListener('keydown',e=>{ if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); }); // ============================================================= // FIREBALL & PLAYER SHOOT HOTKEY // ============================================================= document.addEventListener('keydown', e=>{ if(e.key==='F'||e.key==='f'){ shootFireball(); } }); // Polyfill roundRect if(!CanvasRenderingContext2D.prototype.roundRect){ CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){ if(!r) r=8; this.beginPath(); this.moveTo(x+r,y); this.lineTo(x+w-r,y); this.quadraticCurveTo(x+w,y,x+w,y+r); this.lineTo(x+w,y+h-r); this.quadraticCurveTo(x+w,y+h,x+w-r,y+h); this.lineTo(x+r,y+h); this.quadraticCurveTo(x,y+h,x,y+h-r); this.lineTo(x,y+r); this.quadraticCurveTo(x,y,x+r,y); return this; } } // ============================================================= // DEBUG SHORTCUTS (REMOVE IF YOU WANT PURE EXPERIENCE) // ============================================================= window.addEventListener('keydown',e=>{ if(e.key==='L'){ score+=500; showToast('+500'); } if(e.key==='N'){ nextLevel(); } }); </script> </body> </html>

Below is an AI news video created to promote our project.

thebesteulogowhite.png
estonia-transformed.png
bottom of page