Title of State:

Introduce an Entity System

Goal of Change:

add a persistent game-state structure that can represent:

So from this point forward, the game is not “map + resources.” It is:
map + camera + resources + entities.

PATCH 1 — State: add entities + selection

Code to Remove

566 - 
  resourceSettings: null,
  resources: [],               // [{Type, X, Y, Radius}...]
  selectedResourceIndex: -1,   // index into S.resources

Code to Add

566 - 
  resourceSettings: null,
  resources: [],               // [{Type, X, Y, Radius}...]
  selectedResourceIndex: -1,   // index into S.resources

  entities: [],                // [{Id, Kind, X, Y, Heading, Speed, TargetX, TargetY, SpriteKey, OwnerColor}]
  selectedEntityId: null,      // Id of selected entity


PATCH 2 — Assets: add sprite URLs + preloadAssets()

Code to Remove

732 - 
async function preloadMaps(){

Code to Add

732 -
/* =========================
   ENTITY ASSETS (NATIVE PX)
   ========================= */
const SPRITES = {
  HARVESTER: "https://chrisdeasy.com/wp-content/uploads/image-3817-e1771101213320.png", // 41x75 (points UP)
  CONYARD:   "https://chrisdeasy.com/wp-content/uploads/image-3826.png",               // 150x150
  REFINERY:  "https://chrisdeasy.com/wp-content/uploads/image-3828.png",               // 150x150
};

const Assets = { HARVESTER:null, CONYARD:null, REFINERY:null };

async function preloadAssets(){
  const [h, c, r] = await Promise.all([
    loadImage(SPRITES.HARVESTER),
    loadImage(SPRITES.CONYARD),
    loadImage(SPRITES.REFINERY),
  ]);
  Assets.HARVESTER = h;
  Assets.CONYARD = c;
  Assets.REFINERY = r;
}

async function preloadMaps(){


PATCH 3 — Input: add right-click command (without breaking left-click resource selection)

Code to Remove

619 - 
canvas.addEventListener("mousedown", (e)=>{
  if(e.button!==0) return;
  // real hook: click handler
  onClick(S.mouseX, S.mouseY);
});

Code to Add

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

// right click = command move (prevent browser menu)
canvas.addEventListener("contextmenu", (e)=>{
  e.preventDefault();
  if(S.view !== "GAME") return;
  const r = canvas.getBoundingClientRect();
  const x = e.clientX - r.left;
  const y = e.clientY - r.top;
  onCommandMove(x, y);
});


PATCH 4 — Click logic: select entity first, else resource

Code to Remove

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

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

  const idx = findResourceAtWorld(wx, wy);
  S.selectedResourceIndex = idx;

  // small HUD feedback (selection also reflected in draw + HUD lines)
  // idx === -1 means cleared selection
}

Code to Add

666 - 
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;
  }
}


PATCH 5 — Add entity helpers + setupEntities + command move

Code to Remove

951 - 
function findResourceAtWorld(wx, wy){

Code to Add

951 - 
function findEntityAtWorld(wx, wy){
  if(!S.entities || S.entities.length === 0) return null;

  // simple hit: bounding box using sprite native size
  for(let i=S.entities.length-1;i>=0;i--){
    const e = S.entities[i];
    const img = Assets[e.SpriteKey];
    if(!img) continue;

    const hw = img.width/2;
    const hh = img.height/2;

    // NOTE: ignores rotation for hit-test (good enough for now)
    if(wx >= (e.X - hw) && wx <= (e.X + hw) && wy >= (e.Y - hh) && wy <= (e.Y + hh)){
      return e.Id;
    }
  }
  return null;
}

function setupEntities(){
  S.entities = [];
  S.selectedEntityId = null;

  // One base set (player system comes next slice)
  const baseX = 700;
  const baseY = 700;

  // Construction yard + refinery
  S.entities.push({
    Id: "CY_1",
    Kind: "BUILDING",
    SpriteKey: "CONYARD",
    OwnerColor: "BLUE",
    X: baseX,
    Y: baseY,
    Heading: 0,
    Speed: 0,
    TargetX: null,
    TargetY: null,
  });

  S.entities.push({
    Id: "REF_1",
    Kind: "BUILDING",
    SpriteKey: "REFINERY",
    OwnerColor: "BLUE",
    X: baseX + 220,
    Y: baseY,
    Heading: 0,
    Speed: 0,
    TargetX: null,
    TargetY: null,
  });

  // 4 harvesters
  const spots = [
    [baseX + 120, baseY + 220],
    [baseX + 170, baseY + 260],
    [baseX + 220, baseY + 220],
    [baseX + 170, baseY + 310],
  ];
  for(let i=0;i<4;i++){
    S.entities.push({
      Id: `H_${i+1}`,
      Kind: "VEHICLE",
      SpriteKey: "HARVESTER",
      OwnerColor: "BLUE",
      X: spots[i][0],
      Y: spots[i][1],
      Heading: 0,
      Speed: 260,     // px/sec
      TargetX: null,
      TargetY: null,
    });
  }
}

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;
    }
  }
}

function findResourceAtWorld(wx, wy){


PATCH 6 — Update loop: move vehicles + compute rotation from movement direction

Code to Remove

1248 - 
  // ---- Spawning pass ----
  if(S.resources && S.resources.length){
    const initialCount = S.resources.length;
    for(let i=0;i<initialCount;i++){
      const r = S.resources[i];
      if(r && r.SpawnOn){
        spawnFromNode(r, dt);
      }
    }
  }
}

Code to Add

1248 - 
  // ---- Entity movement pass ----
  if(S.entities && S.entities.length){
    for(const e of S.entities){
      if(e.Kind !== "VEHICLE") continue;
      if(e.TargetX == null || e.TargetY == null) continue;

      const dx = e.TargetX - e.X;
      const dy = e.TargetY - e.Y;
      const dist = Math.hypot(dx, dy);

      if(dist < 2){
        e.X = e.TargetX;
        e.Y = e.TargetY;
        e.TargetX = null;
        e.TargetY = null;
        continue;
      }

      const step = Math.min(dist, (e.Speed || 0) * dt);
      const ux = dx / dist;
      const uy = dy / dist;

      e.X += ux * step;
      e.Y += uy * step;

      // Heading: sprite points UP; y grows downward.
      // Use heading measured clockwise from UP, then rotate canvas CCW by -heading.
      e.Heading = Math.atan2(ux, -uy);
    }
  }

  // ---- Spawning pass ----
  if(S.resources && S.resources.length){
    const initialCount = S.resources.length;
    for(let i=0;i<initialCount;i++){
      const r = S.resources[i];
      if(r && r.SpawnOn){
        spawnFromNode(r, dt);
      }
    }
  }
}


PATCH 7 — Rendering: draw entities at native px with rotation (no stretching)

Code to Remove

1313 - 
  // map first (so other debug overlays draw on top)
  drawMapNative();
  // resources on top of map
  drawResources();

Code to Add

1313 - 
  // map first (so other debug overlays draw on top)
  drawMapNative();

  // entities
  drawEntities();

  // resources on top of map
  drawResources();


Code to Remove

1291 - 
function drawMapNative(){

Code to Add

1291 - 
function drawEntities(){
  if(S.view !== "GAME") return;
  if(!S.entities || S.entities.length === 0) return;

  for(const e of S.entities){
    const img = Assets[e.SpriteKey];
    if(!img) continue;

    const sx = e.X - S.camX;
    const sy = e.Y - S.camY;

    // cull
    if(sx < -200 || sy < -200 || sx > VIEW+200 || sy > VIEW+200) continue;

    ctx.save();
    ctx.translate(sx, sy);

    if(e.Kind === "VEHICLE"){
      // rotate CCW by -heading so that heading (clockwise from up) points correctly
      ctx.rotate(-(e.Heading || 0));
    }

    // draw at native size centered
    ctx.drawImage(img, -img.width/2, -img.height/2);

    // 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();
    }

    ctx.restore();
  }
}

function drawMapNative(){


PATCH 8 — Start Game: call setupEntities() and preload assets at boot

Code to Remove

799 - 
btnStartGame.addEventListener("click", ()=>{
  if(!S.mapKey) return;
  reseed(seedInput.value);
  generateResources();
  S.selectedResourceIndex = -1;
  setView("GAME");
});

Code to Add

799 - 
btnStartGame.addEventListener("click", ()=>{
  if(!S.mapKey) return;
  reseed(seedInput.value);
  generateResources();
  setupEntities();
  S.selectedResourceIndex = -1;
  setView("GAME");
});


Code to Remove

1418 - 
reseed(S.seed);
preloadMaps();
requestAnimationFrame(loop);

Code to Add

1418 - 
reseed(S.seed);
preloadMaps();
preloadAssets();
requestAnimationFrame(loop);


Tweaking Vehicle movement

Code to Remove

1310 - 
      // rotate CCW by -heading so that heading (clockwise from up) points correctly
      ctx.rotate(-(e.Heading || 0));

Code to Add

1310 - 
      // rotate CCW by heading so sprite (points UP) aligns with movement direction
      ctx.rotate((e.Heading || 0));


Result