snapkitty
hardware
erlang
grisp-shadow-fleet / universe /PlanetSystem.ts
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/grisp-shadow-fleet
b8a03fa verified
Raw History Blame Contribute Delete
6.77 kB
import { clamp, distance, hashString, vector } from './deterministic.js';
import { emitWorldEvent, findRequired } from './state-utils.js';
import type { CommandResult, EntityId, PlanetaryRegionState, UniverseState, Vector3, WeatherState } from './types.js';
export interface AtmosphericSample {
bodyId: EntityId;
altitudeM: number;
density: number;
pressureKpa: number;
temperatureC: number;
gravityMps2: number;
}
export interface TerrainSample {
regionId: EntityId;
position: Vector3;
elevationM: number;
slope: number;
material: 'rock' | 'regolith' | 'ice' | 'water' | 'vegetation';
landable: boolean;
}
export class PlanetSystem {
constructor(private state: UniverseState) {}
replaceState(state: UniverseState): void {
this.state = state;
}
sampleAtmosphere(bodyId: EntityId, position: Vector3): AtmosphericSample {
const body = findRequired(this.state.celestialBodies, bodyId, 'Celestial body');
const altitudeM = Math.max(0, distance(position, body.position) - body.radiusM);
const normalizedAltitude = body.atmosphereHeightM > 0 ? clamp(altitudeM / body.atmosphereHeightM, 0, 1) : 1;
const density = body.atmosphereHeightM > 0 ? Math.exp(-normalizedAltitude * 6) * (altitudeM <= body.atmosphereHeightM ? 1 : 0) : 0;
return {
bodyId,
altitudeM,
density,
pressureKpa: density * (body.kind === 'planet' ? 88 : 2.4),
temperatureC: body.kind === 'planet' ? 14 - normalizedAltitude * 55 : -18 - normalizedAltitude * 40,
gravityMps2: body.gravityMps2 * (body.radiusM / Math.max(body.radiusM, body.radiusM + altitudeM)) ** 2,
};
}
sampleTerrain(regionId: EntityId, localPosition: Vector3): TerrainSample {
const region = findRequired(this.state.planetaryRegions, regionId, 'Planetary region');
const seedPhase = (region.terrainSeed % 10_000) / 10_000 * Math.PI * 2;
const broad = Math.sin(localPosition.x * 0.00035 + seedPhase) * 82 + Math.cos(localPosition.z * 0.00029 - seedPhase) * 61;
const detail = Math.sin((localPosition.x + localPosition.z) * 0.0021 + seedPhase * 2) * 9;
const elevationM = broad + detail;
const dx = Math.cos(localPosition.x * 0.00035 + seedPhase) * 0.0287 + Math.cos((localPosition.x + localPosition.z) * 0.0021 + seedPhase * 2) * 0.0189;
const dz = -Math.sin(localPosition.z * 0.00029 - seedPhase) * 0.0177 + Math.cos((localPosition.x + localPosition.z) * 0.0021 + seedPhase * 2) * 0.0189;
const slope = Math.atan(Math.hypot(dx, dz));
const material = this.terrainMaterial(region, elevationM);
return { regionId, position: { ...localPosition }, elevationM, slope, material, landable: slope < 0.18 && material !== 'water' };
}
approachRegion(shipId: EntityId, regionId: EntityId): CommandResult {
const ship = findRequired(this.state.ships, shipId, 'Ship');
const region = findRequired(this.state.planetaryRegions, regionId, 'Planetary region');
if (ship.pilotId !== this.state.player.id || ship.flightMode !== 'free-flight') {
return { ok: false, message: 'Active piloting in free flight is required for planetary approach.', eventIds: [] };
}
const body = findRequired(this.state.celestialBodies, region.planetId, 'Region celestial body');
const altitude = distance(ship.transform.position, body.position) - body.radiusM;
if (altitude > body.atmosphereHeightM + 80_000) {
return { ok: false, message: `${ship.name} is not within planetary approach range.`, eventIds: [] };
}
this.state.player.layer = 'planetary-space';
const event = emitWorldEvent(this.state, 'planetary-approach-started', ship.id, region.id, `${ship.name} entered the approach corridor for ${region.name}.`, { data: { altitudeM: altitude } });
return { ok: true, message: event.summary, eventIds: [event.id] };
}
discoverRegion(regionId: EntityId): CommandResult {
const region = findRequired(this.state.planetaryRegions, regionId, 'Planetary region');
const focus = this.state.player.pilotedShipId
? findRequired(this.state.ships, this.state.player.pilotedShipId, 'Piloted ship').transform.position
: this.state.player.transform.position;
if (distance(focus, region.center) > region.radiusM + 50_000) return { ok: false, message: `${region.name} is outside sensor range.`, eventIds: [] };
region.discovered = true;
if (!this.state.player.discoveredLocationIds.includes(region.id)) this.state.player.discoveredLocationIds.push(region.id);
const event = emitWorldEvent(this.state, 'planetary-region-discovered', this.state.player.id, region.id, `${region.name} was charted.`, { persistent: true });
return { ok: true, message: event.summary, eventIds: [event.id] };
}
tick(deltaMs: number): void {
if (deltaMs <= 0 || this.state.tick % 600 !== 0) return;
for (const region of this.state.planetaryRegions) this.advanceWeather(region);
const shipId = this.state.player.pilotedShipId;
if (!shipId) return;
const ship = findRequired(this.state.ships, shipId, 'Piloted ship');
const nearestBody = this.state.celestialBodies
.filter((body) => body.kind === 'planet' || body.kind === 'moon')
.sort((left, right) => distance(ship.transform.position, left.position) - distance(ship.transform.position, right.position))[0];
if (!nearestBody) return;
const sample = this.sampleAtmosphere(nearestBody.id, ship.transform.position);
if (sample.altitudeM <= nearestBody.atmosphereHeightM && sample.density > 0.001) {
this.state.player.layer = 'planetary-space';
const drag = clamp(sample.density * deltaMs / 50_000, 0, 0.2);
ship.transform.velocity.x *= 1 - drag;
ship.transform.velocity.y *= 1 - drag;
ship.transform.velocity.z *= 1 - drag;
}
}
private advanceWeather(region: PlanetaryRegionState): void {
const phase = hashString(`${this.state.seed}:${region.id}:${Math.floor(this.state.tick / 600)}`) % 100;
const condition: WeatherState['condition'] = phase < 50 ? 'clear' : phase < 70 ? 'cloudy' : phase < 85 ? 'dust' : phase < 96 ? 'storm' : 'electrical';
region.weather.condition = condition;
region.weather.windMps = vector((phase % 13) - 6, 0, ((phase * 7) % 11) - 5);
region.weather.precipitation = condition === 'storm' ? 0.8 : condition === 'cloudy' ? 0.2 : 0;
region.weather.visibilityM = condition === 'clear' ? 25_000 : condition === 'cloudy' ? 9_000 : condition === 'dust' ? 3_000 : 1_500;
}
private terrainMaterial(region: PlanetaryRegionState, elevationM: number): TerrainSample['material'] {
if (elevationM <= region.oceanLevelM) return 'water';
if (region.planetId === 'moon-nacre') return elevationM > 80 ? 'ice' : 'regolith';
return elevationM < 25 ? 'vegetation' : 'rock';
}
}