aether-shards/test/systems/PassiveItemEffects.test.js
Matthew Mone f04905044d Implement EffectProcessor and related systems for enhanced game mechanics
Introduce the EffectProcessor class to manage game state changes through various effects, including damage, healing, and status application. Define type specifications for effects, conditions, and passive abilities in Effects.d.ts. Add a comprehensive JSON registry for passive skills and item effects, enhancing gameplay dynamics. Update the GameLoop and TurnSystem to integrate the EffectProcessor, ensuring proper handling of environmental hazards and passive effects during combat. Enhance testing coverage for the EffectProcessor and environmental interactions to validate functionality and performance.
2025-12-30 20:50:11 -08:00

358 lines
11 KiB
JavaScript

import { expect } from "@esm-bundle/chai";
import { GameLoop } from "../../src/core/GameLoop.js";
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
import { UnitManager } from "../../src/managers/UnitManager.js";
import { EffectProcessor } from "../../src/systems/EffectProcessor.js";
import { itemRegistry } from "../../src/managers/ItemRegistry.js";
describe("Systems: Passive Item Effects", function () {
let gameLoop;
let grid;
let unitManager;
let effectProcessor;
let sourceUnit;
let targetUnit;
let mockRegistry;
beforeEach(async () => {
// Create mock registry
mockRegistry = new Map();
mockRegistry.set("CLASS_VANGUARD", {
id: "CLASS_VANGUARD",
name: "Vanguard",
type: "EXPLORER",
base_stats: {
health: 100,
attack: 10,
defense: 5,
magic: 5,
speed: 10,
movement: 4,
},
});
mockRegistry.set("ENEMY_GOBLIN", {
id: "ENEMY_GOBLIN",
name: "Goblin",
type: "ENEMY",
stats: {
health: 50,
attack: 8,
defense: 3,
magic: 0,
},
});
unitManager = new UnitManager(mockRegistry);
grid = new VoxelGrid(20, 5, 20);
// Create walkable floor
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 (walkable)
grid.setCell(x, 2, z, 0); // Headroom
}
}
effectProcessor = new EffectProcessor(grid, unitManager);
// Create test units
sourceUnit = unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
sourceUnit.position = { x: 5, y: 1, z: 5 };
sourceUnit.currentHealth = 100;
sourceUnit.maxHealth = 100;
grid.placeUnit(sourceUnit, sourceUnit.position);
targetUnit = unitManager.createUnit("ENEMY_GOBLIN", "ENEMY");
targetUnit.position = { x: 6, y: 1, z: 5 };
targetUnit.currentHealth = 50;
targetUnit.maxHealth = 50;
grid.placeUnit(targetUnit, targetUnit.position);
// Load items if needed
if (itemRegistry.items.size === 0) {
await itemRegistry.loadAll();
}
});
describe("Integration Point 2: Passive Item Effects", () => {
it("should process ON_DAMAGED trigger when unit takes damage", () => {
// Create a mock item with ON_DAMAGED passive
const mockItem = {
id: "ITEM_THORNS",
name: "Thorns Armor",
type: "ARMOR",
passives: [
{
trigger: "ON_DAMAGED",
action: "DAMAGE",
params: {
power: 5,
element: "PHYSICAL",
target: "SOURCE",
},
},
],
};
// Mock itemRegistry
const originalGet = itemRegistry.get;
itemRegistry.get = (id) => {
if (id === "ITEM_THORNS") return mockItem;
return originalGet.call(itemRegistry, id);
};
// Equip item to target unit
targetUnit.loadout = {
mainHand: null,
offHand: null,
body: { defId: "ITEM_THORNS", uid: "test-thorns" },
accessory: null,
belt: [null, null],
};
// Create GameLoop instance (simplified for testing)
const gameLoop = {
effectProcessor,
inventoryManager: {
itemRegistry,
},
processPassiveItemEffects: function (unit, trigger, context) {
if (!unit || !unit.loadout || !this.effectProcessor || !this.inventoryManager) {
return;
}
const equippedItems = [
unit.loadout.mainHand,
unit.loadout.offHand,
unit.loadout.body,
unit.loadout.accessory,
...(unit.loadout.belt || []),
].filter(Boolean);
for (const itemInstance of equippedItems) {
if (!itemInstance || !itemInstance.defId) continue;
const itemDef = this.inventoryManager.itemRegistry.get(itemInstance.defId);
if (!itemDef || !itemDef.passives) continue;
for (const passive of itemDef.passives) {
if (passive.trigger !== trigger) continue;
const effectDef = {
type: passive.action,
power: passive.params.power,
element: passive.params.element,
};
let target = context.target || context.source || unit;
if (passive.params.target === "SOURCE" && context.source) {
target = context.source;
}
this.effectProcessor.process(effectDef, unit, target);
}
}
},
};
const initialSourceHP = sourceUnit.currentHealth;
const sourceDefense = sourceUnit.baseStats.defense || 0;
const expectedThornsDamage = Math.max(0, 5 - sourceDefense); // 5 thorns - defense
// Apply damage to target (this should trigger ON_DAMAGED)
const damageEffect = {
type: "DAMAGE",
power: 10,
};
effectProcessor.process(damageEffect, sourceUnit, targetUnit);
// Process passive effects
gameLoop.processPassiveItemEffects(targetUnit, "ON_DAMAGED", {
source: sourceUnit,
damageAmount: 10,
});
// Source should have taken thorns damage (reduced by defense)
if (expectedThornsDamage > 0) {
expect(sourceUnit.currentHealth).to.be.lessThan(initialSourceHP);
expect(sourceUnit.currentHealth).to.equal(initialSourceHP - expectedThornsDamage);
} else {
// If defense blocks all damage, health should be unchanged
expect(sourceUnit.currentHealth).to.equal(initialSourceHP);
}
// Restore original get method
itemRegistry.get = originalGet;
});
it("should process ON_DAMAGE_DEALT trigger when unit deals damage", () => {
// Create a mock item with ON_DAMAGE_DEALT passive (e.g., lifesteal)
const mockItem = {
id: "ITEM_LIFESTEAL",
name: "Lifesteal Blade",
type: "WEAPON",
passives: [
{
trigger: "ON_DAMAGE_DEALT",
action: "HEAL",
params: {
power: 2,
},
target: "SELF",
},
],
};
const originalGet = itemRegistry.get;
itemRegistry.get = (id) => {
if (id === "ITEM_LIFESTEAL") return mockItem;
return originalGet.call(itemRegistry, id);
};
sourceUnit.loadout = {
mainHand: { defId: "ITEM_LIFESTEAL", uid: "test-lifesteal" },
offHand: null,
body: null,
accessory: null,
belt: [null, null],
};
const gameLoop = {
effectProcessor,
inventoryManager: {
itemRegistry,
},
processPassiveItemEffects: function (unit, trigger, context) {
if (!unit || !unit.loadout || !this.effectProcessor || !this.inventoryManager) {
return;
}
const equippedItems = [
unit.loadout.mainHand,
unit.loadout.offHand,
unit.loadout.body,
unit.loadout.accessory,
...(unit.loadout.belt || []),
].filter(Boolean);
for (const itemInstance of equippedItems) {
if (!itemInstance || !itemInstance.defId) continue;
const itemDef = this.inventoryManager.itemRegistry.get(itemInstance.defId);
if (!itemDef || !itemDef.passives) continue;
for (const passive of itemDef.passives) {
if (passive.trigger !== trigger) continue;
const effectDef = {
type: passive.action,
power: passive.params.power,
};
let target = context.target || context.source || unit;
if (passive.params.target === "SELF" || passive.target === "SELF") {
target = unit;
}
this.effectProcessor.process(effectDef, unit, target);
}
}
},
};
const initialSourceHP = sourceUnit.currentHealth;
sourceUnit.currentHealth = 80; // Set to less than max
// Deal damage (this should trigger ON_DAMAGE_DEALT)
const damageEffect = {
type: "DAMAGE",
power: 10,
};
effectProcessor.process(damageEffect, sourceUnit, targetUnit);
// Process passive effects
gameLoop.processPassiveItemEffects(sourceUnit, "ON_DAMAGE_DEALT", {
target: targetUnit,
damageAmount: 10,
});
// Source should have healed from lifesteal
expect(sourceUnit.currentHealth).to.be.greaterThan(80);
itemRegistry.get = originalGet;
});
it("should not process passive effects if trigger doesn't match", () => {
const mockItem = {
id: "ITEM_OTHER",
name: "Other Item",
type: "ARMOR",
passive_effects: [
{
trigger: "ON_HEAL_DEALT", // Different trigger
action: "HEAL",
params: { power: 5 },
},
],
};
const originalGet = itemRegistry.get;
itemRegistry.get = (id) => {
if (id === "ITEM_OTHER") return mockItem;
return originalGet.call(itemRegistry, id);
};
targetUnit.loadout = {
mainHand: null,
offHand: null,
body: { defId: "ITEM_OTHER", uid: "test-other" },
accessory: null,
belt: [null, null],
};
const gameLoop = {
effectProcessor,
inventoryManager: {
itemRegistry,
},
processPassiveItemEffects: function (unit, trigger, context) {
if (!unit || !unit.loadout || !this.effectProcessor || !this.inventoryManager) {
return;
}
const equippedItems = [
unit.loadout.mainHand,
unit.loadout.offHand,
unit.loadout.body,
unit.loadout.accessory,
...(unit.loadout.belt || []),
].filter(Boolean);
for (const itemInstance of equippedItems) {
if (!itemInstance || !itemInstance.defId) continue;
const itemDef = this.inventoryManager.itemRegistry.get(itemInstance.defId);
if (!itemDef || !itemDef.passives) continue;
for (const passive of itemDef.passives) {
if (passive.trigger !== trigger) continue;
// Should not reach here for ON_DAMAGED trigger
expect.fail("Should not process passive with different trigger");
}
}
},
};
// Process ON_DAMAGED trigger (but item has ON_HEAL_DEALT)
gameLoop.processPassiveItemEffects(targetUnit, "ON_DAMAGED", {
source: sourceUnit,
damageAmount: 10,
});
// Should not have processed anything
itemRegistry.get = originalGet;
});
});
});