// Moon Rover level — gentle open driving on a realistic-feeling lunar plain.
// Three.js render loop with a React DOM HUD overlay.

const {
  useState: _mrUseState,
  useEffect: _mrUseEffect,
  useRef: _mrUseRef,
} = React;

const mrUseState = _mrUseState;
const mrUseEffect = _mrUseEffect;
const mrUseRef = _mrUseRef;

const MOON_ROVER_SAMPLES = [
  {
    id: "basalt",
    label: "Moon Rock",
    fact: "This Moon rock was once melted lava.",
    color: "#ffd86b",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-basalt.png",
    x: -14,
    z: -18,
  },
  {
    id: "ice",
    label: "Ice Sparkle",
    fact: "Ice can hide in cold, dark craters.",
    color: "#7ee7ff",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-ice.png",
    x: 18,
    z: -12,
  },
  {
    id: "dust",
    label: "Moon Dust",
    fact: "Moon dust is powdery rock called regolith.",
    color: "#ff9f7a",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-dust.png",
    x: -22,
    z: 12,
  },
  {
    id: "glass",
    label: "Glass Bead",
    fact: "Fast space rocks can melt dust into glass beads.",
    color: "#b8ff9a",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-glass.png",
    x: 10,
    z: 22,
  },
  {
    id: "print",
    label: "Track Print",
    fact: "No wind or rain means tracks can stay a long time.",
    color: "#f4f1e8",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-anorthosite.png",
    x: 27,
    z: 9,
  },
  {
    id: "star",
    label: "Star Pebble",
    fact: "Space rocks hit the Moon and make craters.",
    color: "#c9a7ff",
    image: "data/generated-assets/moon-rover/rocks/moon-rock-meteorite.png",
    x: -7,
    z: 29,
  },
];

const MOON_ROVER_WORLD_LIMIT = 42;
const MOON_ROVER_WORLD_SIZE = MOON_ROVER_WORLD_LIMIT * 2;
const MOON_ROVER_RADIUS = 260;
const MOON_ROVER_BODY_CLEARANCE = 3.2;
const MOON_ROVER_RAMPS = [
  {
    x: -7,
    z: 4,
    heading: 0.72,
    length: 7.2,
    width: 3.6,
    height: 1.28,
    color: 0x9b9a94,
  },
  {
    x: 14,
    z: 12,
    heading: -0.88,
    length: 6.4,
    width: 3.3,
    height: 1.05,
    color: 0xaaa79d,
  },
  {
    x: -18,
    z: 23,
    heading: 1.48,
    length: 6.8,
    width: 3.4,
    height: 1.18,
    color: 0x8f8e88,
  },
  {
    x: 24,
    z: -8,
    heading: -1.2,
    length: 8.4,
    width: 4.1,
    height: 1.54,
    color: 0xa4a096,
  },
  {
    x: -29,
    z: -17,
    heading: 0.35,
    length: 7.8,
    width: 3.8,
    height: 1.36,
    color: 0x8c8c87,
  },
  {
    x: 4,
    z: 32,
    heading: 2.24,
    length: 6.9,
    width: 3.4,
    height: 1.22,
    color: 0xb1aca1,
  },
];
const MOON_ROVER_CRATERS = [
  { x: -18, z: -7, r: 8.8, d: 1.25, skew: 1.06 },
  { x: 20, z: 18, r: 10.5, d: 1.55, skew: 0.92 },
  { x: 2, z: -26, r: 7.4, d: 1.05, skew: 1.14 },
  { x: -31, z: 23, r: 8.2, d: 1.1, skew: 0.88 },
  { x: 34, z: -18, r: 7.6, d: 1.0, skew: 1.1 },
  { x: -6, z: 31, r: 4.2, d: 0.56, skew: 0.96 },
  { x: 29, z: 5, r: 3.7, d: 0.48, skew: 1.18 },
  { x: -27, z: -29, r: 4.8, d: 0.64, skew: 0.9 },
];

// Periodic curve-sag: smooth cosine that peaks at the wrap boundaries and
// returns to zero at center. Same maximum drop as the original quadratic
// (≈3.4 m at edge) but fully continuous across wrap so terrain meshes
// stitch up without a ridge at x = ±MOON_ROVER_WORLD_LIMIT.
function moonRoverCurveSag(x, z) {
  const sx = (1 - Math.cos((2 * Math.PI * x) / MOON_ROVER_WORLD_SIZE)) * 0.5;
  const sz = (1 - Math.cos((2 * Math.PI * z) / MOON_ROVER_WORLD_SIZE)) * 0.5;
  return (
    ((sx + sz) * (MOON_ROVER_WORLD_LIMIT * MOON_ROVER_WORLD_LIMIT)) /
    (4 * MOON_ROVER_RADIUS)
  );
}

// Periodic noise frequencies: each base frequency is snapped to the nearest
// integer number of cycles inside a single MOON_ROVER_WORLD_SIZE tile so the
// noise wraps perfectly across the seam. Differences from the originals are
// tiny (worst case ~6%); kids will not see it.
const TWO_PI_OVER_WS = (2 * Math.PI) / MOON_ROVER_WORLD_SIZE;
const _periodicFreq = (f) =>
  Math.max(1, Math.round((f * MOON_ROVER_WORLD_SIZE) / (2 * Math.PI))) *
  TWO_PI_OVER_WS;
const MR_F1 = _periodicFreq(0.32);
const MR_F2 = _periodicFreq(0.27);
const MR_F3 = _periodicFreq(0.13);
const MR_F4 = _periodicFreq(0.2);
const MR_F5x = _periodicFreq(1.9);
const MR_F5z = _periodicFreq(0.55);
const MR_F6x = _periodicFreq(0.38);
const MR_F6z = _periodicFreq(2.15);

function moonRoverWrapDelta(from, to) {
  let delta = (from - to) % MOON_ROVER_WORLD_SIZE;
  if (delta > MOON_ROVER_WORLD_LIMIT) delta -= MOON_ROVER_WORLD_SIZE;
  if (delta < -MOON_ROVER_WORLD_LIMIT) delta += MOON_ROVER_WORLD_SIZE;
  return delta;
}

function moonRoverRampAt(x, z) {
  let best = { height: 0, slope: 0, ramp: null, localZ: 0 };
  for (const ramp of MOON_ROVER_RAMPS) {
    const dx = moonRoverWrapDelta(x, ramp.x);
    const dz = moonRoverWrapDelta(z, ramp.z);
    const sin = Math.sin(ramp.heading);
    const cos = Math.cos(ramp.heading);
    const localX = dx * cos - dz * sin;
    const localZ = dx * sin + dz * cos;
    const halfLength = ramp.length * 0.5;
    const halfWidth = ramp.width * 0.5;
    if (
      Math.abs(localX) > halfWidth ||
      localZ < -halfLength ||
      localZ > halfLength
    )
      continue;

    const t = (localZ + halfLength) / ramp.length;
    const sideFade = Math.max(0, 1 - Math.pow(Math.abs(localX) / halfWidth, 4));
    const eased = t * t * (3 - 2 * t);
    const height = eased * ramp.height * sideFade;
    if (height > best.height) {
      best = {
        height,
        slope: (ramp.height / ramp.length) * sideFade,
        ramp,
        localZ,
      };
    }
  }
  return best;
}

function moonRoverHeight(x, z) {
  let y =
    Math.sin(x * MR_F1) * 0.22 +
    Math.cos(z * MR_F2) * 0.18 +
    Math.sin((x + z) * MR_F3) * 0.32 +
    Math.cos((x - z) * MR_F4) * 0.14 +
    Math.sin(x * MR_F5x + z * MR_F5z) * 0.035 +
    Math.cos(z * MR_F6z - x * MR_F6x) * 0.028;
  for (const c of MOON_ROVER_CRATERS) {
    const dx = moonRoverWrapDelta(x, c.x);
    const dz = moonRoverWrapDelta(z, c.z) * c.skew;
    const dist = Math.sqrt(dx * dx + dz * dz);
    if (dist < c.r) {
      const t = dist / c.r;
      y -= Math.cos(t * Math.PI * 0.5) * c.d * (1 - t * 0.12);
      if (t > 0.58) {
        const rim = Math.sin((t - 0.58) * Math.PI * 2.65);
        y += Math.max(0, rim) * c.d * 0.42;
      }
      if (t < 0.34) y += Math.sin((dx + dz) * 1.4) * c.d * 0.025;
    }
  }
  return y - moonRoverCurveSag(x, z);
}

function moonRoverGroundHeight(x, z) {
  return moonRoverHeight(x, z) + moonRoverRampAt(x, z).height;
}

function moonRoverNormal(x, z, out) {
  const step = 0.55;
  const left = moonRoverGroundHeight(x - step, z);
  const right = moonRoverGroundHeight(x + step, z);
  const back = moonRoverGroundHeight(x, z - step);
  const front = moonRoverGroundHeight(x, z + step);
  // Caller supplies a scratch Vector3 so the per-frame call doesn't allocate.
  const target = out || new THREE.Vector3();
  return target.set(left - right, step * 2, back - front).normalize();
}

function makeMoonTerrain(size, segments) {
  const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
  geometry.rotateX(-Math.PI / 2);
  const pos = geometry.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i);
    const z = pos.getZ(i);
    pos.setY(i, moonRoverHeight(x, z));
  }
  geometry.computeVertexNormals();
  return geometry;
}

function createRampMesh(ramp) {
  const shape = new THREE.Shape();
  const halfLength = ramp.length * 0.5;
  // Triangular wedge: ground at start, peak at far end, drop straight down.
  shape.moveTo(-halfLength, 0);
  shape.lineTo(halfLength, ramp.height);
  shape.lineTo(halfLength, 0);
  shape.lineTo(-halfLength, 0);

  const geometry = new THREE.ExtrudeGeometry(shape, {
    depth: ramp.width,
    bevelEnabled: false,
    steps: 8,
  });
  geometry.translate(0, 0, -ramp.width * 0.5);
  geometry.rotateY(-Math.PI / 2);
  geometry.computeVertexNormals();

  const mesh = new THREE.Mesh(
    geometry,
    new THREE.MeshStandardMaterial({
      color: ramp.color,
      roughness: 0.94,
      metalness: 0.02,
    }),
  );
  mesh.position.set(ramp.x, moonRoverHeight(ramp.x, ramp.z) + 0.03, ramp.z);
  mesh.rotation.y = ramp.heading;
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function createRover() {
  const rover = new THREE.Group();
  rover.name = "Moon Rover";

  const frameMat = new THREE.MeshStandardMaterial({
    color: 0xd8dde1,
    roughness: 0.54,
    metalness: 0.52,
  });
  const panelMat = new THREE.MeshStandardMaterial({
    color: 0xf1f3ef,
    roughness: 0.62,
    metalness: 0.32,
  });
  const darkMat = new THREE.MeshStandardMaterial({
    color: 0x101219,
    roughness: 0.88,
    metalness: 0.1,
  });
  const tireMat = new THREE.MeshStandardMaterial({
    color: 0x17191f,
    roughness: 0.95,
    metalness: 0.04,
  });
  const hubMat = new THREE.MeshStandardMaterial({
    color: 0xb7c1ca,
    roughness: 0.5,
    metalness: 0.38,
  });
  const foilMat = new THREE.MeshStandardMaterial({
    color: 0xd6a43a,
    roughness: 0.64,
    metalness: 0.34,
  });
  const goldMat = new THREE.MeshStandardMaterial({
    color: 0xf1c453,
    roughness: 0.42,
    metalness: 0.36,
    side: THREE.DoubleSide,
  });
  const blueMat = new THREE.MeshStandardMaterial({
    color: 0x1f5d93,
    roughness: 0.42,
    metalness: 0.2,
    emissive: 0x061223,
  });
  const redMat = new THREE.MeshStandardMaterial({
    color: 0xff4338,
    roughness: 0.52,
    metalness: 0.12,
    emissive: 0x2b0505,
    emissiveIntensity: 0.28,
  });
  const lightMat = new THREE.MeshStandardMaterial({
    color: 0xfff3a6,
    emissive: 0xffd257,
    emissiveIntensity: 1.65,
  });
  const glassMat = new THREE.MeshStandardMaterial({
    color: 0x92c7ff,
    roughness: 0.18,
    metalness: 0.04,
    emissive: 0x102944,
    emissiveIntensity: 0.34,
  });

  const addRod = (from, to, radius = 0.035, mat = frameMat) => {
    const start = new THREE.Vector3(...from);
    const end = new THREE.Vector3(...to);
    const mid = start.clone().lerp(end, 0.5);
    const len = start.distanceTo(end);
    const rod = new THREE.Mesh(
      new THREE.CylinderGeometry(radius, radius, len, 10),
      mat,
    );
    rod.position.copy(mid);
    rod.quaternion.setFromUnitVectors(
      new THREE.Vector3(0, 1, 0),
      end.clone().sub(start).normalize(),
    );
    rover.add(rod);
    return rod;
  };

  const deckPoints = [
    [-0.72, 0.86, -1.16],
    [0.72, 0.86, -1.16],
    [-0.84, 0.78, 1.08],
    [0.84, 0.78, 1.08],
    [-0.46, 1.14, -0.86],
    [0.46, 1.14, -0.86],
    [-0.52, 1.04, 0.78],
    [0.52, 1.04, 0.78],
  ];
  addRod(deckPoints[0], deckPoints[1], 0.045);
  addRod(deckPoints[2], deckPoints[3], 0.045);
  addRod(deckPoints[0], deckPoints[2], 0.045);
  addRod(deckPoints[1], deckPoints[3], 0.045);
  addRod(deckPoints[4], deckPoints[5], 0.035);
  addRod(deckPoints[6], deckPoints[7], 0.035);
  addRod(deckPoints[4], deckPoints[6], 0.035);
  addRod(deckPoints[5], deckPoints[7], 0.035);
  addRod(deckPoints[0], deckPoints[7], 0.022);
  addRod(deckPoints[1], deckPoints[6], 0.022);

  const deck = new THREE.Mesh(
    new THREE.BoxGeometry(1.22, 0.16, 1.72),
    panelMat,
  );
  deck.position.set(0, 0.92, -0.08);
  deck.rotation.x = -0.035;
  rover.add(deck);

  const foilPack = new THREE.Mesh(
    new THREE.BoxGeometry(0.74, 0.34, 0.58, 3, 1, 3),
    foilMat,
  );
  foilPack.position.set(-0.16, 1.15, -0.48);
  foilPack.rotation.set(0.03, -0.08, 0.02);
  rover.add(foilPack);

  for (let i = 0; i < 5; i++) {
    const rib = new THREE.Mesh(
      new THREE.BoxGeometry(0.78, 0.018, 0.024),
      darkMat,
    );
    rib.position.set(-0.16, 1.335, -0.7 + i * 0.11);
    rover.add(rib);
  }

  const frontNose = new THREE.Mesh(
    new THREE.CapsuleGeometry(0.22, 0.54, 8, 18),
    panelMat,
  );
  frontNose.position.set(0, 0.94, 1.1);
  frontNose.rotation.x = Math.PI / 2;
  frontNose.scale.set(1.12, 0.72, 0.72);
  rover.add(frontNose);

  const cameraBar = new THREE.Mesh(
    new THREE.BoxGeometry(0.68, 0.13, 0.16),
    darkMat,
  );
  cameraBar.position.set(0, 1.12, 1.29);
  rover.add(cameraBar);
  [-0.18, 0.18].forEach((x) => {
    const lens = new THREE.Mesh(
      new THREE.CylinderGeometry(0.07, 0.07, 0.035, 18),
      glassMat,
    );
    lens.position.set(x, 1.12, 1.39);
    lens.rotation.x = Math.PI / 2;
    rover.add(lens);
  });

  const solarPanelGeo = new THREE.BoxGeometry(1.22, 0.035, 0.88);
  const solarLeft = new THREE.Mesh(solarPanelGeo, blueMat);
  solarLeft.position.set(-1.34, 1.17, -0.16);
  solarLeft.rotation.set(0.04, 0.04, 0.13);
  rover.add(solarLeft);
  const solarRight = solarLeft.clone();
  solarRight.position.x = 1.34;
  solarRight.rotation.set(0.04, -0.04, -0.13);
  rover.add(solarRight);
  [-1.34, 1.34].forEach((x) => {
    for (let i = -1; i <= 1; i++) {
      const stripe = new THREE.Mesh(
        new THREE.BoxGeometry(0.018, 0.04, 0.86),
        darkMat,
      );
      stripe.position.set(x + i * 0.31, 1.195, -0.16);
      stripe.rotation.copy(x < 0 ? solarLeft.rotation : solarRight.rotation);
      rover.add(stripe);
    }
  });
  addRod([-0.48, 1.0, -0.18], [-1.34, 1.16, -0.16], 0.022);
  addRod([0.48, 1.0, -0.18], [1.34, 1.16, -0.16], 0.022);

  const wheelGeo = new THREE.CylinderGeometry(0.34, 0.34, 0.26, 32);
  const hubGeo = new THREE.CylinderGeometry(0.16, 0.16, 0.31, 20);
  const treadGeo = new THREE.BoxGeometry(0.04, 0.035, 0.3);
  const wheelPositions = [
    [-1.18, 0.38, -1.08],
    [1.18, 0.38, -1.08],
    [-1.28, 0.36, 0],
    [1.28, 0.36, 0],
    [-1.18, 0.38, 1.08],
    [1.18, 0.38, 1.08],
  ];
  const makeWheel = (x, y, z) => {
    const wheel = new THREE.Group();
    wheel.position.set(x, y, z);
    wheel.userData.isWheel = true;
    const tire = new THREE.Mesh(wheelGeo, tireMat);
    tire.rotation.z = Math.PI / 2;
    wheel.add(tire);
    const hub = new THREE.Mesh(hubGeo, hubMat);
    hub.rotation.z = Math.PI / 2;
    wheel.add(hub);
    for (let i = 0; i < 14; i++) {
      const tread = new THREE.Mesh(treadGeo, tireMat);
      const angle = (i / 14) * Math.PI * 2;
      tread.position.set(0, Math.cos(angle) * 0.35, Math.sin(angle) * 0.35);
      tread.rotation.x = angle;
      wheel.add(tread);
    }
    for (let i = 0; i < 6; i++) {
      const spoke = new THREE.Mesh(
        new THREE.BoxGeometry(0.026, 0.24, 0.024),
        hubMat,
      );
      spoke.rotation.x = (i / 6) * Math.PI;
      wheel.add(spoke);
    }
    wheel.rotation.z = Math.PI / 2;
    rover.add(wheel);
  };
  wheelPositions.forEach(([x, y, z]) => {
    const side = Math.sign(x);
    const shoulder = [side * 0.5, 0.86, z * 0.72];
    const elbow = [side * 0.94, 0.64, z * 0.92];
    addRod(shoulder, elbow, 0.026, frameMat);
    addRod(elbow, [x * 0.98, y + 0.06, z], 0.03, frameMat);
    addRod(
      [side * 0.54, 0.72, z * 0.34],
      [x * 0.88, y + 0.16, z],
      0.018,
      darkMat,
    );
    makeWheel(x, y, z);
  });
  addRod([-0.94, 0.58, -1.08], [-1.06, 0.52, 1.08], 0.022, frameMat);
  addRod([0.94, 0.58, -1.08], [1.06, 0.52, 1.08], 0.022, frameMat);

  addRod([0.44, 1.08, 0.12], [0.58, 1.86, 0.18], 0.03, frameMat);
  addRod([0.58, 1.86, 0.18], [0.38, 2.08, 0.46], 0.022, frameMat);
  const head = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.18, 0.18), darkMat);
  head.position.set(0.35, 2.08, 0.5);
  rover.add(head);
  [-0.09, 0.09].forEach((x) => {
    const eye = new THREE.Mesh(
      new THREE.CylinderGeometry(0.045, 0.045, 0.028, 16),
      glassMat,
    );
    eye.position.set(0.35 + x, 2.08, 0.61);
    eye.rotation.x = Math.PI / 2;
    rover.add(eye);
  });
  const dish = new THREE.Mesh(new THREE.CircleGeometry(0.34, 32), goldMat);
  dish.position.set(0.66, 1.82, -0.36);
  dish.rotation.set(-0.76, 0.24, 0.08);
  rover.add(dish);
  addRod([0.46, 1.34, -0.12], [0.66, 1.82, -0.36], 0.016, frameMat);

  addRod([-0.42, 1.05, 0.28], [-0.82, 0.82, 0.76], 0.025, frameMat);
  addRod([-0.82, 0.82, 0.76], [-0.98, 0.72, 1.26], 0.021, frameMat);
  const scoop = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.08, 0.2), foilMat);
  scoop.position.set(-1.04, 0.69, 1.38);
  scoop.rotation.set(0.26, 0.18, 0.08);
  rover.add(scoop);

  [-0.34, 0.34].forEach((x) => {
    const lamp = new THREE.Mesh(
      new THREE.CylinderGeometry(0.09, 0.12, 0.11, 16),
      lightMat,
    );
    lamp.position.set(x, 0.86, 1.34);
    lamp.rotation.x = Math.PI / 2;
    rover.add(lamp);
  });

  const beacon = new THREE.Mesh(
    new THREE.SphereGeometry(0.15, 18, 10),
    new THREE.MeshBasicMaterial({ color: 0xff3030 }),
  );
  beacon.position.set(-0.32, 1.46, 0.5);
  beacon.userData.isRoverBeacon = true;
  rover.add(beacon);
  const beaconGlow = new THREE.Mesh(
    new THREE.SphereGeometry(0.42, 18, 10),
    new THREE.MeshBasicMaterial({
      color: 0xff3030,
      transparent: true,
      opacity: 0.22,
      depthWrite: false,
    }),
  );
  beaconGlow.position.copy(beacon.position);
  beaconGlow.userData.isRoverBeacon = true;
  rover.add(beaconGlow);

  rover.traverse((obj) => {
    if (obj.isMesh) {
      obj.castShadow = true;
      obj.receiveShadow = true;
    }
  });
  rover.scale.setScalar(1.55);
  return rover;
}

function createLunarDebrisField() {
  const group = new THREE.Group();
  const pebbleMat = new THREE.MeshStandardMaterial({
    color: 0x8f8d86,
    roughness: 0.98,
    metalness: 0.01,
  });
  const shardMat = new THREE.MeshStandardMaterial({
    color: 0x6d6c68,
    roughness: 0.96,
    metalness: 0.02,
  });
  const pebbleGeo = new THREE.DodecahedronGeometry(0.1, 0);
  const shardGeo = new THREE.TetrahedronGeometry(0.16, 0);
  const pebbleCount = 160;
  const shardCount = 48;
  const dummy = new THREE.Object3D();
  const pebbles = new THREE.InstancedMesh(pebbleGeo, pebbleMat, pebbleCount);
  const shards = new THREE.InstancedMesh(shardGeo, shardMat, shardCount);

  for (let i = 0; i < pebbleCount; i++) {
    const x = ((i * 37) % 91) - 45;
    const z = ((i * 53) % 93) - 46;
    const scale = 0.45 + (i % 9) * 0.09;
    dummy.position.set(x, moonRoverGroundHeight(x, z) + 0.035, z);
    dummy.rotation.set(i * 0.37, i * 0.19, i * 0.23);
    dummy.scale.set(
      scale * (1 + (i % 3) * 0.35),
      scale * 0.62,
      scale * (0.8 + (i % 4) * 0.18),
    );
    dummy.updateMatrix();
    pebbles.setMatrixAt(i, dummy.matrix);
  }
  for (let i = 0; i < shardCount; i++) {
    const x = ((i * 71) % 88) - 44;
    const z = ((i * 41) % 86) - 43;
    const scale = 0.75 + (i % 7) * 0.12;
    dummy.position.set(x, moonRoverGroundHeight(x, z) + 0.08, z);
    dummy.rotation.set(i * 0.31, i * 0.29, i * 0.43);
    dummy.scale.set(scale * 0.8, scale * 0.42, scale * 1.24);
    dummy.updateMatrix();
    shards.setMatrixAt(i, dummy.matrix);
  }
  pebbles.castShadow = true;
  pebbles.receiveShadow = true;
  shards.castShadow = true;
  shards.receiveShadow = true;
  group.add(pebbles, shards);
  return group;
}

function createCraterDetails() {
  const group = new THREE.Group();
  const rimMat = new THREE.MeshBasicMaterial({
    color: 0xc6c3b8,
    transparent: true,
    opacity: 0.3,
  });
  const shadowMat = new THREE.MeshBasicMaterial({
    color: 0x1b1b1f,
    transparent: true,
    opacity: 0.16,
    depthWrite: false,
  });
  const ejectaMat = new THREE.MeshBasicMaterial({
    color: 0xb5b1a6,
    transparent: true,
    opacity: 0.22,
    depthWrite: false,
  });

  MOON_ROVER_CRATERS.forEach((crater, craterIndex) => {
    const rim = new THREE.Mesh(
      new THREE.TorusGeometry(crater.r, 0.026, 8, 112),
      rimMat,
    );
    rim.rotation.x = Math.PI / 2;
    rim.scale.z = crater.skew;
    rim.position.set(
      crater.x,
      moonRoverHeight(crater.x, crater.z) + 0.1,
      crater.z,
    );
    group.add(rim);

    const shadow = new THREE.Mesh(
      new THREE.CircleGeometry(crater.r * 0.68, 48),
      shadowMat,
    );
    shadow.rotation.x = -Math.PI / 2;
    shadow.scale.set(1, crater.skew, 1);
    shadow.position.set(
      crater.x - crater.r * 0.08,
      moonRoverHeight(crater.x, crater.z) + 0.07,
      crater.z + crater.r * 0.06,
    );
    group.add(shadow);

    for (let i = 0; i < 12; i++) {
      const angle = i * 0.9 + craterIndex * 0.37;
      const length = crater.r * (0.72 + (i % 4) * 0.12);
      const ray = new THREE.Mesh(
        new THREE.PlaneGeometry(0.08 + crater.r * 0.015, length),
        ejectaMat,
      );
      const rayRadius = crater.r * (0.98 + (i % 3) * 0.12);
      const x = crater.x + Math.cos(angle) * rayRadius;
      const z = crater.z + Math.sin(angle) * rayRadius * crater.skew;
      ray.rotation.x = -Math.PI / 2;
      ray.rotation.z = -angle;
      ray.position.set(x, moonRoverGroundHeight(x, z) + 0.035, z);
      group.add(ray);
    }
  });

  return group;
}

function createSampleMarker(sample, texture) {
  const group = new THREE.Group();
  group.userData.sampleId = sample.id;

  const rockMat = new THREE.MeshStandardMaterial({
    color: new THREE.Color(sample.color),
    roughness: 0.6,
    metalness: 0.06,
    emissive: new THREE.Color(sample.color),
    emissiveIntensity: 0.08,
  });
  const glowMat = new THREE.MeshBasicMaterial({
    color: new THREE.Color(sample.color),
    transparent: true,
    opacity: 0.18,
    depthWrite: false,
  });

  if (texture) {
    texture.colorSpace = THREE.SRGBColorSpace;
    const sprite = new THREE.Sprite(
      new THREE.SpriteMaterial({
        map: texture,
        transparent: true,
        depthWrite: false,
      }),
    );
    sprite.position.y = 1.18;
    sprite.scale.set(2.45, 2.45, 1);
    sprite.userData.isRockSprite = true;
    group.add(sprite);
  } else {
    const rock = new THREE.Mesh(
      new THREE.DodecahedronGeometry(0.72, 1),
      rockMat,
    );
    rock.position.y = 0.66;
    rock.rotation.set(sample.x * 0.07, sample.z * 0.05, 0.4);
    rock.scale.set(1, 0.82, 0.92);
    group.add(rock);

    for (let index = 0; index < 4; index += 1) {
      const chip = new THREE.Mesh(
        new THREE.DodecahedronGeometry(0.16 + index * 0.025, 0),
        rockMat,
      );
      const angle = index * 1.7 + sample.x * 0.08;
      chip.position.set(
        Math.cos(angle) * (0.54 + index * 0.06),
        0.18,
        Math.sin(angle) * (0.44 + index * 0.04),
      );
      chip.rotation.set(index * 0.6, angle, 0.2);
      group.add(chip);
    }
  }

  const ring = new THREE.Mesh(
    new THREE.TorusGeometry(1.2, 0.045, 8, 42),
    rockMat,
  );
  ring.rotation.x = Math.PI / 2;
  ring.position.y = 0.08;
  group.add(ring);

  const beam = new THREE.Mesh(
    new THREE.CylinderGeometry(0.16, 0.5, 4.8, 24, 1, true),
    new THREE.MeshBasicMaterial({
      color: new THREE.Color(sample.color),
      transparent: true,
      opacity: 0.2,
      depthWrite: false,
      side: THREE.DoubleSide,
    }),
  );
  beam.position.y = 2.35;
  beam.userData.isSignalBeam = true;
  group.add(beam);

  group.position.set(
    sample.x,
    moonRoverGroundHeight(sample.x, sample.z) + 0.03,
    sample.z,
  );
  return group;
}

function MoonRoverLevel({ onExit }) {
  const mountRef = mrUseRef(null);
  const joystickRef = mrUseRef(null);
  const stateRef = mrUseRef(null);
  const [collected, setCollected] = mrUseState([]);
  const [toast, setToast] = mrUseState({
    title: "Drive to the science beam",
    text: "Drag the joystick or hold the arrow keys. Left and right arrows roll forward while turning.",
  });
  const [speedLabel, setSpeedLabel] = mrUseState("0");
  const [nextDistanceLabel, setNextDistanceLabel] = mrUseState("--");
  const [nextDirectionLabel, setNextDirectionLabel] = mrUseState("--");
  const [nextDistanceClose, setNextDistanceClose] = mrUseState(false);
  const [airborne, setAirborne] = mrUseState(false);

  const remaining = MOON_ROVER_SAMPLES.length - collected.length;
  const nextSample = MOON_ROVER_SAMPLES.find(
    (sample) => !collected.includes(sample.id),
  );
  const missionStep = Math.min(collected.length + 1, MOON_ROVER_SAMPLES.length);
  const missionTitle = remaining
    ? `Find ${nextSample?.label || "the next sample"}`
    : "Moon mission done!";
  const roverGuide =
    remaining && nextDirectionLabel !== "--"
      ? ` Guide: ${nextDirectionLabel} (${nextDistanceLabel}).`
      : "";
  const missionHint = remaining
    ? `Step ${missionStep}/${MOON_ROVER_SAMPLES.length}: drive to the glowing science beam.${roverGuide}`
    : "Every sample is safe in the science basket.";

  mrUseEffect(() => {
    const mount = mountRef.current;
    if (!mount || !window.THREE) return undefined;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x02040b);
    scene.fog = new THREE.FogExp2(0x070813, 0.01);

    const camera = new THREE.PerspectiveCamera(58, 1, 0.1, 420);
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    const pixelRatioCap =
      (window.visualViewport?.width || window.innerWidth || 1000) < 760
        ? 1.45
        : 2;
    renderer.setPixelRatio(
      Math.min(window.devicePixelRatio || 1, pixelRatioCap),
    );
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.domElement.className = "moon-rover-canvas";
    mount.appendChild(renderer.domElement);

    const hemi = new THREE.HemisphereLight(0x8798b8, 0x101016, 1.2);
    scene.add(hemi);

    const sun = new THREE.DirectionalLight(0xfff0c6, 3.1);
    sun.position.set(-24, 34, -18);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    sun.shadow.camera.left = -55;
    sun.shadow.camera.right = 55;
    sun.shadow.camera.top = 55;
    sun.shadow.camera.bottom = -55;
    scene.add(sun);

    const earthGlow = new THREE.PointLight(0x8ec8ff, 0.85, 80);
    earthGlow.position.set(18, 20, 26);
    scene.add(earthGlow);

    const terrainMat = new THREE.MeshStandardMaterial({
      color: 0x878782,
      roughness: 0.93,
      metalness: 0.02,
    });
    const moonBody = new THREE.Mesh(
      new THREE.SphereGeometry(MOON_ROVER_RADIUS, 96, 48),
      new THREE.MeshStandardMaterial({
        color: 0x6f706d,
        roughness: 0.96,
        metalness: 0.01,
      }),
    );
    moonBody.position.set(
      0,
      -MOON_ROVER_RADIUS - MOON_ROVER_BODY_CLEARANCE,
      0,
    );
    moonBody.receiveShadow = true;
    scene.add(moonBody);

    // Terrain plane is wide enough to cover the rover's view in any wrapped
    // tile. Vertices use the periodic moonRoverHeight, so the seam at
    // x = ±MOON_ROVER_WORLD_LIMIT is visually invisible.
    const terrain = new THREE.Mesh(makeMoonTerrain(160, 260), terrainMat);
    terrain.receiveShadow = true;
    scene.add(terrain);

    // Static decoration that should repeat across the wrap (ramps, craters,
    // debris) goes inside worldGroup; eight clone Object3Ds tile it across
    // the 8 surrounding wrap tiles so the rover sees ramps coming up from
    // "behind" the seam exactly where they would on a torus.
    const worldGroup = new THREE.Group();
    MOON_ROVER_RAMPS.forEach((ramp) => {
      worldGroup.add(createRampMesh(ramp));
    });
    worldGroup.add(createLunarDebrisField());
    worldGroup.add(createCraterDetails());
    scene.add(worldGroup);

    const worldTiles = [{ group: worldGroup, dx: 0, dz: 0 }];
    for (let dx = -1; dx <= 1; dx++) {
      for (let dz = -1; dz <= 1; dz++) {
        if (dx === 0 && dz === 0) continue;
        const ghost = worldGroup.clone(true);
        ghost.position.set(
          dx * MOON_ROVER_WORLD_SIZE,
          0,
          dz * MOON_ROVER_WORLD_SIZE,
        );
        // Ghosts don't cast shadows — keeps the shadow map within the central
        // tile where the directional light's frustum was authored.
        ghost.traverse((obj) => {
          if (obj.isMesh) obj.castShadow = false;
        });
        scene.add(ghost);
        worldTiles.push({ group: ghost, dx, dz });
      }
    }

    const earth = new THREE.Mesh(
      new THREE.SphereGeometry(3.4, 40, 24),
      new THREE.MeshStandardMaterial({
        color: 0x4f8fd7,
        roughness: 0.48,
        metalness: 0.05,
        emissive: 0x0c2446,
        emissiveIntensity: 0.32,
      }),
    );
    earth.position.set(26, 28, -52);
    scene.add(earth);

    const starCount = 520;
    const starPositions = new Float32Array(starCount * 3);
    for (let i = 0; i < starCount; i++) {
      const a = i * 2.399;
      const y = 10 + ((i * 37) % 76);
      const radius = 42 + ((i * 19) % 150);
      starPositions[i * 3] = Math.cos(a) * radius;
      starPositions[i * 3 + 1] = y;
      starPositions[i * 3 + 2] = Math.sin(a) * radius - 35;
    }
    const starGeo = new THREE.BufferGeometry();
    starGeo.setAttribute(
      "position",
      new THREE.BufferAttribute(starPositions, 3),
    );
    const stars = new THREE.Points(
      starGeo,
      new THREE.PointsMaterial({
        color: 0xffffff,
        size: 2.1,
        sizeAttenuation: false,
        transparent: true,
        opacity: 1,
        fog: false,
      }),
    );
    scene.add(stars);

    const rover = createRover();
    scene.add(rover);

    const trailMat = new THREE.MeshBasicMaterial({
      color: 0xded9c8,
      transparent: true,
      opacity: 0.22,
      depthWrite: false,
    });
    const roverTrail = Array.from({ length: 18 }, (_, index) => {
      const puff = new THREE.Mesh(
        new THREE.CircleGeometry(0.42 + index * 0.012, 18),
        trailMat.clone(),
      );
      puff.rotation.x = -Math.PI / 2;
      puff.visible = false;
      scene.add(puff);
      return puff;
    });

    const markers = new Map();
    const textureLoader = new THREE.TextureLoader();
    const sampleTextures = new Map();
    MOON_ROVER_SAMPLES.forEach((sample) => {
      if (!sample.image) return;
      const tex = textureLoader.load(sample.image);
      sampleTextures.set(sample.id, tex);
    });
    // Sample-marker visual ghosts: 8 mirrored clones per sample so the
    // signal beam appears on every surrounding wrap tile. Only the original
    // marker is in the markers Map — pickup math still operates on the
    // wrapped sim.position relative to sample.x/z, untouched.
    const sampleGhosts = [];
    MOON_ROVER_SAMPLES.forEach((sample) => {
      const marker = createSampleMarker(sample, sampleTextures.get(sample.id));
      markers.set(sample.id, marker);
      scene.add(marker);
      for (let dx = -1; dx <= 1; dx++) {
        for (let dz = -1; dz <= 1; dz++) {
          if (dx === 0 && dz === 0) continue;
          const ghost = marker.clone(true);
          ghost.position.x += dx * MOON_ROVER_WORLD_SIZE;
          ghost.position.z += dz * MOON_ROVER_WORLD_SIZE;
          ghost.userData.sampleId = sample.id;
          ghost.userData.isGhost = true;
          scene.add(ghost);
          sampleGhosts.push({ ghost, sampleId: sample.id, dx, dz });
        }
      }
    });

    const keys = new Set();
    const sim = {
      renderer,
      scene,
      camera,
      terrain,
      rover,
      roverTrail,
      trailIndex: 0,
      lastTrailAt: 0,
      markers,
      sampleGhosts,
      worldTiles,
      moonBody,
      keys,
      speed: 0,
      heading: 0,
      visualHeading: 0,
      verticalVelocity: 0,
      wasGrounded: true,
      airborneState: false,
      airborneSince: 0,
      airLaunchGroundY: 0,
      airPeakY: 0,
      suppressNextLandingToast: false,
      jumpQueued: false,
      launchRamp: null,
      bodyQuat: new THREE.Quaternion(),
      yawQuat: new THREE.Quaternion(),
      tiltQuat: new THREE.Quaternion(),
      joy: { x: 0, y: 0, active: false, id: null },
      position: new THREE.Vector3(0, 0, 0),
      collected: new Set(),
      last: performance.now(),
      lastRampToast: 0,
      lastLandingToast: 0,
      lastGravityToast: 0,
      lastJumpToast: 0,
      lastSpeedLabelAt: 0,
      speedDisplay: "0",
      lastDistanceLabelAt: 0,
      distanceDisplay: "--",
      directionDisplay: "--",
      distanceClose: false,
      tileX: 0,
      tileZ: 0,
      antiSpin: {
        lastX: 0,
        lastZ: 0,
        headingTravel: 0,
        checkAt: performance.now(),
        coolUntil: 0,
      },
      raf: 0,
      disposed: false,
      width: 0,
      height: 0,
      // Reusable scratch objects for the animation loop — avoids allocating
      // multiple Vector3s and a normal vector every frame (60+/sec).
      _up: new THREE.Vector3(0, 1, 0),
      _normal: new THREE.Vector3(),
      _normalAir: new THREE.Vector3(),
      _camTarget: new THREE.Vector3(),
    };
    stateRef.current = sim;

    const syncCollected = () => {
      setCollected(Array.from(sim.collected));
    };

    const collectNearSamples = () => {
      for (const sample of MOON_ROVER_SAMPLES) {
        if (sim.collected.has(sample.id)) continue;
        const dx = moonRoverWrapDelta(sim.position.x, sample.x);
        const dz = moonRoverWrapDelta(sim.position.z, sample.z);
        if (Math.sqrt(dx * dx + dz * dz) > 3.25) continue;
        sim.collected.add(sample.id);
        const marker = sim.markers.get(sample.id);
        if (marker) marker.visible = false;
        if (sim.sampleGhosts) {
          sim.sampleGhosts.forEach((entry) => {
            if (entry.sampleId === sample.id) entry.ghost.visible = false;
          });
        }
        const left = MOON_ROVER_SAMPLES.length - sim.collected.size;
        const next = MOON_ROVER_SAMPLES.find(
          (moonSample) => !sim.collected.has(moonSample.id),
        );
        setToast({
          title: `${sample.label} in the basket!`,
          text: left
            ? `${sample.fact} Now find ${next?.label || "the next sample"}.`
            : `${sample.fact} Science basket full!`,
        });
        syncCollected();
        if (window.playKidSound) window.playKidSound("chime");
        if (window.__narration) {
          window.__narration.play(
            left ? "game_rover_sample.mp3" : "game_rover_done.mp3",
          );
        }
        break;
      }
    };

    const fit = () => {
      const rect = mount.getBoundingClientRect();
      sim.width = Math.max(1, rect.width);
      sim.height = Math.max(1, rect.height);
      camera.aspect = sim.width / sim.height;
      camera.updateProjectionMatrix();
      renderer.setSize(sim.width, sim.height, false);
    };

    const syncEndlessMoonTiles = () => {
      const tileX =
        Math.round(sim.position.x / MOON_ROVER_WORLD_SIZE) *
        MOON_ROVER_WORLD_SIZE;
      const tileZ =
        Math.round(sim.position.z / MOON_ROVER_WORLD_SIZE) *
        MOON_ROVER_WORLD_SIZE;
      if (tileX === sim.tileX && tileZ === sim.tileZ) return;

      sim.tileX = tileX;
      sim.tileZ = tileZ;
      sim.terrain.position.set(tileX, 0, tileZ);
      sim.worldTiles.forEach(({ group, dx, dz }) => {
        group.position.set(
          tileX + dx * MOON_ROVER_WORLD_SIZE,
          0,
          tileZ + dz * MOON_ROVER_WORLD_SIZE,
        );
      });
      MOON_ROVER_SAMPLES.forEach((sample) => {
        const marker = sim.markers.get(sample.id);
        if (marker) {
          marker.position.x = sample.x + tileX;
          marker.position.z = sample.z + tileZ;
        }
      });
      sim.sampleGhosts.forEach(({ ghost, sampleId, dx, dz }) => {
        const sample = MOON_ROVER_SAMPLES.find(({ id }) => id === sampleId);
        if (!sample) return;
        ghost.position.x = sample.x + tileX + dx * MOON_ROVER_WORLD_SIZE;
        ghost.position.z = sample.z + tileZ + dz * MOON_ROVER_WORLD_SIZE;
      });
    };

    const onKeyDown = (event) => {
      if (event.code === "Space" || event.key === " ") {
        event.preventDefault();
        sim.jumpQueued = true;
        return;
      }
      if (
        [
          "ArrowUp",
          "ArrowDown",
          "ArrowLeft",
          "ArrowRight",
          "w",
          "a",
          "s",
          "d",
          "W",
          "A",
          "S",
          "D",
        ].includes(event.key)
      ) {
        event.preventDefault();
        keys.add(event.key.toLowerCase());
      }
    };
    const onKeyUp = (event) => keys.delete(event.key.toLowerCase());

    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    window.addEventListener("resize", fit);
    if (window.visualViewport)
      window.visualViewport.addEventListener("resize", fit);
    fit();
    camera.position.set(0, 8.4, -13.4);
    camera.lookAt(0, 0.85, 2.2);

    const animate = (now) => {
      if (sim.disposed) return;
      const dt = Math.min(0.045, (now - sim.last) / 1000 || 0.016);
      sim.last = now;

      let steer = 0;
      let throttle = 0;
      if (keys.has("arrowup") || keys.has("w")) throttle += 1;
      if (keys.has("arrowdown") || keys.has("s")) throttle -= 0.72;
      if (keys.has("arrowleft") || keys.has("a")) steer -= 1;
      if (keys.has("arrowright") || keys.has("d")) steer += 1;
      if (Math.abs(steer) > 0.1 && Math.abs(throttle) < 0.05) throttle += 0.62;
      if (sim.joy.active) {
        throttle += -sim.joy.y;
        steer += sim.joy.x;
        if (Math.abs(sim.joy.x) > 0.42 && throttle < 0.28) {
          throttle = 0.28;
        }
      }

      throttle = Math.max(-0.8, Math.min(1, throttle));
      steer = Math.max(-1, Math.min(1, steer));

      const maxSpeed = 7.8;
      const wantedSpeed = throttle * maxSpeed;
      sim.speed += (wantedSpeed - sim.speed) * Math.min(1, dt * 5.6);
      if (Math.abs(throttle) < 0.05) sim.speed *= Math.pow(0.1, dt);
      if (Math.abs(sim.speed) < 0.08) sim.speed = 0;
      const motionGrip = Math.min(1, Math.abs(sim.speed) / 2.2);
      const hardTurnDamp =
        Math.abs(steer) > 0.86 && Math.abs(sim.speed) < 2.4 ? 0.42 : 1;
      const turnGrip =
        (0.72 + Math.min(1, Math.abs(sim.speed) / maxSpeed) * 1.05) *
        motionGrip *
        hardTurnDamp;
      const oldHeading = sim.heading;
      sim.heading -= steer * dt * turnGrip * Math.sign(sim.speed || 1);
      const headingStep = Math.abs(
        Math.atan2(
          Math.sin(sim.heading - oldHeading),
          Math.cos(sim.heading - oldHeading),
        ),
      );
      sim.antiSpin.headingTravel += headingStep;
      sim.visualHeading +=
        Math.atan2(
          Math.sin(sim.heading - sim.visualHeading),
          Math.cos(sim.heading - sim.visualHeading),
        ) * Math.min(1, dt * 8.5);

      sim.position.x += Math.sin(sim.heading) * sim.speed * dt;
      sim.position.z += Math.cos(sim.heading) * sim.speed * dt;
      syncEndlessMoonTiles();
      if (now - sim.antiSpin.checkAt > 2400) {
        const dx = sim.position.x - sim.antiSpin.lastX;
        const dz = sim.position.z - sim.antiSpin.lastZ;
        const travel = Math.sqrt(dx * dx + dz * dz);
        // Looser thresholds (~2 full rotations and barely any translation)
        // so a kid doing a deliberate donut isn't auto-rescued mid-spin.
        if (
          sim.antiSpin.headingTravel > 6.0 &&
          travel < 1.4 &&
          now > sim.antiSpin.coolUntil
        ) {
          const nextSample = MOON_ROVER_SAMPLES.find(
            (sample) => !sim.collected.has(sample.id),
          );
          const guide = nextSample
            ? Math.atan2(
                -moonRoverWrapDelta(sim.position.x, nextSample.x),
                -moonRoverWrapDelta(sim.position.z, nextSample.z),
              )
            : 0;
          sim.heading = guide;
          sim.visualHeading = guide;
          sim.speed = Math.max(1.8, Math.abs(sim.speed));
          sim.antiSpin.coolUntil = now + 3600;
          setToast({
            title: "Wheels straight!",
            text: "Rover drivers steer out of circles and roll toward the next science marker.",
          });
          if (window.__narration)
            window.__narration.play("game_rover_wheels_straight.mp3");
        }
        sim.antiSpin.lastX = sim.position.x;
        sim.antiSpin.lastZ = sim.position.z;
        sim.antiSpin.headingTravel = 0;
        sim.antiSpin.checkAt = now;
      }

      const groundY = moonRoverGroundHeight(sim.position.x, sim.position.z);
      const rampHit = moonRoverRampAt(sim.position.x, sim.position.z);
      const markAirborne = () => {
        sim.wasGrounded = false;
        sim.airborneSince = now;
        sim.airLaunchGroundY = groundY;
        sim.airPeakY = sim.position.y;
      };
      if (sim.jumpQueued && sim.wasGrounded) {
        sim.jumpQueued = false;
        sim.verticalVelocity = 3.35 + Math.min(1.1, Math.abs(sim.speed) * 0.14);
        markAirborne();
        sim.launchRamp = null;
        if (now - sim.lastJumpToast > 1200) {
          sim.lastJumpToast = now;
          setToast({
            title: "Moon hop!",
            text: "Low gravity lets a rover float longer after a gentle jump.",
          });
          if (window.__narration)
            window.__narration.play("game_rover_jump.mp3");
        }
        if (window.playKidSound) window.playKidSound("pop");
      } else {
        sim.jumpQueued = false;
      }
      const rampLip =
        rampHit.ramp && rampHit.localZ > rampHit.ramp.length * 0.18;
      const enoughSpeed = Math.abs(sim.speed) > 3.6;
      if (
        sim.wasGrounded &&
        rampLip &&
        enoughSpeed &&
        sim.launchRamp !== rampHit.ramp
      ) {
        sim.verticalVelocity = 3.9 + rampHit.slope * Math.abs(sim.speed) * 1.35;
        markAirborne();
        sim.launchRamp = rampHit.ramp;
        if (now - sim.lastRampToast > 5200 && Math.random() < 0.36) {
          sim.lastRampToast = now;
          sim.suppressNextLandingToast = true;
          setToast({
            title: "Floaty moon jump!",
            text: "The Moon pulls less than Earth, so the rover stays in the air longer.",
          });
          if (window.__narration)
            window.__narration.play("game_rover_ramp.mp3");
        }
        if (window.playKidSound) window.playKidSound("pop");
      }

      sim.verticalVelocity -= 4.2 * dt;
      sim.position.y += sim.verticalVelocity * dt;
      if (!sim.wasGrounded)
        sim.airPeakY = Math.max(sim.airPeakY, sim.position.y);
      if (sim.position.y <= groundY + 0.08) {
        const wasAirborne = !sim.wasGrounded;
        const airTime = now - (sim.airborneSince || now);
        const hopHeight = sim.airPeakY - sim.airLaunchGroundY;
        if (sim.wasGrounded) {
          sim.position.y = groundY;
        } else {
          sim.position.y += (groundY - sim.position.y) * Math.min(1, dt * 18);
          if (
            Math.abs(sim.position.y - groundY) < 0.08 ||
            sim.verticalVelocity < 0
          )
            sim.position.y = groundY;
        }
        if (
          wasAirborne &&
          airTime > 520 &&
          hopHeight > 0.55 &&
          !sim.suppressNextLandingToast &&
          now - sim.lastLandingToast > 5200 &&
          Math.random() < 0.34
        ) {
          sim.lastLandingToast = now;
          setToast({
            title: "Soft landing",
            text: "Wide rover wheels help it land gently on dusty ground.",
          });
          if (window.__narration)
            window.__narration.play("game_rover_landing.mp3");
        }
        sim.verticalVelocity = Math.max(0, sim.verticalVelocity);
        sim.wasGrounded = true;
        sim.airborneSince = 0;
        sim.airLaunchGroundY = groundY;
        sim.airPeakY = groundY;
        sim.suppressNextLandingToast = false;
        if (!rampHit.ramp || rampHit.localZ < -rampHit.ramp.length * 0.15)
          sim.launchRamp = null;
      } else {
        if (sim.wasGrounded) markAirborne();
      }
      if (sim.airborneState !== !sim.wasGrounded) {
        sim.airborneState = !sim.wasGrounded;
        setAirborne(sim.airborneState);
      }

      const groundNormal = moonRoverNormal(
        sim.position.x,
        sim.position.z,
        sim._normal,
      );
      let normal;
      if (sim.wasGrounded) {
        normal = groundNormal;
      } else {
        // Blend toward straight-up while airborne — reuse scratch vectors so
        // this hot path stays allocation-free.
        normal = sim._normalAir
          .set(0, 1, 0)
          .lerp(groundNormal, 0.35)
          .normalize();
      }
      sim.yawQuat.setFromAxisAngle(sim._up, sim.visualHeading);
      sim.tiltQuat.setFromUnitVectors(sim._up, normal);
      sim.bodyQuat.copy(sim.tiltQuat).multiply(sim.yawQuat);

      rover.position.set(sim.position.x, sim.position.y + 0.26, sim.position.z);
      // Lock the giant moon-body sphere under the rover horizontally so
      // "down" always points at the moon center wherever you are in the
      // wrapped tile. Its y stays fixed; only x/z follow.
      sim.moonBody.position.x = sim.position.x;
      sim.moonBody.position.z = sim.position.z;
      rover.quaternion.slerp(sim.bodyQuat, 1 - Math.pow(0.001, dt));
      rover.children.forEach((child) => {
        if (child.userData.isWheel) child.rotation.x += sim.speed * dt * 2.65;
        if (child.userData.isRoverBeacon) {
          child.scale.setScalar(1 + Math.sin(now * 0.008) * 0.14);
        }
      });
      if (
        Math.abs(sim.speed) > 1.6 &&
        now - sim.lastTrailAt > 130 &&
        sim.wasGrounded
      ) {
        const puff = sim.roverTrail[sim.trailIndex % sim.roverTrail.length];
        puff.visible = true;
        puff.position.set(
          sim.position.x - Math.sin(sim.visualHeading) * 1.05,
          moonRoverGroundHeight(sim.position.x, sim.position.z) + 0.05,
          sim.position.z - Math.cos(sim.visualHeading) * 1.05,
        );
        puff.scale.setScalar(
          0.65 + Math.min(1, Math.abs(sim.speed) / 8) * 0.55,
        );
        puff.material.opacity = 0.18;
        sim.trailIndex += 1;
        sim.lastTrailAt = now;
      }
      sim.roverTrail.forEach((puff) => {
        if (!puff.visible) return;
        puff.material.opacity *= Math.pow(0.08, dt);
        puff.scale.multiplyScalar(1 + dt * 0.32);
        if (puff.material.opacity < 0.018) puff.visible = false;
      });

      const nextSample = MOON_ROVER_SAMPLES.find(
        (sample) => !sim.collected.has(sample.id),
      );
      markers.forEach((marker, id) => {
        if (!marker.visible) return;
        marker.rotation.y += dt * 0.65;
        marker.children.forEach((child) => {
          if (child.userData.isGlow) {
            child.scale.setScalar(1 + Math.sin(now * 0.004 + id.length) * 0.08);
          }
          if (child.userData.isSignalBeam) {
            child.visible = Boolean(nextSample && nextSample.id === id);
            child.material.opacity = child.visible
              ? 0.14 + Math.sin(now * 0.003 + id.length) * 0.05
              : 0;
          }
        });
      });
      // Mirror the per-marker spin/beam state onto the wrap ghosts so they
      // pulse in sync with the real markers across the seam.
      sampleGhosts.forEach(({ ghost, sampleId }) => {
        if (!ghost.visible) return;
        ghost.rotation.y += dt * 0.65;
        const beamLit = Boolean(nextSample && nextSample.id === sampleId);
        ghost.children.forEach((child) => {
          if (child.userData.isSignalBeam) {
            child.visible = beamLit;
          }
        });
      });

      collectNearSamples();

      sim._camTarget.set(
        sim.position.x - Math.sin(sim.visualHeading) * 9.6,
        sim.position.y + 6.4,
        sim.position.z - Math.cos(sim.visualHeading) * 9.6,
      );
      camera.position.lerp(sim._camTarget, 1 - Math.pow(0.015, dt));
      camera.lookAt(
        sim.position.x + Math.sin(sim.visualHeading) * 2,
        sim.position.y + 0.82,
        sim.position.z + Math.cos(sim.visualHeading) * 2,
      );

      stars.position.set(sim.position.x, 0, sim.position.z);
      earth.rotation.y += dt * 0.04;
      const nextSpeedLabel = String(Math.round(Math.abs(sim.speed) * 7));
      if (
        nextSpeedLabel !== sim.speedDisplay &&
        now - sim.lastSpeedLabelAt > 120
      ) {
        sim.speedDisplay = nextSpeedLabel;
        sim.lastSpeedLabelAt = now;
        setSpeedLabel(nextSpeedLabel);
      }
      if (now - sim.lastDistanceLabelAt > 180) {
        const targetSample = MOON_ROVER_SAMPLES.find(
          (sample) => !sim.collected.has(sample.id),
        );
        const nextDistance = targetSample
          ? Math.hypot(
              moonRoverWrapDelta(sim.position.x, targetSample.x),
              moonRoverWrapDelta(sim.position.z, targetSample.z),
            )
          : 0;
        const nextDistanceText = targetSample
          ? nextDistance < 8
            ? `Close! ${Math.max(0, Math.round(nextDistance))} m`
            : `${Math.max(0, Math.round(nextDistance))} m`
          : "done";
        let nextDirectionText = "done";
        if (targetSample) {
          const angleToTarget = Math.atan2(
            -moonRoverWrapDelta(sim.position.x, targetSample.x),
            -moonRoverWrapDelta(sim.position.z, targetSample.z),
          );
          const delta = Math.atan2(
            Math.sin(angleToTarget - sim.visualHeading),
            Math.cos(angleToTarget - sim.visualHeading),
          );
          const absDelta = Math.abs(delta);
          nextDirectionText =
            absDelta < 0.46
              ? "go forward"
              : absDelta > 2.55
                ? "turn around"
                : delta > 0
                  ? "turn right"
                  : "turn left";
        }
        if (nextDistanceText !== sim.distanceDisplay) {
          sim.distanceDisplay = nextDistanceText;
          setNextDistanceLabel(nextDistanceText);
        }
        if (nextDirectionText !== sim.directionDisplay) {
          sim.directionDisplay = nextDirectionText;
          setNextDirectionLabel(nextDirectionText);
        }
        const close = Boolean(targetSample && nextDistance < 8);
        if (close !== sim.distanceClose) {
          sim.distanceClose = close;
          setNextDistanceClose(close);
        }
        sim.lastDistanceLabelAt = now;
      }
      renderer.render(scene, camera);
      sim.raf = requestAnimationFrame(animate);
    };

    sim.raf = requestAnimationFrame(animate);

    return () => {
      sim.disposed = true;
      cancelAnimationFrame(sim.raf);
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("resize", fit);
      if (window.visualViewport)
        window.visualViewport.removeEventListener("resize", fit);
      stateRef.current = null;
      scene.traverse((obj) => {
        if (!obj.isMesh) return;
        if (obj.geometry) obj.geometry.dispose();
        const mats = Array.isArray(obj.material)
          ? obj.material
          : [obj.material];
        mats.forEach((mat) => {
          if (!mat) return;
          // Free any maps Three.js won't auto-release.
          if (mat.map && mat.map.dispose) mat.map.dispose();
          if (mat.alphaMap && mat.alphaMap.dispose) mat.alphaMap.dispose();
          if (mat.dispose) mat.dispose();
        });
      });
      // Sample textures from TextureLoader live outside the scene graph until
      // their sprite materials get disposed; explicitly free them so revisits
      // don't leak GPU memory.
      sampleTextures.forEach((tex) => tex && tex.dispose && tex.dispose());
      sampleTextures.clear();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount)
        mount.removeChild(renderer.domElement);
    };
  }, []);

  const updateJoystick = (event, active) => {
    const sim = stateRef.current;
    const pad = joystickRef.current;
    if (!sim || !pad) return;
    if (active) {
      sim.joy.active = true;
      sim.joy.id = event.pointerId;
      try {
        pad.setPointerCapture(event.pointerId);
      } catch {}
    }
    if (
      !sim.joy.active ||
      (sim.joy.id !== null && sim.joy.id !== event.pointerId)
    )
      return;
    const rect = pad.getBoundingClientRect();
    const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
    const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
    const len = Math.max(1, Math.sqrt(x * x + y * y));
    sim.joy.x = x / len;
    sim.joy.y = y / len;
  };

  const releaseJoystick = (event) => {
    const sim = stateRef.current;
    if (!sim) return;
    if (sim.joy.id !== null && sim.joy.id !== event.pointerId) return;
    // Release the pointer capture taken in updateJoystick — otherwise the
    // pad keeps swallowing pointer events for that pointer id until the
    // browser tears it down.
    const pad = joystickRef.current;
    if (pad && event.pointerId != null) {
      try {
        pad.releasePointerCapture(event.pointerId);
      } catch {}
    }
    sim.joy = { x: 0, y: 0, active: false, id: null };
  };

  const jumpRover = (event) => {
    event.preventDefault();
    const sim = stateRef.current;
    if (!sim) return;
    sim.jumpQueued = true;
  };

  const guideRover = (event) => {
    event.preventDefault();
    const sim = stateRef.current;
    if (!sim) return;
    const target = MOON_ROVER_SAMPLES.find(
      (sample) => !sim.collected.has(sample.id),
    );
    if (!target) return;
    const guide = Math.atan2(
      target.x - sim.position.x,
      target.z - sim.position.z,
    );
    sim.heading = guide;
    sim.visualHeading = guide;
    sim.speed = Math.max(2.2, Math.abs(sim.speed));
    setToast({
      title: "Guide beam locked",
      text: `Rover is pointing toward ${target.label}. Drive forward!`,
    });
    if (window.playKidSound) window.playKidSound("boop");
  };

  return (
    <section className="moon-rover-level" aria-label="Moon Rover driving game">
      <div ref={mountRef} className="moon-rover-stage" />
      <div className="moon-rover-ui">
        <button className="moon-rover-back" onClick={onExit}>
          Back
        </button>
        <div className="moon-rover-hud" aria-live="polite">
          <div className="moon-rover-mission-card">
            <strong>{missionTitle}</strong>
            <span>{missionHint}</span>
          </div>
          <div className="moon-rover-badges">
            <span>
              Samples {collected.length}/{MOON_ROVER_SAMPLES.length}
            </span>
            <span>Speed {speedLabel}</span>
            <span
              className={
                "moon-rover-next-distance " +
                (nextDistanceClose ? "is-close" : "")
              }
            >
              Next {nextDistanceLabel}
            </span>
            <span className="moon-rover-next-direction">
              Target {nextDirectionLabel}
            </span>
            <span className={airborne ? "is-airborne" : ""}>
              Moon gravity: 1/6 Earth
            </span>
          </div>
          <div
            className="moon-rover-sample-dots"
            aria-label={`${collected.length} of ${MOON_ROVER_SAMPLES.length} samples collected`}
          >
            {MOON_ROVER_SAMPLES.map((sample) => (
              <span
                key={sample.id}
                className={collected.includes(sample.id) ? "is-collected" : ""}
                style={{ "--sample-color": sample.color }}
                title={sample.label}
              />
            ))}
          </div>
        </div>
        <div
          className="moon-rover-toast"
          key={`${toast.title}-${collected.length}`}
        >
          <strong>{toast.title}</strong>
          <span>{toast.text}</span>
        </div>
        {remaining === 0 ? (
          <div className="moon-rover-finish">
            <strong>Science basket full!</strong>
            <span>Every sample is safely aboard.</span>
            <button onClick={onExit}>Back to planets</button>
          </div>
        ) : null}
        <div className="moon-rover-controls" aria-label="Drive pad">
          <div
            ref={joystickRef}
            className="moon-rover-joystick"
            aria-label="Drag joystick to drive the rover"
            onPointerDown={(event) => updateJoystick(event, true)}
            onPointerMove={(event) => updateJoystick(event, false)}
            onPointerUp={releaseJoystick}
            onPointerCancel={releaseJoystick}
          >
            <i className="moon-rover-joy-arrow forward" aria-hidden="true">
              ↑
            </i>
            <i className="moon-rover-joy-arrow left" aria-hidden="true">
              ←
            </i>
            <i className="moon-rover-joy-arrow right" aria-hidden="true">
              →
            </i>
            <span>Drive</span>
          </div>
          <button
            className="moon-rover-jump"
            type="button"
            onPointerDown={jumpRover}
          >
            Jump
          </button>
          <button
            className="moon-rover-guide"
            type="button"
            onPointerDown={guideRover}
          >
            Guide
          </button>
        </div>
      </div>
    </section>
  );
}
