/** * 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.'); const durationMin = type.baseDurationMin + Math.random() * (type.baseDurationMax - type.baseDurationMin); 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; };