Refactor codebase

This commit is contained in:
Peter Stockings
2026-01-04 15:56:18 +11:00
parent 3785885abe
commit bfe5ebae8c
18 changed files with 380 additions and 191 deletions

View File

@@ -0,0 +1,138 @@
import { ACTION_COST, ENERGY_THRESHOLD } from "../../core/constants";
import type { World, EntityId, Action, SimEvent, Actor } from "../../core/types";
import { isBlocked } from "../world/world-logic";
export function applyAction(w: World, actorId: EntityId, action: Action): SimEvent[] {
const actor = w.actors.get(actorId);
if (!actor) return [];
const events: SimEvent[] = [];
switch (action.type) {
case "move":
events.push(...handleMove(w, actor, action));
break;
case "attack":
events.push(...handleAttack(w, actor, action));
break;
case "wait":
default:
events.push({ type: "waited", actorId });
break;
}
// Spend energy for any action (move/wait/attack)
actor.energy -= ACTION_COST;
return events;
}
function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }): SimEvent[] {
const from = { ...actor.pos };
const nx = actor.pos.x + action.dx;
const ny = actor.pos.y + action.dy;
if (!isBlocked(w, nx, ny)) {
actor.pos.x = nx;
actor.pos.y = ny;
const to = { ...actor.pos };
return [{ type: "moved", actorId: actor.id, from, to }];
} else {
return [{ type: "waited", actorId: actor.id }];
}
}
function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }): SimEvent[] {
const target = w.actors.get(action.targetId);
if (target && target.stats && actor.stats) {
const events: SimEvent[] = [{ type: "attacked", attackerId: actor.id, targetId: action.targetId }];
const dmg = Math.max(1, actor.stats.attack - target.stats.defense);
target.stats.hp -= dmg;
events.push({
type: "damaged",
targetId: action.targetId,
amount: dmg,
hp: target.stats.hp,
x: target.pos.x,
y: target.pos.y
});
if (target.stats.hp <= 0) {
events.push({
type: "killed",
targetId: target.id,
killerId: actor.id,
x: target.pos.x,
y: target.pos.y,
victimType: target.type
});
w.actors.delete(target.id);
}
return events;
}
return [{ type: "waited", actorId: actor.id }];
}
/**
* Very basic enemy AI:
* - if adjacent to player, attack
* - else step toward player using greedy Manhattan
*/
export function decideEnemyAction(w: World, enemy: Actor, player: Actor): Action {
const dx = player.pos.x - enemy.pos.x;
const dy = player.pos.y - enemy.pos.y;
const dist = Math.abs(dx) + Math.abs(dy);
if (dist === 1) {
return { type: "attack", targetId: player.id };
}
const options: { dx: number; dy: number }[] = [];
if (Math.abs(dx) >= Math.abs(dy)) {
options.push({ dx: Math.sign(dx), dy: 0 });
options.push({ dx: 0, dy: Math.sign(dy) });
} else {
options.push({ dx: 0, dy: Math.sign(dy) });
options.push({ dx: Math.sign(dx), dy: 0 });
}
options.push({ dx: -options[0].dx, dy: -options[0].dy });
for (const o of options) {
if (o.dx === 0 && o.dy === 0) continue;
const nx = enemy.pos.x + o.dx;
const ny = enemy.pos.y + o.dy;
if (!isBlocked(w, nx, ny)) return { type: "move", dx: o.dx, dy: o.dy };
}
return { type: "wait" };
}
/**
* Energy/speed scheduler: runs until it's the player's turn and the game needs input.
* Returns enemy events accumulated along the way.
*/
export function stepUntilPlayerTurn(w: World, playerId: EntityId): { awaitingPlayerId: EntityId; events: SimEvent[] } {
const player = w.actors.get(playerId);
if (!player) throw new Error("Player missing");
const events: SimEvent[] = [];
while (true) {
while (![...w.actors.values()].some(a => a.energy >= ENERGY_THRESHOLD)) {
for (const a of w.actors.values()) a.energy += a.speed;
}
const ready = [...w.actors.values()].filter(a => a.energy >= ENERGY_THRESHOLD);
ready.sort((a, b) => (b.energy - a.energy) || (a.id - b.id));
const actor = ready[0];
if (actor.isPlayer) {
return { awaitingPlayerId: actor.id, events };
}
const action = decideEnemyAction(w, actor, player);
events.push(...applyAction(w, actor.id, action));
}
}