aether-shards/test/systems/SkillTargetingSystem.test.js
Matthew Mone ac0f3cc396 Enhance testing and integration of inventory and character management systems
Add comprehensive tests for the InventoryManager and InventoryContainer to validate item management functionalities. Implement integration tests for the CharacterSheet component, ensuring proper interaction with the inventory system. Update the Explorer class to support new inventory features and maintain backward compatibility. Refactor related components for improved clarity and performance.
2025-12-27 16:54:03 -08:00

459 lines
14 KiB
JavaScript

import { expect } from "@esm-bundle/chai";
import { SkillTargetingSystem } from "../../src/systems/SkillTargetingSystem.js";
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
import { UnitManager } from "../../src/managers/UnitManager.js";
import { Explorer } from "../../src/units/Explorer.js";
import { Enemy } from "../../src/units/Enemy.js";
describe("Systems: SkillTargetingSystem", function () {
let targetingSystem;
let grid;
let unitManager;
let mockRegistry;
let skillRegistry;
beforeEach(() => {
// Create a 20x10x20 grid
grid = new VoxelGrid(20, 10, 20);
// Create floor at y=0
for (let x = 0; x < 20; x++) {
for (let z = 0; z < 20; z++) {
grid.setCell(x, 0, z, 1); // Floor
grid.setCell(x, 1, z, 0); // Air at y=1 (walkable)
}
}
// Create mock unit registry
mockRegistry = new Map();
mockRegistry.set("CLASS_VANGUARD", {
id: "CLASS_VANGUARD",
name: "Vanguard",
base_stats: {
health: 100,
attack: 10,
defense: 5,
speed: 10,
movement: 4,
},
});
mockRegistry.set("ENEMY_DEFAULT", {
id: "ENEMY_DEFAULT",
name: "Enemy",
type: "ENEMY",
base_stats: {
health: 50,
attack: 5,
defense: 2,
speed: 8,
movement: 3,
},
});
unitManager = new UnitManager(mockRegistry);
// Create skill registry with various skill types (using nested format like actual JSON files)
skillRegistry = new Map();
skillRegistry.set("SKILL_SINGLE_TARGET", {
id: "SKILL_SINGLE_TARGET",
name: "Single Target",
costs: { ap: 2 },
targeting: {
range: 5,
type: "ENEMY",
line_of_sight: true,
},
effects: [],
});
skillRegistry.set("SKILL_CIRCLE_AOE", {
id: "SKILL_CIRCLE_AOE",
name: "Circle AoE",
costs: { ap: 3 },
targeting: {
range: 4,
type: "ENEMY",
line_of_sight: true,
area_of_effect: { shape: "CIRCLE", size: 2 },
},
effects: [],
});
skillRegistry.set("SKILL_LINE_AOE", {
id: "SKILL_LINE_AOE",
name: "Line AoE",
costs: { ap: 2 },
targeting: {
range: 6,
type: "ENEMY",
line_of_sight: true,
area_of_effect: { shape: "LINE", size: 4 },
},
effects: [],
});
skillRegistry.set("SKILL_ALLY_HEAL", {
id: "SKILL_ALLY_HEAL",
name: "Heal",
costs: { ap: 2 },
targeting: {
range: 3,
type: "ALLY",
line_of_sight: true,
},
effects: [],
});
skillRegistry.set("SKILL_EMPTY_TARGET", {
id: "SKILL_EMPTY_TARGET",
name: "Place Trap",
costs: { ap: 1 },
targeting: {
range: 3,
type: "EMPTY",
line_of_sight: true,
},
effects: [],
});
skillRegistry.set("SKILL_IGNORE_COVER", {
id: "SKILL_IGNORE_COVER",
name: "Piercing Shot",
costs: { ap: 3 },
targeting: {
range: 8,
type: "ENEMY",
line_of_sight: false, // false means ignore cover
},
effects: [],
});
targetingSystem = new SkillTargetingSystem(
grid,
unitManager,
skillRegistry
);
});
describe("Range Validation", () => {
it("should validate target within range", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
// Add an enemy at target position for ENEMY target type validation
const enemy = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
const targetPos = { x: 7, y: 1, z: 5 }; // 2 tiles away (within range 5)
enemy.position = targetPos;
grid.placeUnit(enemy, targetPos);
const result = targetingSystem.validateTarget(
source,
targetPos,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.true;
});
it("should reject target out of range", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const targetPos = { x: 12, y: 1, z: 5 }; // 7 tiles away (out of range 5)
const result = targetingSystem.validateTarget(
source,
targetPos,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.false;
expect(result.reason).to.include("range");
});
});
describe("Line of Sight (LOS)", () => {
it("should validate target with clear line of sight", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
// Add an enemy at target position for ENEMY target type validation
const enemy = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
const targetPos = { x: 8, y: 1, z: 5 }; // Clear path
enemy.position = targetPos;
grid.placeUnit(enemy, targetPos);
const result = targetingSystem.validateTarget(
source,
targetPos,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.true;
});
it("should reject target blocked by solid voxel", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
// Place a wall between source and target
grid.setCell(6, 1, 5, 1); // Solid block
grid.setCell(6, 2, 5, 1); // Solid block
const targetPos = { x: 8, y: 1, z: 5 };
const result = targetingSystem.validateTarget(
source,
targetPos,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.false;
expect(result.reason).to.include("line of sight");
});
it("should allow target blocked by cover if skill ignores cover", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
// Place a wall between source and target
grid.setCell(6, 1, 5, 1);
grid.setCell(6, 2, 5, 1);
// Add an enemy at target position for ENEMY target type validation
const enemy = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
const targetPos = { x: 8, y: 1, z: 5 };
enemy.position = targetPos;
grid.placeUnit(enemy, targetPos);
const result = targetingSystem.validateTarget(
source,
targetPos,
"SKILL_IGNORE_COVER"
);
expect(result.valid).to.be.true; // Should pass despite cover
});
});
describe("Target Type Validation", () => {
it("should validate ENEMY target type", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const enemy = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy.position = { x: 7, y: 1, z: 5 };
grid.placeUnit(enemy, enemy.position);
const result = targetingSystem.validateTarget(
source,
enemy.position,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.true;
});
it("should reject ENEMY target type when targeting ally", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const ally = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
ally.position = { x: 7, y: 1, z: 5 };
grid.placeUnit(ally, ally.position);
const result = targetingSystem.validateTarget(
source,
ally.position,
"SKILL_SINGLE_TARGET"
);
expect(result.valid).to.be.false;
expect(result.reason).to.include("target type");
});
it("should validate ALLY target type", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const ally = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
ally.position = { x: 7, y: 1, z: 5 };
grid.placeUnit(ally, ally.position);
const result = targetingSystem.validateTarget(
source,
ally.position,
"SKILL_ALLY_HEAL"
);
expect(result.valid).to.be.true;
});
it("should validate EMPTY target type", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const emptyPos = { x: 7, y: 1, z: 5 }; // No unit here
const result = targetingSystem.validateTarget(
source,
emptyPos,
"SKILL_EMPTY_TARGET"
);
expect(result.valid).to.be.true;
});
it("should reject EMPTY target type when tile is occupied", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const enemy = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy.position = { x: 7, y: 1, z: 5 };
grid.placeUnit(enemy, enemy.position);
const result = targetingSystem.validateTarget(
source,
enemy.position,
"SKILL_EMPTY_TARGET"
);
expect(result.valid).to.be.false;
expect(result.reason).to.include("empty");
});
});
describe("AoE Calculation: SINGLE", () => {
it("should return single tile for SINGLE AoE", () => {
const sourcePos = { x: 5, y: 1, z: 5 };
const cursorPos = { x: 7, y: 1, z: 5 };
const tiles = targetingSystem.getAoETiles(
sourcePos,
cursorPos,
"SKILL_SINGLE_TARGET"
);
expect(tiles).to.have.length(1);
expect(tiles[0]).to.deep.equal(cursorPos);
});
});
describe("AoE Calculation: CIRCLE", () => {
it("should return all tiles within radius for CIRCLE AoE", () => {
const sourcePos = { x: 5, y: 1, z: 5 };
const cursorPos = { x: 7, y: 1, z: 5 }; // Center of circle
const tiles = targetingSystem.getAoETiles(
sourcePos,
cursorPos,
"SKILL_CIRCLE_AOE"
);
// Circle with radius 2 should include center + surrounding tiles
// Should have at least the center tile
expect(tiles.length).to.be.greaterThan(0);
expect(tiles.some((t) => t.x === 7 && t.z === 5)).to.be.true;
});
it("should include tiles at exact radius distance", () => {
const sourcePos = { x: 5, y: 1, z: 5 };
const cursorPos = { x: 7, y: 1, z: 5 };
const tiles = targetingSystem.getAoETiles(
sourcePos,
cursorPos,
"SKILL_CIRCLE_AOE"
);
// Check that tiles at radius 2 are included (Manhattan distance)
const hasRadius2Tile = tiles.some((t) => {
const dist =
Math.abs(t.x - cursorPos.x) +
Math.abs(t.y - cursorPos.y) +
Math.abs(t.z - cursorPos.z);
return dist === 2;
});
expect(hasRadius2Tile).to.be.true;
});
});
describe("AoE Calculation: LINE", () => {
it("should return line of tiles from source to target for LINE AoE", () => {
const sourcePos = { x: 5, y: 1, z: 5 };
const cursorPos = { x: 9, y: 1, z: 5 }; // 4 tiles east
const tiles = targetingSystem.getAoETiles(
sourcePos,
cursorPos,
"SKILL_LINE_AOE"
);
// Line should include tiles along the path
expect(tiles.length).to.be.greaterThan(1);
// Should include source position or nearby tiles in the line
const hasSourceDirection = tiles.some(
(t) => t.x >= sourcePos.x && t.x <= cursorPos.x && t.z === sourcePos.z
);
expect(hasSourceDirection).to.be.true;
});
});
describe("getUnitsInAoE", () => {
it("should return all units within AoE", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
// Place enemies in AoE range
const enemy1 = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy1.position = { x: 7, y: 1, z: 5 };
grid.placeUnit(enemy1, enemy1.position);
const enemy2 = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy2.position = { x: 8, y: 1, z: 5 };
grid.placeUnit(enemy2, enemy2.position);
const sourcePos = source.position;
const targetPos = { x: 7, y: 1, z: 5 };
const units = targetingSystem.getUnitsInAoE(
sourcePos,
targetPos,
"SKILL_CIRCLE_AOE"
);
expect(units.length).to.be.greaterThan(0);
expect(units.some((u) => u.id === enemy1.id)).to.be.true;
});
it("should only return units within AoE radius", () => {
const source = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
source.position = { x: 5, y: 1, z: 5 };
grid.placeUnit(source, source.position);
const enemy1 = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy1.position = { x: 7, y: 1, z: 5 }; // Within radius 2
grid.placeUnit(enemy1, enemy1.position);
const enemy2 = unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
enemy2.position = { x: 12, y: 1, z: 5 }; // Outside radius 2
grid.placeUnit(enemy2, enemy2.position);
const sourcePos = { x: 5, y: 1, z: 5 };
const targetPos = { x: 7, y: 1, z: 5 };
const units = targetingSystem.getUnitsInAoE(
sourcePos,
targetPos,
"SKILL_CIRCLE_AOE"
);
expect(units.some((u) => u.id === enemy1.id)).to.be.true;
expect(units.some((u) => u.id === enemy2.id)).to.be.false;
});
});
});