SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,279
Folders
408
Scanned Size
3.84 GB
PHP Files
891
Editable Text Files
12,540
File viewer
guarded to /htdocs
/arcade/games/DEFENDER.HTML
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" /> <title>SIMON Arcade • Gaylord Sinclair — Logo Defender</title> <meta name="theme-color" content="#070b16" /> <style> :root{ --bg:#070b16; --bg2:#050812; --glass:rgba(255,255,255,.06); --glass2:rgba(255,255,255,.035); --edge:rgba(255,255,255,.16); --txt:#eaf2ff; --mut:#a9b6d9; --cyan:#6ae0ff; --ind:#7b62ff; --pink:#ff4fd6; --green:#44d18a; --warn:#ffcc4d; --bad:#ff4f6d; --r:18px; --shadow:0 18px 60px rgba(0,0,0,.55); } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; overflow:hidden; color:var(--txt); font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"; background: radial-gradient(1200px 700px at 50% -10%, #142457, var(--bg) 62%), radial-gradient(900px 520px at 20% 110%, #2a0f4a, transparent 55%), radial-gradient(900px 520px at 110% 80%, #063d4d, transparent 55%), linear-gradient(180deg, var(--bg2), var(--bg)); } #stage{position:fixed; inset:0; width:100vw; height:100vh; display:block} .topbar{ position:fixed; left:14px; right:14px; top:14px; display:flex; align-items:center; justify-content:space-between; gap:12px; z-index:30; pointer-events:none; } .brand,.hud{ pointer-events:auto; display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:999px; background:linear-gradient(180deg,var(--glass),var(--glass2)); border:1px solid var(--edge); box-shadow:var(--shadow); backdrop-filter: blur(10px); } .logo{width:34px; height:34px; filter: drop-shadow(0 0 10px rgba(106,224,255,.22))} .brandTxt{display:flex; flex-direction:column; line-height:1.05} .brandTxt b{font-size:12.5px; letter-spacing:.18em} .brandTxt span{font-size:11px; color:var(--mut); letter-spacing:.08em} .hud{gap:10px; flex-wrap:wrap} .pill{ display:flex; align-items:baseline; gap:8px; padding:6px 10px; border-radius:999px; background:rgba(0,0,0,.18); border:1px solid rgba(255,255,255,.10); } .pill label{font-size:10px; color:var(--mut); letter-spacing:.12em} .pill strong{font-size:13px} .pill small{font-size:11px; color:var(--mut)} .btn{ cursor:pointer; user-select:none; padding:8px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.18); background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04)); color:var(--txt); font-weight:900; letter-spacing:.06em; font-size:11px; transition: transform .08s ease, filter .2s ease; } .btn:hover{filter:brightness(1.12)} .btn:active{transform:translateY(1px)} .btn.good{border-color:rgba(68,209,138,.45)} .btn.bad{border-color:rgba(255,79,109,.45)} .btn.cyan{border-color:rgba(106,224,255,.45)} .btn.pink{border-color:rgba(255,79,214,.45)} .btn.warn{border-color:rgba(255,204,77,.45)} .dock{ position:fixed; left:12px; right:12px; bottom:12px; z-index:35; display:flex; justify-content:center; gap:10px; flex-wrap:wrap; pointer-events:none; } .dock .btn, .dock .slot{pointer-events:auto} .slot{ display:flex; gap:8px; align-items:center; padding:10px 12px; border-radius:999px; background:linear-gradient(180deg,var(--glass),var(--glass2)); border:1px solid var(--edge); box-shadow:var(--shadow); backdrop-filter: blur(10px); font-size:12px; } .slot b{letter-spacing:.10em} .slot small{color:var(--mut)} .kbd{border:1px solid rgba(255,255,255,.14); border-bottom-color:rgba(255,255,255,.08); padding:2px 6px; border-radius:7px; background:rgba(0,0,0,.18); font-size:11px; color:var(--mut)} .overlay{ position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center; padding:22px; background: radial-gradient(900px 520px at 50% 0%, rgba(20,36,87,.35), rgba(7,11,22,.88)); backdrop-filter: blur(6px); } .panel{ width:min(980px, 100%); border-radius: calc(var(--r) + 10px); border:1px solid var(--edge); background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03)); box-shadow: var(--shadow); padding:18px; } .grid{display:grid; grid-template-columns: 1.05fr .95fr; gap:14px} @media (max-width: 900px){ .grid{grid-template-columns:1fr} } .hero{ border-radius: var(--r); border:1px solid rgba(255,255,255,.12); background: radial-gradient(900px 400px at 20% 0%, rgba(106,224,255,.14), transparent 55%), radial-gradient(900px 420px at 110% 40%, rgba(255,79,214,.12), transparent 50%), rgba(0,0,0,.18); padding:16px; } h1{margin:0 0 8px; font-size:20px; letter-spacing:.14em} .sub{color:var(--mut); font-size:12.5px; line-height:1.5} .rules{ margin-top:12px; display:grid; grid-template-columns: 1fr 1fr; gap:10px; } .card{ border-radius:14px; border:1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.16); padding:10px; } .card b{display:block; font-size:12px; letter-spacing:.08em} .card span{display:block; font-size:12px; color:var(--mut); margin-top:4px; line-height:1.35} .controls{ border-radius: var(--r); border:1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.18); padding:14px; } .row{ display:flex; align-items:center; justify-content:space-between; gap:10px; padding:10px 0; border-bottom:1px solid rgba(255,255,255,.08); } .row:last-child{border-bottom:none} .row .k{font-size:12px; color:var(--mut); letter-spacing:.10em} .row .v{display:flex; align-items:center; gap:10px; flex-wrap:wrap} select{ background: rgba(0,0,0,.22); color: var(--txt); border:1px solid rgba(255,255,255,.15); border-radius: 12px; padding:8px 10px; outline:none; } .toast{ position:fixed; left:50%; bottom:18px; transform: translateX(-50%); z-index:50; padding:10px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.16); background: rgba(0,0,0,.35); backdrop-filter: blur(8px); color:var(--txt); font-size:12px; letter-spacing:.06em; opacity:0; pointer-events:none; transition: opacity .25s ease, transform .25s ease; } .toast.on{ opacity:1; transform: translateX(-50%) translateY(-4px); } /* Debug console (auto shows only if error occurs) */ #err{ position:fixed; left:12px; right:12px; top:74px; z-index:9999; display:none; padding:10px 12px; border-radius:14px; border:1px solid rgba(255,255,255,.16); background: rgba(0,0,0,.65); backdrop-filter: blur(10px); box-shadow: var(--shadow); font: 12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; color: #ffd2da; white-space:pre-wrap; } </style> </head> <body> <canvas id="stage"></canvas> <div id="err"></div> <div class="topbar"> <div class="brand" title="Gaylord Sinclair"> <svg class="logo" viewBox="0 0 64 64" aria-hidden="true"> <defs> <radialGradient id="g1" cx="35%" cy="25%" r="75%"> <stop offset="0%" stop-color="#6ae0ff" stop-opacity="1"/> <stop offset="45%" stop-color="#7b62ff" stop-opacity=".95"/> <stop offset="100%" stop-color="#ff4fd6" stop-opacity=".95"/> </radialGradient> </defs> <circle cx="32" cy="32" r="28" fill="url(#g1)" opacity=".95"/> <circle cx="32" cy="32" r="18" fill="rgba(0,0,0,.18)" /> <path d="M18 34c5-10 23-20 28-10 5 10-8 20-18 20-6 0-12-4-10-10z" fill="rgba(255,255,255,.28)"/> <path d="M20 30c8-10 28-12 28 2" stroke="rgba(255,255,255,.45)" stroke-width="2" fill="none" stroke-linecap="round"/> </svg> <div class="brandTxt"> <b>GAYLORD SINCLAIR</b> <span>SIMON Arcade • Logo Defender</span> </div> </div> <div class="hud"> <div class="pill"><label>WAVE</label><strong id="wave">1</strong></div> <div class="pill"><label>HP</label><strong id="hp">100</strong><small>%</small></div> <div class="pill"><label>SIMO</label><strong id="cash">120</strong></div> <div class="pill"><label>SCORE</label><strong id="score">0</strong></div> <div class="pill"><label>BEST</label><strong id="best">0</strong></div> <button class="btn cyan" id="btnPause">PAUSE</button> <button class="btn pink" id="btnMenu">MENU</button> </div> </div> <div class="dock"> <div class="slot"><b>TOWER</b> <span id="selName">PULSE</span> <small>•</small> <span id="selCost">40</span> <small>SIMO</small></div> <button class="btn cyan" id="btnPulse">PULSE <span class="kbd">1</span></button> <button class="btn pink" id="btnLaser">LASER <span class="kbd">2</span></button> <button class="btn warn" id="btnFreeze">FREEZE <span class="kbd">3</span></button> <button class="btn good" id="btnUpgrade">UPGRADE <span class="kbd">U</span></button> <button class="btn bad" id="btnSell">SELL <span class="kbd">X</span></button> <button class="btn bad" id="btnReset">RESET</button> </div> <div class="overlay" id="menu" style="display:flex"> <div class="panel"> <div class="grid"> <div class="hero"> <h1>LOGO DEFENDER</h1> <div class="sub"> Enemies rush the center to breach the <b>Gaylord Core</b>. Place towers on the grid to stop them. Earn <b>SIMO</b> from kills, upgrade towers, and survive waves. 🛡️✨ </div> <div class="rules"> <div class="card"><b style="color:var(--cyan)">PULSE</b><span>AOE burst. Great early wave control.</span></div> <div class="card"><b style="color:var(--pink)">LASER</b><span>High single-target DPS. Melts bosses.</span></div> <div class="card"><b style="color:var(--warn)">FREEZE</b><span>Slows enemies in range. Control tool.</span></div> <div class="card"><b style="color:var(--green)">UPGRADE</b><span>Click a tower then upgrade for more power.</span></div> </div> <div class="sub" style="margin-top:10px"> Controls: Click grid to place • <b>1/2/3</b> select • <b>U</b> upgrade • <b>X</b> sell • <b>P</b> pause • <b>Esc</b> menu </div> </div> <div class="controls"> <div class="row"> <div class="k">DIFFICULTY</div> <div class="v"> <select id="diff"> <option value="chill">Chill</option> <option value="arcade" selected>Arcade</option> <option value="hard">Hard</option> </select> </div> </div> <div class="row"> <div class="k">SOUND</div> <div class="v"><button class="btn cyan" id="btnSound">SOUND: ON</button></div> </div> <div class="row"> <div class="k">VISUAL</div> <div class="v"> <button class="btn" id="btnTrails">TRAILS: ON</button> <button class="btn" id="btnShake">SHAKE: ON</button> </div> </div> <div class="row"> <div class="k">START</div> <div class="v"> <button class="btn good" id="btnStart">PLAY</button> <button class="btn bad" id="btnClearBest">CLEAR BEST</button> </div> </div> <div class="sub" style="margin-top:10px"> Tip: Put <b>Freeze</b> near the center so everything slows before the Core. </div> </div> </div> </div> </div> <div class="toast" id="toast">Ready</div> <script> (() => { "use strict"; // ===== Error Console (shows the *real* problem if anything crashes) const errBox = document.getElementById("err"); function showErr(msg){ errBox.style.display="block"; errBox.textContent = "LOGO DEFENDER ERROR:\n\n" + msg; } window.addEventListener("error", (e)=> showErr(e.message + (e.filename?("\n"+e.filename+":"+e.lineno+":"+e.colno):""))); window.addEventListener("unhandledrejection", (e)=> showErr(String(e.reason || e))); // ===== Utils const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const rand=(a,b)=>a+Math.random()*(b-a); const lerp=(a,b,t)=>a+(b-a)*t; // ===== Canvas const c=document.getElementById("stage"); const ctx=c.getContext("2d",{alpha:true}); let W=0,H=0,DPR=1; function resize(){ DPR=Math.min(2, devicePixelRatio||1); W=Math.floor(innerWidth); H=Math.floor(innerHeight); c.width=Math.floor(W*DPR); c.height=Math.floor(H*DPR); c.style.width=W+"px"; c.style.height=H+"px"; ctx.setTransform(DPR,0,0,DPR,0,0); } addEventListener("resize", resize, { passive:true }); resize(); // ===== DOM const $=(id)=>document.getElementById(id); const elWave=$("wave"), elHp=$("hp"), elCash=$("cash"), elScore=$("score"), elBest=$("best"); const selName=$("selName"), selCost=$("selCost"); const menu=$("menu"), toast=$("toast"); const btnPause=$("btnPause"), btnMenu=$("btnMenu"), btnStart=$("btnStart"), btnReset=$("btnReset"); const btnPulse=$("btnPulse"), btnLaser=$("btnLaser"), btnFreeze=$("btnFreeze"); const btnUpgrade=$("btnUpgrade"), btnSell=$("btnSell"); const btnSound=$("btnSound"), btnTrails=$("btnTrails"), btnShake=$("btnShake"), btnClearBest=$("btnClearBest"); const diffSel=$("diff"); // ===== Storage const BEST_KEY="gs_logo_defender_best"; let best=Number(localStorage.getItem(BEST_KEY)||"0"); elBest.textContent=String(best); // ===== Audio (synth) let audioOn=true, audioCtx=null; function ensureAudio(){ if(!audioOn) return; if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==="suspended") audioCtx.resume().catch(()=>{}); } function tone(type="tick", gain=0.06, dur=0.08){ if(!audioOn) return; ensureAudio(); if(!audioCtx) return; const now=audioCtx.currentTime; const o=audioCtx.createOscillator(); const g=audioCtx.createGain(); const f = type==="place" ? rand(180,260) : type==="pulse" ? rand(90,130) : type==="laser" ? rand(260,420) : type==="freeze"? rand(140,220) : type==="hit" ? rand(70,120) : type==="kill" ? rand(300,520) : type==="bad" ? rand(55,85) : rand(180,280); o.type = (type==="bad") ? "sawtooth" : "triangle"; o.frequency.setValueAtTime(f, now); o.frequency.exponentialRampToValueAtTime(f*1.20, now + dur*0.35); o.frequency.exponentialRampToValueAtTime(f*0.82, now + dur); g.gain.setValueAtTime(0.0001, now); g.gain.exponentialRampToValueAtTime(gain, now + 0.01); g.gain.exponentialRampToValueAtTime(0.0001, now + dur); o.connect(g).connect(audioCtx.destination); o.start(now); o.stop(now + dur + 0.02); } function showToast(msg){ toast.textContent=msg; toast.classList.add("on"); clearTimeout(showToast._t); showToast._t=setTimeout(()=>toast.classList.remove("on"), 950); } // ===== Visual toggles let trailsOn=true, shakeOn=true; let shake={t:0,x:0,y:0}; // ===== Difficulty const DIFF={ chill:{ hp:110, spawn:0.85, enemy:0.92, boss:0.92, reward:1.06, dmg:0.85 }, arcade:{ hp:100, spawn:1.00, enemy:1.00, boss:1.00, reward:1.00, dmg:1.00 }, hard:{ hp:92, spawn:1.12, enemy:1.10, boss:1.14, reward:0.98, dmg:1.10 }, }; let cfg=DIFF.arcade; // ===== Grid layout const grid={ cols:18, rows:12, x:0,y:0,w:0,h:0, cell:0 }; function layout(){ const w = Math.min(W*0.92, 980); const h = Math.min(H*0.70, 620); grid.w=w; grid.h=h; grid.x=(W-w)/2; grid.y=(H-h)/2 + 24; grid.cell = Math.floor(Math.min(w/grid.cols, h/grid.rows)); } layout(); addEventListener("resize", layout, { passive:true }); // ===== Background particles const bg=[]; const BG_N=140; function initBG(){ bg.length=0; for(let i=0;i<BG_N;i++){ bg.push({ x:Math.random()*W, y:Math.random()*H, r:rand(0.8,2.5), vx:rand(-0.10,0.10), vy:rand(-0.08,0.08), a:rand(0.10,0.42), hue:rand(180,320) }); } } initBG(); addEventListener("resize", initBG, { passive:true }); // ===== Game state let running=false, paused=false; let wave=1, hp=100, cash=120, score=0; // IMPORTANT: these used to be stored on a function property; now they’re explicit state: let waveSpawned=0; let spawnTimer=0; let selectedType="pulse"; let selectedTower=null; // ===== Towers & enemies const towers=[], enemies=[], shots=[], sparks=[]; const TOWER = { pulse: { name:"PULSE", cost:40, range:2.4, rate:0.85, dmg:16, aoe:1.6, hue:195 }, laser: { name:"LASER", cost:65, range:3.1, rate:0.45, dmg:24, aoe:0.0, hue:315 }, freeze: { name:"FREEZE", cost:55, range:2.8, rate:0.95, dmg:6, aoe:0.0, slow:0.55, hue:48 }, }; function setSelected(type){ selectedType=type; selName.textContent = TOWER[type].name; selCost.textContent = String(TOWER[type].cost); showToast(TOWER[type].name + " selected"); } setSelected("pulse"); function corePos(){ const cx = grid.x + grid.w/2; const cy = grid.y + grid.h/2; return { x:cx, y:cy, r:Math.max(22, grid.cell*0.55) }; } function resetRun(){ cfg = DIFF[diffSel.value] || DIFF.arcade; wave=1; hp=cfg.hp; cash=120; score=0; waveSpawned=0; spawnTimer=0; towers.length=0; enemies.length=0; shots.length=0; sparks.length=0; selectedTower=null; running=true; paused=false; showToast("Defend the Core!"); updateHUD(); } function gameOver(){ running=false; showToast("CORE BREACHED"); tone("bad", 0.14, 0.18); if(shakeOn) shake.t=0.55; const final = Math.round(score + wave*500); if(final > best){ best = final; localStorage.setItem(BEST_KEY, String(best)); elBest.textContent=String(best); showToast("NEW BEST!"); tone("kill", 0.10, 0.12); } setTimeout(()=>{ menu.style.display="flex"; paused=true; }, 900); } function updateHUD(){ elWave.textContent=String(wave); elHp.textContent=String(Math.max(0, Math.round(hp))); elCash.textContent=String(Math.round(cash)); elScore.textContent=String(Math.round(score)); elBest.textContent=String(best); } function snapCell(px,py){ const x = Math.floor((px - grid.x)/grid.cell); const y = Math.floor((py - grid.y)/grid.cell); if(x<0||y<0||x>=grid.cols||y>=grid.rows) return null; const cx = grid.x + x*grid.cell + grid.cell/2; const cy = grid.y + y*grid.cell + grid.cell/2; return { gx:x, gy:y, x:cx, y:cy }; } function isBlocked(gx,gy){ const core=corePos(); const cx = grid.x + gx*grid.cell + grid.cell/2; const cy = grid.y + gy*grid.cell + grid.cell/2; if(Math.hypot(cx-core.x, cy-core.y) < core.r*1.8) return true; return towers.some(t=>t.gx===gx && t.gy===gy); } function spawnEnemy(isBoss=false){ const core = corePos(); const pad = 60; const edge = Math.floor(rand(0,4)); let x,y; if(edge===0){ x=rand(grid.x-pad, grid.x+grid.w+pad); y=grid.y-pad; } if(edge===1){ x=grid.x+grid.w+pad; y=rand(grid.y-pad, grid.y+grid.h+pad); } if(edge===2){ x=rand(grid.x-pad, grid.x+grid.w+pad); y=grid.y+grid.h+pad; } if(edge===3){ x=grid.x-pad; y=rand(grid.y-pad, grid.y+grid.h+pad); } const baseHP = (32 + wave*10) * cfg.enemy * (isBoss ? 5.2*cfg.boss : 1); const spd = (34 + wave*2.1) * cfg.spawn * (isBoss ? 0.75 : 1.0); enemies.push({ x,y, r: isBoss ? 22 : 12, hp: baseHP, max: baseHP, spd, slowMul:1, slowT:0, hue: isBoss ? 350 : rand(335,355), boss:isBoss }); } function burst(x,y,hue,count=22,pow=1){ for(let i=0;i<count;i++){ const a=rand(0,Math.PI*2); const s=rand(1.0,4.2)*pow; sparks.push({ x,y, vx:Math.cos(a)*s, vy:Math.sin(a)*s, r:rand(1,3), hue:hue+rand(-18,18), a:1, drag:rand(0.92,0.985) }); } } function doTowerAttack(t, dt){ t.cd = Math.max(0, t.cd - dt); if(t.cd>0) return; let bestE=null, bestD=1e9; for(const e of enemies){ const d=Math.hypot(e.x-t.x, e.y-t.y); if(d<=t.range && d<bestD){ bestD=d; bestE=e; } } if(!bestE) return; t.cd = t.rate; if(t.type==="pulse"){ tone("pulse", 0.055, 0.08); burst(bestE.x, bestE.y, 195, 18, 1.2); const radius = t.aoe; for(const e of enemies){ if(Math.hypot(e.x-bestE.x, e.y-bestE.y)<=radius) e.hp -= t.dmg; } } else if(t.type==="laser"){ tone("laser", 0.045, 0.07); shots.push({ x:t.x, y:t.y, tx:bestE.x, ty:bestE.y, spd:860, hue:315, dmg:t.dmg }); } else if(t.type==="freeze"){ tone("freeze", 0.040, 0.08); burst(bestE.x, bestE.y, 48, 14, 1.0); bestE.hp -= t.dmg; bestE.slowMul = Math.min(bestE.slowMul, t.slow); bestE.slowT = Math.max(bestE.slowT, 1.2); } } function resolveKills(){ for(let i=enemies.length-1;i>=0;i--){ const e=enemies[i]; if(e.hp<=0){ enemies.splice(i,1); cash += (e.boss ? 110 : 18) * cfg.reward; score += (e.boss ? 420 : 75); burst(e.x,e.y, 300, e.boss ? 70 : 28, e.boss ? 1.8 : 1.2); tone("kill", 0.06, 0.07); } } } // ===== UI handlers btnPause.addEventListener("click", ()=>{ if(!running) return; paused=!paused; showToast(paused?"Paused":"Go!"); }); btnMenu.addEventListener("click", ()=>{ menu.style.display="flex"; paused=true; showToast("Menu"); }); btnStart.addEventListener("click", ()=>{ menu.style.display="none"; ensureAudio(); resetRun(); }); btnReset.addEventListener("click", ()=>{ ensureAudio(); resetRun(); }); btnPulse.addEventListener("click", ()=>setSelected("pulse")); btnLaser.addEventListener("click", ()=>setSelected("laser")); btnFreeze.addEventListener("click", ()=>setSelected("freeze")); btnUpgrade.addEventListener("click", ()=>{ if(!selectedTower){ showToast("Select a tower"); return; } const cost = Math.round(selectedTower.cost * 0.75); if(cash < cost){ showToast("Not enough SIMO"); tone("hit",0.06,0.07); return; } cash -= cost; selectedTower.lvl++; selectedTower.dmg *= 1.22; selectedTower.range *= 1.06; selectedTower.rate *= 0.92; if(selectedTower.type==="pulse") selectedTower.aoe *= 1.08; burst(selectedTower.x, selectedTower.y, 195, 18, 1.1); tone("place",0.06,0.07); showToast("Upgraded to L" + selectedTower.lvl); }); btnSell.addEventListener("click", ()=>{ if(!selectedTower){ showToast("Select a tower"); return; } const refund = Math.round(selectedTower.cost * (0.55 + selectedTower.lvl*0.07)); cash += refund; burst(selectedTower.x, selectedTower.y, 350, 16, 1.1); tone("hit",0.06,0.07); towers.splice(towers.indexOf(selectedTower), 1); selectedTower=null; showToast("Sold (+"+refund+" SIMO)"); }); btnSound.addEventListener("click", ()=>{ audioOn=!audioOn; btnSound.textContent="SOUND: " + (audioOn?"ON":"OFF"); if(audioOn) ensureAudio(); showToast(audioOn?"Sound on":"Sound off"); }); btnTrails.addEventListener("click", ()=>{ trailsOn=!trailsOn; btnTrails.textContent="TRAILS: " + (trailsOn?"ON":"OFF"); showToast(trailsOn?"Trails on":"Trails off"); }); btnShake.addEventListener("click", ()=>{ shakeOn=!shakeOn; btnShake.textContent="SHAKE: " + (shakeOn?"ON":"OFF"); showToast(shakeOn?"Shake on":"Shake off"); }); btnClearBest.addEventListener("click", ()=>{ best=0; localStorage.setItem(BEST_KEY,"0"); elBest.textContent="0"; showToast("Best cleared"); }); diffSel.addEventListener("change", ()=>{ cfg = DIFF[diffSel.value] || DIFF.arcade; showToast("Difficulty: " + diffSel.value.toUpperCase()); }); // Place/select towers c.addEventListener("pointerdown", (e)=>{ ensureAudio(); if(!running || paused) return; const rect=c.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; if(mx<grid.x||mx>grid.x+grid.w||my<grid.y||my>grid.y+grid.h) return; const cell = snapCell(mx,my); if(!cell) return; const hit = towers.find(t=>t.gx===cell.gx && t.gy===cell.gy); if(hit){ selectedTower = hit; showToast(hit.type.toUpperCase()+" L"+hit.lvl+" selected"); tone("place",0.04,0.06); return; } const def=TOWER[selectedType]; if(isBlocked(cell.gx, cell.gy)){ showToast("Blocked cell"); tone("hit",0.05,0.06); return; } if(cash < def.cost){ showToast("Not enough SIMO"); tone("hit",0.06,0.07); return; } cash -= def.cost; const t = { type:selectedType, gx:cell.gx, gy:cell.gy, x:cell.x, y:cell.y, lvl:1, cost:def.cost, range: def.range*grid.cell, rate: def.rate, dmg: def.dmg, aoe: def.aoe*grid.cell, slow: def.slow || 1, hue: def.hue, cd: rand(0, def.rate*0.6) }; towers.push(t); selectedTower = t; burst(t.x,t.y, t.hue, 16, 1.1); tone("place",0.06,0.07); showToast(def.name + " placed"); }, { passive:true }); addEventListener("keydown", (e)=>{ const k=e.key.toLowerCase(); if(k==="1") setSelected("pulse"); if(k==="2") setSelected("laser"); if(k==="3") setSelected("freeze"); if(k==="u") btnUpgrade.click(); if(k==="x") btnSell.click(); if(k==="p"){ if(running){ paused=!paused; showToast(paused?"Paused":"Go!"); } } if(e.key==="Escape"){ menu.style.display = (menu.style.display==="none") ? "flex" : "none"; if(menu.style.display!=="none") paused=true; } }); // ===== Render function drawBG(){ for(const p of bg){ p.x+=p.vx; p.y+=p.vy; if(p.x<-20) p.x=W+20; if(p.x>W+20) p.x=-20; if(p.y<-20) p.y=H+20; if(p.y>H+20) p.y=-20; ctx.beginPath(); ctx.fillStyle=`hsla(${p.hue},100%,70%,${p.a})`; ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); } } function drawGrid(){ ctx.save(); ctx.strokeStyle="rgba(255,255,255,.10)"; ctx.lineWidth=1; ctx.strokeRect(grid.x, grid.y, grid.w, grid.h); for(let i=1;i<grid.cols;i++){ const x=grid.x+i*grid.cell; ctx.beginPath(); ctx.strokeStyle="rgba(106,224,255,.06)"; ctx.moveTo(x, grid.y); ctx.lineTo(x, grid.y+grid.h); ctx.stroke(); } for(let j=1;j<grid.rows;j++){ const y=grid.y+j*grid.cell; ctx.beginPath(); ctx.strokeStyle="rgba(123,98,255,.06)"; ctx.moveTo(grid.x, y); ctx.lineTo(grid.x+grid.w, y); ctx.stroke(); } ctx.restore(); } function drawCore(now){ const core=corePos(); ctx.save(); ctx.globalCompositeOperation="lighter"; const g = ctx.createRadialGradient(core.x, core.y, core.r*0.4, core.x, core.y, core.r*4.0); g.addColorStop(0, "rgba(255,255,255,.10)"); g.addColorStop(0.20, "rgba(106,224,255,.18)"); g.addColorStop(0.55, "rgba(123,98,255,.12)"); g.addColorStop(1.0, "rgba(255,79,214,.06)"); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(core.x, core.y, core.r*4.0, 0, Math.PI*2); ctx.fill(); ctx.globalCompositeOperation="source-over"; ctx.fillStyle="rgba(0,0,0,.40)"; ctx.beginPath(); ctx.arc(core.x, core.y, core.r*1.35, 0, Math.PI*2); ctx.fill(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(106,224,255,.40)"; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(core.x, core.y, core.r*1.65 + Math.sin(now*0.004)*1.2, 0, Math.PI*2); ctx.stroke(); ctx.fillStyle="rgba(255,255,255,.22)"; ctx.font="900 12px ui-sans-serif,system-ui"; ctx.textAlign="center"; ctx.textBaseline="middle"; ctx.fillText("GS", core.x, core.y); ctx.restore(); } function drawTower(t, now){ ctx.save(); const sel = (t===selectedTower); ctx.globalCompositeOperation="lighter"; ctx.fillStyle=`hsla(${t.hue},100%,70%,${sel?0.20:0.14})`; ctx.beginPath(); ctx.arc(t.x,t.y, grid.cell*0.72, 0, Math.PI*2); ctx.fill(); ctx.globalCompositeOperation="source-over"; ctx.fillStyle="rgba(0,0,0,.28)"; ctx.beginPath(); ctx.arc(t.x,t.y, grid.cell*0.42, 0, Math.PI*2); ctx.fill(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle = sel ? "rgba(255,255,255,.26)" : `hsla(${t.hue},100%,70%,0.26)`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(t.x,t.y, grid.cell*0.52 + Math.sin(now*0.006 + t.gx)*0.5, 0, Math.PI*2); ctx.stroke(); ctx.globalCompositeOperation="source-over"; ctx.fillStyle="rgba(255,255,255,.22)"; ctx.font="800 10px ui-sans-serif,system-ui"; ctx.textAlign="center"; ctx.textBaseline="middle"; ctx.fillText("L"+t.lvl, t.x, t.y+0.5); if(sel){ ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(106,224,255,.10)"; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(t.x,t.y, t.range, 0, Math.PI*2); ctx.stroke(); } ctx.restore(); } function drawEnemy(e){ ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle=`hsla(${e.hue},100%,65%,${e.boss?0.18:0.12})`; ctx.beginPath(); ctx.arc(e.x,e.y, e.r*3.0, 0, Math.PI*2); ctx.fill(); ctx.globalCompositeOperation="source-over"; ctx.fillStyle=`rgba(255,79,109,${e.boss?0.78:0.62})`; ctx.beginPath(); ctx.arc(e.x,e.y, e.r, 0, Math.PI*2); ctx.fill(); const w = e.boss ? 54 : 38, h = 5; const pct = clamp(e.hp/e.max, 0, 1); ctx.fillStyle="rgba(0,0,0,.35)"; ctx.fillRect(e.x-w/2, e.y-e.r-12, w, h); ctx.fillStyle=`rgba(106,224,255,${0.35 + pct*0.35})`; ctx.fillRect(e.x-w/2, e.y-e.r-12, w*pct, h); ctx.restore(); } function drawSparks(){ for(let i=sparks.length-1;i>=0;i--){ const p=sparks[i]; p.vx*=p.drag; p.vy*=p.drag; p.x += p.vx*2.2; p.y += p.vy*2.2; p.a -= 0.022; if(p.a<=0){ sparks.splice(i,1); continue; } ctx.beginPath(); ctx.fillStyle=`hsla(${p.hue},100%,70%,${p.a})`; ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); } } // ===== Main loop let last=performance.now(); function step(now){ const dt=clamp((now-last)/1000, 0, 0.033); last=now; if(trailsOn){ ctx.fillStyle="rgba(7,11,22,0.16)"; ctx.fillRect(0,0,W,H); } else ctx.clearRect(0,0,W,H); drawBG(); if(!running || paused){ requestAnimationFrame(step); return; } if(shakeOn && shake.t>0){ shake.t -= dt; const amp = 10*(shake.t/0.55); shake.x = rand(-amp, amp); shake.y = rand(-amp, amp); } else { shake.x=0; shake.y=0; } ctx.save(); ctx.translate(shake.x, shake.y); drawGrid(); drawCore(now); // wave spawning const bossWave = (wave % 5 === 0); const quota = (10 + wave*2) + (bossWave ? 3 : 0); const interval = clamp(0.65 - wave*0.02, 0.22, 0.65) / cfg.spawn; spawnTimer += dt; if(spawnTimer >= interval && waveSpawned < quota){ spawnTimer = 0; if(bossWave && waveSpawned === 0) spawnEnemy(true); else spawnEnemy(false); waveSpawned++; } // tower attacks for(const t of towers) doTowerAttack(t, dt); // move shots for(let i=shots.length-1;i>=0;i--){ const s=shots[i]; const dx=s.tx - s.x, dy=s.ty - s.y; const d=Math.hypot(dx,dy) || 1; s.x += (dx/d) * s.spd * dt; s.y += (dy/d) * s.spd * dt; let hit=-1; for(let j=0;j<enemies.length;j++){ if(Math.hypot(enemies[j].x-s.x, enemies[j].y-s.y) < enemies[j].r+6){ hit=j; break; } } if(hit>=0){ enemies[hit].hp -= s.dmg; burst(s.x,s.y, 315, 14, 1.0); shots.splice(i,1); continue; } if(s.x<grid.x-120||s.x>grid.x+grid.w+120||s.y<grid.y-120||s.y>grid.y+grid.h+120) shots.splice(i,1); } // move enemies const core=corePos(); for(const e of enemies){ if(e.slowT>0){ e.slowT = Math.max(0, e.slowT - dt); if(e.slowT===0) e.slowMul = 1; } const dx=core.x - e.x, dy=core.y - e.y; const d=Math.hypot(dx,dy) || 1; const sp = e.spd * e.slowMul; e.x += (dx/d) * sp * dt; e.y += (dy/d) * sp * dt; if(d < core.r + e.r*0.7){ const dmg = (e.boss ? 14 : 6) * cfg.dmg; hp -= dmg; e.hp -= (e.boss ? 18 : 9999); burst(core.x, core.y, 355, e.boss ? 34 : 18, e.boss ? 1.4 : 1.1); tone("hit", 0.08, 0.09); if(shakeOn) shake.t = Math.max(shake.t, e.boss ? 0.22 : 0.14); if(hp<=0){ hp=0; updateHUD(); ctx.restore(); gameOver(); requestAnimationFrame(step); return; } } } resolveKills(); // wave advance if(waveSpawned >= quota && enemies.length===0){ wave++; waveSpawned=0; spawnTimer=0; cash += Math.round(55 * cfg.reward); score += 200; showToast("Wave " + wave); tone("place",0.07,0.09); if(shakeOn) shake.t = Math.max(shake.t, 0.12); } // draw for(const t of towers) drawTower(t, now); for(const e of enemies) drawEnemy(e); drawSparks(); // score drip score += dt * (3 + wave*0.9); ctx.restore(); updateHUD(); requestAnimationFrame(step); } // ===== Init menu.style.display="flex"; paused=true; requestAnimationFrame(step); })(); </script> </body> </html>
Save file
Quick jump
open a path
Open