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