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>