Fix bug where slower enemies (ie rat) would never get scheduled a turn
This commit is contained in:
@@ -6,8 +6,6 @@ import { findPathAStar } from "../world/pathfinding";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
import { type EntityManager } from "../EntityManager";
|
||||
import { FOV } from "rot-js";
|
||||
import * as ROT from "rot-js";
|
||||
|
||||
|
||||
export function applyAction(w: World, actorId: EntityId, action: Action, em?: EntityManager): SimEvent[] {
|
||||
const actor = w.actors.get(actorId);
|
||||
@@ -168,6 +166,16 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
|
||||
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
|
||||
if (actor.pos) {
|
||||
target.lastKnownPlayerPos = { ...actor.pos };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Lifesteal Logic
|
||||
if (actor.stats.lifesteal > 0 && dmg > 0) {
|
||||
const healAmount = Math.floor(dmg * (actor.stats.lifesteal / 100));
|
||||
@@ -210,7 +218,6 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
|
||||
// Spawn EXP Orb
|
||||
const enemyDef = (GAME_CONFIG.enemies as any)[target.type || ""];
|
||||
const expAmount = enemyDef?.expValue || 0;
|
||||
const orbId = em ? em.getNextId() : Math.max(0, ...w.actors.keys(), target.id) + 1;
|
||||
|
||||
const orb: CollectibleActor = {
|
||||
@@ -218,7 +225,7 @@ function handleAttack(w: World, actor: Actor, action: { targetId: EntityId }, em
|
||||
category: "collectible",
|
||||
type: "exp_orb",
|
||||
pos: { ...target.pos },
|
||||
expAmount
|
||||
expAmount: enemyDef?.expValue || 0
|
||||
};
|
||||
|
||||
if (em) em.addActor(orb);
|
||||
@@ -243,6 +250,7 @@ function canEnemySeePlayer(w: World, enemy: CombatantActor, player: CombatantAct
|
||||
|
||||
const fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
|
||||
if (!inBounds(w, x, y)) return false;
|
||||
if (x === enemy.pos.x && y === enemy.pos.y) return true; // Can always see out of own tile
|
||||
const idx = y * w.width + x;
|
||||
return !blocksSight(w.tiles[idx]);
|
||||
});
|
||||
@@ -305,6 +313,15 @@ export function decideEnemyAction(w: World, enemy: CombatantActor, player: Comba
|
||||
|
||||
// State transitions
|
||||
let justAlerted = false;
|
||||
|
||||
// Check if alerted state has expired
|
||||
if (enemy.aiState === "alerted") {
|
||||
const alertDuration = 1000;
|
||||
if (Date.now() - (enemy.alertedAt || 0) > alertDuration) {
|
||||
enemy.aiState = "pursuing";
|
||||
}
|
||||
}
|
||||
|
||||
if (canSee) {
|
||||
if (enemy.aiState === "wandering" || enemy.aiState === "searching") {
|
||||
// Spotted player (or re-spotted)! Transition to alerted state
|
||||
@@ -318,13 +335,7 @@ export function decideEnemyAction(w: World, enemy: CombatantActor, player: Comba
|
||||
}
|
||||
} else {
|
||||
// Cannot see player
|
||||
if (enemy.aiState === "alerted") {
|
||||
// Check if alert period is over (1 second = 1000ms)
|
||||
const alertDuration = 1000;
|
||||
if (Date.now() - (enemy.alertedAt || 0) > alertDuration) {
|
||||
enemy.aiState = "pursuing";
|
||||
}
|
||||
} else if (enemy.aiState === "pursuing") {
|
||||
if (enemy.aiState === "pursuing") {
|
||||
// Lost sight while pursuing -> switch to searching
|
||||
enemy.aiState = "searching";
|
||||
} else if (enemy.aiState === "searching") {
|
||||
@@ -402,58 +413,99 @@ export function decideEnemyAction(w: World, enemy: CombatantActor, player: Comba
|
||||
* Returns enemy events accumulated along the way.
|
||||
*/
|
||||
export function stepUntilPlayerTurn(w: World, playerId: EntityId, em?: EntityManager): { awaitingPlayerId: EntityId; events: SimEvent[] } {
|
||||
// Energy Threshold
|
||||
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 events: SimEvent[] = [];
|
||||
|
||||
// Create scheduler and add all combatants
|
||||
const scheduler = new ROT.Scheduler.Speed();
|
||||
// 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.
|
||||
|
||||
for (const actor of w.actors.values()) {
|
||||
if (actor.category === "combatant") {
|
||||
// ROT.Scheduler.Speed expects actors to have a getSpeed() method
|
||||
// Add it dynamically if it doesn't exist
|
||||
const actorWithGetSpeed = actor as any;
|
||||
if (!actorWithGetSpeed.getSpeed) {
|
||||
actorWithGetSpeed.getSpeed = function() { return this.speed; };
|
||||
}
|
||||
scheduler.add(actorWithGetSpeed, true);
|
||||
}
|
||||
// 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) {
|
||||
// Get next actor from scheduler
|
||||
const actor = scheduler.next() as CombatantActor | null;
|
||||
|
||||
if (!actor || !w.actors.has(actor.id)) {
|
||||
// Actor was removed (died), continue to next
|
||||
continue;
|
||||
// If player has enough energy to act, return control to user
|
||||
if (player.energy >= THRESHOLD) {
|
||||
return { awaitingPlayerId: playerId, events };
|
||||
}
|
||||
|
||||
if (actor.isPlayer) {
|
||||
// Player's turn - return control to the user
|
||||
return { awaitingPlayerId: actor.id, events };
|
||||
// Give energy to everyone
|
||||
for (const actor of w.actors.values()) {
|
||||
if (actor.category === "combatant") {
|
||||
actor.energy += actor.speed;
|
||||
}
|
||||
}
|
||||
|
||||
// Enemy turn - decide action and apply it
|
||||
const decision = decideEnemyAction(w, actor, player, em);
|
||||
// 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.
|
||||
|
||||
// Emit alert event if enemy just spotted player
|
||||
if (decision.justAlerted) {
|
||||
events.push({
|
||||
type: "enemy-alerted",
|
||||
enemyId: actor.id,
|
||||
x: actor.pos.x,
|
||||
y: actor.pos.y
|
||||
});
|
||||
}
|
||||
// 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.
|
||||
|
||||
events.push(...applyAction(w, actor.id, decision.action, em));
|
||||
// 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[];
|
||||
|
||||
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);
|
||||
|
||||
if (decision.justAlerted) {
|
||||
events.push({
|
||||
type: "enemy-alerted",
|
||||
enemyId: actor.id,
|
||||
x: actor.pos.x,
|
||||
y: actor.pos.y
|
||||
});
|
||||
}
|
||||
|
||||
events.push(...applyAction(w, actor.id, decision.action, em));
|
||||
|
||||
// Check if player was killed by this action
|
||||
if (!w.actors.has(playerId)) {
|
||||
return { awaitingPlayerId: null as any, events };
|
||||
// Check if player died
|
||||
if (!w.actors.has(playerId)) {
|
||||
return { awaitingPlayerId: null as any, events };
|
||||
}
|
||||
|
||||
actionsTaken++;
|
||||
if (actionsTaken > 1000) break; // Emergency break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user