2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* @typedef {import("./types.js").ClassMastery} ClassMastery
|
|
|
|
|
* @typedef {import("./types.js").Equipment} Equipment
|
|
|
|
|
*/
|
|
|
|
|
|
2025-12-19 16:38:22 +00:00
|
|
|
import { Unit } from "./Unit.js";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Explorer.js
|
|
|
|
|
* Player character class supporting Multi-Class Mastery and Persistent Progression.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @class
|
2025-12-19 16:38:22 +00:00
|
|
|
*/
|
|
|
|
|
export class Explorer extends Unit {
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* @param {string} id - Unique unit identifier
|
|
|
|
|
* @param {string} name - Explorer name
|
|
|
|
|
* @param {string} startingClassId - Starting class ID
|
|
|
|
|
* @param {Record<string, unknown>} classDefinition - Class definition data
|
|
|
|
|
*/
|
2025-12-19 16:38:22 +00:00
|
|
|
constructor(id, name, startingClassId, classDefinition) {
|
|
|
|
|
super(id, name, "EXPLORER", `${startingClassId}_MODEL`);
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {string} */
|
2025-12-19 16:38:22 +00:00
|
|
|
this.activeClassId = startingClassId;
|
|
|
|
|
|
|
|
|
|
// Persistent Mastery: Tracks progress for EVERY class this character has played
|
|
|
|
|
// Key: ClassID, Value: { level, xp, skillPoints, unlockedNodes[] }
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Record<string, ClassMastery>} */
|
2025-12-19 16:38:22 +00:00
|
|
|
this.classMastery = {};
|
|
|
|
|
|
|
|
|
|
// Initialize the starting class entry
|
|
|
|
|
this.initializeMastery(startingClassId);
|
|
|
|
|
|
|
|
|
|
// Hydrate stats based on the provided definition
|
|
|
|
|
if (classDefinition) {
|
|
|
|
|
this.recalculateBaseStats(classDefinition);
|
|
|
|
|
this.currentHealth = this.baseStats.health;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inventory
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Equipment} */
|
2025-12-19 16:38:22 +00:00
|
|
|
this.equipment = {
|
|
|
|
|
weapon: null,
|
|
|
|
|
armor: null,
|
|
|
|
|
utility: null,
|
|
|
|
|
relic: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Active Skills (Populated by Skill Tree)
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {unknown[]} */
|
2025-12-19 16:38:22 +00:00
|
|
|
this.actions = [];
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {unknown[]} */
|
2025-12-19 16:38:22 +00:00
|
|
|
this.passives = [];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Initializes mastery data for a class.
|
|
|
|
|
* @param {string} classId - Class ID to initialize
|
|
|
|
|
*/
|
2025-12-19 16:38:22 +00:00
|
|
|
initializeMastery(classId) {
|
|
|
|
|
if (!this.classMastery[classId]) {
|
|
|
|
|
this.classMastery[classId] = {
|
|
|
|
|
level: 1,
|
|
|
|
|
xp: 0,
|
|
|
|
|
skillPoints: 0,
|
|
|
|
|
unlockedNodes: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates base stats based on the active class's base + growth rates * level.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @param {Record<string, unknown>} classDef - The JSON definition of the class stats.
|
2025-12-19 16:38:22 +00:00
|
|
|
*/
|
|
|
|
|
recalculateBaseStats(classDef) {
|
|
|
|
|
if (classDef.id !== this.activeClassId) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`Mismatch: Recalculating stats for ${this.activeClassId} using definition for ${classDef.id}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mastery = this.classMastery[this.activeClassId];
|
|
|
|
|
|
|
|
|
|
// 1. Start with Class Defaults
|
|
|
|
|
let stats = { ...classDef.base_stats };
|
|
|
|
|
|
|
|
|
|
// 2. Add Level Growth
|
|
|
|
|
// (Level 1 is base, so growth applies for levels 2+)
|
|
|
|
|
const levelsGained = mastery.level - 1;
|
|
|
|
|
if (levelsGained > 0) {
|
|
|
|
|
for (let stat in classDef.growth_rates) {
|
|
|
|
|
if (stats[stat] !== undefined) {
|
|
|
|
|
stats[stat] += classDef.growth_rates[stat] * levelsGained;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.baseStats = stats;
|
|
|
|
|
this.maxHealth = stats.health; // Update MaxHP cap
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Swaps the active class logic.
|
|
|
|
|
* NOTE: Does NOT check unlock requirements (handled by UI/MetaSystem).
|
2025-12-22 20:55:41 +00:00
|
|
|
* @param {string} newClassId - New class ID
|
|
|
|
|
* @param {Record<string, unknown>} newClassDef - New class definition
|
2025-12-19 16:38:22 +00:00
|
|
|
*/
|
|
|
|
|
changeClass(newClassId, newClassDef) {
|
|
|
|
|
// 1. Ensure mastery record exists
|
|
|
|
|
this.initializeMastery(newClassId);
|
|
|
|
|
|
|
|
|
|
// 2. Switch ID
|
|
|
|
|
this.activeClassId = newClassId;
|
|
|
|
|
|
|
|
|
|
// 3. Update Model ID (Visuals)
|
|
|
|
|
this.voxelModelID = `${newClassId}_MODEL`;
|
|
|
|
|
|
|
|
|
|
// 4. Recalculate Stats for the new job
|
|
|
|
|
this.recalculateBaseStats(newClassDef);
|
|
|
|
|
|
|
|
|
|
// 5. Reset Current HP to new Max (or keep percentage? Standard is reset)
|
|
|
|
|
this.currentHealth = this.baseStats.health;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds XP to the *current* class.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @param {number} amount - XP amount to add
|
2025-12-19 16:38:22 +00:00
|
|
|
*/
|
|
|
|
|
gainExperience(amount) {
|
|
|
|
|
const mastery = this.classMastery[this.activeClassId];
|
|
|
|
|
mastery.xp += amount;
|
|
|
|
|
// Level up logic would be handled by a system checking XP curves
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Gets the current level of the active class.
|
|
|
|
|
* @returns {number} - Current level
|
|
|
|
|
*/
|
2025-12-19 16:38:22 +00:00
|
|
|
getLevel() {
|
|
|
|
|
return this.classMastery[this.activeClassId].level;
|
|
|
|
|
}
|
|
|
|
|
}
|