// main.js — game glue: modes, input, flight, FP walk, mining, goal arc, debug hooks.
import * as THREE from 'three';
import { RNG, gameName, dotCanvas } from './util.js';
import { makePlanet } from './palette.js';
import { makeShip } from './ship.js';
import { Space } from './space.js';
import { Surface } from './surface.js';
import { Scanner } from './scanner.js';
import { Post } from './post.js';
import { HUD } from './hud.js';

const SCAN_TARGET = 5;
const MINE_TARGET = 40;

class Game {
  constructor() {
    const canvas = document.getElementById('app');
    this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
    this.renderer.setPixelRatio(Math.min(devicePixelRatio, 1.6));
    this.renderer.setSize(innerWidth, innerHeight);
    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer.toneMappingExposure = 1.05;
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    // ---- world: a system of 3 distinct planets ----
    const sysSeed = 70742;
    this.gameTitle = gameName(new RNG(sysSeed));
    // assign distinct biome templates to the first two landed worlds for contrast
    this.planetDescs = [
      makePlanet(sysSeed + 1, 0),   // coral-bio
      makePlanet(sysSeed + 2, 4),   // ember-volcanic
      makePlanet(sysSeed + 3, 2),   // frozen (sky companion)
    ];

    // ---- cameras ----
    this.spaceCam = new THREE.PerspectiveCamera(62, innerWidth / innerHeight, 0.5, 16000);
    this.surfaceCam = new THREE.PerspectiveCamera(72, innerWidth / innerHeight, 0.1, 4000);

    // ---- ship ----
    this.ship = makeShip();

    // ---- space scene ----
    this.space = new Space(this.planetDescs, this.ship);

    // ---- post ----
    this.post = new Post(this.renderer, this.space.scene, this.spaceCam);

    // ---- hud ----
    this.hud = new HUD(document.getElementById('hud'));

    // ---- state ----
    this.mode = 'space';
    this.phase = 'flight';
    this.units = 0;
    this.mined = 0;
    this.scannedSpecies = new Set();
    this.visited = new Set();
    this.currentPlanet = -1;
    this.surfaces = {};   // cache by index
    this.entry = null;
    this.won = false;

    this.clock = new THREE.Clock();
    this.input = { yaw: 0, pitch: 0, thrust: false, boost: false, fwd: false, back: false, left: false, right: false, jump: false, run: false, mine: false, scan: false };
    this.player = { pos: new THREE.Vector3(), vel: new THREE.Vector3(), yaw: 0, pitch: 0, onGround: true };
    this.frozenCam = false;

    this.initInput();
    this.initMining();
    this.exposeApi();

    this.hud.setMode('SPACE', 'flight');
    this.hud.setResource('Crystallite', 0);
    this.hud.setGoal(0, SCAN_TARGET, 0, MINE_TARGET);
    document.title = this.gameTitle;

    addEventListener('resize', () => this.onResize());
    this.onResize();
    this.loop();
  }

  // ---------------- input ----------------
  initInput() {
    const canvas = this.renderer.domElement;
    canvas.addEventListener('click', () => { if (!this.frozenCam) canvas.requestPointerLock(); });
    addEventListener('mousemove', (e) => {
      if (document.pointerLockElement !== canvas) return;
      const sx = 0.0022, sy = 0.0022;
      if (this.mode === 'space') {
        this.input.yaw += -e.movementX * sx * 30;
        this.input.pitch += -e.movementY * sy * 30;
      } else if (this.mode === 'surface') {
        this.player.yaw -= e.movementX * sx;
        this.player.pitch = THREE.MathUtils.clamp(this.player.pitch - e.movementY * sy, -1.4, 1.4);
      }
    });
    addEventListener('mousedown', (e) => { if (e.button === 0) this.input.mine = true; });
    addEventListener('mouseup', (e) => { if (e.button === 0) this.input.mine = false; });
    const set = (e, v) => {
      switch (e.code) {
        case 'KeyW': this.input.fwd = v; this.input.thrust = v; break;
        case 'KeyS': this.input.back = v; break;
        case 'KeyA': this.input.left = v; break;
        case 'KeyD': this.input.right = v; break;
        case 'ShiftLeft': case 'ShiftRight': this.input.boost = v; this.input.run = v; break;
        case 'Space': this.input.jump = v; if (v) e.preventDefault(); break;
        case 'KeyC': this.input.scan = v; if (v) this.tryScan(); break;
        case 'KeyE': if (v) this.tryAction(); break;
      }
    };
    addEventListener('keydown', (e) => set(e, true));
    addEventListener('keyup', (e) => set(e, false));
  }

  tryAction() {
    if (this.mode === 'space') {
      const { planet, dist } = this.space.nearest(this.spaceCam.position);
      if (dist < 900) this.startEntry(this.space.planetIndex(planet));
    } else if (this.mode === 'surface') {
      // near ship => take off
      if (this.ship.group.position.distanceTo(this.player.pos) < 7) this.takeoff();
    }
  }

  // ---------------- mining ----------------
  initMining() {
    this.ray = new THREE.Raycaster();
    this.ray.far = 40;
    // beam
    const beamMat = new THREE.MeshBasicMaterial({ color: 0xff9a4a, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false });
    this.beam = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.12, 1, 8), beamMat);
    this.beam.visible = false;
    // impact glow
    this.impact = new THREE.Mesh(new THREE.SphereGeometry(0.4, 12, 8), new THREE.MeshBasicMaterial({ color: 0xffd27a, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false }));
    this.impact.visible = false;
    // sparks
    const N = 240;
    this.sparkPos = new Float32Array(N * 3);
    this.sparkData = Array.from({ length: N }, () => ({ life: 0, vx: 0, vy: 0, vz: 0 }));
    const sg = new THREE.BufferGeometry();
    sg.setAttribute('position', new THREE.BufferAttribute(this.sparkPos, 3));
    this.sparks = new THREE.Points(sg, new THREE.PointsMaterial({ map: new THREE.CanvasTexture(dotCanvas()), color: 0xffd27a, size: 0.45, transparent: true, opacity: 0.95, blending: THREE.AdditiveBlending, depthWrite: false }));
    this.sparks.frustumCulled = false;
    this.sparkHead = 0;
    this.mineTarget = null;
    this.mineHeat = 0;
  }

  spawnSparks(p, n, color) {
    for (let i = 0; i < n; i++) {
      const idx = this.sparkHead;
      this.sparkHead = (this.sparkHead + 1) % this.sparkData.length;
      const d = this.sparkData[idx];
      d.life = 0.5 + Math.random() * 0.4;
      d.vx = (Math.random() - 0.5) * 10; d.vy = Math.random() * 9; d.vz = (Math.random() - 0.5) * 10;
      this.sparkPos[idx * 3] = p.x; this.sparkPos[idx * 3 + 1] = p.y; this.sparkPos[idx * 3 + 2] = p.z;
    }
    if (color) this.sparks.material.color.copy(color);
  }

  updateMining(dt, surf) {
    const cam = this.surfaceCam;
    const dir = new THREE.Vector3(); cam.getWorldDirection(dir);
    let hit = null;
    if (this.input.mine && surf) {
      this.ray.set(cam.position, dir);
      const res = this.ray.intersectObject(surf.crystals.inst);
      if (res.length) {
        const node = surf.crystals.nodes.find((n) => n.idx === res[0].instanceId && n.alive);
        if (node) hit = { node, point: res[0].point };
      }
    }
    if (hit) {
      // beam from gun to hit
      const gun = cam.position.clone().add(dir.clone().multiplyScalar(0.6)).add(new THREE.Vector3(0, -0.35, 0));
      const mid = gun.clone().lerp(hit.point, 0.5);
      const len = gun.distanceTo(hit.point);
      this.beam.visible = true;
      this.beam.position.copy(mid);
      this.beam.scale.set(1, len, 1);
      this.beam.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), hit.point.clone().sub(gun).normalize());
      this.impact.visible = true;
      this.impact.position.copy(hit.point);
      this.impact.scale.setScalar(0.7 + Math.sin(this.clock.elapsedTime * 40) * 0.2);
      this.mineHeat += dt;
      if (Math.random() < 0.6) this.spawnSparks(hit.point, 3, surf.planet.colors.crystal);
      if (this.mineHeat > 0.5) {
        this.mineHeat = 0;
        const got = hit.node.ore;
        surf.crystals.shatter(hit.node);
        this.spawnSparks(hit.point, 40, surf.planet.colors.crystal);
        this.mined = Math.min(MINE_TARGET, this.mined + got);
        this.units += got * 2;
        this.refreshHud();
        this.checkRepair();
      }
    } else {
      this.beam.visible = false;
      this.impact.visible = false;
      this.mineHeat = 0;
    }
    // update sparks
    const sp = this.sparkPos;
    for (let i = 0; i < this.sparkData.length; i++) {
      const d = this.sparkData[i];
      if (d.life > 0) {
        d.life -= dt;
        d.vy -= 12 * dt;
        sp[i * 3] += d.vx * dt; sp[i * 3 + 1] += d.vy * dt; sp[i * 3 + 2] += d.vz * dt;
      } else { sp[i * 3 + 1] = -9999; }
    }
    this.sparks.geometry.attributes.position.needsUpdate = true;
  }

  // ---------------- scanning ----------------
  tryScan() {
    if (this.mode !== 'surface') return;
    const surf = this.surfaces[this.currentPlanet];
    if (!surf || !surf.scanner) return;
    surf.scanner.trigger(this.player.pos.clone());
  }

  // ---------------- entry / landing ----------------
  startEntry(index) {
    if (this.mode !== 'space' || index < 0) return;
    const pd = this.planetDescs[index];
    this.entry = { t: 0, dur: 3.2, index };
    this.mode = 'entry';
    this.phase = 'entry';
    const e = this.space.entryMat.uniforms;
    e.uColor.value.copy(pd.colors.skyHorizon);
    e.uColor2.value.copy(pd.colors.skyTop);
    this.hud.setMode('ENTRY', 'atmospheric descent');
    this.hud.hideHint();
    this.space.entryStart = this.space.rig.position.clone();
    this.space.entryTarget = this.space.planets[index].group.position.clone();
  }

  updateEntry(dt) {
    const e = this.entry;
    e.t += dt;
    const p = THREE.MathUtils.clamp(e.t / e.dur, 0, 1);
    // dive the rig toward the planet
    const pl = this.space.planets[e.index];
    const toPlanet = pl.group.position.clone().sub(this.space.rig.position).normalize();
    this.space.vel.lerp(toPlanet.multiplyScalar(700), dt * 2);
    this.space.rig.position.addScaledVector(this.space.vel, dt);
    this.space.boost = 1;
    this.ship.setBoost(1);
    // camera chase + shake
    const q = this.space.rig.quaternion;
    const camOff = new THREE.Vector3(0, 1.4, 6.5).applyQuaternion(q);
    this.spaceCam.position.copy(this.space.rig.position).add(camOff);
    const shake = p * 0.5;
    this.spaceCam.position.add(new THREE.Vector3((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake, 0));
    this.spaceCam.lookAt(pl.group.position);
    this.space.updateSpeedLines(dt, this.spaceCam);
    this.space.speed.material.opacity = 0.85;
    // overlay wash ramps up
    this.space.entryMat.uniforms.uAmt.value = THREE.MathUtils.smoothstep(p, 0.1, 0.95);
    this.space.entryMat.uniforms.uTime.value += dt;
    this.space.planets.forEach((pp) => pp.update(dt));
    // flash near the end
    if (p > 0.86) this.hud.doFlash((p - 0.86) / 0.14);
    if (p >= 1) this.finalizeLand(e.index);
  }

  getSurface(index) {
    if (!this.surfaces[index]) {
      const others = this.planetDescs.filter((_, i) => i !== index);
      const surf = new Surface(this.planetDescs[index], others);
      surf.scanner = new Scanner(surf.scene);
      surf.scanner.setColor(this.planetDescs[index].colors.crystal);
      surf.scene.add(this.beam);
      surf.scene.add(this.impact);
      surf.scene.add(this.sparks);
      this.surfaces[index] = surf;
    }
    return this.surfaces[index];
  }

  finalizeLand(index) {
    const surf = this.getSurface(index);
    // move mining helpers into this scene
    [this.beam, this.impact, this.sparks].forEach((o) => { if (o.parent !== surf.scene) surf.scene.add(o); });
    this.currentPlanet = index;
    this.mode = 'surface';
    this.phase = 'surface';
    this.entry = null;
    this.won && (this.won = this.won);
    // park ship at origin
    this.ship.group.parent && this.ship.group.parent.remove(this.ship.group);
    surf.setShip(this.ship.group);
    const h = surf.heightAt(0, 0);
    this.ship.group.position.set(0, h + 1.35, 0);
    this.ship.group.rotation.set(0, Math.PI * 0.15, 0);
    this.ship.group.scale.setScalar(1.25);
    this.ship.setBoost(0);
    // place player just outside the ship
    this.player.pos.set(5, surf.heightAt(5, 6) + 1.7, 6);
    this.player.vel.set(0, 0, 0);
    this.player.yaw = Math.PI; // face back toward ship / outward
    this.player.pitch = -0.05;
    this.player.onGround = true;
    // reset overlay + speed
    this.space.entryMat.uniforms.uAmt.value = 0;
    this.space.speed.material.opacity = 0;
    this.hud.doFlash(0);
    this.renderer.toneMappingExposure = 0.95;
    this.post.setScene(surf.scene, this.surfaceCam);
    this.post.configure({ strength: 0.5, radius: 0.62, threshold: 0.9 });
    this.hud.setMode('SURFACE', this.planetDescs[index].biome);
    this.hud.setResource('Crystallite', this.mined);
    this.hud.showBanner(this.planetDescs[index].name, this.planetDescs[index].biome);
    this.visited.add(index);
    this.checkWin();
  }

  takeoff() {
    if (this.mode !== 'surface') return;
    const surf = this.surfaces[this.currentPlanet];
    if (surf && surf.scanner) surf.scanner.clear();
    // reattach ship to flight rig
    this.ship.group.parent && this.ship.group.parent.remove(this.ship.group);
    this.ship.group.position.set(0, 0, 0);
    this.ship.group.rotation.set(0, 0, 0);
    this.space.rig.add(this.ship.group);
    // place rig just off the planet we left
    const pl = this.space.planets[this.currentPlanet];
    const out = new THREE.Vector3(0, 0.3, 1).applyQuaternion(this.spaceCam.quaternion);
    this.space.rig.position.copy(pl.group.position).add(new THREE.Vector3(pl.radius * 1.8, pl.radius * 0.6, pl.radius * 1.8));
    this.space.rig.lookAt(pl.group.position.clone().add(new THREE.Vector3(2000, 300, 2000)));
    this.space.vel.set(0, 0, 0);
    this.mode = 'space';
    this.phase = 'flight';
    this.renderer.toneMappingExposure = 1.08;
    this.post.setScene(this.space.scene, this.spaceCam);
    this.post.configure({ strength: 0.6, radius: 0.7, threshold: 0.85 });
    this.hud.setMode('SPACE', 'flight');
  }

  // ---------------- goal arc ----------------
  refreshHud() {
    this.hud.setUnits(this.units);
    this.hud.setResource(this.currentPlanet >= 0 ? this.planetDescs[this.currentPlanet].mineralName : 'Crystallite', this.mined);
    this.hud.setGoal(this.scannedSpecies.size, SCAN_TARGET, this.mined, MINE_TARGET);
  }
  onDiscover(item) {
    this.units += item.value;
    if (item.type === 'flora') this.scannedSpecies.add(item.name);
    this.refreshHud();
    this.checkRepair();
  }
  checkRepair() {
    if (!this.repaired && this.scannedSpecies.size >= SCAN_TARGET && this.mined >= MINE_TARGET) {
      this.repaired = true;
      this.hud.showHint('◊ HYPERDRIVE REPAIRED — return to ship and <b>take off</b>');
      setTimeout(() => this.mode === 'surface' && this.hud.hideHint(), 5000);
    }
  }
  checkWin() {
    if (!this.won && this.repaired && this.visited.size >= 2) {
      this.won = true;
      this.phase = 'win';
      setTimeout(() => this.hud.showWin({ planets: this.visited.size, species: this.scannedSpecies.size, units: this.units }), 1600);
    }
  }

  // ---------------- surface FP controller ----------------
  updateSurface(dt, surf) {
    const p = this.player;
    // look
    const cam = this.surfaceCam;
    cam.rotation.order = 'YXZ';
    cam.rotation.y = p.yaw;
    cam.rotation.x = p.pitch;
    // movement basis
    const fwd = new THREE.Vector3(-Math.sin(p.yaw), 0, -Math.cos(p.yaw));
    const right = new THREE.Vector3(Math.cos(p.yaw), 0, -Math.sin(p.yaw));
    const speed = (this.input.run ? 17 : 8);
    const move = new THREE.Vector3();
    if (this.input.fwd) move.add(fwd);
    if (this.input.back) move.addScaledVector(fwd, -1);
    if (this.input.right) move.add(right);
    if (this.input.left) move.addScaledVector(right, -1);
    if (move.lengthSq() > 0) move.normalize().multiplyScalar(speed);
    p.vel.x = move.x; p.vel.z = move.z;
    // gravity / jump
    const g = surf.planet.gravity * 16;
    const groundY = surf.heightAt(p.pos.x, p.pos.z) + 1.7;
    if (p.onGround && this.input.jump) { p.vel.y = 9.5; p.onGround = false; }
    p.vel.y -= g * dt;
    p.pos.addScaledVector(p.vel, dt);
    if (p.pos.y <= groundY) { p.pos.y = groundY; p.vel.y = 0; p.onGround = true; }
    // clamp to area
    const lim = surf.size * 0.46;
    p.pos.x = THREE.MathUtils.clamp(p.pos.x, -lim, lim);
    p.pos.z = THREE.MathUtils.clamp(p.pos.z, -lim, lim);
    cam.position.copy(p.pos);

    this.updateMining(dt, surf);
    surf.scanner.update(dt, surf.scannables, (it) => this.onDiscover(it));
    surf.update(dt, p.pos);

    // contextual hints
    if (!this.won) {
      if (this.ship.group.position.distanceTo(p.pos) < 7) {
        this.hud.showHint(this.repaired ? '◊ press <b>E</b> to take off' : 'press <b>E</b> to board · drive not yet repaired');
      } else {
        this.hud.showHint('<b>C</b> scan · hold <b>LMB</b> mine · <b>SHIFT</b> run · <b>SPACE</b> jump');
      }
    }
  }

  // ---------------- space update ----------------
  updateSpace(dt) {
    this.space.update(dt, this.input, this.spaceCam);
    // decay steering inputs
    this.input.yaw *= Math.pow(0.0001, dt);
    this.input.pitch *= Math.pow(0.0001, dt);
    const { planet, dist } = this.space.nearest(this.spaceCam.position);
    if (dist < 900) {
      this.hud.showHint(`approaching <b>${this.planetDescs[this.space.planetIndex(planet)].name}</b> · press <b>E</b> to descend`);
    } else { this.hud.hideHint(); }
  }

  // ---------------- main loop ----------------
  loop() {
    const dt = Math.min(this.clock.getDelta(), 0.05);
    if (this.frozenCam) {
      // static screenshot vantage: keep shaders ticking but don't drive logic
      if (this.mode === 'surface') this.surfaces[this.currentPlanet].update(dt, this.surfaceCam.position);
      else this.space.planets.forEach((p) => p.update(dt));
    } else if (this.mode === 'space') this.updateSpace(dt);
    else if (this.mode === 'entry') this.updateEntry(dt);
    else if (this.mode === 'surface') this.updateSurface(dt, this.surfaces[this.currentPlanet]);

    const activeScene = this.mode === 'surface' ? this.surfaces[this.currentPlanet].scene : this.space.scene;
    const activeCam = this.mode === 'surface' ? this.surfaceCam : this.spaceCam;
    this.post.setScene(activeScene, activeCam);
    this.post.render(dt);
    this._frames = (this._frames || 0) + 1;
    requestAnimationFrame(() => this.loop());
  }

  onResize() {
    const w = innerWidth, h = innerHeight;
    this.renderer.setSize(w, h);
    this.post.setSize(w, h);
    this.spaceCam.aspect = w / h; this.spaceCam.updateProjectionMatrix();
    this.surfaceCam.aspect = w / h; this.surfaceCam.updateProjectionMatrix();
  }

  // ---------------- public/debug api ----------------
  exposeApi() {
    const g = this;
    window.__game = {
      get phase() { return g.phase; },
      get mode() { return g.mode; },
      get planet() { return g.currentPlanet >= 0 ? g.planetDescs[g.currentPlanet].name : null; },
      get units() { return g.units; },
      get scans() { return g.scannedSpecies.size; },
      get resources() { return g.mined; },
      land() { const n = g.space.nearest(g.spaceCam.position); g.startEntry(g.space.planetIndex(n.planet)); },
      takeoff() { g.takeoff(); },
      teleportPlanet(i) { g.frozenCam = false; if (g.mode === 'surface') { /* leave */ } g.finalizeLand(i); },
      // debug / screenshot harness
      _title: g.gameTitle,
      _descs: g.planetDescs.map((d) => ({ name: d.name, biome: d.biome, key: d.key })),
      preset(name, i = 0) { g.preset(name, i); },
      forceProgress() { g.scannedSpecies = new Set(['a', 'b', 'c', 'd', 'e']); g.mined = MINE_TARGET; g.repaired = true; g.refreshHud(); },
      // --- live smoke-test helpers ---
      unfreeze() { g.frozenCam = false; },
      aimCrystal() {
        const surf = g.surfaces[g.currentPlanet];
        if (!surf) return false;
        let best = null, bd = Infinity;
        for (const n of surf.crystals.nodes) {
          if (!n.alive) continue;
          const d = n.pos.distanceTo(g.player.pos);
          if (d < bd) { bd = d; best = n; }
        }
        if (!best) return false;
        const dir = new THREE.Vector3().subVectors(g.player.pos, best.pos); dir.y = 0; dir.normalize();
        const stand = best.pos.clone().addScaledVector(dir, 6.5);
        stand.y = surf.heightAt(stand.x, stand.z) + 1.7;
        g.player.pos.copy(stand);
        const to = new THREE.Vector3().subVectors(best.pos, stand);
        g.player.yaw = Math.atan2(-to.x, -to.z);
        g.player.pitch = Math.atan2(to.y, Math.hypot(to.x, to.z));
        g.frozenCam = false;
        return true;
      },
    };
  }

  // canonical screenshot vantages
  preset(name, idx = 0) {
    this.frozenCam = true;
    if (name === 'space') {
      this.mode = 'space'; this.phase = 'flight';
      this.renderer.toneMappingExposure = 1.08;
      this.post.setScene(this.space.scene, this.spaceCam);
      this.post.configure({ strength: 0.6, radius: 0.7, threshold: 0.85 });
      const pl = this.space.planets[idx];
      const pPos = pl.group.position;
      // camera back from the planet; planet then pushed right-of-centre revealing starfield
      const camPos = pPos.clone().add(new THREE.Vector3(pl.radius * 1.9, pl.radius * 0.4, pl.radius * 3.9));
      this.spaceCam.position.copy(camPos);
      this.spaceCam.lookAt(pPos);
      this.spaceCam.rotateY(0.26);
      this.spaceCam.rotateX(0.04);
      this.spaceCam.updateMatrixWorld();
      // park the ship in the lower foreground, banking toward the planet
      this.ship.group.scale.setScalar(1.35);
      const fwd = pPos.clone().sub(camPos).normalize();
      const right = new THREE.Vector3().crossVectors(fwd, new THREE.Vector3(0, 1, 0)).normalize();
      this.space.rig.position.copy(camPos).addScaledVector(fwd, 40).addScaledVector(right, 11).add(new THREE.Vector3(0, -8, 0));
      this.space.rig.lookAt(pPos);
      this.space.rig.rotateZ(-0.25);
      this.space.vel.set(0, 0, 0); this.space.boost = 0; this.ship.setBoost(0.25);
      this.space.entryMat.uniforms.uAmt.value = 0;
      this.space.speed.material.opacity = 0;
      this.hud.setMode('SPACE', 'flight');
      this.hud.showHint(`approaching <b>${this.planetDescs[idx].name}</b> · press <b>E</b> to descend`);
    } else if (name === 'entry') {
      this.mode = 'entry'; this.phase = 'entry';
      this.renderer.toneMappingExposure = 1.1;
      this.post.setScene(this.space.scene, this.spaceCam);
      this.post.configure({ strength: 0.7, radius: 0.72, threshold: 0.78 });
      const pd = this.planetDescs[idx];
      const pl = this.space.planets[idx];
      const P = pl.group.position, R = pl.radius;
      // camera skimming just above the planet, looking along the tangent so the
      // glowing limb curves across the upper frame and surface fills below
      const outward = new THREE.Vector3(0.25, 0.5, 0.86).normalize();
      const camPos = P.clone().addScaledVector(outward, R * 1.14);
      const centerDir = outward.clone().negate();
      const right = new THREE.Vector3().crossVectors(centerDir, new THREE.Vector3(0, 1, 0)).normalize();
      const lookDir = centerDir.clone().applyQuaternion(new THREE.Quaternion().setFromAxisAngle(right, 0.84)).normalize();
      this.spaceCam.position.copy(camPos);
      this.spaceCam.lookAt(camPos.clone().add(lookDir));
      // ship diving in the lower foreground, engines toward camera
      this.ship.group.scale.setScalar(1.7);
      this.space.rig.position.copy(camPos).addScaledVector(lookDir, 34).addScaledVector(right, 5).add(new THREE.Vector3(0, -8, 0));
      this.space.rig.lookAt(P);
      this.ship.setBoost(1);
      const e = this.space.entryMat.uniforms;
      e.uColor.value.copy(pd.colors.skyHorizon); e.uColor2.value.copy(pd.colors.skyTop);
      e.uAmt.value = 0.5;
      this.space.boost = 1; this.space.vel.copy(lookDir).multiplyScalar(500);
      this.space.updateSpeedLines(0.016, this.spaceCam);
      this.space.speed.material.opacity = 0.8;
      this.hud.setMode('ENTRY', 'atmospheric descent');
      this.hud.hideHint();
    } else if (name === 'surface' || name === 'scan' || name === 'planet2') {
      const i = name === 'planet2' ? 1 : idx;
      this.finalizeLand(i);
      this.frozenCam = true;
      this.renderer.toneMappingExposure = 1.0;
      const surf = this.surfaces[i];
      // look ACROSS the terrain with the sun off to one side (not into it)
      const az = Math.atan2(surf.sunDir.z, surf.sunDir.x);
      const viewAz = az - 1.05;                 // ~60deg off-sun
      const Dx = Math.cos(viewAz), Dz = Math.sin(viewAz);  // view forward (horizontal)
      const dist = 27;
      const camX = -Dx * dist, camZ = -Dz * dist;          // camera behind origin so ship is ahead
      const cy = surf.heightAt(camX, camZ) + 6.5;
      this.surfaceCam.position.set(camX, cy, camZ);
      this.player.pos.copy(this.surfaceCam.position);
      this.player.yaw = Math.atan2(-Dx, -Dz);
      this.player.pitch = -0.10;
      this.surfaceCam.rotation.order = 'YXZ';
      this.surfaceCam.rotation.set(this.player.pitch, this.player.yaw, 0);
      // turn the parked ship to present its profile to camera
      this.ship.group.rotation.set(0, viewAz + Math.PI * 0.5, 0);
      if (name === 'scan') {
        const cam = this.surfaceCam;
        // aim at the densest nearby flora cluster so several plates frame up
        const near = surf.flora.scannables.filter((s) => s.pos.distanceTo(cam.position) < 55);
        if (near.length) {
          const c = new THREE.Vector3();
          near.forEach((s) => c.add(s.pos));
          c.multiplyScalar(1 / near.length);
          this.player.yaw = Math.atan2(-(c.x - cam.position.x), -(c.z - cam.position.z));
          this.player.pitch = -0.06;
          cam.rotation.set(this.player.pitch, this.player.yaw, 0);
        }
        surf.scanner.clear();
        surf.scanner.trigger(cam.position.clone().add(new THREE.Vector3(0, -1.0, 0)));
        for (let k = 0; k < 32; k++) surf.scanner.update(0.02, surf.scannables, (it) => this.onDiscover(it));
      }
    }
  }
}

new Game();
