Title of State:

Improving Object Selection

Goal of Change:

PATCH 1 — State: replace single selectedEntityId with multi-select + drag state

Code to Remove

589 - 
  selectedEntityId: null,      // Id of selected entity

Code to Add

589 -
  selectedEntityIds: [],       // array of selected entity Ids

  selecting: false,
  selectStartX: 0, selectStartY: 0,
  selectEndX: 0, selectEndY: 0,


PATCH 2 — Mouse input: implement drag-select (mousedown/mousemove/mouseup)

Code to Remove

635 -
canvas.addEventListener("mousemove", (e)=>{
  const r = canvas.getBoundingClientRect();
  S.mouseX = e.clientX - r.left;
  S.mouseY = e.clientY - r.top;
});

canvas.addEventListener("mousedown", (e)=>{
  if(e.button!==0) return;
  // left click
  onClick(S.mouseX, S.mouseY);
});

Code to Add

635 - 
canvas.addEventListener("mousemove", (e)=>{
  const r = canvas.getBoundingClientRect();
  S.mouseX = e.clientX - r.left;
  S.mouseY = e.clientY - r.top;

  if(S.selecting){
    S.selectEndX = S.mouseX;
    S.selectEndY = S.mouseY;
  }
});

canvas.addEventListener("mousedown", (e)=>{
  if(e.button!==0) return;

  // begin drag-select
  S.selecting = true;
  S.selectStartX = S.mouseX;
  S.selectStartY = S.mouseY;
  S.selectEndX = S.mouseX;
  S.selectEndY = S.mouseY;
});


Code to Remove

683 -
window.addEventListener("keyup", (e)=>{
  S.keys[e.code] = false;
});

Code to Add

683 - 
window.addEventListener("keyup", (e)=>{
  S.keys[e.code] = false;
});

// finalize selection on mouseup
window.addEventListener("mouseup", (e)=>{
  if(e.button !== 0) return;
  if(!S.selecting) return;

  S.selecting = false;

  const dx = S.selectEndX - S.selectStartX;
  const dy = S.selectEndY - S.selectStartY;
  const dist2 = dx*dx + dy*dy;

  const ctrl = !!(S.keys["ControlLeft"] || S.keys["ControlRight"]);

  if(dist2 < 25){
    onClick(S.selectEndX, S.selectEndY, ctrl);
  } else {
    boxSelect(S.selectStartX, S.selectStartY, S.selectEndX, S.selectEndY, ctrl);
  }
});


PATCH 3 — Click logic: ctrl toggle + entity-first + resource fallback

Code to Remove

718 - 
function onClick(x,y){
  if(S.view !== "GAME") return;

  // screen -> world
  const wx = x + S.camX;
  const wy = y + S.camY;

  // 1 entity selection first
  const eid = findEntityAtWorld(wx, wy);
  S.selectedEntityId = eid;

  // 2 if no entity hit, fall back to resource selection
  if(!eid){
    const idx = findResourceAtWorld(wx, wy);
    S.selectedResourceIndex = idx;
  } else {
    S.selectedResourceIndex = -1;
  }
}

Code to Add

718-
function onClick(x,y, ctrl){
  if(S.view !== "GAME") return;

  const wx = x + S.camX;
  const wy = y + S.camY;

  const eid = findEntityAtWorld(wx, wy);

  if(eid){
    // entity click
    if(ctrl){
      const i = S.selectedEntityIds.indexOf(eid);
      if(i >= 0) S.selectedEntityIds.splice(i,1);
      else S.selectedEntityIds.push(eid);
    } else {
      S.selectedEntityIds = [eid];
    }
    S.selectedResourceIndex = -1;
    return;
  }

  // no entity => resource click
  if(!ctrl) S.selectedEntityIds = [];
  S.selectedResourceIndex = findResourceAtWorld(wx, wy);
}


PATCH 4 — Add boxSelect() helper (vehicles + infantry only)

Code to Remove

1009 -
2 new lines

Code to Add

1010 -
function boxSelect(x1,y1,x2,y2, ctrl){
  if(S.view !== "GAME") return;

  const minX = Math.min(x1,x2), maxX = Math.max(x1,x2);
  const minY = Math.min(y1,y2), maxY = Math.max(y1,y2);

  const wMinX = minX + S.camX, wMaxX = maxX + S.camX;
  const wMinY = minY + S.camY, wMaxY = maxY + S.camY;

  const hits = [];
  for(const e of (S.entities || [])){
    // vehicles + infantry only
    if(e.Kind === "BUILDING") continue;

    if(e.X >= wMinX && e.X <= wMaxX && e.Y >= wMinY && e.Y <= wMaxY){
      hits.push(e.Id);
    }
  }

  if(ctrl){
    for(const id of hits){
      if(!S.selectedEntityIds.includes(id)) S.selectedEntityIds.push(id);
    }
  } else {
    S.selectedEntityIds = hits;
  }

  if(hits.length) S.selectedResourceIndex = -1;
}


PATCH 5 — Right-click: move only selected vehicles

Code to Remove

1118 - 
function onCommandMove(screenX, screenY){
  // command selected harvester; if none selected, command all harvesters
  const wx = screenX + S.camX;
  const wy = screenY + S.camY;

  const selected = S.selectedEntityId
    ? S.entities.find(e => e.Id === S.selectedEntityId)
    : null;

  if(selected && selected.Kind === "VEHICLE"){
    selected.TargetX = wx;
    selected.TargetY = wy;
    return;
  }

  for(const e of S.entities){
    if(e.Kind === "VEHICLE"){
      e.TargetX = wx;
      e.TargetY = wy;
    }
  }
}

Code to Add

1118 - 
function onCommandMove(screenX, screenY){
  const wx = screenX + S.camX;
  const wy = screenY + S.camY;

  // move ONLY selected vehicles
  if(!S.selectedEntityIds || S.selectedEntityIds.length === 0) return;

  for(const id of S.selectedEntityIds){
    const e = S.entities.find(x => x.Id === id);
    if(!e) continue;
    if(e.Kind !== "VEHICLE") continue;

    e.TargetX = wx;
    e.TargetY = wy;
  }
}


PATCH 6 — Draw: green selection glow for all selected entities + box rectangle

Code to Remove

1399 -
    // selection ring
    if(e.Id === S.selectedEntityId){
      ctx.globalAlpha = 0.9;
      ctx.strokeStyle = "rgba(255,255,255,.85)";
      ctx.lineWidth = 2;
      const r = Math.max(img.width, img.height)/2 + 8;
      ctx.beginPath();
      ctx.arc(0, 0, r, 0, Math.PI*2);
      ctx.stroke();
    }

Code to Add

1399 - 
    // selection glow (green)
    if(S.selectedEntityIds && S.selectedEntityIds.includes(e.Id)){
      const rr = Math.max(img.width, img.height)/2 + 10;

      ctx.globalAlpha = 0.22;
      ctx.strokeStyle = "rgba(0,255,120,1)";
      ctx.lineWidth = 10;
      ctx.beginPath();
      ctx.arc(0, 0, rr, 0, Math.PI*2);
      ctx.stroke();

      ctx.globalAlpha = 0.85;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(0, 0, rr, 0, Math.PI*2);
      ctx.stroke();
    }


Code to Remove

1565 -
  // mouse dot (proves input works)
  ctx.fillStyle="rgba(255,255,255,.65)";
  ctx.beginPath();
  ctx.arc(S.mouseX, S.mouseY, 3, 0, Math.PI*2);
  ctx.fill();

Code to Add

1565 -
  // selection box overlay
  if(S.selecting){
    const x1 = S.selectStartX, y1 = S.selectStartY;
    const x2 = S.selectEndX, y2 = S.selectEndY;
    const rx = Math.min(x1,x2), ry = Math.min(y1,y2);
    const rw = Math.abs(x2-x1), rh = Math.abs(y2-y1);

    ctx.save();
    ctx.globalAlpha = 0.9;
    ctx.strokeStyle = "rgba(0,255,120,1)";
    ctx.lineWidth = 2;
    ctx.strokeRect(rx, ry, rw, rh);
    ctx.globalAlpha = 0.10;
    ctx.fillStyle = "rgba(0,255,120,1)";
    ctx.fillRect(rx, ry, rw, rh);
    ctx.restore();
  }

  // mouse dot
  ctx.fillStyle="rgba(255,255,255,.65)";
  ctx.beginPath();
  ctx.arc(S.mouseX, S.mouseY, 3, 0, Math.PI*2);
  ctx.fill();


Fix setupEntities() to reset the right selection variable

Code to Remove

1062 - 
  S.selectedEntityId = null;

Code to Add

1062 - 
  S.selectedEntityIds = [];


Replace the ENTITY part of updateSelectionHUD() with grouped selection

Code to Remove

1428 - 
  // 1 entity selection
  if(S.selectedEntityId){
    const e = S.entities ? S.entities.find(x => x.Id === S.selectedEntityId) : null;
    if(e){
      const tx = (e.TargetX == null) ? "-" : Math.floor(e.TargetX);
      const ty = (e.TargetY == null) ? "-" : Math.floor(e.TargetY);

      selhud.textContent =
        "ENTITY\\n" +
        "Id: " + e.Id + "\\n" +
        "Kind: " + e.Kind + "\\n" +
        "Sprite: " + e.SpriteKey + "\\n" +
        "Owner: " + (e.OwnerColor || "-") + "\\n" +
        "Pos: " + Math.floor(e.X) + "," + Math.floor(e.Y) + "\\n" +
        "Heading: " + (e.Heading ? e.Heading.toFixed(2) : "0.00") + "\\n" +
        "Speed: " + (e.Speed || 0) + "\\n" +
        "Target: " + tx + "," + ty;
      return;
    }
  }

Code to Add

1428 - 
  // 1 entity selection (grouped)
  if(S.selectedEntityIds && S.selectedEntityIds.length){
    const selected = S.entities ? S.entities.filter(e => S.selectedEntityIds.includes(e.Id)) : [];
    const total = selected.length;

    const byType = {};
    for(const e of selected){
      const key = e.SpriteKey || e.Kind || "UNKNOWN";
      byType[key] = (byType[key] || 0) + 1;
    }

    const lines = [];
    lines.push("SELECTION");
    lines.push("Total: " + total);
    lines.push("");
    lines.push("By Type:");
    for(const k of Object.keys(byType).sort()){
      lines.push(" - " + k + ": " + byType[k]);
    }

    selhud.textContent = lines.join("\\n");
    return;
  }


Code to Remove

1399 - 
    // selection glow (green)
    if(S.selectedEntityIds && S.selectedEntityIds.includes(e.Id)){
      const rr = Math.max(img.width, img.height)/2 + 10;

      ctx.globalAlpha = 0.22;
      ctx.strokeStyle = "rgba(0,255,120,1)";
      ctx.lineWidth = 10;
      ctx.beginPath();
      ctx.arc(0, 0, rr, 0, Math.PI*2);
      ctx.stroke();

      ctx.globalAlpha = 0.85;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(0, 0, rr, 0, Math.PI*2);
      ctx.stroke();
    }

Code to Add

1399 - 
    // selection glow (green) — RECTANGULAR around sprite edges
    if(S.selectedEntityIds && S.selectedEntityIds.includes(e.Id)){
      const pad = 4;
      const w = img.width + pad*2;
      const h = img.height + pad*2;
      const x = -w/2;
      const y = -h/2;

      // outer glow
      ctx.globalAlpha = 0.18;
      ctx.strokeStyle = "rgba(0,255,120,1)";
      ctx.lineWidth = 10;
      ctx.strokeRect(x, y, w, h);

      // crisp outline
      ctx.globalAlpha = 0.95;
      ctx.lineWidth = 2;
      ctx.strokeRect(x, y, w, h);
    }


Result