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.
358 lines
11 KiB
JavaScript
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;
|
|
});
|
|
});
|
|
});
|
|
|