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