diff --git a/build.js b/build.js
index c6072a8..1ad5fbc 100644
--- a/build.js
+++ b/build.js
@@ -11,10 +11,11 @@ mkdirSync("dist", { recursive: true });
// Build JavaScript
await build({
- entryPoints: ["src/game-viewport.js"],
+ entryPoints: ["src/index.js"],
bundle: true,
+ splitting: true,
format: "esm",
- outfile: "dist/game-viewport.js",
+ outdir: "dist",
sourcemap: true,
platform: "browser",
});
diff --git a/src/assets/data/classes/aether_sentinel.json b/src/assets/data/classes/aether_sentinel.json
new file mode 100644
index 0000000..eaa5e9f
--- /dev/null
+++ b/src/assets/data/classes/aether_sentinel.json
@@ -0,0 +1,38 @@
+{
+ "id": "CLASS_AETHER_SENTINEL",
+ "name": "Aether Sentinel",
+ "tier": 2,
+ "unlock_requirements": { "CLASS_CUSTODIAN": 5, "CLASS_VANGUARD": 3 },
+ "base_stats": {
+ "health": 130,
+ "attack": 8,
+ "defense": 9,
+ "magic": 6,
+ "speed": 5,
+ "willpower": 14,
+ "movement": 3
+ },
+ "growth_rates": {
+ "health": 11,
+ "defense": 1,
+ "willpower": 2
+ },
+ "starting_equipment": ["ITEM_GREATSHIELD", "ITEM_SANCTIFIED_MACE"],
+ "skillTreeData": {
+ "primary_stat": "willpower",
+ "secondary_stat": "defense",
+ "active_skills": [
+ "SKILL_GUARDIAN_LINK",
+ "SKILL_HOLY_NOVA",
+ "SKILL_VOW_SILENCE",
+ "SKILL_AETHER_CLEANSE",
+ "SKILL_DIVINE_INTERVENTION"
+ ],
+ "passive_skills": [
+ "PASSIVE_THORN_HEAL",
+ "PASSIVE_MARTYRDOM",
+ "PASSIVE_SACRED_GROUND",
+ "PASSIVE_UNSHAKEABLE"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/aether_weaver.json b/src/assets/data/classes/aether_weaver.json
new file mode 100644
index 0000000..3a852a7
--- /dev/null
+++ b/src/assets/data/classes/aether_weaver.json
@@ -0,0 +1,38 @@
+{
+ "id": "CLASS_WEAVER",
+ "name": "Aether Weaver",
+ "tier": 1,
+ "unlock_requirements": null,
+ "base_stats": {
+ "health": 80,
+ "attack": 5,
+ "defense": 4,
+ "magic": 12,
+ "speed": 10,
+ "willpower": 8,
+ "movement": 3
+ },
+ "growth_rates": {
+ "health": 6,
+ "magic": 2,
+ "speed": 1
+ },
+ "starting_equipment": ["ITEM_APPRENTICE_WAND", "ITEM_ROBES"],
+ "skillTreeData": {
+ "primary_stat": "magic",
+ "secondary_stat": "speed",
+ "active_skills": [
+ "SKILL_FIREBALL",
+ "SKILL_ICE_WALL",
+ "SKILL_TELEPORT",
+ "SKILL_CHAIN_LIGHTNING",
+ "SKILL_METEOR"
+ ],
+ "passive_skills": [
+ "PASSIVE_GLASS_CANNON",
+ "PASSIVE_MANA_SYPHON",
+ "PASSIVE_ELEMENTAL_AFFINITY",
+ "PASSIVE_DOUBLE_CAST"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/arcane_scourge.json b/src/assets/data/classes/arcane_scourge.json
new file mode 100644
index 0000000..5a0aa5c
--- /dev/null
+++ b/src/assets/data/classes/arcane_scourge.json
@@ -0,0 +1,38 @@
+{
+ "id": "CLASS_ARCANE_SCOURGE",
+ "name": "Arcane Scourge",
+ "tier": 2,
+ "unlock_requirements": { "CLASS_WEAVER": 5, "CLASS_SCAVENGER": 3 },
+ "base_stats": {
+ "health": 70,
+ "attack": 6,
+ "defense": 3,
+ "magic": 15,
+ "speed": 12,
+ "willpower": 6,
+ "movement": 4
+ },
+ "growth_rates": {
+ "health": 5,
+ "magic": 3,
+ "speed": 1
+ },
+ "starting_equipment": ["ITEM_VOID_STAFF", "ITEM_TATTERED_ROBES"],
+ "skillTreeData": {
+ "primary_stat": "magic",
+ "secondary_stat": "speed",
+ "active_skills": [
+ "SKILL_LIFE_TAP",
+ "SKILL_VOID_RAY",
+ "SKILL_SHARD_BLAST",
+ "SKILL_CHAOS_BOLT",
+ "SKILL_VOID_SINGULARITY"
+ ],
+ "passive_skills": [
+ "PASSIVE_RISK_TAKER",
+ "PASSIVE_SHARD_MAGNET",
+ "PASSIVE_OVERCHARGE",
+ "PASSIVE_DESPERATE_POWER"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/battle_mage.json b/src/assets/data/classes/battle_mage.json
new file mode 100644
index 0000000..7a3611a
--- /dev/null
+++ b/src/assets/data/classes/battle_mage.json
@@ -0,0 +1,39 @@
+{
+ "id": "CLASS_BATTLE_MAGE",
+ "name": "Battle Mage",
+ "tier": 2,
+ "unlock_requirements": { "CLASS_VANGUARD": 5, "CLASS_WEAVER": 3 },
+ "base_stats": {
+ "health": 110,
+ "attack": 10,
+ "defense": 7,
+ "magic": 10,
+ "speed": 9,
+ "willpower": 7,
+ "movement": 3
+ },
+ "growth_rates": {
+ "health": 9,
+ "attack": 1,
+ "magic": 1,
+ "defense": 1
+ },
+ "starting_equipment": ["ITEM_RUNE_BLADE", "ITEM_SPELL_SHIELD"],
+ "skillTreeData": {
+ "primary_stat": "attack",
+ "secondary_stat": "magic",
+ "active_skills": [
+ "SKILL_FLAME_STRIKE",
+ "SKILL_AEGIS",
+ "SKILL_WARP_STRIKE",
+ "SKILL_ARCANE_SLASH",
+ "SKILL_RUNE_STORM"
+ ],
+ "passive_skills": [
+ "PASSIVE_BATTLE_RHYTHM",
+ "PASSIVE_ARCANE_ARMOR",
+ "PASSIVE_SPELL_PARRY",
+ "PASSIVE_CONJURED_WEAPON"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/custodian.json b/src/assets/data/classes/custodian.json
new file mode 100644
index 0000000..49a127b
--- /dev/null
+++ b/src/assets/data/classes/custodian.json
@@ -0,0 +1,38 @@
+{
+ "id": "CLASS_CUSTODIAN",
+ "name": "Custodian",
+ "tier": 1,
+ "unlock_requirements": null,
+ "base_stats": {
+ "health": 95,
+ "attack": 6,
+ "defense": 5,
+ "magic": 8,
+ "speed": 6,
+ "willpower": 12,
+ "movement": 3
+ },
+ "growth_rates": {
+ "health": 8,
+ "willpower": 2,
+ "magic": 1
+ },
+ "starting_equipment": ["ITEM_STAFF", "ITEM_LEAF_ROBES"],
+ "skillTreeData": {
+ "primary_stat": "willpower",
+ "secondary_stat": "health",
+ "active_skills": [
+ "SKILL_MEND",
+ "SKILL_PURIFY",
+ "SKILL_SANCTUARY",
+ "SKILL_HASTE",
+ "SKILL_RESURRECT"
+ ],
+ "passive_skills": [
+ "PASSIVE_AURA_OF_PEACE",
+ "PASSIVE_FEEDBACK_LOOP",
+ "PASSIVE_OVERHEAL",
+ "PASSIVE_PACIFIST"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/field_engineer.json b/src/assets/data/classes/field_engineer.json
new file mode 100644
index 0000000..3a876f4
--- /dev/null
+++ b/src/assets/data/classes/field_engineer.json
@@ -0,0 +1,43 @@
+{
+ "id": "CLASS_FIELD_ENGINEER",
+ "name": "Field Engineer",
+ "tier": 2,
+ "unlock_requirements": { "CLASS_TINKER": 5, "CLASS_SCAVENGER": 3 },
+ "base_stats": {
+ "health": 105,
+ "attack": 9,
+ "defense": 6,
+ "magic": 0,
+ "speed": 10,
+ "willpower": 6,
+ "movement": 4,
+ "tech": 12
+ },
+ "growth_rates": {
+ "health": 8,
+ "speed": 1,
+ "tech": 2
+ },
+ "starting_equipment": [
+ "ITEM_AUTO_RIFLE",
+ "ITEM_REINFORCED_VEST",
+ "ITEM_REPAIR_DRONE"
+ ],
+ "skillTreeData": {
+ "primary_stat": "tech",
+ "secondary_stat": "speed",
+ "active_skills": [
+ "SKILL_SCRAP_CANNON",
+ "SKILL_PORTABLE_COVER",
+ "SKILL_REMOTE_OP",
+ "SKILL_EMP_BLAST",
+ "SKILL_ORBITAL_DROP"
+ ],
+ "passive_skills": [
+ "PASSIVE_COMBAT_SALVAGE",
+ "PASSIVE_FIELD_REFIT",
+ "PASSIVE_FAST_HANDS",
+ "PASSIVE_IMPROVISED_EXPLOSIVES"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/sapper.json b/src/assets/data/classes/sapper.json
new file mode 100644
index 0000000..bf09076
--- /dev/null
+++ b/src/assets/data/classes/sapper.json
@@ -0,0 +1,39 @@
+{
+ "id": "CLASS_SAPPER",
+ "name": "Sapper",
+ "tier": 2,
+ "unlock_requirements": { "CLASS_VANGUARD": 5, "CLASS_TINKER": 3 },
+ "base_stats": {
+ "health": 115,
+ "attack": 13,
+ "defense": 7,
+ "magic": 0,
+ "speed": 9,
+ "willpower": 5,
+ "movement": 4,
+ "tech": 8
+ },
+ "growth_rates": {
+ "health": 9,
+ "attack": 2,
+ "speed": 1
+ },
+ "starting_equipment": ["ITEM_EXPLOSIVE_HAMMER", "ITEM_BLAST_ARMOR"],
+ "skillTreeData": {
+ "primary_stat": "attack",
+ "secondary_stat": "tech",
+ "active_skills": [
+ "SKILL_BREACH_CHARGE",
+ "SKILL_ROCKET_JUMP",
+ "SKILL_TUNNEL_VISION",
+ "SKILL_CLUSTER_BOMB",
+ "SKILL_BIG_RED_BUTTON"
+ ],
+ "passive_skills": [
+ "PASSIVE_DEMOLITIONIST",
+ "PASSIVE_ANTI_COVER",
+ "PASSIVE_BLAST_SHIELD",
+ "PASSIVE_RECHARGE_BOMB"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/scavenger.json b/src/assets/data/classes/scavenger.json
new file mode 100644
index 0000000..9468ed4
--- /dev/null
+++ b/src/assets/data/classes/scavenger.json
@@ -0,0 +1,42 @@
+{
+ "id": "CLASS_SCAVENGER",
+ "name": "Scavenger",
+ "tier": 1,
+ "unlock_requirements": null,
+ "base_stats": {
+ "health": 90,
+ "attack": 8,
+ "defense": 5,
+ "magic": 2,
+ "speed": 12,
+ "willpower": 4,
+ "movement": 5
+ },
+ "growth_rates": {
+ "health": 7,
+ "speed": 2,
+ "attack": 1
+ },
+ "starting_equipment": [
+ "ITEM_DAGGER",
+ "ITEM_PADDED_VEST",
+ "ITEM_LOCKPICK_SET"
+ ],
+ "skillTreeData": {
+ "primary_stat": "speed",
+ "secondary_stat": "movement",
+ "active_skills": [
+ "SKILL_FLASHBANG",
+ "SKILL_GRAPPLE_HOOK",
+ "SKILL_STEALTH",
+ "SKILL_COIN_TOSS",
+ "SKILL_ASSASSINATE"
+ ],
+ "passive_skills": [
+ "PASSIVE_LUCKY_FIND",
+ "PASSIVE_BACKSTAB",
+ "PASSIVE_LIGHT_STEP",
+ "PASSIVE_HAGGLER"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/tinker.json b/src/assets/data/classes/tinker.json
new file mode 100644
index 0000000..7927c94
--- /dev/null
+++ b/src/assets/data/classes/tinker.json
@@ -0,0 +1,43 @@
+{
+ "id": "CLASS_TINKER",
+ "name": "Tinker",
+ "tier": 1,
+ "unlock_requirements": null,
+ "base_stats": {
+ "health": 100,
+ "attack": 10,
+ "defense": 6,
+ "magic": 0,
+ "speed": 7,
+ "willpower": 5,
+ "movement": 3,
+ "tech": 10
+ },
+ "growth_rates": {
+ "health": 8,
+ "attack": 1,
+ "tech": 2
+ },
+ "starting_equipment": [
+ "ITEM_WRENCH",
+ "ITEM_LEATHER_APRON",
+ "ITEM_TURRET_KIT"
+ ],
+ "skillTreeData": {
+ "primary_stat": "tech",
+ "secondary_stat": "defense",
+ "active_skills": [
+ "SKILL_DEPLOY_TURRET",
+ "SKILL_REPAIR_BOT",
+ "SKILL_OVERCLOCK",
+ "SKILL_SHOCK_GRENADE",
+ "SKILL_MECH_SUIT"
+ ],
+ "passive_skills": [
+ "PASSIVE_SCRAP_SHIELD",
+ "PASSIVE_EFFICIENT_BUILD",
+ "PASSIVE_RECYCLE",
+ "PASSIVE_CONDUCTIVE"
+ ]
+ }
+}
diff --git a/src/assets/data/classes/vanguard.json b/src/assets/data/classes/vanguard.json
new file mode 100644
index 0000000..36186cc
--- /dev/null
+++ b/src/assets/data/classes/vanguard.json
@@ -0,0 +1,38 @@
+{
+ "id": "CLASS_VANGUARD",
+ "name": "Vanguard",
+ "tier": 1,
+ "unlock_requirements": null,
+ "base_stats": {
+ "health": 120,
+ "attack": 12,
+ "defense": 8,
+ "magic": 0,
+ "speed": 8,
+ "willpower": 5,
+ "movement": 3
+ },
+ "growth_rates": {
+ "health": 10,
+ "attack": 1,
+ "defense": 1
+ },
+ "starting_equipment": ["ITEM_RUSTY_BLADE", "ITEM_SCRAP_PLATE"],
+ "skillTreeData": {
+ "primary_stat": "health",
+ "secondary_stat": "defense",
+ "active_skills": [
+ "SKILL_SHIELD_BASH",
+ "SKILL_TAUNT",
+ "SKILL_INTERCEPT",
+ "SKILL_EXECUTE",
+ "SKILL_AVATAR_OF_IRON"
+ ],
+ "passive_skills": [
+ "PASSIVE_IRON_SKIN",
+ "PASSIVE_THORNS",
+ "PASSIVE_UNYIELDING",
+ "PASSIVE_TITANS_GRIP"
+ ]
+ }
+}
diff --git a/src/factories/SkillTreeFactory.js b/src/factories/SkillTreeFactory.js
new file mode 100644
index 0000000..80d2beb
--- /dev/null
+++ b/src/factories/SkillTreeFactory.js
@@ -0,0 +1,103 @@
+/**
+ * SkillTreeFactory.js
+ * Generates class-specific skill trees by merging a Master Topology with Class Configuration.
+ */
+export class SkillTreeFactory {
+ /**
+ * @param {Object} templateRegistry - Map of Template IDs to JSON structures.
+ * @param {Object} skillRegistry - Map of Skill IDs to Skill Definitions.
+ */
+ constructor(templateRegistry, skillRegistry) {
+ this.templates = templateRegistry;
+ this.skills = skillRegistry;
+ }
+
+ /**
+ * Creates a fully hydrated Skill Tree for a specific class.
+ * @param {Object} classConfig - The Class definition containing 'skillTreeData'.
+ * @param {string} templateId - The ID of the topology to use (default: 'TEMPLATE_STANDARD_30').
+ */
+ createTree(classConfig, templateId = "TEMPLATE_STANDARD_30") {
+ const template = this.templates[templateId];
+ if (!template) throw new Error(`Template not found: ${templateId}`);
+
+ const config = classConfig.skillTreeData;
+ if (!config)
+ throw new Error(`Class ${classConfig.id} missing skillTreeData`);
+
+ const newTree = {
+ id: `TREE_${classConfig.id}`,
+ nodes: {},
+ };
+
+ // Iterate and Inject
+ for (const [nodeId, templateNode] of Object.entries(template.nodes)) {
+ // Clone the node structure to avoid mutating the template
+ let realNode = JSON.parse(JSON.stringify(templateNode));
+
+ // Hydrate based on Slot Type
+ this.hydrateNode(realNode, config, templateNode.tier);
+
+ newTree.nodes[nodeId] = realNode;
+ }
+
+ return newTree;
+ }
+
+ hydrateNode(node, config, tier) {
+ // Scaling Logic for Stats
+ const statValue = this.getTierStatValue(tier);
+
+ switch (node.type) {
+ case "SLOT_STAT_PRIMARY":
+ node.type = "STAT_BOOST";
+ node.data = {
+ stat: config.primary_stat,
+ value: statValue * 2, // Primary gets double value
+ };
+ break;
+
+ case "SLOT_STAT_SECONDARY":
+ node.type = "STAT_BOOST";
+ node.data = {
+ stat: config.secondary_stat,
+ value: statValue,
+ };
+ break;
+
+ case "SLOT_SKILL_ACTIVE_1":
+ node.type = "ACTIVE_SKILL";
+ // Map tier/slot to specific index in the config array
+ // Example: Slot 1 is the 0th skill
+ node.data = this.getSkillData(config.active_skills[0]);
+ break;
+
+ case "SLOT_SKILL_ACTIVE_2":
+ node.type = "ACTIVE_SKILL";
+ node.data = this.getSkillData(config.active_skills[1]);
+ break;
+
+ case "SLOT_SKILL_PASSIVE_1":
+ node.type = "PASSIVE_ABILITY";
+ node.data = { effect_id: config.passive_skills[0] };
+ break;
+
+ // ... Add cases for other slots (ULTIMATE, etc)
+
+ default:
+ // If it's already a concrete type (e.g. fixed layout), leave it alone
+ break;
+ }
+ }
+
+ getSkillData(skillId) {
+ const skill = this.skills[skillId];
+ if (!skill) return { id: skillId, name: "Unknown Skill" }; // Fallback
+ return skill;
+ }
+
+ getTierStatValue(tier) {
+ // Scaling logic: Tier 1 = 1, Tier 5 = 5
+ return tier;
+ }
+}
diff --git a/src/index.html b/src/index.html
index 928f84b..dd70910 100644
--- a/src/index.html
+++ b/src/index.html
@@ -12,7 +12,7 @@
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;700&family=Cinzel:wght@700&display=swap"
rel="stylesheet"
/>
-
+