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