143 lines
5.1 KiB
JavaScript
143 lines
5.1 KiB
JavaScript
/**
|
||
* 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;
|