aether-shards/src/units/Explorer.js

143 lines
3.9 KiB
JavaScript
Raw Normal View History

/**
* @typedef {import("./types.js").ClassMastery} ClassMastery
* @typedef {import("./types.js").Equipment} Equipment
*/
import { Unit } from "./Unit.js";
/**
* Explorer.js
* Player character class supporting Multi-Class Mastery and Persistent Progression.
* @class
*/
export class Explorer extends Unit {
/**
* @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
*/
constructor(id, name, startingClassId, classDefinition) {
super(id, name, "EXPLORER", `${startingClassId}_MODEL`);
/** @type {string} */
this.activeClassId = startingClassId;
// Persistent Mastery: Tracks progress for EVERY class this character has played
// Key: ClassID, Value: { level, xp, skillPoints, unlockedNodes[] }
/** @type {Record<string, ClassMastery>} */
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
/** @type {Equipment} */
this.equipment = {
weapon: null,
armor: null,
utility: null,
relic: null,
};
// Active Skills (Populated by Skill Tree)
/** @type {unknown[]} */
this.actions = [];
/** @type {unknown[]} */
this.passives = [];
}
/**
* Initializes mastery data for a class.
* @param {string} classId - Class ID to initialize
*/
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.
* @param {Record<string, unknown>} classDef - The JSON definition of the class stats.
*/
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).
* @param {string} newClassId - New class ID
* @param {Record<string, unknown>} newClassDef - New class definition
*/
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.
* @param {number} amount - XP amount to add
*/
gainExperience(amount) {
const mastery = this.classMastery[this.activeClassId];
mastery.xp += amount;
// Level up logic would be handled by a system checking XP curves
}
/**
* Gets the current level of the active class.
* @returns {number} - Current level
*/
getLevel() {
return this.classMastery[this.activeClassId].level;
}
}