Begin refactoring GameScene
This commit is contained in:
@@ -3,24 +3,24 @@ import type { World, EntityId, Action, SimEvent, Actor, CombatantActor, Collecti
|
||||
import { isBlocked, tryDestructTile } from "../world/world-logic";
|
||||
import { isDestructibleByWalk } from "../../core/terrain";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
import { type EntityManager } from "../EntityManager";
|
||||
import { type EntityAccessor } from "../EntityAccessor";
|
||||
import { AISystem } from "../ecs/AISystem";
|
||||
import { Prefabs } from "../ecs/Prefabs";
|
||||
|
||||
export function applyAction(w: World, actorId: EntityId, action: Action, em?: EntityManager): SimEvent[] {
|
||||
const actor = w.actors.get(actorId);
|
||||
export function applyAction(w: World, actorId: EntityId, action: Action, accessor: EntityAccessor): SimEvent[] {
|
||||
const actor = accessor.getActor(actorId);
|
||||
if (!actor) return [];
|
||||
|
||||
const events: SimEvent[] = [];
|
||||
|
||||
switch (action.type) {
|
||||
case "move":
|
||||
events.push(...handleMove(w, actor, action, em));
|
||||
events.push(...handleMove(w, actor, action, accessor));
|
||||
break;
|
||||
case "attack":
|
||||
events.push(...handleAttack(w, actor, action, em));
|
||||
events.push(...handleAttack(w, actor, action, accessor));
|
||||
break;
|
||||
case "throw":
|
||||
// Throwing consumes a turn but visuals are handled by the renderer/scene directly
|
||||
// so we do NOT emit a "waited" event.
|
||||
break;
|
||||
case "wait":
|
||||
default:
|
||||
@@ -28,19 +28,16 @@ export function applyAction(w: World, actorId: EntityId, action: Action, em?: En
|
||||
break;
|
||||
}
|
||||
|
||||
// Note: Energy is now managed by ROT.Scheduler, no need to deduct manually
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function handleExpCollection(w: World, player: Actor, events: SimEvent[], em?: EntityManager) {
|
||||
function handleExpCollection(player: Actor, events: SimEvent[], accessor: EntityAccessor) {
|
||||
if (player.category !== "combatant") return;
|
||||
|
||||
const orbs = [...w.actors.values()].filter(a =>
|
||||
const actorsAtPos = accessor.getActorsAt(player.pos.x, player.pos.y);
|
||||
const orbs = actorsAtPos.filter(a =>
|
||||
a.category === "collectible" &&
|
||||
a.type === "exp_orb" &&
|
||||
a.pos.x === player.pos.x &&
|
||||
a.pos.y === player.pos.y
|
||||
a.type === "exp_orb"
|
||||
) as CollectibleActor[];
|
||||
|
||||
for (const orb of orbs) {
|
||||
@@ -55,8 +52,7 @@ function handleExpCollection(w: World, player: Actor, events: SimEvent[], em?: E
|
||||
});
|
||||
|
||||
checkLevelUp(player, events);
|
||||
if (em) em.removeActor(orb.id);
|
||||
else w.actors.delete(orb.id);
|
||||
accessor.removeActor(orb.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,47 +87,26 @@ function checkLevelUp(player: CombatantActor, events: SimEvent[]) {
|
||||
}
|
||||
|
||||
|
||||
function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }, em?: EntityManager): SimEvent[] {
|
||||
function handleMove(w: World, actor: Actor, action: { dx: number; dy: number }, accessor: EntityAccessor): SimEvent[] {
|
||||
const from = { ...actor.pos };
|
||||
const nx = actor.pos.x + action.dx;
|
||||
const ny = actor.pos.y + action.dy;
|
||||
|
||||
if (em) {
|
||||
const moved = em.movement.move(actor.id, action.dx, action.dy);
|
||||
if (moved) {
|
||||
const to = { ...actor.pos };
|
||||
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
|
||||
|
||||
const tileIdx = ny * w.width + nx;
|
||||
const tile = w.tiles[tileIdx];
|
||||
if (isDestructibleByWalk(tile)) {
|
||||
tryDestructTile(w, nx, ny);
|
||||
}
|
||||
|
||||
if (actor.category === "combatant" && actor.isPlayer) {
|
||||
handleExpCollection(w, actor, events, em);
|
||||
}
|
||||
|
||||
return events;
|
||||
if (!isBlocked(w, nx, ny, accessor)) {
|
||||
actor.pos.x = nx;
|
||||
actor.pos.y = ny;
|
||||
const to = { ...actor.pos };
|
||||
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
|
||||
|
||||
const tileIdx = ny * w.width + nx;
|
||||
if (isDestructibleByWalk(w.tiles[tileIdx])) {
|
||||
tryDestructTile(w, nx, ny);
|
||||
}
|
||||
} else {
|
||||
// Fallback for cases without EntityManager (e.g. tests)
|
||||
if (!isBlocked(w, nx, ny)) {
|
||||
actor.pos.x = nx;
|
||||
actor.pos.y = ny;
|
||||
const to = { ...actor.pos };
|
||||
const events: SimEvent[] = [{ type: "moved", actorId: actor.id, from, to }];
|
||||
|
||||
const tileIdx = ny * w.width + nx;
|
||||
if (isDestructibleByWalk(w.tiles[tileIdx])) {
|
||||
tryDestructTile(w, nx, ny);
|
||||
}
|
||||
|
||||
if (actor.category === "combatant" && actor.isPlayer) {
|
||||
handleExpCollection(w, actor, events);
|
||||
}
|
||||
return events;
|
||||
if (actor.category === "combatant" && actor.isPlayer) {
|
||||
handleExpCollection(actor, events, accessor);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
return [{ type: "move-blocked", actorId: actor.id, x: nx, y: ny }];
|
||||
@@ -139,8 +114,8 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number },
|
||||
|
||||
|
||||
|
||||
function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em?: EntityManager): SimEvent[] {
|
||||
const target = w.actors.get(action.targetId);
|
||||
function handleAttack(_w: World, actor: Actor, action: { targetId: EntityId }, accessor: EntityAccessor): SimEvent[] {
|
||||
const target = accessor.getActor(action.targetId);
|
||||
if (target && target.category === "combatant" && actor.category === "combatant") {
|
||||
const events: SimEvent[] = [{ type: "attacked", attackerId: actor.id, targetId: action.targetId }];
|
||||
|
||||
@@ -149,7 +124,6 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
const hitRoll = Math.random() * 100;
|
||||
|
||||
if (hitRoll > hitChance) {
|
||||
// Miss!
|
||||
events.push({
|
||||
type: "dodged",
|
||||
targetId: action.targetId,
|
||||
@@ -173,17 +147,15 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
const blockRoll = Math.random() * 100;
|
||||
let isBlock = false;
|
||||
if (blockRoll < target.stats.blockChance) {
|
||||
dmg = Math.floor(dmg * 0.5); // Block reduces damage by 50%
|
||||
dmg = Math.floor(dmg * 0.5);
|
||||
isBlock = true;
|
||||
}
|
||||
|
||||
target.stats.hp -= dmg;
|
||||
|
||||
// Aggression on damage: if target is enemy and attacker is player (or vice versa), alert them
|
||||
if (target.stats.hp > 0 && target.category === "combatant" && !target.isPlayer) {
|
||||
// Switch to pursuing immediately
|
||||
target.aiState = "pursuing";
|
||||
target.alertedAt = Date.now(); // Reset alert timer if any
|
||||
target.alertedAt = Date.now();
|
||||
if (actor.pos) {
|
||||
target.lastKnownPlayerPos = { ...actor.pos };
|
||||
}
|
||||
@@ -224,28 +196,18 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
y: target.pos.y,
|
||||
victimType: target.type as ActorType
|
||||
});
|
||||
if (em) em.removeActor(target.id);
|
||||
else w.actors.delete(target.id);
|
||||
|
||||
|
||||
|
||||
accessor.removeActor(target.id);
|
||||
|
||||
// Spawn EXP Orb
|
||||
const enemyDef = (GAME_CONFIG.enemies as any)[target.type || ""];
|
||||
const orbId = em ? em.getNextId() : Math.max(0, ...w.actors.keys(), target.id) + 1;
|
||||
const expAmount = enemyDef?.expValue || 0;
|
||||
|
||||
const orb: CollectibleActor = {
|
||||
id: orbId,
|
||||
category: "collectible",
|
||||
type: "exp_orb",
|
||||
pos: { ...target.pos },
|
||||
expAmount: enemyDef?.expValue || 0
|
||||
};
|
||||
|
||||
if (em) em.addActor(orb);
|
||||
else w.actors.set(orbId, orb);
|
||||
|
||||
|
||||
events.push({ type: "orb-spawned", orbId, x: target.pos.x, y: target.pos.y });
|
||||
const ecsWorld = accessor.context;
|
||||
if (ecsWorld) {
|
||||
const orbId = Prefabs.expOrb(ecsWorld, target.pos.x, target.pos.y, expAmount);
|
||||
events.push({ type: "orb-spawned", orbId, x: target.pos.x, y: target.pos.y });
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
@@ -260,12 +222,13 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
* - Alerted: Brief period after spotting player (shows "!")
|
||||
* - Pursuing: Chase player while in FOV or toward last known position
|
||||
*/
|
||||
export function decideEnemyAction(_w: World, enemy: CombatantActor, player: CombatantActor, em?: EntityManager): { action: Action; justAlerted: boolean } {
|
||||
if (em) {
|
||||
const result = em.ai.update(enemy.id, player.id);
|
||||
export function decideEnemyAction(w: World, enemy: CombatantActor, player: CombatantActor, accessor: EntityAccessor): { action: Action; justAlerted: boolean } {
|
||||
const ecsWorld = accessor.context;
|
||||
if (ecsWorld) {
|
||||
const aiSystem = new AISystem(ecsWorld, w, accessor);
|
||||
const result = aiSystem.update(enemy.id, player.id);
|
||||
|
||||
// Sync ECS component state back to Actor object for compatibility with tests and old logic
|
||||
const aiComp = em.ecsWorld.getComponent(enemy.id, "ai");
|
||||
const aiComp = ecsWorld.getComponent(enemy.id, "ai");
|
||||
if (aiComp) {
|
||||
enemy.aiState = aiComp.state;
|
||||
enemy.alertedAt = aiComp.alertedAt;
|
||||
@@ -275,8 +238,6 @@ export function decideEnemyAction(_w: World, enemy: CombatantActor, player: Comb
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback for tests or cases without EntityManager
|
||||
// [Existing decideEnemyAction logic could be kept here as fallback, or just return wait]
|
||||
return { action: { type: "wait" }, justAlerted: false };
|
||||
}
|
||||
|
||||
@@ -284,81 +245,42 @@ export function decideEnemyAction(_w: World, enemy: CombatantActor, player: Comb
|
||||
* Speed-based scheduler using rot-js: 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, em?: EntityManager): { awaitingPlayerId: EntityId; events: SimEvent[] } {
|
||||
// Energy Threshold
|
||||
export function stepUntilPlayerTurn(w: World, playerId: EntityId, accessor: EntityAccessor): { awaitingPlayerId: EntityId; events: SimEvent[] } {
|
||||
const THRESHOLD = 100;
|
||||
|
||||
// Ensure player exists
|
||||
const player = w.actors.get(playerId) as CombatantActor;
|
||||
if (!player || player.category !== "combatant") throw new Error("Player missing or invalid");
|
||||
const player = accessor.getCombatant(playerId);
|
||||
if (!player) throw new Error("Player missing or invalid");
|
||||
|
||||
const events: SimEvent[] = [];
|
||||
|
||||
// If player already has enough energy (from previous accumulation), return immediately to let them act
|
||||
// NOTE: We do NOT deduct player energy here. The player's action will cost energy in the next turn processing or we expect the caller to have deducted it?
|
||||
// Actually, standard roguelike loop:
|
||||
// 1. Player acts. Deduct cost.
|
||||
// 2. Loop game until Player has energy >= Threshold.
|
||||
|
||||
// Since this function is called AFTER user input (Player just acted), we assume Player needs to recover energy.
|
||||
// BUT, we should check if we need to deduct energy first?
|
||||
// The caller just applied an action. We should probably deduct energy for that action BEFORE entering the loop?
|
||||
// For now, let's assume the player is at < 100 energy and needs to wait.
|
||||
// Wait, if we don't deduct energy, the player stays at high energy?
|
||||
// The caller doesn't manage energy. WE manage energy.
|
||||
|
||||
// Implicitly, the player just spent 100 energy to trigger this call.
|
||||
// So we should deduct it from the player NOW.
|
||||
if (player.energy >= THRESHOLD) {
|
||||
player.energy -= THRESHOLD;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// If player has enough energy to act, return control to user
|
||||
if (player.energy >= THRESHOLD) {
|
||||
return { awaitingPlayerId: playerId, events };
|
||||
}
|
||||
|
||||
// Give energy to everyone
|
||||
for (const actor of w.actors.values()) {
|
||||
const actors = [...accessor.getAllActors()];
|
||||
for (const actor of actors) {
|
||||
if (actor.category === "combatant") {
|
||||
actor.energy += actor.speed;
|
||||
}
|
||||
}
|
||||
|
||||
// Process turns for everyone who has enough energy (except player, who breaks the loop)
|
||||
// We sort by energy to give priority to those who have waited longest/are fastest?
|
||||
// ROT.Scheduler uses a priority queue. Here we can iterate.
|
||||
// Iterating map values is insertion order.
|
||||
// Ideally we'd duplicate the list to sort it, but for performance let's simple iterate.
|
||||
|
||||
// We need to loop multiple times if someone has A LOT of energy (e.g. speed 200 vs speed 50)
|
||||
// But typically we step 1 tick.
|
||||
|
||||
// Simpler approach:
|
||||
// Process all actors with energy >= THRESHOLD.
|
||||
// If multiple have >= THRESHOLD, who goes first?
|
||||
// Usually the one with highest energy.
|
||||
|
||||
// Let's protect against infinite loops if someone has infinite speed.
|
||||
let actionsTaken = 0;
|
||||
while (true) {
|
||||
const eligibleActors = [...w.actors.values()].filter(
|
||||
a => a.category === "combatant" && a.energy >= THRESHOLD && !a.isPlayer
|
||||
) as CombatantActor[];
|
||||
const eligibleActors = accessor.getEnemies().filter(a => a.energy >= THRESHOLD);
|
||||
|
||||
if (eligibleActors.length === 0) break;
|
||||
|
||||
// Sort by energy descending
|
||||
eligibleActors.sort((a, b) => b.energy - a.energy);
|
||||
|
||||
const actor = eligibleActors[0];
|
||||
|
||||
// Actor takes a turn
|
||||
actor.energy -= THRESHOLD;
|
||||
|
||||
// Decide logic
|
||||
const decision = decideEnemyAction(w, actor, player, em);
|
||||
const decision = decideEnemyAction(w, actor, player, accessor);
|
||||
|
||||
if (decision.justAlerted) {
|
||||
events.push({
|
||||
@@ -369,15 +291,14 @@ export function stepUntilPlayerTurn(w: World, playerId: EntityId, em?: EntityMan
|
||||
});
|
||||
}
|
||||
|
||||
events.push(...applyAction(w, actor.id, decision.action, em));
|
||||
events.push(...applyAction(w, actor.id, decision.action, accessor));
|
||||
|
||||
// Check if player died
|
||||
if (!w.actors.has(playerId)) {
|
||||
if (!accessor.isPlayerAlive()) {
|
||||
return { awaitingPlayerId: null as any, events };
|
||||
}
|
||||
|
||||
actionsTaken++;
|
||||
if (actionsTaken > 1000) break; // Emergency break
|
||||
if (actionsTaken > 1000) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user