2025-12-19 16:38:22 +00:00
|
|
|
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");
|
|
|
|
|
});
|
2025-12-28 00:54:03 +00:00
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-12-19 16:38:22 +00:00
|
|
|
});
|