/** * 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 = '
Loading skills from server...