import { useState, useEffect, useRef, useCallback } from "react"; /* ══════════════════════════════════════════════════════════════════════════ CONSTANTS ══════════════════════════════════════════════════════════════════════════ */ const WEAPONS = [ { id:"ketchup", name:"Ketchup Blaster", color:"#e63946", dmg:18, speed:9, range:320, spread:0.08, emoji:"🍅", ammo:8, desc:"Rapid-fire ketchup bursts", rarity:"common", shots:1 }, { id:"mustard", name:"Mustard Cannon", color:"#f4c430", dmg:35, speed:6, range:200, spread:0.15, emoji:"🌭", ammo:4, desc:"Slow but powerful blobs", rarity:"common", shots:1 }, { id:"icecream", name:"Ice Cream Launcher", color:"#a8dadc", dmg:25, speed:7, range:260, spread:0.05, emoji:"🍦", ammo:6, desc:"Freezing scoop shots", rarity:"rare", shots:1 }, { id:"pizza", name:"Pizza Disc", color:"#ff6b35", dmg:28, speed:11, range:350, spread:0.03, emoji:"🍕", ammo:5, desc:"Bouncing pizza slices", rarity:"rare", shots:1 }, { id:"taco", name:"Taco Trishot", color:"#7bc67e", dmg:14, speed:8, range:240, spread:0.25, emoji:"🌮", ammo:9, desc:"Three-way taco spray", rarity:"epic", shots:3 }, { id:"sushi", name:"Sushi Sniper", color:"#e9c46a", dmg:55, speed:14, range:500, spread:0.01, emoji:"🍱", ammo:3, desc:"Long-range precise roll", rarity:"epic", shots:1 }, { id:"burrito", name:"Burrito Bomb", color:"#c084fc", dmg:48, speed:5, range:180, spread:0.4, emoji:"🌯", ammo:3, desc:"Explosive wrapped blast", rarity:"legendary",shots:1, expl:true }, { id:"ramen", name:"Ramen Wave", color:"#fb923c", dmg:20, speed:10, range:300, spread:0.12, emoji:"🍜", ammo:6, desc:"Noodle whip spray", rarity:"legendary",shots:5 }, { id:"corn", name:"Corn Shotgun", color:"#fde047", dmg:22, speed:9, range:200, spread:0.35, emoji:"🌽", ammo:5, desc:"5-pellet corn burst", rarity:"epic", shots:5 }, { id:"melon", name:"Watermelon Slam", color:"#4ade80", dmg:80, speed:4, range:100, spread:0.05, emoji:"🍉", ammo:2, desc:"Massive close-range splash", rarity:"legendary",shots:1, expl:true, splashR:90 }, ]; const RARITY_COLORS = { common:"#9ca3af", rare:"#3b82f6", epic:"#a855f7", legendary:"#f59e0b" }; const SKINS = [ { id:"default", name:"Chef", bodyColor:"#ff6b6b", headColor:"#ffd6a5", hatColor:"#fff", hatStyle:"chef" }, { id:"burger", name:"Burger Boy", bodyColor:"#c8a951", headColor:"#f4a261", hatColor:"#8b4513", hatStyle:"bun" }, { id:"grape", name:"Grape Gang", bodyColor:"#7c3aed", headColor:"#c4b5fd", hatColor:"#4c1d95", hatStyle:"crown" }, { id:"lime", name:"Lime Rider", bodyColor:"#16a34a", headColor:"#bbf7d0", hatColor:"#14532d", hatStyle:"cap" }, { id:"hotdog", name:"Hot Doggo", bodyColor:"#dc2626", headColor:"#fde68a", hatColor:"#92400e", hatStyle:"bun" }, { id:"blueberry", name:"Blueberry", bodyColor:"#1d4ed8", headColor:"#93c5fd", hatColor:"#1e3a8a", hatStyle:"crown" }, { id:"donut", name:"Donutron", bodyColor:"#f472b6", headColor:"#fce7f3", hatColor:"#be185d", hatStyle:"chef" }, { id:"avocado", name:"Avocado", bodyColor:"#65a30d", headColor:"#d9f99d", hatColor:"#3f6212", hatStyle:"cap" }, { id:"strawberry", name:"Berry Boss", bodyColor:"#be123c", headColor:"#fda4af", hatColor:"#881337", hatStyle:"crown" }, { id:"waffle", name:"Waffle King",bodyColor:"#ca8a04", headColor:"#fef3c7", hatColor:"#78350f", hatStyle:"chef" }, { id:"mushroom", name:"Shroomie", bodyColor:"#7c3aed", headColor:"#ede9fe", hatColor:"#4c1d95", hatStyle:"bun" }, ]; const PASS_REWARDS = [ {level:1, xpNeeded:0, type:"coins", value:100, emoji:"🪙", label:"100 Coins" }, {level:2, xpNeeded:200, type:"skin", value:"burger", emoji:"🍔", label:"Burger Boy Skin" }, {level:3, xpNeeded:500, type:"weapon", value:"pizza", emoji:"🍕", label:"Pizza Disc" }, {level:4, xpNeeded:900, type:"coins", value:300, emoji:"🪙", label:"300 Coins" }, {level:5, xpNeeded:1400, type:"weapon", value:"sushi", emoji:"🍱", label:"Sushi Sniper" }, {level:6, xpNeeded:2000, type:"skin", value:"blueberry", emoji:"🫐", label:"Blueberry Skin" }, {level:7, xpNeeded:2800, type:"coins", value:500, emoji:"🪙", label:"500 Coins" }, {level:8, xpNeeded:3800, type:"weapon", value:"taco", emoji:"🌮", label:"Taco Trishot" }, {level:9, xpNeeded:5000, type:"skin", value:"lime", emoji:"🍋", label:"Lime Rider" }, {level:10, xpNeeded:6500, type:"weapon", value:"burrito", emoji:"🌯", label:"Burrito Bomb" }, {level:11, xpNeeded:8500, type:"skin", value:"donut", emoji:"🍩", label:"Donutron Skin" }, {level:12, xpNeeded:11000,type:"weapon", value:"ramen", emoji:"🍜", label:"Ramen Wave" }, {level:13, xpNeeded:14000,type:"skin", value:"strawberry", emoji:"🍓", label:"Berry Boss Skin" }, {level:14, xpNeeded:18000,type:"weapon", value:"corn", emoji:"🌽", label:"Corn Shotgun" }, {level:15, xpNeeded:23000,type:"skin", value:"waffle", emoji:"🧇", label:"Waffle King Skin" }, {level:16, xpNeeded:30000,type:"weapon", value:"melon", emoji:"🍉", label:"Watermelon Slam" }, {level:17, xpNeeded:40000,type:"skin", value:"mushroom", emoji:"🍄", label:"Shroomie Skin" }, {level:18, xpNeeded:55000,type:"coins", value:5000, emoji:"👑", label:"5000 Coins + Crown"}, ]; const DAILY = [ {id:"kills5", desc:"Get 5 kills", goal:5, reward:100, emoji:"💀"}, {id:"score500", desc:"Score 500 pts", goal:500, reward:150, emoji:"⭐"}, {id:"food10", desc:"Collect 10 food items", goal:10, reward:80, emoji:"🍎"}, {id:"survive90",desc:"Survive 90s", goal:90, reward:120, emoji:"⏱️"}, {id:"waves3", desc:"Clear 3 waves", goal:3, reward:200, emoji:"🌊"}, {id:"combo10", desc:"Get a 10x combo", goal:10, reward:250, emoji:"🔥"}, ]; /* ─── helpers ─── */ const dist = (a,b)=>Math.hypot(a.x-b.x,a.y-b.y); const clamp = (v,lo,hi)=>Math.max(lo,Math.min(hi,v)); const rr = (a,b)=>a+Math.random()*(b-a); const ri = (a,b)=>Math.floor(rr(a,b+1)); /* ══════════════════════════════════════════════════════════════════════════ CHARACTER SVG ══════════════════════════════════════════════════════════════════════════ */ function CharSVG({skin,weapon,scale=1,animated=false}) { const s=SKINS.find(x=>x.id===skin)||SKINS[0]; const w=WEAPONS.find(x=>x.id===weapon); return ( {s.hatStyle==="chef"&&<> } {s.hatStyle==="bun"&&} {s.hatStyle==="crown"&&} {s.hatStyle==="cap"&&<> } {w&&{w.emoji}} ); } /* ══════════════════════════════════════════════════════════════════════════ FLOATING JOYSTICK HOOK - Touch can start ANYWHERE in the zone (not just the center) - Thumb follows from actual touch-start point - preventDefault on move so page doesn't scroll ══════════════════════════════════════════════════════════════════════════ */ function useFloatJoy() { const zoneRef = useRef(null); const output = useRef({dx:0,dy:0,mag:0}); const vis = useRef({active:false,bx:0,by:0,tx:0,ty:0}); // base & thumb screen coords const idRef = useRef(null); const MAX = 50; useEffect(()=>{ const el=zoneRef.current; if(!el) return; const onStart=e=>{ const t=e.changedTouches[0]; const r=el.getBoundingClientRect(); const bx=t.clientX-r.left, by=t.clientY-r.top; idRef.current=t.identifier; vis.current={active:true,bx,by,tx:bx,ty:by}; output.current={dx:0,dy:0,mag:0}; e.preventDefault(); }; const onMove=e=>{ const t=[...e.changedTouches].find(t=>t.identifier===idRef.current); if(!t) return; const r=el.getBoundingClientRect(); const {bx,by}=vis.current; let dx=(t.clientX-r.left)-bx, dy=(t.clientY-r.top)-by; const mag=Math.hypot(dx,dy); if(mag>MAX){dx*=MAX/mag;dy*=MAX/mag;} vis.current.tx=bx+dx; vis.current.ty=by+dy; output.current={dx:dx/MAX,dy:dy/MAX,mag:Math.min(1,mag/MAX)}; e.preventDefault(); }; const onEnd=e=>{ const t=[...e.changedTouches].find(t=>t.identifier===idRef.current); if(!t) return; vis.current={active:false,bx:0,by:0,tx:0,ty:0}; output.current={dx:0,dy:0,mag:0}; idRef.current=null; }; el.addEventListener("touchstart", onStart,{passive:false}); el.addEventListener("touchmove", onMove, {passive:false}); el.addEventListener("touchend", onEnd, {passive:false}); el.addEventListener("touchcancel",onEnd, {passive:false}); return()=>{ el.removeEventListener("touchstart",onStart); el.removeEventListener("touchmove", onMove); el.removeEventListener("touchend", onEnd); el.removeEventListener("touchcancel",onEnd); }; },[]); return {zoneRef, output, vis}; } /* Floating joystick visual overlay */ function FloatJoyVis({vis,color="#ffffff"}) { const v=vis.current; if(!v.active) return null; return ( <>
); } /* ══════════════════════════════════════════════════════════════════════════ GAME ENGINE ══════════════════════════════════════════════════════════════════════════ */ class GameEngine { constructor(canvas,cfg,onEnd,onHUD) { this.canvas=canvas; this.ctx=canvas.getContext("2d"); this.cfg=cfg; this.onEnd=onEnd; this.onHUD=onHUD||(()=>{}); this.keys={}; this.mouse={x:canvas.width/2,y:canvas.height/2}; this.joyMove={dx:0,dy:0}; this.joyAim={dx:0,dy:0,mag:0}; this.running=false; this.lastTime=0; this.elapsed=0; this.GAME_TIME=150; this.MAP_W=1800; this.MAP_H=1400; this.cam={x:0,y:0}; this.shake=0; this.bullets=[]; this.enemies=[]; this.particles=[]; this.food=[]; this.powerups=[]; this.floats=[]; this.score=0; this.kills=0; this.foodEaten=0; this.wave=0; this.waveClear=true; this.waveNext=2; this.hudTimer=0; // Combo system this.combo=0; this.comboTimer=0; this.maxCombo=0; // Power-up state this.shielded=0; this.speedy=0; this.initPlayer(); this.spawnFood(14); this.setupIO(); } setupIO(){ const c=this.canvas; this._kd=e=>{this.keys[e.key.toLowerCase()]=true;}; this._ku=e=>{this.keys[e.key.toLowerCase()]=false;}; this._mm=e=>{const r=c.getBoundingClientRect();this.mouse.x=(e.clientX-r.left)*(c.width/r.width);this.mouse.y=(e.clientY-r.top)*(c.height/r.height);}; this._md=e=>{if(e.button===0)this.shoot();}; window.addEventListener("keydown",this._kd); window.addEventListener("keyup", this._ku); c.addEventListener("mousemove",this._mm); c.addEventListener("mousedown",this._md); } destroy(){ window.removeEventListener("keydown",this._kd); window.removeEventListener("keyup", this._ku); this.canvas.removeEventListener("mousemove",this._mm); this.canvas.removeEventListener("mousedown",this._md); } initPlayer(){ const skin=SKINS.find(s=>s.id===this.cfg.skin)||SKINS[0]; const weap=WEAPONS.find(w=>w.id===this.cfg.weapon)||WEAPONS[0]; this.player={x:this.MAP_W/2,y:this.MAP_H/2,r:22,speed:200,hp:100,maxHp:100,skin,weap, ammo:weap.ammo,maxAmmo:weap.ammo,reload:false,reloadT:0,shotCD:0,angle:0,inv:0}; } spawnFood(n){ const T=["🍎","🍊","🍋","🍇","🍓","🫐","🍑","🥝","🍒","🥭"]; for(let i=0;i0||p.reload||p.ammo<=0) return; const w=p.weap; let angle=p.angle; if(Math.hypot(this.joyAim.dx,this.joyAim.dy)>0.15) angle=Math.atan2(this.joyAim.dy,this.joyAim.dx); const cnt=w.shots||1; const isExpl=w.expl||false; const splashR=w.splashR||72; for(let i=0;ithis.maxCombo) this.maxCombo=this.combo; const mult=Math.floor(this.combo/3)+1; return mult; } update(dt){ const p=this.player; this.elapsed+=dt; if(this.elapsed>=this.GAME_TIME){this.end();return;} // HUD this.hudTimer-=dt; if(this.hudTimer<=0){ this.hudTimer=.18; this.onHUD({hp:p.hp,maxHp:p.maxHp,ammo:p.ammo,maxAmmo:p.maxAmmo,reload:p.reload,reloadT:p.reloadT,score:this.score,kills:this.kills,wave:this.wave,timeLeft:Math.max(0,this.GAME_TIME-this.elapsed),combo:this.combo,shielded:this.shielded>0,speedy:this.speedy>0}); } // Wave if(this.waveClear && this.elapsed>=this.waveNext) this.startWave(); // Combo decay this.comboTimer-=dt; if(this.comboTimer<=0&&this.combo>0){this.combo=0;} // Power-up timers if(this.shielded>0) this.shielded-=dt; if(this.speedy>0) this.speedy-=dt; // Shake this.shake=Math.max(0,this.shake-dt*7); // Player movement let dx=0,dy=0; if(this.keys["w"]||this.keys["arrowup"]) dy-=1; if(this.keys["s"]||this.keys["arrowdown"]) dy+=1; if(this.keys["a"]||this.keys["arrowleft"]) dx-=1; if(this.keys["d"]||this.keys["arrowright"]) dx+=1; if(Math.hypot(this.joyMove.dx,this.joyMove.dy)>.1){dx=this.joyMove.dx;dy=this.joyMove.dy;} if(dx||dy){ const l=Math.hypot(dx,dy); const spMult=this.speedy>0?1.55:1; p.x+=dx/l*p.speed*spMult*dt; p.y+=dy/l*p.speed*spMult*dt; } p.x=clamp(p.x,p.r,this.MAP_W-p.r); p.y=clamp(p.y,p.r,this.MAP_H-p.r); // Aim if(Math.hypot(this.joyAim.dx,this.joyAim.dy)>.15){ p.angle=Math.atan2(this.joyAim.dy,this.joyAim.dx); if(this.joyAim.mag>.28) this.shoot(); } else { p.angle=Math.atan2(this.mouse.y+this.cam.y-p.y,this.mouse.x+this.cam.x-p.x); } if(this.keys[" "]||this.keys["f"]) this.shoot(); // Cooldowns if(p.shotCD>0) p.shotCD-=dt; if(p.inv>0) p.inv-=dt; if(p.reload){p.reloadT-=dt;if(p.reloadT<=0){p.reload=false;p.ammo=p.maxAmmo;}} // Camera this.cam.x=clamp(p.x-this.canvas.width/2, 0,this.MAP_W-this.canvas.width); this.cam.y=clamp(p.y-this.canvas.height/2, 0,this.MAP_H-this.canvas.height); // Bullets this.bullets=this.bullets.filter(b=>{ b.x+=b.vx*dt; b.y+=b.vy*dt; b.life-=dt; if(b.life<=0||b.x<0||b.x>this.MAP_W||b.y<0||b.y>this.MAP_H) return false; if(b.owner==="player"){ for(let i=this.enemies.length-1;i>=0;i--){ const e=this.enemies[i]; if(dist(b,e){if(dist(b,en)1?`+${pts} x${mult}`:"+100","#ffd700",e.isBoss?22:14); // chance to drop power-up if(Math.random()<(e.isBoss?.8:.18)) this.spawnPowerup(e.x,e.y); this.enemies.splice(i,1); if(this.enemies.length===0&&!this.waveClear){ this.waveClear=true; p.hp=Math.min(p.maxHp,p.hp+30); this.score+=this.wave*50; this.spawnFood(6); this.ft(p.x,p.y-60,"🌊 WAVE CLEAR! +30 HP","#22c55e",20); this.waveNext=this.elapsed+3; } } return false; } } } else { if(dist(b,p)0&&p.inv<=0){ p.inv=.3; this.ft(p.x,p.y-28,"🛡️ BLOCKED!","#60a5fa"); this.addParticles(p.x,p.y,"#60a5fa",10); return false; } } return true; }); // Enemies AI this.enemies.forEach(e=>{ if(e.stun>0){e.stun-=dt;return;} const a=Math.atan2(p.y-e.y,p.x-e.x),d=dist(e,p); if(d>(e.isBoss?160:110)){e.x+=Math.cos(a)*e.spd*dt;e.y+=Math.sin(a)*e.spd*dt;} e.angle=a; e.x=clamp(e.x,e.r,this.MAP_W-e.r); e.y=clamp(e.y,e.r,this.MAP_H-e.r); e.shootT-=dt; if(e.shootT<=0&&d<(e.isBoss?650:400)){this.eshoot(e);e.shootT=e.isBoss?rr(.8,1.5):rr(1.5,3);} }); // Food this.food=this.food.filter(f=>{ f.pulse+=dt*3; if(dist(f,p){ pu.pulse+=dt*4; if(dist(pu,p){pt.x+=pt.vx*dt;pt.y+=pt.vy*dt;pt.vy+=220*dt;pt.life-=dt;return pt.life>0;}); this.floats=this.floats.filter(ft=>{ft.y+=ft.vy*dt;ft.life-=dt;return ft.life>0;}); } drawChar(ctx,e,isPlayer){ const {x,y,r,angle}=e; const bc=isPlayer?e.skin.bodyColor:e.color; const hc=isPlayer?e.skin.headColor:e.hc; const sx=x-this.cam.x, sy=y-this.cam.y; ctx.save(); ctx.translate(sx,sy); if(isPlayer&&e.inv>0&&Math.floor(e.inv*12)%2===0) ctx.globalAlpha=.35; // shield glow if(isPlayer&&this.shielded>0){ ctx.save(); ctx.globalAlpha=.4+(Math.sin(Date.now()*.005)*.2); ctx.fillStyle="#60a5fa"; ctx.beginPath(); ctx.arc(0,-r*.3,r*1.5,0,Math.PI*2); ctx.fill(); ctx.restore(); } // speed trail if(isPlayer&&this.speedy>0){ ctx.save(); ctx.globalAlpha=.25; ctx.fillStyle="#fde047"; ctx.beginPath(); ctx.ellipse(0,r*.5,r*.6,r*1.4,0,0,Math.PI*2); ctx.fill(); ctx.restore(); } // shadow ctx.fillStyle="rgba(0,0,0,.2)";ctx.beginPath();ctx.ellipse(0,r*.9,r*1.1,r*.3,0,0,Math.PI*2);ctx.fill(); // body const bg=ctx.createLinearGradient(-r,0,r,0); bg.addColorStop(0,bc+"aa");bg.addColorStop(.4,bc);bg.addColorStop(1,bc+"66"); ctx.fillStyle=bg;ctx.beginPath();ctx.roundRect(-r*.82,-r*.25,r*1.64,r*1.12,r*.5);ctx.fill(); ctx.fillStyle=bc;ctx.beginPath();ctx.ellipse(0,-r*.25,r*.82,r*.28,0,0,Math.PI*2);ctx.fill(); // head const hg=ctx.createRadialGradient(-r*.25,-r*1.15,r*.1,0,-r*.88,r*.8); hg.addColorStop(0,hc);hg.addColorStop(1,hc+"bb"); ctx.fillStyle=hg;ctx.beginPath();ctx.arc(0,-r*.88,r*.66,0,Math.PI*2);ctx.fill(); ctx.strokeStyle="rgba(0,0,0,.1)";ctx.lineWidth=1;ctx.stroke(); // eyes const eo=r*.21,es=r*.14; ctx.fillStyle="#1a1a2e";ctx.beginPath();ctx.arc(-eo,-r*.94,es,0,Math.PI*2);ctx.fill(); ctx.beginPath();ctx.arc(eo,-r*.94,es,0,Math.PI*2);ctx.fill(); ctx.fillStyle="white";ctx.beginPath();ctx.arc(-eo+es*.4,-r*.96,es*.5,0,Math.PI*2);ctx.fill(); ctx.beginPath();ctx.arc(eo+es*.4,-r*.96,es*.5,0,Math.PI*2);ctx.fill(); if(e.isBoss){ctx.font=`${r*.7}px serif`;ctx.textAlign="center";ctx.textBaseline="middle";ctx.fillText("👑",0,-r*1.7);} // weapon/icon const wx=Math.cos(angle)*r*1.45,wy=Math.sin(angle)*r*1.45; ctx.font=`${r*.85}px serif`;ctx.textAlign="center";ctx.textBaseline="middle"; if(isPlayer&&e.weap){ctx.save();ctx.translate(wx,wy);ctx.rotate(angle+(angle>Math.PI/2||angle<-Math.PI/2?Math.PI:0));ctx.scale(angle>Math.PI/2||angle<-Math.PI/2?-1:1,1);ctx.fillText(e.weap.emoji,0,0);ctx.restore();} else if(!isPlayer) ctx.fillText(e.emoji,wx,wy); ctx.globalAlpha=1;ctx.restore(); // HP bar const bw=r*2.6,bx=sx-bw/2,by=sy-r*2.15; ctx.fillStyle="rgba(0,0,0,.5)";ctx.beginPath();ctx.roundRect(bx,by,bw,6,3);ctx.fill(); const ratio=e.hp/e.maxHp; ctx.fillStyle=isPlayer?"#22c55e":ratio>.6?"#facc15":ratio>.3?"#f97316":"#ef4444"; ctx.beginPath();ctx.roundRect(bx,by,bw*ratio,6,3);ctx.fill(); if(e.isBoss){ctx.fillStyle="white";ctx.font="bold 10px sans-serif";ctx.textAlign="center";ctx.textBaseline="middle";ctx.fillText(`${Math.round(e.hp)}/${e.maxHp}`,sx,by-8);} } draw(){ const ctx=this.ctx,W=this.canvas.width,H=this.canvas.height; ctx.clearRect(0,0,W,H); const sx=(Math.random()-.5)*this.shake*14,sy=(Math.random()-.5)*this.shake*14; ctx.save();ctx.translate(sx,sy); // Floor const ts=72; for(let tx=Math.floor(this.cam.x/ts)-1;tx<(this.cam.x+W)/ts+2;tx++) for(let ty=Math.floor(this.cam.y/ts)-1;ty<(this.cam.y+H)/ts+2;ty++){ ctx.fillStyle=(tx+ty)%2===0?"#a8d5a2":"#95c98e"; ctx.fillRect(tx*ts-this.cam.x,ty*ts-this.cam.y,ts,ts); } // Walls [[0,0,this.MAP_W,30],[0,this.MAP_H-30,this.MAP_W,30],[0,0,30,this.MAP_H],[this.MAP_W-30,0,30,this.MAP_H]].forEach(([x,y,w,h])=>{ ctx.fillStyle="#6b3a2a";ctx.fillRect(x-this.cam.x,y-this.cam.y,w,h); }); // Bushes [[200,200],[400,600],[900,300],[1300,800],[600,1050],[1050,400],[1500,200],[350,950],[750,700],[1100,950],[1600,600],[500,400],[1200,300]].forEach(([bx,by])=>{ const sx2=bx-this.cam.x,sy2=by-this.cam.y; if(sx2<-80||sx2>W+80||sy2<-80||sy2>H+80) return; ctx.fillStyle="#2d6a4f";ctx.beginPath();ctx.arc(sx2,sy2,30,0,Math.PI*2);ctx.fill(); ctx.fillStyle="#40916c";ctx.beginPath();ctx.arc(sx2-11,sy2-9,22,0,Math.PI*2);ctx.fill(); ctx.beginPath();ctx.arc(sx2+13,sy2-6,19,0,Math.PI*2);ctx.fill(); }); // Food this.food.forEach(f=>{ const fx=f.x-this.cam.x,fy=f.y-this.cam.y,p2=Math.sin(f.pulse)*3; ctx.save();ctx.shadowColor="#00ff88";ctx.shadowBlur=12+p2*2; ctx.font="22px serif";ctx.textAlign="center";ctx.textBaseline="middle"; ctx.fillText(f.emoji,fx,fy+p2);ctx.restore(); ctx.strokeStyle=`rgba(0,255,136,${.25+Math.sin(f.pulse)*.18})`; ctx.lineWidth=2;ctx.beginPath();ctx.arc(fx,fy,f.r+p2,0,Math.PI*2);ctx.stroke(); }); // Power-ups this.powerups.forEach(pu=>{ const px=pu.x-this.cam.x,py=pu.y-this.cam.y; const pulse=Math.sin(pu.pulse)*4; ctx.save();ctx.shadowColor=pu.color;ctx.shadowBlur=16+pulse*2; // glow ring ctx.strokeStyle=pu.color+`${Math.floor((.5+Math.sin(pu.pulse)*.3)*255).toString(16).padStart(2,"0")}`; ctx.lineWidth=2.5;ctx.beginPath();ctx.arc(px,py,pu.r+pulse,0,Math.PI*2);ctx.stroke(); ctx.font="20px serif";ctx.textAlign="center";ctx.textBaseline="middle"; ctx.fillText(pu.emoji,px,py+pulse*.5);ctx.restore(); }); // Bullets this.bullets.forEach(b=>{ const bx=b.x-this.cam.x,by=b.y-this.cam.y; ctx.save();ctx.shadowColor=b.color;ctx.shadowBlur=10; ctx.fillStyle=b.color;ctx.beginPath();ctx.arc(bx,by,b.r,0,Math.PI*2);ctx.fill(); if(b.emoji){ctx.font="13px serif";ctx.textAlign="center";ctx.textBaseline="middle";ctx.fillText(b.emoji,bx,by);} ctx.restore(); }); this.enemies.forEach(e=>this.drawChar(ctx,e,false)); this.drawChar(ctx,this.player,true); this.particles.forEach(pt=>{ const t=pt.life/(pt.ml||.6); ctx.save();ctx.globalAlpha=t;ctx.fillStyle=pt.color; ctx.beginPath();ctx.arc(pt.x-this.cam.x,pt.y-this.cam.y,pt.r*t,0,Math.PI*2);ctx.fill();ctx.restore(); }); this.floats.forEach(ft=>{ const t=ft.life/1.6; ctx.save();ctx.globalAlpha=Math.min(1,t*2); ctx.font=`bold ${ft.size||14}px 'Fredoka One',sans-serif`; ctx.textAlign="center";ctx.textBaseline="middle"; ctx.strokeStyle="rgba(0,0,0,.6)";ctx.lineWidth=3; ctx.strokeText(ft.text,ft.x-this.cam.x,ft.y-this.cam.y); ctx.fillStyle=ft.color;ctx.fillText(ft.text,ft.x-this.cam.x,ft.y-this.cam.y); ctx.restore(); }); ctx.restore(); this.drawMiniMap(ctx,W,H); } drawMiniMap(ctx,W,H){ const mW=100,mH=78,mX=W-mW-8,mY=50; const sx=mW/this.MAP_W,sy=mH/this.MAP_H; ctx.save(); ctx.fillStyle="rgba(0,0,0,.6)";ctx.beginPath();ctx.roundRect(mX,mY,mW,mH,6);ctx.fill(); ctx.strokeStyle="rgba(255,255,255,.15)";ctx.lineWidth=1;ctx.stroke(); this.enemies.forEach(e=>{ctx.fillStyle=e.isBoss?"#ffd700":e.color;ctx.beginPath();ctx.arc(mX+e.x*sx,mY+e.y*sy,e.isBoss?5:3,0,Math.PI*2);ctx.fill();}); this.food.forEach(f=>{ctx.fillStyle="#00ff88";ctx.beginPath();ctx.arc(mX+f.x*sx,mY+f.y*sy,2,0,Math.PI*2);ctx.fill();}); this.powerups.forEach(pu=>{ctx.fillStyle=pu.color;ctx.beginPath();ctx.arc(mX+pu.x*sx,mY+pu.y*sy,3,0,Math.PI*2);ctx.fill();}); ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(mX+this.player.x*sx,mY+this.player.y*sy,4,0,Math.PI*2);ctx.fill(); ctx.strokeStyle="#ff6b35";ctx.lineWidth=1.5;ctx.stroke(); ctx.fillStyle="rgba(255,255,255,.4)";ctx.font="8px sans-serif";ctx.textAlign="left";ctx.fillText("MAP",mX+3,mY+mH-3); ctx.restore(); } start(){ this.running=true; const loop=ts=>{ if(!this.running) return; const dt=Math.min((ts-this.lastTime)/1000,.05); this.lastTime=ts; if(dt>0) this.update(dt); this.draw(); this.raf=requestAnimationFrame(loop); }; this.raf=requestAnimationFrame(ts=>{this.lastTime=ts;loop(ts);}); } end(){ this.running=false; cancelAnimationFrame(this.raf); this.destroy(); this.onEnd({score:this.score,kills:this.kills,xp:this.kills*50+this.score,wave:this.wave,food:this.foodEaten,survived:Math.floor(this.elapsed),maxCombo:this.maxCombo}); } } /* ══════════════════════════════════════════════════════════════════════════ SCREENS ══════════════════════════════════════════════════════════════════════════ */ function LoadingScreen({onDone}) { const [p,setP]=useState(0); useEffect(()=>{const t=setInterval(()=>setP(x=>{if(x>=100){clearInterval(t);setTimeout(onDone,300);return 100;}return x+3;}),50);return()=>clearInterval(t);},[]); return (
🍔
FOOD ARENA
The tastiest battle royale
{p}%
); } /* ── LOBBY ── */ function Lobby({ps,setPs,onPlay,isMobile}) { const [tab,setTab]=useState("home"); const [settings,setSettings]=useState(false); const skin=SKINS.find(s=>s.id===ps.skin)||SKINS[0]; const weap=WEAPONS.find(w=>w.id===ps.weapon)||WEAPONS[0]; const TABS=isMobile ?[["home","🏠"],["brawler","🎮"],["weapons","⚔️"],["pass","🍽️"],["daily","📋"]] :[["home","🏠 Home"],["brawler","🎮 Brawler"],["weapons","⚔️ Weapons"],["pass","🍽️ Food Pass"],["daily","📋 Challenges"]]; return (
{"🍕🍔🌮🍣🍦🍩🧁🌭🍜🥑".split("").map((e,i)=>(
{e}
))}
🍔 {!isMobile&&FOOD ARENA}
{[["🪙",ps.coins,"#ffd700"],["🏆",ps.trophies,"#86efac"],["✨",ps.xp+" XP","#c4b5fd"]].map(([ico,val,col])=>(
{ico} {val}
))}
⚙️
😋
{TABS.map(([id,label])=>(
{label}
))}
{tab==="home" && } {tab==="brawler" && } {tab==="weapons" && } {tab==="pass" && } {tab==="daily" && }
{settings && }
); } function HomeTab({ps,skin,weap,onPlay,isMobile,setTab}) { return (
{!isMobile&&(
{skin.name}
{weap.emoji}
{weap.name}
{weap.rarity}
)}
▶ PLAY NOW
Wave Mode · 2:30 match
YOUR STATS
{[["🏆","Trophies",ps.trophies],["🎮","Games",ps.gamesPlayed],["💀","Kills",ps.totalKills],["🌊","Best Wave",ps.bestWave||1],["🔥","Best Combo",ps.bestCombo||1],["⭐","High Score",ps.highScore||0]].map(([e,l,v])=>(
{e}
{l}
{v}
))}
💡 POWER-UPS
{[["🛡️","Shield","8s invincibility"],["⚡","Speed","55% faster"],["💖","Heal","+30 HP"],["🔋","Ammo","Instant reload"]].map(([e,n,d])=>(
{e}
{n} · {d}
))}
🍽️
FOOD PASS — Season 1 · Lv.{ps.passLevel}
r.level===ps.passLevel+1)?.xpNeeded||99999))*100)}%`,transition:"width .5s"}}/>
{ps.xp} XP total
); } function BrawlerTab({ps,setPs,isMobile}) { const unlocked=ps.unlockedSkins||["default"]; return (
CHOOSE YOUR BRAWLER
{SKINS.map(s=>{ const locked=!unlocked.includes(s.id); return (
{locked&&
🔒
}
{s.name}
{ps.skin===s.id&&
✓ EQUIPPED
}
); })}
); } function WeaponsTab({ps,setPs,isMobile}) { const unlocked=ps.unlockedWeapons||["ketchup","mustard"]; return (
CHOOSE YOUR WEAPON
{WEAPONS.map(w=>{ const locked=!unlocked.includes(w.id); return (
{locked&&
🔒
}
{w.emoji}
{w.name}
{w.rarity}
{ps.weapon===w.id&&}
{w.desc}
{[["💥 DMG",w.dmg,80,"#ef4444"],["⚡ SPD",w.speed,15,"#3b82f6"],["🎯 RNG",w.range,500,"#22c55e"]].map(([l,v,mx,c])=>(
{l}
))}
); })}
); } function PassTab({ps,setPs,isMobile}) { const nextR=PASS_REWARDS.find(r=>r.level>ps.passLevel); const pct=nextR?Math.min(100,(ps.xp/nextR.xpNeeded)*100):100; const claim=r=>{ if(ps.xp>=r.xpNeeded&&r.level>ps.passLevel){ setPs(p=>{ const ns={...p,passLevel:r.level}; if(r.type==="coins") ns.coins=p.coins+r.value; if(r.type==="skin") ns.unlockedSkins=[...(p.unlockedSkins||[]),r.value]; if(r.type==="weapon") ns.unlockedWeapons=[...(p.unlockedWeapons||[]),r.value]; return ns; }); } }; return (
🍽️
FOOD PASS — Season 1
FREE rewards — just play and earn XP!
Level {ps.passLevel} · {ps.xp} XP {nextR&&{nextR.xpNeeded-ps.xp} to Lv.{nextR.level}}
{PASS_REWARDS.map(r=>{ const unlocked=ps.xp>=r.xpNeeded,claimed=ps.passLevel>=r.level; return (
{claimed?"✓":r.level}
{r.emoji}
{r.label}
{r.xpNeeded} XP
{!claimed&&unlocked?() :claimed?(✓ Done) :(🔒)}
); })}
); } function DailyTab({ps,isMobile}) { return (
DAILY CHALLENGES
Resets in 12h · Complete for bonus coins & XP
{DAILY.map(ch=>{ const prog=ps.dailyProgress?.[ch.id]||0,done=prog>=ch.goal,pct=Math.min(100,(prog/ch.goal)*100); return (
{ch.emoji}
{ch.desc}
Reward: 🪙{ch.reward} + ✨{Math.floor(ch.reward*.5)} XP
{done&&}
{prog}/{ch.goal}
); })}
); } function SettingsModal({ps,setPs,onClose}) { const [name,setName]=useState(ps.name); return (
⚙️ Settings
Player Name
{[["🔊 Sound Effects","sfx"],["🎵 Music","music"],["📳 Haptics","haptics"],["🎯 Auto-Aim (mobile)","autoAim"]].map(([label,key])=>(
{label}
))}
); } /* ── GAME SCREEN ── */ function GameScreen({ps,onGameEnd,isMobile}) { const canvasRef=useRef(null); const engineRef=useRef(null); const moveJoy=useFloatJoy(); const aimJoy=useFloatJoy(); const [hud,setHud]=useState({hp:100,maxHp:100,ammo:8,maxAmmo:8,reload:false,reloadT:0,score:0,kills:0,wave:1,timeLeft:150,combo:0,shielded:false,speedy:false}); const [paused,setPaused]=useState(false); const [,forceUpdate]=useState(0); // for joystick visual re-renders // Feed joystick into engine each frame useEffect(()=>{ let raf; const loop=()=>{ if(engineRef.current){ engineRef.current.joyMove=moveJoy.output.current; engineRef.current.joyAim=aimJoy.output.current; } forceUpdate(n=>n+1); raf=requestAnimationFrame(loop); }; raf=requestAnimationFrame(loop); return()=>cancelAnimationFrame(raf); },[]); useEffect(()=>{ const canvas=canvasRef.current; if(!canvas) return; const resize=()=>{canvas.width=canvas.offsetWidth;canvas.height=canvas.offsetHeight;}; resize(); const eng=new GameEngine(canvas,ps,onGameEnd,setHud); engineRef.current=eng; eng.start(); const ro=new ResizeObserver(resize); ro.observe(canvas); return()=>{ eng.running=false; eng.destroy(); cancelAnimationFrame(eng.raf); ro.disconnect(); }; },[]); const togglePause=()=>{ const eng=engineRef.current; if(!eng) return; if(paused){eng.running=true;eng.start();setPaused(false);} else{eng.running=false;cancelAnimationFrame(eng.raf);setPaused(true);} }; const weap=WEAPONS.find(w=>w.id===ps.weapon)||WEAPONS[0]; const m=Math.floor(hud.timeLeft/60),s=Math.floor(hud.timeLeft%60); const comboMult=hud.combo>0?Math.floor(hud.combo/3)+1:1; return (
⭐ {hud.score} {m}:{s.toString().padStart(2,"0")} 🌊 {hud.wave} 💀 {hud.kills}
{hud.combo>=3&&(
🔥 {hud.combo}x COMBO · x{comboMult}
)}
{hud.shielded?"🛡️":"❤️"}
.4?"#22c55e":"#ef4444",borderRadius:4,width:`${(hud.hp/hud.maxHp)*100}%`,transition:"width .1s"}}/>
{hud.hp}
{hud.reload ?
🔄
:
{weap.emoji}{Array.from({length:hud.maxAmmo}).map((_,i)=>
)}
} {(hud.shielded||hud.speedy)&&(
{hud.shielded&&🛡️} {hud.speedy&&}
)}
{isMobile&&( <>
MOVE
AIM & SHOOT
)} {!isMobile&&
WASD: Move · Mouse: Aim · Click / Space: Shoot · F: Shoot
} {paused&&(
⏸ PAUSED
)}
); } /* ── GAME OVER ── */ function GameOverScreen({result,ps,onBack,isMobile}) { const newHigh=result.score>(ps.highScore||0); return (
{newHigh&&
🎉 NEW HIGH SCORE!
}
{result.survived<10?"💀 DEFEATED!":"🏆 TIME'S UP!"}
{[["⭐","Score",result.score],["💀","Kills",result.kills],["🌊","Wave",result.wave],["✨","XP",result.xp],["🍎","Food",result.food],["🔥","Best Combo",result.maxCombo||0]].map(([e,l,v])=>(
{e}
{v}
{l}
))}
Season XP · Pass Level {ps.passLevel}
{ps.xp} XP total
); } /* ══════════════════════════════════════════════════════════════════════════ MAIN APP ══════════════════════════════════════════════════════════════════════════ */ export default function FoodArena() { const [screen,setScreen]=useState("loading"); const [result,setResult]=useState(null); const [isMobile,setMobile]=useState(false); useEffect(()=>{ const check=()=>setMobile(window.innerWidth<700||'ontouchstart' in window); check(); window.addEventListener("resize",check); return()=>window.removeEventListener("resize",check); },[]); const [ps,setPs]=useState({ name:"Chef Player", level:7, skin:"default", weapon:"ketchup", coins:850, trophies:142, xp:650, passLevel:2, gamesPlayed:24, totalKills:87, bestWave:1, highScore:0, bestCombo:0, unlockedSkins: ["default","burger"], unlockedWeapons: ["ketchup","mustard","icecream","pizza"], sfx:true, music:true, haptics:true, autoAim:false, dailyProgress:{ kills5:2, score500:180, food10:7, survive90:45, waves3:1, combo10:0 }, }); const handleGameEnd=useCallback(res=>{ setPs(p=>{ const dp={...p.dailyProgress}; dp.kills5 =Math.min(5, (dp.kills5||0)+res.kills); dp.score500=Math.min(500,(dp.score500||0)+res.score); dp.food10 =Math.min(10, (dp.food10||0)+(res.food||0)); dp.survive90=Math.min(90,(dp.survive90||0)+(res.survived||0)); dp.waves3 =Math.min(3, (dp.waves3||0)+(res.wave>1?1:0)); dp.combo10 =Math.min(10, (dp.combo10||0)+(res.maxCombo||0)); return { ...p, xp:p.xp+res.xp, trophies:p.trophies+Math.floor(res.score/50), gamesPlayed:p.gamesPlayed+1, totalKills:p.totalKills+res.kills, bestWave:Math.max(p.bestWave||1,res.wave||1), highScore:Math.max(p.highScore||0,res.score), bestCombo:Math.max(p.bestCombo||0,res.maxCombo||0), dailyProgress:dp, }; }); setResult(res); setScreen("gameover"); },[]); return (
{screen==="loading" && } {screen==="lobby" && } {screen==="game" && } {screen==="gameover" && }
); }q