Title of State:
Introduce an Entity System
Goal of Change:
add a persistent game-state structure that can represent:
- buildings
- vehicles
- infantry dots
- ownership/color
- position + facing
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));