const SHOOTING_STAR_TARGET_SNACKS = 6;
const SHOOTING_STAR_START_HEARTS = 3;
const SHOOTING_STAR_REFILL_HEARTS = 2;
const SHOOTING_STAR_BASE_SPEED = 170;
const SHOOTING_STAR_BUMP_SPEED = 122;
const SHOOTING_STAR_KEYBOARD_SPEED = 360;
const SHOOTING_STAR_KEY_NUDGE = 64;
const SHOOTING_STAR_BOOST_NUDGE = 78;
const SHOOTING_STAR_PLAYER_RESPONSE = 26.78;
const SHOOTING_STAR_PLAYER_MAX_SPEED = 760;
const SHOOTING_STAR_PLAYER_TILT_PER_SPEED = 0.003;
const SHOOTING_STAR_SNACK_RADIUS = 106;
const SHOOTING_STAR_DUST_RADIUS = 82;
const SHOOTING_STAR_GATE_RADIUS = 138;
const SHOOTING_STAR_DELTA_CAP = 48;
const SHOOTING_STAR_HELPER_MISS_LIMIT = 2;
const SHOOTING_STAR_SPRITE_SIZE = 256;
const SHOOTING_STAR_SHOW_CANVAS_HINTS = false;
const SHOOTING_STAR_CONTROL_COPY = "Drag up and down. Catch stars. Dodge dust.";
const SHOOTING_STAR_DESKTOP_HUD_SAFE_TOP = 300;
const SHOOTING_STAR_COMPACT_HUD_SAFE_ZONE = 248;
const SHOOTING_STAR_COMPACT_PLAYFIELD_TOP = 138;
const SHOOTING_STAR_KEY_DIRECTIONS = {
  ArrowUp: "up",
  KeyW: "up",
  ArrowDown: "down",
  KeyS: "down",
};
const SHOOTING_STAR_ACTION_KEYS = new Set([
  ...Object.keys(SHOOTING_STAR_KEY_DIRECTIONS),
  "Space",
]);
const SHOOTING_STAR_EXIT_X = {
  snack: -96,
  dust: -84,
  gate: -170,
};
const SHOOTING_STAR_STATUS_COPY = {
  start: "Glide up and down. Collect 6 sparkle snacks.",
  phaserMissing: "Phaser did not load. Check the network and refresh this level.",
  pouchFull: "Snack pouch full. Fly through the comet gate.",
  gateAhead: "Comet gate ahead. Glide through the sparkles.",
  gateMiss: "The comet gate is looping back for another try.",
  helperSnack: "A helper snack swoops closer.",
  boost: "Sparkle hop.",
  refill: "The star caught you. Hearts refilled and snacks stayed safe.",
};
const SHOOTING_STAR_TIMING = {
  snackDelay: 2200,
  dustDelay: 3300,
  firstSnackDelay: 700,
  firstDustDelay: 1850,
  boostCooldown: 900,
  bumpSlowMs: 680,
  invulnerableMs: 1200,
};
const SHOOTING_STAR_ASSET_ROOT =
  "data/generated-assets/shooting-star-ride/";
const SHOOTING_STAR_BACKDROP_IMAGES = {
  far: {
    key: "shootingStarBgFar",
    src: `${SHOOTING_STAR_ASSET_ROOT}shooting-star-bg-far-v1.png`,
    depth: 0.01,
    alpha: 0.9,
    speed: 14,
  },
  near: {
    key: "shootingStarBgNear",
    src: `${SHOOTING_STAR_ASSET_ROOT}shooting-star-bg-near-v1.png`,
    depth: 0.64,
    alpha: 0.28,
    speed: 58,
  },
};
const SHOOTING_STAR_LANE_FACTORS = [0.18, 0.34, 0.5, 0.66, 0.82];
const SHOOTING_STAR_STAR_COLORS = [0xffffff, 0xfff1b8, 0x8feaff, 0xd777ff];
const SHOOTING_STAR_LETTERS = ["A", "B", "C", "D", "E", "F"];
const SHOOTING_STAR_LETTER_CLIPS = {
  A: "shooting-star-letter-a.mp3",
  B: "shooting-star-letter-b.mp3",
  C: "shooting-star-letter-c.mp3",
  D: "shooting-star-letter-d.mp3",
  E: "shooting-star-letter-e.mp3",
  F: "shooting-star-letter-f.mp3",
};
const SHOOTING_STAR_PLANET_ORBS = [
  { x: 0.82, y: 0.21, r: 42, color: 0x8feaff, ring: 0xffd96c, alpha: 0.2, crescent: true },
  { x: 0.1, y: 0.72, r: 58, color: 0xd777ff, ring: 0x8feaff, alpha: 0.14 },
  { x: 0.62, y: 0.82, r: 34, color: 0xffd96c, ring: 0xffffff, alpha: 0.12, crescent: true },
  { x: 0.93, y: 0.56, r: 18, color: 0xfff1b8, ring: 0xd777ff, alpha: 0.16 },
];
const SHOOTING_STAR_NEBULA_BANDS = [
  { x: 0.22, y: 0.27, w: 0.74, h: 0.2, color: 0x24427d, alpha: 0.16 },
  { x: 0.78, y: 0.42, w: 0.45, h: 0.18, color: 0x6b3e96, alpha: 0.15 },
  { x: 0.36, y: 0.57, w: 0.76, h: 0.12, color: 0x315f84, alpha: 0.12 },
  { x: 0.52, y: 0.79, w: 0.54, h: 0.15, color: 0x8d7a45, alpha: 0.1 },
];
const SHOOTING_STAR_DUST_CLUSTERS = [
  { x: 0.18, y: 0.2, color: 0xffd96c },
  { x: 0.72, y: 0.34, color: 0x8feaff },
  { x: 0.28, y: 0.76, color: 0xd777ff },
];
const SHOOTING_STAR_GLOW_CLOUDS = [
  { x: 0.14, y: 0.22, w: 0.26, h: 0.12, color: 0x3c61d6, alpha: 0.16, drift: 0.011 },
  { x: 0.78, y: 0.28, w: 0.22, h: 0.1, color: 0x8a53f0, alpha: 0.14, drift: 0.016 },
  { x: 0.52, y: 0.71, w: 0.38, h: 0.14, color: 0xffc04d, alpha: 0.08, drift: 0.009 },
];
const SHOOTING_STAR_FRONT_SPARKLES = [
  { y: 0.22, speed: 138, width: 58, alpha: 0.18, color: 0x8feaff },
  { y: 0.44, speed: 102, width: 72, alpha: 0.16, color: 0xffd96c },
  { y: 0.68, speed: 124, width: 64, alpha: 0.14, color: 0xd777ff },
  { y: 0.84, speed: 156, width: 54, alpha: 0.17, color: 0xfff1b8 },
];

// Sprite sheets keep fixed 256x256 cell coordinates; do not crop/recenter
// frames independently or the rider/hazards will visibly jump during animation.
const SHOOTING_STAR_SPRITES = {
  rider: {
    key: "shootingStarRider",
    src: `${SHOOTING_STAR_ASSET_ROOT}astronaut-star-rider-sheet-v7-imagegen-anchored.png`,
    animation: "riderGlide",
    frames: [0, 1, 4, 5, 8, 9, 12, 13],
    frameRate: 10,
    repeat: -1,
  },
  boost: {
    key: "shootingStarBoost",
    src: `${SHOOTING_STAR_ASSET_ROOT}astronaut-star-boost-sheet-v3-padded.png`,
    animation: "riderBoost",
    frameRate: 18,
    repeat: 1,
  },
  snack: {
    key: "starSnack",
    src: `${SHOOTING_STAR_ASSET_ROOT}star-snack-sheet-v2-anchored.png`,
    animation: "snackTwinkle",
    frameRate: 12,
    repeat: -1,
  },
  dust: {
    key: "nebulaDustPuff",
    src: `${SHOOTING_STAR_ASSET_ROOT}nebula-dust-puff-sheet-v3-anchored.png`,
    animation: "dustWobble",
    frameRate: 9,
    repeat: -1,
  },
  gate: {
    key: "cometGate",
    src: `${SHOOTING_STAR_ASSET_ROOT}comet-gate-sheet-v2-anchored.png`,
    animation: "gateSparkle",
    frameRate: 12,
    repeat: -1,
  },
};

function prefersShootingStarReducedMotion() {
  return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
}

function createShootingStarRideGame(mount, callbacks = {}) {
  const PhaserLib = window.Phaser;
  if (!PhaserLib) return null;
  const playRideSound = (kind) => {
    if (typeof playKidSound === "function") playKidSound(kind);
  };

  class ShootingStarRideScene extends PhaserLib.Scene {
    constructor() {
      super("ShootingStarRideScene");
      this.snacks = 0;
      this.hearts = SHOOTING_STAR_START_HEARTS;
      this.phase = "play";
      this.speed = SHOOTING_STAR_BASE_SPEED;
      this.playerYTarget = null;
      this.lastBoostAt = -9999;
      this.invulnerableUntil = 0;
      this.gateSpawned = false;
      this.finished = false;
      this.distance = 0;
      this.spawnLaneIndex = 0;
      this.spawnLetterIndex = 0;
      this.missedSnacks = 0;
      this.pausedForPage = false;
      this.reducedMotion = prefersShootingStarReducedMotion();
      this.globalKeyState = { up: false, down: false };
      this.backdropTime = 0;
    }

    preload() {
      Object.values(SHOOTING_STAR_SPRITES).forEach((sprite) => {
        this.load.spritesheet(sprite.key, sprite.src, {
          frameWidth: SHOOTING_STAR_SPRITE_SIZE,
          frameHeight: SHOOTING_STAR_SPRITE_SIZE,
        });
      });
      Object.values(SHOOTING_STAR_BACKDROP_IMAGES).forEach((image) => {
        this.load.image(image.key, image.src);
      });
      const seen = new Set();
      this.load.on("loaderror", (file) => {
        const key = file && file.key;
        if (!key || seen.has(key)) return;
        seen.add(key);
        if (typeof reportSpaceError === "function") {
          reportSpaceError("Phaser image failed to load.", {
            source: "ShootingStarRideScene",
            asset: file && file.src ? file.src : key,
          });
        }
      });
    }

    rideStatus(extra = {}) {
      return {
        snacks: this.snacks,
        hearts: this.hearts,
        target: SHOOTING_STAR_TARGET_SNACKS,
        missedSnacks: this.missedSnacks,
        phase: this.phase,
        ...extra,
      };
    }

    emitRideEvent(name, extra) {
      const callback = callbacks[name];
      if (typeof callback !== "function") return;
      try {
        callback(this.rideStatus(extra));
      } catch (error) {
        if (typeof reportSpaceError === "function") {
          reportSpaceError("Shooting Star Ride HUD callback failed.", {
            source: "ShootingStarRideScene",
            event: name,
            message: error?.message || String(error),
          });
        }
      }
    }

    nextSnackLetter() {
      const letter = SHOOTING_STAR_LETTERS[
        this.spawnLetterIndex % SHOOTING_STAR_LETTERS.length
      ];
      this.spawnLetterIndex += 1;
      return letter;
    }

    create() {
      const { width, height } = this.scale;
      this.cameras.main.setBackgroundColor("#07091f");
      this.createAnimations();
      this.createGeneratedBackdrops(width, height);
      this.createPlayer(width, height);
      if (SHOOTING_STAR_SHOW_CANVAS_HINTS) this.createSceneText(width, height);
      this.snackGroup = this.add.group();
      this.dustGroup = this.add.group();
      this.gateGroup = this.add.group();
      this.spawnSnackTimer = this.time.addEvent({
        delay: SHOOTING_STAR_TIMING.snackDelay,
        loop: true,
        callback: () => this.spawnSnack(),
      });
      this.spawnDustTimer = this.time.addEvent({
        delay: SHOOTING_STAR_TIMING.dustDelay,
        loop: true,
        callback: () => this.spawnDust(),
      });
      this.input.on("pointerdown", (pointer) => this.setPointerTarget(pointer));
      this.input.on("pointermove", (pointer) => {
        if (pointer.isDown) this.setPointerTarget(pointer);
      });
      this.keys = this.input.keyboard?.addKeys({
        up: PhaserLib.Input.Keyboard.KeyCodes.UP,
        down: PhaserLib.Input.Keyboard.KeyCodes.DOWN,
        w: PhaserLib.Input.Keyboard.KeyCodes.W,
        s: PhaserLib.Input.Keyboard.KeyCodes.S,
        space: PhaserLib.Input.Keyboard.KeyCodes.SPACE,
      });
      this.keys?.space.on("down", () => this.boost());
      this.scale.on("resize", this.relayout, this);
      this.pagePauseHandlers = {
        visibility: () => (document.hidden ? this.pauseForPage() : this.resumeFromPage()),
        blur: () => {
          this.clearGlobalKeys();
          this.pauseForPage();
        },
        focus: () => this.resumeFromPage(),
      };
      this.globalKeyHandlers = {
        down: (event) => this.handleGlobalKey(event, true),
        up: (event) => this.handleGlobalKey(event, false),
      };
      document.addEventListener("visibilitychange", this.pagePauseHandlers.visibility);
      window.addEventListener("blur", this.pagePauseHandlers.blur);
      window.addEventListener("focus", this.pagePauseHandlers.focus);
      window.addEventListener("keydown", this.globalKeyHandlers.down, { passive: false });
      window.addEventListener("keyup", this.globalKeyHandlers.up, { passive: false });
      this.events.once("shutdown", () => {
        this.spawnSnackTimer?.remove(false);
        this.spawnDustTimer?.remove(false);
        this.firstSnackTimer?.remove(false);
        this.firstDustTimer?.remove(false);
        this.scale.off("resize", this.relayout, this);
        if (this.pagePauseHandlers) {
          document.removeEventListener("visibilitychange", this.pagePauseHandlers.visibility);
          window.removeEventListener("blur", this.pagePauseHandlers.blur);
          window.removeEventListener("focus", this.pagePauseHandlers.focus);
          this.pagePauseHandlers = null;
        }
        if (this.globalKeyHandlers) {
          window.removeEventListener("keydown", this.globalKeyHandlers.down);
          window.removeEventListener("keyup", this.globalKeyHandlers.up);
          this.globalKeyHandlers = null;
        }
        if (this.keys?.space) this.keys.space.removeAllListeners("down");
      });
      this.emitRideEvent("onRound");
      this.firstSnackTimer = this.time.delayedCall(
        SHOOTING_STAR_TIMING.firstSnackDelay,
        () => this.spawnSnack(),
      );
      this.firstDustTimer = this.time.delayedCall(
        SHOOTING_STAR_TIMING.firstDustDelay,
        () => this.spawnDust(),
      );
    }

    createAnimations() {
      Object.values(SHOOTING_STAR_SPRITES).forEach((sprite) => {
        this.anims.create({
          key: sprite.animation,
          frames: Array.isArray(sprite.frames)
            ? sprite.frames.map((frame) => ({ key: sprite.key, frame }))
            : this.anims.generateFrameNumbers(sprite.key, {
                start: 0,
                end: 15,
              }),
          frameRate: sprite.frameRate,
          repeat: sprite.repeat,
        });
      });
    }

    drawBackdrop(width, height) {
      this.nebula = this.add.graphics();
      this.nebula.setDepth(0.05);
      this.planetOrbs = this.add.graphics();
      this.planetOrbs.setDepth(0.12);
      this.frontSparkles = this.add.graphics();
      this.frontSparkles.setDepth(0.72);
      this.starLayers = [];
      const compactScene = width < 640 || height < 700;
      for (let layer = 0; layer < 3; layer += 1) {
        const graphics = this.add.graphics();
        graphics.setDepth(0.2 + layer * 0.2);
        const stars = [];
        const count = this.reducedMotion
          ? [44, 28, 16][layer]
          : compactScene
            ? [62, 36, 18][layer]
          : [96, 54, 28][layer];
        for (let i = 0; i < count; i += 1) {
          stars.push({
            x: PhaserLib.Math.Between(0, width),
            y: PhaserLib.Math.Between(0, height),
            r: PhaserLib.Math.FloatBetween(
              layer === 0 ? 0.55 : 0.9,
              layer === 2 ? 3.1 : 2.1,
            ),
            a: PhaserLib.Math.FloatBetween(0.34, 0.92),
            twinkle: PhaserLib.Math.FloatBetween(0, Math.PI * 2),
            color: SHOOTING_STAR_STAR_COLORS[
              PhaserLib.Math.Between(0, SHOOTING_STAR_STAR_COLORS.length - 1)
            ],
            speed: [16, 34, 62][layer],
          });
        }
        this.starLayers.push({ graphics, stars });
      }
      this.paintBackdrop(width, height);
    }

    paintBackdrop(width, height) {
      if (!this.nebula) return;
      const driftTime = this.backdropTime || 0;
      this.nebula.clear();
      SHOOTING_STAR_NEBULA_BANDS.forEach((band) => {
        const driftX = Math.sin(driftTime * (0.00015 + band.alpha * 0.0004) + band.x * 7) * width * 0.025;
        const driftY = Math.cos(driftTime * (0.00012 + band.alpha * 0.0003) + band.y * 6) * height * 0.016;
        this.nebula.fillStyle(band.color, band.alpha);
        this.nebula.fillEllipse(
          width * band.x + driftX,
          height * band.y + driftY,
          width * band.w,
          height * band.h,
        );
      });
      SHOOTING_STAR_GLOW_CLOUDS.forEach((cloud, index) => {
        const x = width * cloud.x + Math.sin(driftTime * cloud.drift + index) * width * 0.04;
        const y = height * cloud.y + Math.cos(driftTime * cloud.drift * 0.8 + index * 0.7) * height * 0.02;
        this.nebula.fillStyle(cloud.color, cloud.alpha);
        this.nebula.fillEllipse(x, y, width * cloud.w, height * cloud.h);
        this.nebula.fillStyle(0xffffff, cloud.alpha * 0.08);
        this.nebula.fillEllipse(x - width * 0.018, y - height * 0.006, width * cloud.w * 0.42, height * cloud.h * 0.3);
      });
      this.nebula.lineStyle(2, 0x8feaff, 0.1);
      this.nebula.beginPath();
      this.nebula.moveTo(-20, height * 0.48);
      this.nebula.lineTo(width * 0.24, height * 0.54);
      this.nebula.lineTo(width * 0.48, height * 0.5);
      this.nebula.lineTo(width * 0.76, height * 0.57);
      this.nebula.lineTo(width + 20, height * 0.52);
      this.nebula.strokePath();
      this.nebula.lineStyle(2, 0xffd96c, 0.08);
      this.nebula.beginPath();
      this.nebula.moveTo(-20, height * 0.39);
      this.nebula.lineTo(width * 0.2, height * 0.43);
      this.nebula.lineTo(width * 0.52, height * 0.4);
      this.nebula.lineTo(width * 0.82, height * 0.46);
      this.nebula.lineTo(width + 20, height * 0.42);
      this.nebula.strokePath();
      SHOOTING_STAR_DUST_CLUSTERS.forEach((cluster, clusterIndex) => {
        for (let i = 0; i < 10; i += 1) {
          const angle = (i / 10) * Math.PI * 2 + clusterIndex * 0.6;
          const radius = 10 + (i % 4) * 8;
          const drift = Math.sin(driftTime * 0.001 + clusterIndex * 1.4 + i * 0.33) * 12;
          this.nebula.fillStyle(cluster.color, 0.07 + (i % 3) * 0.025);
          this.nebula.fillCircle(
            width * cluster.x + Math.cos(angle) * radius + drift,
            height * cluster.y + Math.sin(angle) * radius * 0.56,
            i % 4 === 0 ? 2.4 : 1.5,
          );
        }
      });
      if (this.planetOrbs) {
        this.planetOrbs.clear();
        SHOOTING_STAR_PLANET_ORBS.forEach((orb) => {
          const radius = Math.min(width, height) < 560 ? orb.r * 0.7 : orb.r;
          const x = width * orb.x + Math.sin(driftTime * 0.00018 + orb.x * 5.5) * width * 0.012;
          const y = height * orb.y + Math.cos(driftTime * 0.00014 + orb.y * 4.2) * height * 0.01;
          this.planetOrbs.fillStyle(orb.color, orb.alpha);
          this.planetOrbs.fillCircle(x, y, radius);
          if (orb.crescent) {
            this.planetOrbs.fillStyle(0x07091f, 0.54);
            this.planetOrbs.fillCircle(x + radius * 0.24, y - radius * 0.08, radius * 0.92);
          }
          this.planetOrbs.lineStyle(2, orb.ring, orb.alpha + 0.07);
          this.planetOrbs.strokeEllipse(x, y, radius * 2.7, radius * 0.64);
          this.planetOrbs.lineStyle(1, orb.ring, orb.alpha * 0.55);
          this.planetOrbs.strokeEllipse(x, y + radius * 0.08, radius * 2.15, radius * 0.42);
          this.planetOrbs.fillStyle(0xffffff, orb.alpha * 0.55);
          this.planetOrbs.fillCircle(x - radius * 0.28, y - radius * 0.32, radius * 0.18);
        });
      }
      if (this.frontSparkles) {
        this.frontSparkles.clear();
        SHOOTING_STAR_FRONT_SPARKLES.forEach((sparkle, index) => {
          const y = height * sparkle.y + Math.sin(driftTime * 0.0007 + index) * 18;
          const baseX = ((driftTime * sparkle.speed) / 1000) % (width + sparkle.width * 4);
          for (let i = -1; i < 4; i += 1) {
            const x = baseX + i * (width * 0.34);
            this.frontSparkles.lineStyle(3, sparkle.color, sparkle.alpha * 0.16);
            this.frontSparkles.beginPath();
            this.frontSparkles.moveTo(x, y);
            this.frontSparkles.lineTo(x - sparkle.width, y + sparkle.width * 0.1);
            this.frontSparkles.strokePath();
            this.frontSparkles.fillStyle(sparkle.color, sparkle.alpha + 0.08);
            this.frontSparkles.fillCircle(x, y, 2.2);
          }
        });
      }
    }

    createCometStreams(width, height) {
      this.cometStreams = [];
      const compactScene = width < 640 || height < 700;
      const streamCount = this.reducedMotion ? 3 : compactScene ? 3 : 5;
      for (let i = 0; i < streamCount; i += 1) {
        const stream = this.add.graphics();
        stream.setDepth(0.5);
        this.cometStreams.push({
          graphics: stream,
          x: PhaserLib.Math.Between(-width * 0.2, width),
          y: PhaserLib.Math.Between(120, Math.max(160, height - 160)),
          length: PhaserLib.Math.Between(90, 180),
          speed: PhaserLib.Math.Between(84, 132),
          width: PhaserLib.Math.FloatBetween(2, 4.5),
          color: i % 2 === 0 ? 0xffd96c : 0x8feaff,
          alpha: PhaserLib.Math.FloatBetween(0.18, 0.34),
        });
      }
    }

    updateCometStreams(delta) {
      if (!this.cometStreams) return;
      const { width, height } = this.scale;
      this.cometStreams.forEach((stream) => {
        stream.x += (stream.speed * delta) / 1000;
        if (stream.x > width + stream.length + 40) {
          stream.x = -PhaserLib.Math.Between(40, 220);
          stream.y = PhaserLib.Math.Between(120, Math.max(160, height - 160));
          stream.length = PhaserLib.Math.Between(90, 180);
          stream.width = PhaserLib.Math.FloatBetween(2, 4.5);
        }
        const g = stream.graphics;
        g.clear();
        g.lineStyle(stream.width + 6, stream.color, stream.alpha * 0.16);
        g.beginPath();
        g.moveTo(stream.x, stream.y);
        g.lineTo(stream.x - stream.length * 1.12, stream.y + stream.length * 0.14);
        g.strokePath();
        g.lineStyle(stream.width, stream.color, stream.alpha);
        g.beginPath();
        g.moveTo(stream.x, stream.y);
        g.lineTo(stream.x - stream.length, stream.y + stream.length * 0.12);
        g.strokePath();
        g.fillStyle(stream.color, stream.alpha + 0.12);
        g.fillCircle(stream.x, stream.y, 3);
      });
    }

    drawStarLayers(delta) {
      const { width, height } = this.scale;
      this.starLayers.forEach((layer) => {
        layer.graphics.clear();
        layer.stars.forEach((star) => {
          star.x += (star.speed * delta) / 1000;
          if (star.x > width + 8) {
            star.x = -PhaserLib.Math.Between(4, 40);
            star.y = PhaserLib.Math.Between(0, height);
          }
          star.twinkle += delta * 0.002;
          const alpha = PhaserLib.Math.Clamp(
            star.a + Math.sin(star.twinkle) * 0.16,
            0.18,
            1,
          );
          layer.graphics.fillStyle(star.color, alpha);
          layer.graphics.fillCircle(star.x, star.y, star.r);
          if (star.r > 2.3) {
            layer.graphics.lineStyle(1, star.color, alpha * 0.34);
            layer.graphics.beginPath();
            layer.graphics.moveTo(star.x - star.r * 2.8, star.y);
            layer.graphics.lineTo(star.x + star.r * 2.8, star.y);
            layer.graphics.moveTo(star.x, star.y - star.r * 1.9);
            layer.graphics.lineTo(star.x, star.y + star.r * 1.9);
            layer.graphics.strokePath();
          }
        });
      });
    }

    createTrail(width, height) {
      this.pathRibbon = this.add.graphics();
      this.pathRibbon.setDepth(1);
      this.paintTrail(width, height);
    }

    paintTrail(width, height) {
      if (!this.pathRibbon) return;
      this.pathRibbon.clear();
      const y = height * 0.5;
      this.pathRibbon.lineStyle(30, 0xffd96c, 0.05);
      this.pathRibbon.beginPath();
      this.pathRibbon.moveTo(-60, y + 82);
      this.pathRibbon.lineTo(width + 60, y - 56);
      this.pathRibbon.strokePath();
      this.pathRibbon.lineStyle(18, 0x8feaff, 0.07);
      this.pathRibbon.beginPath();
      this.pathRibbon.moveTo(-30, y - 74);
      this.pathRibbon.lineTo(width + 30, y + 48);
      this.pathRibbon.strokePath();
      this.pathRibbon.lineStyle(8, 0xffd96c, 0.22);
      this.pathRibbon.beginPath();
      this.pathRibbon.moveTo(0, y + 46);
      this.pathRibbon.lineTo(width, y - 26);
      this.pathRibbon.strokePath();
      this.pathRibbon.lineStyle(4, 0x7ee7ff, 0.26);
      this.pathRibbon.beginPath();
      this.pathRibbon.moveTo(0, y - 36);
      this.pathRibbon.lineTo(width, y + 32);
      this.pathRibbon.strokePath();
      this.pathRibbon.fillStyle(0xffffff, 0.18);
      for (let i = 0; i < 12; i += 1) {
        const x = (width / 11) * i;
        const wave = Math.sin(i * 1.7) * 34;
        this.pathRibbon.fillCircle(x, y + wave, i % 3 === 0 ? 3 : 2);
      }
    }

    createPlayer(width, height) {
      this.player = this.add.sprite(width * 0.2, height * 0.52, SHOOTING_STAR_SPRITES.rider.key);
      this.player.setDepth(8);
      this.player.setDisplaySize(width < 640 ? 152 : 196, width < 640 ? 152 : 196);
      this.player.setFlipX(true);
      this.player.play(SHOOTING_STAR_SPRITES.rider.animation);
      this.playerYTarget = this.player.y;
      this.player.angle = -2;
    }

    createSceneText(width, height) {
      const compact = width < 640 || height < 700;
      this.statusText = this.add
        .text(compact ? 24 : 28, compact ? 104 : 28, "Ride the star trail", {
          fontFamily: "Inter, sans-serif",
          fontSize: width < 640 ? "15px" : "20px",
          fontStyle: "900",
          color: "#fff7d8",
          wordWrap: { width: Math.min(560, width - 48) },
        })
        .setDepth(6)
        .setAlpha(0.86);
      this.promptText = this.add
        .text(compact ? 24 : 28, compact ? 128 : 58, "Collect 6 sparkle snacks. Dust puffs only slow you down.", {
          fontFamily: "Inter, sans-serif",
          fontSize: width < 640 ? "11px" : "14px",
          fontStyle: "700",
          color: "#dcecff",
          wordWrap: { width: Math.min(620, width - 48) },
        })
        .setDepth(6)
        .setAlpha(0.76);
    }

    setSceneHint(title, prompt) {
      if (!SHOOTING_STAR_SHOW_CANVAS_HINTS) return;
      if (this.statusText && title) this.statusText.setText(title);
      if (this.promptText && prompt) this.promptText.setText(prompt);
    }

    clearGlobalKeys() {
      this.globalKeyState.up = false;
      this.globalKeyState.down = false;
    }

    handleGlobalKey(event, isDown) {
      if (!SHOOTING_STAR_ACTION_KEYS.has(event.code)) return;
      event.preventDefault();
      const direction = SHOOTING_STAR_KEY_DIRECTIONS[event.code];
      if (direction) {
        this.globalKeyState[direction] = isDown;
        if (isDown && !event.repeat) this.nudgePlayer(direction);
        return;
      }
      if (event.code === "Space" && isDown && !event.repeat) this.boost();
    }

    nudgePlayer(direction) {
      if (this.phase !== "play" || !this.player) return;
      const bounds = this.playfieldY();
      const offset = direction === "up" ? -SHOOTING_STAR_KEY_NUDGE : SHOOTING_STAR_KEY_NUDGE;
      this.playerYTarget = PhaserLib.Math.Clamp(
        this.player.y + offset,
        bounds.min,
        bounds.max,
      );
    }

    pauseForPage() {
      if (!this.sys || !this.scene) return;
      if (this.phase === "complete" || this.pausedForPage) return;
      this.clearGlobalKeys();
      this.pausedForPage = true;
      this.scene.pause();
    }

    resumeFromPage() {
      if (!this.sys || !this.scene || !this.pausedForPage) return;
      this.pausedForPage = false;
      if (!document.hidden) {
        this.scene.resume();
      }
    }

    relayout() {
      if (!this.sys || !this.sys.isActive()) return;
      const { width, height } = this.scale;
      const compact = width < 640 || height < 700;
      this.fitGeneratedBackdrops(width, height);
      if (this.player) {
        this.player.x = compact ? width * 0.23 : width * 0.2;
        this.player.setDisplaySize(compact ? 152 : 196, compact ? 152 : 196);
        const bounds = this.playfieldY();
        this.player.y = PhaserLib.Math.Clamp(this.player.y, bounds.min, bounds.max);
        this.playerYTarget = PhaserLib.Math.Clamp(this.playerYTarget || this.player.y, bounds.min, bounds.max);
      }
      if (this.statusText) {
        this.statusText.setPosition(compact ? 24 : 28, compact ? 104 : 28);
        this.statusText.setStyle({
          fontSize: compact ? "15px" : "20px",
          wordWrap: { width: Math.min(560, width - 48) },
        });
      }
      if (this.promptText) {
        this.promptText.setPosition(compact ? 24 : 28, compact ? 128 : 58);
        this.promptText.setStyle({
          fontSize: compact ? "11px" : "14px",
          wordWrap: { width: Math.min(620, width - 48) },
        });
      }
    }

    createGeneratedBackdrops(width, height) {
      this.generatedBackdropLayers = Object.values(SHOOTING_STAR_BACKDROP_IMAGES).map((image) => {
        const texture = this.textures.get(image.key).getSourceImage();
        const layer = this.add
          .tileSprite(width * 0.5, height * 0.5, width, height, image.key)
          .setDepth(image.depth)
          .setAlpha(image.alpha);
        return {
          ...image,
          layer,
          sourceWidth: texture?.width || 1774,
          sourceHeight: texture?.height || 887,
        };
      });
      this.fitGeneratedBackdrops(width, height);
    }

    fitGeneratedBackdrops(width, height) {
      if (!this.generatedBackdropLayers) return;
      this.generatedBackdropLayers.forEach((backdrop) => {
        const scale = Math.max(
          height / backdrop.sourceHeight,
          width / backdrop.sourceWidth,
        );
        backdrop.layer
          .setPosition(width * 0.5, height * 0.5)
          .setSize(width, height)
          .setTileScale(scale, scale);
      });
    }

    updateGeneratedBackdrops(delta) {
      if (!this.generatedBackdropLayers || this.reducedMotion) return;
      this.generatedBackdropLayers.forEach((backdrop) => {
        backdrop.layer.tilePositionX += (backdrop.speed * delta) / 1000;
      });
    }

    playfieldY() {
      const { width, height } = this.scale;
      const compact = width < 640 || height < 700;
      const min = compact ? SHOOTING_STAR_COMPACT_PLAYFIELD_TOP : SHOOTING_STAR_DESKTOP_HUD_SAFE_TOP;
      const max = compact ? height - SHOOTING_STAR_COMPACT_HUD_SAFE_ZONE : height - 128;
      return {
        min,
        max: Math.max(min + 80, max),
      };
    }

    nextLaneY(avoidY = null) {
      const PhaserMath = PhaserLib.Math;
      const bounds = this.playfieldY();
      const laneYs = SHOOTING_STAR_LANE_FACTORS.map((factor) =>
        PhaserMath.Linear(bounds.min, bounds.max, factor),
      );
      let laneIndex = this.spawnLaneIndex % laneYs.length;
      this.spawnLaneIndex += 1;
      if (
        typeof avoidY === "number" &&
        Math.abs(laneYs[laneIndex] - avoidY) < 86
      ) {
        laneIndex = (laneIndex + 2) % laneYs.length;
      }
      return laneYs[laneIndex] + PhaserMath.Between(-18, 18);
    }

    setPointerTarget(pointer) {
      if (this.phase !== "play") return;
      const bounds = this.playfieldY();
      this.playerYTarget = PhaserLib.Math.Clamp(pointer.y, bounds.min, bounds.max);
    }

    boost() {
      if (this.phase !== "play") return;
      if (this.time.now - this.lastBoostAt < SHOOTING_STAR_TIMING.boostCooldown) return;
      this.lastBoostAt = this.time.now;
      const bounds = this.playfieldY();
      this.playerYTarget = Math.max(bounds.min, this.player.y - SHOOTING_STAR_BOOST_NUDGE);
      this.player.play(SHOOTING_STAR_SPRITES.boost.animation, true);
      this.player.once(`animationcomplete-${SHOOTING_STAR_SPRITES.boost.animation}`, () => {
        if (this.player && this.phase === "play") {
          this.player.play(SHOOTING_STAR_SPRITES.rider.animation, true);
        }
      });
      this.emitSparkles(this.player.x - 54, this.player.y + 34, 0xffd96c, 14);
      this.emitRideEvent("onBoost");
    }

    spawnSnack() {
      if (this.phase !== "play" || this.snacks >= SHOOTING_STAR_TARGET_SNACKS) return;
      const { width } = this.scale;
      const snack = this.add.sprite(width + 84, this.nextLaneY(), SHOOTING_STAR_SPRITES.snack.key);
      snack.setData("kind", "snack");
      this.decorateSnackWithLetter(snack, this.nextSnackLetter());
      snack.setDisplaySize(82, 82);
      snack.play(SHOOTING_STAR_SPRITES.snack.animation);
      snack.setDepth(5);
      this.snackGroup.add(snack);
    }

    decorateSnackWithLetter(snack, letter) {
      snack.setData("letter", letter);
      const label = this.add
        .text(snack.x, snack.y - 2, letter, {
          fontFamily: "Inter, sans-serif",
          fontSize: "30px",
          fontStyle: "900",
          color: "#241247",
          stroke: "#fff7d8",
          strokeThickness: 5,
        })
        .setOrigin(0.5)
        .setDepth(7);
      snack.setData("letterText", label);
      snack.once("destroy", () => {
        if (label.active) label.destroy();
      });
    }

    spawnDust() {
      if (this.phase !== "play" || this.finished) return;
      const { width } = this.scale;
      const dust = this.add.sprite(width + 96, this.nextLaneY(this.player?.y), SHOOTING_STAR_SPRITES.dust.key);
      dust.setData("kind", "dust");
      dust.setDisplaySize(width < 640 ? 126 : 152, width < 640 ? 126 : 152);
      dust.play(SHOOTING_STAR_SPRITES.dust.animation);
      dust.setDepth(4);
      this.dustGroup.add(dust);
    }

    spawnGate() {
      if (this.gateSpawned || this.phase !== "play") return;
      this.gateSpawned = true;
      const { width, height } = this.scale;
      const gate = this.add.sprite(width + 150, height * 0.5, SHOOTING_STAR_SPRITES.gate.key);
      gate.setData("kind", "gate");
      gate.setDisplaySize(width < 640 ? 188 : 244, width < 640 ? 188 : 244);
      gate.play(SHOOTING_STAR_SPRITES.gate.animation);
      gate.setDepth(3);
      this.gateGroup.add(gate);
      this.setSceneHint(
        "Comet gate ahead",
        "Glide through the sparkle gate to finish the ride.",
      );
      this.emitRideEvent("onGate");
    }

    update(time, delta) {
      if (this.phase !== "play") return;
      const stepDelta = Math.min(delta, SHOOTING_STAR_DELTA_CAP);
      this.updateGeneratedBackdrops(stepDelta);
      this.updatePlayer(stepDelta);
      this.moveObjects(stepDelta);
      this.checkCollisions();
      if (this.snacks >= SHOOTING_STAR_TARGET_SNACKS) this.spawnGate();
    }

    updatePlayer(delta) {
      const bounds = this.playfieldY();
      const keyboardSpeed = SHOOTING_STAR_KEYBOARD_SPEED;
      const holdingUp = this.globalKeyState.up || this.keys?.up.isDown || this.keys?.w.isDown;
      const holdingDown = this.globalKeyState.down || this.keys?.down.isDown || this.keys?.s.isDown;
      const keyDirection = holdingUp === holdingDown ? 0 : holdingUp ? -1 : 1;
      if (keyDirection !== 0) {
        const currentTarget = Number.isFinite(this.playerYTarget)
          ? this.playerYTarget
          : this.player.y;
        this.playerYTarget =
          currentTarget + (keyDirection * keyboardSpeed * delta) / 1000;
      }
      this.playerYTarget = PhaserLib.Math.Clamp(this.playerYTarget, bounds.min, bounds.max);
      const dtSeconds = delta / 1000;
      const targetGap = this.playerYTarget - this.player.y;
      const maxStep = SHOOTING_STAR_PLAYER_MAX_SPEED * dtSeconds;
      const ease = 1 - Math.exp(-SHOOTING_STAR_PLAYER_RESPONSE * dtSeconds);
      const easedStep = targetGap * ease;
      const step = PhaserLib.Math.Clamp(easedStep, -maxStep, maxStep);
      this.player.y += step;
      const playerVelocity = dtSeconds > 0 ? step / dtSeconds : 0;
      this.player.angle = PhaserLib.Math.Clamp(
        playerVelocity * SHOOTING_STAR_PLAYER_TILT_PER_SPEED,
        -8,
        8,
      );
    }

    moveObjects(delta) {
      const move = (sprite) => {
        const kind = sprite.getData("kind");
        const speedBoost = kind === "gate" ? 0.8 : 1;
        sprite.x -= (this.speed * speedBoost * delta) / 1000;
        const letterText = sprite.getData("letterText");
        if (letterText?.active) {
          letterText.setPosition(sprite.x, sprite.y - 2);
        }
        if (sprite.x < (SHOOTING_STAR_EXIT_X[kind] || -120)) {
          if (kind === "snack") this.recordMissedSnack();
          if (kind === "gate") this.recordMissedGate();
          sprite.destroy();
        }
      };
      this.snackGroup.getChildren().forEach(move);
      this.dustGroup.getChildren().forEach(move);
      this.gateGroup.getChildren().forEach(move);
    }

    recordMissedGate() {
      if (this.phase !== "play" || this.finished) return;
      this.gateSpawned = false;
      this.setSceneHint(
        "Comet gate looping back",
        "The gate circles around. Stay on the trail and try again.",
      );
      this.emitRideEvent("onGateMiss");
      this.time.delayedCall(900, () => {
        if (!this.sys || !this.sys.isActive()) return;
        this.spawnGate();
      });
    }

    recordMissedSnack() {
      if (this.phase !== "play" || this.snacks >= SHOOTING_STAR_TARGET_SNACKS) return;
      this.missedSnacks += 1;
      if (this.missedSnacks < SHOOTING_STAR_HELPER_MISS_LIMIT) return;
      this.missedSnacks = 0;
      this.spawnHelperSnack();
    }

    spawnHelperSnack() {
      const { width } = this.scale;
      const y = this.player
        ? PhaserLib.Math.Clamp(this.player.y, this.playfieldY().min, this.playfieldY().max)
        : this.nextLaneY();
      const snack = this.add.sprite(width + 54, y, SHOOTING_STAR_SPRITES.snack.key);
      snack.setData("kind", "snack");
      snack.setData("helper", true);
      this.decorateSnackWithLetter(snack, this.nextSnackLetter());
      snack.setDisplaySize(96, 96);
      snack.play(SHOOTING_STAR_SPRITES.snack.animation);
      snack.setDepth(6);
      this.snackGroup.add(snack);
      this.emitSparkles(width - 34, y, 0x8feaff, 8);
      this.emitRideEvent("onHelperSnack");
    }

    checkCollisions() {
      const player = this.player;
      this.snackGroup.getChildren().forEach((snack) => {
        if (!snack.active) return;
        if (
          PhaserLib.Math.Distance.Between(player.x, player.y, snack.x, snack.y) >
          SHOOTING_STAR_SNACK_RADIUS
        ) return;
        const letter = snack.getData("letter");
        snack.destroy();
        this.snacks = Math.min(SHOOTING_STAR_TARGET_SNACKS, this.snacks + 1);
        this.emitSparkles(snack.x, snack.y, 0xffe47a, 16);
        this.setSceneHint(
          letter ? `Letter ${letter}` : `Sparkle snack ${this.snacks}`,
          this.snacks >= SHOOTING_STAR_TARGET_SNACKS
            ? "Snack pouch full. Watch for the comet gate."
            : letter
              ? `Nice catch. Letter ${letter} joined the sparkle pouch.`
              : "Nice catch. Keep gliding along the star trail.",
        );
        this.emitRideEvent("onSnack", { letter });
        playRideSound("chime");
      });

      this.dustGroup.getChildren().forEach((dust) => {
        if (!dust.active || this.time.now < this.invulnerableUntil) return;
        if (
          PhaserLib.Math.Distance.Between(player.x, player.y, dust.x, dust.y) >
          SHOOTING_STAR_DUST_RADIUS
        ) return;
        this.bumpDust(dust);
      });

      this.gateGroup.getChildren().forEach((gate) => {
        if (!gate.active || this.finished) return;
        if (
          PhaserLib.Math.Distance.Between(player.x, player.y, gate.x, gate.y) >
          SHOOTING_STAR_GATE_RADIUS
        ) return;
        this.finishRide(gate);
      });
    }

    bumpDust(dust) {
      dust.destroy();
      this.hearts -= 1;
      this.invulnerableUntil = this.time.now + SHOOTING_STAR_TIMING.invulnerableMs;
      this.speed = SHOOTING_STAR_BUMP_SPEED;
      if (!this.reducedMotion) {
        this.cameras.main.shake(140, 0.004);
        this.tweens.add({
          targets: this.player,
          alpha: 0.56,
          duration: 120,
          yoyo: true,
          repeat: 4,
          onComplete: () => this.player.setAlpha(1),
        });
      }
      this.time.delayedCall(SHOOTING_STAR_TIMING.bumpSlowMs, () => {
        if (!this.sys || !this.sys.isActive()) return;
        this.speed = SHOOTING_STAR_BASE_SPEED;
      });
      this.emitSparkles(this.player.x + 12, this.player.y, 0x9bd7ff, 12);
      if (this.hearts <= 0) {
        this.hearts = SHOOTING_STAR_REFILL_HEARTS;
        this.setSceneHint(
          "Sparkle reset",
          "The star catches you. Hearts refilled, snacks stay safe.",
        );
        this.emitRideEvent("onRefill");
      } else {
        this.setSceneHint(
          "Soft dust puff",
          `You slowed down, but the ride keeps going. Hearts: ${this.hearts}.`,
        );
        this.emitRideEvent("onBump");
      }
      playRideSound("boop");
    }

    finishRide(gate) {
      this.finished = true;
      this.phase = "complete";
      gate.destroy();
      this.spawnSnackTimer?.remove(false);
      this.spawnDustTimer?.remove(false);
      this.setSceneHint(
        "Shooting star landing",
        "You filled the sparkle pouch and reached the comet gate.",
      );
      this.emitSparkles(this.player.x + 54, this.player.y, 0xffd96c, 34);
      this.tweens.add({
        targets: this.player,
        x: this.scale.width + 140,
        y: this.scale.height * 0.46,
        scale: 0.88,
        duration: this.reducedMotion ? 520 : 1100,
        ease: "Sine.in",
      });
      this.emitRideEvent("onComplete");
      playRideSound("whoosh");
    }

    emitSparkles(x, y, color, count) {
      const sparkleCount = this.reducedMotion ? Math.max(3, Math.ceil(count * 0.45)) : count;
      for (let i = 0; i < sparkleCount; i += 1) {
        const sparkle = this.add.graphics();
        sparkle.fillStyle(color, 1);
        sparkle.fillCircle(0, 0, PhaserLib.Math.Between(2, 4));
        sparkle.setPosition(x, y);
        sparkle.setDepth(9);
        const angle = PhaserLib.Math.FloatBetween(0, Math.PI * 2);
        const distance = PhaserLib.Math.Between(24, 92);
        this.tweens.add({
          targets: sparkle,
          x: x + Math.cos(angle) * distance,
          y: y + Math.sin(angle) * distance,
          alpha: 0,
          duration: 620,
          ease: "Sine.out",
          onComplete: () => sparkle.destroy(),
        });
      }
    }
  }

  return new PhaserLib.Game({
    type: PhaserLib.AUTO,
    parent: mount,
    width: mount.clientWidth || 960,
    height: mount.clientHeight || 620,
    backgroundColor: "#07091f",
    scale: {
      mode: PhaserLib.Scale.RESIZE,
      autoCenter: PhaserLib.Scale.CENTER_BOTH,
    },
    scene: ShootingStarRideScene,
  });
}

function shootingStarGoalText(goal, snacks) {
  if (goal === "gate") return "Goal: comet gate";
  if (goal === "done") return "Goal complete";
  return `Goal: snacks ${snacks}/${SHOOTING_STAR_TARGET_SNACKS}`;
}

function shootingStarSnackScoreText(snacks) {
  return `${snacks} of ${SHOOTING_STAR_TARGET_SNACKS}`;
}

function playShootingStarLetterVoice(letter) {
  const clip = SHOOTING_STAR_LETTER_CLIPS[letter];
  if (!clip || !window.__narration?.play) return;
  window.__narration.play(clip);
}

function ShootingStarRidePhaserLevel({ onExit }) {
  const gameRef = useRef(null);
  const mountRef = useRef(null);
  const [runKey, setRunKey] = useState(0);
  const [status, setStatus] = useState(SHOOTING_STAR_STATUS_COPY.start);
  const [snacks, setSnacks] = useState(0);
  const [hearts, setHearts] = useState(SHOOTING_STAR_START_HEARTS);
  const [goal, setGoal] = useState("snacks");
  const [complete, setComplete] = useState(false);

  useEffect(() => {
    if (!mountRef.current) return undefined;
    if (!window.Phaser) {
      setStatus(SHOOTING_STAR_STATUS_COPY.phaserMissing);
      return undefined;
    }
    gameRef.current = createShootingStarRideGame(mountRef.current, {
      onRound: ({ hearts: nextHearts }) => {
        setComplete(false);
        setGoal("snacks");
        setHearts(nextHearts);
        setStatus(SHOOTING_STAR_STATUS_COPY.start);
      },
      onSnack: ({ snacks: nextSnacks, hearts: nextHearts, target, letter }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setGoal(nextSnacks >= target ? "gate" : "snacks");
        playShootingStarLetterVoice(letter);
        setStatus(
          nextSnacks >= target
            ? SHOOTING_STAR_STATUS_COPY.pouchFull
            : letter
              ? `Letter ${letter} captured. ${nextSnacks}/${target} snacks.`
              : `Sparkle snack ${nextSnacks}/${target}.`,
        );
      },
      onBump: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setStatus(`Soft dust puff. ${nextHearts} hearts left.`);
      },
      onRefill: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setStatus(SHOOTING_STAR_STATUS_COPY.refill);
      },
      onGate: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setGoal("gate");
        setStatus(SHOOTING_STAR_STATUS_COPY.gateAhead);
      },
      onGateMiss: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setGoal("gate");
        setStatus(SHOOTING_STAR_STATUS_COPY.gateMiss);
      },
      onHelperSnack: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setStatus(SHOOTING_STAR_STATUS_COPY.helperSnack);
      },
      onBoost: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setStatus(SHOOTING_STAR_STATUS_COPY.boost);
      },
      onComplete: ({ snacks: nextSnacks, hearts: nextHearts }) => {
        setSnacks(nextSnacks);
        setHearts(nextHearts);
        setComplete(true);
        setGoal("done");
        setStatus(`Ride complete with ${nextSnacks} snacks and ${nextHearts} hearts.`);
      },
    });
    const cleanupRuntime = () => {
      if (gameRef.current) {
        gameRef.current.destroy(true);
        gameRef.current = null;
      }
      if (window.__narration) window.__narration.stop();
    };
    const unregisterRuntimeCleanup =
      window.SpaceExplorerFoundation?.registerGameRuntimeCleanup?.(
        cleanupRuntime,
      ) || (() => {});
    return () => {
      unregisterRuntimeCleanup();
      cleanupRuntime();
    };
  }, [runKey]);

  const restart = () => {
    setStatus(SHOOTING_STAR_STATUS_COPY.start);
    setSnacks(0);
    setHearts(SHOOTING_STAR_START_HEARTS);
    setGoal("snacks");
    setComplete(false);
    setRunKey((key) => key + 1);
  };

  return (
    <section className="shooting-star-level" aria-label="Shooting Star Ride Phaser game">
      <button className="planet-sort-back" onClick={onExit}>
        Back to planets
      </button>
      <div className="shooting-star-frame">
        <div ref={mountRef} className="shooting-star-game" />
        <div className="shooting-star-hud">
          <span>Shooting Star Ride</span>
          <div className="shooting-star-score-row" aria-label="Ride score">
            <div className="shooting-star-score-card">
              <small>Snacks</small>
              <strong>{shootingStarSnackScoreText(snacks)}</strong>
            </div>
            <div className="shooting-star-score-card">
              <small>Hearts</small>
              <strong>{hearts}</strong>
            </div>
          </div>
          <strong className="shooting-star-helper" role="status" aria-live="polite">
            {status}
          </strong>
          <small className="shooting-star-goal">
            {shootingStarGoalText(goal, snacks)}
          </small>
          <small className="shooting-star-controls">{SHOOTING_STAR_CONTROL_COPY}</small>
          <div className="shooting-star-flow" aria-label="Ride flow">
            <span className={snacks > 0 ? "on" : ""}>Catch snacks</span>
            <span className={hearts < SHOOTING_STAR_START_HEARTS ? "warn" : ""}>
              Dodge dust
            </span>
            <span className={goal === "gate" || goal === "done" ? "on" : ""}>
              Glide gate
            </span>
          </div>
          <div className="shooting-star-progress" aria-label="Sparkle snacks collected">
            {Array.from({ length: SHOOTING_STAR_TARGET_SNACKS }, (_, index) => (
              <i key={index} className={index < snacks ? "on" : ""} />
            ))}
          </div>
          {complete ? (
            <button className="shooting-star-replay" onClick={restart}>
              Replay
            </button>
          ) : null}
        </div>
      </div>
    </section>
  );
}
