aether-shards/test/systems/PassiveItemEffects.test.js

359 lines
11 KiB
JavaScript
Raw Normal View History

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;
});
});
});