import { expect } from "@esm-bundle/chai"; import { Explorer } from "../../src/units/Explorer.js"; // Mock Class Definitions const CLASS_VANGUARD = { id: "CLASS_VANGUARD", base_stats: { health: 100, attack: 10, speed: 5 }, growth_rates: { health: 10, attack: 1 }, }; const CLASS_TINKER = { id: "CLASS_TINKER", base_stats: { health: 80, attack: 8, speed: 7 }, growth_rates: { health: 5, attack: 2 }, }; describe("Unit: Explorer Class Logic", () => { it("CoA 1: Should initialize with base stats from definition", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); expect(hero.baseStats.health).to.equal(100); expect(hero.baseStats.attack).to.equal(10); expect(hero.classMastery["CLASS_VANGUARD"]).to.exist; expect(hero.classMastery["CLASS_VANGUARD"].level).to.equal(1); }); it("CoA 2: Should calculate stats based on Level Growth", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); // Manually level up to 3 hero.classMastery["CLASS_VANGUARD"].level = 3; hero.recalculateBaseStats(CLASS_VANGUARD); // Level 3 means 2 level-ups. // Health: 100 + (10 * 2) = 120 // Attack: 10 + (1 * 2) = 12 expect(hero.baseStats.health).to.equal(120); expect(hero.baseStats.attack).to.equal(12); }); it("CoA 3: changeClass should switch stats and persist old progress", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); // Level up Vanguard hero.classMastery["CLASS_VANGUARD"].level = 5; hero.recalculateBaseStats(CLASS_VANGUARD); expect(hero.baseStats.health).to.equal(140); // 100 + 40 // Switch to Tinker (New Job) hero.changeClass("CLASS_TINKER", CLASS_TINKER); // Should have Level 1 Tinker Stats expect(hero.activeClassId).to.equal("CLASS_TINKER"); expect(hero.baseStats.health).to.equal(80); // Base Tinker // Verify Vanguard history is saved expect(hero.classMastery["CLASS_VANGUARD"].level).to.equal(5); }); it("CoA 4: Switching BACK to old class should restore high stats", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.classMastery["CLASS_VANGUARD"].level = 5; // Switch Away hero.changeClass("CLASS_TINKER", CLASS_TINKER); // Switch Back hero.changeClass("CLASS_VANGUARD", CLASS_VANGUARD); // Should be back to Level 5 Stats expect(hero.baseStats.health).to.equal(140); }); describe("recalculateStats with Skill Tree", () => { it("should apply skill tree stat boosts to maxHealth", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); hero.maxHealth = hero.baseStats.health; hero.currentHealth = hero.maxHealth; // Initial health should be 100 expect(hero.maxHealth).to.equal(100); // Create skill tree with health boost const treeDef = { nodes: { ROOT: { id: "ROOT", type: "STAT_BOOST", data: { stat: "health", value: 10 }, }, }, }; // Unlock the node hero.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"]; // Recalculate stats with tree definition hero.recalculateStats(null, treeDef); // Health should be increased by 10 expect(hero.maxHealth).to.equal(110); }); it("should apply multiple skill tree stat boosts", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); hero.maxHealth = hero.baseStats.health; hero.currentHealth = hero.maxHealth; const treeDef = { nodes: { ROOT: { id: "ROOT", type: "STAT_BOOST", data: { stat: "health", value: 10 }, }, NODE_2: { id: "NODE_2", type: "STAT_BOOST", data: { stat: "health", value: 5 }, }, }, }; // Unlock both nodes hero.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT", "NODE_2"]; hero.recalculateStats(null, treeDef); // Health should be increased by 15 (10 + 5) expect(hero.maxHealth).to.equal(115); }); it("should only apply stat boosts from unlocked nodes", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); hero.maxHealth = hero.baseStats.health; hero.currentHealth = hero.maxHealth; const treeDef = { nodes: { ROOT: { id: "ROOT", type: "STAT_BOOST", data: { stat: "health", value: 10 }, }, LOCKED_NODE: { id: "LOCKED_NODE", type: "STAT_BOOST", data: { stat: "health", value: 20 }, }, }, }; // Only unlock ROOT, not LOCKED_NODE hero.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"]; hero.recalculateStats(null, treeDef); // Should only get boost from ROOT (10), not LOCKED_NODE (20) expect(hero.maxHealth).to.equal(110); }); it("should apply stat boosts to non-health stats", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); const treeDef = { nodes: { ATTACK_BOOST: { id: "ATTACK_BOOST", type: "STAT_BOOST", data: { stat: "attack", value: 5 }, }, }, }; hero.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ATTACK_BOOST"]; // Note: recalculateStats doesn't return stats, but we can verify it was called // The actual stat application is tested through CharacterSheet UI tests hero.recalculateStats(null, treeDef); // Verify the method completed without error expect(hero.maxHealth).to.exist; }); it("should update currentHealth proportionally when maxHealth changes", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); hero.maxHealth = 100; hero.currentHealth = 50; // 50% health const treeDef = { nodes: { ROOT: { id: "ROOT", type: "STAT_BOOST", data: { stat: "health", value: 20 }, }, }, }; hero.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"]; hero.recalculateStats(null, treeDef); // maxHealth should be 120 (100 + 20) expect(hero.maxHealth).to.equal(120); // currentHealth should be proportionally adjusted (50% of 120 = 60) expect(hero.currentHealth).to.equal(60); }); it("should handle treeDef with no unlocked nodes", () => { const hero = new Explorer("p1", "Hero", "CLASS_VANGUARD", CLASS_VANGUARD); hero.recalculateBaseStats(CLASS_VANGUARD); hero.maxHealth = hero.baseStats.health; hero.currentHealth = hero.maxHealth; const treeDef = { nodes: { ROOT: { id: "ROOT", type: "STAT_BOOST", data: { stat: "health", value: 10 }, }, }, }; // No unlocked nodes hero.classMastery["CLASS_VANGUARD"].unlockedNodes = []; hero.recalculateStats(null, treeDef); // Health should remain unchanged expect(hero.maxHealth).to.equal(100); }); }); });