(function () {
  const slot = "ash-below-the-lake";
  const fallbackKey = "play:rpg:ash-below-the-lake";
  const tile = 32;
  const cols = 30;
  const rows = 15;
  const worldH = tile * rows;
  const uiY = worldH;
  const actions = ["Confront", "Listen", "Forget"];
  const areas = {
    harbor: {
      name: "Harbor Row",
      act: "Act 1",
      track: "harbor",
      floor: ["#222c36", "#263541", "#304650"],
      accent: "#e0a451",
      walls: rects([[0, 0, 30, 1], [0, 14, 30, 1], [0, 0, 1, 15], [29, 0, 1, 15], [3, 3, 5, 2], [12, 2, 4, 3], [20, 3, 6, 2], [5, 11, 7, 1], [17, 10, 5, 1]]),
      exits: [{ x: 28, y: 7, to: "red", px: 1, py: 7 }],
      props: [{ kind: "lake", x: 1, y: 11, w: 28, h: 3 }, { kind: "stall", x: 9, y: 5 }, { kind: "flowers", x: 23, y: 9 }],
      start: { x: 3, y: 8 }
    },
    red: {
      name: "The Red Streets",
      act: "Act 1",
      track: "red",
      floor: ["#2f1f2c", "#442237", "#5b283f"],
      accent: "#db4f62",
      walls: rects([[0, 0, 30, 1], [0, 14, 30, 1], [0, 0, 1, 6], [0, 9, 1, 6], [29, 0, 1, 15], [4, 3, 4, 3], [12, 4, 3, 8], [20, 2, 5, 3], [22, 9, 4, 2]]),
      exits: [{ x: 0, y: 7, to: "harbor", px: 28, py: 7 }, { x: 28, y: 12, to: "roots", px: 1, py: 12 }],
      props: [{ kind: "sign", x: 9, y: 3 }, { kind: "stage", x: 17, y: 6 }, { kind: "poster", x: 26, y: 4 }],
      start: { x: 2, y: 7 }
    },
    roots: {
      name: "The Roots",
      act: "Act 2",
      track: "roots",
      floor: ["#182725", "#203832", "#2e493e"],
      accent: "#66c7a2",
      walls: rects([[0, 0, 30, 1], [0, 14, 30, 1], [0, 0, 1, 11], [0, 13, 1, 2], [29, 0, 1, 15], [5, 2, 2, 10], [11, 5, 7, 1], [15, 8, 2, 5], [22, 3, 3, 9]]),
      exits: [{ x: 0, y: 12, to: "red", px: 28, py: 12 }, { x: 28, y: 4, to: "chapel", px: 1, py: 4 }],
      props: [{ kind: "archive", x: 8, y: 8 }, { kind: "root", x: 18, y: 3 }, { kind: "home", x: 25, y: 11 }],
      start: { x: 2, y: 12 }
    },
    chapel: {
      name: "The Drowned Chapel",
      act: "Act 3",
      track: "chapel",
      floor: ["#172232", "#1c3043", "#263b4f"],
      accent: "#8bd3ff",
      walls: rects([[0, 0, 30, 1], [0, 14, 30, 1], [0, 0, 1, 3], [0, 6, 1, 9], [29, 0, 1, 15], [6, 4, 3, 7], [13, 2, 4, 3], [19, 4, 3, 7], [10, 12, 10, 1]]),
      exits: [{ x: 0, y: 4, to: "roots", px: 28, py: 4 }, { x: 28, y: 10, to: "shore", px: 1, py: 10 }],
      props: [{ kind: "pew", x: 10, y: 7 }, { kind: "pew", x: 16, y: 7 }, { kind: "glass", x: 14, y: 5 }],
      start: { x: 2, y: 4 }
    },
    shore: {
      name: "The Hollow Shore",
      act: "Final Act",
      track: "shore",
      floor: ["#111827", "#1b2440", "#211c33"],
      accent: "#f1f5f9",
      walls: rects([[0, 0, 30, 1], [0, 14, 30, 1], [0, 0, 1, 9], [0, 12, 1, 3], [29, 0, 1, 15], [7, 3, 3, 2], [16, 2, 2, 10], [22, 7, 4, 1]]),
      exits: [{ x: 0, y: 10, to: "chapel", px: 28, py: 10 }],
      props: [{ kind: "lake", x: 2, y: 10, w: 27, h: 4 }, { kind: "flowers", x: 5, y: 6 }, { kind: "rift", x: 24, y: 4 }],
      start: { x: 2, y: 10 }
    }
  };
  const characters = {
    ilyas: { name: "Ilyas", colors: ["#3d2b2f", "#e3b27f", "#334b7a", "#f2f0e6"] },
    mira: { name: "Mira", colors: ["#21191e", "#d98d70", "#b73550", "#f6d36f"] },
    ferryman: { name: "Ferryman", colors: ["#151719", "#dad9cd", "#56616d", "#0b0d10"] },
    cinder: { name: "Brother Cinder", colors: ["#1d1512", "#d59667", "#6b2e2f", "#f4c35c"] },
    sibling: { name: "Your Sibling", colors: ["#1d2531", "#bec8d8", "#52616f", "#dbeafe"] },
    vendor: { name: "Maribel", colors: ["#2a2023", "#d5a06f", "#3d7b65", "#ffd166"] },
    performer: { name: "Velvet Noon", colors: ["#2a1724", "#f2bd93", "#8f244d", "#ffe6a8"] },
    archivist: { name: "Memory Archivist", colors: ["#151f1d", "#b9ddc9", "#386b5c", "#dff7eb"] }
  };
  const npcs = {
    mira: { character: "mira", area: "harbor", x: 10, y: 8 },
    ferryman: { character: "ferryman", area: "harbor", x: 25, y: 10 },
    vendor: { character: "vendor", area: "harbor", x: 9, y: 6 },
    performer: { character: "performer", area: "red", x: 17, y: 8 },
    archivist: { character: "archivist", area: "roots", x: 9, y: 9 },
    cinder: { character: "cinder", area: "chapel", x: 15, y: 6 },
    sibling: { character: "sibling", area: "shore", x: 24, y: 8 }
  };
  const items = {
    letter: { name: "Wet Letter", area: "harbor", x: 5, y: 10 },
    bell: { name: "Brass Bell", area: "red", x: 27, y: 5 },
    photo: { name: "Blurred Photo", area: "roots", x: 13, y: 9 },
    cinderKey: { name: "Chapel Key", area: "chapel", x: 22, y: 12 }
  };
  const encounters = {
    first_erased: {
      title: "First Erased Person",
      area: "red",
      x: 6,
      y: 9,
      hp: 18,
      text: "A person-shaped absence blocks the alley. Their outline mouths a name no one else can hear.",
      bullets: "drift"
    },
    root_husk: {
      title: "Root Husk",
      area: "roots",
      x: 21,
      y: 9,
      hp: 24,
      text: "A house nobody remembers learned to walk. It wants its rooms back.",
      bullets: "rain"
    },
    chapel_memory: {
      title: "Choir of Ash",
      area: "chapel",
      x: 11,
      y: 10,
      hp: 30,
      text: "Voices rise from the drowned pews. Every note sounds like an apology.",
      bullets: "spiral"
    }
  };

  window.AshBelowLakeRpg = function init(root) {
    const canvas = root.querySelector("[data-rpg-canvas]");
    const ctx = canvas.getContext("2d");
    const els = {
      speaker: root.querySelector("[data-rpg-speaker]"),
      line: root.querySelector("[data-rpg-line]"),
      area: root.querySelector("[data-rpg-room]"),
      route: root.querySelector("[data-rpg-route]"),
      resolve: root.querySelector("[data-rpg-hearts]"),
      journal: root.querySelector("[data-rpg-quests]"),
      inventory: root.querySelector("[data-rpg-inventory]"),
      status: root.querySelector("[data-rpg-status]")
    };
    const ui = buildUi(root);
    const audio = makeAudio();
    let state = freshState();
    let keys = new Set();
    let last = performance.now();
    let saveBusy = false;
    let backendAvailable = true;

    bind();
    loadLocal();
    requestAnimationFrame(frame);

    function freshState() {
      return {
        started: false,
        mode: "title",
        area: "harbor",
        player: { x: 3, y: 8, facing: "down", step: 0 },
        route: "Undecided",
        resolve: 20,
        toldName: null,
        flags: {},
        inventory: [],
        memories: [
          { id: "letter", text: "Your sibling's letter says: Don't let the lake remember your name.", stable: true },
          { id: "town", text: "Morrow's End smiles like a town in a postcard someone tried to burn.", stable: true }
        ],
        forgets: 0,
        listens: 0,
        confronts: 0,
        companion: false,
        battle: null,
        ending: null,
        dialogue: { speaker: "The Letter", line: "Don't let the lake remember your name." }
      };
    }

    function buildUi(rootEl) {
      const panel = rootEl.querySelector(".rpg-panel");
      const actionsWrap = document.createElement("div");
      actionsWrap.className = "ash-choicebar";
      actionsWrap.innerHTML = actions.map((name) => `<button type="button" data-ash-action="${name.toLowerCase()}">${name}</button>`).join("");
      panel.insertBefore(actionsWrap, panel.querySelector(".rpg-list"));
      const meter = document.createElement("div");
      meter.className = "ash-memory-meter";
      meter.innerHTML = "<span>Lake Memory</span><i data-ash-memory-fill></i>";
      panel.insertBefore(meter, actionsWrap);
      const nameBox = document.createElement("form");
      nameBox.className = "ash-name-form";
      nameBox.innerHTML = "<label>Name told to town <input maxlength=\"18\" placeholder=\"Ilyas\" data-ash-name></label><button type=\"submit\">Tell</button>";
      panel.insertBefore(nameBox, meter);
      return {
        actionButtons: Array.from(actionsWrap.querySelectorAll("button")),
        memoryFill: meter.querySelector("[data-ash-memory-fill]"),
        nameForm: nameBox,
        nameInput: nameBox.querySelector("[data-ash-name]")
      };
    }

    function bind() {
      root.querySelector("[data-rpg-start]").addEventListener("click", start);
      root.querySelector("[data-rpg-save]").addEventListener("click", save);
      root.querySelector("[data-rpg-load]").addEventListener("click", () => load(true));
      root.querySelector("[data-rpg-reset]").addEventListener("click", newGame);
      root.querySelector("[data-rpg-sound]").addEventListener("click", () => {
        audio.start();
        audio.setTrack(state.started ? areas[state.area].track : "title");
        els.status.textContent = audio.supported() ? "Sound on." : "This browser does not expose Web Audio.";
      });
      root.querySelector("[data-rpg-act]").addEventListener("click", act);
      root.querySelectorAll("[data-rpg-move]").forEach((button) => {
        button.addEventListener("click", () => move(button.dataset.rpgMove));
      });
      ui.actionButtons.forEach((button) => button.addEventListener("click", () => chooseBattle(button.dataset.ashAction)));
      ui.nameForm.addEventListener("submit", (event) => {
        event.preventDefault();
        tellName(ui.nameInput.value.trim() || "Ilyas");
      });
      window.addEventListener("keydown", (event) => {
        if (!root.isConnected || ["INPUT", "TEXTAREA"].includes(event.target.tagName)) return;
        if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "w", "a", "s", "d", " ", "Enter", "Escape"].includes(event.key)) event.preventDefault();
        keys.add(event.key.toLowerCase());
        audio.start();
        if (event.key === " " || event.key === "Enter") act();
        if (event.key === "Escape" && state.mode === "battle") leaveBattle("You step back from the memory and let it breathe.");
      });
      window.addEventListener("keyup", (event) => keys.delete(event.key.toLowerCase()));
    }

    function start() {
      audio.start();
      state.started = true;
      state.mode = "world";
      state.dialogue = { speaker: "Ilyas", line: "The letter is damp, but the ink has not run. Morrow's End waits beyond the harbor lamps." };
      audio.setTrack(areas[state.area].track);
      save();
    }

    async function newGame() {
      state = freshState();
      localStorage.removeItem(fallbackKey);
      if (backendAvailable) {
        try {
          const response = await fetch(`/api/play/rpg/save/${slot}`, { method: "DELETE" });
          if (response.status === 404) els.status.textContent = "New game started locally. There was no backend save to clear.";
        } catch (_error) {
          backendAvailable = false;
        }
      }
      audio.setTrack("title");
      if (!els.status.textContent.includes("backend save to clear")) {
        els.status.textContent = backendAvailable ? "New game started and local save cleared." : "New game started locally. Backend save endpoint is not reachable.";
      }
      renderHud();
    }

    async function save() {
      localStorage.setItem(fallbackKey, JSON.stringify(state));
      if (saveBusy || !backendAvailable) {
        els.status.textContent = backendAvailable ? "Save already running." : "Saved locally. Backend save endpoint is not available.";
        return;
      }
      saveBusy = true;
      try {
        const response = await fetch(`/api/play/rpg/save/${slot}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ payload: state })
        });
        if (response.status === 404) {
          backendAvailable = false;
          els.status.textContent = "Saved locally. Backend save endpoint is not deployed yet.";
        } else {
          els.status.textContent = response.ok ? "Saved to backend." : "Saved locally. Backend rejected the save.";
        }
      } catch (_error) {
        backendAvailable = false;
        els.status.textContent = "Saved locally. Backend save API was not reachable.";
      } finally {
        saveBusy = false;
      }
    }

    async function load(allowBackend) {
      if (loadLocal("Loaded local save.")) return;
      if (!allowBackend || !backendAvailable) {
        els.status.textContent = backendAvailable ? "No local save found." : "No local save found. Backend save endpoint is not available.";
        return;
      }
      try {
        const response = await fetch(`/api/play/rpg/save/${slot}`);
        if (response.status === 404) {
          els.status.textContent = "No save file exists yet. Press Save after starting a game to create one.";
          return;
        }
        if (!response.ok) {
          els.status.textContent = "No local save found. Backend did not return a save.";
          return;
        }
        const data = await response.json();
        state = normalize(data.payload);
        localStorage.setItem(fallbackKey, JSON.stringify(state));
        els.status.textContent = "Loaded from backend.";
        audio.setTrack(state.started ? areas[state.area].track : "title");
      } catch (_error) {
        backendAvailable = false;
        els.status.textContent = "No local save found. Backend save API was not reachable.";
      }
    }

    function loadLocal(message) {
      const local = localStorage.getItem(fallbackKey);
      if (local) {
        state = normalize(JSON.parse(local));
        if (message) els.status.textContent = message;
        audio.setTrack(state.started ? areas[state.area].track : "title");
        return true;
      }
      audio.setTrack(state.started ? areas[state.area].track : "title");
      return false;
    }

    function normalize(candidate) {
      const base = freshState();
      const next = Object.assign(base, candidate || {});
      next.player = Object.assign(base.player, (candidate && candidate.player) || {});
      next.flags = Object.assign({}, (candidate && candidate.flags) || {});
      next.inventory = Array.isArray(next.inventory) ? next.inventory.filter((id) => items[id]) : [];
      next.memories = Array.isArray(next.memories) ? next.memories : base.memories;
      next.area = areas[next.area] ? next.area : "harbor";
      next.dialogue = Object.assign(base.dialogue, (candidate && candidate.dialogue) || {});
      return next;
    }

    function frame(now) {
      const dt = Math.min(48, now - last);
      last = now;
      update(dt);
      draw(now);
      renderHud();
      requestAnimationFrame(frame);
    }

    function update(dt) {
      if (state.mode !== "world") return;
      const dir = keyDirection();
      if (!dir) return;
      state.player.step += dt;
      if (state.player.step < 115) return;
      state.player.step = 0;
      move(dir);
    }

    function keyDirection() {
      if (keys.has("arrowup") || keys.has("w")) return "up";
      if (keys.has("arrowdown") || keys.has("s")) return "down";
      if (keys.has("arrowleft") || keys.has("a")) return "left";
      if (keys.has("arrowright") || keys.has("d")) return "right";
      return null;
    }

    function move(direction) {
      if (state.mode !== "world" || !state.started || state.ending) return;
      const delta = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[direction];
      state.player.facing = direction;
      const nx = state.player.x + delta[0];
      const ny = state.player.y + delta[1];
      const area = areas[state.area];
      const exit = area.exits.find((item) => item.x === nx && item.y === ny);
      if (exit && canEnter(exit.to)) {
        state.area = exit.to;
        state.player.x = exit.px;
        state.player.y = exit.py;
        audio.setTrack(areas[state.area].track);
        remember(`Entered ${areas[state.area].name}.`, true);
        say("Morrow's End", areaEnterLine(state.area));
        save();
        return;
      }
      if (area.walls.has(`${nx},${ny}`) || npcAt(nx, ny) || nx < 0 || nx >= cols || ny < 0 || ny >= rows) return;
      state.player.x = nx;
      state.player.y = ny;
      const found = itemAt(nx, ny);
      if (found) take(found);
      const encounter = encounterAt(nx, ny);
      if (encounter && !state.flags[`encounter_${encounter}`]) startBattle(encounter);
    }

    function canEnter(to) {
      if (to === "chapel" && !state.flags.rootsTruth && state.forgets < 2) {
        say("Ferryman", "The chapel is under the lake. It only opens for truth, or for forgetting.");
        return false;
      }
      if (to === "shore" && !state.flags.cinderMet) {
        say("Brother Cinder", "The shore is not a place. It is a decision. Hear me first.");
        return false;
      }
      return true;
    }

    function act() {
      audio.start();
      if (!state.started) return start();
      if (state.mode === "battle") return chooseBattle("listen");
      if (state.ending) return;
      const front = inFront();
      const npc = npcAt(front.x, front.y) || npcAt(state.player.x, state.player.y);
      if (npc) return talk(npc);
      const found = itemAt(state.player.x, state.player.y);
      if (found) return take(found);
      const special = propAt(front.x, front.y) || propAt(state.player.x, state.player.y);
      if (special) return inspectProp(special);
      if (state.area === "shore" && state.player.x > 22 && state.player.y > 8) return finalChoice();
      say("Ilyas", "The town holds its breath. Somewhere, water knocks against stone.");
    }

    function inFront() {
      const d = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[state.player.facing] || [0, 1];
      return { x: state.player.x + d[0], y: state.player.y + d[1] };
    }

    function npcAt(x, y) {
      return Object.keys(npcs).find((id) => {
        const npc = npcs[id];
        return npc.area === state.area && npc.x === x && npc.y === y && !forgottenNpc(id);
      });
    }

    function itemAt(x, y) {
      return Object.keys(items).find((id) => {
        const item = items[id];
        return item.area === state.area && item.x === x && item.y === y && !state.inventory.includes(id);
      });
    }

    function encounterAt(x, y) {
      return Object.keys(encounters).find((id) => {
        const e = encounters[id];
        return e.area === state.area && e.x === x && e.y === y;
      });
    }

    function propAt(x, y) {
      return areas[state.area].props.find((prop) => x >= prop.x && x < prop.x + (prop.w || 1) && y >= prop.y && y < prop.y + (prop.h || 1));
    }

    function take(id) {
      state.inventory.push(id);
      audio.sfx("item");
      if (id === "letter") remember("The wet letter refuses to dry.", true);
      if (id === "photo") remember("A photo shows you beside someone whose face keeps changing.", true);
      if (id === "cinderKey") state.flags.chapelKey = true;
      say("Found", `${items[id].name} added to your bag.`);
      save();
    }

    function talk(id) {
      audio.sfx("talk");
      if (id === "mira") {
        state.companion = true;
        state.flags.metMira = true;
        remember("Mira remembers what other people lose.", true);
        say("Mira", state.forgets > 1 ? "You keep looking through people like windows. Don't do that to me, okay?" : "You look lost. Good. Everyone interesting does.");
      }
      if (id === "ferryman") {
        remember("The Ferryman knows where you have been before you arrive.", state.forgets < 2);
        say("Ferryman", state.toldName ? `${state.toldName} is a fine name to keep away from water.` : "Names are hooks. I would not throw yours into the lake.");
      }
      if (id === "vendor") say("Maribel", "Today's special: one jar of fog, two apologies, and a fish that predicts Thursday badly.");
      if (id === "performer") {
        remember("A performer advertised a show called YOUR MISSING YEARS.", state.forgets < 3);
        say("Velvet Noon", "Clap if you remember me. Clap twice if you are pretending.");
      }
      if (id === "archivist") {
        state.flags.rootsTruth = true;
        remember("The lake feeds on suffering, but calls it mercy.", true);
        say("Memory Archivist", "The archives are homes with the people removed. The lake calls that tidying.");
      }
      if (id === "cinder") {
        state.flags.cinderMet = true;
        remember("Brother Cinder believes forgetting pain is mercy.", true);
        say("Brother Cinder", "I have seen grief make animals of gentle people. The lake did not begin as a hunger. It began as a kindness.");
      }
      if (id === "sibling") {
        remember("Your sibling entered the lake willingly.", true);
        say("Your Sibling", "I gave it my name because I was tired of carrying it. Please don't give it yours.");
        finalChoice();
      }
      save();
    }

    function inspectProp(prop) {
      audio.sfx("talk");
      if (prop.kind === "lake") say("Black Lake", state.toldName ? `It almost says ${state.toldName}. Almost.` : "It reflects nothing. Not clouds. Not you.");
      if (prop.kind === "stall") say("Market Sign", "Fresh eels. Older than regret. Discount if they remember your birthday.");
      if (prop.kind === "flowers") say("Glowing Flowers", "They lean toward the water like they are listening.");
      if (prop.kind === "sign") say("Advertisement", "TONIGHT: The Amazing Vanishing Audience. Refunds unavailable after erasure.");
      if (prop.kind === "stage") say("Stage", "The curtain twitches though there is no wind indoors.");
      if (prop.kind === "poster") say("Poster", "A smiling face has been scratched out. The scratch is smiling too.");
      if (prop.kind === "archive") {
        state.flags.rootsTruth = true;
        remember("Some memory shelves are empty because nobody dared open them.", true);
        say("Memory Archive", "A drawer opens by itself. Inside: houses, birthdays, funeral songs, a ticket stub from a train that never arrived.");
      }
      if (prop.kind === "root") say("Roots", "They pulse slowly under the town, carrying lake-water like blood.");
      if (prop.kind === "home") say("Abandoned Home", "Every room is furnished except for the idea of a family.");
      if (prop.kind === "pew") say("Drowned Pew", "The wood is wet. The prayer book is dry.");
      if (prop.kind === "glass") say("Sunken Window", "Saints in blue glass lower their eyes toward the lake.");
      if (prop.kind === "rift") finalChoice();
      save();
    }

    function tellName(name) {
      state.toldName = name.slice(0, 18);
      remember(`You told Morrow's End your name is ${state.toldName}.`, false);
      say("Morrow's End", `The town repeats ${state.toldName} very softly. Too softly.`);
      state.route = state.route === "Undecided" ? "Named" : state.route;
      save();
    }

    function startBattle(id) {
      const e = encounters[id];
      state.mode = "battle";
      state.battle = {
        id,
        hp: e.hp,
        maxHp: e.hp,
        soul: { x: 480, y: 382 },
        bullets: [],
        t: 0,
        grace: 0,
        turn: 1,
        message: e.text
      };
      audio.setTrack("battle");
      audio.sfx("battle");
      say(e.title, e.text);
      save();
    }

    function chooseBattle(action) {
      if (state.mode !== "battle" || !state.battle) return;
      const battle = state.battle;
      if (action === "confront") {
        battle.hp -= 9;
        state.confronts += 1;
        state.route = "Confronting";
        battle.message = "You refuse to move. The memory recoils from your shape.";
        audio.sfx("hit");
      }
      if (action === "listen") {
        battle.hp -= 7;
        state.listens += 1;
        state.route = state.forgets === 0 ? "Carry" : "Listening";
        battle.message = "You listen until the ache becomes words. The attack softens.";
        remember(`Listened to ${encounters[battle.id].title}.`, true);
        audio.sfx("listen");
      }
      if (action === "forget") {
        battle.hp -= 18;
        state.forgets += 1;
        state.route = state.forgets >= 3 ? "Forgotten" : "Silence";
        battle.message = "You let the lake take the hard part. Something else goes with it.";
        rewriteMemory();
        audio.sfx("forget");
      }
      battle.turn += 1;
      if (battle.hp <= 0) resolveBattle(action);
      else say(encounters[battle.id].title, battle.message);
      save();
    }

    function resolveBattle(action) {
      const id = state.battle.id;
      state.flags[`encounter_${id}`] = action;
      if (id === "root_husk") state.flags.rootsTruth = true;
      if (action === "forget" && state.forgets >= 3) return end("forgotten");
      const line = action === "listen"
        ? "The memory steps aside, still hurting, but no longer alone."
        : action === "forget"
          ? "The memory becomes easy to pass. Behind you, a street sign loses a letter."
          : "The memory breaks apart like ash under rain.";
      leaveBattle(line);
    }

    function leaveBattle(line) {
      state.mode = "world";
      state.battle = null;
      audio.setTrack(areas[state.area].track);
      say("Afterimage", line);
      save();
    }

    function finalChoice() {
      if (state.forgets >= 3) return end("forgotten");
      if (state.forgets === 0 && state.listens >= 3 && state.flags.metMira) return end("rest");
      if (state.route === "Silence" || state.forgets > state.listens) return end("silence");
      end("carry");
    }

    function end(kind) {
      state.ending = kind;
      state.mode = "ending";
      state.route = kind === "rest" ? "True: Rest" : kind === "carry" ? "Carry" : kind === "silence" ? "Silence" : "Forgotten";
      audio.setTrack(kind === "forgotten" ? "void" : "ending");
      say("Ending", endingLine(kind));
      remember(endingLine(kind), kind !== "forgotten");
      save();
    }

    function endingLine(kind) {
      if (kind === "silence") return "Silence: you join the lake. Morrow's End becomes peaceful, clean, and almost empty.";
      if (kind === "carry") return "Carry: the lake breaks. Everyone remembers. The town survives with tears in every lit window.";
      if (kind === "forgotten") return "Forgotten: the story continues politely around the space where Ilyas used to be.";
      return "Rest: the lake is forgiven, not destroyed. Mira laughs once, real and afraid, as the shore finally reflects the sky.";
    }

    function rewriteMemory() {
      const mutable = state.memories.find((memory) => !memory.stable && !memory.erased) || state.memories.find((memory) => !memory.erased);
      if (mutable) {
        mutable.text = state.forgets > 2 ? "There was someone here. There was no one here. Both feel true." : mutable.text.replace(/[aeiou]/gi, "_");
        mutable.erased = state.forgets > 2;
      }
      if (state.forgets === 2) state.flags.vendorForgotten = true;
      if (state.forgets >= 3) state.flags.performerForgotten = true;
    }

    function forgottenNpc(id) {
      return (id === "vendor" && state.flags.vendorForgotten) || (id === "performer" && state.flags.performerForgotten);
    }

    function remember(text, stable) {
      if (!state.memories.some((memory) => memory.text === text)) state.memories.push({ id: `m${Date.now()}`, text, stable: Boolean(stable) });
      if (state.memories.length > 8) state.memories.splice(2, 1);
    }

    function say(speaker, line) {
      state.dialogue = { speaker, line };
    }

    function areaEnterLine(area) {
      if (area === "red") return "The Red Streets glow warmly, but the laughter arrives half a second late.";
      if (area === "roots") return "The Roots smell like wet stone and bedrooms left untouched for years.";
      if (area === "chapel") return "The Drowned Chapel rings with a bell under water.";
      if (area === "shore") return "The Hollow Shore bends at the edges, as if the world has been folded too many times.";
      return "Harbor Row keeps its lamps low and its names lower.";
    }

    function draw(now) {
      ctx.imageSmoothingEnabled = false;
      if (!state.started || state.mode === "title") drawTitle(now);
      else drawWorld(now);
      if (state.mode === "battle") drawBattle(now);
      if (state.mode === "ending") drawEnding(now);
      drawVignette(now);
    }

    function drawTitle(now) {
      const g = ctx.createLinearGradient(0, 0, 0, canvas.height);
      g.addColorStop(0, "#05070c");
      g.addColorStop(0.55, "#101b28");
      g.addColorStop(1, "#020306");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      drawLake(0, 345, 960, 195, now, 0.4);
      ctx.fillStyle = "#f8efe0";
      ctx.font = "700 50px Georgia, serif";
      ctx.textAlign = "center";
      ctx.fillText("Ash Below the Lake", 480, 142);
      ctx.font = "18px Georgia, serif";
      ctx.fillStyle = "#b9c6d4";
      ctx.fillText("A quiet town. A black lake. A name you should not give away.", 480, 178);
      ctx.fillStyle = "#e0a451";
      ctx.font = "700 18px ui-monospace, monospace";
      ctx.fillText("Press Start or Space", 480, 246);
      drawSprite("ilyas", 424, 288, 3, now);
      drawSprite("mira", 500, 288, 3, now);
    }

    function drawWorld(now) {
      const area = areas[state.area];
      for (let y = 0; y < rows; y += 1) {
        for (let x = 0; x < cols; x += 1) {
          const wall = area.walls.has(`${x},${y}`);
          const c = area.floor[(x + y) % area.floor.length];
          ctx.fillStyle = wall ? "#0b0d10" : c;
          ctx.fillRect(x * tile, y * tile, tile, tile);
          if (!wall) {
            ctx.fillStyle = (x + y) % 2 === 0 ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.035)";
            ctx.fillRect(x * tile, y * tile + 30, tile, 2);
          }
        }
      }
      area.exits.forEach((exit) => drawDoor(exit.x, exit.y, area.accent));
      area.props.forEach((prop) => drawProp(prop, now));
      Object.keys(items).forEach((id) => {
        const item = items[id];
        if (item.area === state.area && !state.inventory.includes(id)) drawItem(id, item, now);
      });
      Object.keys(encounters).forEach((id) => {
        const e = encounters[id];
        if (e.area === state.area && !state.flags[`encounter_${id}`]) drawEncounter(e, now);
      });
      Object.keys(npcs).forEach((id) => {
        const npc = npcs[id];
        if (npc.area === state.area && !forgottenNpc(id)) drawSprite(npc.character, npc.x * tile, npc.y * tile - 12, 2, now);
      });
      drawSprite("ilyas", state.player.x * tile, state.player.y * tile - 12, 2, now);
      if (state.companion && state.area !== "chapel") {
        const offset = state.player.facing === "left" ? 1 : -1;
        drawSprite("mira", (state.player.x + offset) * tile, state.player.y * tile - 12, 2, now + 200);
      }
      drawTopBar(area);
    }

    function drawTopBar(area) {
      ctx.fillStyle = "rgba(3, 6, 10, 0.82)";
      ctx.fillRect(0, 0, canvas.width, 42);
      ctx.fillStyle = area.accent;
      ctx.font = "700 14px ui-monospace, monospace";
      ctx.textAlign = "left";
      ctx.fillText(`${area.act} / ${area.name}`, 18, 26);
      ctx.textAlign = "right";
      ctx.fillStyle = "#f8efe0";
      ctx.fillText(`Resolve ${state.resolve}  Lake ${state.forgets}`, 940, 26);
    }

    function drawBattle(now) {
      const battle = state.battle;
      battle.t += 1;
      updateBullets(battle, now);
      ctx.fillStyle = "rgba(4, 5, 8, 0.88)";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.strokeStyle = "#f8efe0";
      ctx.lineWidth = 3;
      ctx.strokeRect(296, 252, 368, 190);
      ctx.fillStyle = "#f8efe0";
      ctx.font = "700 24px Georgia, serif";
      ctx.textAlign = "center";
      ctx.fillText(encounters[battle.id].title, 480, 82);
      ctx.font = "16px ui-monospace, monospace";
      ctx.fillText(`Memory ${Math.max(0, battle.hp)} / ${battle.maxHp}`, 480, 116);
      drawBattleEnemy(battle.id, now);
      ctx.fillStyle = "#f35f5f";
      ctx.beginPath();
      ctx.moveTo(battle.soul.x, battle.soul.y - 8);
      ctx.bezierCurveTo(battle.soul.x - 13, battle.soul.y - 17, battle.soul.x - 20, battle.soul.y + 4, battle.soul.x, battle.soul.y + 18);
      ctx.bezierCurveTo(battle.soul.x + 20, battle.soul.y + 4, battle.soul.x + 13, battle.soul.y - 17, battle.soul.x, battle.soul.y - 8);
      ctx.fill();
      ctx.fillStyle = "#dbeafe";
      battle.bullets.forEach((b) => {
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
        ctx.fill();
      });
      ctx.fillStyle = "#f8efe0";
      ctx.font = "15px Georgia, serif";
      wrapText(battle.message, 148, 480, 660, 22);
    }

    function updateBullets(battle) {
      const speed = state.forgets > 0 ? 1.15 : 1.55;
      const move = keyDirection();
      if (move === "up") battle.soul.y -= 3.3;
      if (move === "down") battle.soul.y += 3.3;
      if (move === "left") battle.soul.x -= 3.3;
      if (move === "right") battle.soul.x += 3.3;
      battle.soul.x = clamp(battle.soul.x, 312, 648);
      battle.soul.y = clamp(battle.soul.y, 270, 426);
      if (battle.t % 26 === 0) {
        if (encounters[battle.id].bullets === "rain") battle.bullets.push({ x: 315 + Math.random() * 330, y: 256, vx: 0, vy: speed * 2.4, r: 5 });
        else if (encounters[battle.id].bullets === "spiral") {
          const a = battle.t / 14;
          battle.bullets.push({ x: 480, y: 346, vx: Math.cos(a) * speed * 2, vy: Math.sin(a) * speed * 2, r: 5 });
        } else battle.bullets.push({ x: Math.random() > 0.5 ? 304 : 656, y: 275 + Math.random() * 130, vx: (Math.random() > 0.5 ? -1 : 1) * speed * 2, vy: Math.sin(battle.t) * 0.6, r: 5 });
      }
      battle.bullets.forEach((b) => {
        b.x += b.vx;
        b.y += b.vy;
        if (battle.grace <= 0 && Math.hypot(b.x - battle.soul.x, b.y - battle.soul.y) < b.r + 10) {
          state.resolve = Math.max(0, state.resolve - 1);
          battle.grace = 24;
          audio.sfx("hurt");
          if (state.resolve <= 0) end("forgotten");
        }
      });
      battle.grace -= 1;
      battle.bullets = battle.bullets.filter((b) => b.x > 280 && b.x < 680 && b.y > 230 && b.y < 455);
    }

    function renderHud() {
      root.classList.toggle("is-lake-awake", state.forgets > 1);
      root.classList.toggle("is-ending", Boolean(state.ending));
      els.speaker.textContent = state.dialogue.speaker;
      els.line.textContent = state.dialogue.line;
      els.area.textContent = areas[state.area].name;
      els.route.textContent = state.route;
      els.resolve.textContent = String(state.resolve);
      ui.memoryFill.style.width = `${Math.min(100, state.forgets * 34)}%`;
      els.inventory.innerHTML = state.inventory.length ? state.inventory.map((id) => `<li>${items[id].name}</li>`).join("") : "<li>Empty</li>";
      els.journal.innerHTML = state.memories.map((memory) => `<li class="${memory.erased ? "is-erased" : memory.stable ? "is-done" : ""}">${memory.erased ? "[entry missing]" : memory.text}</li>`).join("");
      ui.actionButtons.forEach((button) => {
        button.disabled = state.mode !== "battle";
      });
    }

    function drawProp(prop, now) {
      const x = prop.x * tile;
      const y = prop.y * tile;
      if (prop.kind === "lake") return drawLake(x, y, (prop.w || 1) * tile, (prop.h || 1) * tile, now, 1);
      if (prop.kind === "stall") {
        ctx.fillStyle = "#7f2d3a"; ctx.fillRect(x, y + 8, 64, 34);
        ctx.fillStyle = "#e0a451"; ctx.fillRect(x - 4, y, 72, 12);
      }
      if (prop.kind === "flowers") for (let i = 0; i < 6; i += 1) { ctx.fillStyle = `rgba(133, 255, 214, ${0.45 + Math.sin(now / 400 + i) * 0.2})`; ctx.fillRect(x + i * 8, y + 12 + (i % 2) * 8, 5, 5); }
      if (prop.kind === "sign" || prop.kind === "poster") { ctx.fillStyle = "#12151d"; ctx.fillRect(x, y, 38, 32); ctx.fillStyle = "#f3b15d"; ctx.fillRect(x + 5, y + 6, 28, 3); }
      if (prop.kind === "stage") { ctx.fillStyle = "#210d18"; ctx.fillRect(x, y + 10, 96, 46); ctx.fillStyle = "#d64d62"; ctx.fillRect(x + 8, y, 80, 14); }
      if (prop.kind === "archive") { ctx.fillStyle = "#4b3a25"; ctx.fillRect(x, y, 58, 52); ctx.fillStyle = "#d8cba8"; for (let i = 0; i < 4; i += 1) ctx.fillRect(x + 8, y + 8 + i * 10, 42, 3); }
      if (prop.kind === "root") { ctx.strokeStyle = "#6aa27d"; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(x, y + 28); ctx.bezierCurveTo(x + 26, y - 8, x + 52, y + 48, x + 80, y + 6); ctx.stroke(); }
      if (prop.kind === "home") { ctx.fillStyle = "#263332"; ctx.fillRect(x, y, 54, 48); ctx.fillStyle = "#080a0c"; ctx.fillRect(x + 20, y + 18, 14, 30); }
      if (prop.kind === "pew") { ctx.fillStyle = "#33251d"; ctx.fillRect(x, y + 10, 76, 20); }
      if (prop.kind === "glass") { ctx.fillStyle = "rgba(139,211,255,0.55)"; ctx.fillRect(x, y, 52, 72); ctx.fillStyle = "rgba(255,255,255,0.3)"; ctx.fillRect(x + 23, y, 5, 72); }
      if (prop.kind === "rift") { ctx.strokeStyle = `rgba(248,239,224,${0.5 + Math.sin(now / 200) * 0.3})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x + 20, y); ctx.lineTo(x + 5, y + 22); ctx.lineTo(x + 27, y + 40); ctx.lineTo(x + 10, y + 70); ctx.stroke(); }
    }

    function drawLake(x, y, w, h, now, alpha) {
      const g = ctx.createLinearGradient(0, y, 0, y + h);
      g.addColorStop(0, `rgba(7, 18, 30, ${0.7 * alpha})`);
      g.addColorStop(1, `rgba(0, 0, 0, ${0.95 * alpha})`);
      ctx.fillStyle = g;
      ctx.fillRect(x, y, w, h);
      ctx.strokeStyle = `rgba(114, 169, 191, ${0.2 * alpha})`;
      for (let i = 0; i < 8; i += 1) {
        ctx.beginPath();
        const yy = y + 12 + i * 17 + Math.sin(now / 700 + i) * 4;
        ctx.moveTo(x, yy);
        for (let xx = 0; xx < w; xx += 24) ctx.lineTo(x + xx, yy + Math.sin(now / 350 + xx / 35 + i) * 3);
        ctx.stroke();
      }
    }

    function drawDoor(x, y, color) {
      ctx.fillStyle = color;
      ctx.fillRect(x * tile + 7, y * tile + 4, 18, 26);
      ctx.fillStyle = "#05070c";
      ctx.fillRect(x * tile + 19, y * tile + 17, 3, 3);
    }

    function drawItem(id, item, now) {
      const x = item.x * tile;
      const y = item.y * tile + Math.sin(now / 220) * 2;
      ctx.fillStyle = id === "bell" ? "#d6a24a" : id === "photo" ? "#dbeafe" : id === "cinderKey" ? "#f59e0b" : "#f8efe0";
      ctx.fillRect(x + 10, y + 10, 13, 13);
      ctx.fillStyle = "rgba(255,255,255,0.45)";
      ctx.fillRect(x + 13, y + 7, 7, 5);
    }

    function drawEncounter(e, now) {
      const x = e.x * tile;
      const y = e.y * tile;
      ctx.fillStyle = `rgba(210, 225, 245, ${0.35 + Math.sin(now / 260) * 0.16})`;
      ctx.fillRect(x + 8, y + 4, 16, 27);
      ctx.clearRect(x + 12, y + 10, 3, 4);
      ctx.clearRect(x + 19, y + 10, 3, 4);
    }

    function drawSprite(id, x, y, scale, now) {
      const c = characters[id].colors;
      const bob = Math.sin(now / 260 + x) * 1.5;
      const s = scale;
      ctx.fillStyle = "rgba(0,0,0,0.28)";
      ctx.fillRect(x + 5 * s, y + 30 * s, 12 * s, 3 * s);
      ctx.fillStyle = c[0]; ctx.fillRect(x + 7 * s, y + bob + 2 * s, 10 * s, 7 * s);
      ctx.fillStyle = c[1]; ctx.fillRect(x + 6 * s, y + bob + 8 * s, 12 * s, 10 * s);
      ctx.fillStyle = c[2]; ctx.fillRect(x + 5 * s, y + bob + 18 * s, 14 * s, 13 * s);
      ctx.fillStyle = c[3]; ctx.fillRect(x + 8 * s, y + bob + 12 * s, 2 * s, 2 * s); ctx.fillRect(x + 14 * s, y + bob + 12 * s, 2 * s, 2 * s);
      ctx.fillStyle = c[0]; ctx.fillRect(x + 4 * s, y + bob + 22 * s, 3 * s, 9 * s); ctx.fillRect(x + 17 * s, y + bob + 22 * s, 3 * s, 9 * s);
    }

    function drawBattleEnemy(id, now) {
      if (id === "first_erased") drawEncounter({ x: 14, y: 4 }, now);
      if (id === "root_husk") { ctx.strokeStyle = "#79c89d"; ctx.lineWidth = 8; ctx.beginPath(); ctx.moveTo(440, 180); ctx.bezierCurveTo(470, 120, 510, 230, 540, 160); ctx.stroke(); }
      if (id === "chapel_memory") { ctx.fillStyle = "rgba(139,211,255,0.5)"; for (let i = 0; i < 5; i += 1) ctx.fillRect(420 + i * 28, 150 + Math.sin(now / 300 + i) * 8, 16, 54); }
    }

    function drawEnding(now) {
      ctx.fillStyle = "rgba(0,0,0,0.72)";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = "#f8efe0";
      ctx.font = "700 42px Georgia, serif";
      ctx.textAlign = "center";
      ctx.fillText(state.route, 480 + Math.sin(now / 120) * (state.ending === "forgotten" ? 3 : 0), 202);
      ctx.font = "18px Georgia, serif";
      wrapText(state.dialogue.line, 250, 250, 460, 28);
    }

    function drawVignette(now) {
      if (state.forgets > 0) {
        ctx.fillStyle = `rgba(0,0,0,${0.06 + state.forgets * 0.05 + Math.sin(now / 800) * 0.02})`;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
      }
      const g = ctx.createRadialGradient(480, 245, 120, 480, 245, 560);
      g.addColorStop(0, "rgba(0,0,0,0)");
      g.addColorStop(1, "rgba(0,0,0,0.48)");
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    function wrapText(text, x, y, maxWidth, lineHeight) {
      ctx.textAlign = "left";
      const words = text.split(" ");
      let line = "";
      for (const word of words) {
        const test = `${line}${word} `;
        if (ctx.measureText(test).width > maxWidth && line) {
          ctx.fillText(line, x, y);
          line = `${word} `;
          y += lineHeight;
        } else {
          line = test;
        }
      }
      ctx.fillText(line, x, y);
    }
  };

  function makeAudio() {
    let ctx = null;
    let master = null;
    let track = "title";
    let timer = null;
    const patterns = {
      title: [220, 277, 330, 277],
      harbor: [262, 330, 392, 330],
      red: [196, 233, 294, 349],
      roots: [174, 220, 261, 220],
      chapel: [147, 196, 247, 294],
      shore: [110, 165, 220, 165],
      battle: [330, 311, 294, 247],
      ending: [262, 392, 440, 523],
      void: [110, 98, 82, 73]
    };
    function start() {
      const Audio = window.AudioContext || window.webkitAudioContext;
      if (!Audio) return;
      if (ctx) {
        if (ctx.state === "suspended") ctx.resume();
        return;
      }
      ctx = new Audio();
      master = ctx.createGain();
      master.gain.value = 0.14;
      master.connect(ctx.destination);
      if (ctx.state === "suspended") ctx.resume();
      loop();
      const notes = patterns[track] || patterns.title;
      tone(notes[0], 0.22, "triangle", 0.9);
    }
    function loop() {
      clearInterval(timer);
      let i = 0;
      timer = setInterval(() => {
        if (!ctx) return;
        const notes = patterns[track] || patterns.title;
        tone(notes[i % notes.length], 0.28, track === "battle" ? "sawtooth" : "sine");
        if (track !== "battle") tone(notes[(i + 2) % notes.length] / 2, 0.7, "triangle", 0.45);
        i += 1;
      }, track === "battle" ? 230 : 520);
    }
    function tone(freq, dur, type, gainScale) {
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = type;
      osc.frequency.value = freq;
      gain.gain.setValueAtTime(0, ctx.currentTime);
      gain.gain.linearRampToValueAtTime(0.25 * (gainScale || 1), ctx.currentTime + 0.02);
      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
      osc.connect(gain);
      gain.connect(master);
      osc.start();
      osc.stop(ctx.currentTime + dur + 0.03);
    }
    return {
      start,
      supported() {
        return Boolean(window.AudioContext || window.webkitAudioContext);
      },
      setTrack(next) {
        track = next;
        if (ctx && ctx.state === "suspended") ctx.resume();
        if (ctx) loop();
        if (ctx) tone((patterns[track] || patterns.title)[0], 0.16, track === "battle" ? "sawtooth" : "triangle", 0.65);
      },
      sfx(kind) {
        if (!ctx) return;
        if (ctx.state === "suspended") ctx.resume();
        const map = { item: 660, talk: 440, battle: 130, hit: 180, listen: 523, forget: 96, hurt: 80 };
        tone(map[kind] || 330, kind === "forget" ? 0.8 : 0.18, kind === "forget" ? "sawtooth" : "square", kind === "hurt" ? 1.3 : 0.8);
      }
    };
  }

  function rects(sources) {
    const set = new Set();
    sources.forEach(([x, y, w, h]) => {
      for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set.add(`${xx},${yy}`);
    });
    return set;
  }

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