snapkitty
hardware
erlang
File size: 6,768 Bytes
b8a03fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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';
  }
}