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/MissionSystem.js
2026-03-11 00:32:45 -03:00

241 lines
9.3 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 — Mission System (GDD §8.3, §15)
* Fleet Missions: Attack, Mine, Patrol, Transport
* Reward formula: base_reward = difficulty × 100 × (1 + level × 0.02)
*/
const MISSION_TYPES = {
patrol: {
label: 'Patrol', icon: '🛡',
desc: 'Patrol a sector — passive XP and chance of intercepting pirates.',
baseDurationMin: 3, baseDurationMax: 5, // minutes
rewards: (level) => ({ xp: Math.floor(50 * (1 + level * 0.02)), credits: Math.floor(30 * (1 + level * 0.02)) }),
riskFactor: 0.05,
},
mine: {
label: 'Mining Run', icon: '⛏',
desc: 'Send fleet to mine asteroids in a sector. Returns Metal and Gas.',
baseDurationMin: 5, baseDurationMax: 10,
rewards: (level) => ({
xp: Math.floor(30 * (1 + level * 0.02)),
metal: Math.floor(200 * (1 + level * 0.05)),
gas: Math.floor(80 * (1 + level * 0.05)),
}),
riskFactor: 0.02,
},
transport: {
label: 'Trade Run', icon: '🚚',
desc: 'Transport cargo between sectors for credits.',
baseDurationMin: 8, baseDurationMax: 15,
rewards: (level) => ({ xp: Math.floor(40 * (1 + level * 0.02)), credits: Math.floor(150 * (1 + level * 0.03)) }),
riskFactor: 0.01,
},
attack: {
label: 'Raid', icon: '⚔',
desc: 'Raid a pirate outpost. High rewards, high risk.',
baseDurationMin: 10, baseDurationMax: 20,
rewards: (level) => ({
xp: Math.floor(120 * (1 + level * 0.02)),
credits: Math.floor(200 * (1 + level * 0.03)),
loot: true,
}),
riskFactor: 0.25,
},
explore: {
label: 'Explore', icon: '🔭',
desc: 'Explore an unknown sector for intel and resources.',
baseDurationMin: 6, baseDurationMax: 12,
rewards: (level) => ({ xp: Math.floor(80 * (1 + level * 0.02)), darkMatter: Math.floor(2 + Math.floor(level / 5)) }),
riskFactor: 0.10,
},
};
// Random loot table for raid missions
const RAID_LOOT = ['repair_kit','warp_token','shield_charge','metal_cache','crystal_shard'];
class MissionSystem {
constructor() {
this.MISSION_TYPES = MISSION_TYPES;
}
/** Start a fleet mission */
startMission(playerData, { missionType, fleetShipIds = [] }) {
if (!MISSION_TYPES[missionType]) throw new Error('Unknown mission type: ' + missionType);
const type = MISSION_TYPES[missionType];
const level = playerData.stats?.level || 1;
// Need at least one ship
const ships = (playerData.inventory || []).filter(i => i.type === 'ship' && fleetShipIds.includes(i.id));
if (ships.length === 0) throw new Error('Select at least one ship for the mission.');
// Ships can't be on multiple missions
const activeMissions = playerData.fleetMissions || [];
const busyShips = new Set(activeMissions.flatMap(m => m.shipIds || []));
const conflicting = fleetShipIds.filter(id => busyShips.has(id));
if (conflicting.length > 0) throw new Error('Some ships are already on a mission.');
// Apply travelSpeed research bonus — reduces mission duration (GDD §8 travel formula)
const travelBonus = Math.min(0.60, (playerData.research?.effects?.travelSpeed || 0) / 100);
const baseDurationMin = type.baseDurationMin + Math.random() * (type.baseDurationMax - type.baseDurationMin);
const durationMin = baseDurationMin * (1 - travelBonus);
const durationMs = Math.round(durationMin * 60 * 1000);
const now = Date.now();
const mission = {
id: 'mission_' + now + '_' + Math.random().toString(36).slice(2, 7),
type: missionType,
label: type.label,
icon: type.icon,
shipIds: fleetShipIds,
startedAt: now,
completesAt: now + durationMs,
level,
status: 'active',
};
playerData.fleetMissions = [...activeMissions, mission];
return mission;
}
/** Collect completed missions, apply rewards, return results */
collectMissions(playerData, resourceSystem) {
const now = Date.now();
const active = playerData.fleetMissions || [];
const done = active.filter(m => m.completesAt <= now && m.status === 'active');
const remaining = active.filter(m => m.completesAt > now || m.status !== 'active');
playerData.fleetMissions = remaining;
const results = [];
for (const mission of done) {
const type = MISSION_TYPES[mission.type];
if (!type) continue;
const rewards = type.rewards(mission.level || 1);
// Risk roll — small chance mission fails
const failed = Math.random() < type.riskFactor;
if (failed) {
results.push({ mission, success: false, message: `Mission failed! Fleet was intercepted.` });
continue;
}
// Apply XP + credits
if (rewards.xp) playerData.stats.experience = (playerData.stats.experience || 0) + rewards.xp;
if (rewards.credits) playerData.stats.credits = (playerData.stats.credits || 0) + rewards.credits;
// Apply resource rewards
const resGains = {};
if (rewards.metal) resGains.metal = rewards.metal;
if (rewards.gas) resGains.gas = rewards.gas;
if (rewards.crystal) resGains.crystal = rewards.crystal;
if (rewards.darkMatter) resGains.darkMatter = rewards.darkMatter;
if (Object.keys(resGains).length > 0 && resourceSystem) {
resourceSystem.initResources(playerData);
resourceSystem.add(playerData, resGains);
}
// Random loot for raids
const lootItem = rewards.loot ? RAID_LOOT[Math.floor(Math.random() * RAID_LOOT.length)] : null;
if (lootItem) {
if (!playerData.inventory) playerData.inventory = [];
playerData.inventory.push({ id: lootItem + '_' + Date.now(), type: 'consumable', name: lootItem.replace('_', ' '), icon: '📦', description: 'Mission loot' });
}
results.push({ mission, success: true, rewards, lootItem, message: `${type.label} completed successfully!` });
}
return results;
}
/** Recall a mission early (no rewards, ships return instantly) */
recallMission(playerData, missionId) {
const idx = (playerData.fleetMissions || []).findIndex(m => m.id === missionId);
if (idx < 0) throw new Error('Mission not found');
playerData.fleetMissions.splice(idx, 1);
}
getMissionTypes() { return MISSION_TYPES; }
getMissionsForPlayer(playerData) {
return {
active: (playerData.fleetMissions || []).filter(m => m.status === 'active'),
types: MISSION_TYPES,
};
}
}
module.exports = MissionSystem;
// ── Faction NPC Missions (GDD §15.1) ─────────────────────────────────────
const fs = require('fs');
const path = require('path');
let FACTION_MISSIONS = [];
try {
const p = path.join(__dirname, '../data/gso/missions/faction_missions.json');
FACTION_MISSIONS = JSON.parse(fs.readFileSync(p, 'utf8')).missions;
} catch(e) { /* data not loaded */ }
MissionSystem.prototype.getAvailableFactionMissions = function(playerData) {
const level = playerData.stats?.level || 1;
const rep = playerData.reputation || {};
return FACTION_MISSIONS.filter(m => {
if (m.minLevel > level) return false;
if (m.requires_rep !== undefined) {
const fRep = rep[m.faction] || 0;
if (fRep < m.requires_rep) return false;
}
return true;
});
};
MissionSystem.prototype.startFactionMission = function(playerData, missionId) {
const mission = FACTION_MISSIONS.find(m => m.id === missionId);
if (!mission) throw new Error('Faction mission not found: ' + missionId);
const level = playerData.stats?.level || 1;
if (mission.minLevel > level) throw new Error(`Requires Commander Level ${mission.minLevel}`);
const now = Date.now();
const job = {
id: 'fmission_' + now,
type: 'faction',
factionMissionId: mission.id,
label: mission.name,
icon: '📋',
faction: mission.faction,
startedAt: now,
completesAt: now + mission.duration * 1000,
status: 'active',
rewards: mission.rewards,
};
playerData.fleetMissions = [...(playerData.fleetMissions||[]), job];
return job;
};
MissionSystem.prototype.collectFactionMissions = function(playerData, resourceSystem, reputationSystem) {
const now = Date.now();
const done = (playerData.fleetMissions||[]).filter(m => m.type==='faction' && m.completesAt <= now && m.status==='active');
playerData.fleetMissions = (playerData.fleetMissions||[]).filter(m => !(m.type==='faction' && m.completesAt <= now && m.status==='active'));
const results = [];
for (const job of done) {
const r = job.rewards || {};
if (r.credits) playerData.stats.credits = (playerData.stats.credits||0) + r.credits;
if (r.xp) playerData.addExperience?.(r.xp);
if (r.metal || r.gas || r.crystal || r.darkMatter) {
resourceSystem?.initResources(playerData);
const gains = {};
if (r.metal) gains.metal = r.metal;
if (r.gas) gains.gas = r.gas;
if (r.crystal) gains.crystal = r.crystal;
if (r.darkMatter) gains.darkMatter = r.darkMatter;
resourceSystem?.add(playerData, gains);
}
if (r.reputation && reputationSystem) {
for (const [factionId, delta] of Object.entries(r.reputation)) {
reputationSystem.adjustReputation(playerData, factionId, delta);
}
}
results.push({ mission: job, success: true, rewards: r });
}
return results;
};