One Pad Defense — Implement path progress, target eligibility, cooldown and one-time kill/leak reconciliation in a playable local prototype.

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>One Pad Defense</title><style>:root{--bg:#edf3ef;--ink:#173b32;--accent:#286047;--line:#c2d3c7;--wash:#e0efe4;}*{box-sizing:border-box}body{margin:0;font:16px/1.5 Arial,Helvetica,sans-serif;color:var(--ink);background:var(--bg)}main{max-width:1120px;padding:24px;margin:auto}h1,h2,h3,p{margin:0}h1{font-size:clamp(26px,4vw,38px);line-height:1.15;letter-spacing:-.03em}h2{font-size:23px}h3{font-size:18px}header{margin-bottom:20px}header p{margin-top:8px}.eyebrow{text-transform:uppercase;letter-spacing:.13em;font-size:12px;font-weight:bold;color:var(--accent)}button,input,select,textarea{font:inherit}button{cursor:pointer;min-height:44px;padding:9px 15px;border:1px solid var(--accent);background:var(--accent);color:white;border-radius:7px;font-weight:600}button.secondary{background:white;color:var(--accent)}button:disabled{opacity:.45;cursor:default}input,select,textarea{width:100%;min-height:44px;border:1px solid #a2aaba;border-radius:5px;background:white;color:var(--ink);padding:9px}textarea{resize:vertical;min-height:95px}label{display:block;font-size:14px;font-weight:bold;margin:12px 0 5px}input[type=checkbox]{width:20px;min-height:20px;vertical-align:middle;margin-right:8px}:focus-visible{outline:3px solid #df7a16;outline-offset:3px}.actions{display:flex;gap:9px;flex-wrap:wrap;margin:14px 0}.panel{padding:22px;background:white;border:1px solid var(--line);border-radius:12px;min-width:0}.columns{display:grid;grid-template-columns:1.6fr 1fr;gap:20px}.two{display:grid;grid-template-columns:1fr 1fr;gap:18px}.stats{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:15px}.stat{flex:1;min-width:92px;background:var(--wash);padding:10px 14px;border-radius:8px}.stat small{display:block}.stat strong{font-size:25px}.muted{color:#526178;font-size:14px}.notice{padding:14px 16px;background:var(--wash);border-left:4px solid var(--accent);margin:14px 0;overflow-wrap:anywhere}.error{color:#a12e25;font-weight:bold}.below{margin-top:28px;padding-top:22px;border-top:1px solid var(--line)}.below p{margin-top:9px}svg.board{width:100%;display:block;border-radius:10px;border:1px solid var(--line)}table{border-collapse:collapse;width:100%;font-size:14px}td,th{text-align:left;padding:10px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}th{font-size:12px;text-transform:uppercase;letter-spacing:.04em}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:14px/1.6 ui-monospace,monospace}.scroll{overflow-x:auto}.tag{font-size:12px;padding:4px 8px;background:var(--wash);border-radius:4px}.big{font-size:clamp(36px,6vw,62px);font-weight:bold;line-height:1.15}.foot{font-size:14px;color:#526178;margin-top:25px}details{margin:18px 0}summary{cursor:pointer;font-weight:bold;min-height:44px;padding:10px 0}[hidden]{display:none!important}@media(max-width:800px){.columns{grid-template-columns:1fr}.two{grid-template-columns:1fr}}@media(max-width:480px){main{padding:18px 14px}.panel{padding:16px}.stat{min-width:78px;padding:8px}.actions button{flex-grow:1}td,th{padding:7px}.big{font-size:38px}}.ledger-row{display:grid;grid-template-columns:40px 1fr;gap:8px;padding:9px 0;border-bottom:1px solid var(--line)}#event-log{max-height:210px;overflow:auto;font:13px/1.6 ui-monospace,monospace}.field-shell{background:#d3e6cc;padding:12px;border-radius:12px}.legend{display:flex;gap:18px;font-size:13px;margin-top:8px}</style></head><body><main><header><p class="eyebrow">One pad · three enemies · one outcome each</p><h1>Hold the center path</h1><p>Build before the wave, then defend two lives with a 70-pixel tower range.</p></header>
<div class="stats"><div class="stat"><small>Wave state</small><strong id="state">Ready</strong></div><div class="stat"><small>Coins</small><strong id="coins">3</strong></div><div class="stat"><small>Lives</small><strong id="lives">2</strong></div><div class="stat"><small>Simulation time</small><strong id="time">0.0 s</strong></div></div>
<div class="columns"><section><div class="field-shell"><svg class="board" id="field" viewBox="0 0 320 200" role="img" aria-label="Defense field: a straight path, one tower pad and three scheduled enemies"></svg><div class="legend"><span>● Tower pad (150, 60)</span><span>Path → endpoint (300, 100)</span></div></div><div class="actions"><button type="button" id="build">Build pad · 2 coins</button><button type="button" id="start">Start wave</button><button type="button" id="pause" class="secondary">Pause</button><button type="button" id="resume" class="secondary">Resume</button><button type="button" id="reset" class="secondary">Reset</button></div><p id="message" class="notice" role="status">Ready: build once or start without a tower.</p><div class="actions"><button type="button" id="step" class="secondary">Advance 1.0 s while paused</button><button type="button" id="trace" class="secondary">Resolve wave without animation</button></div><p class="muted">Step execution uses the same 0.1-second updates as live play. Resolve runs the current ready/paused wave to its terminal result.</p></section>
<aside class="panel"><h2>Target &amp; resolution ledger</h2><p id="target" class="notice">No active target</p><p id="cooldown" class="muted">No tower cooldown</p><div id="enemies"></div><h3 style="margin-top:18px">Shot / outcome trace</h3><pre id="event-log">No events yet.</pre></aside></div>
<section class="below"><h2>Inspect the rules</h2><p>E1/E2/E3 spawn at 0/2/4 s with 2 HP each. Speed is 30 logical px/s. The furthest eligible enemy wins targeting priority; equal progress breaks by stable ID. Each shot deals 1 damage, with a one-second simulation cooldown. Kills award one coin; living endpoint arrivals cost one life.</p><p>A prebuilt tower should finish at 8.1 s with three kills, 4 coins and 2 lives. Without a tower, the second leak at 12.0 s ends the wave. A kill is reconciled before a leak, so one ID never receives both outcomes.</p><p class="foot">Original bounded prototype, one wave only. One scheduler uses 0.1 s steps and at most ten steps per frame; excess elapsed time is discarded as slowdown. Pause or a hidden tab freezes simulation; Resume discards paused time. No saved progress or network. Actual browser rendering and hosted execution remain subject to separate review.</p></section></main><script>'use strict';const $=id=>document.getElementById(id);let s,gen=0,frameId=0,last=null,acc=0;
function fresh(){return {state:'Ready',tick:0,coins:3,lives:2,tower:false,next:10,enemies:[0,20,40].map((spawn,i)=>({id:'E'+(i+1),spawn,hp:2,x:0,status:'Pending'})),events:[]};}
function eligible(e){return e.status==='Alive'&&e.hp>0&&(e.x-150)**2+1600<=4900+1e-8;}
function target(){return s.enemies.filter(eligible).sort((a,b)=>b.x-a.x||a.id.localeCompare(b.id))[0];}
function reconcile(){for(const e of s.enemies){if(e.status==='Alive'&&e.hp<=0){e.status='Killed';s.coins++;s.events.push((s.tick/10).toFixed(1)+' s '+e.id+' killed · +1 coin');}}for(const e of s.enemies){if(e.status==='Alive'&&e.x>=300){e.status='Leaked';s.lives=Math.max(0,s.lives-1);s.events.push((s.tick/10).toFixed(1)+' s '+e.id+' leaked · −1 life');if(s.lives===0)break;}}if(!s.lives)s.state='Lost';else if(s.enemies.every(e=>e.status==='Killed'||e.status==='Leaked'))s.state='Won';}
function step(){if(s.state!=='Running')return;s.tick++;for(const e of s.enemies){if(e.status==='Pending'&&s.tick>=e.spawn)e.status='Alive';if(e.status==='Alive')e.x=Math.min(300,(s.tick-e.spawn)*3);}const t=target();if(s.tower&&s.tick>=s.next&&t){t.hp--;s.next=s.tick+10;s.events.push((s.tick/10).toFixed(1)+' s shot '+t.id+' → HP '+t.hp);}reconcile();}
function stopLoop(){gen++;cancelAnimationFrame(frameId);last=null;acc=0;}
function loop(stamp,g){if(g!==gen||s.state!=='Running')return;if(last!==null){acc+=Math.max(0,(stamp-last)/1000);const n=Math.min(10,Math.floor((acc+1e-9)/.1));for(let i=0;i<n;i++)step();acc=acc>=1?0:Math.max(0,acc-n*.1);}last=stamp;render();if(s.state==='Running')frameId=requestAnimationFrame(t=>loop(t,g));}
function schedule(){stopLoop();const g=gen;frameId=requestAnimationFrame(t=>loop(t,g));}
function begin(animated=true){if(s.state!=='Ready')return;s.state='Running';s.tick=0;s.enemies[0].status='Alive';if(animated)schedule();render();}
function build(){if(s.state==='Ready'&&!s.tower&&s.coins>=2){s.coins-=2;s.tower=true;s.next=10;$('message').textContent='Tower built. 1 coin remains; first shot can occur after 1.0 s.';}render();}
function pause(){if(s.state==='Running'){s.state='Paused';stopLoop();render();}}
function reset(){stopLoop();s=fresh();$('message').textContent='Ready: build once or start without a tower.';render();}
function resolve(){if(s.state==='Ready')begin(false);else if(s.state==='Paused')s.state='Running';else return;stopLoop();for(let i=0;i<200&&s.state==='Running';i++)step();render();}
function render(){const t=target();$('state').textContent=s.state;$('coins').textContent=s.coins;$('lives').textContent=s.lives;$('time').textContent=(s.tick/10).toFixed(1)+' s';$('target').textContent=t?t.id+' · distance '+Math.hypot(t.x-150,40).toFixed(1)+' / 70 · HP '+t.hp:'No eligible active target';$('cooldown').textContent=s.tower?'Cooldown: '+(s.tick>=s.next?'ready':((s.next-s.tick)/10).toFixed(1)+' s until ready'):'No tower cooldown';$('enemies').innerHTML=s.enemies.map(e=>'<div class="ledger-row"><strong>'+e.id+'</strong><span>'+e.status+' · HP '+e.hp+' · x '+e.x+'<br><small>spawn '+e.spawn/10+' s</small></span></div>').join('');$('event-log').textContent=s.events.join('\n')||'No events yet.';if(s.state==='Won')$('message').textContent='Won — all 3 enemies killed. 4 coins · 2 lives.';if(s.state==='Lost')$('message').textContent='Lost — no lives remain. Reset to try a tower.';$('build').disabled=s.state!=='Ready'||s.tower||s.coins<2;$('start').disabled=s.state!=='Ready';$('pause').disabled=s.state!=='Running';$('resume').disabled=s.state!=='Paused';$('step').disabled=s.state!=='Paused';$('trace').disabled=!['Ready','Paused'].includes(s.state);$('field').innerHTML='<rect width="320" height="200" fill="#cfe4c4"/><path d="M0 100H300" stroke="#a68554" stroke-width="28"/><path d="M0 100H300" stroke="#f4dfaf" stroke-width="3" stroke-dasharray="6 6"/><rect x="295" y="78" width="18" height="44" rx="3" fill="#34694c"/><text x="293" y="137" font-size="9">BASE</text>'+(s.tower?'<circle cx="150" cy="60" r="70" fill="#3d8b6d" fill-opacity=".12" stroke="#3d8b6d" stroke-dasharray="4 3"/>':'')+'<circle cx="150" cy="60" r="13" fill="'+(s.tower?'#244e67':'#f4e6b3')+'" stroke="#365c42" stroke-width="2"/>'+ (s.tower?'<path d="M150 60V40" stroke="#183e50" stroke-width="7"/>':'<text x="145" y="64" font-size="14">+</text>')+s.enemies.filter(e=>e.status==='Alive').map(e=>'<g><circle cx="'+e.x+'" cy="100" r="9" fill="#c55d41"/><text x="'+e.x+'" y="84" text-anchor="middle" font-size="10">'+e.id+' · '+e.hp+'</text></g>').join('');}
$('build').addEventListener('click',build);$('start').addEventListener('click',()=>begin());$('pause').addEventListener('click',pause);$('resume').addEventListener('click',()=>{if(s.state==='Paused'){s.state='Running';schedule();render();}});$('reset').addEventListener('click',reset);$('trace').addEventListener('click',resolve);$('step').addEventListener('click',()=>{if(s.state==='Paused'){s.state='Running';for(let i=0;i<10&&s.state==='Running';i++)step();if(s.state==='Running')s.state='Paused';render();}});document.addEventListener('visibilitychange',()=>{if(document.hidden)pause();});globalThis.app={get:()=>s,step,begin,build,reset,resolve,reconcile,target,eligible,loop};reset();</script></body></html>