aether-shards/test/factories/SkillTreeFactory.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

745 lines
23 KiB
JavaScript

import { expect } from "@esm-bundle/chai";
import { SkillTreeFactory } from "../../src/factories/SkillTreeFactory.js";
describe("System: Skill Tree Factory", () => {
let factory;
let mockTemplates;
let mockSkills;
let mockClassConfig;
beforeEach(() => {
// 1. Setup Mock Data
mockTemplates = {
TEMPLATE_STANDARD_30: {
nodes: {
ROOT_NODE: {
tier: 1,
type: "SLOT_STAT_PRIMARY",
children: ["CHILD_NODE"],
},
CHILD_NODE: { tier: 1, type: "SLOT_SKILL_ACTIVE_1", children: [] },
},
},
};
mockSkills = {
SKILL_FIREBALL: { id: "SKILL_FIREBALL", name: "Fireball", damage: 10 },
};
mockClassConfig = {
id: "TEST_CLASS",
skillTreeData: {
primary_stat: "attack",
secondary_stat: "defense",
active_skills: ["SKILL_FIREBALL"],
passive_skills: ["PASSIVE_BURNING"],
},
};
// 2. Initialize Factory
factory = new SkillTreeFactory(mockTemplates, mockSkills);
});
it("CoA 1: Should maintain the topology (structure) of the template", () => {
const tree = factory.createTree(mockClassConfig);
expect(tree.nodes).to.have.property("ROOT_NODE");
expect(tree.nodes).to.have.property("CHILD_NODE");
expect(tree.nodes["ROOT_NODE"].children).to.include("CHILD_NODE");
});
it("CoA 2: Should inject Primary Stats based on Class Config", () => {
const tree = factory.createTree(mockClassConfig);
const rootNode = tree.nodes["ROOT_NODE"];
expect(rootNode.type).to.equal("STAT_BOOST");
expect(rootNode.data.stat).to.equal("attack"); // Injected from config
expect(rootNode.data.value).to.equal(2); // Tier 1 * 2
});
it("CoA 3: Should inject Active Skills based on Class Config", () => {
const tree = factory.createTree(mockClassConfig);
const childNode = tree.nodes["CHILD_NODE"];
expect(childNode.type).to.equal("ACTIVE_SKILL");
// Should resolve the full skill object from registry
expect(childNode.data.name).to.equal("Fireball");
});
describe("30-Node Template Support", () => {
let fullTemplate;
let fullClassConfig;
let fullSkills;
beforeEach(() => {
// Create a mock 30-node template
fullTemplate = {
TEMPLATE_STANDARD_30: {
nodes: {
NODE_T1_1: {
tier: 1,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T2_1", "NODE_T2_2"],
req: 1,
cost: 1,
},
NODE_T2_1: {
tier: 2,
type: "SLOT_STAT_SECONDARY",
children: ["NODE_T3_1"],
req: 2,
cost: 1,
},
NODE_T2_2: {
tier: 2,
type: "SLOT_SKILL_ACTIVE_1",
children: ["NODE_T3_2"],
req: 2,
cost: 1,
},
NODE_T3_1: {
tier: 3,
type: "SLOT_STAT_PRIMARY",
children: [],
req: 3,
cost: 1,
},
NODE_T3_2: {
tier: 3,
type: "SLOT_SKILL_ACTIVE_2",
children: [],
req: 3,
cost: 1,
},
},
},
};
fullClassConfig = {
id: "CLASS_VANGUARD",
skillTreeData: {
primary_stat: "health",
secondary_stat: "defense",
active_skills: ["SKILL_SHIELD_BASH", "SKILL_TAUNT"],
passive_skills: ["PASSIVE_IRON_SKIN", "PASSIVE_THORNS"],
},
};
fullSkills = {
SKILL_SHIELD_BASH: { id: "SKILL_SHIELD_BASH", name: "Shield Bash" },
SKILL_TAUNT: { id: "SKILL_TAUNT", name: "Taunt" },
};
});
it("should generate tree with all nodes from template", () => {
const fullFactory = new SkillTreeFactory(fullTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// Should have all 5 nodes from template
expect(Object.keys(tree.nodes)).to.have.length(5);
expect(tree.nodes).to.have.property("NODE_T1_1");
expect(tree.nodes).to.have.property("NODE_T2_1");
expect(tree.nodes).to.have.property("NODE_T2_2");
expect(tree.nodes).to.have.property("NODE_T3_1");
expect(tree.nodes).to.have.property("NODE_T3_2");
});
it("should hydrate all stat boost nodes correctly", () => {
const fullFactory = new SkillTreeFactory(fullTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// Tier 1 primary stat (health) - should be tier * 2 = 2
const t1Node = tree.nodes["NODE_T1_1"];
expect(t1Node.type).to.equal("STAT_BOOST");
expect(t1Node.data.stat).to.equal("health");
expect(t1Node.data.value).to.equal(2); // Tier 1 * 2
// Tier 2 secondary stat (defense) - should be tier = 2
const t2Node = tree.nodes["NODE_T2_1"];
expect(t2Node.type).to.equal("STAT_BOOST");
expect(t2Node.data.stat).to.equal("defense");
expect(t2Node.data.value).to.equal(2); // Tier 2
// Tier 3 primary stat (health) - should be tier * 2 = 6
const t3Node = tree.nodes["NODE_T3_1"];
expect(t3Node.type).to.equal("STAT_BOOST");
expect(t3Node.data.stat).to.equal("health");
expect(t3Node.data.value).to.equal(6); // Tier 3 * 2
});
it("should hydrate all active skill nodes correctly", () => {
const fullFactory = new SkillTreeFactory(fullTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// ACTIVE_1 should map to first skill
const active1Node = tree.nodes["NODE_T2_2"];
expect(active1Node.type).to.equal("ACTIVE_SKILL");
expect(active1Node.data.name).to.equal("Shield Bash");
// ACTIVE_2 should map to second skill
const active2Node = tree.nodes["NODE_T3_2"];
expect(active2Node.type).to.equal("ACTIVE_SKILL");
expect(active2Node.data.name).to.equal("Taunt");
});
it("should handle missing skills gracefully", () => {
const classConfigMissingSkills = {
id: "TEST_CLASS",
skillTreeData: {
primary_stat: "attack",
secondary_stat: "defense",
active_skills: ["SKILL_EXISTS"], // Only one skill
passive_skills: [],
},
};
const skills = {
SKILL_EXISTS: { id: "SKILL_EXISTS", name: "Existing Skill" },
};
const fullFactory = new SkillTreeFactory(fullTemplate, skills);
const tree = fullFactory.createTree(classConfigMissingSkills);
// ACTIVE_1 should work
const active1Node = tree.nodes["NODE_T2_2"];
expect(active1Node.data.name).to.equal("Existing Skill");
// ACTIVE_2 should fallback to "Unknown Skill"
const active2Node = tree.nodes["NODE_T3_2"];
expect(active2Node.data.name).to.equal("Unknown Skill");
});
});
describe("Extended Skill Slots", () => {
let extendedTemplate;
let extendedClassConfig;
let extendedSkills;
beforeEach(() => {
extendedTemplate = {
TEMPLATE_STANDARD_30: {
nodes: {
ACTIVE_1: {
tier: 2,
type: "SLOT_SKILL_ACTIVE_1",
children: [],
req: 2,
cost: 1,
},
ACTIVE_2: {
tier: 2,
type: "SLOT_SKILL_ACTIVE_2",
children: [],
req: 2,
cost: 1,
},
ACTIVE_3: {
tier: 3,
type: "SLOT_SKILL_ACTIVE_3",
children: [],
req: 3,
cost: 1,
},
ACTIVE_4: {
tier: 3,
type: "SLOT_SKILL_ACTIVE_4",
children: [],
req: 3,
cost: 1,
},
PASSIVE_1: {
tier: 2,
type: "SLOT_SKILL_PASSIVE_1",
children: [],
req: 2,
cost: 2,
},
PASSIVE_2: {
tier: 3,
type: "SLOT_SKILL_PASSIVE_2",
children: [],
req: 3,
cost: 2,
},
PASSIVE_3: {
tier: 4,
type: "SLOT_SKILL_PASSIVE_3",
children: [],
req: 4,
cost: 2,
},
PASSIVE_4: {
tier: 4,
type: "SLOT_SKILL_PASSIVE_4",
children: [],
req: 4,
cost: 2,
},
},
},
};
extendedClassConfig = {
id: "TEST_CLASS",
skillTreeData: {
primary_stat: "attack",
secondary_stat: "defense",
active_skills: [
"SKILL_1",
"SKILL_2",
"SKILL_3",
"SKILL_4",
],
passive_skills: [
"PASSIVE_1",
"PASSIVE_2",
"PASSIVE_3",
"PASSIVE_4",
],
},
};
extendedSkills = {
SKILL_1: { id: "SKILL_1", name: "Skill 1" },
SKILL_2: { id: "SKILL_2", name: "Skill 2" },
SKILL_3: { id: "SKILL_3", name: "Skill 3" },
SKILL_4: { id: "SKILL_4", name: "Skill 4" },
};
});
it("should hydrate ACTIVE_3 and ACTIVE_4 slots", () => {
const extendedFactory = new SkillTreeFactory(
extendedTemplate,
extendedSkills
);
const tree = extendedFactory.createTree(extendedClassConfig);
const active3Node = tree.nodes["ACTIVE_3"];
expect(active3Node.type).to.equal("ACTIVE_SKILL");
expect(active3Node.data.name).to.equal("Skill 3");
const active4Node = tree.nodes["ACTIVE_4"];
expect(active4Node.type).to.equal("ACTIVE_SKILL");
expect(active4Node.data.name).to.equal("Skill 4");
});
it("should hydrate PASSIVE_2, PASSIVE_3, and PASSIVE_4 slots", () => {
const extendedFactory = new SkillTreeFactory(
extendedTemplate,
extendedSkills
);
const tree = extendedFactory.createTree(extendedClassConfig);
const passive2Node = tree.nodes["PASSIVE_2"];
expect(passive2Node.type).to.equal("PASSIVE_ABILITY");
expect(passive2Node.data.name).to.equal("PASSIVE_2");
expect(passive2Node.data.effect_id).to.equal("PASSIVE_2");
const passive3Node = tree.nodes["PASSIVE_3"];
expect(passive3Node.type).to.equal("PASSIVE_ABILITY");
expect(passive3Node.data.name).to.equal("PASSIVE_3");
const passive4Node = tree.nodes["PASSIVE_4"];
expect(passive4Node.type).to.equal("PASSIVE_ABILITY");
expect(passive4Node.data.name).to.equal("PASSIVE_4");
});
it("should handle missing extended skills with fallbacks", () => {
const limitedClassConfig = {
id: "TEST_CLASS",
skillTreeData: {
primary_stat: "attack",
secondary_stat: "defense",
active_skills: ["SKILL_1"], // Only one skill
passive_skills: ["PASSIVE_1"], // Only one passive
},
};
const limitedFactory = new SkillTreeFactory(
extendedTemplate,
extendedSkills
);
const tree = limitedFactory.createTree(limitedClassConfig);
// ACTIVE_3 should fallback
const active3Node = tree.nodes["ACTIVE_3"];
expect(active3Node.data.name).to.equal("Unknown Skill");
// PASSIVE_3 should fallback
const passive3Node = tree.nodes["PASSIVE_3"];
expect(passive3Node.data.name).to.equal("Unknown Passive");
});
});
describe("Full 30-Node Template Generation", () => {
let full30NodeTemplate;
let fullClassConfig;
let fullSkills;
beforeEach(() => {
// Create a simplified but representative 30-node template structure
// This mirrors the actual template_standard_30.json structure
full30NodeTemplate = {
TEMPLATE_STANDARD_30: {
nodes: {
// Tier 1: 1 node
NODE_T1_1: {
tier: 1,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T2_1", "NODE_T2_2", "NODE_T2_3"],
req: 1,
cost: 1,
},
// Tier 2: 3 nodes
NODE_T2_1: {
tier: 2,
type: "SLOT_STAT_SECONDARY",
children: ["NODE_T3_1", "NODE_T3_2"],
req: 2,
cost: 1,
},
NODE_T2_2: {
tier: 2,
type: "SLOT_SKILL_ACTIVE_1",
children: ["NODE_T3_3", "NODE_T3_4"],
req: 2,
cost: 1,
},
NODE_T2_3: {
tier: 2,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T3_5", "NODE_T3_6"],
req: 2,
cost: 1,
},
// Tier 3: 6 nodes
NODE_T3_1: {
tier: 3,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T4_1", "NODE_T4_2"],
req: 3,
cost: 1,
},
NODE_T3_2: {
tier: 3,
type: "SLOT_STAT_SECONDARY",
children: ["NODE_T4_3"],
req: 3,
cost: 1,
},
NODE_T3_3: {
tier: 3,
type: "SLOT_SKILL_ACTIVE_2",
children: ["NODE_T4_4", "NODE_T4_5"],
req: 3,
cost: 1,
},
NODE_T3_4: {
tier: 3,
type: "SLOT_SKILL_PASSIVE_1",
children: ["NODE_T4_6"],
req: 3,
cost: 2,
},
NODE_T3_5: {
tier: 3,
type: "SLOT_STAT_SECONDARY",
children: ["NODE_T4_7"],
req: 3,
cost: 1,
},
NODE_T3_6: {
tier: 3,
type: "SLOT_SKILL_ACTIVE_1",
children: ["NODE_T4_8", "NODE_T4_9"],
req: 3,
cost: 1,
},
// Tier 4: 9 nodes
NODE_T4_1: {
tier: 4,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T5_1", "NODE_T5_2"],
req: 4,
cost: 2,
},
NODE_T4_2: {
tier: 4,
type: "SLOT_STAT_SECONDARY",
children: ["NODE_T5_3"],
req: 4,
cost: 2,
},
NODE_T4_3: {
tier: 4,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T5_4"],
req: 4,
cost: 2,
},
NODE_T4_4: {
tier: 4,
type: "SLOT_SKILL_ACTIVE_3",
children: ["NODE_T5_5", "NODE_T5_6"],
req: 4,
cost: 2,
},
NODE_T4_5: {
tier: 4,
type: "SLOT_SKILL_ACTIVE_4",
children: ["NODE_T5_7"],
req: 4,
cost: 2,
},
NODE_T4_6: {
tier: 4,
type: "SLOT_SKILL_PASSIVE_2",
children: ["NODE_T5_8"],
req: 4,
cost: 2,
},
NODE_T4_7: {
tier: 4,
type: "SLOT_STAT_PRIMARY",
children: ["NODE_T5_9"],
req: 4,
cost: 2,
},
NODE_T4_8: {
tier: 4,
type: "SLOT_SKILL_PASSIVE_3",
children: ["NODE_T5_10"],
req: 4,
cost: 2,
},
NODE_T4_9: {
tier: 4,
type: "SLOT_STAT_SECONDARY",
children: [],
req: 4,
cost: 2,
},
// Tier 5: 11 nodes (to make 30 total)
NODE_T5_1: {
tier: 5,
type: "SLOT_STAT_PRIMARY",
children: [],
req: 5,
cost: 3,
},
NODE_T5_2: {
tier: 5,
type: "SLOT_STAT_SECONDARY",
children: [],
req: 5,
cost: 3,
},
NODE_T5_3: {
tier: 5,
type: "SLOT_STAT_PRIMARY",
children: [],
req: 5,
cost: 3,
},
NODE_T5_4: {
tier: 5,
type: "SLOT_STAT_SECONDARY",
children: [],
req: 5,
cost: 3,
},
NODE_T5_5: {
tier: 5,
type: "SLOT_SKILL_ACTIVE_3",
children: [],
req: 5,
cost: 3,
},
NODE_T5_6: {
tier: 5,
type: "SLOT_SKILL_ACTIVE_4",
children: [],
req: 5,
cost: 3,
},
NODE_T5_7: {
tier: 5,
type: "SLOT_SKILL_PASSIVE_2",
children: [],
req: 5,
cost: 3,
},
NODE_T5_8: {
tier: 5,
type: "SLOT_SKILL_PASSIVE_4",
children: [],
req: 5,
cost: 3,
},
NODE_T5_9: {
tier: 5,
type: "SLOT_STAT_SECONDARY",
children: [],
req: 5,
cost: 3,
},
NODE_T5_10: {
tier: 5,
type: "SLOT_SKILL_ACTIVE_1",
children: [],
req: 5,
cost: 3,
},
NODE_T5_11: {
tier: 5,
type: "SLOT_STAT_PRIMARY",
children: [],
req: 5,
cost: 3,
},
},
},
};
fullClassConfig = {
id: "CLASS_VANGUARD",
skillTreeData: {
primary_stat: "health",
secondary_stat: "defense",
active_skills: [
"SKILL_SHIELD_BASH",
"SKILL_TAUNT",
"SKILL_CHARGE",
"SKILL_SHIELD_WALL",
],
passive_skills: [
"PASSIVE_IRON_SKIN",
"PASSIVE_THORNS",
"PASSIVE_REGEN",
"PASSIVE_FORTIFY",
],
},
};
fullSkills = {
SKILL_SHIELD_BASH: { id: "SKILL_SHIELD_BASH", name: "Shield Bash" },
SKILL_TAUNT: { id: "SKILL_TAUNT", name: "Taunt" },
SKILL_CHARGE: { id: "SKILL_CHARGE", name: "Charge" },
SKILL_SHIELD_WALL: { id: "SKILL_SHIELD_WALL", name: "Shield Wall" },
};
});
it("should generate exactly 30 nodes from template", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
expect(Object.keys(tree.nodes)).to.have.length(30);
});
it("should maintain all node relationships (children)", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// Verify root node has 3 children
expect(tree.nodes.NODE_T1_1.children).to.have.length(3);
expect(tree.nodes.NODE_T1_1.children).to.include("NODE_T2_1");
expect(tree.nodes.NODE_T1_1.children).to.include("NODE_T2_2");
expect(tree.nodes.NODE_T1_1.children).to.include("NODE_T2_3");
// Verify tier 2 nodes have children
expect(tree.nodes.NODE_T2_1.children).to.have.length(2);
expect(tree.nodes.NODE_T2_2.children).to.have.length(2);
// Verify tier 5 nodes have no children (leaf nodes)
expect(tree.nodes.NODE_T5_1.children).to.have.length(0);
expect(tree.nodes.NODE_T5_11.children).to.have.length(0);
});
it("should hydrate all stat boost nodes with correct values", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// Tier 1 primary stat: tier * 2 = 2
expect(tree.nodes.NODE_T1_1.type).to.equal("STAT_BOOST");
expect(tree.nodes.NODE_T1_1.data.stat).to.equal("health");
expect(tree.nodes.NODE_T1_1.data.value).to.equal(2);
// Tier 2 secondary stat: tier = 2
expect(tree.nodes.NODE_T2_1.type).to.equal("STAT_BOOST");
expect(tree.nodes.NODE_T2_1.data.stat).to.equal("defense");
expect(tree.nodes.NODE_T2_1.data.value).to.equal(2);
// Tier 5 primary stat: tier * 2 = 10
expect(tree.nodes.NODE_T5_1.type).to.equal("STAT_BOOST");
expect(tree.nodes.NODE_T5_1.data.stat).to.equal("health");
expect(tree.nodes.NODE_T5_1.data.value).to.equal(10);
});
it("should hydrate all active skill nodes correctly", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// ACTIVE_1 should map to first skill
expect(tree.nodes.NODE_T2_2.type).to.equal("ACTIVE_SKILL");
expect(tree.nodes.NODE_T2_2.data.name).to.equal("Shield Bash");
// ACTIVE_2 should map to second skill
expect(tree.nodes.NODE_T3_3.type).to.equal("ACTIVE_SKILL");
expect(tree.nodes.NODE_T3_3.data.name).to.equal("Taunt");
// ACTIVE_3 should map to third skill
expect(tree.nodes.NODE_T4_4.type).to.equal("ACTIVE_SKILL");
expect(tree.nodes.NODE_T4_4.data.name).to.equal("Charge");
// ACTIVE_4 should map to fourth skill
expect(tree.nodes.NODE_T4_5.type).to.equal("ACTIVE_SKILL");
expect(tree.nodes.NODE_T4_5.data.name).to.equal("Shield Wall");
});
it("should hydrate all passive skill nodes correctly", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// PASSIVE_1
expect(tree.nodes.NODE_T3_4.type).to.equal("PASSIVE_ABILITY");
expect(tree.nodes.NODE_T3_4.data.effect_id).to.equal("PASSIVE_IRON_SKIN");
expect(tree.nodes.NODE_T3_4.data.name).to.equal("PASSIVE_IRON_SKIN");
// PASSIVE_2
expect(tree.nodes.NODE_T4_6.type).to.equal("PASSIVE_ABILITY");
expect(tree.nodes.NODE_T4_6.data.effect_id).to.equal("PASSIVE_THORNS");
// PASSIVE_3
expect(tree.nodes.NODE_T4_8.type).to.equal("PASSIVE_ABILITY");
expect(tree.nodes.NODE_T4_8.data.effect_id).to.equal("PASSIVE_REGEN");
// PASSIVE_4 (if it exists in tier 5)
if (tree.nodes.NODE_T5_8) {
expect(tree.nodes.NODE_T5_8.type).to.equal("PASSIVE_ABILITY");
expect(tree.nodes.NODE_T5_8.data.effect_id).to.equal("PASSIVE_FORTIFY");
}
});
it("should preserve tier, req, and cost properties", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
// Check tier 1 node
expect(tree.nodes.NODE_T1_1.tier).to.equal(1);
expect(tree.nodes.NODE_T1_1.req).to.equal(1);
expect(tree.nodes.NODE_T1_1.cost).to.equal(1);
// Check tier 5 node
expect(tree.nodes.NODE_T5_1.tier).to.equal(5);
expect(tree.nodes.NODE_T5_1.req).to.equal(5);
expect(tree.nodes.NODE_T5_1.cost).to.equal(3);
});
it("should generate tree with correct ID format", () => {
const fullFactory = new SkillTreeFactory(full30NodeTemplate, fullSkills);
const tree = fullFactory.createTree(fullClassConfig);
expect(tree.id).to.equal("TREE_CLASS_VANGUARD");
});
});
});