aether-shards/test/factories/SkillTreeFactory.test.js

746 lines
23 KiB
JavaScript
Raw Permalink Normal View History

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