Code for Above
<div style="max-width:1500px;margin:0 auto;">
<iframe
title="Laser Sandbox — Weapon Lineup Revised (Ricochet)"
scrolling="no"
style="display:block;margin:0 auto;border:0;width:1500px;height:1500px;overflow:hidden;border-radius:14px;background:#000;"
sandbox="allow-scripts allow-same-origin"
srcdoc='<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Laser Sandbox — Weapon Lineup Revised</title>
<style>
:root{ color-scheme:dark; }
html,body{margin:0;height:100%;background:#05070b;overflow:hidden;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;}
#app{
width:1500px;height:1500px;margin:0 auto;display:grid;
grid-template-columns: 1fr 450px;
gap:12px; padding:16px; box-sizing:border-box;
background:radial-gradient(1200px 1000px at 55% 35%, rgba(25,55,110,.30), rgba(5,7,11,0) 58%), #05070b;
}
.panel{
background:rgba(8,10,16,.70);
border:1px solid rgba(255,255,255,.10);
box-shadow:0 14px 60px rgba(0,0,0,.60);
border-radius:16px;
backdrop-filter: blur(10px);
}
#worldPanel{ position:relative; overflow:hidden; }
#c{ display:block; width:100%; height:100%; }
#hud{
position:absolute; left:14px; top:14px; right:14px;
display:flex; justify-content:space-between; gap:12px;
pointer-events:none;
}
#hud .box{
padding:10px 12px; border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background:rgba(8,10,16,.55);
font-size:12px; line-height:1.35;
}
#hud b{ font-size:13px; letter-spacing:.2px; }
#heatBar{
height:10px; width:260px; border-radius:999px;
background:rgba(255,255,255,.10);
overflow:hidden; margin-top:6px;
}
#heatFill{ height:100%; width:0%; background:rgba(120,255,210,.85); }
#flash{
position:absolute; left:50%; top:50%;
transform:translate(-50%,-50%);
padding:14px 16px;
border-radius:16px;
border:1px solid rgba(255,255,255,.14);
background:rgba(8,10,16,.84);
box-shadow:0 18px 60px rgba(0,0,0,.65);
font-weight:950;
font-size:14px;
opacity:0;
pointer-events:none;
transition: opacity .10s ease;
white-space:nowrap;
}
#side{ display:grid; grid-template-rows: auto 1fr; gap:12px; min-height:0; }
#sideTop{ padding:12px 14px; }
#sideTop b{ font-size:14px; letter-spacing:.2px; }
#sideTop .muted{ opacity:.80; font-size:12px; margin-top:4px; line-height:1.35; }
#sideScroll{ padding:12px; min-height:0; overflow:auto; }
.row{
display:flex; justify-content:space-between; gap:12px; align-items:center;
padding:10px; border-radius:14px;
border:1px solid rgba(255,255,255,.10);
background:rgba(255,255,255,.04);
margin-bottom:10px;
}
.row .left{ display:flex; flex-direction:column; gap:2px; }
.row .left span{ font-size:12px; opacity:.82; }
.row .left b{ font-size:13px; }
.row .right{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; opacity:.9; text-align:right; }
.key{
display:inline-flex; min-width:22px; height:22px; padding:0 6px;
align-items:center; justify-content:center;
border-radius:8px;
border:1px solid rgba(255,255,255,.16);
background:rgba(255,255,255,.06);
font-weight:900;
margin-left:6px;
}
.muted{ opacity:.78; }
</style>
</head>
<body>
<div id="app">
<div id="worldPanel" class="panel">
<canvas id="c"></canvas>
<div id="hud">
<div class="box">
<b>LASER SANDBOX</b><br/>
Rotate: <span class="key">A</span><span class="key">D</span> or <span class="key">←</span><span class="key">→</span><br/>
Fire: <span class="key">Space</span> (hold / release for CHARGE) • Reset: <span class="key">R</span>
</div>
<div class="box" style="min-width:390px;">
<div>Mode <span id="modeHud">HITSCAN</span> • Score <span id="score">0</span> • Hits <span id="hits">0</span> • Miss <span id="miss">0</span></div>
<div style="margin-top:4px;">Heat <span id="heatTxt">0%</span> • Shield <span id="shieldTxt">100</span> • Hull <span id="hullTxt">100</span></div>
<div id="heatBar"><div id="heatFill"></div></div>
</div>
</div>
<div id="flash"></div>
</div>
<div id="side" class="panel">
<div id="sideTop">
<b>Sandbox Controls</b>
<div class="muted">Targets unchanged. Weapon lineup revised. WAVE removed. Added RICOCHET + bounce count.</div>
</div>
<div id="sideScroll">
<div class="row">
<div class="left">
<b>Weapon Mode</b>
<span>Hitscan / Burst / Pierce / Spread / Beam / Charge / Ricochet</span>
</div>
<div class="right">
<span id="modeVal">HITSCAN</span><br/>
Prev <span class="key">Q</span> Next <span class="key">E</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Beam Width</b>
<span>1 / 2 / 4 / 8</span>
</div>
<div class="right">
<span id="widthVal">2</span><br/>
- <span class="key">Z</span> + <span class="key">C</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Range</b>
<span>600 / 900 / 1200 / 1500</span>
</div>
<div class="right">
<span id="rangeVal">1200</span><br/>
- <span class="key">X</span> + <span class="key">V</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Fire Rate</b>
<span>Slow / Normal / Fast</span>
</div>
<div class="right">
<span id="rofVal">NORMAL</span><br/>
Prev <span class="key">1</span> Next <span class="key">2</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Overheat</b>
<span>On / Off</span>
</div>
<div class="right">
<span id="heatModeVal">ON</span><br/>
Toggle <span class="key">3</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Damage Model</b>
<span>One-shot / 3-hit / Shield+Hull</span>
</div>
<div class="right">
<span id="dmgVal">ONE</span><br/>
Prev <span class="key">T</span> Next <span class="key">Y</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Aim Assist</b>
<span>Off / Mild / Strong</span>
</div>
<div class="right">
<span id="aimVal">OFF</span><br/>
Prev <span class="key">B</span> Next <span class="key">N</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Reticle + Aim Guide</b>
<span>On / Off</span>
</div>
<div class="right">
<span id="retVal">ON</span><br/>
Toggle <span class="key">M</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Target Behavior</b>
<span>Static / Drift / Evade</span>
</div>
<div class="right">
<span id="behVal">DRIFT</span><br/>
Prev <span class="key">U</span> Next <span class="key">I</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Spawn Pattern</b>
<span>Cone / Ring / Swarm</span>
</div>
<div class="right">
<span id="spawnVal">CONE</span><br/>
Prev <span class="key">O</span> Next <span class="key">P</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Ricochet Bounces</b>
<span>0 / 1 / 2 / 3 (RICOCHET mode)</span>
</div>
<div class="right">
<span id="bounceVal">1</span><br/>
- <span class="key">K</span> + <span class="key">L</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Explosion Size</b>
<span>Small / Medium / Large</span>
</div>
<div class="right">
<span id="boomVal">MED</span><br/>
Prev <span class="key">5</span> Next <span class="key">6</span>
</div>
</div>
<div class="row">
<div class="left">
<b>Respawn Density</b>
<span>6 / 10 / 14</span>
</div>
<div class="right">
<span id="densVal">10</span><br/>
Prev <span class="key">8</span> Next <span class="key">9</span>
</div>
</div>
<div class="muted" style="font-size:12px;line-height:1.45;">
Weapon note: RICOCHET is hitscan with wall bounces. It draws segmented beams, damages the first valid hit along each segment.
</div>
</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const W = canvas.width = canvas.parentElement.clientWidth;
const H = canvas.height = canvas.parentElement.clientHeight;
const TAU = Math.PI*2;
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const rand = (a,b)=>a+Math.random()*(b-a);
const UI = {
score: document.getElementById("score"),
hits: document.getElementById("hits"),
miss: document.getElementById("miss"),
heatTxt: document.getElementById("heatTxt"),
heatFill: document.getElementById("heatFill"),
flash: document.getElementById("flash"),
modeHud: document.getElementById("modeHud"),
shieldTxt: document.getElementById("shieldTxt"),
hullTxt: document.getElementById("hullTxt"),
modeVal: document.getElementById("modeVal"),
widthVal: document.getElementById("widthVal"),
rangeVal: document.getElementById("rangeVal"),
rofVal: document.getElementById("rofVal"),
heatModeVal: document.getElementById("heatModeVal"),
dmgVal: document.getElementById("dmgVal"),
aimVal: document.getElementById("aimVal"),
retVal: document.getElementById("retVal"),
behVal: document.getElementById("behVal"),
spawnVal: document.getElementById("spawnVal"),
bounceVal: document.getElementById("bounceVal"),
boomVal: document.getElementById("boomVal"),
densVal: document.getElementById("densVal"),
};
let flashT=0;
function flash(msg){
UI.flash.textContent = msg;
UI.flash.style.opacity = "1";
flashT = 0.6;
}
// ===== Settings =====
const modes = ["HITSCAN","BURST","PIERCE","SPREAD","BEAM","CHARGE","RICOCHET"];
const widths = [1,2,4,8];
const ranges = [600,900,1200,1500];
const rofs = [
{name:"SLOW", sec:0.18},
{name:"NORMAL", sec:0.12},
{name:"FAST", sec:0.07}
];
const boomSizes = [
{name:"SM", mult:0.75},
{name:"MED",mult:1.00},
{name:"LG", mult:1.35}
];
const densities = [6,10,14];
const dmgModels = ["ONE","THREE","SHIELD"];
const aimAssists = ["OFF","MILD","STRONG"];
const behaviors = ["STATIC","DRIFT","EVADE"];
const spawns = ["CONE","RING","SWARM"];
const S = {
modeIdx: 0,
widthIdx: 1,
rangeIdx: 2,
rofIdx: 1,
heatOn: true,
dmgIdx: 0,
aimIdx: 0,
reticleOn: true,
behIdx: 1,
spawnIdx: 0,
bounce: 1, // 0..3
boomIdx: 1,
densIdx: 1,
};
function syncUI(){
UI.modeVal.textContent = modes[S.modeIdx];
UI.modeHud.textContent = modes[S.modeIdx];
UI.widthVal.textContent = String(widths[S.widthIdx]);
UI.rangeVal.textContent = String(ranges[S.rangeIdx]);
UI.rofVal.textContent = rofs[S.rofIdx].name;
UI.heatModeVal.textContent = S.heatOn ? "ON" : "OFF";
UI.dmgVal.textContent = dmgModels[S.dmgIdx];
UI.aimVal.textContent = aimAssists[S.aimIdx];
UI.retVal.textContent = S.reticleOn ? "ON" : "OFF";
UI.behVal.textContent = behaviors[S.behIdx];
UI.spawnVal.textContent = spawns[S.spawnIdx];
UI.bounceVal.textContent = String(S.bounce);
UI.boomVal.textContent = boomSizes[S.boomIdx].name;
UI.densVal.textContent = String(densities[S.densIdx]);
}
syncUI();
// ===== State =====
const ship = { x: W/2, y: H*0.72, a: -Math.PI/2 };
const targets = [];
const particles = [];
const beams = []; // brief beam visuals: {t,dur,segments:[{x1,y1,x2,y2}],w,hit}
let score=0, hits=0, misses=0;
let shake=0, kick=0;
let heat=0;
let fireCooldown=0;
let triggerHeld=false;
let charge=0; // 0..1
let shield=100, hull=100;
const keys = Object.create(null);
function addExplosion(x,y){
const mult = boomSizes[S.boomIdx].mult;
particles.push({type:"boom",x,y,t:0,dur:0.55*mult});
particles.push({type:"ring",x,y,t:0,dur:0.40*mult});
const n = Math.floor(26*mult);
for(let i=0;i<n;i++){
const a=Math.random()*TAU, sp=rand(240,780)*mult;
particles.push({type:"frag",x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,t:0,dur:rand(0.35,0.8)*mult,s:rand(2.0,5.0)});
}
const m = Math.floor(18*mult);
for(let i=0;i<m;i++){
const a=Math.random()*TAU, sp=rand(260,920)*mult;
particles.push({type:"spark",x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,t:0,dur:rand(0.14,0.30)*mult,s:rand(1.2,2.2)});
}
}
function addImpact(x,y){
particles.push({type:"impact",x,y,t:0,dur:0.18});
particles.push({type:"ring",x,y,t:0,dur:0.22});
for(let i=0;i<12;i++){
const a=Math.random()*TAU, sp=rand(260,720);
particles.push({type:"spark",x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,t:0,dur:rand(0.12,0.24),s:rand(1.0,2.0)});
}
}
function spawnTarget(){
const mode = spawns[S.spawnIdx];
let x,y;
if(mode==="RING"){
const a = rand(0,TAU);
const d = rand(360, 700);
x = ship.x + Math.cos(a)*d;
y = ship.y + Math.sin(a)*d;
} else if(mode==="SWARM"){
const cx = ship.x + rand(-220,220);
const cy = ship.y + rand(-420,-120);
x = cx + rand(-140,140);
y = cy + rand(-140,140);
} else {
const dist = rand(240, 740);
const ang = ship.a + rand(-0.80, 0.80);
x = ship.x + Math.cos(ang)*dist;
y = ship.y + Math.sin(ang)*dist;
}
targets.push({
x: clamp(x, 80, W-80),
y: clamp(y, 120, H-80),
r: rand(12, 20),
vx: rand(-26,26),
vy: rand(-26,26),
hp: (dmgModels[S.dmgIdx]==="THREE") ? 3 : 1,
shield: (dmgModels[S.dmgIdx]==="SHIELD") ? 2 : 0
});
}
function ensureTargets(){
const desired = densities[S.densIdx];
while(targets.length < desired) spawnTarget();
}
function reset(){
score=0; hits=0; misses=0;
heat=0; fireCooldown=0; charge=0;
shield=100; hull=100;
beams.length=0; particles.length=0; targets.length=0;
for(let i=0;i<densities[S.densIdx];i++) spawnTarget();
flash("RESET");
}
function applyHeat(amount){
if(!S.heatOn) return;
heat = clamp(heat + amount, 0, 1);
}
function canFire(){
if(fireCooldown>0) return false;
if(!S.heatOn) return true;
return heat < 0.90;
}
function currentRange(){ return ranges[S.rangeIdx]; }
function currentWidth(){ return widths[S.widthIdx]; }
function currentROF(){ return rofs[S.rofIdx].sec; }
function aimAssistAdjustAngle(angle){
const lvl = aimAssists[S.aimIdx];
if(lvl==="OFF") return angle;
const maxAng = (lvl==="MILD") ? 0.10 : 0.18;
let best=null;
for(const t of targets){
const dx=t.x-ship.x, dy=t.y-ship.y;
const d = Math.hypot(dx,dy);
if(d < 60 || d > currentRange()) continue;
const ang = Math.atan2(dy,dx);
let diff = ((ang - angle + Math.PI*3)%(TAU)) - Math.PI;
diff = Math.max(-Math.PI, Math.min(Math.PI, diff));
if(Math.abs(diff) <= maxAng){
const score = Math.abs(diff) + d*0.0007;
if(!best || score < best.score) best={score, ang};
}
}
if(!best) return angle;
const strength = (lvl==="MILD") ? 0.35 : 0.65;
let diff = ((best.ang - angle + Math.PI*3)%(TAU)) - Math.PI;
return angle + diff*strength;
}
function hitTarget(t, ix, iy, dmg=1){
if(t.shield > 0){
t.shield -= dmg;
addImpact(ix,iy);
if(t.shield < 0){
t.hp += t.shield;
t.shield = 0;
}
} else {
t.hp -= dmg;
addImpact(ix,iy);
}
if(t.hp <= 0){
const idx = targets.indexOf(t);
if(idx>=0) targets.splice(idx,1);
addExplosion(t.x,t.y);
hits++;
score += 10;
ensureTargets();
return true;
}
hits++;
score += 3;
return false;
}
function spawnBeamSegments(segments, w, hit){
beams.push({t:0, dur:0.08, segments, w, hit});
}
// ===== Raycast utility (for hitscan + ricochet) =====
function castRay(x0,y0, ang, maxDist, w){
const dx=Math.cos(ang), dy=Math.sin(ang);
let best=null;
for(const t of targets){
const px=t.x-x0, py=t.y-y0;
const proj=px*dx + py*dy;
if(proj<=0 || proj>maxDist) continue;
const perp=Math.abs(px*dy - py*dx);
if(perp < (t.r + w*1.4)){
if(!best || proj < best.proj) best={t,proj, ix:x0+dx*proj, iy:y0+dy*proj};
}
}
return best;
}
function fireHitscanSingle(kind){
const range=currentRange();
const w=currentWidth();
shake = Math.max(shake, 14);
kick = 14;
fireCooldown=currentROF();
applyHeat(0.14 + w*0.01);
let ang = aimAssistAdjustAngle(ship.a);
const hit = castRay(ship.x, ship.y, ang, range, w);
const x2 = hit ? hit.ix : ship.x + Math.cos(ang)*range;
const y2 = hit ? hit.iy : ship.y + Math.sin(ang)*range;
spawnBeamSegments([{x1:ship.x,y1:ship.y,x2,y2}], w, !!hit);
if(hit){
hitTarget(hit.t, hit.ix, hit.iy, 1);
} else {
misses++; score=Math.max(0, score-1);
}
}
function fireSpread(){
const range=currentRange();
const w=currentWidth();
shake=Math.max(shake, 16); kick=14;
fireCooldown=currentROF();
applyHeat(0.17 + w*0.015);
const base = aimAssistAdjustAngle(ship.a);
const pellets=7, spread=0.28;
let any=false;
for(let i=0;i<pellets;i++){
const t = (i-(pellets-1)/2)/((pellets-1)/2);
const a = base + t*spread;
const hit = castRay(ship.x, ship.y, a, range, w);
const x2 = hit ? hit.ix : ship.x + Math.cos(a)*range;
const y2 = hit ? hit.iy : ship.y + Math.sin(a)*range;
beams.push({t:0,dur:0.06,segments:[{x1:ship.x,y1:ship.y,x2,y2}],w,hit:!!hit});
if(hit){ any=true; hitTarget(hit.t, hit.ix, hit.iy, 1); }
}
if(!any){ misses++; score=Math.max(0, score-1); }
}
function firePierce(){
const range=currentRange();
const w=currentWidth();
shake=Math.max(shake, 16); kick=14;
fireCooldown=currentROF();
applyHeat(0.16 + w*0.015);
const ang = aimAssistAdjustAngle(ship.a);
const dx=Math.cos(ang), dy=Math.sin(ang);
const hitsAlong=[];
for(const t of targets){
const px=t.x-ship.x, py=t.y-ship.y;
const proj=px*dx + py*dy;
if(proj<=0 || proj>range) continue;
const perp=Math.abs(px*dy - py*dx);
if(perp < (t.r + w*1.4)){
hitsAlong.push({t,proj, ix:ship.x+dx*proj, iy:ship.y+dy*proj});
}
}
hitsAlong.sort((a,b)=>a.proj-b.proj);
if(hitsAlong.length){
const last=hitsAlong[hitsAlong.length-1];
spawnBeamSegments([{x1:ship.x,y1:ship.y,x2:last.ix,y2:last.iy}], w, true);
for(const h of hitsAlong) hitTarget(h.t, h.ix, h.iy, 1);
} else {
spawnBeamSegments([{x1:ship.x,y1:ship.y,x2:ship.x+dx*range,y2:ship.y+dy*range}], w, false);
misses++; score=Math.max(0, score-1);
}
}
function fireBurst(){
const base=ship.a;
for(let i=0;i<3;i++){
ship.a = base + (i===0?0:rand(-0.02,0.02));
fireHitscanSingle("HITSCAN");
}
ship.a = base;
}
function updateBeamContinuous(dt){
const range=currentRange();
const w=currentWidth();
applyHeat((0.35 + w*0.02)*dt);
const ang = aimAssistAdjustAngle(ship.a);
const dx=Math.cos(ang), dy=Math.sin(ang);
const hit = castRay(ship.x, ship.y, ang, range, w+2);
const x2 = hit ? hit.ix : ship.x + dx*range;
const y2 = hit ? hit.iy : ship.y + dy*range;
beams.push({t:0,dur:0.04,segments:[{x1:ship.x,y1:ship.y,x2,y2}],w:w+2,hit:!!hit});
if(hit){
// fractional dps
const dmg = 0.9*dt;
hitTarget(hit.t, hit.ix, hit.iy, dmg);
}
}
function fireChargeRelease(){
const mult = clamp(charge, 0, 1);
if(mult < 0.15){
fireHitscanSingle("HITSCAN");
charge = 0;
return;
}
const w = Math.min(8, currentWidth()+4);
const range = Math.min(1500, currentRange()+300);
shake = Math.max(shake, 22);
kick = 18;
fireCooldown = Math.max(0.12, currentROF());
applyHeat(0.25 + 0.25*mult);
const ang = aimAssistAdjustAngle(ship.a);
const dx=Math.cos(ang), dy=Math.sin(ang);
// pierce-ish, dmg=2
const hitsAlong=[];
for(const t of targets){
const px=t.x-ship.x, py=t.y-ship.y;
const proj=px*dx + py*dy;
if(proj<=0 || proj>range) continue;
const perp=Math.abs(px*dy - py*dx);
if(perp < (t.r + w*1.2)){
hitsAlong.push({t,proj, ix:ship.x+dx*proj, iy:ship.y+dy*proj});
}
}
hitsAlong.sort((a,b)=>a.proj-b.proj);
if(hitsAlong.length){
const last=hitsAlong[hitsAlong.length-1];
spawnBeamSegments([{x1:ship.x,y1:ship.y,x2:last.ix,y2:last.iy}], w, true);
for(const h of hitsAlong) hitTarget(h.t, h.ix, h.iy, 2);
score += Math.round(10*mult);
} else {
spawnBeamSegments([{x1:ship.x,y1:ship.y,x2:ship.x+dx*range,y2:ship.y+dy*range}], w, false);
misses++; score=Math.max(0, score-1);
}
charge = 0;
}
function fireRicochet(){
const w=currentWidth();
const totalRange=currentRange();
const bounces = S.bounce;
shake = Math.max(shake, 16);
kick = 14;
fireCooldown=currentROF();
applyHeat(0.16 + w*0.015 + bounces*0.03);
let ang = aimAssistAdjustAngle(ship.a);
let x=ship.x, y=ship.y;
let remaining = totalRange;
const segments=[];
let anyHit=false;
for(let bounce=0; bounce<=bounces; bounce++){
const dx=Math.cos(ang), dy=Math.sin(ang);
// compute intersection with world bounds (minus padding)
const pad=20;
const minX=pad, maxX=W-pad, minY=pad, maxY=H-pad;
// param t for wall hit
let tWall = Infinity;
let hitWall = null;
if(dx > 0){
const t = (maxX - x)/dx;
if(t>0 && t<tWall) { tWall=t; hitWall="R"; }
} else if(dx < 0){
const t = (minX - x)/dx;
if(t>0 && t<tWall) { tWall=t; hitWall="L"; }
}
if(dy > 0){
const t = (maxY - y)/dy;
if(t>0 && t<tWall) { tWall=t; hitWall="B"; }
} else if(dy < 0){
const t = (minY - y)/dy;
if(t>0 && t<tWall) { tWall=t; hitWall="T"; }
}
// check target hit before wall, within remaining distance
const hit = castRay(x,y, ang, Math.min(remaining, tWall), w);
if(hit){
anyHit=true;
segments.push({x1:x,y1:y,x2:hit.ix,y2:hit.iy});
hitTarget(hit.t, hit.ix, hit.iy, 1);
remaining = 0;
break;
} else {
// travel to wall or max distance
const step = Math.min(remaining, tWall);
const nx = x + dx*step;
const ny = y + dy*step;
segments.push({x1:x,y1:y,x2:nx,y2:ny});
remaining -= step;
if(remaining <= 0.001) break;
// reflect angle on wall
if(!hitWall) break;
if(hitWall==="L" || hitWall==="R"){
ang = Math.PI - ang;
} else {
ang = -ang;
}
x = nx; y = ny;
// small jitter after bounce for “energy loss”
ang += rand(-0.02,0.02);
}
}
spawnBeamSegments(segments, w, anyHit);
if(!anyHit){
misses++; score=Math.max(0, score-1);
}
}
// ===== Input =====
addEventListener("keydown",(e)=>{
const k=e.key.toLowerCase();
keys[k]=true;
if(e.key===" "){ e.preventDefault(); triggerHeld=true; }
if(k==="r") reset();
if(k==="q"){ S.modeIdx = (S.modeIdx + modes.length - 1) % modes.length; syncUI(); flash("MODE"); }
if(k==="e"){ S.modeIdx = (S.modeIdx + 1) % modes.length; syncUI(); flash("MODE"); }
if(k==="z"){ S.widthIdx = Math.max(0, S.widthIdx-1); syncUI(); flash("WIDTH"); }
if(k==="c"){ S.widthIdx = Math.min(widths.length-1, S.widthIdx+1); syncUI(); flash("WIDTH"); }
if(k==="x"){ S.rangeIdx = Math.max(0, S.rangeIdx-1); syncUI(); flash("RANGE"); }
if(k==="v"){ S.rangeIdx = Math.min(ranges.length-1, S.rangeIdx+1); syncUI(); flash("RANGE"); }
if(k==="1"){ S.rofIdx = Math.max(0, S.rofIdx-1); syncUI(); flash("ROF"); }
if(k==="2"){ S.rofIdx = Math.min(rofs.length-1, S.rofIdx+1); syncUI(); flash("ROF"); }
if(k==="3"){ S.heatOn = !S.heatOn; syncUI(); flash("OVERHEAT"); }
if(k==="t"){ S.dmgIdx = Math.max(0, S.dmgIdx-1); syncUI(); flash("DMG"); }
if(k==="y"){ S.dmgIdx = Math.min(dmgModels.length-1, S.dmgIdx+1); syncUI(); flash("DMG"); }
if(k==="b"){ S.aimIdx = Math.max(0, S.aimIdx-1); syncUI(); flash("AIM"); }
if(k==="n"){ S.aimIdx = Math.min(aimAssists.length-1, S.aimIdx+1); syncUI(); flash("AIM"); }
if(k==="m"){ S.reticleOn = !S.reticleOn; syncUI(); flash("RETICLE"); }
if(k==="u"){ S.behIdx = Math.max(0, S.behIdx-1); syncUI(); flash("BEHAV"); }
if(k==="i"){ S.behIdx = Math.min(behaviors.length-1, S.behIdx+1); syncUI(); flash("BEHAV"); }
if(k==="o"){ S.spawnIdx = Math.max(0, S.spawnIdx-1); syncUI(); flash("SPAWN"); }
if(k==="p"){ S.spawnIdx = Math.min(spawns.length-1, S.spawnIdx+1); syncUI(); flash("SPAWN"); }
// Ricochet bounces
if(k==="k"){ S.bounce = Math.max(0, S.bounce-1); syncUI(); flash("BOUNCE"); }
if(k==="l"){ S.bounce = Math.min(3, S.bounce+1); syncUI(); flash("BOUNCE"); }
if(k==="5"){ S.boomIdx = Math.max(0, S.boomIdx-1); syncUI(); flash("BOOM"); }
if(k==="6"){ S.boomIdx = Math.min(boomSizes.length-1, S.boomIdx+1); syncUI(); flash("BOOM"); }
if(k==="8"){ S.densIdx = Math.max(0, S.densIdx-1); syncUI(); flash("DENSITY"); }
if(k==="9"){ S.densIdx = Math.min(densities.length-1, S.densIdx+1); syncUI(); flash("DENSITY"); ensureTargets(); }
});
addEventListener("keyup",(e)=>{
const k=e.key.toLowerCase();
keys[k]=false;
if(e.key===" "){
triggerHeld=false;
if(modes[S.modeIdx]==="CHARGE" && charge>0){
fireChargeRelease();
}
}
});
// ===== Draw =====
function drawHUD(){
UI.score.textContent = score;
UI.hits.textContent = hits;
UI.miss.textContent = misses;
const pct = Math.round(heat*100);
UI.heatTxt.textContent = pct + "%";
UI.heatFill.style.width = pct + "%";
UI.heatFill.style.background = (heat < 0.75) ? "rgba(120,255,210,.85)" : "rgba(255,120,140,.90)";
UI.shieldTxt.textContent = Math.round(shield);
UI.hullTxt.textContent = Math.round(hull);
UI.modeHud.textContent = modes[S.modeIdx];
}
function drawShip(){
ctx.globalAlpha = 0.20;
ctx.fillStyle = "rgba(0,255,200,.50)";
ctx.beginPath(); ctx.arc(ship.x, ship.y, 42, 0, TAU); ctx.fill();
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.a);
ctx.globalAlpha = 1;
ctx.strokeStyle = "rgba(240,245,255,.95)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(22,0);
ctx.lineTo(-14,12);
ctx.lineTo(-10,0);
ctx.lineTo(-14,-12);
ctx.closePath();
ctx.stroke();
if(S.reticleOn){
ctx.globalAlpha = 0.30;
ctx.beginPath();
ctx.moveTo(22,0);
ctx.lineTo(70,0);
ctx.stroke();
ctx.globalAlpha = 0.18;
ctx.beginPath();
ctx.arc(70,0,10,0,TAU);
ctx.stroke();
}
// charge indicator
if(modes[S.modeIdx]==="CHARGE" && triggerHeld){
ctx.globalAlpha = 0.9;
ctx.strokeStyle = "rgba(255,220,160,.95)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0,0, 18 + charge*16, 0, TAU*charge);
ctx.stroke();
}
ctx.restore();
ctx.globalAlpha = 1;
}
function drawTargets(){
for(const t of targets){
ctx.globalAlpha = 0.22;
ctx.strokeStyle = "rgba(240,245,255,.90)";
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(t.x,t.y,t.r+10,0,TAU); ctx.stroke();
if(t.shield>0){
ctx.globalAlpha = 0.35;
ctx.strokeStyle = "rgba(120,170,255,.95)";
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(t.x,t.y,t.r+4,0,TAU); ctx.stroke();
}
ctx.globalAlpha = 0.95;
ctx.fillStyle = "rgba(255,200,120,.95)";
ctx.beginPath(); ctx.arc(t.x,t.y,t.r,0,TAU); ctx.fill();
if(t.hp>1){
ctx.globalAlpha = 0.85;
ctx.fillStyle = "rgba(255,240,240,.9)";
for(let i=0;i<t.hp;i++){
ctx.beginPath();
ctx.arc(t.x - (t.hp-1)*5 + i*10, t.y + t.r + 18, 2.2, 0, TAU);
ctx.fill();
}
}
}
ctx.globalAlpha = 1;
}
function drawBeams(dt){
for(let i=beams.length-1;i>=0;i--){
const b=beams[i];
b.t += dt;
const u=b.t/b.dur;
if(u>=1){ beams.splice(i,1); continue; }
const a=1-u;
for(const seg of b.segments){
// glow
ctx.globalAlpha = 0.12*a;
ctx.strokeStyle="rgba(255,80,80,1)";
ctx.lineWidth = 10 + b.w*1.2;
ctx.beginPath(); ctx.moveTo(seg.x1,seg.y1); ctx.lineTo(seg.x2,seg.y2); ctx.stroke();
// core
ctx.globalAlpha = 0.85*a;
ctx.lineWidth = 2.2 + b.w*0.55;
ctx.beginPath(); ctx.moveTo(seg.x1,seg.y1); ctx.lineTo(seg.x2,seg.y2); ctx.stroke();
}
// endpoints highlight on last segment
const last = b.segments[b.segments.length-1];
if(last && b.hit){
ctx.globalAlpha = 0.85*a;
ctx.fillStyle="rgba(255,240,240,1)";
ctx.beginPath(); ctx.arc(last.x2,last.y2, 3.2+(1-a)*7, 0, TAU); ctx.fill();
}
ctx.globalAlpha = 1;
}
}
function drawParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.t += dt;
const u=p.t/p.dur;
if(u>=1){ particles.splice(i,1); continue; }
if(p.type==="frag"||p.type==="spark"){
p.x += p.vx*dt; p.y += p.vy*dt;
p.vx *= Math.pow(0.08, dt);
p.vy *= Math.pow(0.08, dt);
const a=1-u;
ctx.globalAlpha=(p.type==="spark"?0.9:0.65)*a;
ctx.fillStyle=(p.type==="spark")?"rgba(255,220,160,1)":"rgba(255,140,120,1)";
ctx.beginPath(); ctx.arc(p.x,p.y, p.s*(0.6+a), 0, TAU); ctx.fill();
} else if(p.type==="boom"){
const a=1-u;
const r=18 + u*u*120;
ctx.globalAlpha=0.28*a;
ctx.fillStyle="rgba(255,160,120,1)";
ctx.beginPath(); ctx.arc(p.x,p.y,r,0,TAU); ctx.fill();
ctx.globalAlpha=0.18*a;
ctx.fillStyle="rgba(255,220,160,1)";
ctx.beginPath(); ctx.arc(p.x,p.y,r*0.55,0,TAU); ctx.fill();
} else if(p.type==="ring"){
const a=1-u;
const r=10 + u*u*90;
ctx.globalAlpha=0.38*a;
ctx.strokeStyle="rgba(240,245,255,1)";
ctx.lineWidth=2.0*a;
ctx.beginPath(); ctx.arc(p.x,p.y,r,0,TAU); ctx.stroke();
} else if(p.type==="impact"){
const a=1-u;
const r=6 + u*u*46;
ctx.globalAlpha=0.55*a;
ctx.fillStyle="rgba(255,240,240,1)";
ctx.beginPath(); ctx.arc(p.x,p.y,r,0,TAU); ctx.fill();
}
ctx.globalAlpha=1;
}
}
// ===== Loop =====
let last = performance.now();
function tick(now){
const dt = Math.min(0.033,(now-last)/1000);
last = now;
if(flashT>0){
flashT = Math.max(0, flashT-dt);
if(flashT===0) UI.flash.style.opacity="0";
}
// rotate
const turn=2.4;
if(keys["a"]||keys["arrowleft"]) ship.a -= turn*dt;
if(keys["d"]||keys["arrowright"]) ship.a += turn*dt;
// positional kick only
if(kick>0){
ship.x += Math.cos(ship.a + Math.PI) * kick * dt * 18;
ship.y += Math.sin(ship.a + Math.PI) * kick * dt * 18;
ship.x = clamp(ship.x, 80, W-80);
ship.y = clamp(ship.y, 80, H-80);
kick *= Math.pow(0.02, dt);
}
// cooldown / heat
if(fireCooldown>0) fireCooldown -= dt;
if(S.heatOn) heat = clamp(heat - 0.35*dt, 0, 1);
else heat = 0;
// charge build
if(modes[S.modeIdx]==="CHARGE"){
if(triggerHeld && canFire()){
charge = clamp(charge + 0.7*dt, 0, 1);
applyHeat(0.10*dt);
} else {
charge = Math.max(0, charge - 0.5*dt);
}
}
// firing by mode
const mode = modes[S.modeIdx];
if(triggerHeld && canFire()){
if(mode==="BEAM"){
if(!S.heatOn || heat < 0.90) updateBeamContinuous(dt);
}
else if(mode==="CHARGE"){
// wait for release
}
else if(mode==="BURST"){
fireBurst();
}
else if(mode==="PIERCE"){
firePierce();
}
else if(mode==="SPREAD"){
fireSpread();
}
else if(mode==="RICOCHET"){
fireRicochet();
}
else {
fireHitscanSingle("HITSCAN");
}
}
// target behavior
const beh = behaviors[S.behIdx];
for(const t of targets){
if(beh==="STATIC") continue;
if(beh==="DRIFT"){
t.x += t.vx*dt; t.y += t.vy*dt;
} else {
const dx=t.x-ship.x, dy=t.y-ship.y;
const d=Math.hypot(dx,dy);
const ang=Math.atan2(dy,dx);
let diff = ((ang - ship.a + Math.PI*3)%(TAU)) - Math.PI;
const sign = diff>=0 ? 1 : -1;
const px = -Math.sin(ship.a)*sign;
const py = Math.cos(ship.a)*sign;
const speed = (d < 650) ? 120 : 60;
t.x += px*speed*dt;
t.y += py*speed*dt;
t.x += t.vx*0.35*dt;
t.y += t.vy*0.35*dt;
}
if(t.x < 80 || t.x > W-80) t.vx *= -1;
if(t.y < 120 || t.y > H-80) t.vy *= -1;
t.x = clamp(t.x, 80, W-80);
t.y = clamp(t.y, 120, H-80);
}
ensureTargets();
// render
ctx.setTransform(1,0,0,1,0,0);
if(shake>0.5){
ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
shake *= 0.82;
} else shake=0;
ctx.fillStyle="#05070b";
ctx.fillRect(0,0,W,H);
ctx.globalAlpha=0.18;
ctx.fillStyle="rgba(220,235,255,.9)";
for(let i=0;i<220;i++) ctx.fillRect((i*173)%W,(i*97)%H,1,1);
ctx.globalAlpha=1;
drawTargets();
drawParticles(dt);
drawBeams(dt);
drawShip();
drawHUD();
requestAnimationFrame(tick);
}
// ===== HUD =====
function drawHUD(){
UI.score.textContent = score;
UI.hits.textContent = hits;
UI.miss.textContent = misses;
const pct = Math.round(heat*100);
UI.heatTxt.textContent = pct + "%";
UI.heatFill.style.width = pct + "%";
UI.heatFill.style.background = (heat < 0.75) ? "rgba(120,255,210,.85)" : "rgba(255,120,140,.90)";
UI.shieldTxt.textContent = Math.round(shield);
UI.hullTxt.textContent = Math.round(hull);
UI.modeHud.textContent = modes[S.modeIdx];
}
reset();
requestAnimationFrame(tick);
})();
</script>
</body>
</html>'
></iframe>
</div>