Highlight active item slot and activate when shortcut key is pressed

This commit is contained in:
Peter Stockings
2026-01-06 22:33:51 +11:00
parent 309ab19e8c
commit 505f62ac97
6 changed files with 275 additions and 23 deletions

View File

@@ -0,0 +1,132 @@
import { describe, it, expect } from "vitest";
import { getClosestVisibleEnemy } from "../gameplay/CombatLogic";
import type { World, CombatantActor } from "../../core/types";
describe("CombatLogic - getClosestVisibleEnemy", () => {
// Helper to create valid default stats for testing
const createMockStats = () => ({
hp: 10, maxHp: 10, attack: 1, defense: 0,
accuracy: 100, evasion: 0, critChance: 0, critMultiplier: 0,
blockChance: 0, lifesteal: 0, mana: 0, maxMana: 0,
level: 1, exp: 0, expToNextLevel: 100, luck: 0,
statPoints: 0, skillPoints: 0,
strength: 10, dexterity: 10, intelligence: 10,
passiveNodes: []
});
it("should return null if no enemies are visible", () => {
const world: World = {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors: new Map(),
exit: { x: 9, y: 9 }
};
const player: CombatantActor = {
id: 0, category: "combatant", type: "player", pos: { x: 5, y: 5 }, isPlayer: true,
stats: createMockStats(),
inventory: { gold: 0, items: [] }, equipment: {},
speed: 1, energy: 0
};
world.actors.set(0, player);
const enemy: CombatantActor = {
id: 1, category: "combatant", type: "rat", pos: { x: 6, y: 6 }, isPlayer: false,
stats: createMockStats(),
speed: 1, energy: 0
};
world.actors.set(1, enemy);
// Mock seenArray where nothing is seen
const seenArray = new Uint8Array(100).fill(0);
const result = getClosestVisibleEnemy(world, player.pos, seenArray, 10);
expect(result).toBeNull();
});
it("should return the closest visible enemy", () => {
const world: World = {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors: new Map(),
exit: { x: 9, y: 9 }
};
const player: CombatantActor = {
id: 0, category: "combatant", type: "player", pos: { x: 5, y: 5 }, isPlayer: true,
stats: createMockStats(),
inventory: { gold: 0, items: [] }, equipment: {},
speed: 1, energy: 0
};
world.actors.set(0, player);
// Enemy 1: Close (distance sqrt(2) ~= 1.41)
const enemy1: CombatantActor = {
id: 1, category: "combatant", type: "rat", pos: { x: 6, y: 6 }, isPlayer: false,
stats: createMockStats(),
speed: 1, energy: 0
};
world.actors.set(1, enemy1);
// Enemy 2: Farther (distance sqrt(8) ~= 2.82)
const enemy2: CombatantActor = {
id: 2, category: "combatant", type: "rat", pos: { x: 7, y: 7 }, isPlayer: false,
stats: createMockStats(),
speed: 1, energy: 0
};
world.actors.set(2, enemy2);
// Mock seenArray where both are seen
const seenArray = new Uint8Array(100).fill(0);
seenArray[6 * 10 + 6] = 1; // Enemy 1 visible
seenArray[7 * 10 + 7] = 1; // Enemy 2 visible
const result = getClosestVisibleEnemy(world, player.pos, seenArray, 10);
expect(result).toEqual({ x: 6, y: 6 });
});
it("should ignore invisible closer enemies and select visible farther ones", () => {
const world: World = {
width: 10,
height: 10,
tiles: new Array(100).fill(0),
actors: new Map(),
exit: { x: 9, y: 9 }
};
const player: CombatantActor = {
id: 0, category: "combatant", type: "player", pos: { x: 5, y: 5 }, isPlayer: true,
stats: createMockStats(),
inventory: { gold: 0, items: [] }, equipment: {},
speed: 1, energy: 0
};
world.actors.set(0, player);
// Enemy 1: Close but invisible
const enemy1: CombatantActor = {
id: 1, category: "combatant", type: "rat", pos: { x: 6, y: 6 }, isPlayer: false,
stats: createMockStats(),
speed: 1, energy: 0
};
world.actors.set(1, enemy1);
// Enemy 2: Farther but visible
const enemy2: CombatantActor = {
id: 2, category: "combatant", type: "rat", pos: { x: 8, y: 5 }, isPlayer: false,
stats: createMockStats(),
speed: 1, energy: 0
};
world.actors.set(2, enemy2);
// Mock seenArray where only Enemy 2 is seen
const seenArray = new Uint8Array(100).fill(0);
seenArray[5 * 10 + 8] = 1; // Enemy 2 visible at (8,5)
const result = getClosestVisibleEnemy(world, player.pos, seenArray, 10);
expect(result).toEqual({ x: 8, y: 5 });
});
});

View File

@@ -51,3 +51,46 @@ export function traceProjectile(
hitActorId
};
}
/**
* Finds the closest visible enemy to a given position.
*/
export function getClosestVisibleEnemy(
world: World,
origin: Vec2,
seenTiles: Set<string> | boolean[] | Uint8Array, // Support various visibility structures
width?: number // Required if seenTiles is a flat array
): Vec2 | null {
let closestDistSq = Infinity;
let closestPos: Vec2 | null = null;
// Helper to check visibility
const isVisible = (x: number, y: number) => {
if (Array.isArray(seenTiles) || seenTiles instanceof Uint8Array || seenTiles instanceof Int8Array) {
// Flat array
if (!width) return false;
return (seenTiles as any)[y * width + x];
} else {
// Set<string>
return (seenTiles as Set<string>).has(`${x},${y}`);
}
};
for (const actor of world.actors.values()) {
if (actor.category !== "combatant" || actor.isPlayer) continue;
// Check visibility
if (!isVisible(actor.pos.x, actor.pos.y)) continue;
const dx = actor.pos.x - origin.x;
const dy = actor.pos.y - origin.y;
const distSq = dx*dx + dy*dy;
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestPos = { x: actor.pos.x, y: actor.pos.y };
}
}
return closestPos;
}