This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/GameServer/systems/SkillSystem.js
2026-03-11 00:32:45 -03:00

238 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Galaxy Strike Online — SkillSystem (GDD §10)
* Server-side skill definitions, player tracking, XP, allocation, and bonus computation.
*
* Skill effect pipeline:
* getSkillBonuses(userId) → aggregates all levelled skill bonuses for a player.
* applySkillsToCombat(userId, combatStats) → mutates combatStats in-place.
* applyCraftingBonuses(userId, baseCraftTime) → returns reduced craft time.
*/
class SkillSystem {
constructor(contentLoader) {
this.loader = contentLoader;
this.playerSkills = new Map(); // userId → { skillPoints, skills, unlockedSkills }
}
// ── Content helpers ────────────────────────────────────────────────────────
getAllSkills() { return this.loader.getAllSkills(); }
getSkill(id) { return this.loader.getSkill(id); }
getSkillsByCategory(cat) { return this.loader.getSkillsByCategory(cat); }
// ── Player data ────────────────────────────────────────────────────────────
initializePlayerData(userId) {
if (this.playerSkills.has(userId)) return this.playerSkills.get(userId);
const defaultUnlocked = this.loader.getAllSkills()
.filter(s => s.defaultUnlocked)
.map(s => s.id);
const skills = {};
for (const id of defaultUnlocked) skills[id] = { level: 1, experience: 0 };
const data = { skillPoints: 0, skills, unlockedSkills: new Set(defaultUnlocked) };
this.playerSkills.set(userId, data);
return data;
}
getPlayerSkills(userId) { return this.initializePlayerData(userId); }
// Restore from persisted DB object (called on player load)
restorePlayerData(userId, saved) {
if (!saved) return this.initializePlayerData(userId);
const data = {
skillPoints: saved.skillPoints || 0,
skills: saved.skills || {},
unlockedSkills: new Set(saved.unlockedSkills || Object.keys(saved.skills || {})),
};
this.playerSkills.set(userId, data);
return data;
}
// ── XP & levelling ────────────────────────────────────────────────────────
/**
* Award experience to a specific skill.
* Returns { skillId, newLevel, leveled, xpToNextLevel }.
*/
addExperience(userId, skillId, amount) {
const playerData = this.initializePlayerData(userId);
const skillDef = this.getSkill(skillId);
if (!skillDef) throw new Error(`Unknown skill: ${skillId}`);
if (!playerData.unlockedSkills.has(skillId)) throw new Error(`Skill ${skillId} not unlocked`);
if (!playerData.skills[skillId]) playerData.skills[skillId] = { level: 1, experience: 0 };
const entry = playerData.skills[skillId];
const xpPerLvl = skillDef.experiencePerLevel || 1000;
const maxLevel = skillDef.maxLevel || 100;
entry.experience += amount;
let leveled = false;
while (entry.level < maxLevel && entry.experience >= xpPerLvl) {
entry.experience -= xpPerLvl;
entry.level++;
leveled = true;
}
if (entry.level >= maxLevel) entry.experience = 0;
return {
skillId,
newLevel: entry.level,
experience: entry.experience,
xpToNextLevel: entry.level >= maxLevel ? 0 : xpPerLvl - entry.experience,
leveled,
};
}
// ── Skill points ──────────────────────────────────────────────────────────
awardSkillPoints(userId, amount) {
const playerData = this.initializePlayerData(userId);
playerData.skillPoints = (playerData.skillPoints || 0) + amount;
return { skillPoints: playerData.skillPoints };
}
/**
* Spend 1 skill point to increase a skill's level by 1.
*/
allocateSkillPoint(userId, skillId) {
const playerData = this.initializePlayerData(userId);
const skillDef = this.getSkill(skillId);
if (!skillDef) throw new Error(`Unknown skill: ${skillId}`);
if (!playerData.unlockedSkills.has(skillId)) throw new Error(`Skill ${skillId} not unlocked`);
if ((playerData.skillPoints || 0) < 1) throw new Error('No skill points available');
const maxLevel = skillDef.maxLevel || 100;
if (!playerData.skills[skillId]) playerData.skills[skillId] = { level: 0, experience: 0 };
if (playerData.skills[skillId].level >= maxLevel) throw new Error(`Skill ${skillId} already at max level`);
playerData.skills[skillId].level++;
playerData.skillPoints--;
return {
skillId,
newLevel: playerData.skills[skillId].level,
skillPoints: playerData.skillPoints,
};
}
// ── Unlock ────────────────────────────────────────────────────────────────
unlockSkill(userId, skillId) {
const playerData = this.initializePlayerData(userId);
const skillDef = this.getSkill(skillId);
if (!skillDef) throw new Error(`Unknown skill: ${skillId}`);
if (playerData.unlockedSkills.has(skillId)) throw new Error(`Skill ${skillId} already unlocked`);
const req = skillDef.unlockRequires;
if (req) {
const prereq = playerData.skills[req.skill];
if (!prereq || prereq.level < req.level) {
throw new Error(`Requires ${req.skill} at level ${req.level}`);
}
}
playerData.unlockedSkills.add(skillId);
playerData.skills[skillId] = { level: 1, experience: 0 };
return { skillId, unlocked: true };
}
// ── Bonus aggregation ─────────────────────────────────────────────────────
/**
* Returns an object of aggregated stat bonuses for a player across
* all levelled unlocked skills. Values are per-level; total = level × bonus.
*/
getSkillBonuses(userId) {
const playerData = this.initializePlayerData(userId);
const bonuses = {
// Combat
damage: 0, accuracy: 0, criticalChance: 0, defense: 0, speed: 0,
// Crafting
craftingBonus: 0, craftingSpeed: 0, armorStats: 0, durabilityBonus: 0,
shipBuildSpeed: 0, hullStrengthBonus: 0, shipRepairSpeed: 0, fleetCapacity: 0,
circuitYield: 0, techComponentQuality: 0, researchSpeed: 0,
// Science
xpGainBonus: 0, alienTechBonus: 0, energyEfficiency: 0,
quantumBonus: 0, bioEngineeringBonus: 0,
};
for (const [skillId, entry] of Object.entries(playerData.skills)) {
if (!playerData.unlockedSkills.has(skillId)) continue;
const def = this.getSkill(skillId);
if (!def || !def.bonuses) continue;
const level = entry.level || 1;
for (const [key, value] of Object.entries(def.bonuses)) {
if (bonuses[key] !== undefined) bonuses[key] += value * level;
}
}
return bonuses;
}
/**
* Apply a player's skill bonuses to a combat stats object.
* combatStats shape: { attack, defense, critChance, speed, accuracy }
* Mutates in-place and returns modified object.
*/
applySkillsToCombat(userId, combatStats) {
const b = this.getSkillBonuses(userId);
combatStats.attack = Math.floor((combatStats.attack || 0) * (1 + b.damage / 100));
combatStats.defense = Math.floor((combatStats.defense || 0) * (1 + b.defense / 100));
combatStats.critChance= Math.min(0.75, (combatStats.critChance || 0.05) + b.criticalChance / 100);
combatStats.accuracy = Math.min(1.0, (combatStats.accuracy || 0.9) + b.accuracy / 100);
combatStats.speed = Math.floor((combatStats.speed || 10) * (1 + b.speed / 100));
return combatStats;
}
/**
* Apply crafting bonuses to a base craft time in seconds.
* Returns reduced craft time (minimum 1s).
*/
applyCraftingBonuses(userId, baseCraftTime) {
const b = this.getSkillBonuses(userId);
const speedBonus = Math.min(0.8, (b.craftingBonus + b.craftingSpeed) / 100);
return Math.max(1, Math.floor(baseCraftTime * (1 - speedBonus)));
}
/**
* Apply XP gain bonus from science skills.
* Returns adjusted XP amount.
*/
applyXpBonus(userId, baseXp) {
const b = this.getSkillBonuses(userId);
return Math.floor(baseXp * (1 + b.xpGainBonus / 100));
}
// ── Statistics ────────────────────────────────────────────────────────────
getSkillStatistics(userId) {
const playerData = this.initializePlayerData(userId);
const allSkills = this.getAllSkills();
const unlocked = playerData.unlockedSkills.size;
let totalLevels = 0;
let maxedSkills = 0;
for (const [skillId, entry] of Object.entries(playerData.skills)) {
const def = this.getSkill(skillId);
if (!def) continue;
totalLevels += entry.level || 0;
if ((entry.level || 0) >= (def.maxLevel || 100)) maxedSkills++;
}
return {
totalSkills: allSkills.length,
unlockedSkills: unlocked,
totalLevels,
maxedSkills,
skillPoints: playerData.skillPoints || 0,
};
}
}
module.exports = SkillSystem;