From 391abd6ea630d6f2159068855435708a9b907610 Mon Sep 17 00:00:00 2001 From: Matthew Mone Date: Fri, 19 Dec 2025 08:38:22 -0800 Subject: [PATCH] Refactor build process to use index.js as entry point, enabling module splitting and outputting to dist directory. Introduce new game logic in index.js, including team builder and game viewport components. Add JSON class definitions for new character classes and implement item and skill tree systems. Enhance unit tests for item and explorer functionalities. --- build.js | 5 +- src/assets/data/classes/aether_sentinel.json | 38 +++ src/assets/data/classes/aether_weaver.json | 38 +++ src/assets/data/classes/arcane_scourge.json | 38 +++ src/assets/data/classes/battle_mage.json | 39 +++ src/assets/data/classes/custodian.json | 38 +++ src/assets/data/classes/field_engineer.json | 43 +++ src/assets/data/classes/sapper.json | 39 +++ src/assets/data/classes/scavenger.json | 42 +++ src/assets/data/classes/tinker.json | 43 +++ src/assets/data/classes/vanguard.json | 38 +++ src/factories/SkillTreeFactory.js | 103 ++++++ src/index.html | 55 +--- src/index.js | 63 ++++ src/items/Item.js | 54 ++++ src/items/tier1_gear.json | 117 +++++++ src/{ => ui}/game-viewport.js | 10 +- src/ui/team-builder.js | 320 +++++++++++++++++++ src/units/Explorer.js | 114 +++++++ src/units/Unit.js | 60 ++++ test/factories/SkillTreeFactory.test.js | 68 ++++ test/items/Item.test.js | 73 +++++ test/units/Explorer.test.js | 73 +++++ test/units/Unit.test.js | 0 24 files changed, 1451 insertions(+), 60 deletions(-) create mode 100644 src/assets/data/classes/aether_sentinel.json create mode 100644 src/assets/data/classes/aether_weaver.json create mode 100644 src/assets/data/classes/arcane_scourge.json create mode 100644 src/assets/data/classes/battle_mage.json create mode 100644 src/assets/data/classes/custodian.json create mode 100644 src/assets/data/classes/field_engineer.json create mode 100644 src/assets/data/classes/sapper.json create mode 100644 src/assets/data/classes/scavenger.json create mode 100644 src/assets/data/classes/tinker.json create mode 100644 src/assets/data/classes/vanguard.json create mode 100644 src/factories/SkillTreeFactory.js create mode 100644 src/index.js create mode 100644 src/items/Item.js create mode 100644 src/items/tier1_gear.json rename src/{ => ui}/game-viewport.js (91%) create mode 100644 src/ui/team-builder.js create mode 100644 src/units/Explorer.js create mode 100644 src/units/Unit.js create mode 100644 test/factories/SkillTreeFactory.test.js create mode 100644 test/items/Item.test.js create mode 100644 test/units/Explorer.test.js create mode 100644 test/units/Unit.test.js 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" /> - +