API/Galaxy-Strike-Online-main/Client/js/systems/SkillSystem.js

408 lines
16 KiB
JavaScript

/**
* Galaxy Strike Online - Client Skill System
* Skill definitions are loaded from the server; this file handles
* local progression tracking, UI rendering, and skill-point spending.
*/
class SkillSystem {
constructor(gameEngine) {
this.game = gameEngine;
// Populated after server responds to 'get_skills'
this.skills = { combat: {}, science: {}, crafting: {} };
this.categories = {
combat: 'Combat',
science: 'Science',
crafting: 'Crafting'
};
this.experienceRates = { combat: 1.0, science: 0.8, crafting: 0.6 };
this.activeBuffs = {};
// Loading state
this._loaded = false;
this._loading = false;
}
// ------------------------------------------------------------------ //
// Initialisation — request skill definitions from the server
// ------------------------------------------------------------------ //
async initialize() {
if (this._loaded || this._loading) return;
this._loading = true;
console.log('[SKILL SYSTEM] Requesting skill definitions from server');
if (!window.game?.socket) {
console.warn('[SKILL SYSTEM] No socket connection — skills will load when connected');
this._loading = false;
return;
}
try {
const serverSkills = await this._fetchSkillsFromServer();
this._applyServerDefinitions(serverSkills);
this._loaded = true;
console.log('[SKILL SYSTEM] Skill definitions loaded from server');
} catch (err) {
console.error('[SKILL SYSTEM] Failed to load skills from server:', err);
} finally {
this._loading = false;
}
}
_fetchSkillsFromServer() {
return new Promise((resolve, reject) => {
const socket = window.game.socket;
const timeout = setTimeout(() => {
socket.off('skills_data', handler);
reject(new Error('Skill data request timed out'));
}, 10000);
const handler = (data) => {
clearTimeout(timeout);
socket.off('skills_data', handler);
if (data && (Array.isArray(data) || typeof data === 'object')) {
resolve(data);
} else {
reject(new Error('Invalid skill data from server'));
}
};
socket.on('skills_data', handler);
socket.emit('get_skills');
});
}
/**
* Merge server skill definitions into the local skill map.
* Preserves any progress already loaded from playerData.
*/
_applyServerDefinitions(serverSkills) {
// Server may return an array or a categorised object
const asList = Array.isArray(serverSkills)
? serverSkills
: Object.values(serverSkills).flat();
// Reset to empty categories first
this.skills = { combat: {}, science: {}, crafting: {} };
for (const skill of asList) {
const cat = skill.category || 'combat';
if (!this.skills[cat]) this.skills[cat] = {};
// Keep any existing progress if already loaded from save data
const existing = this.skills[cat][skill.id] || {};
this.skills[cat][skill.id] = {
name: skill.name,
description: skill.description,
maxLevel: skill.maxLevel || 100,
effects: skill.bonuses || skill.effects || {},
icon: skill.icon || 'fa-star',
// Progress fields — kept from existing save data if present
currentLevel: existing.currentLevel ?? 0,
experience: existing.experience ?? 0,
experienceToNext: existing.experienceToNext ?? (skill.experiencePerLevel || 1000),
unlocked: existing.unlocked ?? (skill.defaultUnlocked !== false),
requiredLevel: skill.requiredLevel ?? null
};
}
console.log('[SKILL SYSTEM] Applied server definitions. Categories:', Object.keys(this.skills));
}
// ------------------------------------------------------------------ //
// Skill progression
// ------------------------------------------------------------------ //
addSkillExperience(category, skillId, amount) {
const skill = this.skills[category]?.[skillId];
if (!skill || skill.currentLevel >= skill.maxLevel) return false;
skill.experience += amount;
while (skill.experience >= skill.experienceToNext && skill.currentLevel < skill.maxLevel) {
this.levelUpSkill(category, skillId);
}
this.applySkillEffects();
return true;
}
levelUpSkill(category, skillId) {
const skill = this.skills[category][skillId];
const excess = skill.experience - skill.experienceToNext;
skill.currentLevel++;
skill.experienceToNext = Math.floor(skill.experienceToNext * 1.5);
skill.experience = Math.max(0, excess);
this.applySkillEffects();
this.game.showNotification(`${skill.name} leveled up to ${skill.currentLevel}!`, 'success', 4000);
}
upgradeSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
if (!skill.unlocked) { this.game.showNotification('Skill is locked', 'error', 3000); return false; }
if (skill.currentLevel >= skill.maxLevel){ this.game.showNotification('Skill is at maximum level', 'warning', 3000); return false; }
if (player.stats.skillPoints < 1) { this.game.showNotification('Not enough skill points', 'error', 3000); return false; }
player.stats.skillPoints--;
this.levelUpSkill(category, skillId);
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
return true;
}
unlockSkill(category, skillId) {
const skill = this.skills[category]?.[skillId];
const player = this.game.systems.player;
if (!skill) { this.game.showNotification('Skill not found', 'error', 3000); return false; }
if (skill.unlocked) { this.game.showNotification('Skill is already unlocked', 'warning', 3000); return false; }
if (skill.requiredLevel && player.stats.level < skill.requiredLevel) {
this.game.showNotification(`Requires level ${skill.requiredLevel}`, 'error', 3000);
return false;
}
if (player.stats.skillPoints < 2) {
this.game.showNotification('Requires 2 skill points to unlock', 'error', 3000);
return false;
}
player.stats.skillPoints -= 2;
skill.unlocked = true;
skill.currentLevel = 1;
this.applySkillEffects();
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
this.game.showNotification(`${skill.name} unlocked!`, 'success', 4000);
return true;
}
applySkillEffects() {
const player = this.game.systems.player;
this.resetToBaseStats();
for (const category of Object.values(this.skills)) {
for (const skill of Object.values(category)) {
if (!skill.unlocked || skill.currentLevel <= 0) continue;
for (const [effect, value] of Object.entries(skill.effects || {})) {
const total = value * skill.currentLevel;
switch (effect) {
case 'attack': player.attributes.attack += total; break;
case 'defense': player.attributes.defense += total; break;
case 'speed': player.attributes.speed += total; break;
case 'health':
case 'maxHealth': player.attributes.maxHealth += total; break;
case 'maxEnergy': player.attributes.maxEnergy += total; break;
case 'criticalChance': player.attributes.criticalChance += total; break;
case 'criticalDamage': player.attributes.criticalDamage += total; break;
default:
if (!this.activeBuffs[effect]) this.activeBuffs[effect] = 0;
this.activeBuffs[effect] += total;
}
}
}
}
player.updateUI();
}
resetToBaseStats() {
const player = this.game.systems.player;
const lvl = player.stats.level || 1;
Object.assign(player.attributes, {
attack: 10 + (lvl - 1) * 2,
defense: 5 + (lvl - 1) * 1,
speed: 10,
maxHealth: 100 + (lvl - 1) * 10,
maxEnergy: 100 + (lvl - 1) * 5,
criticalChance: 0.05,
criticalDamage: 1.5
});
this.activeBuffs = {};
}
// ------------------------------------------------------------------ //
// Combat / science / crafting XP helpers
// ------------------------------------------------------------------ //
awardCombatExperience(amount) {
this.addSkillExperience('combat', 'weapons_mastery', amount);
this.addSkillExperience('combat', 'tactical_analysis', amount * 0.5);
}
awardScienceExperience(amount) {
this.addSkillExperience('science', 'energy_manipulation', amount);
this.addSkillExperience('science', 'alien_technology', amount * 0.3);
}
awardCraftingExperience(amount) {
this.addSkillExperience('crafting', 'weapons_crafting', amount);
this.addSkillExperience('crafting', 'armor_forging', amount * 0.5);
}
// ------------------------------------------------------------------ //
// Queries
// ------------------------------------------------------------------ //
getSkillLevel(category, skillId) {
// Support single-arg form used by CraftingSystem: getSkillLevel('crafting')
if (skillId === undefined) {
let max = 0;
for (const cat of Object.values(this.skills)) {
if (cat[category]) max = Math.max(max, cat[category].currentLevel || 0);
}
return max;
}
return this.skills[category]?.[skillId]?.currentLevel || 0;
}
getSkillExperience(skillId) {
for (const cat of Object.values(this.skills)) {
if (cat[skillId]) return cat[skillId].experience || 0;
}
return 0;
}
getExperienceNeeded(skillId) {
for (const cat of Object.values(this.skills)) {
if (cat[skillId]) return cat[skillId].experienceToNext || 0;
}
return 0;
}
hasSkill(category, skillId, minimumLevel = 1) {
const skill = this.skills[category]?.[skillId];
return skill && skill.unlocked && skill.currentLevel >= minimumLevel;
}
getSkillBonus(effect) {
return this.activeBuffs[effect] || 0;
}
// ------------------------------------------------------------------ //
// UI
// ------------------------------------------------------------------ //
updateUI() {
this.updateSkillsGrid();
this.updateSkillPointsDisplay();
}
updateSkillsGrid() {
const grid = document.getElementById('skillsGrid');
if (!grid) return;
const activeCategory = document.querySelector('.skill-cat-btn.active')?.dataset.category || 'combat';
const skills = this.skills[activeCategory] || {};
if (!this._loaded) {
grid.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading skills from server...</p></div>';
return;
}
grid.innerHTML = '';
Object.entries(skills).forEach(([skillId, skill]) => {
const el = document.createElement('div');
el.className = `skill-item ${!skill.unlocked ? 'locked' : ''}`;
const progressPercent = skill.currentLevel > 0
? (skill.experience / skill.experienceToNext) * 100
: 0;
const iconClass = this.game.systems.textureManager
? this.game.systems.textureManager.getIcon(skill.icon)
: (skill.icon || 'fa-question');
el.innerHTML = `
<div class="skill-header">
<div class="skill-icon"><i class="fas ${iconClass}"></i></div>
<div class="skill-info">
<div class="skill-name">${skill.name}</div>
<div class="skill-level">Lv. ${skill.currentLevel}/${skill.maxLevel}</div>
</div>
</div>
<div class="skill-description">${skill.description}</div>
${skill.currentLevel > 0 && skill.currentLevel < skill.maxLevel ? `
<div class="skill-progress">
<div class="progress-bar">
<div class="progress-fill" style="width:${progressPercent}%"></div>
</div>
<span>${skill.experience}/${skill.experienceToNext} XP</span>
</div>
` : skill.currentLevel >= skill.maxLevel ? `
<div class="skill-max-level"><span>MAX LEVEL</span></div>
` : ''}
<div class="skill-actions">
${!skill.unlocked ? `
<button class="btn btn-warning" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.unlockSkill('${activeCategory}','${skillId}')">
Unlock (2 Points)
</button>
` : skill.currentLevel < skill.maxLevel ? `
<button class="btn btn-primary" onclick="if(window.game&&window.game.systems)window.game.systems.skillSystem.upgradeSkill('${activeCategory}','${skillId}')">
Upgrade (1 Point)
</button>
` : `<span class="max-level">MAX LEVEL</span>`}
</div>
${skill.requiredLevel && !skill.unlocked ? `
<div class="skill-requirement">Requires Level ${skill.requiredLevel}</div>
` : ''}
`;
grid.appendChild(el);
});
}
updateSkillPointsDisplay() {
const player = this.game.systems.player;
document.querySelectorAll('.skill-points').forEach(el => {
el.textContent = `Skill Points: ${player.stats.skillPoints}`;
});
}
// ------------------------------------------------------------------ //
// Save / Load
// ------------------------------------------------------------------ //
save() {
return { skills: this.skills, activeBuffs: this.activeBuffs };
}
load(data) {
if (data.skills) {
for (const [category, skills] of Object.entries(data.skills)) {
if (!this.skills[category]) this.skills[category] = {};
for (const [skillId, skillData] of Object.entries(skills)) {
if (this.skills[category][skillId]) {
Object.assign(this.skills[category][skillId], skillData);
} else {
// Store progress even before server definitions arrive
this.skills[category][skillId] = { ...skillData };
}
}
}
}
if (data.activeBuffs) this.activeBuffs = data.activeBuffs;
this.applySkillEffects();
}
reset() {
this.activeBuffs = {};
for (const category of Object.values(this.skills)) {
for (const skill of Object.values(category)) {
skill.currentLevel = 0;
skill.experience = 0;
}
}
}
clear() { this.reset(); }
}