Refactor codebase
This commit is contained in:
138
src/engine/simulation/simulation.ts
Normal file
138
src/engine/simulation/simulation.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user