'use strict'; /* ============================================================ TOWER DEFENSE β€” 100 WAVES Place towers, evolve them through 4 stages, survive 100 waves. ============================================================ */ const cv = document.getElementById('td100game'); const ctx = cv.getContext('2d'); // ---------- Layout ---------- const W = 960, HUD_H = 64, COLS = 20, ROWS = 12, CELL = 48; const GRID_Y = HUD_H, GRID_H = ROWS * CELL; // 576 const SHOP_Y = GRID_Y + GRID_H; // 640 const H = SHOP_Y + 120; // 760 // ---------- Path (cell coords) ---------- const WAY_CELLS = [[-1,2],[3,2],[3,5],[8,5],[8,9],[14,9],[14,4],[21,4]]; const waypoints = WAY_CELLS.map(([c,r]) => ({ x:(c+0.5)*CELL, y:GRID_Y+(r+0.5)*CELL })); const blocked = new Set(); (function buildBlocked(){ for(let s=0;s=0&&c=0&&r({type:t.type,evo:t.evo,gx:t.gx,gy:t.gy,invested:t.invested})) })); }catch(e){} } function loadGame(){ try{ const s=JSON.parse(localStorage.getItem(SAVE_KEY)); if(!s||!s.wave||s.wave>100) return false; wave=s.wave; gold=s.gold; lives=s.lives; score=s.score||0; towers=[]; unlocked=Object.assign({gun:true,cannon:false,frost:false,laser:false,sniper:false}, s.unlocked||{}); buyStage=Object.assign({gun:0,cannon:0,frost:0,laser:0,sniper:0}, s.buyStage||{}); for(const st of (s.towers||[])){ towers.push({ type:st.type, evo:st.evo, gx:st.gx, gy:st.gy, x:(st.gx+0.5)*CELL, y:GRID_Y+(st.gy+0.5)*CELL, cd:0, angle:-Math.PI/2, invested:st.invested, beam:null, pulse:null, target:null }); } return true; }catch(e){ return false; } } function hasSave(){ try{ const s=JSON.parse(localStorage.getItem(SAVE_KEY)); return !!(s&&s.wave&&s.wave<=100); } catch(e){ return false; } } function clearSave(){ try{ localStorage.removeItem(SAVE_KEY); }catch(e){} } // ---------- Helpers ---------- function towerStats(t){ return TOWER_DEFS[t.type].evo[t.evo]; } function fmt(n){ return Math.floor(n).toLocaleString('en-US'); } function rr(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); } function addText(x,y,text,color,size){ texts.push({x,y,text,color:color||'#ffe27a',size:size||15,ttl:1.1}); } function cellCenter(gx,gy){ return { x:(gx+0.5)*CELL, y:GRID_Y+(gy+0.5)*CELL }; } function towerAt(gx,gy){ return towers.find(t=>t.gx===gx&&t.gy===gy); } // ================= WAVE GENERATION ================= function waveSpec(n){ const isGiga = (n===100), isBoss = (n%10===0)&&!isGiga, isMini = (n%5===0)&&!isBoss&&!isGiga; const count = isGiga ? 150 : Math.min(10+Math.floor(n*1.5), 120); const hp = Math.round(22*Math.pow(1.084,n) + 5*n); const speed = Math.min(52 + n*1.0, 145); const size = Math.min(9 + n*0.18, 27); const reward= 5 + n; return { n, isGiga, isBoss, isMini, count, hp, speed, size, reward, interval: Math.max(0.22, 0.9 - n*0.006) }; } function powerMult(){ return 1+0.06*wave; } function showAd(rewarded){ adPopup={t:5,rewarded:!!rewarded}; paused=true; } function startWave(){ if(state!=='play'||waveActive||wave>100) return; const s = waveSpec(wave); queue = []; const mk=(hpM,spM,szM,rwM,kind)=>({hp:s.hp*hpM,maxHp:s.hp*hpM,speed:s.speed*spM, size:s.size*szM,reward:Math.max(1,Math.round(s.reward*rwM)),kind}); if(s.isGiga){ queue.push(mk(25,0.6,2.3,40,'giga')); queue.push(mk(6,0.7,1.5,8,'mini')); queue.push(mk(6,0.7,1.5,8,'mini')); for(let i=0;i<147;i++){ const tank=s.n>=12&&i%5===4, fast=i%4===3; queue.push(mk(tank?2.5:(fast?0.6:1),tank?0.75:(fast?1.6:1),tank?1.3:(fast?0.75:1),tank?2:1,tank?'tank':(fast?'fast':'normal'))); } } else if(s.isBoss){ const ne=Math.min(6+Math.floor(s.n/10),12); for(let i=0;i=12&&i%5===4; const fast=(s.n>=8&&i%4===3)||(s.isMini&&i%3===0); queue.push(mk(tank?2.5:(fast?0.6:1),tank?0.75:(fast?1.6:1),tank?1.3:(fast?0.75:1),tank?2:1,tank?'tank':(fast?'fast':'normal'))); } } for(const t of SHOP_ORDER){ if(!unlocked[t]&&wave>=UNLOCK_WAVE[t]){ unlocked[t]=true; addText(W/2, GRID_Y+210, 'πŸ”“ '+TOWER_DEFS[t].name.toUpperCase()+' UNLOCKED!', '#7CFC9A', 24); sfx('evolve'); } } spawnInterval = s.interval; spawnT = 0.3; waveActive = true; towerSel = null; shopSel = null; sfx('start'); addText(W/2, GRID_Y+120, s.isGiga?'⚠ GIGA WAVE 100 β€” ALL OF THEM ⚠':(s.isBoss?('⚠ BOSS WAVE '+wave+' ⚠'):(s.isMini?('MINI-BOSS β€” WAVE '+wave):('WAVE '+wave))), s.isGiga?'#ff1744':'#ffd166', 34); } function spawnEnemy(q){ enemies.push({ x:waypoints[0].x, y:waypoints[0].y, wp:1, dist:0, hp:q.hp, maxHp:q.maxHp, baseSpeed:q.speed, size:q.size, reward:q.reward, kind:q.kind||'normal', slowUntil:0, slowAmt:0, wob:Math.random()*6.28, dead:false }); } function killEnemy(e){ if(e.dead) return; e.dead = true; gold += e.reward; score += e.reward*10 + wave*5; explosions.push({x:e.x,y:e.y,r:e.size*0.6,max:e.size*2.4,ttl:0.3,t:0}); const bigK=e.kind==='mini'||e.kind==='boss'||e.kind==='giga'; addText(e.x, e.y-14, '+$'+e.reward, '#ffe27a', bigK?20:14); sfx(bigK?'evolve':'kill'); } // ================= ACTIONS ================= function tryPlace(gx,gy){ const def = TOWER_DEFS[shopSel]; if(!def||!unlocked[shopSel]) return; if(gx<0||gx>=COLS||gy<0||gy>=ROWS) return; if(blocked.has(gx+','+gy)||towerAt(gx,gy)){ deny('Blocked'); return; } const stage=buyStage[shopSel]||0, cost=stageCost(shopSel,stage); if(gold=3) return; const cost = TOWER_DEFS[t.type].evo[t.evo+1].cost; if(goldt.type===type&&t.evo<3) .sort((a,b)=>def.evo[a.evo+1].cost-def.evo[b.evo+1].cost); if(!cands.length){ deny('All maxed'); return; } let n=0; for(const t of cands){ const cost=def.evo[t.evo+1].cost; if(goldx!==t); towerSel = null; sfx('coin'); if(!waveActive) saveGame(); } function deny(msg){ shakeT=0.35; shakeMsg=msg; sfx('leak'); } function gameOver(){ state='defeat'; waveActive=false; clearSave(); sfx('leak'); } function victory(){ state='victory'; waveActive=false; clearSave(); sfx('evolve'); } function newGame(){ wave=1; gold=300; lives=20; score=0; unlocked={gun:true,cannon:false,frost:false,laser:false,sniper:false}; buyStage={gun:0,cannon:0,frost:0,laser:0,sniper:0}; towers=[]; enemies=[]; projectiles=[]; queue=[]; tracers=[]; sparks=[]; explosions=[]; pulses=[]; texts=[]; waveActive=false; speed=1; paused=false; time=0; shopSel=null; towerSel=null; leakFlash=0; autoMode=false; autoWaves=0; adPopup=null; clearSave(); state='play'; } // ================= UPDATE ================= function update(dt){ if(adPopup){ adPopup.t-=dt; return; } if(state!=='play'||paused) return; const PM=powerMult(); const sdt = Math.min(dt,0.05)*speed; time += sdt; if(leakFlash>0) leakFlash-=sdt; // spawning if(waveActive && queue.length){ spawnT -= sdt; while(spawnT<=0 && queue.length){ spawnEnemy(queue.shift()); spawnT += spawnInterval; } } // enemies move for(let i=enemies.length-1;i>=0;i--){ const e=enemies[i]; if(e.dead) continue; const slowed = time < e.slowUntil; const sp = e.baseSpeed * (slowed ? (1-e.slowAmt) : 1); let rem = sp*sdt; while(rem>0 && e.wp=waypoints.length){ e.dead=true; const dmg = e.kind==='giga'?10 : e.kind==='boss'?5 : e.kind==='mini'?3 : 1; lives -= dmg; addText(W-90, GRID_Y+50, '-'+dmg+' ❀', '#ff5d5d', 22); leakFlash=0.4; sfx('leak'); if(lives<=0){ lives=0; gameOver(); return; } } } // towers fire for(const t of towers){ t.cd -= sdt; const st = towerStats(t), def = TOWER_DEFS[t.type]; const R = st.range*CELL; let best=null, bd=-1; for(const e of enemies){ if(e.dead) continue; if(Math.hypot(e.x-t.x,e.y-t.y) <= R + e.size*0.4 && e.dist>bd){ bd=e.dist; best=e; } } t.target = best; if(best) t.angle = Math.atan2(best.y-t.y, best.x-t.x); if(!best){ t.beam=null; continue; } if(t.type==='laser'){ best.hp -= st.dps*PM*sdt; t.beam = {x:best.x, y:best.y, ttl:0.12}; if(best.hp<=0) killEnemy(best); } else if(t.type==='frost'){ if(t.cd<=0){ t.cd = 1/st.rate; pulses.push({x:t.x,y:t.y,r:10,max:R,ttl:0.4,t:0,color:def.color}); sfx('frost'); for(const e of enemies){ if(e.dead) continue; if(Math.hypot(e.x-t.x,e.y-t.y)<=R){ e.hp -= st.dmg*PM; if(time>=e.slowUntil || st.slow>e.slowAmt){ e.slowUntil=time+1.6; e.slowAmt=st.slow; } if(e.hp<=0) killEnemy(e); } } } } else if(t.cd<=0){ t.cd = 1/st.rate; if(t.type==='cannon'){ const d = Math.hypot(best.x-t.x,best.y-t.y); projectiles.push({x:t.x,y:t.y,tx:best.x,ty:best.y,t:0, dur:Math.max(0.12,d/480),dmg:st.dmg*PM,splash:st.splash*CELL}); sfx('cannon'); } else { best.hp -= st.dmg*PM; tracers.push({x1:t.x,y1:t.y,x2:best.x,y2:best.y,ttl:0.09,color:def.color,big:t.type==='sniper'}); sparks.push({x:best.x,y:best.y,ttl:0.14,r:t.type==='sniper'?11:6}); sfx(t.type); if(best.hp<=0) killEnemy(best); } } } // projectiles for(let i=projectiles.length-1;i>=0;i--){ const p=projectiles[i]; p.t += sdt; const k = Math.min(1, p.t/p.dur); p.x += (p.tx-p.x)*Math.min(1,sdt*14); p.y += (p.ty-p.y)*Math.min(1,sdt*14); if(k>=1){ projectiles.splice(i,1); explosions.push({x:p.tx,y:p.ty,r:8,max:p.splash,ttl:0.32,t:0}); for(const e of enemies){ if(e.dead) continue; if(Math.hypot(e.x-p.tx,e.y-p.ty)<=p.splash){ e.hp-=p.dmg; if(e.hp<=0) killEnemy(e); } } } } // sweep dead if(enemies.some(e=>e.dead)) enemies = enemies.filter(e=>!e.dead); // effects decay for(const a of [tracers,sparks,explosions,pulses]) for(let i=a.length-1;i>=0;i--){ a[i].ttl-=sdt; if(a[i].ttl<=0) a.splice(i,1); } for(let i=texts.length-1;i>=0;i--){ const x=texts[i]; x.ttl-=sdt; x.y-=34*sdt; if(x.ttl<=0) texts.splice(i,1); } for(const t of towers){ if(t.beam){ t.beam.ttl-=sdt; if(t.beam.ttl<=0) t.beam=null; } } if(shakeT>0) shakeT-=sdt; // wave complete? if(waveActive && !queue.length && !enemies.length){ waveActive=false; const bonus = 25 + wave*3; gold += bonus; score += 100 + wave*10; addText(W/2, GRID_Y+170, 'WAVE '+wave+' CLEAR +$'+bonus, '#7CFC9A', 24); sfx('coin'); wave++; if(wave>100){ victory(); } else if(autoWaves>0){ autoWaves--; if(autoWaves===0){ autoMode=false; saveGame(); showAd(false); } else { saveGame(); startWave(); } } else saveGame(); } } // ================= LAYOUT (shared by draw + clicks) ================= const UI = { mute: {x:W-96, y:12, w:40, h:40}, restart:{x:W-52, y:12, w:40, h:40}, auto:{x:648, y:12, w:100, h:40}, ad:{x:754, y:12, w:40, h:40}, cards: SHOP_ORDER.map((t,i)=>({x:12+i*160, y:SHOP_Y+12, w:150, h:96, type:t})), start: {x:824, y:SHOP_Y+8, w:124, h:54}, speed: {x:824, y:SHOP_Y+68, w:60, h:44}, pause: {x:890, y:SHOP_Y+68, w:58, h:44}, evolve: {x:560, y:SHOP_Y+16, w:170, h:46}, evolveAll: {x:560, y:SHOP_Y+68, w:336, h:36}, sell: {x:740, y:SHOP_Y+16, w:100, h:46}, pclose: {x:850, y:SHOP_Y+16, w:46, h:46}, menuStart: {x:W/2-150, y:498, w:300, h:60}, menuContinue:{x:W/2-150, y:568, w:300, h:54}, again: {x:W/2-150, y:500, w:300, h:60}, adClose:{x:W/2-90, y:H/2+96, w:180, h:52}, }; function inRect(x,y,r){ return x>=r.x&&x<=r.x+r.w&&y>=r.y&&y<=r.y+r.h; } // ================= RENDER ================= function render(){ ctx.save(); if(shakeT>0) ctx.translate((Math.random()-0.5)*8*shakeT*3,(Math.random()-0.5)*8*shakeT*3); drawBG(); drawGrid(); drawPath(); drawPortals(); drawPlacementGhost(); for(const t of towers) drawTower(t); for(const e of enemies) drawEnemy(e); drawProjectiles(); drawEffects(); drawTexts(); drawHUD(); drawBottom(); if(leakFlash>0){ ctx.fillStyle=`rgba(255,40,40,${leakFlash*0.5})`; ctx.fillRect(0,0,W,H); } if(shakeT>0&&shakeMsg){ ctx.fillStyle='#ff8a8a'; ctx.font='bold 18px sans-serif'; ctx.textAlign='center'; ctx.fillText(shakeMsg, W/2, GRID_Y+40); } if(state==='menu') drawMenu(); if(state==='victory') drawEnd(true); if(state==='defeat') drawEnd(false); if(adPopup) drawAdPopup(); ctx.restore(); } function drawBG(){ const g=ctx.createLinearGradient(0,0,0,H); g.addColorStop(0,'#070c1e'); g.addColorStop(0.6,'#0a1330'); g.addColorStop(1,'#060a1a'); ctx.fillStyle=g; ctx.fillRect(0,0,W,H); ctx.fillStyle='rgba(255,255,255,0.05)'; for(let i=0;i<70;i++){ const x=(i*137.5)%W, y=(i*89.3)%(GRID_Y+GRID_H); ctx.fillRect(x,y,2,2); } } function drawGrid(){ ctx.strokeStyle='rgba(120,160,255,0.06)'; ctx.lineWidth=1; ctx.beginPath(); for(let c=0;c<=COLS;c++){ ctx.moveTo(c*CELL,GRID_Y); ctx.lineTo(c*CELL,GRID_Y+GRID_H); } for(let r=0;r<=ROWS;r++){ ctx.moveTo(0,GRID_Y+r*CELL); ctx.lineTo(W,GRID_Y+r*CELL); } ctx.stroke(); } function strokePath(){ ctx.beginPath(); ctx.moveTo(waypoints[0].x,waypoints[0].y); for(let i=1;i=COLS||gy<0||gy>=ROWS) return; const c=cellCenter(gx,gy), def=TOWER_DEFS[shopSel], st=def.evo[buyStage[shopSel]||0]; const ok=!blocked.has(gx+','+gy)&&!towerAt(gx,gy)&&gold>=def.cost; ctx.globalAlpha=0.35; ctx.fillStyle=ok?def.color:'#ff5252'; ctx.beginPath(); ctx.arc(c.x,c.y,st.range*CELL,0,7); ctx.fill(); ctx.globalAlpha=0.8; ctx.fillStyle=def.color; ctx.beginPath(); ctx.arc(c.x,c.y,16,0,7); ctx.fill(); ctx.globalAlpha=1; if(!ok){ ctx.strokeStyle='#ff5252'; ctx.lineWidth=4; ctx.beginPath(); ctx.moveTo(c.x-12,c.y-12); ctx.lineTo(c.x+12,c.y+12); ctx.moveTo(c.x+12,c.y-12); ctx.lineTo(c.x-12,c.y+12); ctx.stroke(); } } function drawTower(t){ const def=TOWER_DEFS[t.type], st=towerStats(t); // selection / placement range if(towerSel===t){ ctx.fillStyle='rgba(255,255,255,0.10)'; ctx.beginPath(); ctx.arc(t.x,t.y,st.range*CELL,0,7); ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.setLineDash([8,6]); ctx.beginPath(); ctx.arc(t.x,t.y,st.range*CELL,0,7); ctx.stroke(); ctx.setLineDash([]); } // shadow + base ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.ellipse(t.x,t.y+16,20,8,0,0,7); ctx.fill(); ctx.fillStyle='#1a2447'; rr(t.x-20,t.y-20,40,40,10); ctx.fill(); ctx.strokeStyle=def.color; ctx.lineWidth=2.5; rr(t.x-20,t.y-20,40,40,10); ctx.stroke(); // glyph ctx.save(); ctx.translate(t.x,t.y); const evoScale=1+t.evo*0.12; ctx.scale(evoScale,evoScale); ctx.fillStyle=def.color; ctx.strokeStyle=def.color; if(t.type==='gun'||t.type==='cannon'||t.type==='sniper'){ ctx.rotate(t.angle); ctx.lineWidth=t.type==='cannon'?9:5; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(t.type==='sniper'?26:20,0); ctx.stroke(); ctx.beginPath(); ctx.arc(0,0,t.type==='cannon'?11:9,0,7); ctx.fill(); ctx.fillStyle='#0a1330'; ctx.beginPath(); ctx.arc(0,0,4,0,7); ctx.fill(); } else if(t.type==='laser'){ ctx.rotate(time*1.5); ctx.beginPath(); for(let i=0;i<4;i++){ const a=i*Math.PI/2; ctx.moveTo(Math.cos(a)*4,Math.sin(a)*4); ctx.lineTo(Math.cos(a)*15,Math.sin(a)*15); } ctx.lineWidth=5; ctx.stroke(); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(0,0,4,0,7); ctx.fill(); } else { // frost ctx.lineWidth=3.5; for(let i=0;i<3;i++){ ctx.rotate(Math.PI/3); ctx.beginPath(); ctx.moveTo(-12,0); ctx.lineTo(12,0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(7,-5); ctx.lineTo(12,0); ctx.lineTo(7,5); ctx.stroke(); } } ctx.restore(); // evolution pips for(let i=0;i<4;i++){ ctx.fillStyle=i<=t.evo?def.color:'rgba(255,255,255,0.18)'; ctx.beginPath(); ctx.arc(t.x-15+i*10,t.y+26,3.4,0,7); ctx.fill(); } if(t.beam){ const fl=0.75+Math.random()*0.25; const g=ctx.createLinearGradient(t.x,t.y,t.beam.x,t.beam.y); g.addColorStop(0,'rgba(255,255,255,0.95)'); g.addColorStop(1,def.color); ctx.strokeStyle=g; ctx.lineWidth=(5+t.evo*2)*fl; ctx.beginPath(); ctx.moveTo(t.x,t.y); ctx.lineTo(t.beam.x,t.beam.y); ctx.stroke(); ctx.fillStyle='rgba(255,120,140,0.8)'; ctx.beginPath(); ctx.arc(t.beam.x,t.beam.y,6+t.evo*2,0,7); ctx.fill(); } } function drawEnemy(e){ const bigE = e.kind==='boss'||e.kind==='mini'||e.kind==='giga'; const hue = e.kind==='giga'?0 : e.kind==='boss'?278 : e.kind==='mini'?340 : e.kind==='tank'?210 : Math.max(0, 128 - wave*1.22); const wob = Math.sin(e.wob)*e.size*0.08; const r = e.size + wob; if(bigE){ // spikes ctx.fillStyle=`hsl(${hue},70%,30%)`; ctx.beginPath(); for(let i=0;i<10;i++){ const a=i*Math.PI/5+time*0.8; const x1=e.x+Math.cos(a)*r*0.9, y1=e.y+Math.sin(a)*r*0.9; const x2=e.x+Math.cos(a+0.31)*r*1.5, y2=e.y+Math.sin(a+0.31)*r*1.5; ctx.moveTo(e.x,e.y); ctx.lineTo(x1,y1); ctx.lineTo(x2,y2); } ctx.fill(); } const g=ctx.createRadialGradient(e.x-r*0.3,e.y-r*0.3,r*0.2,e.x,e.y,r); g.addColorStop(0,`hsl(${hue},95%,${bigE?60:68}%)`); g.addColorStop(1,`hsl(${hue},85%,${bigE?32:38}%)`); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(e.x,e.y,r,0,7); ctx.fill(); if(e.kind==='tank'){ ctx.strokeStyle='rgba(200,210,230,0.85)'; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(e.x,e.y,r+5,0,7); ctx.stroke(); } if(time0.5?'#5dff7a':p>0.25?'#ffd166':'#ff5252'; ctx.fillRect(e.x-w/2,e.y-r-12,w*p,6); } } function drawProjectiles(){ for(const p of projectiles){ ctx.fillStyle='#ffb347'; ctx.beginPath(); ctx.arc(p.x,p.y,6,0,7); ctx.fill(); ctx.fillStyle='rgba(255,180,70,0.35)'; ctx.beginPath(); ctx.arc(p.x,p.y,11,0,7); ctx.fill(); } } function drawEffects(){ for(const t of tracers){ ctx.globalAlpha=Math.max(0,t.ttl/0.09); ctx.strokeStyle=t.color; ctx.lineWidth=t.big?4:2.5; ctx.beginPath(); ctx.moveTo(t.x1,t.y1); ctx.lineTo(t.x2,t.y2); ctx.stroke(); } ctx.globalAlpha=1; for(const s of sparks){ ctx.globalAlpha=Math.max(0,s.ttl/0.15); ctx.fillStyle='#fff7c4'; ctx.beginPath(); ctx.arc(s.x,s.y,s.r*(s.ttl/0.15),0,7); ctx.fill(); } ctx.globalAlpha=1; for(const x of explosions){ const k=1-x.ttl/0.32, r=x.r+(x.max-x.r)*k; ctx.globalAlpha=Math.max(0,1-k); ctx.strokeStyle='#ffb347'; ctx.lineWidth=5; ctx.beginPath(); ctx.arc(x.x,x.y,r,0,7); ctx.stroke(); ctx.fillStyle='rgba(255,200,100,0.5)'; ctx.beginPath(); ctx.arc(x.x,x.y,r*0.6,0,7); ctx.fill(); } ctx.globalAlpha=1; for(const p of pulses){ const k=1-p.ttl/0.5, r=8+(p.max-8)*k; ctx.globalAlpha=Math.max(0,1-k)*0.9; ctx.strokeStyle=p.color; ctx.lineWidth=4; ctx.beginPath(); ctx.arc(p.x,p.y,r,0,7); ctx.stroke(); } ctx.globalAlpha=1; } function drawTexts(){ ctx.textAlign='center'; for(const t of texts){ ctx.globalAlpha=Math.min(1,t.ttl*2); ctx.font=`bold ${t.size}px sans-serif`; ctx.strokeStyle='rgba(0,0,0,0.7)'; ctx.lineWidth=3; ctx.strokeText(t.text,t.x,t.y); ctx.fillStyle=t.color; ctx.fillText(t.text,t.x,t.y); } ctx.globalAlpha=1; } function drawAdPopup(){ ctx.fillStyle='rgba(2,4,12,0.88)'; ctx.fillRect(0,0,W,H); const bx=W/2-240, by=H/2-170, bw=480, bh=340; ctx.fillStyle='#0d1530'; rr(bx,by,bw,bh,16); ctx.fill(); ctx.strokeStyle='#38e1ff'; ctx.lineWidth=2; rr(bx,by,bw,bh,16); ctx.stroke(); ctx.textAlign='center'; ctx.fillStyle='#9fb3ff'; ctx.font='bold 14px sans-serif'; ctx.fillText('β€” ADVERTISEMENT β€”', W/2, by+34); ctx.fillStyle='rgba(56,225,255,0.06)'; rr(bx+40,by+54,bw-80,170,10); ctx.fill(); ctx.setLineDash([8,6]); ctx.strokeStyle='rgba(160,180,220,0.4)'; ctx.lineWidth=1.5; rr(bx+40,by+54,bw-80,170,10); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle='#cfe0ff'; ctx.font='16px sans-serif'; ctx.fillText(adPopup.rewarded?'πŸ“Ί Thanks for watching!':'πŸ“Ί Your ad here', W/2, by+130); ctx.fillStyle='#9fb3ff'; ctx.font='13px sans-serif'; ctx.fillText('Real ads activate once the AdSense', W/2, by+158); ctx.fillText('ad codes are connected.', W/2, by+178); if(adPopup.rewarded){ ctx.fillStyle='#7CFC9A'; ctx.font='bold 15px sans-serif'; ctx.fillText('Reward: +5 AUTO waves', W/2, by+206); } const c=UI.adClose, can=adPopup.t<=0; ctx.fillStyle=can?'rgba(76,224,138,0.25)':'rgba(90,90,110,0.4)'; rr(c.x,c.y,c.w,c.h,10); ctx.fill(); ctx.strokeStyle=can?'#4be08a':'#666'; ctx.lineWidth=2; rr(c.x,c.y,c.w,c.h,10); ctx.stroke(); ctx.fillStyle=can?'#c9ffd9':'#999'; ctx.font='bold 16px sans-serif'; ctx.fillText(can?'βœ• CLOSE':('…'+Math.ceil(adPopup.t)), c.x+c.w/2, c.y+32); ctx.textAlign='left'; } function drawHUD(){ ctx.fillStyle='rgba(4,8,20,0.85)'; ctx.fillRect(0,0,W,HUD_H); ctx.strokeStyle='rgba(56,225,255,0.25)'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(0,HUD_H); ctx.lineTo(W,HUD_H); ctx.stroke(); ctx.textAlign='left'; ctx.fillStyle='#fff'; ctx.font='bold 20px sans-serif'; ctx.fillText(`WAVE ${Math.min(wave,100)}/100`, 16, 30); ctx.font='bold 19px sans-serif'; ctx.fillStyle='#ffe27a'; ctx.fillText(`πŸ’° ${fmt(gold)}`, 16, 54); ctx.fillStyle = lives<=5 ? '#ff5d5d' : '#ff8a8a'; ctx.fillText(`❀ ${lives}`, 150, 54); ctx.fillStyle='#9fb3ff'; ctx.font='16px sans-serif'; ctx.fillText(`SCORE ${fmt(score)}`, 230, 54); if(waveActive){ const left=queue.length+enemies.length; ctx.fillStyle='#ffd166'; ctx.font='bold 17px sans-serif'; ctx.textAlign='center'; ctx.fillText(`πŸ‘Ύ ${left} left`, W/2, 40); ctx.textAlign='left'; } else if(state==='play'&&wave<=100){ ctx.fillStyle='#7CFC9A'; ctx.font='bold 17px sans-serif'; ctx.textAlign='center'; ctx.fillText('Ready β€” start the wave when you are', W/2, 40); ctx.textAlign='left'; } if(logoReady){ const lhh=30, lww=lhh*logoImg.width/logoImg.height; ctx.drawImage(logoImg, W-108-lww, 17, lww, lhh); } else { ctx.fillStyle='rgba(160,180,220,0.5)'; ctx.font='11px sans-serif'; ctx.textAlign='right'; ctx.fillText('WIKIBUSINESS ARCADE', W-110, 56); ctx.textAlign='left'; } // buttons const ab=UI.auto; ctx.fillStyle=autoMode?'rgba(76,224,138,0.22)':'rgba(56,225,255,0.10)'; rr(ab.x,ab.y,ab.w,ab.h,8); ctx.fill(); ctx.strokeStyle=autoMode?'#4be08a':'rgba(56,225,255,0.4)'; ctx.lineWidth=1.5; rr(ab.x,ab.y,ab.w,ab.h,8); ctx.stroke(); ctx.fillStyle=autoMode?'#c9ffd9':'#cfe9ff'; ctx.font='bold 13px sans-serif'; ctx.textAlign='center'; ctx.fillText(autoWaves>0?('AUTO Γ—'+autoWaves):(autoMode?'AUTO ON':'AUTO'), ab.x+ab.w/2, ab.y+26); ctx.textAlign='left'; hudBtn(UI.ad, 'πŸ“Ί'); hudBtn(UI.mute, muted?'πŸ”‡':'πŸ”Š'); hudBtn(UI.restart, '⟳'); } function hudBtn(r,icon){ ctx.fillStyle='rgba(56,225,255,0.10)'; rr(r.x,r.y,r.w,r.h,8); ctx.fill(); ctx.strokeStyle='rgba(56,225,255,0.4)'; ctx.lineWidth=1.5; rr(r.x,r.y,r.w,r.h,8); ctx.stroke(); ctx.fillStyle='#cfe9ff'; ctx.font='20px sans-serif'; ctx.textAlign='center'; ctx.fillText(icon, r.x+r.w/2, r.y+27); ctx.textAlign='left'; } function drawBottom(){ ctx.fillStyle='rgba(4,8,20,0.92)'; ctx.fillRect(0,SHOP_Y,W,H-SHOP_Y); ctx.strokeStyle='rgba(56,225,255,0.25)'; ctx.beginPath(); ctx.moveTo(0,SHOP_Y); ctx.lineTo(W,SHOP_Y); ctx.stroke(); if(towerSel) drawTowerPanel(); else drawShop(); } function drawShop(){ ctx.fillStyle='#9fb3ff'; ctx.font='bold 13px sans-serif'; ctx.textAlign='left'; ctx.fillText('BUILD (tap a tower, then tap the map)', 16, SHOP_Y+10); for(let i=0;i=price:gold>=UNLOCK_COST[type]; ctx.fillStyle=!isU?'rgba(12,18,38,0.95)':(sel?'rgba(56,225,255,0.16)':'rgba(20,30,60,0.9)'); rr(c.x,c.y,c.w,c.h,10); ctx.fill(); ctx.strokeStyle=!isU?'rgba(130,130,150,0.25)':(sel?def.color:(afford?'rgba(120,160,255,0.35)':'rgba(120,120,140,0.2)')); ctx.lineWidth=sel?3:1.5; rr(c.x,c.y,c.w,c.h,10); ctx.stroke(); ctx.fillStyle=!isU?'#5a5a6a':def.color; rr(c.x,c.y,c.w,6,3); ctx.fill(); if(!isU){ const uc=UNLOCK_COST[type], can=gold>=uc; ctx.globalAlpha=can?1:0.55; ctx.fillStyle='#fff'; ctx.font='bold 15px sans-serif'; ctx.fillText('\uD83D\uDD12 '+def.name, c.x+12, c.y+32); ctx.fillStyle=can?'#7CFC9A':'#ffe27a'; ctx.font='bold 14px sans-serif'; ctx.fillText('UNLOCK $'+uc, c.x+12, c.y+54); ctx.fillStyle='#9fb3ff'; ctx.font='12px sans-serif'; ctx.fillText('or free at wave '+UNLOCK_WAVE[type], c.x+12, c.y+74); ctx.globalAlpha=1; } else { ctx.globalAlpha=afford?1:0.45; ctx.fillStyle='#fff'; ctx.font='bold 16px sans-serif'; ctx.fillText(def.name, c.x+12, c.y+30); ctx.fillStyle='#ffe27a'; ctx.font='bold 15px sans-serif'; ctx.fillText('$'+fmt(price)+(stage>0?' '+EVO_ROMAN[stage]:''), c.x+12, c.y+52); ctx.fillStyle='rgba(200,220,255,0.55)'; ctx.font='bold 12px sans-serif'; ctx.fillText('['+(i+1)+']', c.x+c.w-30, c.y+30); if(sel){ for(let s=0;s<4;s++){ const r=stageRect(i,s), on=s===stage; ctx.fillStyle=on?def.color:'rgba(56,225,255,0.08)'; rr(r.x,r.y,r.w,r.h,6); ctx.fill(); ctx.strokeStyle=on?'#ffffff':def.color; ctx.lineWidth=on?2:1; rr(r.x,r.y,r.w,r.h,6); ctx.stroke(); ctx.fillStyle=on?'#06121f':def.color; ctx.font='bold 12px sans-serif'; ctx.textAlign='center'; ctx.fillText(EVO_ROMAN[s], r.x+r.w/2, r.y+19); ctx.textAlign='left'; } } else { ctx.fillStyle='#9fb3ff'; ctx.font='12px sans-serif'; ctx.fillText(def.desc, c.x+12, c.y+70); } ctx.globalAlpha=1; } } // start / speed / pause const s=UI.start; ctx.fillStyle=waveActive?'rgba(90,90,110,0.5)':'rgba(60,220,120,0.25)'; rr(s.x,s.y,s.w,s.h,10); ctx.fill(); ctx.strokeStyle=waveActive?'#666':'#4be08a'; ctx.lineWidth=2; rr(s.x,s.y,s.w,s.h,10); ctx.stroke(); ctx.fillStyle=waveActive?'#999':'#c9ffd9'; ctx.font='bold 16px sans-serif'; ctx.textAlign='center'; ctx.fillText(waveActive?'FIGHTING…':(autoMode?'β–Ά AUTO Γ—5':'β–Ά START'), s.x+s.w/2, s.y+23); ctx.font='bold 13px sans-serif'; ctx.fillText(waveActive?'wave '+wave:'wave '+Math.min(wave,100), s.x+s.w/2, s.y+42); ctx.textAlign='left'; bigBtn(UI.speed, speed+'Γ—'); bigBtn(UI.pause, paused?'β–Ά':'⏸'); } function bigBtn(r,label){ ctx.fillStyle='rgba(56,225,255,0.10)'; rr(r.x,r.y,r.w,r.h,10); ctx.fill(); ctx.strokeStyle='rgba(56,225,255,0.4)'; ctx.lineWidth=1.5; rr(r.x,r.y,r.w,r.h,10); ctx.stroke(); ctx.fillStyle='#cfe9ff'; ctx.font='bold 18px sans-serif'; ctx.textAlign='center'; ctx.fillText(label, r.x+r.w/2, r.y+29); ctx.textAlign='left'; } function statLine(type,st,pm){ pm=pm||1; const D=v=>Math.round(v*pm); if(type==='laser') return `DPS ${D(st.dps)} β€’ RANGE ${st.range.toFixed(1)}`; if(type==='frost') return `DMG ${D(st.dmg)} β€’ SLOW ${Math.round(st.slow*100)}% β€’ RANGE ${st.range.toFixed(1)}`; const s=`DMG ${D(st.dmg)} β€’ ${st.rate}/s β€’ RANGE ${st.range.toFixed(1)}`; return type==='cannon'? s+` β€’ SPLASH ${st.splash.toFixed(1)}` : s; } function drawTowerPanel(){ const t=towerSel, def=TOWER_DEFS[t.type], st=towerStats(t), PM=powerMult(); ctx.fillStyle='rgba(20,30,60,0.95)'; rr(8,SHOP_Y+6,W-16,108,10); ctx.fill(); ctx.strokeStyle=def.color; ctx.lineWidth=2; rr(8,SHOP_Y+6,W-16,108,10); ctx.stroke(); ctx.fillStyle='#fff'; ctx.font='bold 19px sans-serif'; ctx.fillText(`${def.name} β€” Evolution ${EVO_ROMAN[t.evo]} βš”Γ—${PM.toFixed(1)}`, 28, SHOP_Y+36); ctx.fillStyle='#9fb3ff'; ctx.font='14px sans-serif'; ctx.fillText(statLine(t.type,st,PM), 28, SHOP_Y+60); ctx.fillStyle='rgba(200,220,255,0.6)'; ctx.font='12px sans-serif'; if(t.evo<3){ const nx=def.evo[t.evo+1]; ctx.fillText('Next ('+EVO_ROMAN[t.evo+1]+'): '+statLine(t.type,nx,PM), 28, SHOP_Y+84); } else ctx.fillText('MAX evolution reached β€” a true monster.', 28, SHOP_Y+84); // evolve const e=UI.evolve; const canEvo=t.evo<3, cost=canEvo?def.evo[t.evo+1].cost:0, afford=gold>=cost; ctx.fillStyle=!canEvo?'rgba(90,90,110,0.5)':(afford?'rgba(60,220,120,0.25)':'rgba(120,60,60,0.25)'); rr(e.x,e.y,e.w,e.h,10); ctx.fill(); ctx.strokeStyle=!canEvo?'#666':(afford?'#4be08a':'#a05050'); ctx.lineWidth=2; rr(e.x,e.y,e.w,e.h,10); ctx.stroke(); ctx.fillStyle=!canEvo?'#999':(afford?'#c9ffd9':'#ffb0b0'); ctx.font='bold 16px sans-serif'; ctx.textAlign='center'; ctx.fillText(canEvo?`⬆ EVOLVE $${cost}`:'MAX', e.x+e.w/2, e.y+29); // evolve all of same type const ea=UI.evolveAll; const nAll=towers.filter(x=>x.type===t.type&&x.evo<3).length; const allCost=evolveAllCost(t.type); const affordAll=nAll>0&&gold>=allCost; ctx.fillStyle=nAll===0?'rgba(90,90,110,0.5)':(affordAll?'rgba(60,220,120,0.25)':'rgba(120,60,60,0.25)'); rr(ea.x,ea.y,ea.w,ea.h,9); ctx.fill(); ctx.strokeStyle=nAll===0?'#666':(affordAll?'#4be08a':'#a05050'); ctx.lineWidth=2; rr(ea.x,ea.y,ea.w,ea.h,9); ctx.stroke(); ctx.fillStyle=nAll===0?'#999':(affordAll?'#c9ffd9':'#ffb0b0'); ctx.font='bold 15px sans-serif'; ctx.textAlign='center'; ctx.fillText(nAll===0?'ALL MAXED':`\u2B06\u2B06 EVOLVE ALL ${nAll} - $${fmt(allCost)}`, ea.x+ea.w/2, ea.y+24); ctx.textAlign='left'; // sell const sl=UI.sell, val=Math.floor(t.invested*0.7); ctx.fillStyle='rgba(255,180,80,0.18)'; rr(sl.x,sl.y,sl.w,sl.h,10); ctx.fill(); ctx.strokeStyle='#ffb050'; ctx.lineWidth=2; rr(sl.x,sl.y,sl.w,sl.h,10); ctx.stroke(); ctx.fillStyle='#ffe0b0'; ctx.fillText(`SELL +$${val}`, sl.x+sl.w/2, sl.y+29); // close const c=UI.pclose; ctx.fillStyle='rgba(120,140,180,0.15)'; rr(c.x,c.y,c.w,c.h,10); ctx.fill(); ctx.strokeStyle='#8fa0c0'; ctx.lineWidth=2; rr(c.x,c.y,c.w,c.h,10); ctx.stroke(); ctx.fillStyle='#dfe8ff'; ctx.fillText('βœ•', c.x+c.w/2, c.y+29); ctx.textAlign='left'; } function drawMenu(){ ctx.fillStyle='rgba(2,4,12,0.82)'; ctx.fillRect(0,0,W,H); ctx.textAlign='center'; if(logoReady){ const lw=280, lh=lw*logoImg.height/logoImg.width; ctx.drawImage(logoImg, W/2-lw/2, 26, lw, lh); } ctx.fillStyle='#38e1ff'; ctx.font='bold 44px sans-serif'; ctx.fillText('TOWER DEFENSE', W/2, 228); ctx.fillStyle='#ffd166'; ctx.font='bold 26px sans-serif'; ctx.fillText('100 WAVES', W/2, 260); ctx.fillStyle='#cfe0ff'; ctx.font='16px sans-serif'; const lines=[ 'Enemies march from the portal (left) to your base (right).', 'Tap a tower below, then tap the map to build it.', 'New towers unlock every 5 waves β€” or pay to unlock early.', 'Pick a stage on a tower card to buy it pre-evolved.', 'Pick a stage on a card to buy it pre-evolved. Tap a tower to evolve/sell.', 'Mini-boss every 5 waves, BOSS every 10, GIGA at wave 100.', 'Towers grow +6% stronger every wave. Kills earn gold.', 'Leaks cost ❀ (mini 3, boss 5, giga 10). Hold the line!', 'AUTO chains 5 waves then shows an ad β€” πŸ“Ί grants free AUTO.', ]; lines.forEach((l,i)=>ctx.fillText(l, W/2, 284+i*24)); ctx.fillStyle='#9fb3ff'; ctx.font='14px sans-serif'; ctx.fillText('Keys: 1-5 build β€’ Space start β€’ A auto β€’ F speed β€’ P pause', W/2, 476); menuBtn(UI.menuStart,'β–Ά START NEW GAME','#4be08a'); if(hasSave()) menuBtn(UI.menuContinue,'↻ CONTINUE β€” WAVE '+JSON.parse(localStorage.getItem(SAVE_KEY)).wave,'#38e1ff'); ctx.textAlign='left'; } function menuBtn(r,label,color){ ctx.fillStyle='rgba(10,16,36,0.95)'; rr(r.x,r.y,r.w,r.h,12); ctx.fill(); ctx.strokeStyle=color; ctx.lineWidth=3; rr(r.x,r.y,r.w,r.h,12); ctx.stroke(); ctx.fillStyle=color; ctx.font='bold 21px sans-serif'; ctx.textAlign='center'; ctx.fillText(label, r.x+r.w/2, r.y+37); ctx.textAlign='left'; } function drawEnd(win){ ctx.fillStyle='rgba(2,4,12,0.85)'; ctx.fillRect(0,0,W,H); ctx.textAlign='center'; ctx.fillStyle=win?'#7CFC9A':'#ff5d5d'; ctx.font='bold 54px sans-serif'; ctx.fillText(win?'πŸ† VICTORY!':'πŸ’€ BASE OVERRUN', W/2, 220); ctx.fillStyle='#cfe0ff'; ctx.font='20px sans-serif'; ctx.fillText(win?'You survived all 100 waves. Legendary commander.': `You fell on wave ${Math.min(wave,100)}. The blobs send their regards.`, W/2, 280); ctx.fillStyle='#ffe27a'; ctx.font='bold 22px sans-serif'; ctx.fillText(`SCORE ${fmt(score)} β€’ TOWERS ${towers.length}`, W/2, 330); ctx.fillStyle='#9fb3ff'; ctx.font='16px sans-serif'; ctx.fillText(win?'Your max-evolution army held the line. Play again?':'Evolve more, slow them down, try again.', W/2, 370); menuBtn(UI.again, win?'↻ PLAY AGAIN':'↻ TRY AGAIN', win?'#7CFC9A':'#ff8a8a'); ctx.textAlign='left'; } // ================= INPUT ================= function canvasPos(ev){ const r=cv.getBoundingClientRect(); const cx=(ev.touches&&ev.touches[0]?ev.touches[0].clientX:ev.clientX); const cy=(ev.touches&&ev.touches[0]?ev.touches[0].clientY:ev.clientY); return { x:(cx-r.left)*W/r.width, y:(cy-r.top)*H/r.height }; } cv.addEventListener('pointerdown', ev=>{ ev.preventDefault(); try{ AC=AC||new (window.AudioContext||window.webkitAudioContext)(); if(AC.state==='suspended')AC.resume(); }catch(e){} const p=canvasPos(ev); handleTap(p.x,p.y); }); cv.addEventListener('pointermove', ev=>{ const p=canvasPos(ev); if(p.y>=GRID_Y&&p.yev.preventDefault()); function handleTap(x,y){ if(adPopup){ if(adPopup.t<=0&&inRect(x,y,UI.adClose)){ const rw=adPopup.rewarded; adPopup=null; paused=false; sfx('coin'); if(rw){ autoWaves=5; autoMode=true; addText(W/2,GRID_Y+160,'πŸ“Ί +5 AUTO WAVES!','#7CFC9A',24); } } return; } if(state==='menu'){ if(inRect(x,y,UI.menuStart)){ newGame(); return; } if(hasSave()&&inRect(x,y,UI.menuContinue)){ if(loadGame()){ state='play'; } return; } return; } if(state==='victory'||state==='defeat'){ if(inRect(x,y,UI.again)){ state='menu'; } return; } // HUD if(inRect(x,y,UI.auto)){ autoMode=!autoMode; if(autoMode&&autoWaves===0) autoWaves=5; if(!autoMode) autoWaves=0; sfx('coin'); return; } if(inRect(x,y,UI.ad)){ if(state==='play') showAd(true); return; } if(inRect(x,y,UI.mute)){ muted=!muted; return; } if(inRect(x,y,UI.restart)){ saveGame(); state='menu'; return; } // bottom area if(y>=SHOP_Y){ if(towerSel){ if(inRect(x,y,UI.evolve)){ tryEvolve(); return; } if(inRect(x,y,UI.evolveAll)){ tryEvolveAll(); return; } if(inRect(x,y,UI.sell)){ trySell(); return; } if(inRect(x,y,UI.pclose)){ towerSel=null; return; } return; } for(let i=0;i=3?1:speed+1; return; } if(inRect(x,y,UI.pause)){ paused=!paused; return; } return; } // grid if(y>=GRID_Y&&y{ if(state!=='play') return; const k=ev.key.toLowerCase(); const idx=['1','2','3','4','5'].indexOf(k); if(idx>=0){ const t=SHOP_ORDER[idx]; if(!unlocked[t]){ tryUnlock(t); } else if(shopSel===t){ buyStage[t]=((buyStage[t]||0)+1)%4; sfx('coin'); } else { shopSel=t; towerSel=null; sfx('coin'); } } else if(k==='escape'){ shopSel=null; towerSel=null; } else if(k===' '){ ev.preventDefault(); if(autoMode&&!waveActive&&autoWaves===0) autoWaves=5; startWave(); } else if(k==='a'){ autoMode=!autoMode; if(autoMode&&autoWaves===0) autoWaves=5; if(!autoMode) autoWaves=0; sfx('coin'); } else if(k==='f'){ speed=speed>=3?1:speed+1; } else if(k==='p'){ paused=!paused; } else if(k==='m'){ muted=!muted; } }); window.addEventListener('pagehide', saveGame); // ================= MAIN LOOP ================= let lastT=performance.now(); function loop(now){ const dt=(now-lastT)/1000; lastT=now; update(dt); render(); requestAnimationFrame(loop); } render(); requestAnimationFrame(loop);