// Mars Rover level — Three.js driving scene inspired by Moon Rover, with
// kid-sized Mars science stops, mountains, and dust storms.

const {
  useEffect: _marsRoverUseEffect,
  useRef: _marsRoverUseRef,
  useState: _marsRoverUseState,
} = React;

const marsRoverUseEffect = _marsRoverUseEffect;
const marsRoverUseRef = _marsRoverUseRef;
const marsRoverUseState = _marsRoverUseState;

const MARS_ROVER_MAP_WIDTH = 720;
const MARS_ROVER_MAP_DEPTH = 360;
const MARS_ROVER_WORLD_LIMIT_X = MARS_ROVER_MAP_WIDTH / 2 - 34;
const MARS_ROVER_WORLD_LIMIT_Z = MARS_ROVER_MAP_DEPTH / 2 - 30;
const MARS_ROVER_START = { x: -306, z: 126, heading: 2.82 };
const MARS_ROVER_PROP_ASSET_BASE = "public/generated-assets/mars-rover/props";
const MARS_ROVER_SCANNER_ATLAS = `${MARS_ROVER_PROP_ASSET_BASE}/scanner-beacon-impostor-atlas.png`;
const MARS_ROVER_ORBITER_ATLAS = `${MARS_ROVER_PROP_ASSET_BASE}/orbiter-impostor-atlas.png`;
const MARS_ROVER_DEFAULT_PROP_SCALE = {
  width: 6.4,
  height: 6.4,
  lift: 2.45,
  shadow: 2.3,
  haze: 0.24,
  hazeScale: 1.85,
  opacity: 0.72,
};
const MARS_ROVER_STORM_DODGES_TO_PASS = 3;
const MARS_ROVER_PICKUP_RADIUS = 24;
const MARS_ROVER_DRIVE_OVER_RADIUS = 36;
const MARS_ROVER_PROP_VISIT_RADIUS = 28;
const MARS_ROVER_STOP_POINTS = 100;
const MARS_ROVER_PROP_POINTS = 25;
const MARS_ROVER_TRACK_MARK_COUNT = 72;
const MARS_ROVER_WAKE_DUST_COUNT = 48;
const MARS_ROVER_INTRO_LINE =
  "Mission route: orient the rover, test tools, look below ground, check weather and power, then build a rock story.";
const MARS_ROVER_ROUTE_POINTS_PER_LEG = 5;
const MARS_ROVER_STOPS = [
  {
    id: "pass",
    title: "Mountain Pass",
    stage: "1 Orientation",
    shortTitle: "Orient",
    shortTask: "Start at the pass.",
    shortAction: "Scan",
    route: "Orientation: read the horizon before the science work begins.",
    task: "Drive toward the glowing pass and scan the cliffs to map the route.",
    action: "Scan cliffs",
    x: -252,
    z: 58,
    color: 0xffd36f,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/scanner-beacon-v2.png`,
    atlas: MARS_ROVER_SCANNER_ATLAS,
    propScale: { width: 7.8, height: 7.8, lift: 3.05, shadow: 2.8, haze: 0.24, hazeScale: 1.75, opacity: 0.78 },
    fact: "Big Martian landforms, including giant volcanoes and long canyons, help scientists choose safe routes.",
    clip: "game_mars_rover_pass.mp3",
    doneClip: "game_mars_rover_pass_done.mp3",
    grokClip: "game_mars_rover_grok_pass.mp3",
    guide:
      "First, orient the rover. The cliffs stand in for Mars canyon walls, so scan them and decide where the route should lead.",
    success:
      "Orientation scan complete. You mapped the land before touching a sample.",
  },
  {
    id: "sample",
    title: "Sample Tube",
    stage: "2 Tools and Samples",
    shortTitle: "Sample",
    shortTask: "Collect a clean sample.",
    shortAction: "Scoop",
    route: "Tools: collect carefully so the evidence stays useful.",
    task: "Scoop red dust into the clean sample tube without mixing it with rover dirt.",
    action: "Scoop dust",
    x: -178,
    z: 122,
    color: 0xffb66f,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/sample-tube-v2.png`,
    propScale: { width: 5.4, height: 8.1, lift: 3.25, shadow: 2.35, haze: 0.22, opacity: 0.86 },
    fact: "A rover can seal tiny samples so scientists can study them later in a clean lab.",
    clip: null,
    doneClip: null,
    grokClip: "game_mars_rover_grok_sample.mp3",
    miniSteps: 3,
    miniVerb: "Scoop",
    miniCues: ["Lower scoop.", "Seal tube.", "Log sample."],
    guide:
      "Now use the rover tools. A sample tube protects one tiny piece of Mars so the team can compare it with the rest of the route.",
    success:
      "Sample sealed. The mission now has a real piece of the surface to compare with deeper clues.",
  },
  {
    id: "ice",
    title: "Ice Drill",
    stage: "3 Below the Surface",
    shortTitle: "Ice",
    shortTask: "Search for buried ice.",
    shortAction: "Drill",
    route: "Subsurface: Mars clues can hide under the red dust.",
    task: "Drill the pale patch to test for hidden ice below the surface.",
    action: "Drill ice",
    x: -112,
    z: -70,
    color: 0x8feaff,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/ice-core-v2.png`,
    propScale: { width: 6.2, height: 7.6, lift: 3.05, shadow: 2.35, haze: 0.22, opacity: 0.84 },
    fact: "Some Mars ice hides under red dust, especially in cold places and shadowed ground.",
    clip: "game_mars_rover_ice.mp3",
    doneClip: "game_mars_rover_ice_done.mp3",
    grokClip: "game_mars_rover_grok_ice.mp3",
    guide:
      "The surface is only the top page. Drill gently to learn whether water ice is hidden under the dust.",
    success:
      "Ice check logged. Subsurface clues can tell whether Mars once stored more water nearby.",
  },
  {
    id: "radar",
    title: "Radar Sled",
    stage: "3 Below the Surface",
    shortTitle: "Radar",
    shortTask: "Ping below the dust.",
    shortAction: "Ping",
    route: "Subsurface: compare the drill spot with a wider radar scan.",
    task: "Ping the ground to look for layers, ice, or buried rock shapes.",
    action: "Ping radar",
    x: -28,
    z: -128,
    color: 0x8feaff,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/radar-sled-v2.png`,
    propScale: { width: 8.2, height: 5.8, lift: 2.35, shadow: 2.9, haze: 0.18, opacity: 0.82 },
    fact: "Radar can help a rover look below dusty ground without digging everywhere.",
    clip: null,
    doneClip: null,
    grokClip: "game_mars_rover_grok_radar.mp3",
    miniSteps: 3,
    miniVerb: "Ping",
    miniCues: ["Send ping.", "Listen back.", "Mark layer."],
    guide:
      "Radar gives the rover a bigger underground picture. Send pings, listen for echoes, and mark the strongest layer.",
    success:
      "Radar map ready. The rover now has both a drill clue and a wider underground clue.",
  },
  {
    id: "satellite",
    title: "Satellite Beacon",
    stage: "3 Communications",
    shortTitle: "Relay",
    shortTask: "Send the data home.",
    shortAction: "Signal",
    route: "Communications: science only helps when the data gets home.",
    task: "Find the satellite beacon and blink it so the route data can relay to Earth.",
    action: "Blink beacon",
    x: 72,
    z: -30,
    color: 0x7ee7ff,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/satellite-beacon-v2.png`,
    atlas: MARS_ROVER_ORBITER_ATLAS,
    propScale: {
      width: 9.2,
      height: 9.2,
      lift: 7.4,
      shadow: 3.1,
      haze: 0.58,
      hazeScale: 2.4,
      hazeOverlay: true,
      opacity: 0.72,
    },
    fact: "Orbiters above Mars can relay rover messages when Earth is not in direct view.",
    clip: null,
    doneClip: null,
    grokClip: "game_mars_rover_grok_satellite.mp3",
    miniSteps: 3,
    miniVerb: "Blink",
    miniCues: ["Aim antenna.", "Blink beacon.", "Confirm relay."],
    guide:
      "Pause for communications. The rover has useful samples and subsurface data, so the satellite must relay the notes home.",
    success:
      "Relay confirmed. The team on Earth can now read the rover's first science packet.",
  },
  {
    id: "storm",
    title: "Dust Storm",
    stage: "4 Weather and Energy",
    shortTitle: "Storm",
    shortTask: "Weather alert.",
    shortAction: "Shield",
    route: "Weather: thin air can still move fine dust and change the mission plan.",
    task: "Hold the shield while wind pushes dust across the route.",
    action: "Hold shield",
    x: 126,
    z: 114,
    color: 0xff9d5a,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/dust-shield-v2.png`,
    propScale: { width: 6.5, height: 6.5, lift: 2.55, shadow: 2.4, haze: 0.22, opacity: 0.8 },
    fact: "Mars wind is thin compared with Earth's air, but it can still lift fine dust into the sky.",
    clip: "game_mars_rover_storm.mp3",
    doneClip: "game_mars_rover_storm_done.mp3",
    grokClip: "game_mars_rover_grok_storm.mp3",
    guide:
      "Weather comes before power planning. Dodge the blowing dust so the rover can keep instruments and panels clear.",
    success:
      "Storm handled. The rover protected its instruments before checking the weather mast.",
    storm: true,
  },
  {
    id: "weather",
    title: "Weather Mast",
    stage: "4 Weather and Energy",
    shortTitle: "Weather",
    shortTask: "Measure the air.",
    shortAction: "Wind",
    route: "Weather: measure wind and temperature after the dust front passes.",
    task: "Read the wind mast, temperature sensor, and vane.",
    action: "Read wind",
    x: 198,
    z: -104,
    color: 0xffc86f,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/weather-mast-v2.png`,
    propScale: { width: 4.8, height: 7.6, lift: 3.05, shadow: 2, haze: 0.2, opacity: 0.78 },
    fact: "A weather mast can measure wind, temperature, and pressure on Mars.",
    clip: null,
    doneClip: null,
    grokClip: "game_mars_rover_grok_weather.mp3",
    miniSteps: 3,
    miniVerb: "Wind",
    miniCues: ["Read vane.", "Check temp.", "Log pressure."],
    guide:
      "The mast turns the storm into data. Read the wind, temperature, and pressure so the team knows the rover's working conditions.",
    success:
      "Weather logged. Now the rover can make a smarter energy plan.",
  },
  {
    id: "solar",
    title: "Solar Ridge",
    stage: "4 Weather and Energy",
    shortTitle: "Power",
    shortTask: "Recharge on the ridge.",
    shortAction: "Sun",
    route: "Energy: aim for sunlight after checking dusty weather.",
    task: "Park high, face the Sun, and aim the solar panel.",
    action: "Aim panel",
    x: 268,
    z: -38,
    color: 0xfff0a8,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/solar-pack-v2.png`,
    propScale: { width: 7.6, height: 5.4, lift: 2.15, shadow: 2.8, haze: 0.18, opacity: 0.82 },
    fact: "Rovers need power from batteries, sunlight, or both, and dust can make sunlight harder to collect.",
    clip: "game_mars_rover_solar.mp3",
    doneClip: "game_mars_rover_solar_done.mp3",
    grokClip: "game_mars_rover_grok_solar.mp3",
    guide:
      "Energy keeps the journey moving. After reading the weather, aim the panel where the rover can collect the most sunlight.",
    success:
      "Power plan set. The rover is ready for the final geology story.",
  },
  {
    id: "layers",
    title: "Layer Match",
    stage: "5 Geology Story",
    shortTitle: "Layers",
    shortTask: "Read the rock layers.",
    shortAction: "Match",
    route: "Geology: layers are the timeline of this place.",
    task: "Match the wavy rock layers to see which bands formed together.",
    action: "Match layers",
    x: 312,
    z: 68,
    color: 0xb8ff9a,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/layered-tablet-v2.png`,
    propScale: { width: 5.8, height: 6.8, lift: 2.65, shadow: 2.1, haze: 0.18, opacity: 0.82 },
    fact: "Rock layers can show where water, wind, and dust moved long ago.",
    clip: null,
    doneClip: null,
    grokClip: "game_mars_rover_grok_layers.mp3",
    miniSteps: 3,
    miniVerb: "Line",
    miniCues: ["Trace low layer.", "Match middle layer.", "Compare top layer."],
    guide:
      "Now turn measurements into a story. Matching layers helps the rover decide what happened here first, next, and last.",
    success:
      "Layer story started. The rover has a timeline to photograph and send home.",
  },
  {
    id: "photo",
    title: "Rock Photo",
    stage: "5 Geology Story",
    shortTitle: "Photo",
    shortTask: "Capture the evidence.",
    shortAction: "Photo",
    route: "Photo story: finish by sending evidence the team can inspect.",
    task: "Frame the layered rock and send home a clear science photo.",
    action: "Send photo",
    x: 246,
    z: 132,
    color: 0xb8ff9a,
    image: `${MARS_ROVER_PROP_ASSET_BASE}/layered-rock-v2.png`,
    propScale: { width: 6.8, height: 5.2, lift: 2.05, shadow: 2.4, haze: 0.18, opacity: 0.82 },
    fact: "A clear rock photo lets scientists compare color, grain size, and layers after the rover drives away.",
    clip: "game_mars_rover_photo.mp3",
    doneClip: "game_mars_rover_photo_done.mp3",
    grokClip: "game_mars_rover_grok_photo.mp3",
    guide:
      "Finish with evidence. A photo preserves the rock story so scientists can study the layers beside the sample, radar, weather, and power notes.",
    success:
      "Photo sent. The route now has a complete science story from landform to evidence.",
  },
];
const MARS_ROVER_DECOR_PROPS = [
  {
    id: "science-crate",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/science-crate-v2.png`,
    x: -318,
    z: -44,
    width: 6.6,
    height: 6.6,
    lift: 2.35,
    shadow: 3.1,
    haze: 0.22,
    opacity: 0.58,
  },
  {
    id: "antenna-relay",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/antenna-relay-v2.png`,
    x: -338,
    z: -112,
    width: 8.2,
    height: 10.8,
    lift: 3.8,
    shadow: 3.05,
    haze: 0.12,
    opacity: 0.72,
  },
  {
    id: "glowing-geode",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/glowing-geode-v2.png`,
    x: -52,
    z: 118,
    width: 6,
    height: 6,
    lift: 1.95,
    shadow: 2.4,
    haze: 0.06,
  },
  {
    id: "fossil-stone",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/fossil-stone-v2.png`,
    x: 56,
    z: -146,
    width: 6.4,
    height: 5,
    lift: 1.7,
    shadow: 2.6,
    haze: 0.06,
  },
  {
    id: "wind-spinner",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/wind-spinner-v2.png`,
    x: 214,
    z: 112,
    width: 4.8,
    height: 6.2,
    lift: 2.55,
    shadow: 2,
    haze: 0.18,
    opacity: 0.68,
  },
  {
    id: "signal-pylon",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/signal-pylon-v2.png`,
    x: 326,
    z: -132,
    width: 7.2,
    height: 9.6,
    lift: 3.35,
    shadow: 2.8,
    haze: 0.08,
  },
  {
    id: "solar-panel-array",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/solar-panel-array-v1.png`,
    x: -196,
    z: 72,
    width: 9.6,
    height: 7.7,
    lift: 3.05,
    shadow: 3.2,
    haze: 0.14,
    opacity: 0.7,
  },
  {
    id: "dusty-solar-array",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/solar-panel-array-v1.png`,
    x: 238,
    z: -92,
    width: 8.2,
    height: 6.6,
    lift: 2.75,
    shadow: 2.8,
    haze: 0.16,
    opacity: 0.62,
  },
  {
    id: "route-crate",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/science-crate-v2.png`,
    x: -224,
    z: 16,
    width: 5.6,
    height: 5.6,
    lift: 2.05,
    shadow: 2.5,
    haze: 0.24,
    opacity: 0.56,
  },
  {
    id: "route-geode",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/glowing-geode-v2.png`,
    x: -194,
    z: -34,
    width: 6.6,
    height: 6.6,
    lift: 2.1,
    shadow: 2.3,
    haze: 0.08,
  },
  {
    id: "route-relay",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/antenna-relay-v2.png`,
    x: -156,
    z: 44,
    width: 6.2,
    height: 8.2,
    lift: 2.9,
    shadow: 2.2,
    haze: 0.08,
  },
  {
    id: "route-fossil",
    image: `${MARS_ROVER_PROP_ASSET_BASE}/fossil-stone-v2.png`,
    x: -122,
    z: -42,
    width: 6.4,
    height: 5,
    lift: 1.7,
    shadow: 2.4,
    haze: 0.07,
  },
];
const MARS_ROVER_DECOR_PROP_FACTS = {
  "science-crate": {
    title: "Science Crate",
    fact: "A sealed field crate keeps tools clean when red dust blows across Mars.",
    grokClip: "game_mars_rover_prop_science_crate.mp3",
    icon: "▣",
  },
  "antenna-relay": {
    title: "Antenna Relay",
    fact: "A relay dish can pass rover messages along when hills block a direct signal.",
    grokClip: "game_mars_rover_prop_antenna_relay.mp3",
    icon: "◌",
  },
  "glowing-geode": {
    title: "Glowing Geode",
    fact: "Bright mineral pockets are clues that water may have changed rocks long ago.",
    grokClip: "game_mars_rover_prop_glowing_geode.mp3",
    icon: "◇",
  },
  "fossil-stone": {
    title: "Fossil Stone",
    fact: "Patterned stones make scientists slow down and compare shapes carefully.",
    grokClip: "game_mars_rover_prop_fossil_stone.mp3",
    icon: "◎",
  },
  "wind-spinner": {
    title: "Wind Spinner",
    fact: "A spinner makes invisible air movement easier to see in the thin Martian sky.",
    grokClip: "game_mars_rover_prop_wind_spinner.mp3",
    icon: "✦",
  },
  "signal-pylon": {
    title: "Signal Pylon",
    fact: "A pylon gives the rover a landmark for navigation and radio checks.",
    grokClip: "game_mars_rover_prop_signal_pylon.mp3",
    icon: "⌁",
  },
  "solar-panel-array": {
    title: "Solar Panel Array",
    fact: "Solar panels turn sunlight into power, but Martian dust can make them less efficient.",
    grokClip: "game_mars_rover_grok_solar.mp3",
    icon: "▦",
  },
  "dusty-solar-array": {
    title: "Dusty Solar Array",
    fact: "A dusty panel teaches rover teams to plan energy around weather, wind, and sunlight.",
    grokClip: "game_mars_rover_grok_solar.mp3",
    icon: "▦",
  },
  "route-crate": {
    title: "Route Crate",
    fact: "Supply crates mark the organized route so the rover does not skip a science stage.",
    grokClip: "game_mars_rover_prop_route_crate.mp3",
    icon: "▣",
  },
  "route-geode": {
    title: "Route Geode",
    fact: "A glowing rock waypoint reminds the team to compare surface color with sample notes.",
    grokClip: "game_mars_rover_prop_route_geode.mp3",
    icon: "◇",
  },
  "route-relay": {
    title: "Route Relay",
    fact: "Small relays keep the evidence moving from rover to orbiter to Earth.",
    grokClip: "game_mars_rover_prop_route_relay.mp3",
    icon: "◌",
  },
  "route-fossil": {
    title: "Route Fossil",
    fact: "Strange rock marks are a photo-story clue, not proof by themselves.",
    grokClip: "game_mars_rover_prop_route_fossil.mp3",
    icon: "◎",
  },
};
const MARS_ROVER_PROP_VISITS = [
  ...MARS_ROVER_DECOR_PROPS.map((prop) => ({
    ...prop,
    ...(MARS_ROVER_DECOR_PROP_FACTS[prop.id] || {}),
    points: MARS_ROVER_PROP_POINTS,
    radius: prop.radius || MARS_ROVER_PROP_VISIT_RADIUS,
  })),
  {
    id: "flag-pole",
    title: "Waving Flag",
    x: -300,
    z: -86,
    icon: "⚑",
    points: 40,
    radius: 24,
    fact: "The tall flag pole shows wind direction and gives the rover an easy landmark.",
    grokClip: "game_mars_rover_prop_flag_pole.mp3",
  },
  {
    id: "rocket-launch",
    title: "Launch Rocket",
    x: -236,
    z: -74,
    icon: "△",
    points: 50,
    radius: 30,
    fact: "The launch rocket carries instruments upward while the rover explores the ground.",
    grokClip: "game_mars_rover_prop_rocket_launch.mp3",
  },
  {
    id: "futuristic-windmill",
    title: "Energy Windmill",
    x: -72,
    z: -84,
    icon: "✦",
    points: 45,
    radius: 28,
    fact: "The spinning energy tower turns motion into power for nearby science gear.",
    grokClip: "game_mars_rover_prop_futuristic_windmill.mp3",
  },
  {
    id: "geometry-outpost",
    title: "Geometry Outpost",
    x: 22,
    z: -96,
    icon: "⬡",
    points: 35,
    radius: 30,
    fact: "A clean outpost shape makes shadows and distance easier for drivers to judge.",
    grokClip: "game_mars_rover_prop_geometry_outpost.mp3",
  },
  {
    id: "radio-waves",
    title: "Radio Waves",
    x: -338,
    z: -112,
    icon: "⌁",
    points: 35,
    radius: 24,
    fact: "Subtle radio waves show the relay whispering data across the dusty horizon.",
    grokClip: "game_mars_rover_prop_radio_waves.mp3",
  },
];
const MARS_ROVER_JOURNEY_PHASES = [
  {
    id: "orient",
    label: "Orient",
    stopIds: ["pass"],
    lesson: "Read the land before collecting samples.",
  },
  {
    id: "tools",
    label: "Sample",
    stopIds: ["sample"],
    lesson: "Collect a clean sample before comparing deeper clues.",
  },
  {
    id: "subsurface",
    label: "Below",
    stopIds: ["ice", "radar", "satellite"],
    lesson: "Look below the dust, then relay the data home.",
  },
  {
    id: "weather",
    label: "Weather",
    stopIds: ["storm", "weather", "solar"],
    lesson: "Handle dust, measure the air, and manage rover energy.",
  },
  {
    id: "story",
    label: "Story",
    stopIds: ["layers", "photo"],
    lesson: "Use rock layers and a photo to tell the Mars geology story.",
  },
];
function marsRoverJourneyPhaseForStop(stopId) {
  return (
    MARS_ROVER_JOURNEY_PHASES.find((phase) => phase.stopIds.includes(stopId)) ||
    MARS_ROVER_JOURNEY_PHASES[0]
  );
}
function marsRoverNextUncollectedIndex(collectedIds) {
  const nextIndex = MARS_ROVER_STOPS.findIndex((stop) => !collectedIds.has(stop.id));
  return nextIndex >= 0 ? nextIndex : MARS_ROVER_STOPS.length - 1;
}
function marsRoverStopHitRadius(marker) {
  const propScale = marker?.propScale || MARS_ROVER_DEFAULT_PROP_SCALE;
  return Math.max(
    MARS_ROVER_DRIVE_OVER_RADIUS,
    (propScale.shadow || 0) * 7.5,
    (Math.max(propScale.width || 0, propScale.height || 0) * 4.2),
  );
}
const MARS_ROVER_CRATERS = [
  { x: -248, z: 112, radius: 18, depth: 1.6, rim: 0.58 },
  { x: -286, z: 36, radius: 21, depth: 2.4, rim: 0.95 },
  { x: -172, z: -142, radius: 15, depth: 1.7, rim: 0.62 },
  { x: -18, z: 118, radius: 26, depth: 2.8, rim: 0.78 },
  { x: 148, z: -118, radius: 18, depth: 2.1, rim: 0.7 },
  { x: 286, z: 22, radius: 23, depth: 2.5, rim: 0.86 },
];
const MARS_ROVER_CLIMB_HILLS = [
  { x: -292, z: 94, radiusX: 30, radiusZ: 24, height: 7.4 },
  { x: -256, z: 54, radiusX: 34, radiusZ: 26, height: 6.4 },
  { x: -224, z: 8, radiusX: 46, radiusZ: 34, height: 6.8 },
  { x: -106, z: -70, radiusX: 42, radiusZ: 30, height: 5.4 },
  { x: 46, z: -40, radiusX: 48, radiusZ: 36, height: 5.8 },
  { x: 166, z: 46, radiusX: 52, radiusZ: 38, height: 6.2 },
  { x: 286, z: 82, radiusX: 40, radiusZ: 30, height: 5.6 },
];
const MARS_ROVER_VOLCANO = { x: 292, z: -150, radius: 24, height: 10.4, baseOffset: -0.2 };
const MARS_ROVER_STARGAZER_PLANK = {
  x: -248,
  z: -34,
  width: 126,
  depth: 52,
  height: 5.8,
  rotation: 2.82,
  baseOffset: -0.35,
};

function marsRoverClamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function marsRoverHill(x, z, centerX, centerZ, radiusX, radiusZ, height) {
  const dx = (x - centerX) / radiusX;
  const dz = (z - centerZ) / radiusZ;
  return Math.exp(-(dx * dx + dz * dz)) * height;
}

function marsRoverCraterShape(x, z, crater) {
  const distance = Math.hypot(x - crater.x, z - crater.z);
  const inside = marsRoverClamp(1 - distance / crater.radius, 0, 1);
  const floor = -Math.sin(inside * Math.PI) * crater.depth;
  const rimDistance = Math.abs(distance - crater.radius);
  const rimWidth = crater.radius * 0.22;
  const rim = Math.max(0, 1 - rimDistance / rimWidth) * crater.rim;
  return floor + rim;
}

function marsRoverCraterDarkness(x, z) {
  return MARS_ROVER_CRATERS.reduce((amount, crater) => {
    const distance = Math.hypot(x - crater.x, z - crater.z);
    if (distance > crater.radius * 0.92) return amount;
    return Math.max(amount, 1 - distance / (crater.radius * 0.92));
  }, 0);
}

function marsRoverClimbHillHeight(x, z) {
  return MARS_ROVER_CLIMB_HILLS.reduce(
    (height, hill) =>
      height + marsRoverHill(x, z, hill.x, hill.z, hill.radiusX, hill.radiusZ, hill.height),
    0,
  );
}

function marsRoverClimbHillLight(x, z) {
  return MARS_ROVER_CLIMB_HILLS.reduce((amount, hill) => {
    const dx = (x - hill.x) / hill.radiusX;
    const dz = (z - hill.z) / hill.radiusZ;
    return Math.max(amount, Math.exp(-(dx * dx + dz * dz)));
  }, 0);
}

function marsRoverTerrainHeight(x, z) {
  return (
    Math.sin(x * 0.032) * 1.1 +
    Math.cos(z * 0.046) * 0.86 +
    Math.sin((x + z) * 0.024) * 0.64 +
    Math.cos((x - z) * 0.018) * 0.46 +
    marsRoverHill(x, z, -245, -86, 46, 34, 7.4) +
    marsRoverHill(x, z, -118, 54, 58, 42, 5.2) +
    marsRoverHill(x, z, -8, -122, 64, 36, 4.6) +
    marsRoverHill(x, z, 106, 70, 58, 46, 6.4) +
    marsRoverHill(x, z, 220, -58, 70, 38, 5.8) +
    marsRoverHill(x, z, 312, 118, 50, 38, 4.8) -
    marsRoverHill(x, z, -20, 4, 120, 38, 1.15) +
    marsRoverClimbHillHeight(x, z) +
    MARS_ROVER_CRATERS.reduce(
      (height, crater) => height + marsRoverCraterShape(x, z, crater),
      0,
    )
  );
}

function marsRoverVolcanoSurfaceHeight(x, z) {
  const dx = x - MARS_ROVER_VOLCANO.x;
  const dz = z - MARS_ROVER_VOLCANO.z;
  const distance = Math.hypot(dx, dz);
  if (distance > MARS_ROVER_VOLCANO.radius) return -Infinity;
  const base =
    marsRoverTerrainHeight(MARS_ROVER_VOLCANO.x, MARS_ROVER_VOLCANO.z) +
    MARS_ROVER_VOLCANO.baseOffset;
  const radial = 1 - distance / MARS_ROVER_VOLCANO.radius;
  const softened = Math.pow(marsRoverClamp(radial, 0, 1), 1.08);
  return base + softened * MARS_ROVER_VOLCANO.height;
}

function marsRoverPlankLocal(x, z) {
  const dx = x - MARS_ROVER_STARGAZER_PLANK.x;
  const dz = z - MARS_ROVER_STARGAZER_PLANK.z;
  const cos = Math.cos(-MARS_ROVER_STARGAZER_PLANK.rotation);
  const sin = Math.sin(-MARS_ROVER_STARGAZER_PLANK.rotation);
  return {
    x: dx * cos - dz * sin,
    z: dx * sin + dz * cos,
  };
}

function marsRoverPlankSurfaceHeight(x, z) {
  const local = marsRoverPlankLocal(x, z);
  const halfWidth = MARS_ROVER_STARGAZER_PLANK.width / 2;
  const halfDepth = MARS_ROVER_STARGAZER_PLANK.depth / 2;
  const rampStart = -halfDepth - 18;
  if (Math.abs(local.x) > halfWidth || local.z < rampStart || local.z > halfDepth) {
    return -Infinity;
  }
  const base =
    marsRoverTerrainHeight(MARS_ROVER_STARGAZER_PLANK.x, MARS_ROVER_STARGAZER_PLANK.z) +
    MARS_ROVER_STARGAZER_PLANK.baseOffset;
  if (local.z < -halfDepth) {
    const ramp = marsRoverClamp((local.z - rampStart) / (-halfDepth - rampStart), 0, 1);
    return base + ramp * MARS_ROVER_STARGAZER_PLANK.height;
  }
  return base + MARS_ROVER_STARGAZER_PLANK.height;
}

function marsRoverPlankRevealAmount(x, z, y) {
  const local = marsRoverPlankLocal(x, z);
  const halfWidth = MARS_ROVER_STARGAZER_PLANK.width / 2;
  const halfDepth = MARS_ROVER_STARGAZER_PLANK.depth / 2;
  const nearDeckEdge =
    Math.abs(local.x) < halfWidth * 0.76 &&
    local.z > halfDepth * 0.42 &&
    local.z < halfDepth * 1.08;
  if (!nearDeckEdge) return 0;
  const deckY =
    marsRoverTerrainHeight(MARS_ROVER_STARGAZER_PLANK.x, MARS_ROVER_STARGAZER_PLANK.z) +
    MARS_ROVER_STARGAZER_PLANK.baseOffset +
    MARS_ROVER_STARGAZER_PLANK.height;
  const edgeAmount = marsRoverClamp(
    (local.z - halfDepth * 0.42) / (halfDepth * 0.5),
    0,
    1,
  );
  const heightAmount = marsRoverClamp((y - deckY + 0.55) / 2.4, 0, 1);
  return edgeAmount * heightAmount;
}

function marsRoverDriveHeight(x, z) {
  return Math.max(
    marsRoverTerrainHeight(x, z),
    marsRoverVolcanoSurfaceHeight(x, z),
    marsRoverPlankSurfaceHeight(x, z),
  );
}

function marsRoverSurfaceTint(x, z, y) {
  const banding =
    Math.sin(x * 0.055 + z * 0.012) * 0.5 +
    Math.sin(z * 0.071 - x * 0.018) * 0.34 +
    Math.cos((x + z) * 0.031) * 0.24;
  const color = new THREE.Color(0xa86642);
  color.lerp(new THREE.Color(0x694231), marsRoverClamp(0.1 + banding * 0.08, 0, 0.22));
  color.lerp(new THREE.Color(0xd08b5c), marsRoverClamp((y + 2) * 0.03, 0, 0.24));
  color.lerp(new THREE.Color(0xd39a6b), marsRoverClimbHillLight(x, z) * 0.12);
  color.lerp(new THREE.Color(0x3d2018), marsRoverCraterDarkness(x, z) * 0.2);
  return color;
}

function marsRoverSeeded(index, salt = 0) {
  const value = Math.sin(index * 127.1 + salt * 311.7) * 43758.5453123;
  return value - Math.floor(value);
}

function marsRoverCreateSkyTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 512;
  const ctx = canvas.getContext("2d");
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height);
  sky.addColorStop(0, "#1b1118");
  sky.addColorStop(0.36, "#4d261c");
  sky.addColorStop(0.72, "#b05d36");
  sky.addColorStop(1, "#d28550");
  ctx.fillStyle = sky;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const sun = ctx.createRadialGradient(380, 126, 8, 380, 126, 170);
  sun.addColorStop(0, "rgba(255, 235, 188, 0.52)");
  sun.addColorStop(0.22, "rgba(255, 185, 112, 0.28)");
  sun.addColorStop(1, "rgba(255, 160, 86, 0)");
  ctx.fillStyle = sun;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < 70; i += 1) {
    const x = marsRoverSeeded(i, 1) * canvas.width;
    const y = 18 + marsRoverSeeded(i, 2) * 230;
    const size = 0.7 + marsRoverSeeded(i, 3) * 1.4;
    ctx.fillStyle = `rgba(255, 226, 188, ${0.16 + marsRoverSeeded(i, 4) * 0.28})`;
    ctx.fillRect(x, y, size, size);
  }

  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverCreateSpaceTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 768;
  canvas.height = 768;
  const ctx = canvas.getContext("2d");
  const voidGradient = ctx.createRadialGradient(250, 180, 0, 384, 384, 560);
  voidGradient.addColorStop(0, "#0c1426");
  voidGradient.addColorStop(0.42, "#050914");
  voidGradient.addColorStop(1, "#01030a");
  ctx.fillStyle = voidGradient;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < 240; i += 1) {
    const x = marsRoverSeeded(i, 81) * canvas.width;
    const y = marsRoverSeeded(i, 82) * canvas.height;
    const size = 0.35 + marsRoverSeeded(i, 83) * 1.05;
    const alpha = 0.24 + marsRoverSeeded(i, 84) * 0.42;
    ctx.fillStyle = `rgba(235, 248, 255, ${alpha})`;
    ctx.beginPath();
    ctx.arc(x, y, size, 0, Math.PI * 2);
    ctx.fill();
  }

  for (let i = 0; i < 2; i += 1) {
    const x = 120 + marsRoverSeeded(i, 85) * 520;
    const y = 70 + marsRoverSeeded(i, 86) * 260;
    const nebula = ctx.createRadialGradient(x, y, 0, x, y, 130 + marsRoverSeeded(i, 87) * 90);
    nebula.addColorStop(0, `rgba(93, 176, 255, ${0.025 + marsRoverSeeded(i, 88) * 0.025})`);
    nebula.addColorStop(1, "rgba(93, 176, 255, 0)");
    ctx.fillStyle = nebula;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverCreateRouteSignTexture(THREE, label, accent = "#ffd36f") {
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 96;
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "rgba(28, 12, 10, 0.86)";
  ctx.strokeStyle = "rgba(255, 235, 190, 0.72)";
  ctx.lineWidth = 5;
  ctx.beginPath();
  if (ctx.roundRect) {
    ctx.roundRect(10, 12, 236, 72, 18);
  } else {
    ctx.rect(10, 12, 236, 72);
  }
  ctx.fill();
  ctx.stroke();
  ctx.fillStyle = accent;
  ctx.fillRect(28, 72, 200, 5);
  ctx.fillStyle = "#fff7df";
  ctx.font = "900 34px system-ui, -apple-system, sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText(label, 128, 46);

  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function marsRoverCreateCometTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 192;
  canvas.height = 48;
  const ctx = canvas.getContext("2d");
  const tail = ctx.createLinearGradient(0, 24, 192, 24);
  tail.addColorStop(0, "rgba(140, 205, 255, 0)");
  tail.addColorStop(0.62, "rgba(170, 220, 255, 0.07)");
  tail.addColorStop(0.88, "rgba(255, 242, 198, 0.28)");
  tail.addColorStop(1, "rgba(255, 255, 255, 0.56)");
  ctx.fillStyle = tail;
  ctx.fillRect(0, 22, 190, 4);
  const head = ctx.createRadialGradient(170, 24, 0, 170, 24, 18);
  head.addColorStop(0, "rgba(255, 255, 255, 0.72)");
  head.addColorStop(0.42, "rgba(255, 228, 172, 0.32)");
  head.addColorStop(1, "rgba(255, 228, 172, 0)");
  ctx.fillStyle = head;
  ctx.fillRect(152, 8, 34, 32);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverCreateCometField(THREE) {
  const group = new THREE.Group();
  const texture = marsRoverCreateCometTexture();
  const comets = Array.from({ length: 4 }, (_, index) => {
    const material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      depthTest: false,
      blending: THREE.AdditiveBlending,
      side: THREE.DoubleSide,
    });
    const comet = new THREE.Mesh(new THREE.PlaneGeometry(20 + index * 2.2, 2.4 + index * 0.25), material);
    comet.position.set(
      -145 + marsRoverSeeded(index, 91) * 290,
      62 + marsRoverSeeded(index, 92) * 46,
      112 - marsRoverSeeded(index, 93) * 36,
    );
    comet.rotation.set(-0.18, 0.08, -0.16 - marsRoverSeeded(index, 94) * 0.2);
    comet.userData.speed = 10 + marsRoverSeeded(index, 95) * 18;
    comet.userData.phase = marsRoverSeeded(index, 96);
    comet.renderOrder = -4;
    group.add(comet);
    return comet;
  });
  return {
    group,
    texture,
    comets,
    update(now, reveal) {
      group.visible = reveal > 0.02;
      comets.forEach((comet, index) => {
        const cycle = (comet.userData.phase + now * 0.000035 * comet.userData.speed) % 1;
        comet.position.x = -170 + cycle * 340;
        comet.position.y =
          66 +
          marsRoverSeeded(index, 97) * 48 -
          cycle * (12 + marsRoverSeeded(index, 98) * 18) +
          Math.sin(now * 0.0007 + index) * 1.2;
        const revealGate = marsRoverClamp((reveal - 0.38) / 0.62, 0, 1);
        comet.material.opacity =
          revealGate * (0.08 + Math.sin(cycle * Math.PI) * 0.18) * (0.55 + marsRoverSeeded(index, 99) * 0.22);
      });
    },
    dispose() {
      comets.forEach((comet) => {
        comet.geometry.dispose();
        comet.material.dispose();
      });
      texture.dispose();
    },
  };
}

function marsRoverCreateTerrainTextures() {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 512;
  const ctx = canvas.getContext("2d");
  const base = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
  base.addColorStop(0, "#a86642");
  base.addColorStop(0.45, "#c88456");
  base.addColorStop(1, "#704733");
  ctx.fillStyle = base;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (let i = 0; i < 380; i += 1) {
    const x = marsRoverSeeded(i, 7) * canvas.width;
    const y = marsRoverSeeded(i, 8) * canvas.height;
    const length = 18 + marsRoverSeeded(i, 9) * 84;
    const alpha = 0.035 + marsRoverSeeded(i, 10) * 0.06;
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(-0.16 + marsRoverSeeded(i, 11) * 0.34);
    ctx.fillStyle =
      i % 3 === 0 ? `rgba(232, 171, 112, ${alpha})` : `rgba(62, 35, 26, ${alpha})`;
    ctx.fillRect(-length / 2, -0.8, length, 1.6);
    ctx.restore();
  }

  for (let i = 0; i < 95; i += 1) {
    const x = marsRoverSeeded(i, 12) * canvas.width;
    const y = marsRoverSeeded(i, 13) * canvas.height;
    const radius = 2 + marsRoverSeeded(i, 14) * 8;
    const pebble = ctx.createRadialGradient(x, y, 0, x, y, radius);
    pebble.addColorStop(0, "rgba(58, 22, 14, 0.16)");
    pebble.addColorStop(1, "rgba(58, 22, 14, 0)");
    ctx.fillStyle = pebble;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fill();
  }

  const bumpCanvas = document.createElement("canvas");
  bumpCanvas.width = canvas.width;
  bumpCanvas.height = canvas.height;
  const bumpCtx = bumpCanvas.getContext("2d");
  bumpCtx.fillStyle = "#808080";
  bumpCtx.fillRect(0, 0, bumpCanvas.width, bumpCanvas.height);
  bumpCtx.globalCompositeOperation = "overlay";
  bumpCtx.drawImage(canvas, 0, 0);

  const color = new THREE.CanvasTexture(canvas);
  color.colorSpace = THREE.SRGBColorSpace;
  color.wrapS = THREE.RepeatWrapping;
  color.wrapT = THREE.RepeatWrapping;
  color.repeat.set(12, 7);

  const bump = new THREE.CanvasTexture(bumpCanvas);
  bump.wrapS = THREE.RepeatWrapping;
  bump.wrapT = THREE.RepeatWrapping;
  bump.repeat.copy(color.repeat);
  return { color, bump };
}

function marsRoverCreateMoonTexture(seed = 1) {
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 256;
  const ctx = canvas.getContext("2d");
  const base = ctx.createRadialGradient(82, 64, 18, 142, 142, 150);
  base.addColorStop(0, "#e7c9b3");
  base.addColorStop(0.42, "#b18b76");
  base.addColorStop(1, "#5d463d");
  ctx.fillStyle = base;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < 44; i += 1) {
    const n = Math.sin((i + 1) * 91.7 + seed * 13.1);
    const m = Math.cos((i + 3) * 57.3 + seed * 8.2);
    const x = 128 + n * 96;
    const y = 128 + m * 96;
    const r = 3 + Math.abs(Math.sin(i * 17.9 + seed)) * 18;
    const crater = ctx.createRadialGradient(x - r * 0.3, y - r * 0.35, 1, x, y, r);
    crater.addColorStop(0, "rgba(255,236,212,0.22)");
    crater.addColorStop(0.42, "rgba(82,55,47,0.28)");
    crater.addColorStop(1, "rgba(30,20,18,0)");
    ctx.fillStyle = crater;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();
  }
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverCreatePropHazeTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 256;
  const ctx = canvas.getContext("2d");
  const haze = ctx.createRadialGradient(128, 132, 18, 128, 132, 126);
  haze.addColorStop(0, "rgba(255, 198, 132, 0.52)");
  haze.addColorStop(0.34, "rgba(230, 138, 82, 0.22)");
  haze.addColorStop(0.72, "rgba(190, 84, 45, 0.08)");
  haze.addColorStop(1, "rgba(190, 84, 45, 0)");
  ctx.fillStyle = haze;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < 54; i += 1) {
    const x = marsRoverSeeded(i, 82) * canvas.width;
    const y = marsRoverSeeded(i, 83) * canvas.height;
    const r = 3 + marsRoverSeeded(i, 84) * 14;
    const puff = ctx.createRadialGradient(x, y, 0, x, y, r);
    puff.addColorStop(0, `rgba(255, 211, 154, ${0.06 + marsRoverSeeded(i, 85) * 0.08})`);
    puff.addColorStop(1, "rgba(255, 211, 154, 0)");
    ctx.fillStyle = puff;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();
  }
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverCreateAmericanFlagTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 270;
  const ctx = canvas.getContext("2d");
  const stripeHeight = canvas.height / 13;
  for (let i = 0; i < 13; i += 1) {
    ctx.fillStyle = i % 2 === 0 ? "#b3262f" : "#fff4e2";
    ctx.fillRect(0, i * stripeHeight, canvas.width, stripeHeight + 0.5);
  }
  const cantonWidth = canvas.width * 0.42;
  const cantonHeight = stripeHeight * 7;
  ctx.fillStyle = "#24477d";
  ctx.fillRect(0, 0, cantonWidth, cantonHeight);
  ctx.fillStyle = "rgba(255, 247, 224, 0.92)";
  for (let row = 0; row < 5; row += 1) {
    for (let col = 0; col < 6; col += 1) {
      const x = 18 + col * 31 + (row % 2) * 15.5;
      const y = 15 + row * 24;
      ctx.beginPath();
      ctx.arc(x, y, 3.4, 0, Math.PI * 2);
      ctx.fill();
    }
  }
  const dust = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
  dust.addColorStop(0, "rgba(255, 232, 190, 0.12)");
  dust.addColorStop(0.62, "rgba(180, 79, 42, 0.1)");
  dust.addColorStop(1, "rgba(111, 44, 24, 0.22)");
  ctx.fillStyle = dust;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

function marsRoverTextureFrame(texture, frame, columns = 4, rows = 4) {
  const wrappedFrame = ((frame % (columns * rows)) + columns * rows) % (columns * rows);
  const col = wrappedFrame % columns;
  const row = Math.floor(wrappedFrame / columns);
  texture.repeat.set(1 / columns, 1 / rows);
  texture.offset.set(col / columns, 1 - (row + 1) / rows);
}

function marsRoverCreatePickupBurst(THREE) {
  const count = 44;
  const sparkCanvas = document.createElement("canvas");
  sparkCanvas.width = 64;
  sparkCanvas.height = 64;
  const sparkCtx = sparkCanvas.getContext("2d");
  const spark = sparkCtx.createRadialGradient(32, 32, 0, 32, 32, 31);
  spark.addColorStop(0, "rgba(255, 246, 198, 0.95)");
  spark.addColorStop(0.32, "rgba(255, 211, 111, 0.58)");
  spark.addColorStop(0.72, "rgba(255, 155, 83, 0.16)");
  spark.addColorStop(1, "rgba(255, 155, 83, 0)");
  sparkCtx.fillStyle = spark;
  sparkCtx.fillRect(0, 0, 64, 64);
  const sparkTexture = new THREE.CanvasTexture(sparkCanvas);
  sparkTexture.colorSpace = THREE.SRGBColorSpace;
  const geometry = new THREE.BufferGeometry();
  const positions = new Float32Array(count * 3);
  const velocities = new Float32Array(count * 3);
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  const material = new THREE.PointsMaterial({
    color: 0xffd36f,
    map: sparkTexture,
    size: 0.72,
    transparent: true,
    opacity: 0,
    depthWrite: false,
    depthTest: true,
    blending: THREE.AdditiveBlending,
  });
  const points = new THREE.Points(geometry, material);
  points.visible = false;
  points.renderOrder = 9;
  return {
    points,
    positions,
    velocities,
    age: 1,
    trigger(origin) {
      points.visible = true;
      this.age = 0;
      for (let i = 0; i < count; i += 1) {
        const angle = marsRoverSeeded(i, 101) * Math.PI * 2;
        const radius = 0.3 + marsRoverSeeded(i, 102) * 2.4;
        positions[i * 3] = origin.x + Math.cos(angle) * radius;
        positions[i * 3 + 1] = origin.y + 1.3 + marsRoverSeeded(i, 103) * 2.6;
        positions[i * 3 + 2] = origin.z + Math.sin(angle) * radius;
        velocities[i * 3] = Math.cos(angle) * (1.6 + marsRoverSeeded(i, 104) * 4.2);
        velocities[i * 3 + 1] = 2.8 + marsRoverSeeded(i, 105) * 5.4;
        velocities[i * 3 + 2] = Math.sin(angle) * (1.6 + marsRoverSeeded(i, 106) * 4.2);
      }
      geometry.attributes.position.needsUpdate = true;
    },
    update(dt) {
      if (this.age >= 1) return;
      this.age = Math.min(1, this.age + dt * 1.65);
      for (let i = 0; i < count; i += 1) {
        positions[i * 3] += velocities[i * 3] * dt;
        positions[i * 3 + 1] += velocities[i * 3 + 1] * dt;
        positions[i * 3 + 2] += velocities[i * 3 + 2] * dt;
        velocities[i * 3 + 1] -= 8.2 * dt;
      }
      material.opacity = Math.sin((1 - this.age) * Math.PI) * 0.62;
      geometry.attributes.position.needsUpdate = true;
      if (this.age >= 1) {
        points.visible = false;
        material.opacity = 0;
      }
    },
  };
}

function marsRoverCreateLaunchRocket(THREE) {
  const group = new THREE.Group();
  const extraMaterials = [];
  const shared = {
    white: new THREE.MeshStandardMaterial({
      color: 0xf4eee2,
      roughness: 0.38,
      metalness: 0.22,
      emissive: 0x25170f,
      emissiveIntensity: 0.04,
    }),
    red: new THREE.MeshStandardMaterial({
      color: 0xcd5c5c,
      roughness: 0.48,
      metalness: 0.12,
      emissive: 0x3a0c08,
      emissiveIntensity: 0.08,
    }),
    dark: new THREE.MeshStandardMaterial({ color: 0x3c251c, roughness: 0.85, metalness: 0.04 }),
    metal: new THREE.MeshStandardMaterial({ color: 0xb8a18a, roughness: 0.38, metalness: 0.46 }),
    cyan: new THREE.MeshStandardMaterial({
      color: 0x7ee7ff,
      roughness: 0.28,
      metalness: 0.18,
      emissive: 0x1aa8ff,
      emissiveIntensity: 0.82,
    }),
    orange: new THREE.MeshBasicMaterial({
      color: 0xff6b22,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    }),
    yellow: new THREE.MeshBasicMaterial({
      color: 0xffd36f,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    }),
    flame: new THREE.MeshBasicMaterial({
      color: 0xffc15d,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    }),
    smoke: new THREE.MeshBasicMaterial({
      color: 0xd59a6f,
      transparent: true,
      opacity: 0,
      depthWrite: false,
    }),
  };
  const pad = new THREE.Mesh(new THREE.CylinderGeometry(3.2, 3.8, 0.55, 22), shared.dark);
  pad.position.y = 0.28;
  pad.scale.z = 0.68;
  const mast = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.18, 12.8, 10), shared.metal);
  mast.position.set(-2.6, 6.6, -0.45);
  mast.rotation.z = -0.06;
  const gantry = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.22, 0.22), shared.metal);
  gantry.position.set(-1.9, 9.4, -0.45);
  gantry.rotation.z = -0.2;

  const rocket = new THREE.Group();
  const body = new THREE.Mesh(new THREE.CylinderGeometry(0.66, 0.88, 8.8, 32), shared.white);
  body.position.y = 5.32;
  body.castShadow = true;
  body.receiveShadow = true;
  const lowerBand = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.93, 0.22, 32), shared.metal);
  lowerBand.position.y = 1.05;
  const upperBand = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.73, 0.16, 32), shared.metal);
  upperBand.position.y = 8.8;
  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.68, 2.45, 32), shared.red);
  nose.position.y = 10.96;
  nose.castShadow = true;
  const noseRim = new THREE.Mesh(new THREE.TorusGeometry(0.69, 0.035, 8, 40), shared.metal);
  noseRim.position.y = 9.74;
  noseRim.rotation.x = Math.PI / 2;
  const window = new THREE.Mesh(new THREE.SphereGeometry(0.27, 20, 12), shared.metal);
  window.position.set(0, 7.72, 0.67);
  window.scale.set(1.2, 1, 0.28);
  const windowGlow = new THREE.Mesh(new THREE.SphereGeometry(0.18, 16, 10), shared.cyan);
  windowGlow.position.set(0.02, 7.72, 0.83);
  windowGlow.scale.set(1.3, 0.9, 0.22);
  const logoPanelMaterial = new THREE.MeshBasicMaterial({
    color: 0x222222,
    transparent: true,
    opacity: 0.42,
    depthWrite: false,
    side: THREE.DoubleSide,
  });
  extraMaterials.push(logoPanelMaterial);
  const logoPanel = new THREE.Mesh(new THREE.PlaneGeometry(0.62, 0.32), logoPanelMaterial);
  logoPanel.position.set(0, 6.05, 0.882);
  logoPanel.rotation.x = -0.04;
  const logoMarkMaterial = new THREE.MeshBasicMaterial({
    color: 0xeafaff,
    transparent: true,
    opacity: 0.82,
    depthWrite: false,
  });
  extraMaterials.push(logoMarkMaterial);
  const logoMark = new THREE.Mesh(new THREE.TorusGeometry(0.12, 0.015, 6, 20), logoMarkMaterial);
  logoMark.position.set(0, 6.05, 0.896);
  logoMark.rotation.y = Math.PI / 2;
  [2.25, 4.05, 6.85].forEach((y, index) => {
    const seam = new THREE.Mesh(new THREE.TorusGeometry(0.78 - index * 0.04, 0.012, 6, 36), shared.metal);
    seam.position.y = y;
    seam.rotation.x = Math.PI / 2;
    rocket.add(seam);
  });
  [3.2, 5.2, 7.15].forEach((y, row) => {
    for (let i = 0; i < 8; i += 1) {
      const angle = i * Math.PI * 2 / 8 + row * 0.1;
      const rivet = new THREE.Mesh(new THREE.SphereGeometry(0.035, 8, 6), shared.metal);
      rivet.position.set(Math.sin(angle) * 0.79, y, Math.cos(angle) * 0.79);
      rocket.add(rivet);
    }
  });
  [3.25, 6.55].forEach((y) => {
    const accent = new THREE.Mesh(new THREE.TorusGeometry(0.81, 0.026, 8, 42), shared.cyan);
    accent.position.y = y;
    accent.rotation.x = Math.PI / 2;
    rocket.add(accent);
  });
  for (let i = 0; i < 4; i += 1) {
    const angle = i * (Math.PI * 2 / 4);
    const fin = new THREE.Mesh(new THREE.BoxGeometry(0.22, 2.05, 0.78), shared.red);
    fin.position.set(Math.sin(angle) * 0.93, 1.82, Math.cos(angle) * 0.93);
    fin.rotation.y = angle;
    fin.rotation.z = 0.18;
    fin.castShadow = true;
    rocket.add(fin);
    const smallFin = new THREE.Mesh(new THREE.BoxGeometry(0.12, 1.05, 0.42), shared.red);
    smallFin.position.set(Math.sin(angle + Math.PI / 4) * 0.82, 2.6, Math.cos(angle + Math.PI / 4) * 0.82);
    smallFin.rotation.y = angle + Math.PI / 4;
    smallFin.rotation.z = 0.12;
    rocket.add(smallFin);
  }
  const engine = new THREE.Mesh(new THREE.CylinderGeometry(0.62, 0.78, 0.72, 24), shared.dark);
  engine.position.y = 0.9;
  engine.castShadow = true;
  const nozzles = Array.from({ length: 6 }, (_, index) => {
    const angle = index * Math.PI * 2 / 6;
    const nozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.15, 0.36, 12), shared.orange);
    nozzle.position.set(Math.sin(angle) * 0.34, 0.48, Math.cos(angle) * 0.34);
    nozzle.userData.phase = index * 0.8;
    rocket.add(nozzle);
    return nozzle;
  });
  const flames = Array.from({ length: 6 }, (_, index) => {
    const angle = index * Math.PI * 2 / 6;
    const flameRoot = new THREE.Group();
    flameRoot.position.set(Math.sin(angle) * 0.34, 0.22, Math.cos(angle) * 0.34);
    const outer = new THREE.Mesh(new THREE.ConeGeometry(0.18, 1.7, 14), shared.yellow);
    outer.rotation.x = Math.PI;
    outer.position.y = -0.68;
    const inner = new THREE.Mesh(new THREE.ConeGeometry(0.11, 1.25, 12), shared.orange);
    inner.rotation.x = Math.PI;
    inner.position.y = -0.52;
    flameRoot.add(outer, inner);
    flameRoot.userData.phase = index * 0.72;
    rocket.add(flameRoot);
    return { root: flameRoot, outer, inner };
  });
  rocket.add(body, lowerBand, upperBand, nose, noseRim, window, windowGlow, logoPanel, logoMark, engine);
  rocket.position.y = 0.3;
  group.add(pad, mast, gantry, rocket);

  const smoke = Array.from({ length: 11 }, (_, index) => {
    const puff = new THREE.Mesh(new THREE.SphereGeometry(0.8 + (index % 4) * 0.18, 12, 8), shared.smoke);
    puff.position.set((marsRoverSeeded(index, 120) - 0.5) * 5, 0.8, (marsRoverSeeded(index, 121) - 0.5) * 3.2);
    puff.scale.set(1.4, 0.38, 0.82);
    puff.userData.phase = marsRoverSeeded(index, 122) * Math.PI * 2;
    group.add(puff);
    return puff;
  });
  const emberCount = 48;
  const emberPositions = new Float32Array(emberCount * 3);
  const emberGeometry = new THREE.BufferGeometry();
  emberGeometry.setAttribute("position", new THREE.BufferAttribute(emberPositions, 3));
  const emberMaterial = new THREE.PointsMaterial({
    color: 0xffb65c,
    size: 0.17,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  extraMaterials.push(emberMaterial);
  const embers = new THREE.Points(emberGeometry, emberMaterial);
  embers.renderOrder = 9;
  rocket.add(embers);

  return {
    group,
    rocket,
    flame: flames[0]?.outer,
    flames,
    nozzles,
    embers,
    smoke,
    materials: shared,
    startAt: null,
    update(now) {
      if (this.startAt === null) this.startAt = now;
      const elapsed = now - this.startAt;
      const cycle = ((elapsed * 0.001) % 13) / 13;
      const launch = marsRoverClamp((cycle - 0.18) / 0.52, 0, 1);
      const falloff = 1 - marsRoverClamp((cycle - 0.76) / 0.16, 0, 1);
      const lift = launch * launch * 24;
      const idleHover = Math.sin(now * 0.0019) * 0.08 * (1 - launch);
      rocket.position.y = 0.3 + idleHover + lift;
      rocket.rotation.y = Math.sin(now * 0.00055) * 0.08 * (1 - launch);
      rocket.rotation.z = Math.sin(now * 0.0024) * 0.012 * (1 + launch * 3);
      flames.forEach((flameSet, index) => {
        const flicker = 0.82 + Math.sin(now * 0.032 + flameSet.root.userData.phase) * 0.18;
        const visible = launch > 0.02 && falloff > 0.01;
        flameSet.root.visible = visible;
        flameSet.outer.material.opacity = (0.22 + Math.sin(launch * Math.PI) * 0.62) * launch * falloff;
        flameSet.inner.material.opacity = (0.36 + Math.sin(launch * Math.PI) * 0.64) * launch * falloff;
        flameSet.outer.scale.set(0.9 * flicker, 0.78 + launch * 0.75, 0.9 * flicker);
        flameSet.inner.scale.set(0.72 * flicker, 0.7 + launch * 0.6, 0.72 * flicker);
      });
      nozzles.forEach((nozzle, index) => {
        nozzle.material.opacity = 0.38 + launch * falloff * (0.52 + Math.sin(now * 0.02 + index) * 0.1);
      });
      embers.visible = launch > 0.02 && falloff > 0.01;
      emberMaterial.opacity = launch * falloff * 0.72;
      for (let i = 0; i < emberCount; i += 1) {
        const seedA = marsRoverSeeded(i, 140);
        const seedB = marsRoverSeeded(i, 141);
        const seedC = marsRoverSeeded(i, 142);
        const t = (seedA + now * 0.0018) % 1;
        const angle = seedB * Math.PI * 2 + Math.sin(now * 0.006 + i) * 0.16;
        const radius = 0.2 + seedC * 0.42 + t * 0.7;
        const index3 = i * 3;
        emberPositions[index3] = Math.sin(angle) * radius;
        emberPositions[index3 + 1] = 0.12 - t * (2.15 + seedB * 1.25);
        emberPositions[index3 + 2] = Math.cos(angle) * radius;
      }
      emberGeometry.attributes.position.needsUpdate = true;
      smoke.forEach((puff, index) => {
        const spread = launch * (2.8 + index * 0.18);
        puff.position.x = (marsRoverSeeded(index, 120) - 0.5) * (5 + spread);
        puff.position.z = (marsRoverSeeded(index, 121) - 0.5) * (3.2 + spread);
        puff.position.y = 0.75 + launch * (1.2 + marsRoverSeeded(index, 123) * 2.2);
        puff.scale.setScalar(0.75 + launch * 1.45 + Math.sin(now * 0.003 + puff.userData.phase) * 0.08);
        puff.scale.y *= 0.34;
        puff.material.opacity = launch * falloff * (0.16 + marsRoverSeeded(index, 124) * 0.11);
      });
    },
    dispose() {
      group.traverse((child) => {
        child.geometry?.dispose?.();
      });
      [...Object.values(shared), ...extraMaterials].forEach((material) => material.dispose?.());
    },
  };
}

function marsRoverCreateGeometryOutpost(THREE) {
  const group = new THREE.Group();
  const materials = {
    shell: new THREE.MeshStandardMaterial({
      color: 0xe6c7a0,
      roughness: 0.76,
      metalness: 0.04,
      transparent: true,
      opacity: 0.82,
    }),
    glass: new THREE.MeshStandardMaterial({
      color: 0xffd36f,
      roughness: 0.32,
      metalness: 0.08,
      transparent: true,
      opacity: 0.34,
      emissive: 0xff9d4d,
      emissiveIntensity: 0.1,
    }),
    metal: new THREE.MeshStandardMaterial({ color: 0xa4866e, roughness: 0.62, metalness: 0.24 }),
    dark: new THREE.MeshStandardMaterial({ color: 0x3a2017, roughness: 0.92 }),
    panel: new THREE.MeshStandardMaterial({
      color: 0x244a66,
      roughness: 0.42,
      metalness: 0.12,
      emissive: 0x082235,
      emissiveIntensity: 0.18,
    }),
  };
  const addGroundPad = (parent, radius, y = 0.05) => {
    const pad = new THREE.Mesh(
      new THREE.CircleGeometry(radius, 32),
      new THREE.MeshBasicMaterial({
        color: 0x5f2d1f,
        transparent: true,
        opacity: 0.18,
        depthWrite: false,
      }),
    );
    pad.rotation.x = -Math.PI / 2;
    pad.position.y = y;
    pad.scale.z = 0.42;
    parent.add(pad);
    return pad;
  };

  const habitat = new THREE.Group();
  const dome = new THREE.Mesh(new THREE.SphereGeometry(4.4, 28, 14, 0, Math.PI * 2, 0, Math.PI / 2), materials.glass);
  dome.scale.y = 0.74;
  dome.position.y = 1.4;
  const base = new THREE.Mesh(new THREE.CylinderGeometry(4.8, 5.1, 0.7, 28), materials.shell);
  base.position.y = 0.35;
  const door = new THREE.Mesh(new THREE.BoxGeometry(1.3, 1.6, 0.2), materials.dark);
  door.position.set(0, 0.9, 4.8);
  addGroundPad(habitat, 6.1);
  habitat.add(base, dome, door);
  habitat.position.set(-74, 0, 118);
  habitat.rotation.y = -0.42;
  group.add(habitat);

  const dish = new THREE.Group();
  const mast = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.2, 5.4, 12), materials.metal);
  mast.position.y = 2.7;
  const bowl = new THREE.Mesh(new THREE.SphereGeometry(2.2, 24, 12, 0, Math.PI * 2, 0, Math.PI / 2), materials.shell);
  bowl.position.y = 5.4;
  bowl.rotation.x = Math.PI * 0.74;
  bowl.scale.set(1.15, 0.42, 1);
  const feed = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 8), materials.panel);
  feed.position.set(0, 4.9, 1.5);
  addGroundPad(dish, 3.4);
  dish.add(mast, bowl, feed);
  dish.position.set(58, 0, 106);
  dish.rotation.y = 0.48;
  group.add(dish);

  const solar = new THREE.Group();
  const stand = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.18, 2.2, 10), materials.metal);
  stand.position.y = 1.1;
  for (let i = 0; i < 3; i += 1) {
    const panel = new THREE.Mesh(new THREE.BoxGeometry(2.7, 0.08, 1.7), materials.panel);
    panel.position.set((i - 1) * 2.85, 2.2, 0);
    panel.rotation.x = -0.45;
    solar.add(panel);
  }
  addGroundPad(solar, 4.2);
  solar.add(stand);
  solar.position.set(136, 0, -132);
  solar.rotation.y = -0.28;
  group.add(solar);

  return {
    group,
    dish,
    solar,
    update(now) {
      dish.rotation.y = 0.48 + Math.sin(now * 0.00065) * 0.16;
      solar.children.forEach((child, index) => {
        if (child.geometry?.type === "BoxGeometry") {
          child.rotation.x = -0.45 + Math.sin(now * 0.0009 + index) * 0.025;
        }
      });
    },
    dispose() {
      group.traverse((child) => child.geometry?.dispose?.());
      Object.values(materials).forEach((material) => material.dispose?.());
    },
  };
}

function marsRoverCreateFuturisticWindmill(THREE) {
  const group = new THREE.Group();
  const materials = {
    base: new THREE.MeshStandardMaterial({ color: 0x9f6845, roughness: 0.82, metalness: 0.08 }),
    metal: new THREE.MeshStandardMaterial({ color: 0xb7a088, roughness: 0.44, metalness: 0.34 }),
    dark: new THREE.MeshStandardMaterial({ color: 0x2e1a14, roughness: 0.88, metalness: 0.08 }),
    blade: new THREE.MeshStandardMaterial({
      color: 0xf3d09d,
      roughness: 0.36,
      metalness: 0.2,
      emissive: 0x3c1708,
      emissiveIntensity: 0.1,
      transparent: true,
      opacity: 0.88,
    }),
    glow: new THREE.MeshBasicMaterial({
      color: 0x72d8ff,
      transparent: true,
      opacity: 0.42,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    }),
    haze: new THREE.MeshBasicMaterial({
      color: 0xd18a5d,
      transparent: true,
      opacity: 0.12,
      depthWrite: false,
    }),
  };

  const groundHaze = new THREE.Mesh(new THREE.CircleGeometry(5.4, 32), materials.haze);
  groundHaze.rotation.x = -Math.PI / 2;
  groundHaze.scale.z = 0.34;
  groundHaze.position.y = 0.04;

  const base = new THREE.Mesh(new THREE.CylinderGeometry(1.25, 1.8, 0.82, 20), materials.base);
  base.position.y = 0.42;
  base.castShadow = true;
  base.receiveShadow = true;

  const mast = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.44, 12.8, 16), materials.metal);
  mast.position.y = 6.8;
  mast.castShadow = true;
  mast.receiveShadow = true;

  const braceMaterial = materials.dark;
  for (let i = 0; i < 3; i += 1) {
    const angle = i * (Math.PI * 2 / 3);
    const brace = new THREE.Mesh(new THREE.BoxGeometry(0.16, 7.6, 0.16), braceMaterial);
    brace.position.set(Math.sin(angle) * 0.62, 4.1, Math.cos(angle) * 0.62);
    brace.rotation.z = Math.sin(angle) * 0.11;
    brace.rotation.x = Math.cos(angle) * -0.11;
    brace.castShadow = true;
    group.add(brace);
  }

  const head = new THREE.Group();
  head.position.y = 13.7;
  head.rotation.y = -0.42;
  const nacelle = new THREE.Mesh(new THREE.CapsuleGeometry(0.52, 1.65, 5, 16), materials.metal);
  nacelle.rotation.z = Math.PI / 2;
  nacelle.castShadow = true;
  const hub = new THREE.Mesh(new THREE.SphereGeometry(0.62, 20, 12), materials.glow);
  hub.position.x = 0.95;
  hub.castShadow = true;

  const rotor = new THREE.Group();
  rotor.position.x = 1.05;
  const bladeShape = new THREE.Shape();
  bladeShape.moveTo(0, -0.2);
  bladeShape.quadraticCurveTo(0.72, -0.14, 1.06, -0.02);
  bladeShape.lineTo(5.3, 0.17);
  bladeShape.quadraticCurveTo(5.65, 0.32, 5.28, 0.48);
  bladeShape.lineTo(0.68, 0.34);
  bladeShape.quadraticCurveTo(0.18, 0.22, 0, -0.2);
  const bladeGeometry = new THREE.ShapeGeometry(bladeShape);
  bladeGeometry.translate(0.34, 0, 0);
  for (let i = 0; i < 3; i += 1) {
    const blade = new THREE.Mesh(bladeGeometry, materials.blade);
    blade.rotation.z = i * (Math.PI * 2 / 3);
    blade.rotation.y = 0.06;
    blade.castShadow = true;
    rotor.add(blade);
  }

  const energyRing = new THREE.Mesh(new THREE.TorusGeometry(4.2, 0.045, 10, 96), materials.glow);
  energyRing.position.x = 1.02;
  energyRing.rotation.y = Math.PI / 2;
  energyRing.renderOrder = 5;
  head.add(nacelle, rotor, hub, energyRing);

  const windRibbons = Array.from({ length: 4 }, (_, index) => {
    const curve = new THREE.CatmullRomCurve3([
      new THREE.Vector3(-4.8, 10.6 + index * 0.62, -1.4 + index * 0.42),
      new THREE.Vector3(-2.2, 11.1 + index * 0.42, -0.8 + index * 0.25),
      new THREE.Vector3(1.4, 11.4 + index * 0.28, -0.18 + index * 0.16),
      new THREE.Vector3(4.8, 11.2 + index * 0.18, 0.36 + index * 0.18),
    ]);
    const ribbon = new THREE.Mesh(
      new THREE.TubeGeometry(curve, 28, 0.035, 6, false),
      materials.glow.clone(),
    );
    ribbon.material.opacity = 0.12;
    ribbon.userData.baseOpacity = 0.1 + index * 0.025;
    ribbon.userData.phase = index * 0.9;
    ribbon.renderOrder = 4;
    group.add(ribbon);
    return ribbon;
  });

  group.add(groundHaze, base, mast, head);
  return {
    group,
    rotor,
    head,
    energyRing,
    windRibbons,
    materials,
    update(now, dt) {
      rotor.rotation.x += dt * 7.8;
      head.rotation.y = -0.42 + Math.sin(now * 0.00055) * 0.18;
      energyRing.rotation.z += dt * 0.7;
      energyRing.material.opacity = 0.24 + Math.sin(now * 0.004) * 0.1;
      materials.glow.opacity = 0.3 + Math.sin(now * 0.0032) * 0.08;
      windRibbons.forEach((ribbon, index) => {
        ribbon.position.x = Math.sin(now * 0.0014 + ribbon.userData.phase) * 0.42;
        ribbon.position.y = Math.sin(now * 0.0018 + index) * 0.16;
        ribbon.material.opacity =
          ribbon.userData.baseOpacity + Math.sin(now * 0.0024 + ribbon.userData.phase) * 0.035;
      });
      groundHaze.material.opacity = 0.11 + Math.sin(now * 0.0018) * 0.025;
    },
    dispose() {
      group.traverse((child) => {
        child.geometry?.dispose?.();
        if (child.material && child.material !== materials.glow) child.material.dispose?.();
      });
      Object.values(materials).forEach((material) => material.dispose?.());
    },
  };
}

function marsRoverCreateSatelliteScan(THREE) {
  const group = new THREE.Group();
  group.visible = false;
  const materials = {
    beam: new THREE.MeshBasicMaterial({
      color: 0x8eeaff,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      depthTest: false,
      side: THREE.DoubleSide,
      blending: THREE.AdditiveBlending,
    }),
    band: new THREE.MeshBasicMaterial({
      color: 0xe6fbff,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      depthTest: false,
      blending: THREE.AdditiveBlending,
    }),
    ground: new THREE.MeshBasicMaterial({
      color: 0x8eeaff,
      transparent: true,
      opacity: 0,
      depthWrite: false,
      depthTest: true,
      blending: THREE.AdditiveBlending,
    }),
  };
  const beam = new THREE.Mesh(new THREE.ConeGeometry(3.8, 10.4, 44, 1, true), materials.beam);
  beam.position.y = 5.25;
  beam.renderOrder = 7;
  const scanBand = new THREE.Mesh(new THREE.TorusGeometry(2.8, 0.045, 8, 72), materials.band);
  scanBand.rotation.x = Math.PI / 2;
  scanBand.renderOrder = 8;
  const groundGlow = new THREE.Mesh(new THREE.CircleGeometry(3.2, 40), materials.ground);
  groundGlow.rotation.x = -Math.PI / 2;
  groundGlow.position.y = 0.08;
  groundGlow.renderOrder = 6;
  group.add(groundGlow, beam, scanBand);
  return {
    group,
    beam,
    scanBand,
    groundGlow,
    materials,
    update(now, rover, satelliteMarker) {
      if (!satelliteMarker) {
        group.visible = false;
        return;
      }
      const distance = Math.hypot(
        rover.position.x - satelliteMarker.group.position.x,
        rover.position.z - satelliteMarker.group.position.z,
      );
      const scanRadius = 24;
      const proximity = marsRoverClamp((scanRadius - distance) / 10, 0, 1);
      if (distance > scanRadius) {
        group.visible = false;
        materials.beam.opacity = 0;
        materials.ground.opacity = 0;
        materials.band.opacity = 0;
        return;
      }
      group.visible = true;
      group.position.set(rover.position.x, rover.position.y - 0.42, rover.position.z);
      group.rotation.y = rover.rotation.y + Math.sin(now * 0.0018) * 0.18;
      const pulse = 0.72 + Math.sin(now * 0.009) * 0.18;
      materials.beam.opacity = 0.26 * proximity * pulse;
      materials.ground.opacity = 0.42 * proximity * pulse;
      const sweep = (now * 0.0015) % 1;
      scanBand.position.y = 0.72 + sweep * 3.15;
      scanBand.scale.setScalar(0.82 + sweep * 0.34);
      materials.band.opacity = 0.9 * proximity * (1 - Math.abs(sweep - 0.5) * 0.55);
      beam.scale.set(0.85 + proximity * 0.18, 1, 0.85 + proximity * 0.18);
      groundGlow.scale.setScalar(0.92 + proximity * 0.2 + Math.sin(now * 0.006) * 0.04);
    },
    dispose() {
      group.traverse((child) => child.geometry?.dispose?.());
      Object.values(materials).forEach((material) => material.dispose?.());
    },
  };
}

function marsRoverCreateRadioWaveRelay(THREE) {
  const group = new THREE.Group();
  const material = new THREE.MeshBasicMaterial({
    color: 0xbff6ff,
    transparent: true,
    opacity: 0.32,
    depthWrite: false,
    depthTest: false,
    blending: THREE.AdditiveBlending,
  });
  const sourceMaterial = new THREE.MeshBasicMaterial({
    color: 0xdffaff,
    transparent: true,
    opacity: 0.42,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const rings = Array.from({ length: 5 }, (_, index) => {
    const ring = new THREE.Mesh(new THREE.TorusGeometry(1.1, 0.055, 8, 80), material.clone());
    ring.rotation.y = Math.PI / 2;
    ring.userData.phase = index / 5;
    ring.renderOrder = 12;
    group.add(ring);
    return ring;
  });
  const source = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 8), sourceMaterial);
  source.position.set(0, 0, 0.12);
  source.renderOrder = 13;
  group.add(source);
  return {
    group,
    rings,
    source,
    material,
    sourceMaterial,
    update(now) {
      rings.forEach((ring, index) => {
        const cycle = (ring.userData.phase + now * 0.00028) % 1;
        const ease = cycle * cycle * (3 - 2 * cycle);
        const scale = 0.95 + ease * 6.4;
        ring.scale.setScalar(scale);
        ring.position.z = ease * 7.8;
        ring.position.y = Math.sin(now * 0.0016 + index) * 0.14;
        ring.material.opacity = (1 - cycle) * (0.46 + Math.sin(now * 0.003 + index) * 0.06);
      });
      source.scale.setScalar(1 + Math.sin(now * 0.006) * 0.22);
      source.material.opacity = 0.5 + Math.sin(now * 0.005) * 0.14;
    },
    dispose() {
      rings.forEach((ring) => {
        ring.geometry.dispose();
        ring.material.dispose();
      });
      source.geometry.dispose();
      source.material.dispose();
      material.dispose();
      sourceMaterial.dispose();
    },
  };
}

function marsRoverResetStormDebris(debris, rover, scatter = 1) {
  const side = Math.random() > 0.5 ? -1 : 1;
  const ahead = 24 + Math.random() * 38 * scatter;
  debris.position.x = rover.position.x + side * ahead;
  debris.position.z = rover.position.z + (Math.random() - 0.5) * 36;
  debris.position.y =
    marsRoverTerrainHeight(debris.position.x, debris.position.z) + 2.4 + Math.random() * 3.8;
  debris.userData.speed = (16 + Math.random() * 10) * -side;
  debris.userData.drift = (Math.random() - 0.5) * 7;
  debris.userData.phase = Math.random() * Math.PI * 2;
  debris.userData.radius = 0.9 + Math.random() * 0.7;
}

function marsRoverClearInput(runtime) {
  Object.keys(runtime?.keysRef?.current || {}).forEach((key) => {
    runtime.keysRef.current[key] = false;
  });
  if (!runtime) return;
  runtime.driveIntent = 0;
  runtime.turnIntent = 0;
}

function MarsRoverLevel({ onExit }) {
  const mountRef = marsRoverUseRef(null);
  const runtimeRef = marsRoverUseRef(null);
  const keysRef = marsRoverUseRef({});
  const promptAutoDismissRef = marsRoverUseRef(null);
  const collectedIdsRef = marsRoverUseRef(new Set());
  const completedRef = marsRoverUseRef(false);
  const [step, setStep] = marsRoverUseState(0);
  const [completed, setCompleted] = marsRoverUseState(false);
  const [collectedIds, setCollectedIds] = marsRoverUseState(() => new Set());
  const [stormHold, setStormHold] = marsRoverUseState(0);
  const [miniProgress, setMiniProgress] = marsRoverUseState(0);
  const [energy, setEnergy] = marsRoverUseState(5);
  const [guideOn, setGuideOn] = marsRoverUseState(() =>
    window.__narration ? window.__narration.isEnabled() : true,
  );
  const [guideLine, setGuideLine] = marsRoverUseState(MARS_ROVER_INTRO_LINE);
  const [message, setMessage] = marsRoverUseState(
    MARS_ROVER_STOPS[0].shortTask,
  );
  const [score, setScore] = marsRoverUseState(0);
  const [collectedStop, setCollectedStop] = marsRoverUseState(null);
  const [propPrompt, setPropPrompt] = marsRoverUseState(null);

  const activeStopIndex =
    completed || !MARS_ROVER_STOPS[step] || collectedIds.has(MARS_ROVER_STOPS[step].id)
      ? marsRoverNextUncollectedIndex(collectedIds)
      : step;
  const activeStop = MARS_ROVER_STOPS[activeStopIndex];
  const completedJobs = completed ? MARS_ROVER_STOPS.length : collectedIds.size;
  const activeMiniSteps = activeStop.miniSteps || 1;
  const routePercent = Math.round((completedJobs / MARS_ROVER_STOPS.length) * 100);
  const activeJourneyPhase = marsRoverJourneyPhaseForStop(activeStop.id);
  const objectiveText = completed
    ? "Mars badge earned"
    : `${activeStop.shortAction}: ${activeStop.shortTitle}`;
  const objectiveDetail = completed
    ? "All science stops collected."
    : `Stop ${activeStopIndex + 1} of ${MARS_ROVER_STOPS.length}`;
  const collectButtonLabel = completed
    ? "Again"
    : activeMiniSteps > 1
      ? `${activeStop.miniVerb || activeStop.shortAction} ${Math.min(miniProgress + 1, activeMiniSteps)}/${activeMiniSteps}`
      : "Collect";

  const getRoverDistance = () => {
    const rover = runtimeRef.current?.rover;
    if (!rover) return 999;
    return Math.hypot(rover.position.x - activeStop.x, rover.position.z - activeStop.z);
  };

  const updateHudDistance = () => {
    const distance = getRoverDistance();
    runtimeRef.current.distance = distance;
    return distance;
  };

  const nearestCollectibleStop = () => {
    const rover = runtimeRef.current?.rover;
    if (!rover || completedRef.current) return null;
    return MARS_ROVER_STOPS.reduce((best, stop, index) => {
      if (collectedIdsRef.current.has(stop.id)) return best;
      const marker = runtimeRef.current?.stopMarkers?.[index];
      const hitRadius = marsRoverStopHitRadius(marker);
      const distance = Math.hypot(rover.position.x - stop.x, rover.position.z - stop.z);
      if (distance > hitRadius) return best;
      if (best && best.distance <= distance) return best;
      return { stop, index, distance };
    }, null);
  };

  const speakGuide = (line, clip, { force = false } = {}) => {
    if (!line) return;
    setGuideLine(line);
    if (!guideOn && !force) return;
    if (!clip || !window.__narration) return;
    window.__narration.play(clip);
  };

  const drive = (turn, forward) => {
    const runtime = runtimeRef.current;
    if (!runtime || completed) return;
    runtime.turnIntent += turn;
    runtime.driveIntent += forward;
    setMessage(activeStop.storm ? "Dust is moving." : activeStop.shortTask);
  };

  const guide = () => {
    const runtime = runtimeRef.current;
    if (!runtime || completed) return;
    const approachHeading = Math.atan2(
      activeStop.x - runtime.rover.position.x,
      activeStop.z - runtime.rover.position.z,
    );
    runtime.rover.position.set(
      activeStop.x - Math.sin(approachHeading) * 7,
      marsRoverTerrainHeight(
        activeStop.x - Math.sin(approachHeading) * 7,
        activeStop.z - Math.cos(approachHeading) * 7,
      ) + 0.45,
      activeStop.z - Math.cos(approachHeading) * 7,
    );
    runtime.rover.rotation.y = approachHeading;
    runtime.heading = approachHeading;
    marsRoverClearInput(runtime);
    setMessage(activeStop.task);
    speakGuide(activeStop.guide, activeStop.clip);
  };

  const collectStopByIndex = (stopIndex) => {
    if (completedRef.current) {
      reset();
      return false;
    }
    const stop = MARS_ROVER_STOPS[stopIndex];
    if (!stop || collectedIdsRef.current.has(stop.id)) return false;
    const nextCollectedIds = new Set(collectedIdsRef.current);
    nextCollectedIds.add(stop.id);
    collectedIdsRef.current = nextCollectedIds;
    const allDone = nextCollectedIds.size >= MARS_ROVER_STOPS.length;
    const runtime = runtimeRef.current;
    runtime?.markStopDone?.(stopIndex);
    if (runtime) runtime.overlayOpen = true;
    setCollectedIds(nextCollectedIds);
    setMessage(stop.success || "Science stop complete.");
    setPropPrompt(null);
    setScore((value) => value + MARS_ROVER_STOP_POINTS);
    setCollectedStop({
      ...stop,
      points: MARS_ROVER_STOP_POINTS,
      eyebrow: "Artifact indexed",
    });
    speakGuide(
      `${stop.success || `${stop.action} complete.`} ${stop.fact}`,
      stop.grokClip || stop.doneClip,
    );
    setEnergy((value) => Math.min(5, value + 1));
    setStormHold(0);
    setMiniProgress(0);

    if (allDone) {
      completedRef.current = true;
      setCompleted(true);
      window.setTimeout(() => {
        const doneLine =
          "Mars badge earned. You completed every science artifact on the route.";
        setMessage("Mars badge!");
        speakGuide(doneLine, "game_mars_rover_done.mp3");
      }, 700);
      return true;
    }

    const nextIndex = marsRoverNextUncollectedIndex(nextCollectedIds);
    window.setTimeout(() => {
      const next = MARS_ROVER_STOPS[nextIndex];
      setStep(nextIndex);
      setMessage(next.shortTask);
      setMiniProgress(0);
    }, 650);
    return true;
  };

  const completeMiniGame = () => {
    if (completed) {
      reset();
      return;
    }
    marsRoverClearInput(runtimeRef.current);
    const nearbyStop = nearestCollectibleStop();
    if (!nearbyStop) {
      updateHudDistance();
      setMessage("Get closer.");
      speakGuide(
        "Drive over any glowing science artifact to collect it.",
        "game_mars_rover_closer.mp3",
      );
      return;
    }
    const stopMiniSteps = nearbyStop.stop.miniSteps || 1;
    if (stopMiniSteps > 1) {
      const nextProgress = Math.min(miniProgress + 1, stopMiniSteps);
      const cue =
        nearbyStop.stop.miniCues?.[nextProgress - 1] ||
        `${nearbyStop.stop.miniVerb || nearbyStop.stop.shortAction} ${nextProgress}.`;
      setMiniProgress(nextProgress);
      setMessage(cue);
      speakGuide(cue, null);
      if (nextProgress < stopMiniSteps) return;
      window.setTimeout(() => collectStopByIndex(nearbyStop.index), 220);
      return;
    }
    collectStopByIndex(nearbyStop.index);
  };

  const reset = () => {
    const runtime = runtimeRef.current;
    if (runtime) {
      if (runtime.collectModalTimer) {
        window.clearTimeout(runtime.collectModalTimer);
      }
      if (promptAutoDismissRef.current) {
        window.clearTimeout(promptAutoDismissRef.current);
        promptAutoDismissRef.current = null;
      }
      runtime.rover.position.set(
        MARS_ROVER_START.x,
        marsRoverTerrainHeight(MARS_ROVER_START.x, MARS_ROVER_START.z) + 0.45,
        MARS_ROVER_START.z,
      );
      runtime.rover.rotation.y = MARS_ROVER_START.heading;
      runtime.heading = MARS_ROVER_START.heading;
      runtime.stormDodgeScore = 0;
      runtime.stormHitCooldown = 0;
      runtime.stormScoreCooldown = 0;
      runtime.spaceReveal = 0;
      runtime.targetSpaceReveal = 0;
      runtime.discoveredProps?.clear?.();
      runtime.overlayOpen = false;
      marsRoverClearInput(runtime);
      runtime.stormDebris?.forEach((debris) => {
        debris.visible = false;
        marsRoverResetStormDebris(debris, runtime.rover, 2);
      });
      runtime.trackMarks?.forEach((mark) => {
        mark.visible = false;
      });
      runtime.trackCursor = 0;
      runtime.lastTrackX = runtime.rover.position.x;
      runtime.lastTrackZ = runtime.rover.position.z;
      runtime.routeGuides?.forEach((guideMark) => {
        guideMark.doneHidden = false;
        if (guideMark.type === "bead") {
          guideMark.mesh.visible = true;
          return;
        }
        guideMark.group.visible = true;
      });
      runtime.stopMarkers.forEach((marker) => {
        marker.userData.done = false;
        marker.core.visible = false;
        marker.ring.visible = true;
        marker.beam.visible = true;
        marker.core.material.color.set(marker.userData.baseColor);
        marker.ring.material.opacity = 0.45;
      });
    }
    setStep(0);
    setCompleted(false);
    completedRef.current = false;
    collectedIdsRef.current = new Set();
    setCollectedIds(new Set());
    setStormHold(0);
    setMiniProgress(0);
    setEnergy(5);
    setScore(0);
    setCollectedStop(null);
    setPropPrompt(null);
    setGuideLine(`${MARS_ROVER_INTRO_LINE} ${MARS_ROVER_STOPS[0].guide}`);
    setMessage(MARS_ROVER_STOPS[0].shortTask);
  };

  marsRoverUseEffect(() => {
    if (!mountRef.current || !window.THREE) return undefined;
    const THREE = window.THREE;
    const mount = mountRef.current;
    const perfProbe =
      window.SpaceExplorerFoundation?.createSpacePerfProbe?.("Mars Rover");
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x2a1514);
    scene.fog = new THREE.FogExp2(0xb97754, 0.0066);

    const camera = new THREE.PerspectiveCamera(
      58,
      mount.clientWidth / Math.max(1, mount.clientHeight),
      0.1,
      520,
    );
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setSize(mount.clientWidth, mount.clientHeight);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.04;
    renderer.shadowMap.enabled = true;
    renderer.domElement.className = "mars-rover-canvas";
    mount.appendChild(renderer.domElement);

    const skyTexture = marsRoverCreateSkyTexture();
    const skyDome = new THREE.Mesh(
      new THREE.SphereGeometry(430, 48, 24),
      new THREE.MeshBasicMaterial({
        map: skyTexture,
        side: THREE.BackSide,
        fog: false,
        transparent: true,
        opacity: 1,
        depthWrite: false,
      }),
    );
    skyDome.renderOrder = -20;
    scene.add(skyDome);
    const spaceTexture = marsRoverCreateSpaceTexture();
    const spaceDome = new THREE.Mesh(
      new THREE.SphereGeometry(426, 48, 24),
      new THREE.MeshBasicMaterial({
        map: spaceTexture,
        side: THREE.BackSide,
        fog: false,
        transparent: true,
        opacity: 0,
        depthWrite: false,
      }),
    );
    spaceDome.renderOrder = -21;
    scene.add(spaceDome);

    const hemi = new THREE.HemisphereLight(0xffe2bc, 0x4c2418, 1.72);
    scene.add(hemi);
    const sun = new THREE.DirectionalLight(0xffdfb8, 1.45);
    sun.position.set(-38, 138, 54);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    sun.shadow.camera.left = -140;
    sun.shadow.camera.right = 140;
    sun.shadow.camera.top = 140;
    sun.shadow.camera.bottom = -140;
    scene.add(sun);

    const skyGroup = new THREE.Group();
    const cometField = marsRoverCreateCometField(THREE);
    cometField.group.visible = false;
    skyGroup.add(cometField.group);
    const atmosphereVeil = new THREE.Mesh(
      new THREE.PlaneGeometry(270, 74, 1, 1),
      new THREE.MeshBasicMaterial({
        color: 0xc66e48,
        transparent: true,
        opacity: 0,
        depthWrite: false,
        depthTest: false,
        blending: THREE.AdditiveBlending,
      }),
    );
    atmosphereVeil.position.set(0, 28, 118);
    atmosphereVeil.renderOrder = -3;
    skyGroup.add(atmosphereVeil);
    const sunGlow = new THREE.Mesh(
      new THREE.SphereGeometry(4.2, 32, 16),
      new THREE.MeshBasicMaterial({
        color: 0xffd8a4,
        transparent: true,
        opacity: 0.12,
        depthWrite: false,
      }),
    );
    sunGlow.position.set(132, 48, 116);
    skyGroup.add(sunGlow);
    const moonTextures = [];
    [
      [102, 40, 98, 2.35, 1, 1.06, 0.82, 0.94],
      [118, 36, 92, 1.35, 2, 0.88, 1.08, 0.9],
    ].forEach(([x, y, z, radius, seed, scaleX, scaleY, scaleZ]) => {
      const moonTexture = marsRoverCreateMoonTexture(seed);
      moonTextures.push(moonTexture);
      const moon = new THREE.Mesh(
        new THREE.IcosahedronGeometry(radius, 2),
        new THREE.MeshStandardMaterial({
          map: moonTexture,
          roughness: 1,
          metalness: 0,
          emissive: 0x1e130f,
          emissiveIntensity: 0.22,
        }),
      );
      moon.position.set(x, y, z);
      moon.scale.set(scaleX, scaleY, scaleZ);
      moon.rotation.set(seed * 0.6, seed * -0.36, seed * 0.22);
      skyGroup.add(moon);
      const glow = new THREE.Mesh(
        new THREE.SphereGeometry(radius * 1.55, 18, 10),
        new THREE.MeshBasicMaterial({
          color: 0xc88a62,
          transparent: true,
          opacity: 0.06,
          depthWrite: false,
        }),
      );
      glow.position.copy(moon.position);
      skyGroup.add(glow);
    });
    scene.add(skyGroup);

    const terrainTextures = marsRoverCreateTerrainTextures();
    const terrainGeometry = new THREE.PlaneGeometry(
      MARS_ROVER_MAP_WIDTH,
      MARS_ROVER_MAP_DEPTH,
      200,
      112,
    );
    terrainGeometry.rotateX(-Math.PI / 2);
    const pos = terrainGeometry.attributes.position;
    const colors = new Float32Array(pos.count * 3);
    for (let i = 0; i < pos.count; i += 1) {
      const x = pos.getX(i);
      const z = pos.getZ(i);
      const y = marsRoverTerrainHeight(x, z);
      pos.setY(i, y);
      const tint = marsRoverSurfaceTint(x, z, y);
      colors[i * 3] = tint.r;
      colors[i * 3 + 1] = tint.g;
      colors[i * 3 + 2] = tint.b;
    }
    terrainGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
    terrainGeometry.computeVertexNormals();
    const terrain = new THREE.Mesh(
      terrainGeometry,
      new THREE.MeshStandardMaterial({
        map: terrainTextures.color,
        bumpMap: terrainTextures.bump,
        bumpScale: 0.72,
        vertexColors: true,
        roughness: 0.98,
        metalness: 0.02,
        emissive: 0x3a1f16,
        emissiveIntensity: 0.24,
      }),
    );
    terrain.receiveShadow = true;
    scene.add(terrain);

    const mountainMaterial = new THREE.MeshStandardMaterial({
      color: 0x7f2f1e,
      roughness: 0.96,
      flatShading: true,
    });
    const mountainCapMaterial = new THREE.MeshStandardMaterial({
      color: 0xa94c2c,
      roughness: 0.98,
      flatShading: true,
    });
    const addDistantButte = (x, z, radius, height, stretch = 0.74) => {
      const body = new THREE.Mesh(
        new THREE.CylinderGeometry(radius * 0.48, radius * 1.14, height, 8, 2),
        mountainMaterial,
      );
      body.position.set(x, marsRoverTerrainHeight(x, z) + height / 2 - 1.2, z);
      body.scale.z = stretch;
      body.rotation.y = x * 0.025;
      body.castShadow = false;
      body.receiveShadow = true;
      scene.add(body);

      const cap = new THREE.Mesh(
        new THREE.CylinderGeometry(radius * 0.5, radius * 0.52, 0.34, 8),
        mountainCapMaterial,
      );
      cap.position.set(x, marsRoverTerrainHeight(x, z) + height - 1.05, z);
      cap.scale.z = stretch * 0.9;
      cap.rotation.y = body.rotation.y;
      cap.receiveShadow = true;
      scene.add(cap);
    };
    [
      [-356, -176, 24, 24, 0.58],
      [-302, -174, 18, 20, 0.7],
      [-228, -176, 25, 26, 0.62],
      [-142, -174, 18, 19, 0.8],
      [-34, -176, 25, 27, 0.64],
      [82, -174, 20, 22, 0.72],
      [190, -176, 28, 29, 0.6],
      [318, -174, 22, 23, 0.74],
      [-356, 176, 28, 27, 0.62],
      [-270, 174, 19, 20, 0.76],
      [-128, 176, 25, 25, 0.66],
      [28, 174, 19, 20, 0.74],
      [172, 176, 25, 24, 0.64],
      [334, 174, 24, 24, 0.72],
    ].forEach(([x, z, radius, height, stretch]) => addDistantButte(x, z, radius, height, stretch));

    const mesaMaterial = new THREE.MeshStandardMaterial({
      color: 0x9a3c23,
      roughness: 0.98,
      flatShading: true,
    });
    const mesaTopMaterial = new THREE.MeshStandardMaterial({
      color: 0xc0673a,
      roughness: 0.98,
      flatShading: true,
    });
    const strataMaterial = new THREE.MeshBasicMaterial({
      color: 0xf0a36c,
      transparent: true,
      opacity: 0.28,
      depthWrite: false,
    });
    const addLayeredMesa = (x, z, width, depth, height, rotation = 0) => {
      const group = new THREE.Group();
      group.position.set(x, marsRoverTerrainHeight(x, z) - 0.3, z);
      group.rotation.y = rotation;
      const body = new THREE.Mesh(
        new THREE.BoxGeometry(width, height, depth, 3, 2, 2),
        mesaMaterial,
      );
      body.position.y = height / 2;
      body.castShadow = false;
      body.receiveShadow = true;
      const cap = new THREE.Mesh(
        new THREE.BoxGeometry(width * 1.03, 0.55, depth * 1.04),
        mesaTopMaterial,
      );
      cap.position.y = height + 0.12;
      cap.castShadow = false;
      cap.receiveShadow = true;
      group.add(body, cap);

      [0.34, 0.55, 0.72].forEach((level, lineIndex) => {
        const front = new THREE.Mesh(
          new THREE.BoxGeometry(width * (0.86 + lineIndex * 0.04), 0.14, 0.08),
          strataMaterial,
        );
        front.position.set(0, height * level, depth / 2 + 0.05);
        const back = front.clone();
        back.position.z = -depth / 2 - 0.05;
        group.add(front, back);
      });
      scene.add(group);
    };
    const addStargazerPlank = () => {
      const { x, z, width, depth, height, rotation, baseOffset } = MARS_ROVER_STARGAZER_PLANK;
      const group = new THREE.Group();
      const baseY = marsRoverTerrainHeight(x, z) + baseOffset;
      group.position.set(x, baseY, z);
      group.rotation.y = rotation;
      const deck = new THREE.Mesh(
        new THREE.BoxGeometry(width, 0.62, depth, 4, 1, 2),
        mesaTopMaterial,
      );
      deck.position.y = height + 0.24;
      deck.castShadow = false;
      deck.receiveShadow = true;
      const body = new THREE.Mesh(
        new THREE.BoxGeometry(width, 1.25, depth, 4, 1, 2),
        mesaMaterial,
      );
      body.position.y = height - 0.48;
      body.castShadow = false;
      body.receiveShadow = true;
      const ramp = new THREE.Mesh(
        new THREE.BoxGeometry(width * 0.94, 0.52, 22, 4, 1, 1),
        mesaTopMaterial,
      );
      ramp.position.set(0, height * 0.5, -depth / 2 - 10.8);
      ramp.rotation.x = -Math.atan2(height, 20);
      ramp.castShadow = false;
      ramp.receiveShadow = true;
      group.add(body, deck, ramp);
      [0.3, 0.52, 0.72].forEach((level, lineIndex) => {
        const front = new THREE.Mesh(
          new THREE.BoxGeometry(width * (0.78 + lineIndex * 0.05), 0.12, 0.08),
          strataMaterial,
        );
        front.position.set(0, height - 0.92 + lineIndex * 0.22, depth / 2 + 0.08);
        const side = front.clone();
        side.position.set(width / 2 + 0.08, height - 0.92 + lineIndex * 0.22, 0);
        side.rotation.y = Math.PI / 2;
        group.add(front, side);
      });
      scene.add(group);
      return group;
    };
    const stargazerPlank = addStargazerPlank();
    [
      [-322, 206, 40, 16, 6.5],
      [-214, -204, 46, 18, 7.2],
      [294, -202, 38, 15, 6.4],
      [-92, 188, 36, 13, 5.6],
      [236, 186, 34, 13, 5.4],
    ].forEach(([x, z, width, depth, height]) =>
      addLayeredMesa(x, z, width, depth, height, (x + z) * 0.018),
    );

    const volcano = new THREE.Group();
    const volcanoBase = new THREE.Mesh(
      new THREE.ConeGeometry(24, 11, 48),
      new THREE.MeshStandardMaterial({ color: 0x7f2b1c, roughness: 0.98 }),
    );
    volcanoBase.position.y = 4.8;
    const caldera = new THREE.Mesh(
      new THREE.TorusGeometry(7.4, 0.8, 12, 48),
      new THREE.MeshBasicMaterial({
        color: 0xffbc73,
        transparent: true,
        opacity: 0.38,
      }),
    );
    caldera.rotation.x = Math.PI / 2;
    caldera.position.y = 10.2;
    volcano.add(volcanoBase, caldera);
    volcano.position.set(292, marsRoverTerrainHeight(292, -150) - 0.2, -150);
    volcano.rotation.y = -0.35;
    scene.add(volcano);

    const canyonMat = new THREE.MeshStandardMaterial({
      color: 0x653324,
      roughness: 1,
      metalness: 0,
      transparent: true,
      opacity: 0.42,
      depthWrite: false,
      side: THREE.DoubleSide,
    });
    [-5, 0, 5].forEach((offset, index) => {
      const canyon = new THREE.Mesh(
        new THREE.PlaneGeometry(132 - index * 15, 3.2 + index * 0.7),
        canyonMat,
      );
      canyon.position.set(
        -24 + index * 2,
        marsRoverTerrainHeight(-24, -8) + 0.09,
        -8 + offset,
      );
      canyon.rotation.x = -Math.PI / 2;
      canyon.rotation.z = -0.12;
      canyon.renderOrder = 2;
      scene.add(canyon);
    });

    const craterFloorMat = new THREE.MeshStandardMaterial({
      color: 0x4f2016,
      roughness: 1,
      transparent: true,
      opacity: 0.46,
      depthWrite: false,
    });
    const craterRimMat = new THREE.MeshStandardMaterial({
      color: 0xb85a31,
      roughness: 1,
      transparent: true,
      opacity: 0.38,
    });
    MARS_ROVER_CRATERS.forEach((crater, index) => {
      const floor = new THREE.Mesh(
        new THREE.CircleGeometry(crater.radius * 0.82, 34),
        craterFloorMat,
      );
      floor.rotation.x = -Math.PI / 2;
      floor.position.set(
        crater.x,
        marsRoverTerrainHeight(crater.x, crater.z) + 0.08,
        crater.z,
      );
      floor.rotation.z = index * 0.74;
      scene.add(floor);

      const rim = new THREE.Mesh(
        new THREE.TorusGeometry(crater.radius, 0.55, 8, 48),
        craterRimMat,
      );
      rim.rotation.x = Math.PI / 2;
      rim.scale.z = 0.18;
      rim.position.set(
        crater.x,
        marsRoverTerrainHeight(crater.x, crater.z) + crater.rim * 0.34,
        crater.z,
      );
      rim.rotation.z = index * 0.41;
      rim.receiveShadow = true;
      scene.add(rim);
    });

    const trailMat = new THREE.MeshStandardMaterial({
      color: 0x5f2418,
      roughness: 1,
      transparent: true,
      opacity: 0.62,
    });
    const routePoints = [
      [MARS_ROVER_START.x, MARS_ROVER_START.z],
      ...MARS_ROVER_STOPS.map((stop) => [stop.x, stop.z]),
    ];
    routePoints.slice(1).forEach(([endX, endZ], segmentIndex) => {
      const [startX, startZ] = routePoints[segmentIndex];
      const steps = 16;
      const routeMarkerGeometry = new THREE.CylinderGeometry(1.05, 1.4, 0.12, 10);
      for (let i = 1; i <= steps; i += 1) {
        if (i % 2 === 0) continue;
        const t = i / steps;
        const x = startX + (endX - startX) * t;
        const z = startZ + (endZ - startZ) * t;
        const marker = new THREE.Mesh(
          routeMarkerGeometry,
          trailMat,
        );
        marker.position.set(x, marsRoverTerrainHeight(x, z) + 0.07, z);
        marker.rotation.y = Math.atan2(endX - startX, endZ - startZ);
        marker.receiveShadow = true;
        scene.add(marker);
      }
    });

    const rippleMat = new THREE.MeshBasicMaterial({
      color: 0xffbb79,
      transparent: true,
      opacity: 0.16,
      depthWrite: false,
      side: THREE.DoubleSide,
    });
    const rippleGeometry = new THREE.PlaneGeometry(1, 0.18);
    for (let i = 0; i < 92; i += 1) {
      const x = (marsRoverSeeded(i, 21) - 0.5) * (MARS_ROVER_MAP_WIDTH - 42);
      const z = (marsRoverSeeded(i, 22) - 0.5) * (MARS_ROVER_MAP_DEPTH - 38);
      const length = 12 + marsRoverSeeded(i, 23) * 42;
      const ripple = new THREE.Mesh(rippleGeometry, rippleMat);
      ripple.position.set(x, marsRoverTerrainHeight(x, z) + 0.065, z);
      ripple.scale.x = length;
      ripple.rotation.x = -Math.PI / 2;
      ripple.rotation.z = -0.16 + marsRoverSeeded(i, 24) * 0.32;
      ripple.renderOrder = 1;
      scene.add(ripple);
    }

    const rockMats = [0x552016, 0x71311e, 0x8c4228, 0x3d1a13].map(
      (color) => new THREE.MeshStandardMaterial({ color, roughness: 0.98, flatShading: true }),
    );
    const rockGeometry = new THREE.DodecahedronGeometry(1, 0);
    for (let i = 0; i < 180; i += 1) {
      const x = (marsRoverSeeded(i, 31) - 0.5) * (MARS_ROVER_MAP_WIDTH - 36);
      const z = (marsRoverSeeded(i, 32) - 0.5) * (MARS_ROVER_MAP_DEPTH - 32);
      const rockSize = 0.35 + marsRoverSeeded(i, 33) * 0.85;
      const rock = new THREE.Mesh(
        rockGeometry,
        rockMats[i % rockMats.length],
      );
      rock.position.set(x, marsRoverTerrainHeight(x, z) + 0.22, z);
      rock.rotation.set(
        marsRoverSeeded(i, 34) * 3,
        marsRoverSeeded(i, 35) * 3,
        marsRoverSeeded(i, 36) * 3,
      );
      rock.scale.set(
        rockSize,
        rockSize * (0.45 + marsRoverSeeded(i, 37) * 0.8),
        rockSize,
      );
      rock.castShadow = true;
      scene.add(rock);
    }

    const rover =
      typeof window.SpaceExplorerCreateRover === "function"
        ? window.SpaceExplorerCreateRover()
        : new THREE.Group();
    rover.name = "Mars Rover";
    rover.position.set(
      MARS_ROVER_START.x,
      marsRoverTerrainHeight(MARS_ROVER_START.x, MARS_ROVER_START.z) + 0.45,
      MARS_ROVER_START.z,
    );
    rover.rotation.y = MARS_ROVER_START.heading;
    rover.scale.setScalar(1.5);
    scene.add(rover);
    rover.traverse((child) => {
      if (child.isMesh) {
        child.castShadow = false;
        child.receiveShadow = true;
      }
    });

    const trackMarkGeometry = new THREE.PlaneGeometry(0.72, 2.75);
    const trackMarkMaterial = new THREE.MeshBasicMaterial({
      color: 0x422016,
      transparent: true,
      opacity: 0.2,
      depthWrite: false,
      side: THREE.DoubleSide,
    });
    const trackMarks = Array.from({ length: MARS_ROVER_TRACK_MARK_COUNT }, () => {
      const mark = new THREE.Mesh(trackMarkGeometry, trackMarkMaterial);
      mark.rotation.x = -Math.PI / 2;
      mark.visible = false;
      mark.renderOrder = 3;
      scene.add(mark);
      return mark;
    });
    const stampTrackPair = (runtime) => {
      const heading = runtime.heading;
      const forwardX = Math.sin(heading);
      const forwardZ = Math.cos(heading);
      const sideX = Math.cos(heading);
      const sideZ = -Math.sin(heading);
      [-2.25, 2.25].forEach((sideOffset) => {
        const mark = trackMarks[runtime.trackCursor % trackMarks.length];
        runtime.trackCursor += 1;
        const x = rover.position.x - forwardX * 1.65 + sideX * sideOffset;
        const z = rover.position.z - forwardZ * 1.65 + sideZ * sideOffset;
        mark.position.set(x, marsRoverDriveHeight(x, z) + 0.085, z);
        mark.rotation.set(-Math.PI / 2, 0, -heading);
        mark.visible = true;
      });
      runtime.lastTrackX = rover.position.x;
      runtime.lastTrackZ = rover.position.z;
    };

    const wakeGeometry = new THREE.BufferGeometry();
    const wakePositions = new Float32Array(MARS_ROVER_WAKE_DUST_COUNT * 3);
    wakeGeometry.setAttribute("position", new THREE.BufferAttribute(wakePositions, 3));
    const wakeDust = new THREE.Points(
      wakeGeometry,
      new THREE.PointsMaterial({
        color: 0xd59461,
        size: 0.42,
        transparent: true,
        opacity: 0,
        depthWrite: false,
      }),
    );
    wakeDust.renderOrder = 4;
    scene.add(wakeDust);

    const setMarsRoverCamera = (blend = 1) => {
      const heading = runtimeRef.current?.heading ?? rover.rotation.y;
      const forwardX = Math.sin(heading);
      const forwardZ = Math.cos(heading);
      const nearFlagLift = marsRoverClamp(
        (70 - Math.hypot(rover.position.x + 300, rover.position.z + 86)) / 70,
        0,
        1,
      );
      const cameraLift = 8.6 + nearFlagLift * 5.2;
      const lookLift = 0.85 + nearFlagLift * 2.1;
      const desiredCamera = new THREE.Vector3(
        rover.position.x - forwardX * 24,
        rover.position.y + cameraLift,
        rover.position.z - forwardZ * 23,
      );
      const lookTarget = new THREE.Vector3(
        rover.position.x + forwardX * 28,
        rover.position.y + lookLift,
        rover.position.z + forwardZ * 28,
      );
      skyGroup.position.set(rover.position.x, rover.position.y, rover.position.z);
      skyGroup.rotation.y = heading;
      camera.position.lerp(desiredCamera, blend);
      camera.lookAt(lookTarget);
      skyDome.position.copy(camera.position);
      spaceDome.position.copy(camera.position);
    };
    setMarsRoverCamera(1);

    const textureLoader = new THREE.TextureLoader();
    const propHazeTexture = marsRoverCreatePropHazeTexture();
    const itemTextures = MARS_ROVER_STOPS.map((stop) => {
      const texture = textureLoader.load(stop.atlas || stop.image);
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.magFilter = THREE.LinearFilter;
      texture.minFilter = THREE.LinearMipmapLinearFilter;
      if (stop.atlas) marsRoverTextureFrame(texture, 0);
      return texture;
    });
    const routeBeadGeometry = new THREE.CircleGeometry(1.35, 18);
    const routeGuides = [];
    const routeAnchors = [
      { x: MARS_ROVER_START.x, z: MARS_ROVER_START.z },
      ...MARS_ROVER_STOPS.map((stop) => ({ x: stop.x, z: stop.z })),
    ];
    for (let legIndex = 0; legIndex < MARS_ROVER_STOPS.length; legIndex += 1) {
      const from = routeAnchors[legIndex];
      const to = routeAnchors[legIndex + 1];
      for (let beadIndex = 1; beadIndex <= MARS_ROVER_ROUTE_POINTS_PER_LEG; beadIndex += 1) {
        const amount = beadIndex / (MARS_ROVER_ROUTE_POINTS_PER_LEG + 1);
        const x = from.x + (to.x - from.x) * amount;
        const z = from.z + (to.z - from.z) * amount;
        const bead = new THREE.Mesh(
          routeBeadGeometry,
          new THREE.MeshBasicMaterial({
            color: MARS_ROVER_STOPS[legIndex].color,
            transparent: true,
            opacity: 0.16,
            depthWrite: false,
            depthTest: true,
          }),
        );
        bead.position.set(x, marsRoverDriveHeight(x, z) + 0.14, z);
        bead.rotation.x = -Math.PI / 2;
        bead.renderOrder = 2;
        scene.add(bead);
        routeGuides.push({ type: "bead", mesh: bead, stopIndex: legIndex, seed: legIndex * 10 + beadIndex });
      }
    }
    const stopMarkers = MARS_ROVER_STOPS.map((stop, index) => {
      const group = new THREE.Group();
      group.position.set(stop.x, marsRoverTerrainHeight(stop.x, stop.z) + 0.3, stop.z);
      const propScale = { ...MARS_ROVER_DEFAULT_PROP_SCALE, ...(stop.propScale || {}) };
      const ring = new THREE.Mesh(
        new THREE.TorusGeometry(3.15, 0.12, 8, 44),
        new THREE.MeshBasicMaterial({
          color: stop.color,
          transparent: true,
          opacity: 0.68,
          depthWrite: false,
        }),
      );
      ring.rotation.x = Math.PI / 2;
      const core = new THREE.Mesh(
        new THREE.SphereGeometry(1.05, 18, 12),
        new THREE.MeshStandardMaterial({
          color: stop.color,
          emissive: stop.color,
          emissiveIntensity: 0.58,
        }),
      );
      core.position.y = 1.25;
      core.visible = false;
      const beam = new THREE.Mesh(
        new THREE.CylinderGeometry(0.28, 1.45, 7.2, 28, 1, true),
        new THREE.MeshBasicMaterial({
          color: stop.color,
          transparent: true,
          opacity: 0.24,
          depthWrite: false,
          depthTest: false,
          side: THREE.DoubleSide,
        }),
      );
      beam.position.y = 3.45;
      beam.userData.isMarsItemBeam = true;
      const itemMaterial = stop.atlas
        ? new THREE.MeshBasicMaterial({
          map: itemTextures[index],
          color: 0xffc79a,
          transparent: true,
          opacity: propScale.opacity,
          alphaTest: 0.16,
          depthWrite: false,
          depthTest: true,
          side: THREE.DoubleSide,
          fog: true,
        })
        : new THREE.SpriteMaterial({
          map: itemTextures[index],
          color: 0xffc79a,
          transparent: true,
          opacity: propScale.opacity,
          alphaTest: 0.08,
          depthWrite: false,
          depthTest: true,
          sizeAttenuation: true,
          fog: true,
        });
      const item = stop.atlas
        ? new THREE.Mesh(
          new THREE.PlaneGeometry(propScale.width, propScale.height),
          itemMaterial,
        )
        : new THREE.Sprite(itemMaterial);
      item.position.y = propScale.lift;
      if (!stop.atlas) item.scale.set(propScale.width, propScale.height, 1);
      item.renderOrder = 4;
      item.userData.isMarsItemSprite = true;
      const shadow = new THREE.Mesh(
        new THREE.CircleGeometry(propScale.shadow || 5.2, 28),
        new THREE.MeshBasicMaterial({
          color: 0x24110c,
          transparent: true,
          opacity: 0.24,
          depthWrite: false,
        }),
      );
      shadow.rotation.x = -Math.PI / 2;
      shadow.position.y = 0.08;
      shadow.scale.z = 0.42;
      shadow.renderOrder = 2;
      const groundHaze = new THREE.Mesh(
        new THREE.CircleGeometry(propScale.shadow * 1.35, 32),
        new THREE.MeshBasicMaterial({
          color: 0xc36b42,
          transparent: true,
          opacity: 0.12,
          depthWrite: false,
          depthTest: true,
        }),
      );
      groundHaze.rotation.x = -Math.PI / 2;
      groundHaze.position.y = 0.13;
      groundHaze.scale.z = 0.34;
      groundHaze.renderOrder = 1;
      const propHaze = new THREE.Mesh(
        new THREE.PlaneGeometry(
          propScale.width * propScale.hazeScale,
          propScale.height * propScale.hazeScale,
        ),
        new THREE.MeshBasicMaterial({
          map: propHazeTexture,
          color: 0xd88b5a,
          transparent: true,
          opacity: propScale.haze,
          depthWrite: false,
          depthTest: !propScale.hazeOverlay,
          side: THREE.DoubleSide,
          fog: true,
        }),
      );
      propHaze.position.y = propScale.lift + 0.1;
      propHaze.renderOrder = item.renderOrder + 1;
      group.add(groundHaze, shadow, ring, beam, item, propHaze);
      group.userData.baseColor = stop.color;
      scene.add(group);
      return {
        group,
        ring,
        core,
        beam,
        item,
        propHaze,
        groundHaze,
        shadow,
        index,
        atlas: stop.atlas,
        propScale,
        texture: itemTextures[index],
        userData: group.userData,
        baseItemOpacity: propScale.opacity,
        baseHaze: propScale.haze,
        baseShadow: 0.24,
        baseGroundHaze: 0.12,
      };
    });
    const satelliteMarker = stopMarkers[MARS_ROVER_STOPS.findIndex((stop) => stop.id === "satellite")];
    const satelliteScan = marsRoverCreateSatelliteScan(THREE);
    scene.add(satelliteScan.group);

    const decorTextures = MARS_ROVER_DECOR_PROPS.map((prop) => {
      const texture = textureLoader.load(prop.image);
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.magFilter = THREE.LinearFilter;
      texture.minFilter = THREE.LinearMipmapLinearFilter;
      return texture;
    });
    const decorativeProps = MARS_ROVER_DECOR_PROPS.map((prop, index) => {
      const group = new THREE.Group();
      group.position.set(prop.x, marsRoverTerrainHeight(prop.x, prop.z) + 0.2, prop.z);
      const shadow = new THREE.Mesh(
        new THREE.CircleGeometry(prop.shadow || 3.5, 24),
        new THREE.MeshBasicMaterial({
          color: 0x24110c,
          transparent: true,
          opacity: 0.16,
          depthWrite: false,
        }),
      );
      shadow.rotation.x = -Math.PI / 2;
      shadow.position.y = 0.08;
      shadow.scale.z = 0.44;
      const groundHaze = new THREE.Mesh(
        new THREE.CircleGeometry((prop.shadow || 3.5) * 1.45, 28),
        new THREE.MeshBasicMaterial({
          color: 0xbf6540,
          transparent: true,
          opacity: 0.08,
          depthWrite: false,
          depthTest: true,
        }),
      );
      groundHaze.rotation.x = -Math.PI / 2;
      groundHaze.position.y = 0.12;
      groundHaze.scale.z = 0.36;
      groundHaze.renderOrder = 1;
      const propSprite = new THREE.Sprite(
        new THREE.SpriteMaterial({
          map: decorTextures[index],
          color: 0xffc08d,
          transparent: true,
          opacity: prop.opacity || 0.66,
          alphaTest: 0.08,
          depthWrite: false,
          depthTest: true,
          sizeAttenuation: true,
          fog: true,
        }),
      );
      propSprite.position.y = prop.lift || 4.8;
      propSprite.scale.set(prop.width || 12, prop.height || 12, 1);
      propSprite.renderOrder = 3;
      const haze = new THREE.Mesh(
        new THREE.PlaneGeometry((prop.width || 12) * 1.24, (prop.height || 12) * 1.12),
        new THREE.MeshBasicMaterial({
          map: propHazeTexture,
          color: 0xd88b5a,
          transparent: true,
          opacity: prop.haze || 0.06,
          depthWrite: false,
          depthTest: true,
          side: THREE.DoubleSide,
          fog: true,
        }),
      );
      haze.position.y = propSprite.position.y + 0.08;
      haze.renderOrder = 4;
      group.add(groundHaze, shadow, propSprite, haze);
      scene.add(group);
      return {
        group,
        sprite: propSprite,
        haze,
        groundHaze,
        shadow,
        prop,
        index,
        baseOpacity: prop.opacity || 0.66,
        baseHaze: prop.haze || 0.16,
        baseShadow: 0.13,
        baseGroundHaze: 0.08,
      };
    });

    const flagGroup = new THREE.Group();
      const flagX = -300;
      const flagZ = -86;
    flagGroup.position.set(flagX, marsRoverTerrainHeight(flagX, flagZ) + 0.1, flagZ);
    const poleMaterial = new THREE.MeshStandardMaterial({
      color: 0xd6c5a7,
      roughness: 0.72,
      metalness: 0.22,
    });
    const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.3, 20, 14), poleMaterial);
    pole.position.y = 10;
    pole.castShadow = true;
    pole.receiveShadow = true;
    const poleBase = new THREE.Mesh(
      new THREE.CylinderGeometry(1.35, 1.7, 0.7, 18),
      new THREE.MeshStandardMaterial({ color: 0x8a4b31, roughness: 0.92, metalness: 0.05 }),
    );
    poleBase.position.y = 0.35;
    const finial = new THREE.Mesh(
      new THREE.SphereGeometry(0.5, 18, 12),
      new THREE.MeshStandardMaterial({ color: 0xffc66c, roughness: 0.48, metalness: 0.18 }),
    );
    finial.position.y = 20.4;
    const flagGeometry = new THREE.PlaneGeometry(12.4, 6.5, 32, 10);
    flagGeometry.translate(6.2, 0, 0);
    const flagBasePositions = new Float32Array(flagGeometry.attributes.position.array);
    const flagCloth = new THREE.Mesh(
      flagGeometry,
      new THREE.MeshBasicMaterial({
        map: marsRoverCreateAmericanFlagTexture(),
        side: THREE.DoubleSide,
        transparent: true,
        alphaTest: 0.05,
        fog: true,
      }),
    );
    flagCloth.position.set(0.18, 16.8, 0);
    flagCloth.rotation.y = -0.18;
    flagCloth.userData.basePositions = flagBasePositions;
    flagCloth.renderOrder = 3;
    const flagShadow = new THREE.Mesh(
      new THREE.CircleGeometry(3.2, 24),
      new THREE.MeshBasicMaterial({
        color: 0x24110c,
        transparent: true,
        opacity: 0.14,
        depthWrite: false,
      }),
    );
    flagShadow.rotation.x = -Math.PI / 2;
    flagShadow.scale.z = 0.34;
    flagShadow.position.y = 0.06;
    flagGroup.add(flagShadow, poleBase, pole, finial, flagCloth);
    flagGroup.rotation.y = 0.52;
    scene.add(flagGroup);

    const flagRelayWaves = marsRoverCreateRadioWaveRelay(THREE);
    const relayWaveX = -338;
    const relayWaveZ = -112;
    flagRelayWaves.group.position.set(
      relayWaveX,
      marsRoverTerrainHeight(relayWaveX, relayWaveZ) + 7.2,
      relayWaveZ,
    );
    flagRelayWaves.group.rotation.y = -0.72;
    flagRelayWaves.group.rotation.z = -0.08;
    flagRelayWaves.group.scale.setScalar(1.05);
    scene.add(flagRelayWaves.group);

    const launchRocket = marsRoverCreateLaunchRocket(THREE);
    const rocketX = -236;
    const rocketZ = -74;
    launchRocket.group.position.set(
      rocketX,
      marsRoverTerrainHeight(rocketX, rocketZ) + 0.12,
      rocketZ,
    );
    launchRocket.group.rotation.y = 0.28;
    launchRocket.group.scale.setScalar(2.15);
    scene.add(launchRocket.group);

    const geometryOutpost = marsRoverCreateGeometryOutpost(THREE);
    geometryOutpost.group.children.forEach((child) => {
      child.position.y = marsRoverTerrainHeight(child.position.x, child.position.z) + 0.06;
    });
    scene.add(geometryOutpost.group);

    const futuristicWindmill = marsRoverCreateFuturisticWindmill(THREE);
    const windmillX = -72;
    const windmillZ = -84;
    futuristicWindmill.group.position.set(
      windmillX,
      marsRoverTerrainHeight(windmillX, windmillZ) + 0.08,
      windmillZ,
    );
    futuristicWindmill.group.rotation.y = 0.22;
    futuristicWindmill.group.scale.setScalar(1.4);
    scene.add(futuristicWindmill.group);

    const dustGeometry = new THREE.BufferGeometry();
    const dustCount = 420;
    const dustPositions = new Float32Array(dustCount * 3);
    for (let i = 0; i < dustCount; i += 1) {
      dustPositions[i * 3] = (marsRoverSeeded(i, 41) - 0.5) * MARS_ROVER_MAP_WIDTH;
      dustPositions[i * 3 + 1] = marsRoverSeeded(i, 42) * 20 + 1;
      dustPositions[i * 3 + 2] = (marsRoverSeeded(i, 43) - 0.5) * MARS_ROVER_MAP_DEPTH;
    }
    dustGeometry.setAttribute("position", new THREE.BufferAttribute(dustPositions, 3));
    const dust = new THREE.Points(
      dustGeometry,
      new THREE.PointsMaterial({
        color: 0xffc07a,
        size: 0.18,
        transparent: true,
        opacity: 0.08,
      }),
    );
    scene.add(dust);
    const pickupBurst = marsRoverCreatePickupBurst(THREE);
    scene.add(pickupBurst.points);

    const stormDebrisMat = new THREE.MeshStandardMaterial({
      color: 0xc77a45,
      roughness: 1,
      flatShading: true,
      emissive: 0x2b1008,
      emissiveIntensity: 0.18,
    });
    const stormDebris = Array.from({ length: 9 }, (_, index) => {
      const debris =
        index % 3 === 0
          ? new THREE.Mesh(new THREE.DodecahedronGeometry(0.75 + (index % 4) * 0.12, 0), stormDebrisMat)
          : new THREE.Mesh(new THREE.IcosahedronGeometry(0.55 + (index % 5) * 0.1, 0), stormDebrisMat);
      debris.visible = false;
      debris.castShadow = true;
      marsRoverResetStormDebris(debris, rover, 2.2);
      scene.add(debris);
      return debris;
    });

    const runtime = {
      scene,
      camera,
      renderer,
      rover,
      keysRef,
      stopMarkers,
      driveIntent: 0,
      turnIntent: 0,
      distance: 999,
      storm: false,
      stormDebris,
      pickupBurst,
      launchRocket,
      geometryOutpost,
      futuristicWindmill,
      satelliteMarker,
      satelliteScan,
      flagRelayWaves,
      cometField,
      stargazerPlank,
      atmosphereVeil,
      skyDome,
      spaceDome,
      hemi,
      sun,
      trackMarks,
      stormDodgeScore: 0,
      stormHitCooldown: 0,
      stormScoreCooldown: 0,
      trackCursor: 0,
      lastTrackX: rover.position.x,
      lastTrackZ: rover.position.z,
      spaceReveal: 0,
      targetSpaceReveal: 0,
      discoveredProps: new Set(),
      propPromptCooldown: 0,
      overlayOpen: false,
      heading: MARS_ROVER_START.heading,
      routeGuides,
      stopMarkers,
      currentStep: step,
      completedRoute: completed,
      collectStopByIndex,
      markStopDone(index) {
        const marker = stopMarkers[index];
        if (!marker) return;
        marker.userData.done = true;
        marker.core.visible = false;
        marker.ring.visible = false;
        marker.beam.visible = false;
        routeGuides.forEach((guideMark) => {
          if (guideMark.stopIndex !== index) return;
          guideMark.doneHidden = true;
          if (guideMark.type === "bead") {
            guideMark.mesh.visible = false;
            guideMark.mesh.material.opacity = 0;
            return;
          }
          guideMark.group.visible = false;
          guideMark.label.material.opacity = 0;
        });
        pickupBurst.trigger(marker.group.position);
      },
    };
    runtimeRef.current = runtime;

    let frameId = 0;
    let last = performance.now();
    const markerLookTarget = new THREE.Vector3();
    const stopAnimationFrame = () => {
      if (!frameId) return;
      window.cancelAnimationFrame(frameId);
      frameId = 0;
    };
    const requestNextFrame = () => {
      if (document.hidden || frameId) return;
      frameId = window.requestAnimationFrame(animate);
    };
    const animate = (now) => {
      frameId = 0;
      if (document.hidden) return;
      const dt = Math.min(0.035, (now - last) / 1000);
      last = now;
      const keys = keysRef.current;
      let driveIntent = runtime.driveIntent;
      let turnIntent = runtime.turnIntent;
      runtime.driveIntent *= 0.78;
      runtime.turnIntent *= 0.78;
      if (keys.ArrowUp || keys.KeyW) driveIntent += 1;
      if (keys.ArrowDown || keys.KeyS) driveIntent -= 0.7;
      if (keys.ArrowLeft || keys.KeyA) turnIntent += 1;
      if (keys.ArrowRight || keys.KeyD) turnIntent -= 1;

      runtime.heading += turnIntent * dt * 1.85;
      const velocity = driveIntent * 18;
      const distanceThisFrame = velocity * dt;
      const forwardX = Math.sin(runtime.heading);
      const forwardZ = Math.cos(runtime.heading);
      rover.position.x += forwardX * distanceThisFrame;
      rover.position.z += forwardZ * distanceThisFrame;
      rover.position.x = marsRoverClamp(
        rover.position.x,
        -MARS_ROVER_WORLD_LIMIT_X,
        MARS_ROVER_WORLD_LIMIT_X,
      );
      rover.position.z = marsRoverClamp(
        rover.position.z,
        -MARS_ROVER_WORLD_LIMIT_Z,
        MARS_ROVER_WORLD_LIMIT_Z,
      );
      const terrainY = marsRoverDriveHeight(rover.position.x, rover.position.z);
      rover.position.y = terrainY + 0.45;
      const slopeSample = 3.6;
      const frontY = marsRoverDriveHeight(
        rover.position.x + forwardX * slopeSample,
        rover.position.z + forwardZ * slopeSample,
      );
      const backY = marsRoverDriveHeight(
        rover.position.x - forwardX * slopeSample,
        rover.position.z - forwardZ * slopeSample,
      );
      const sideX = Math.cos(runtime.heading);
      const sideZ = -Math.sin(runtime.heading);
      const leftY = marsRoverDriveHeight(
        rover.position.x - sideX * slopeSample,
        rover.position.z - sideZ * slopeSample,
      );
      const rightY = marsRoverDriveHeight(
        rover.position.x + sideX * slopeSample,
        rover.position.z + sideZ * slopeSample,
      );
      rover.rotation.set(
        marsRoverClamp(Math.atan2(backY - frontY, slopeSample * 2), -0.34, 0.34),
        runtime.heading,
        marsRoverClamp(Math.atan2(rightY - leftY, slopeSample * 2), -0.28, 0.28),
      );
      rover.traverse((child) => {
        if (child.userData.isWheel) child.rotation.x += velocity * dt * 2.65;
        if (child.userData.isRoverBeacon) {
          child.scale.setScalar(1 + Math.sin(now * 0.008) * 0.14);
        }
      });
      const movedSinceTrack = Math.hypot(
        rover.position.x - runtime.lastTrackX,
        rover.position.z - runtime.lastTrackZ,
      );
      if (Math.abs(velocity) > 2.5 && movedSinceTrack > 1.9) {
        stampTrackPair(runtime);
      }
      const wakeAttrs = wakeGeometry.attributes.position;
      const movingFast = Math.abs(velocity) > 3;
      if (movingFast) {
        for (let i = 0; i < MARS_ROVER_WAKE_DUST_COUNT; i += 1) {
          const drift = marsRoverSeeded(i, 51) - 0.5;
          const back = 2.2 + marsRoverSeeded(i, 52) * 6.4;
          const side = drift * (4.8 + marsRoverSeeded(i, 53) * 3.2);
          const lift = 0.18 + marsRoverSeeded(i, 54) * 1.25;
          const x =
            rover.position.x -
            forwardX * back +
            sideX * side +
            Math.sin(now * 0.004 + i) * 0.35;
          const z =
            rover.position.z -
            forwardZ * back +
            sideZ * side +
            Math.cos(now * 0.004 + i) * 0.35;
          wakeAttrs.setXYZ(i, x, marsRoverDriveHeight(x, z) + lift, z);
        }
        wakeAttrs.needsUpdate = true;
      }
      wakeDust.material.opacity = movingFast
        ? 0.26
        : Math.max(0, wakeDust.material.opacity - dt * 1.7);

      stopMarkers.forEach((marker) => {
        const cameraDistance = camera.position.distanceTo(marker.group.position);
        const closeFade = marsRoverClamp(
          (cameraDistance - 20) / 28,
          0.18,
          1,
        );
        const closeHaze = marsRoverClamp((34 - cameraDistance) / 18, 0, 1);
        const doneFade = marker.userData.done ? 0.18 : 1;
        const markerFade = closeFade * doneFade;
        const hazeFade = Math.max(markerFade, closeHaze * 0.42);
        marker.ring.rotation.z += dt * 1.4;
        marker.beam.rotation.y += dt * 0.55;
        marker.beam.material.opacity =
          (0.1 + Math.sin(now * 0.004 + marker.index) * 0.04) * markerFade;
        marker.item.position.y =
          (marker.propScale?.lift || 6.4) + Math.sin(now * 0.003 + marker.index) * 0.18;
        marker.item.material.opacity = marker.baseItemOpacity * markerFade;
        marker.propHaze.position.y = marker.item.position.y + 0.08;
        marker.propHaze.material.opacity =
          (marker.baseHaze + closeHaze * 0.18 + Math.sin(now * 0.0025 + marker.index) * 0.018) *
          hazeFade;
        marker.propHaze.scale.setScalar(1 + closeHaze * 0.28);
        marker.groundHaze.material.opacity =
          (marker.baseGroundHaze + closeHaze * 0.12 + Math.sin(now * 0.002 + marker.index) * 0.018) *
          hazeFade;
        marker.shadow.material.opacity =
          (marker.baseShadow + Math.sin(now * 0.003 + marker.index) * 0.025) * markerFade;
        if (marker.atlas) {
          const angle = Math.atan2(
            camera.position.x - marker.group.position.x,
            camera.position.z - marker.group.position.z,
          );
          const frame = Math.floor(
            (((angle + Math.PI) % (Math.PI * 2)) / (Math.PI * 2)) * 16,
          );
          marsRoverTextureFrame(marker.texture, frame);
          marker.item.getWorldPosition(markerLookTarget);
          markerLookTarget.set(camera.position.x, markerLookTarget.y, camera.position.z);
          marker.item.lookAt(markerLookTarget);
          marker.propHaze.lookAt(markerLookTarget);
        } else {
          marker.propHaze.quaternion.copy(camera.quaternion);
        }
      });
      decorativeProps.forEach((decor) => {
        const cameraDistance = camera.position.distanceTo(decor.group.position);
        const closeFade = marsRoverClamp(
          (cameraDistance - 20) / 26,
          0,
          1,
        );
        const closeHaze = marsRoverClamp((34 - cameraDistance) / 18, 0, 1);
        const hazeFade = Math.max(closeFade, closeHaze * 0.46);
        decor.sprite.material.opacity = decor.baseOpacity * closeFade;
        decor.haze.position.y = decor.sprite.position.y + 0.08;
        decor.haze.material.opacity =
          (decor.baseHaze + closeHaze * 0.16 + Math.sin(now * 0.002 + decor.index) * 0.012) *
          hazeFade;
        decor.haze.scale.setScalar(1 + closeHaze * 0.36);
        decor.haze.quaternion.copy(camera.quaternion);
        decor.groundHaze.material.opacity =
          (decor.baseGroundHaze + closeHaze * 0.1 + Math.sin(now * 0.0022 + decor.index) * 0.012) *
          hazeFade;
        decor.shadow.material.opacity =
          (decor.baseShadow + Math.sin(now * 0.0024 + decor.index) * 0.018) * closeFade;
      });
      routeGuides.forEach((guideMark) => {
        const routeStep = runtime.currentStep ?? step;
        const routeComplete = runtime.completedRoute ?? completed;
        const current = guideMark.stopIndex === routeStep && !routeComplete && !guideMark.doneHidden;
        const done = routeComplete || guideMark.stopIndex < routeStep || guideMark.doneHidden;
        const pulse = 0.5 + Math.sin(now * 0.004 + guideMark.seed) * 0.5;
        if (guideMark.type === "bead") {
          guideMark.mesh.visible = current;
          if (!current || done) {
            guideMark.mesh.material.opacity = 0;
            return;
          }
          guideMark.mesh.material.opacity = current ? 0.34 + pulse * 0.18 : 0.13;
          guideMark.mesh.scale.setScalar(current ? 1.08 + pulse * 0.16 : 1);
          return;
        }
        guideMark.group.visible = current;
        if (!current || done) {
          guideMark.label.material.opacity = 0;
          return;
        }
        guideMark.label.material.opacity = current ? 0.78 + pulse * 0.16 : 0.52;
        guideMark.group.scale.setScalar(current ? 1.06 + pulse * 0.04 : 0.96);
      });
      const flagPositions = flagCloth.geometry.attributes.position;
      const flagBase = flagCloth.userData.basePositions;
      for (let i = 0; i < flagPositions.count; i += 1) {
        const baseX = flagBase[i * 3];
        const baseY = flagBase[i * 3 + 1];
        const baseZ = flagBase[i * 3 + 2];
        const loose = marsRoverClamp(baseX / 12.4, 0, 1);
        const wave = Math.sin(now * 0.0055 + baseX * 0.72 + baseY * 0.25);
        const ripple = Math.sin(now * 0.008 + baseX * 1.36) * 0.12;
        flagPositions.setXYZ(
          i,
          baseX,
          baseY + wave * loose * 0.16,
          baseZ + (wave * 0.52 + ripple) * loose,
        );
      }
      flagPositions.needsUpdate = true;

      const attrs = dustGeometry.attributes.position;
      for (let i = 0; i < dustCount; i += 1) {
        const x = attrs.getX(i) + (runtime.storm ? 16 : 2.2) * dt;
        const dustLimit = MARS_ROVER_MAP_WIDTH / 2 - 4;
        attrs.setX(i, x > dustLimit ? -dustLimit : x);
      }
      attrs.needsUpdate = true;
      dust.material.opacity = runtime.storm ? 0.46 : 0.08;
      runtime.pickupBurst.update(dt);
      runtime.propPromptCooldown = Math.max(0, runtime.propPromptCooldown - dt);
      if (!runtime.overlayOpen && !runtime.completedRoute) {
        const nearbyStop = nearestCollectibleStop();
        if (nearbyStop && (nearbyStop.stop.miniSteps || 1) <= 1) {
          runtime.collectStopByIndex?.(nearbyStop.index);
        }
      }
      if (!runtime.overlayOpen && runtime.propPromptCooldown <= 0) {
        const visitedProp = MARS_ROVER_PROP_VISITS.find((prop) => {
          if (runtime.discoveredProps.has(prop.id)) return false;
          const radius = prop.radius || MARS_ROVER_PROP_VISIT_RADIUS;
          return Math.hypot(rover.position.x - prop.x, rover.position.z - prop.z) <= radius;
        });
        if (visitedProp) {
          runtime.discoveredProps.add(visitedProp.id);
          runtime.overlayOpen = true;
          runtime.propPromptCooldown = 0.8;
          setScore((value) => value + (visitedProp.points || MARS_ROVER_PROP_POINTS));
          setMessage(`+${visitedProp.points || MARS_ROVER_PROP_POINTS} points`);
          setPropPrompt({
            ...visitedProp,
            eyebrow: "Artifact indexed",
            points: visitedProp.points || MARS_ROVER_PROP_POINTS,
          });
          speakGuide(
            `${visitedProp.title}. ${visitedProp.fact}`,
            visitedProp.grokClip,
          );
          pickupBurst.trigger({
            x: visitedProp.x,
            y: marsRoverDriveHeight(visitedProp.x, visitedProp.z) + 1.2,
            z: visitedProp.z,
          });
        }
      }
      runtime.launchRocket.update(now);
      runtime.geometryOutpost.update(now);
      runtime.futuristicWindmill.update(now, dt);
      runtime.satelliteScan.update(now, rover, runtime.satelliteMarker);
      runtime.flagRelayWaves.update(now);
      runtime.targetSpaceReveal = marsRoverPlankRevealAmount(
        rover.position.x,
        rover.position.z,
        rover.position.y,
      );
      runtime.spaceReveal += (runtime.targetSpaceReveal - runtime.spaceReveal) * Math.min(1, dt * 2.4);
      const reveal = runtime.spaceReveal;
      const skyReveal = marsRoverClamp(reveal * 1.18, 0, 1);
      skyDome.material.opacity = 1 - skyReveal;
      spaceDome.material.opacity = marsRoverClamp(reveal * 0.88, 0, 0.9);
      scene.fog.color.set(0xb97754).lerp(new THREE.Color(0x050711), skyReveal);
      scene.fog.density = 0.0066 * (1 - skyReveal);
      scene.background.set(0x2a1514).lerp(new THREE.Color(0x01030a), reveal);
      hemi.intensity = 1.72 - skyReveal * 0.88;
      sun.intensity = 1.45 - skyReveal * 0.72;
      dust.material.opacity *= 1 - reveal * 0.78;
      atmosphereVeil.position.y = 28 + reveal * 48;
      atmosphereVeil.material.opacity = Math.sin(reveal * Math.PI) * 0.16;
      cometField.update(now, reveal);

      if (runtime.storm) {
        runtime.stormHitCooldown = Math.max(0, runtime.stormHitCooldown - dt);
        runtime.stormScoreCooldown = Math.max(0, runtime.stormScoreCooldown - dt);
        runtime.stormDebris.forEach((debris) => {
          debris.visible = true;
          debris.position.x += debris.userData.speed * dt;
          debris.position.z +=
            (rover.position.z - debris.position.z) * dt * 0.12 +
            debris.userData.drift * dt;
          debris.position.y =
            marsRoverTerrainHeight(debris.position.x, debris.position.z) +
            2.7 +
            Math.sin(now * 0.006 + debris.userData.phase) * 1.55;
          debris.rotation.x += dt * 2.4;
          debris.rotation.y += dt * 3.2;
          debris.rotation.z += dt * 1.8;
          const xDistance = Math.abs(debris.position.x - rover.position.x);
          const zDistance = Math.abs(debris.position.z - rover.position.z);
          if (xDistance > 52 || zDistance > 32) {
            if (xDistance > 42 && runtime.stormScoreCooldown <= 0) {
              runtime.stormDodgeScore = marsRoverClamp(
                runtime.stormDodgeScore + 1,
                0,
                MARS_ROVER_STORM_DODGES_TO_PASS,
              );
              setStormHold(runtime.stormDodgeScore);
              setMessage(
                runtime.stormDodgeScore >= MARS_ROVER_STORM_DODGES_TO_PASS
                  ? "Storm shield steady."
                  : "Keep the dust off.",
              );
              runtime.stormScoreCooldown = 0.55;
            }
            marsRoverResetStormDebris(debris, rover);
          }
          const hitDistance = Math.hypot(
            debris.position.x - rover.position.x,
            debris.position.z - rover.position.z,
          );
          if (hitDistance < debris.userData.radius + 1.25 && runtime.stormHitCooldown <= 0) {
            runtime.stormDodgeScore = 0;
            runtime.stormHitCooldown = 1.15;
            setStormHold(0);
            setEnergy((value) => Math.max(1, value - 1));
            setMessage("Dust hit the rover.");
            marsRoverResetStormDebris(debris, rover, 1.8);
          }
        });
      } else {
        runtime.stormDodgeScore = 0;
        runtime.stormHitCooldown = 0;
        runtime.stormScoreCooldown = 0;
        runtime.stormDebris.forEach((debris) => {
          debris.visible = false;
        });
      }

      setMarsRoverCamera(0.08);
      renderer.render(scene, camera);
      perfProbe?.markInteractive({ renderer: "three" });
      perfProbe?.collectFrame(now);
      requestNextFrame();
    };

    const resize = () => {
      const width = mount.clientWidth || window.innerWidth;
      const height = mount.clientHeight || window.innerHeight;
      camera.aspect = width / Math.max(1, height);
      camera.updateProjectionMatrix();
      renderer.setSize(width, height);
    };
    window.addEventListener("resize", resize);
    const keyDown = (event) => {
      if (/^(Arrow|KeyW|KeyA|KeyS|KeyD|Space|Enter)/.test(event.code || event.key)) {
        keysRef.current[event.code || event.key] = true;
      }
    };
    const keyUp = (event) => {
      keysRef.current[event.code || event.key] = false;
    };
    const clearControls = () => marsRoverClearInput(runtime);
    window.addEventListener("keydown", keyDown);
    window.addEventListener("keyup", keyUp);
    window.addEventListener("blur", clearControls);
    window.addEventListener("pointerup", clearControls);
    window.addEventListener("pointercancel", clearControls);
    const onVisibilityChange = () => {
      if (document.hidden) {
        clearControls();
        stopAnimationFrame();
        return;
      }
      last = performance.now();
      requestNextFrame();
    };
    document.addEventListener("visibilitychange", onVisibilityChange);
    requestNextFrame();

    return () => {
      stopAnimationFrame();
      if (runtime.collectModalTimer) window.clearTimeout(runtime.collectModalTimer);
      document.removeEventListener("visibilitychange", onVisibilityChange);
      window.removeEventListener("resize", resize);
      window.removeEventListener("keydown", keyDown);
      window.removeEventListener("keyup", keyUp);
      window.removeEventListener("blur", clearControls);
      window.removeEventListener("pointerup", clearControls);
      window.removeEventListener("pointercancel", clearControls);
      renderer.dispose();
      skyTexture.dispose();
      skyDome.geometry.dispose();
      skyDome.material.dispose();
      spaceTexture.dispose();
      spaceDome.geometry.dispose();
      spaceDome.material.dispose();
      cometField.dispose();
      atmosphereVeil.geometry.dispose();
      atmosphereVeil.material.dispose();
      terrainTextures.color.dispose();
      terrainTextures.bump.dispose();
      terrain.material.dispose();
      mountainMaterial.dispose();
      mountainCapMaterial.dispose();
      stargazerPlank.traverse((child) => child.geometry?.dispose?.());
      mesaMaterial.dispose();
      mesaTopMaterial.dispose();
      strataMaterial.dispose();
      rippleMat.dispose();
      canyonMat.dispose();
      itemTextures.forEach((texture) => texture.dispose());
      routeGuides.forEach((guideMark) => {
        scene.remove(guideMark.mesh);
        guideMark.mesh.material.dispose();
      });
      routeBeadGeometry.dispose();
      moonTextures.forEach((texture) => texture.dispose());
      craterFloorMat.dispose();
      craterRimMat.dispose();
      rockMats.forEach((material) => material.dispose());
      stormDebris.forEach((debris) => debris.geometry.dispose());
      stormDebrisMat.dispose();
      trackMarkGeometry.dispose();
      trackMarkMaterial.dispose();
      wakeGeometry.dispose();
      wakeDust.material.dispose();
      terrainGeometry.dispose();
      dustGeometry.dispose();
      dust.material.dispose();
      pickupBurst.points.geometry.dispose();
      pickupBurst.points.material.map?.dispose?.();
      pickupBurst.points.material.dispose();
      launchRocket.dispose();
      geometryOutpost.dispose();
      futuristicWindmill.dispose();
      satelliteScan.dispose();
      flagRelayWaves.dispose();
      mount.removeChild(renderer.domElement);
      runtimeRef.current = null;
    };
  }, []);

  marsRoverUseEffect(() => {
    if (runtimeRef.current) {
      runtimeRef.current.currentStep = activeStopIndex;
      runtimeRef.current.completedRoute = completed;
      runtimeRef.current.collectStopByIndex = collectStopByIndex;
    }
    completedRef.current = completed;
    collectedIdsRef.current = collectedIds;
  }, [activeStopIndex, completed, collectedIds, collectStopByIndex]);

  marsRoverUseEffect(() => {
    if (!runtimeRef.current) return;
    const storming = Boolean(activeStop.storm && !completed);
    runtimeRef.current.storm = storming;
    runtimeRef.current.stormDodgeScore = 0;
    runtimeRef.current.stormHitCooldown = 0;
    runtimeRef.current.stormScoreCooldown = 0;
    setStormHold(0);
  }, [activeStop.id, completed]);

  marsRoverUseEffect(() => {
    setMiniProgress(0);
  }, [activeStop.id]);

  marsRoverUseEffect(() => {
    const line = step === 0 ? `${MARS_ROVER_INTRO_LINE} ${activeStop.guide}` : activeStop.guide;
    const clip = step === 0 ? "game_mars_rover_start.mp3" : activeStop.clip;
    setGuideLine(line);
    const timer = window.setTimeout(
      () => speakGuide(line, clip),
      step === 0 ? 520 : 260,
    );
    return () => window.clearTimeout(timer);
  }, [step, activeStop.id]);

  marsRoverUseEffect(() => {
    return () => {
      if (promptAutoDismissRef.current) window.clearTimeout(promptAutoDismissRef.current);
      window.__narration?.stop?.();
    };
  }, []);

  const dismissPrompt = () => {
    if (promptAutoDismissRef.current) {
      window.clearTimeout(promptAutoDismissRef.current);
      promptAutoDismissRef.current = null;
    }
    setCollectedStop(null);
    setPropPrompt(null);
    if (runtimeRef.current) runtimeRef.current.overlayOpen = false;
  };
  const overlayPrompt = collectedStop || propPrompt;

  marsRoverUseEffect(() => {
    if (!overlayPrompt) return undefined;
    if (promptAutoDismissRef.current) {
      window.clearTimeout(promptAutoDismissRef.current);
      promptAutoDismissRef.current = null;
    }
    const closeAfterPause = () => {
      if (promptAutoDismissRef.current) window.clearTimeout(promptAutoDismissRef.current);
      promptAutoDismissRef.current = window.setTimeout(() => {
        setCollectedStop(null);
        setPropPrompt(null);
        if (runtimeRef.current) runtimeRef.current.overlayOpen = false;
        promptAutoDismissRef.current = null;
      }, 2400);
    };
    const fallbackMs = guideOn ? 11500 : 6500;
    promptAutoDismissRef.current = window.setTimeout(closeAfterPause, fallbackMs);
    const unsubscribeEnded = window.__narration?.onEnded?.(closeAfterPause);
    return () => {
      if (promptAutoDismissRef.current) {
        window.clearTimeout(promptAutoDismissRef.current);
        promptAutoDismissRef.current = null;
      }
      unsubscribeEnded?.();
    };
  }, [overlayPrompt?.id, overlayPrompt?.title, guideOn]);

  return (
    <section
      className={`mars-rover-level mars-rover-3d ${activeStop.storm && !completed ? "storming" : ""}`}
      aria-label="Mars Rover 3D level"
    >
      <div className="mars-rover-stage" ref={mountRef} />
      {activeStop.storm && !completed ? (
        <div className="mars-rover-wind-cues" aria-hidden="true">
          {Array.from({ length: 7 }, (_, index) => (
            <i
              key={index}
              style={{
                top: `${18 + index * 8}%`,
                animationDelay: `${index * -0.34}s`,
                transform: `scale(${0.85 + (index % 3) * 0.12})`,
              }}
            >
              ≋
            </i>
          ))}
        </div>
      ) : null}
      <div className="mars-rover-stars" aria-hidden="true">
        {Array.from({ length: 26 }, (_, index) => (
          <i
            key={index}
            style={{
              left: `${(index * 37) % 100}%`,
              top: `${7 + ((index * 23) % 70)}%`,
              width: `${1 + (index % 2)}px`,
              height: `${1 + (index % 2)}px`,
              opacity: 0.16 + ((index * 13) % 28) / 100,
              animationDelay: `${(index % 9) * -0.42}s`,
            }}
          />
        ))}
      </div>
      <button className="mars-rover-back" onClick={onExit}>
        ← Back
      </button>
      <div className="mars-rover-hud">
        <div className="mars-rover-card">
          <span>Mars Rover</span>
          <strong>{completed ? "Done!" : activeStop.shortTitle}</strong>
          <p>{completed ? "Badge earned" : activeJourneyPhase.lesson}</p>
        </div>
        <div
          className="mars-rover-collected-strip"
          aria-label={`${completedJobs} collected Mars items`}
        >
          {MARS_ROVER_STOPS.map((stop, index) => {
            const collected = completed || collectedIds.has(stop.id);
            return (
              <span
                key={stop.id}
                className={collected ? "collected" : ""}
                title={stop.title}
                aria-label={
                  collected
                    ? `${stop.title} collected`
                    : `${stop.title} not collected yet`
                }
              >
                {collected ? <img src={stop.image} alt="" /> : null}
              </span>
            );
          })}
        </div>
        <div
          className="mars-rover-journey-rail"
          aria-label="Mars learning journey"
        >
          {MARS_ROVER_JOURNEY_PHASES.map((phase, index) => {
            const firstStopIndex = MARS_ROVER_STOPS.findIndex(
              (stop) => stop.id === phase.stopIds[0],
            );
            const lastStopIndex = MARS_ROVER_STOPS.findIndex(
              (stop) => stop.id === phase.stopIds[phase.stopIds.length - 1],
            );
            const phaseDone =
              completed ||
              phase.stopIds.every((stopId) => collectedIds.has(stopId));
            const phaseActive =
              !completed && phase.id === activeJourneyPhase.id;
            return (
              <span
                key={phase.id}
                className={`${phaseActive ? "active" : ""} ${phaseDone ? "done" : ""}`}
                title={phase.lesson}
                aria-label={`${phase.label} phase${phaseActive ? ", current" : phaseDone ? ", complete" : ""}`}
              >
                <b>{index + 1}</b>
                <em>{phase.label}</em>
                <i>
                  {firstStopIndex + 1}-{lastStopIndex + 1}
                </i>
              </span>
            );
          })}
        </div>
        <div
          className="mars-rover-badges"
          aria-label={`${completedJobs} of ${MARS_ROVER_STOPS.length} jobs complete`}
        >
          <span>
            {completedJobs}/{MARS_ROVER_STOPS.length}
          </span>
          <span>{score} pts</span>
          <span>⚡ {energy}</span>
          {activeStop.storm && !completed ? (
            <span>
              💨 {stormHold}/{MARS_ROVER_STORM_DODGES_TO_PASS}
            </span>
          ) : null}
        </div>
      </div>
      <aside className="mars-rover-guide" aria-live="polite">
        <div className="mars-rover-guide-actions">
          <button
            type="button"
            onClick={() => {
              const next = !guideOn;
              setGuideOn(next);
              if (next) {
                window.__narration?.setEnabled?.(true);
                speakGuide(
                  guideLine,
                  step === 0 ? "game_mars_rover_start.mp3" : activeStop.clip,
                  { force: true },
                );
              } else {
                window.__narration?.stop?.();
              }
            }}
            aria-label={guideOn ? "Mute voice guide" : "Unmute voice guide"}
          >
            <span aria-hidden="true">{guideOn ? "🔊" : "🔇"}</span>
          </button>
          <button
            type="button"
            onClick={completed ? reset : guide}
            aria-label={
              completed
                ? "Run Mars Rover again"
                : `Help drive to ${activeStop.title}`
            }
          >
            <span aria-hidden="true">{completed ? "↻" : "❓"}</span>
          </button>
        </div>
      </aside>
      {overlayPrompt ? (
        <aside
          className={`mars-rover-collect-modal ${propPrompt ? "prop-visit" : ""}`}
          role="dialog"
          aria-live="polite"
          aria-label={`${overlayPrompt.title} ${propPrompt ? "found" : "collected"}`}
        >
          <div className="mars-rover-collect-art" aria-hidden="true">
            {overlayPrompt.image ? (
              <img src={overlayPrompt.image} alt="" />
            ) : (
              <span className="mars-rover-prop-icon">
                {overlayPrompt.icon || "✦"}
              </span>
            )}
          </div>
          <div className="mars-rover-collect-copy">
            <span>
              {overlayPrompt.eyebrow} +
              {overlayPrompt.points || MARS_ROVER_PROP_POINTS} pts
            </span>
            <strong>{overlayPrompt.title}</strong>
            <p>
              {overlayPrompt.route
                ? `${overlayPrompt.route} ${overlayPrompt.fact}`
                : overlayPrompt.fact}
            </p>
          </div>
          <button
            type="button"
            onClick={dismissPrompt}
            aria-label="Dismiss prop explainer"
          >
            OK
          </button>
        </aside>
      ) : null}
      <div className="mars-rover-panel">
        <div className="mars-rover-message" role="status" aria-live="polite">
          <div className="mars-rover-objective">
            <span>{objectiveDetail}</span>
            <strong>{objectiveText}</strong>
          </div>
          <strong>{message}</strong>
          <span>
            {completed
              ? "All science stops cleared."
              : activeStop.route || activeStop.task}
          </span>
          {activeMiniSteps > 1 && !completed ? (
            <div
              className="mars-rover-mini-task"
              aria-label={`${activeStop.shortTitle} task ${miniProgress} of ${activeMiniSteps}`}
            >
              <span>{activeStop.miniVerb || activeStop.shortAction}</span>
              <div className="mars-rover-progress mini" aria-hidden="true">
                {Array.from({ length: activeMiniSteps }, (_, index) => (
                  <span
                    key={`${activeStop.id}-mini-${index}`}
                    className={index < miniProgress ? "on" : ""}
                  />
                ))}
              </div>
            </div>
          ) : null}
        </div>
        <div
          className="mars-rover-progress"
          aria-label={`Mars route ${routePercent}% complete`}
        >
          {MARS_ROVER_STOPS.map((stop, index) => {
            const state =
              completed || collectedIds.has(stop.id)
                ? "done"
                : index === activeStopIndex
                  ? "current"
                  : "upcoming";
            return (
              <span
                key={stop.id}
                className={state}
                title={`${index + 1}. ${stop.title}`}
                aria-label={`${stop.title}: ${state}`}
              />
            );
          })}
        </div>
        <div className="mars-rover-controls">
          <button onPointerDown={() => drive(1, 0)} aria-label="Turn left">
            ←
          </button>
          <button className="primary" onClick={completeMiniGame}>
            {collectButtonLabel}
          </button>
          <button onPointerDown={() => drive(-1, 0)} aria-label="Turn right">
            →
          </button>
          <button onPointerDown={() => drive(0, 1)}>Go</button>
          <button onClick={completed ? reset : guide}>
            {completed ? "Again" : "Help"}
          </button>
        </div>
      </div>
    </section>
  );
}
