/** * Galaxy Strike Online - Galaxy System (GDD §8) * Procedurally generates and manages the 30×20 sector grid. * Visibility radius is driven by sensor_array building level + research sensorRange bonus. */ class GalaxySystem { constructor() { this.GRID_W = 30; this.GRID_H = 20; this._sectors = null; } /** Lazy-generate the galaxy grid once per server process */ getSectors() { if (!this._sectors) this._sectors = this._generate(); return this._sectors; } _generate() { const sectors = {}; for (let y = 0; y < this.GRID_H; y++) { for (let x = 0; x < this.GRID_W; x++) { const id = `${x}_${y}`; const distFromCenter = Math.hypot(x - 15, y - 10) / 18; const typeRoll = Math.random(); let type = 'empty'; if (typeRoll > 0.70) type = 'asteroid'; if (typeRoll > 0.88) type = 'npc_territory'; if (typeRoll > 0.94) type = 'trade_hub'; if (typeRoll > 0.97 && distFromCenter > 0.4) type = 'ruins'; if (typeRoll > 0.99 && distFromCenter > 0.6) type = 'void_rift'; const richness = Math.random(); sectors[id] = { id, x, y, type, name: this._sectorName(x, y), threat: Math.min(10, Math.floor(distFromCenter * 10 + Math.random() * 3)), richness: type === 'asteroid' ? (0.3 + richness * 0.7) : 0, owner: null, explored: false, }; } } // Always make the start sector safe sectors['15_10'] = { id: '15_10', x: 15, y: 10, type: 'trade_hub', name: 'New Haven', threat: 0, richness: 0, owner: null, explored: true, }; return sectors; } _sectorName(x, y) { const prefixes = ['Alpha','Beta','Gamma','Delta','Sigma','Tau','Zeta','Omega','Nova','Vega','Lyra','Cygni']; const suffixes = ['Prime','Station','Reach','Deep','Expanse','Crossing','Rift','Gate','Void','Fields']; const seed = (x * 31 + y * 17) % (prefixes.length * suffixes.length); return prefixes[seed % prefixes.length] + ' ' + suffixes[Math.floor(seed / prefixes.length) % suffixes.length]; } getSector(id) { return this.getSectors()[id] || null; } /** * Mark a sector explored by a player. * visRadius controls how many rings of adjacent sectors are also added to exploredSectors. * Default radius 1 = only direct neighbours revealed. */ exploreSector(sectorId, playerData, visRadius = 1) { const sector = this.getSector(sectorId); if (!sector) throw new Error('Sector not found'); const explored = new Set(playerData.exploredSectors || []); explored.add(sectorId); // Reveal all sectors within visRadius rings const [cx, cy] = sectorId.split('_').map(Number); for (let dy = -visRadius; dy <= visRadius; dy++) { for (let dx = -visRadius; dx <= visRadius; dx++) { if (Math.abs(dx) + Math.abs(dy) > visRadius) continue; // diamond shape const nid = `${cx + dx}_${cy + dy}`; if (this.getSector(nid)) explored.add(nid); } } playerData.exploredSectors = Array.from(explored); return sector; } /** * Return all sectors visible to a player: explored + one ring of fog neighbours. * visRadius from sensor_array building + research is already applied by exploreSector; * here we add one extra fog ring beyond whatever has been explored. */ getVisibleSectors(playerData, visRadius = 1) { const explored = new Set(playerData.exploredSectors || ['15_10']); const visible = new Set(explored); // One fog ring beyond all explored sectors for (const id of explored) { const [x, y] = id.split('_').map(Number); for (const [dx, dy] of [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[1,-1],[-1,1],[1,1]]) { const nid = `${x + dx}_${y + dy}`; if (this.getSector(nid)) visible.add(nid); } } const all = this.getSectors(); return Array.from(visible).map(id => { const s = { ...all[id] }; s.explored = explored.has(id); if (!s.explored) { // Fog-of-war: hide details for unreached but adjacent sectors s.name = '???'; s.richness = 0; s.owner = null; s.type = 'unknown'; s.threat = null; } return s; }); } /** Player claims a sector as home base */ claimSector(sectorId, playerData) { const sector = this.getSector(sectorId); if (!sector) throw new Error('Sector not found'); if (sector.type === 'void_rift') throw new Error('Cannot claim Void Rift sectors'); sector.owner = playerData.userId; this.exploreSector(sectorId, playerData); playerData.homeSector = sectorId; return sector; } /** Calculate travel time between two sector IDs using the GDD formula */ travelSeconds(fromId, toId, fleetSpeed = 10, travelSpeedBonus = 0) { const [fx, fy] = fromId.split('_').map(Number); const [tx, ty] = toId.split('_').map(Number); const distance = Math.hypot(tx - fx, ty - fy); const effectiveSpeed = fleetSpeed * (1 + travelSpeedBonus / 100); return Math.max(10, Math.round((distance * 300) / Math.max(1, effectiveSpeed))); } } module.exports = GalaxySystem;