Allow melee attacking diagonally as well
This commit is contained in:
@@ -377,4 +377,82 @@ describe('Combat Simulation', () => {
|
||||
expect(events.some(e => e.type === "leveled-up")).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("Diagonal Mechanics", () => {
|
||||
it("should allow enemy to attack player diagonally", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Enemy at 4,4. Player at 5,5 (diagonal)
|
||||
const enemy = {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
pos: { x: 4, y: 4 },
|
||||
stats: createTestStats(),
|
||||
aiState: "pursuing", // Skip alert phase
|
||||
energy: 0
|
||||
} as any;
|
||||
const player = {
|
||||
id: 2,
|
||||
category: "combatant",
|
||||
isPlayer: true,
|
||||
pos: { x: 5, y: 5 },
|
||||
stats: createTestStats(),
|
||||
energy: 0
|
||||
} as any;
|
||||
|
||||
actors.set(1, enemy);
|
||||
actors.set(2, player);
|
||||
const world = createTestWorld(actors);
|
||||
|
||||
// Enemy should decide to attack
|
||||
const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
|
||||
|
||||
expect(decision.action.type).toBe("attack");
|
||||
if (decision.action.type === "attack") {
|
||||
expect(decision.action.targetId).toBe(player.id);
|
||||
}
|
||||
});
|
||||
|
||||
it("should allow player to attack enemy diagonally via applyAction", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 4 }, stats: createTestStats(), energy: 0 } as any;
|
||||
const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 5, y: 5 }, stats: createTestStats(), energy: 0 } as any;
|
||||
|
||||
actors.set(1, player);
|
||||
actors.set(2, enemy);
|
||||
const world = createTestWorld(actors);
|
||||
|
||||
const action: any = { type: "attack", targetId: 2 };
|
||||
const events = applyAction(world, 1, action, new EntityManager(world));
|
||||
|
||||
const attackEvent = events.find(e => e.type === "attacked");
|
||||
expect(attackEvent).toBeDefined();
|
||||
expect(attackEvent?.targetId).toBe(2);
|
||||
});
|
||||
|
||||
it("should NOT generate diagonal move for enemy", () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Enemy at 4,4. Player at 4,6. Dist 2.
|
||||
const enemy = {
|
||||
id: 1,
|
||||
category: "combatant",
|
||||
isPlayer: false,
|
||||
pos: { x: 4, y: 4 },
|
||||
stats: createTestStats(),
|
||||
aiState: "pursuing",
|
||||
energy: 0
|
||||
} as any;
|
||||
const player = { id: 2, category: "combatant", isPlayer: true, pos: { x: 4, y: 6 }, stats: createTestStats(), energy: 0 } as any;
|
||||
|
||||
actors.set(1, enemy);
|
||||
actors.set(2, player);
|
||||
const world = createTestWorld(actors);
|
||||
|
||||
const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
|
||||
if (decision.action.type === "move") {
|
||||
const { dx, dy } = decision.action;
|
||||
// Should be (0, 1) or cardinal, sum of abs should be 1
|
||||
expect(Math.abs(dx) + Math.abs(dy)).toBe(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,7 +309,6 @@ export function decideEnemyAction(w: World, enemy: CombatantActor, player: Comba
|
||||
const canSee = canEnemySeePlayer(w, enemy, player);
|
||||
const dx = player.pos.x - enemy.pos.x;
|
||||
const dy = player.pos.y - enemy.pos.y;
|
||||
const dist = Math.abs(dx) + Math.abs(dy);
|
||||
|
||||
// State transitions
|
||||
let justAlerted = false;
|
||||
@@ -369,8 +368,9 @@ export function decideEnemyAction(w: World, enemy: CombatantActor, player: Comba
|
||||
const targetDx = targetPos.x - enemy.pos.x;
|
||||
const targetDy = targetPos.y - enemy.pos.y;
|
||||
|
||||
// If adjacent to player, attack
|
||||
if (dist === 1 && canSee) {
|
||||
// If adjacent or diagonal to player, attack
|
||||
const chebyshevDist = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
if (chebyshevDist === 1 && canSee) {
|
||||
return { action: { type: "attack", targetId: player.id }, justAlerted };
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,23 @@ export class GameScene extends Phaser.Scene {
|
||||
a.category === "combatant" && a.pos.x === tx && a.pos.y === ty && !a.isPlayer
|
||||
);
|
||||
|
||||
// Check for diagonal adjacency for immediate attack
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
const dx = tx - player.pos.x;
|
||||
const dy = ty - player.pos.y;
|
||||
const isDiagonalNeighbor = Math.abs(dx) === 1 && Math.abs(dy) === 1;
|
||||
|
||||
if (isEnemy && isDiagonalNeighbor) {
|
||||
// Check targetId again to get the ID... technically we just did .some() above.
|
||||
const targetId = [...this.world.actors.values()].find(
|
||||
a => a.category === "combatant" && a.pos.x === tx && a.pos.y === ty && !a.isPlayer
|
||||
)?.id;
|
||||
if (targetId !== undefined) {
|
||||
this.commitPlayerAction({ type: "attack", targetId });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const path = findPathAStar(
|
||||
this.world,
|
||||
this.dungeonRenderer.seenArray,
|
||||
@@ -274,15 +290,44 @@ export class GameScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
// Arrow keys - Support diagonals for attacking only
|
||||
let action: Action | null = null;
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.left!)) dx = -1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.right!)) dx = 1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) dy = -1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.down!)) dy = 1;
|
||||
// Check all keys to allow simultaneous presses
|
||||
if (this.cursors.left!.isDown) dx -= 1;
|
||||
if (this.cursors.right!.isDown) dx += 1;
|
||||
if (this.cursors.up!.isDown) dy -= 1;
|
||||
if (this.cursors.down!.isDown) dy += 1;
|
||||
|
||||
// Force single step input "just now" check to avoid super speed,
|
||||
// OR we rely on `awaitingPlayer` to throttle us.
|
||||
// `update` runs every frame. `awaitingPlayer` is set to false in `commitPlayerAction`.
|
||||
// It remains false until `stepUntilPlayerTurn` returns true.
|
||||
// So as long as we only act when `awaitingPlayer` is true, simple `isDown` works for direction combination.
|
||||
// BUT we need to ensure we don't accidentally move if we just want to tap.
|
||||
// However, common roguelike Input: if you hold, you repeat.
|
||||
// We already have `awaitingPlayer` logic.
|
||||
|
||||
// One nuance: mixing JustDown and isDown.
|
||||
// If we use isDown, we might act immediately.
|
||||
// If we want to support "turn based", usually we wait for "JustDown" of *any* key.
|
||||
// But if we want diagonal, we need 2 keys.
|
||||
// Simpler approach:
|
||||
// If any direction key is JustDown, capture the state of ALL direction keys.
|
||||
const anyJustDown = Phaser.Input.Keyboard.JustDown(this.cursors.left!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.right!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.up!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.down!);
|
||||
|
||||
if (anyJustDown) {
|
||||
// Recalculate dx/dy based on currently held keys to catch the combo
|
||||
dx = 0; dy = 0;
|
||||
if (this.cursors.left!.isDown) dx -= 1;
|
||||
if (this.cursors.right!.isDown) dx += 1;
|
||||
if (this.cursors.up!.isDown) dy -= 1;
|
||||
if (this.cursors.down!.isDown) dy += 1;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
@@ -297,9 +342,13 @@ export class GameScene extends Phaser.Scene {
|
||||
if (targetId !== undefined) {
|
||||
action = { type: "attack", targetId };
|
||||
} else {
|
||||
// Only move if strictly cardinal (no diagonals)
|
||||
if (Math.abs(dx) + Math.abs(dy) === 1) {
|
||||
action = { type: "move", dx, dy };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
this.playerPath = [];
|
||||
|
||||
Reference in New Issue
Block a user