// Waldlichtung: Welt, NPCs, Gespraeche, Sammeln, Angeln, Werkbuch, Tauschen, Tageszeit (Teil von index.html).
// Kein Geld und niemand mit Amt: alle auf der Lichtung sind gleichgestellt.
// Karte aus Assets/10_testmap/testmap.js, NPCs aus Assets/Menschen/1_figuren/npcs, Tilesets aus Assets/quellen/3_mana_seed.
// Pfade ueber WURZEL relativ zum Projektordner. Spielstand (stand) und Ablauf: spiel.js.

const WURZEL = "";
// Aktuelle Karte: eine der zehn Basiskarten (Assets/10_testmap/karten.js), gesetzt von weltLaden()
let KARTE = null, aktWelt = null;
const TK = 16;
// Ein Ding misst auf der Karte TK Weltpixel, sein Bild im Blatt aber DING_TK: die
// Gegenstandsblaetter haben doppelte Dichte. Grund ist das Taschenfach -- es ist bis zu
// 490 Bildschirmpixel breit, und eine 16er-Kachel wurde darin 31-fach aufgeblasen, bis
// nur noch Kloetze zu sehen waren. Wolfgangs Quellen geben die Feinheit her.
const DING_TK = 32;
const karteDaten = nr => { const { karten, ...gemeinsam } = window.KARTEN; return { ...gemeinsam, ...karten[(nr || 1) - 1] }; };
const GEHTASTEN = { ArrowLeft: [-1, 0, "W"], ArrowRight: [1, 0, "O"], ArrowUp: [0, -1, "N"], ArrowDown: [0, 1, "S"],
  a: [-1, 0, "W"], d: [1, 0, "O"], w: [0, -1, "N"], s: [0, 1, "S"] };
// Gehen und Rennen liegen auf getrennten Spalten: 0-3 sind die Gehbilder, 4-7 die
// Rennbilder. Vorher teilten sie sich Spalten (Mana Seed hatte acht gezeichnete
// Laufbilder, aus denen beide Posen schoepften) - mit dem neuen Figurenblatt griff das
// Gehen dadurch auf Rennbilder zu.
const GEHEN = { spalten: [0, 1, 2, 3], zeiten: [135, 135, 135, 135] };
// Der Schritthub. Gemessen am 10.10.: In der Sued- und Nordansicht ist die Fussbreite in
// allen vier Gehbildern gleich (5 Pixel) -- die Beine bewegen sich dort praktisch nicht,
// die Figur glitt ueber den Boden. Eine Figur, die geht, hebt und senkt sich aber im
// Takt. Das Bild wird dafuer um einen Pixel versetzt: in der Schwungphase (Bild 2 und 4)
// hoch, beim Aufsetzen (1 und 3) wieder herunter. Die Blaetter bleiben unberuehrt.
const gehHub = bild => (bild === 1 || bild === 3) ? -1 : 0;
const RENNEN = { spalten: [4, 5, 6, 7], zeiten: [80, 55, 80, 55] };
const BLICK = { S: 0, N: 1, O: 2, W: 3 };
const FUSS = { x: 32, y: 44 };                  // Fusspunkt im 64er-Frame

// ---------------------------------------------------------------------------
// Sitzen: eigene Beinpartie, eingefaerbt nach der Hose der jeweiligen Figur
// ---------------------------------------------------------------------------
// Das Mana-Seed-Blatt kennt keine Sitzpose (nur stehen, gehen, rennen, springen, schieben,
// ziehen). Statt 140 Kleidungsblaetter nachzuziehen, ist nur die Beinpartie neu gezeichnet -
// als Maske in sechs Stufen (Assets/14_nachbau_mana/posen/). Hier bekommt sie die Farben der Figur.
const SITZ_HUEFTE = 37;                         // ab hier faengt das gezeichnete Bein an
// Das Blatt hat sechs Spalten (eine Schablone je Farbstufe) und vier Zeilen (S, N, O, W).
// Eingefaerbt wird per Kompositing, nicht durch Pixelschreiben: getImageData scheitert,
// sobald die Bilder von einem anderen Ursprung kommen, und faellt dann still aus.
const SITZ_STUFEN = ["umriss", "dunkel", "mittel", "hell", "schuhD", "schuhH"];
// Wenn sich die Farben der Figur nicht auslesen lassen, sieht sie wenigstens ordentlich aus
const SITZ_ERSATZ = { umriss: [24, 24, 24], dunkel: [40, 62, 96], mittel: [63, 108, 155],
                      hell: [101, 172, 197], schuhD: [64, 44, 30], schuhH: [110, 80, 52] };
const sitzBlaetter = {};                        // je Figur einmal erzeugt und behalten
// Hosen- und Schuhfarben aus der Suedansicht der Figur ablesen
function figurFarben(quelle, quellY = 0) {
  try {
    const c = document.createElement("canvas");
    c.width = 64; c.height = 64;
    const g = c.getContext("2d");
    g.drawImage(quelle, 0, quellY, 64, 64, 0, 0, 64, 64);
    const d = g.getImageData(0, 0, 64, 64).data;
    const sammeln = (y0, y1) => {
      const zaehler = new Map();
      for (let y = y0; y < y1; y++) for (let x = 20; x < 44; x++) {
        const i = (y * 64 + x) * 4;
        if (d[i + 3] < 200) continue;
        const k = `${d[i]},${d[i + 1]},${d[i + 2]}`;
        zaehler.set(k, (zaehler.get(k) || 0) + 1);
      }
      return [...zaehler.keys()].map(k => k.split(",").map(Number))
        .filter(([r, gg, b]) => r + gg + b > 90)                 // der Umriss faellt weg
        .sort((a, b) => (a[0] + a[1] + a[2]) - (b[0] + b[1] + b[2]));
    };
    const hose = sammeln(35, 41), schuh = sammeln(41, 44);
    const w = (liste, i, ersatz) => (liste.length ? liste[Math.min(i, liste.length - 1)] : ersatz);
    return {
      umriss: SITZ_ERSATZ.umriss,
      dunkel: w(hose, 0, SITZ_ERSATZ.dunkel),
      mittel: w(hose, Math.floor(hose.length / 2), SITZ_ERSATZ.mittel),
      hell:   w(hose, hose.length - 1, SITZ_ERSATZ.hell),
      schuhD: w(schuh, 0, SITZ_ERSATZ.schuhD),
      schuhH: w(schuh, schuh.length - 1, SITZ_ERSATZ.schuhH),
    };
  } catch (e) {                                  // Canvas nicht lesbar (fremder Ursprung)
    return { ...SITZ_ERSATZ };
  }
}
// Die Schablonen einmal je Figur in deren Farben giessen - Ergebnis: 64 x 256, vier Richtungen
function sitzBlatt(id, farbQuelle, farbY = 0) {
  if (sitzBlaetter[id]) return sitzBlaetter[id];
  const maske = welt && welt.sitzbeine;
  if (!maske) return null;
  const farben = figurFarben(farbQuelle, farbY);
  const c = document.createElement("canvas");
  c.width = 64; c.height = 256;
  const g = c.getContext("2d");
  const t = document.createElement("canvas");
  t.width = t.height = 64;
  const tg = t.getContext("2d");
  for (let r = 0; r < 4; r++) {
    for (let si = 0; si < SITZ_STUFEN.length; si++) {
      const f = farben[SITZ_STUFEN[si]] || SITZ_ERSATZ[SITZ_STUFEN[si]];
      tg.globalCompositeOperation = "source-over";
      tg.clearRect(0, 0, 64, 64);
      tg.fillStyle = `rgb(${f[0]}, ${f[1]}, ${f[2]})`;
      tg.fillRect(0, 0, 64, 64);
      tg.globalCompositeOperation = "destination-in";      // nur wo die Schablone deckt
      tg.drawImage(maske, si * 64, r * 64, 64, 64, 0, 0, 64, 64);
      g.drawImage(t, 0, r * 64);
    }
  }
  return (sitzBlaetter[id] = c);
}
// Eine sitzende Figur malen. `rumpf` ist der fertige 64er-Frame der Blickrichtung (beim
// Spieler aus figurFrame, bei Bewohnern der Ausschnitt ihres Blattes); `farbQuelle`/`farbY`
// zeigen auf die Suedansicht derselben Figur, damit die Beine ihre Hosenfarbe bekommen.
function sitzendMalen(k, id, rumpf, blick, x, y, farbQuelle = rumpf, farbY = 0, rumpfY = 0) {
  if (!rumpf) return;
  const r = BLICK[blick] !== undefined ? BLICK[blick] : BLICK.S;
  const x0 = Math.round(x - FUSS.x), y0 = Math.round(y - FUSS.y);
  k.drawImage(rumpf, 0, rumpfY, 64, SITZ_HUEFTE, x0, y0, 64, SITZ_HUEFTE);   // Kopf und Rumpf
  const beine = sitzBlatt(id, farbQuelle, farbY);
  if (beine) k.drawImage(beine, 0, r * 64 + SITZ_HUEFTE, 64, 64 - SITZ_HUEFTE,
                         x0, y0 + SITZ_HUEFTE, 64, 64 - SITZ_HUEFTE);
}

const NPC_INFO = Object.fromEntries(NPCS.map(n => [n.id, n]));
const PERSONEN = {};
const npcKlasse = {};                                     // welche Klasse das gemerkte Blatt zeigt                                      // Bewohner erzeugter Welten (universum.js)
const person = id => NPC_INFO[id] || PERSONEN[id] || { name: "?" };
const weltCanvas = document.getElementById("welt");
const wctx = weltCanvas.getContext("2d");
const tasten = new Set();
const npcBlaetter = {};
// Ein Kopf neben dem Namen macht aus der Bewohnerliste ein Dorf (Vorschlag V8). Jeder hat
// laengst ein Sprite - es fehlte nur der Ausschnitt. Nachgemessen am Blatt: im 64er-Frame
// (Zeile 0 = Blick nach vorn, Spalte 0 = stehen) sitzt der Kopf bei 24,12 auf 16x16.
const KOPF = { x: 24, y: 12, gr: 16 };
// Ein Canvas, kein <img>: toDataURL() wirft, sobald ein Blatt von file:// stammt (das
// Canvas gilt dann als fremd). Im Artifact ginge es, beim oertlichen Pruefen nicht - und
// eine Anzeige, die sich nicht pruefen laesst, ist keine.
let kopfGrund = {};                                        // warum ein Kopf fehlt - fuer die Pruefung
function kopfBild(id) {
  // Der Spieler hat kein eigenes Blatt in npcBlaetter - seine Figur wird live aus den
  // Schichten gebaut, genauso wie sie in der Welt gezeichnet wird.
  let quelle = npcBlaetter[id];
  if (!quelle && id === "spieler") {
    if (typeof figurFrame !== "function") kopfGrund[id] = "figurFrame fehlt";
    else try { quelle = figurFrame("p1", 0, 0); if (!quelle) kopfGrund[id] = "figurFrame gab nichts"; }
         catch (e) { quelle = null; kopfGrund[id] = "figurFrame wirft: " + e.message; }
  }
  if (!quelle) { kopfGrund[id] = kopfGrund[id] || "kein Blatt"; return null; }
  delete kopfGrund[id];
  const c = document.createElement("canvas");
  c.width = c.height = KOPF.gr;
  c.className = "dkopf";
  c.getContext("2d").drawImage(quelle, KOPF.x, KOPF.y, KOPF.gr, KOPF.gr, 0, 0, KOPF.gr, KOPF.gr);
  return c;
}
let welt = null, weltLaedt = null, zuletzt = 0;

const mitte = (x, y) => ({ x: x * TK + TK / 2, y: (y + 1) * TK - 2 });
const spieler = { x: 0, y: 0, blick: "O", bewegt: false, rennt: false, bild: 0, seit: 0, sprung: 0 };
// Sprung (Leertaste): Spalten 6-8 der Bewegungsseite, dazu ein kleiner Hopser nach oben
const SPRUNG = [{ spalte: 5, dauer: 110, hoehe: 0 }, { spalte: 6, dauer: 140, hoehe: 5 }, { spalte: 7, dauer: 130, hoehe: 9 },
                { spalte: 6, dauer: 110, hoehe: 4 }, { spalte: 5, dauer: 130, hoehe: 0 }];
const SPRUNG_DAUER = SPRUNG.reduce((a, f) => a + f.dauer, 0);
// Felskante (Anhoehe, Plateau): festes Gelaende, das kein Wasser und kein Kartenrand ist
const klippe = (x, y) => x > 0 && y >= 6 && x < KARTE.breite - 1 && y < KARTE.hoehe - 1 &&
  KARTE.grund && KARTE.grund[y][x] && !KARTE.wasser[y][x];
// Springt man vor einer Felskante, traegt der Sprung ueber die Kante (hinauf oder hinunter)
function sprungZiel() {
  const [dx, dy] = RICHTUNG[spieler.blick], [zx, zy] = zelleVon(spieler.x, spieler.y);
  if (!klippe(zx + dx, zy + dy)) return null;
  for (let k = 2; k <= 3; k++) {
    const x = zx + dx * k, y = zy + dy * k;
    if (klippe(x, y)) continue;
    const p = mitte(x, y);
    return begehbar(p.x, p.y) ? p : null;
  }
  return null;
}
function springen() {
  if (spieler.sprung) return;
  spieler.sprung = performance.now();
  const ziel = sprungZiel();
  spieler.sprungWeg = ziel ? { vonX: spieler.x, vonY: spieler.y, nachX: ziel.x, nachY: ziel.y } : null;
  if (angeln) { angeln = null; meldung(t("Angel eingeholt.")); }
  geraeusch("absprung");
}
function sprungBild(t) {
  if (!spieler.sprung) return null;
  let rest = t - spieler.sprung;
  if (rest >= SPRUNG_DAUER) {
    if (spieler.sprungWeg) { spieler.x = spieler.sprungWeg.nachX; spieler.y = spieler.sprungWeg.nachY; spieler.sprungWeg = null; ortMerken(true); }
    spieler.sprung = 0; geraeusch("landen"); return null;
  }
  const w = spieler.sprungWeg;
  if (w) {                                                  // im Bogen ueber die Kante
    const k = rest / SPRUNG_DAUER;
    spieler.x = w.vonX + (w.nachX - w.vonX) * k; spieler.y = w.vonY + (w.nachY - w.vonY) * k;
  }
  for (const f of SPRUNG) { if (rest < f.dauer) return f; rest -= f.dauer; }
  return null;
}
let kartenNpcs = [];
// Bewohner einer Welt auf die Plaetze der Karte stellen (daheim an ihren festen Plaetzen)
function bewohnerAufstellen(K, w) {
  return (w.bewohner || []).map((b, i) => {
    const fest = (K.npcs || []).find(n => n.id === b.id) || K.plaetze[(i * 3 + 1) % K.plaetze.length];
    const p = mitte(fest.x, fest.y);
    return { id: b.id, stimme: b.stimme, blick: fest.blick || "S", ...p, heim: p, ziel: null, warte: 200 + Math.random() * 1500,
             bewegt: false, bild: 0, seit: 0, emote: null, emoteBis: 0, naechstesEmote: 5000 + Math.random() * 20000 };
  });
}
const hash = text => [...text].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
// In fremden Doerfern gehoert zu jedem Bewohner ein Haus (bauen.js) - dort faengt er den Tag an
function npcsNachHause() {
  if (!welt || !welt.tueren) return;
  if (aktWelt.heim) {                                      // daheim: nur die Leute der Nachbardörfer
    for (const n of kartenNpcs) {
      const dorfN = typeof dorfVon === "function" ? dorfVon(n.id) : null;
      if (!dorfN) continue;
      const nr = dorfN.bewohner.findIndex(b => b.id === n.id);
      const t = welt.tueren.find(q => q.schluessel === `${dorfN.id}:${nr}`) || welt.tueren.find(q => q.schluessel.startsWith(dorfN.id));
      if (t) {
        n.haus = t.schluessel;
        n.x = t.mitteX + (nr % 2 ? 18 : -18); n.y = t.vorY + 8;
      } else {                                               // noch kein Haus: sie stehen auf ihrem Land
        n.haus = null;
        const [px, py] = dorfN.felder[nr % dorfN.felder.length].split(",").map(Number);
        const m = mitte(px * PARZ + 1 + (nr % 2), py * PARZ + 1 + (nr % 3));
        n.x = m.x; n.y = m.y;
      }
      n.heim = { x: n.x, y: n.y };
      n.ziel = null;
    }
    return;
  }
  kartenNpcs.forEach((n, i) => {
    // Wessen Haus nicht mehr auf die Karte passte, wohnt bei jemandem mit
    const t = welt.tueren.find(q => q.schluessel.endsWith(`:${i}`)) || welt.tueren[i % (welt.tueren.length || 1)];
    if (!t) return;
    n.haus = t.schluessel;
    const eigenes = t.schluessel.endsWith(`:${i}`);
    n.x = t.mitteX + (eigenes ? 0 : (i % 2 ? 22 : -22));     // Mitbewohner stehen daneben
    n.y = t.vorY + 10 + (eigenes ? 0 : 6);
    n.heim = { x: n.x, y: n.y };
    n.ziel = null;
  });
}
// Wer nachts (21-5 Uhr) schlaeft - im eigenen Dorf bleiben alle wach (Nachtwache)
// Nachts sind die Leute in fremden Doerfern in ihren Haeusern
const schlaeft = n => istNacht() && (n.haus) && !(gespraech && gespraech.npc === n);
const wach = () => kartenNpcs.filter(n => !schlaeft(n));

// ---------------------------------------------------------------------------
// Zeit: echte Ortszeit. Zum Testen ?zeit=22.5 an die Adresse haengen.
// ---------------------------------------------------------------------------
const ZEIT_TEST = new URLSearchParams(location.search).get("zeit");
function jetzt() {
  const d = new Date();
  if (ZEIT_TEST !== null && !isNaN(parseFloat(ZEIT_TEST))) {
    const h = parseFloat(ZEIT_TEST);
    d.setHours(Math.floor(h), Math.round((h % 1) * 60), 0, 0);
  }
  return d;
}
const zwei = n => String(n).padStart(2, "0");
const tagSchluessel = (d = jetzt()) => `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`;
const tagNummer = (d = jetzt()) => Math.floor(new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() / 864e5);
// Spielzeit statt Uhrzeit: Der Tag wandert mit der Arbeit, die getan ist (dorf.js).
// Ohne Dorf (Titelbild, Schmiede) laeuft weiter die echte Tageszeit.
const stunde = (d = jetzt()) => {
  if (typeof tagFortschritt === "function" && stand && stand.dorf) {
    if (stand.dorf.phase === "nacht") return 23;
    return 6 + tagFortschritt() * 13;                       // 6 Uhr morgens bis 19 Uhr abends
  }
  return d.getHours() + d.getMinutes() / 60;
};
const istNacht = () => { const h = stunde(); return h >= 21 || h < 5; };
const MONATE = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];

// Regentage: etwa jeder fuenfte Tag, fest aus dem Datum. Zum Testen ?regen=1 oder ?regen=0
const REGEN_TEST = new URLSearchParams(location.search).get("regen");
const regenTag = () => (REGEN_TEST !== null ? REGEN_TEST === "1" : hash(spielTagSchluessel()) % 5 === 0);

// Ambiente nach Tageszeit: morgens (5-10 Uhr) Vogelzwitschern, nachts der Nachtwald mit Grillen,
// sonst keins - tagsueber traegt die Musik (musikTakt in schmiede.js)
function weltAmbiente() {
  const h = stunde();
  if (istNacht()) return KLANG.ambiente.find(k => k.datei === "grillen");
  if (h >= 5 && h < 10) return KLANG.ambiente.find(k => k.datei === "voegel");
  return null;
}
function weltUntergrund() {
  const x = Math.floor(spieler.x / TK), y = Math.floor((spieler.y - 2) / TK);
  return KARTE.untergrund[y] && KARTE.untergrund[y][x] ? "schritte_kies" : "schritte_laub";
}
// Trampelpfade: Wer oft dieselbe Strecke geht, tritt sie aus - nach einigen Malen wird Gras zu Weg
const TRAMPEL_SCHWELLE = 8;
let trampelZelle = { x: -1, y: -1 };
function trampeln() {
  if (!stand || !aktWelt || innen || !KARTE.wiese) return;
  const x = Math.floor(spieler.x / TK), y = Math.floor((spieler.y - 2) / TK);
  if (x === trampelZelle.x && y === trampelZelle.y) return;
  trampelZelle = { x, y };
  if (!KARTE.wiese[y] || !KARTE.wiese[y][x]) return;        // nur auf freier Wiese
  if (KARTE.untergrund[y][x]) return;                       // hier ist schon Weg
  const ws = weltSpeicher();
  ws.trampel = ws.trampel || {};
  const k = `${x},${y}`;
  ws.trampel[k] = (ws.trampel[k] || 0) + 1;
  if (ws.trampel[k] >= TRAMPEL_SCHWELLE) {
    KARTE.untergrund[y][x] = 1;
    welt.wege.push({ x, y });
    blumenWeg(x, y);                                        // wo alle laufen, waechst nichts mehr
    speichernBald();
  }
}
// ---------------------------------------------------------------------------
// Wegkacheln nach Nachbarschaft (Beschluss M1)
// ---------------------------------------------------------------------------
// Das Gelaendeblatt "gentle forest v01" traegt ein 4x4-Randset: Erdflaeche mit Grasrand an
// allen vier Seiten samt Ecken. Welche Kachel wohin gehoert, wurde aus der vorhandenen Karte
// abgelesen (die benutzt dasselbe Set). Maske: N=1, O=2, S=4, W=8 - gesetzt heisst "Nachbar
// ist auch Weg", die Kachel braucht dort also keinen Rand.
// Das Blatt "gentle forest v01" traegt ab gid 1 ein vollstaendiges 4x4-Randset - im Bild
// nachgesehen, nicht geraten:
//
//      1  2  3  4     Ecke oben links · obere Kante · obere Kante · Ecke oben rechts
//     17 18 19 20     linke Kante · Mitte · Mitte · rechte Kante
//     33 34 35 36     linke Kante · Mitte · Mitte · rechte Kante
//     49 50 51 52     Ecke unten links · untere Kante · untere Kante · Ecke unten rechts
//
// Die vier Ecken kommen in den Basiskarten nicht vor - deren Wege sind breit. Die
// getrampelten Pfade im Spiel sind schmal und brauchen sie: ohne Ecken franst jeder Knick
// aus. Aus "steht in keiner Karte" folgt eben nicht "gibt es nicht".
// Maske: N=1, O=2, S=4, W=8 - gesetzt heisst "Nachbar ist auch Weg", dort braucht es
// also KEINEN Rand.
const WEG_SET = {
  15: [18, 19, 34, 35],   // ringsum Weg: Mitte, vier Varianten gegen den Stempeleffekt
  14: [2, 3],             // kein Norden  -> obere Kante
  11: [50, 51],           // kein Sueden  -> untere Kante
  7:  [17, 33],           // kein Westen  -> linke Kante
  13: [20, 36],           // kein Osten   -> rechte Kante
  6:  [1],                // nur O und S  -> Ecke oben links
  12: [4],                // nur S und W  -> Ecke oben rechts
  3:  [49],               // nur N und O  -> Ecke unten links
  9:  [52],               // nur N und W  -> Ecke unten rechts
};
// Schmale Pfade (hoechstens zwei Nachbarn in einer Linie) hat das Set nicht - dort liegt
// einfach Erde, was zu einem ausgetretenen Trampelpfad ohnehin passt.
const WEG_SCHMAL = [18, 19, 34, 35];   // dieselben Mittelkacheln wie bei Maske 15
const istWeg = (x, y) => !!(KARTE && KARTE.untergrund && KARTE.untergrund[y] && KARTE.untergrund[y][x]);
function wegKachel(x, y) {
  if (!welt || !welt.wegGid) return 0;
  const m = (istWeg(x, y - 1) ? 1 : 0) | (istWeg(x + 1, y) ? 2 : 0)
          | (istWeg(x, y + 1) ? 4 : 0) | (istWeg(x - 1, y) ? 8 : 0);
  // Eine einzelne Trittstelle ist noch kein Weg. Das Kachelset hat fuer sie keine Form mit
  // Rand ringsum, es kaeme volle Erde mit harten Kanten heraus - ein brauner Fleck im Gras.
  // Erst wenn zwei Stellen aneinanderstossen, zeichnen wir einen Pfad.
  if (!m) return 0;
  const liste = WEG_SET[m] || WEG_SCHMAL;
  return liste[hash(`${x},${y}w`) % liste.length];          // feste Variante je Ort, kein Flackern
}
// ---------------------------------------------------------------------------
// Beim Laden der Welt: ausgetretene Pfade wieder einzeichnen
// Was auf einem Weg nicht stehenbleibt.
//
// Ein Trampelpfad entsteht dort, wo oft genug jemand geht - bisher ohne Ruecksicht darauf,
// was da wuchs. Die Blume blieb auf dem Weg stehen, und das sah aus, als waere sie mitten
// im Trampelpfad aufgegangen. Jetzt ist sie zertreten, und zwar auch im Speicher: wer sie
// selbst hingelegt hatte, findet sie nach dem Neuladen nicht wieder.
//
// Kiesel und Eicheln bleiben liegen - die wachsen nicht, die liegen nur herum.
function blumenWeg(x, y) {
  if (!welt || !welt.pflanzen) return 0;
  const trifft = p => p.x === x && p.y === y && typeof BLUMEN !== "undefined" && BLUMEN.includes(p.ding);
  const weg = welt.pflanzen.filter(trifft);
  if (!weg.length) return 0;
  welt.pflanzen = welt.pflanzen.filter(p => !trifft(p));
  // Selbst Hingelegtes steht auch im Weltspeicher
  if (stand && typeof weltSpeicher === "function") {
    const ws2 = weltSpeicher();
    if (ws2 && ws2.gelegt) ws2.gelegt = ws2.gelegt.filter(q => !(q.x === x && q.y === y
      && typeof BLUMEN !== "undefined" && BLUMEN.includes(q.ding)));
  }
  return weg.length;
}

function wegeAnwenden() {
  welt.wege = [];
  const ws = stand ? weltSpeicher() : null;
  if (!ws || !ws.trampel) return;
  for (const [k, n] of Object.entries(ws.trampel)) {
    if (n < TRAMPEL_SCHWELLE) continue;
    const [x, y] = k.split(",").map(Number);
    if (KARTE.untergrund[y]) KARTE.untergrund[y][x] = 1;
    welt.wege.push({ x, y });
  }
}

// Nach dem Aufbauen: was auf einem Weg steht, gehoert da nicht hin. Betrifft alte Staende,
// in denen Wege ueber Blumen gewachsen sind, bevor blumenWeg() das verhinderte.
function blumenAufWegenRaeumen() {
  if (!welt || !welt.pflanzen || !KARTE || !KARTE.untergrund) return 0;
  let weg = 0;
  for (const p of welt.pflanzen.slice())
    if (typeof BLUMEN !== "undefined" && BLUMEN.includes(p.ding)
        && KARTE.untergrund[p.y] && KARTE.untergrund[p.y][p.x]) weg += blumenWeg(p.x, p.y);
  return weg;
}

// Welche Bilder des Laufzyklus den Fuss aufsetzen - dann kommt der Tritt
const AUFTRITT = [1, 3];   // bei vier Laufbildern setzt der Fuss beim zweiten und vierten auf
const bodenArt = () => (innen ? "holz" : weltUntergrund() === "schritte_kies" ? "kies" : "laub");

// Licht ueber den Tag: [Stunde, [r, g, b, Deckkraft]], dazwischen weich uebergeblendet
const LICHT = [[0, [16, 24, 64, .5]], [4.5, [16, 24, 64, .5]], [6, [255, 150, 90, .16]], [7.5, [255, 200, 150, 0]],
  [16.5, [255, 200, 150, 0]], [18, [255, 120, 60, .18]], [19.5, [60, 40, 110, .3]], [21, [16, 24, 64, .5]], [24, [16, 24, 64, .5]]];
function licht(h) {
  for (let i = 0; i < LICHT.length - 1; i++) {
    const [h0, a] = LICHT[i], [h1, b] = LICHT[i + 1];
    if (h >= h0 && h <= h1) { const t = (h - h0) / (h1 - h0); return a.map((v, k) => v + (b[k] - v) * t); }
  }
  return LICHT[0][1];
}

// ---------------------------------------------------------------------------
// Dinge: Blumen und Kiesel (Deko-Ebene, taeglich neu), Aeste und Eicheln (Baeume schuetteln),
// Fische (angeln), Werkstuecke (Werkbuch). Kein Geld - getauscht und verschenkt wird direkt.
// bild = Nummer in atlas/dinge.png (freigestellt von dinge.py), sonst ein Pixelbild aus PIXEL; mehr = Mehrzahl
// ---------------------------------------------------------------------------
const DINGE = {
  rosa:     { name: "Rosa Blüte",    mehr: "Rosa Blüten",    bild: 0 },
  ringel:   { name: "Ringelblume",   mehr: "Ringelblumen",   bild: 1 },
  gaense:   { name: "Gänseblümchen",                         bild: 2 },
  lavendel: { name: "Lavendel",                              bild: 3 },
  kiesel:   { name: "Kiesel",                                bild: 4 },
  ast:      { name: "Zweig",         mehr: "Zweige" },       // nicht "Äste": das Ä der Pixelschrift liest sich wie ß
  eichel:   { name: "Eichel",        mehr: "Eicheln" },
  rotauge:  { name: "Rotauge",       mehr: "Rotaugen" },
  barsch:   { name: "Barsch",        mehr: "Barsche" },
  karpfen:  { name: "Karpfen" },
  hecht:    { name: "Hecht",         mehr: "Hechte" },
  stiefel:  { name: "Alter Stiefel", mehr: "Alte Stiefel" },
  angel:    { name: "Angel",         mehr: "Angeln" },
  floete:   { name: "Holzflöte",     mehr: "Holzflöten" },
  kranz:    { name: "Blumenkranz",   mehr: "Blumenkränze" },
  strauss:  { name: "Blumenstrauß",  mehr: "Blumensträuße" },
  kette:    { name: "Kieselkette",   mehr: "Kieselketten" },
  vogelhaus:{ name: "Vogelhäuschen" },
  kraeuterfisch: { name: "Kräuterfisch", mehr: "Kräuterfische" },
  balken:   { name: "Balken" },
  stein:    { name: "Steinblock",    mehr: "Steinblöcke" },
  gans:     { name: "Gans",          mehr: "Gänse" },
  huhn:     { name: "Huhn",          mehr: "Hühner" },
  ziege:    { name: "Ziege",         mehr: "Ziegen" },
  schaf:    { name: "Schaf",         mehr: "Schafe" },
  schwein:  { name: "Schwein",       mehr: "Schweine" },
  esel:     { name: "Esel" },
  hund:     { name: "Hund",          mehr: "Hunde" },
};
const TILE_DING = { 104: "rosa", 105: "ringel", 106: "gaense", 90: "lavendel", 88: "kiesel", 89: "kiesel" };
const BLUMEN = ["rosa", "ringel", "gaense", "lavendel"];
// ---------------------------------------------------------------------------
// Was ein Kraut ist
//
// Nicht jede Blüte heilt. Heilkräuter sind Lavendel und Ringelblume - die beiden
// ausgesprochenen Heilpflanzen: Lavendel beruhigt, die Ringelblume (Calendula) ist seit je
// das Wundkraut. Rosa Blüte und Gänseblümchen bleiben Zierde: zum Schenken, für Kränze und
// Sträuße, für die Schönheit des Dorfes.
//
// Vorher galten drei verschiedene Auffassungen nebeneinander: das Lager nahm Lavendel,
// Rosa und Ringel, beim Verbinden zählten nur Lavendel und Rosa, und das Gänseblümchen war
// nirgends etwas wert. Wer eine Ringelblume dabeihatte, konnte damit niemanden versorgen -
// im Lager galt sie aber als Kraut.
// ---------------------------------------------------------------------------
const KRAEUTER = ["lavendel", "ringel"];
const istKraut = ding => KRAEUTER.includes(ding);
const FISCHE = ["rotauge", "barsch", "karpfen", "hecht"];
const TASCHENPLAETZE = 20, STAPEL = 10;
// Der Name eines Dings in der aktuellen Sprache. Die Namen stehen deutsch in DINGE und
// sind der Schluessel im Katalog - ohne t() blieben sie deutsch, auch wenn der Satz
// drumherum uebersetzt ist ("Korb hergestellt!" in einem englischen Spiel).
const dingName = (ding, viele = false) => t(viele && DINGE[ding].mehr ? DINGE[ding].mehr : DINGE[ding].name);
const menge = (ding, n) => `${n} ${dingName(ding, n !== 1)}`;

// Pixelbilder 16x16: Zeichen -> Farbe (PALETTE, je Bild ergaenzbar), "." = leer
const PALETTE = { o: "#2a1a10", b: "#7a4a24", h: "#b8844a", t: "#d8b070", g: "#8a8f96", G: "#c4c8cc", w: "#ece6d6",
  r: "#c83c3c", y: "#e8b84a", p: "#d27aa0", l: "#9a7ad0", n: "#4f8a34", N: "#8cc454", e: "#1e120a" };
const FISCH = ["", "", "", "......oooo", ".....oxxxxo", "...oossssssoo.oo", "..osesssssssssoxo", ".osssssssssssxxo",
  ".oSSSSSSSSSSSoxo", "..oSSSSSSSSSo.oo", "...ooSSSSSoo", ".....ooooo"];
const fisch = (s1, s2, x) => ({ bild: FISCH, farben: { s: s1, S: s2, x } });
const PIXEL = {
  ast: { bild: ["", ".............o", "............oho", "...........ohbo", "..........ohbo", "......o..ohbo", ".....oho.hbo",
    "......ohohbo", ".......ohbo", "......ohbo", ".....ohbo", "....ohbo", "...ohbo", "..ohbo", "..obo", "...o"] },
  eichel: { bild: ["", "", "......oo", ".....obbo", "...oobbbboo", "..obhbhbhbbo", "..ohbhbhbhbo", "..oooooooooo",
    "...ottttttto", "...othtttto", "...othtttto", "....otttto", ".....otto", "......oo"] },
  rotauge: fisch("#b8c8d0", "#e6eef0", "#c83c3c"),
  barsch:  fisch("#7a9a3a", "#d8d890", "#e07a30"),
  karpfen: fisch("#c8963c", "#ecd08a", "#9a6a2a"),
  hecht:   fisch("#4f7a44", "#b8c890", "#6a8a3a"),
  kraeuterfisch: { bild: FISCH.map((z, i) => i === 4 ? ".....olNlo" : z), farben: { s: "#a86a34", S: "#d09a5a", x: "#7a4a24" } },
  stiefel: { bild: ["", "", "....oooooo", "....obbbbo", "....obhbbo", "....obbbbo", "....obbbbo", "....obbbbooooo",
    "....obbbbbbbbbo", "...oobbbbbbbhbo", "...obbbbbbbbbbo", "...oooooooooooo", "....o.o.o.o.o"] },
  angel: { bild: ["", ".............o", "............oho", "...........oho.w", "..........oho..w", ".........oho...w",
    "........oho....w", ".......oho.....w", "......oho......w", ".....oho.......w", "....obo........w", "...obo........oro",
    "..obo..........o", ".oyo", ".oo"] },
  floete: { bild: ["", "", "", "", "", ".oooooooooooooo", ".ohhhhhhhhhhhho", ".obhobobobobbho", ".obbbbbbbbbbbbo", ".oooooooooooooo"] },
  kranz: { bild: ["", "", ".....oooooo", "...ooNpNNyNoo", "..oNNoooooNNo", ".oNpo.....oNpo", ".oNo.......oNo", ".oyo.......oyo",
    ".oNo.......oNo", ".oNpo.....opNo", "..oNNoooooNNo", "...ooNyNNpNoo", ".....oooooo"] },
  strauss: { bild: ["", "....op.oy.ol", "...opppoyyolllo", "...oppoyyyollo", "....oNoyyoNlo", ".....oNoNoNo", "......oNNNo",
    "......oNNNo", ".....orrrrro", "......oNNNo", "......oNNNo", ".......oNo", ".......ooo"] },
  kette: { bild: ["", "", "....bbbbbbbb", "...b........b", "..b..........b", "..b..........b", "..b..........b",
    "...b........b", "...oo......oo", "..oGgo.bb.oGgo", "..oggob..boggo", "...oo.oGgo.oo", ".......oggo", "........oo"] },
  vogelhaus: { bild: [".......oo", "......orro", ".....orrrro", "....orrrrrro", "...orrrrrrrro", "..oooooooooooo",
    "....ohhhhhho", "....ohhoohho", "....ohoeeohho", "....ohhoohho", "....ohhhhhho", "....ohhhhhho", "....oooooooo",
    ".......ob", ".......ob"] },
};
// Baustoffe
Object.assign(PIXEL, {
  balken: { bild: ["", "", "", "", "..oooooooooooo", ".ohhhhhhhhhhhho", ".obhbbbbbbbbhbo", ".oooooooooooooo",
    "..oooooooooooo", ".ohhhhhhhhhhhho", ".obbbbbhbbbbbbo", ".oooooooooooooo"] },
  stein: { bild: ["", "", "", "...oooooooooo", "..oGGGGGGGGGGo", "..oGggGGGGgGGo", "..oGGGGGGGGGGo", "..ogGGGgGGGGgo",
    "..oggggggggggo", "..oggggggggggo", "...oooooooooo"] },
});
// Tiere (Tauschwaren) - Blick nach links
Object.assign(PIXEL, {
  gans: { bild: ["", "...oo", "..owwo", "..oewo", "yyowwo", ".ooowo", "...owo", "...owoooooo", "..owwwwwwwwo",
    "..owwGwwwwwwo", "..owwwGGwwwwo", "...owwwwwwwo", "....ooooooo", ".....y..y", ".....yy.yy"] },
  huhn: { bild: ["", "", "...rr", "..orro", "..ohho", ".yoehho", "..ohhho..oo", "...ohhhoohbo", "...ohhhhhhbo",
    "...ohbhhhhbo", "....ohhbbho", ".....ooooo", "......y.y", "......y.y"] },
  ziege: { bild: ["", ".b..b", ".bb.bb", "..owwo", ".owewwo", ".owwwwoooooooo", "..oGwwwwwwwwwwo", "...owwwwwwwwwwo",
    "...owGwwwwwGwwo", "....owwwwwwwwo", "....ob.ob.ob.ob", "....ob.ob.ob.ob", "....oo.oo.oo.oo"] },
  schaf: { bild: ["", "", ".....oooooooo", "..oooGwwwwwwGoo", ".oeeoGwGwwGwwGwo", ".oeeowwwwwwwwwwo", ".oeeowwGwwwwGwwo",
    "..oooGwwwwwwwwGo", "....oGwwGwwwGwo", ".....oooooooooo", ".....oe.oe.oe.oe", ".....oe.oe.oe.oe"] },
  schwein: { bild: ["", "", "", ".....ooooooooo", "...oopppppppppoo", "..opppppppppppppo", ".oopepppppppppppo",
    "oPPoppppppppppppo", "oPPopppppppppppo", ".ooppppppppppppo", "...oppoooooppo", "...opo....opo", "...oo.....oo"],
    farben: { p: "#e8a0b4", P: "#b8587a" } },
  esel: { bild: ["", "..o..o", ".ogoogo", ".ogoogo", ".oggggo", "ogeggggo", "oGGgggggooooooo", ".ooogggggggggggo",
    "...oggggggggggo", "...oggggggggggo", "...ogo.ogo.ogo", "...ogo.ogo.ogo", "...ooo.ooo.ooo"],
    farben: { g: "#8a7a6a", G: "#c8b8a0" } },
});
const dingBilder = {};
// Symbole im Mana-Seed-Stil (Assets/14_nachbau_mana/spielsymbole.py -> atlas/dinge_mana.png); PIXEL bleibt Rueckfall
// Die Reihenfolge entspricht den Feldern in atlas/dinge_mana.png (siehe dinge_mana.json).
const MANA_SYMBOLE = ["ast", "eichel", "stiefel", "angel", "floete", "kranz", "strauss", "kette", "vogelhaus", "hund", "stein", "rotauge", "barsch", "karpfen", "hecht", "kraeuterfisch", "gans", "huhn", "ziege", "schaf", "schwein", "esel"];
// Moebel sind Dinge wie alles andere: gebaut im Werkbuch, getragen in der Tasche, gestellt im Haus
function moebelDingeAnlegen() {
  if (!window.INNEN || DINGE.bett) return;
  for (const [art, m] of Object.entries(INNEN.moebel)) DINGE[art] = { name: m.name, mehr: m.mehr, moebel: m.nr };
}
function dingBild(ding) {
  if (dingBilder[ding]) return dingBilder[ding];
  const c = document.createElement("canvas"); c.width = c.height = DING_TK;
  const x = c.getContext("2d"), d = DINGE[ding];
  const m = MANA_SYMBOLE.indexOf(ding);
  const T = DING_TK;
  if (d && d.moebel !== undefined) {
    if (welt.moebelSymbole) x.drawImage(welt.moebelSymbole, d.moebel * T, 0, T, T, 0, 0, T, T);
    return (dingBilder[ding] = c);
  }
  if (d.bild !== undefined) {
    if (welt.dinge) x.drawImage(welt.dinge, d.bild * T, 0, T, T, 0, 0, T, T);
  } else if (m >= 0 && welt.dingeMana) {
    x.drawImage(welt.dingeMana, m * T, 0, T, T, 0, 0, T, T);
  } else if (!PIXEL[ding]) {
    // Kein Blatt, keine Pixelzeichnung: lieber ein schlichtes Kaestchen als ein Absturz
    const e = T / 16;
    x.fillStyle = "#8a7a5a"; x.fillRect(3*e, 4*e, 10*e, 9*e);
    x.fillStyle = "#5a4a34"; x.fillRect(3*e, 4*e, 10*e, 1*e); x.fillRect(3*e, 12*e, 10*e, 1*e);
  } else {
    // Pixelzeichnungen sind in 16er-Raster geschrieben; jedes Zeichen wird ein e x e-Block.
    const { bild, farben = {} } = PIXEL[ding], pal = { ...PALETTE, ...farben }, e = T / 16;
    const oben = Math.floor((16 - bild.length) / 2);
    bild.forEach((z, j) => [...z.slice(0, 16)].forEach((ch, i) => { if (pal[ch]) { x.fillStyle = pal[ch]; x.fillRect(i*e, (oben + j)*e, e, e); } }));
  }
  return (dingBilder[ding] = c);
}

// Was am Boden liegt, waechst ueber Nacht nach - gerechnet wird in SPIELtagen, nicht in
// echten Kalendertagen. Vorher kamen aufgehobene Kiesel erst am naechsten echten Tag wieder:
// wer mehrere Spieltage am Stueck spielte, hatte irgendwann keine Kiesel mehr fuer Haeuser.
const spielTagSchluessel = () => (typeof dorf === "function" && dorf() ? `t${dorf().tag}` : tagSchluessel());
// Dasselbe als Zahl, fuer alles, was im Takt der Spieltage wechseln soll: Handelskurse,
// Tauschangebote, was die Leute heute erzaehlen. Vorher hing das am echten Kalendertag -
// wer an einem Nachmittag zehn Spieltage spielte, erlebte zehnmal denselben Tag.
const spielTagNummer = () => (typeof dorf === "function" && dorf() ? dorf().tag : tagNummer());
function gepflueckt() {
  if (!stand) return [];
  const ws = weltSpeicher();                                   // je Welt eigener Stand (universum.js)
  const heute = spielTagSchluessel();
  if (!ws.gepflueckt || ws.gepflueckt.tag !== heute) ws.gepflueckt = { tag: heute, zellen: [] };
  return ws.gepflueckt.zellen;
}
function einstecken(ding) {
  const t = stand.taschen;
  let fach = t.find(f => f && f.ding === ding && f.anzahl < STAPEL);
  if (fach) { fach.anzahl += 1; return true; }
  const frei = t.findIndex(f => !f);
  if (frei < 0) return false;
  t[frei] = { ding, anzahl: 1 };
  return true;
}
const anzahlIn = liste => stand.taschen.reduce((s2, f) => s2 + (f && liste.includes(f.ding) ? f.anzahl : 0), 0);
function herausnehmen(liste, n) {
  for (let i = 0; i < stand.taschen.length && n > 0; i++) {
    const f = stand.taschen[i];
    if (!f || !liste.includes(f.ding)) continue;
    const weg = Math.min(n, f.anzahl);
    f.anzahl -= weg; n -= weg;
    if (!f.anzahl) stand.taschen[i] = null;
  }
  return n === 0;                                            // alles gefunden?
}
// Nehmen und Geben in einem Zug (Herstellen, Tauschen): alles oder nichts.
// nehmen: [[liste, n], ...], geben: [[ding, n], ...] -> "ok" | "fehlt" | "voll"
function umtauschen(nehmen, geben) {
  if (nehmen.some(([liste, n]) => anzahlIn(liste) < n)) return "fehlt";
  const vorher = JSON.stringify(stand.taschen);
  nehmen.forEach(([liste, n]) => herausnehmen(liste, n));
  for (const [ding, n] of geben) for (let i = 0; i < n; i++)
    if (!einstecken(ding)) { stand.taschen = JSON.parse(vorher); return "voll"; }
  return "ok";
}

// ---------------------------------------------------------------------------
// Laden: Tilesets, NPC-Blaetter; Boden vorrendern, alles mit Hoehe als Sprite
// ---------------------------------------------------------------------------
const ladeBild = pfad => new Promise(ok => {
  const i = new Image(); i.onload = () => ok(i); i.onerror = () => { console.warn("Fehlt:", pfad); ok(null); }; i.src = pfad;
});

// Eine Welt laden: Basiskarte (w.karte), Faerbung (w.biom, Assets/10_testmap/biome.js) und Bewohner.
// Alles wird erst fertig gebaut und dann auf einmal gesetzt - die laufende Welt stoert das nicht.
async function weltLaden(w = { id: "titel", karte: 1, biom: "lichtung", bewohner: [] }) {
  // Gelaende der Basiskarte, geschmueckt aus dem Startwert der Welt (universum.js)
  const G = karteDaten(w.karte), biomId = w.biom;
  const K = schmuecken(G, weltSaat(w), [], { heim: !!w.heim });
  // Biom und Jahreszeit tauschen die Kachelblätter: die Jahreszeit hat Vorrang
  const biomBilder = ((window.BIOME || []).find(b => b.id === biomId) || {}).bilder || {};
  const zeit = typeof jahreszeit === "function" ? jahreszeit() : null;
  const ersatz = zeit && Object.keys(zeit.bilder || {}).length ? { ...biomBilder, ...zeit.bilder } : biomBilder;
  const tilesets = await Promise.all(K.tilesets.map(async ts => ({ ...ts, img: await ladeBild(WURZEL + (ersatz[ts.name] || ts.bild)) })));
  const npcs = bewohnerAufstellen(K, w);
  // Auch die Verwundeten brauchen ihr Blatt: sie stehen nicht auf der Karte (dorfBewohner()
  // laesst sie weg), liegen aber im Krankenlager - ohne Blatt blieben dort nur Stroh und
  // Decke uebrig, also zwei nackte Rechtecke.
  // ALLE Dorfbewohner brauchen ihr Blatt, nicht nur die, die bei der Welterzeugung dabei
  // waren. w.bewohner ist die Liste von damals; wer spaeter zuwandert oder als Ueberlebender
  // gerettet wird, steht nur in dorf().bewohner - und bekam nie eines. Gemessen an Wolfgangs
  // Welt: 10 Bewohner, 3 Blaetter. Die anderen sieben wurden in der Stube als brauner Kasten
  // gezeichnet (haus.js #8a6a4a), im Krankenlager stand ein Klotz neben dem Kranken.
  const dorfLeute = w.heim && typeof dorf === "function" && dorf() ? (dorf().bewohner || []) : [];
  const schonDa = new Set((w.bewohner || []).map(b2 => b2.id));
  const dazu = dorfLeute.filter(b2 => b2 && b2.id && !schonDa.has(b2.id));
  await Promise.all([...(w.bewohner || []), ...dazu].map(async b => {
    if (b.fest) npcBlaetter[b.id] = npcBlaetter[b.id] || await ladeBild(`${WURZEL}Assets/Menschen/1_figuren/npcs/${b.id}/${b.id}_p1.png`);
    else {
      PERSONEN[b.id] = { name: b.name, fremd: true };
      // Wer eine Klasse ausuebt, traegt deren Outfit - wechselt er die Klasse, wird das Blatt
      // neu gebaut. Deshalb steht die Klasse im Schluessel.
      const kl = typeof wert === "function" && wert(b.id) ? wert(b.id).klasse : null;
      if (npcKlasse[b.id] !== kl) { delete npcBlaetter[b.id]; npcKlasse[b.id] = kl; }
      npcBlaetter[b.id] = npcBlaetter[b.id] || await figurBlatt(b.figur, kl);
    }
  }));
  const tsVon = gid => tilesets.find(t => gid >= t.firstgid && gid < t.firstgid + t.anzahl);
  const kachel = (c, gid, x, y, dx = 0) => {
    const ts = tsVon(gid);
    if (!ts) return;                                         // Kachel ohne Blatt: nichts zu malen
    const i = gid - ts.firstgid;
    if (ts.img) c.drawImage(ts.img, (i % ts.spalten) * ts.tw, Math.floor(i / ts.spalten) * ts.th, ts.tw, ts.th,
                            x * TK + dx, (y + 1) * TK - ts.th, ts.tw, ts.th);      // unten links verankert
  };
  const baumTs = tilesets.find(t => t.name === "gentle trees 80x96 v01");
  const forest = tilesets.find(t => t.name === "gentle forest v01");
  const boden = document.createElement("canvas");
  boden.width = K.breite * TK; boden.height = K.hoehe * TK;
  const b = boden.getContext("2d");
  const pflanzen = [];
  const grundFest = K.fest.map(z => z.slice());
  K.ebenen.boden.forEach((z, y) => z.forEach((g, x) => g && kachel(b, g, x, y)));
  K.ebenen.deko.forEach((z, y) => z.forEach((g, x) => {
    if (!g) return;
    // Ohne das Waldblatt gibt es hier nichts zu erkennen - vorher warf diese Zeile bei
    // jedem Weltladen eine Ausnahme, die niemand sah, weil sie in einer Promise versackte.
    const ding = forest && g >= forest.firstgid && g < forest.firstgid + forest.anzahl
      ? TILE_DING[g - forest.firstgid] : null;
    if (ding) {
      pflanzen.push({ x, y, g, ding });
      const rand = y < 6 || x === 0 || y === K.hoehe - 1 || x === K.breite - 1;
      if (ding === "kiesel" && !rand) grundFest[y][x] = 0;              // Kiesel blockiert nur, solange er liegt
    } else kachel(b, g, x, y);
  }));
  // -------------------------------------------------------------------------
  // Raeumbare Hindernisse: Fels, Baumstumpf und Farn belegen je vier Kacheln. Gemessen waren
  // von 144 Kacheln auf eigenem Land nur 8 bebaubar - Bauplatz war der knappste Rohstoff des
  // Dorfes, und daran hing die ganze Abwaertsspirale (kein Platz -> keine Haeuser -> keine
  // Betten -> keine Zuwanderung). Also stehen sie nicht mehr bloss im Weg: dreimal P, dann
  // sind sie weg und geben ihre vier Kacheln frei. Baeume und Buesche bleiben Landschaft.
  // -------------------------------------------------------------------------
  const ts32 = tilesets.find(t => t.name === "gentle 32x32 v01");
  const G32 = ts32 ? ts32.firstgid : -99;                    // 32er-Blatt: 0 Stumpf, 1 Fels, 2 Busch, 3 Farn
  const FELS16 = forest ? forest.firstgid + 86 : -1;         // linke obere Ecke des kleinen Felsens
  const hindernisse = [], hindernisZelle = new Map(), hindernisFort = new Set();
  // Alte Staende trugen die Liste unter "felsWeg" - sie wird uebernommen, damit ein
  // zerklopfter Fels nicht ploetzlich wieder dasteht.
  // Die Welt, die GERADE geladen wird - nicht aktWelt. Das wird erst weiter unten gesetzt,
  // und beim Neuladen des Spiels ist es hier noch null: die Liste kam dann leer heraus, und
  // jeder mühsam weggeklopfte Fels stand nach einem Neustart wieder da. Geschrieben wurde
  // korrekt, gelesen nichts. (Auf dem Titelbild gibt es keinen Stand - dann bleibt sie leer.)
  const geraeumt = stand && w && w.id
    ? (weltSpeicher(w.id).geraeumt || weltSpeicher(w.id).felsWeg || []) : [];
  const hindernisAnlegen = (art, x, y, zellen) => {
    const id = `${x},${y}`;
    if (geraeumt.includes(id)) {                             // schon geraeumt: Kacheln bleiben frei
      zellen.forEach(([cx, cy]) => {
        hindernisFort.add(`${cx},${cy}`);
        // Beide Karten freigeben. Vorher stand hier nur K.fest - grundFest ist aber eine
        // Kopie, die vorher gezogen wurde, und istFest() fragt zuerst grundFest. Wer einen
        // Fels weggeklopft hatte, fand nach dem Neuladen eine unsichtbare Wand an seiner
        // Stelle: genau die Bauplaetze, fuer die das Raeumen gebaut wurde, blieben belegt.
        if (K.fest[cy] && !K.grund[cy][cx]) K.fest[cy][cx] = 0;
        if (grundFest[cy] && !K.grund[cy][cx]) grundFest[cy][cx] = 0;
      });
      return;
    }
    const h = { id, art, x, y, zellen, schlaege: 0, wackelBis: 0, ruheBis: 0 };
    hindernisse.push(h);
    zellen.forEach(([cx, cy]) => hindernisZelle.set(`${cx},${cy}`, h));
  };
  K.ebenen.gross.forEach((z, y) => z.forEach((g, x) => {
    const art = { 0: "stumpf", 1: "fels", 3: "farn" }[g - G32];
    if (art) hindernisAnlegen(art, x, y, [[x, y], [x + 1, y], [x, y - 1], [x + 1, y - 1]]);
  }));
  K.ebenen.objekte.forEach((z, y) => z.forEach((g, x) => {
    if (g === FELS16) hindernisAnlegen("fels", x, y, [[x, y], [x + 1, y], [x, y + 1], [x + 1, y + 1]]);
  }));
  const sprites = [], baeume = [];
  ["objekte", "gross", "haus", "baeume"].forEach(n => K.ebenen[n].forEach((z, y) => z.forEach((g, x) => {
    if (!g) return;
    if ((n === "gross" || n === "objekte") && hindernisFort.has(`${x},${y}`)) return;   // geraeumt, malt nichts mehr
    if (n === "gross" || n === "objekte") {
      const h = hindernisZelle.get(`${x},${y}`);
      if (h) {                                               // wackelt beim Anfassen
        sprites.push({ unten: (y + 1) * TK, hindernis: h, zx: x, zy: y,
                       male: (c, t) => kachel(c, g, x, y, t < h.wackelBis ? Math.round(Math.sin(t / 22) * 2) : 0) });
        return;
      }
    }
    if (n === "baeume" && baumTs && g >= baumTs.firstgid && g < baumTs.firstgid + baumTs.anzahl) {
      // Stamm in der dritten Spalte der Krone; Schuetteln laesst den Baum kurz wackeln
      const b = { x, y, id: `${x},${y}`, stammX: (x + 2) * TK + TK / 2, stammY: (y + 1) * TK, wackelBis: 0 };
      baeume.push(b);
      sprites.push({ unten: (y + 1) * TK, male: (c, t) => kachel(c, g, x, y, t < b.wackelBis ? Math.round(Math.sin(t / 30) * 1.5) : 0) });
    } else sprites.push({ unten: (y + 1) * TK, male: c => kachel(c, g, x, y) });
  })));
  const glitzer = [];
  K.ebenen.glitzer.forEach((z, y) => z.forEach((g, x) => g && glitzer.push({ g, x, y })));
  const dinge = await ladeBild("atlas/dinge.png");
  const dingeMana = welt && welt.dingeMana ? welt.dingeMana : await ladeBild("atlas/dinge_mana.png");
  const falter = welt && welt.falter ? welt.falter : await ladeBild("atlas/falter_mana.png");
  const haeuser = welt && welt.haeuser ? welt.haeuser : await ladeBild(`${WURZEL}Assets/20_haeuser/haeuser.png`);
  const stuben = welt && welt.stuben ? welt.stuben : await ladeBild(`${WURZEL}Assets/21_innenraum/innen.png`);
  const tuerbilder = welt && welt.tuerbilder ? welt.tuerbilder : await ladeBild(`${WURZEL}Assets/21_innenraum/tueren.png`);
  const moebelBlatt = welt && welt.moebelBlatt ? welt.moebelBlatt : await ladeBild(`${WURZEL}Assets/21_innenraum/moebel.png`);
  // Die Liegepose fuers Bett (vier Atembilder a 22x10, seit 11.10.)
  const liegen = welt && welt.liegen ? welt.liegen : await ladeBild("atlas/p1_liegen.png").catch(() => null);
  // Palisade aus dem Zaun-Paket (15_zaeune_mauern, Mana Seed "timber fort wall"): 19 Spalten à 16x64
  const palisade = welt && welt.palisade ? welt.palisade : await ladeBild(`${WURZEL}Assets/15_zaeune_mauern/palisade.png`);
  // Lattenzaun: Geruest fuer alles, was noch im Bau ist (4x3 Kacheln)
  const zaun = welt && welt.zaun ? welt.zaun : await ladeBild(`${WURZEL}Assets/15_zaeune_mauern/lattenzaun.png`);
  const steinmauer = welt && welt.steinmauer ? welt.steinmauer : await ladeBild(`${WURZEL}Assets/15_zaeune_mauern/steinmauer.png`);
  // Wehrgang: Palisade mit Laufgang (Beschluss aus der ersten Fragerunde, endlich gebaut)
  const wehrgang = welt && welt.wehrgang ? welt.wehrgang : await ladeBild(`${WURZEL}Assets/15_zaeune_mauern/wehrgang.png`);
  const moebelSymbole = welt && welt.moebelSymbole ? welt.moebelSymbole : await ladeBild("atlas/moebel_dinge.png");
  // Sitzbeine: eigene Zeichnung als Maske, wird je Figur eingefaerbt (siehe sitzBlatt weiter unten)
  const sitzbeine = welt && welt.sitzbeine ? welt.sitzbeine : await ladeBild(`${WURZEL}Assets/14_nachbau_mana/posen/sitzbeine.png`);
  moebelDingeAnlegen();
  KARTE = K; aktWelt = w; kartenNpcs = npcs; lieblingsorte = null;
  if (typeof begleiterNachziehen === "function") begleiterNachziehen();   // wer mitging, geht weiter mit
  abstecken = false;
  schilder.forEach(el => { el.hidden = true; });
  if (stand && w.id !== "titel") ortZeile();
  let wegGid = 0;                                          // eine Kachel des vorhandenen Weges als Vorlage
  for (let y = 6; y < K.hoehe - 1 && !wegGid; y++)
    for (let x = 1; x < K.breite - 1 && !wegGid; x++)
      if (K.untergrund[y][x] && K.ebenen.boden[y][x]) wegGid = K.ebenen.boden[y][x];
  welt = { biom: biomId, boden, sprites, glitzer, pflanzen, grundFest, baeume, kachel, forest, dinge, dingeMana, falter, haeuser, stuben, tuerbilder, moebelBlatt, moebelSymbole, liegen, palisade, zaun, steinmauer, wehrgang, sitzbeine, hindernisse, hindernisZelle, wege: [], wegGid };
  for (const k in dingBilder) delete dingBilder[k];
  for (const q of ((stand && w.id !== "titel" ? weltSpeicher().gelegt : null) || []))   // selbst Hingelegtes
    if (!pflanzen.some(p => p.x === q.x && p.y === q.y)) pflanzen.push({ ...q, g: 0, gelegt: true });
  // Was noch in der Szenenschlange steht, gehoert zur alten Welt - sonst spielt beim
  // Betreten einer anderen Karte ploetzlich eine Szene von dort. Dasselbe gilt fuer ein
  // offenes Gespraech: es haengt sonst ueber der neuen Welt, und man redet mit jemandem,
  // der dort gar nicht steht.
  if (typeof szenenLeeren === "function") szenenLeeren();
  if (typeof gespraech !== "undefined" && gespraech && typeof gespraechEnde === "function") gespraechEnde();
  wegeAnwenden();
  blumenAufWegenRaeumen();
  // Vor dem Aufbau: halten die Formeln noch? Ein Spielstand ueberlebt viele Fassungen,
  // und ein Widerspruch darin (volles Material, Haus steht nicht) faellt sonst niemandem
  // auf ausser dem Spieler. Nur im echten Spiel - das Titelbild hat kein Dorf.
  if (stand && w.id !== "titel" && typeof regelnBeimBetreten === "function") regelnBeimBetreten();
  bauAufbauen();
  npcsNachHause();                                           // fremde Bewohner stehen bei ihren Haeusern
  teilchenAnlegen();
}

// Kartenbild ohne Figuren (fuer das Titelbild)
function weltStandbild(c) {
  c.imageSmoothingEnabled = false;
  c.drawImage(welt.boden, 0, 0);
  welt.pflanzen.forEach(p => welt.kachel(c, p.g, p.x, p.y));
  welt.glitzer.forEach(({ g, x, y }) => welt.kachel(c, g, x, y));
  welt.sprites.slice().sort((a, b) => a.unten - b.unten).forEach(sp => sp.male(c, 0));
}

// ---------------------------------------------------------------------------
// Kollision
// ---------------------------------------------------------------------------
function istFest(px, py) {
  const x = Math.floor(px / TK), y = Math.floor(py / TK);
  if (x < 0 || y < 0 || x >= KARTE.breite || y >= KARTE.hoehe) return true;
  if (welt.grundFest[y][x] === 1 || (welt.bauFest && welt.bauFest.has(`${x},${y}`))) return true;   // auch Haeuser
  return KARTE.fest[y][x] === 1 && !gepflueckt().includes(`${x},${y}`);   // Kiesel noch da?
}
// Fussbox 10x5 px gegen Karte und andere Figuren; "selbst" = die Figur, die sich bewegt
// Fremde Doerfer betritt man erst, wenn das eigene Land an sie stoesst. Vorher steht man
// vor ihren Parzellen wie vor einer Grenze - die Nachbarn lassen keinen herein, der keiner
// ist. "Angrenzen" ist dasselbe, was die Diplomatie schon verlangt (nachbarnAmRand).
let grenzGemahnt = 0;
function fremdesDorfSperrt(px, py, still = false) {
  if (typeof nachbardorfListe !== "function" || typeof PARZ === "undefined") return false;
  const kx = Math.floor(px / TK), ky = Math.floor((py - 2) / TK);
  const k = Math.floor(kx / PARZ) + "," + Math.floor(ky / PARZ);
  const n = nachbardorfListe().find(d => (d.felder || []).includes(k));
  if (!n) return false;
  if (typeof nachbarnAmRand === "function" && nachbarnAmRand().some(d => d.id === n.id)) return false;
  // Ein laufender Auftrag darf hinfuehren - sonst waere er nicht mehr zu erfuellen.
  if (typeof offeneQuests === "function" && offeneQuests().some(q => q.ziel &&
      Math.floor(q.ziel.x / PARZ) === Math.floor(kx / PARZ) &&
      Math.floor(q.ziel.y / PARZ) === Math.floor(ky / PARZ))) return false;
  const jetzt = performance.now();
  if (!still && jetzt - grenzGemahnt > 3200) {
    grenzGemahnt = jetzt;
    meldung(t("{0} lässt dich nicht herein. Erst musst du an ihr Land grenzen.", n.name), 3200);
  }
  return true;
}
function begehbar(x, y, selbst = spieler) {
  for (const [dx, dy] of [[-5, -5], [4, -5], [-5, -1], [4, -1]]) if (istFest(x + dx, y + dy)) return false;
  for (const n of wach()) if (n !== selbst && Math.abs(n.x - x) < 11 && Math.abs(n.y - y) < 6) return false;
  if (selbst !== spieler && Math.abs(spieler.x - x) < 11 && Math.abs(spieler.y - y) < 6) return false;
  return true;
}

// ---------------------------------------------------------------------------
// NPCs wandern ueber die ganze Lichtung: Ziel waehlen (irgendwo begehbar oder ein
// Lieblingsort), Weg per Breitensuche ueber das Kachelraster, hingehen, kurz verweilen.
// Bei Naehe oder im Gespraech bleiben sie stehen und schauen zur Spielfigur.
// ---------------------------------------------------------------------------
const NPC_TEMPO = 32;
const zelleVon = (px, py) => [Math.floor(px / TK), Math.floor((py - 2) / TK)];
const zellMitte = (x, y) => ({ x: x * TK + TK / 2, y: (y + 1) * TK - 2 });
const frei4 = (x, y) => x > 0 && y > 5 && x < KARTE.breite - 1 && y < KARTE.hoehe - 1 && KARTE.fest[y][x] !== 1 &&
  !(welt && welt.bauFest && welt.bauFest.has(`${x},${y}`));

// Lieblingsorte: Wegkreuzung, Teichufer, Baumstamm, Waldrand im Sueden ...
let lieblingsorte = null;
// Wo haelt sich jemand auf, der gerade nichts vorhat? Dort, wo im Dorf etwas los ist:
// vor den Haustueren, auf dem Platz, an den Aeckern. Hier standen zehn feste Kartenpunkte,
// die von einer bestimmten Karte stammten. Auf einem anders gewachsenen Dorf lagen sie
// samt und sonders ausserhalb - gemessen 40 von 40 Zielen - und weil sie in ueber der
// Haelfte der Faelle gewaehlt werden, lief das halbe Dorf den ganzen Tag ins Gelaende.
function lieblingsorteFinden() {
  const orte = [];
  const nahe = (x, y) => { for (let r = 0; r < 4; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++)
    if (frei4(x + dx, y + dy)) return [x + dx, y + dy]; return null; };
  const dazu = (x, y) => { const p = nahe(x, y); if (p) orte.push(p); };
  // Vor jeder Haustuer
  if (welt && welt.tueren) for (const tr of welt.tueren) dazu(Math.floor(tr.mitteX / TK), Math.floor(tr.vorY / TK) + 1);
  // Der Platz, an dem alles angefangen hat
  if (KARTE && KARTE.start) dazu(KARTE.start.x, KARTE.start.y);
  // Bei den Aeckern
  if (typeof aecker === "function") for (const a2 of aecker()) dazu(a2.x, a2.y + 1);
  // Und ein Fleck in jeder eigenen Parzelle, damit nicht immer dieselben Ecken belebt sind
  if (typeof felder === "function" && typeof parzTeile === "function" && typeof PARZ !== "undefined") {
    for (const f of felder()) {
      const [px, py] = parzTeile(f);
      dazu(px * PARZ + Math.floor(PARZ / 2), py * PARZ + Math.floor(PARZ / 2));
    }
  }
  // Notnagel: hat das Dorf noch gar nichts, bleiben die alten festen Punkte
  if (!orte.length)
    [[21, 15], [12, 22], [26, 12], [30, 12], [9, 15], [33, 19], [21, 24], [16, 12], [36, 17], [5, 12]]
      .forEach(([x, y]) => dazu(x, y));
  // Doppelte raus - vor zwei benachbarten Tueren kommt leicht derselbe Fleck heraus
  const gesehen = new Set();
  let fertig = orte.filter(p => { const k = `${p[0]},${p[1]}`; if (gesehen.has(k)) return false; gesehen.add(k); return true; });
  // Und was trotzdem ausserhalb liegt, fliegt raus: nahe() weicht bis zu vier Kacheln aus,
  // um einen freien Fleck zu finden, und landet dabei leicht jenseits der Dorfgrenze. Weil
  // ein Lieblingsort in ueber der Haelfte der Faelle gewaehlt wird, lief das Dorf dann doch
  // wieder ins Gelaende - im Testfall "npcwege" sporadisch 40 von 40 Zielen draussen.
  if (typeof istDorf === "function" && aktWelt && aktWelt.heim) {
    const drin = fertig.filter(p => istDorf(p[0], p[1]));
    if (drin.length) fertig = drin;                          // nur wenn ueberhaupt etwas drin liegt
  }
  lieblingsorte = fertig;
}
function wegSuchen(von, nach) {
  const [sx, sy] = von, [zx, zy] = nach, B = KARTE.breite;
  if (!frei4(zx, zy)) return null;
  const vorher = new Map([[sy * B + sx, -1]]), schlange = [[sx, sy]];
  while (schlange.length) {
    const [x, y] = schlange.shift();
    if (x === zx && y === zy) {
      const weg = []; let k = y * B + x;
      while (k !== -1 && k !== sy * B + sx) { weg.unshift(zellMitte(k % B, Math.floor(k / B))); k = vorher.get(k); }
      return weg;
    }
    for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
      const nx = x + dx, ny = y + dy, key = ny * B + nx;
      if (!vorher.has(key) && frei4(nx, ny)) { vorher.set(key, y * B + x); schlange.push([nx, ny]); }
    }
  }
  return null;
}
// Arbeit statt Spazieren: wer eine Aufgabe hat, geht dorthin, wo man sie sieht - zum Kiesel,
// zum Baum, zum eigenen Bauplatz. Der Ertrag selbst wird beim Tagesende verrechnet (dorf.js).
function npcArbeitsziel(n) {
  if (!aktWelt || !aktWelt.heim || !stand || typeof wert !== "function" || istNacht()) return null;
  if (typeof leute !== "function" || !leute().includes(n.id)) return null;
  const w = wert(n.id);
  if (!w || w.tot) return null;
  const a = w.aufgabe;
  const hier = zelleVon(n.x, n.y);
  const bau = typeof bauStand === "function" ? bauStand(n.id) : null;
  const wege = [];
  // 0. Heilen heisst: bei den Kranken sein. Fuer "heilen" gab es hier gar kein Arbeitsziel -
  // der Druide fiel durch alle Zweige und streifte durchs Dorf, waehrend er im Gespraech
  // sagte "Ich sitze bei den Kranken". Jetzt steht er wirklich dort: am Krankenlager, und
  // solange keines steht, vor der Tuer, hinter der die Verwundeten liegen.
  if (a === "heilen" && typeof liegende === "function" && liegende().length
      && typeof hausTuer === "function") {
    // Krankenlager, sonst dein Haus, sonst irgendein Dach - Hauptsache, er ist bei ihnen.
    // Steht gar nichts, bleibt er in der Naehe der Mitte statt ziellos umherzulaufen.
    let tuer = hausTuer(typeof LAGER_ID !== "undefined" ? LAGER_ID : "lager") || hausTuer("spieler");
    if (!tuer && welt && welt.tueren && welt.tueren.length) tuer = welt.tueren[0];
    if (tuer) {
      const zx = Math.floor(tuer.mitteX / TK), zy = Math.floor(tuer.vorY / TK);
      // Ein Platz neben der Tuer - mehrere Druiden sollen nicht uebereinander stehen
      const platz = [[0, 0], [1, 0], [-1, 0], [2, 0], [-2, 0], [0, 1], [1, 1], [-1, 1]]
        .map(([dx, dy]) => [zx + dx, zy + dy]).find(([x, y]) => frei4(x, y));
      if (platz) wege.push(["heilen", platz, 12]);         // schwer gewichtet: dorthin gehoert er
    }
  }
  // 1. Eigenes Haus im Bau: davor stellen und hämmern
  if (bau && bau.platz && !bau.fertig) {
    const zx = bau.platz.x + 1, zy = bau.platz.y + HH;
    if (frei4(zx, zy)) wege.push(["bauen", [zx, zy], a === "bauen" ? 3 : 1]);
  }
  // 2. Kiesel einsammeln (was heute schon geholt wurde, liegt nicht mehr da)
  if (!a || a === "sammeln") {
    const weg2 = gepflueckt();
    const kiesel = welt.pflanzen.filter(p => p.ding === "kiesel" && !weg2.includes(`${p.x},${p.y}`));
    const nah = kiesel.sort((p, q) => Math.hypot(p.x * TK - n.x, p.y * TK - n.y) - Math.hypot(q.x * TK - n.x, q.y * TK - n.y))[0];
    if (nah) {
      const platz = [[0, 1], [0, -1], [1, 0], [-1, 0]].map(([dx, dy]) => [nah.x + dx, nah.y + dy]).find(([x, y]) => frei4(x, y));
      if (platz) wege.push(["kiesel", platz, 3]);
    }
  }
  // 3. Jede Aufgabe hat ihren eigenen Ort. Vorher endeten Jagen, Farmen und Kraeutersuchen
  // alle drei am selben "irgendein Baum" - und zwar an einem zufaellig gewaehlten, quer ueber
  // die Karte (gemessen: 23 Wegfelder im Mittel). Schmieden und Zaehmen hatten gar kein Ziel,
  // ihre Leute liefen den ganzen Tag zufaellig umher. Darum: naechstgelegenes statt
  // zufaelliges Ziel, und fuer jede Aufgabe der Ort, an den sie gehoert.
  const naechstes = (liste, hol) => {
    let best = null, weit = Infinity;
    for (const e of liste || []) {
      const [ex, ey] = hol(e);
      const d2 = Math.hypot(ex * TK - n.x, ey * TK - n.y);
      if (d2 < weit) { weit = d2; best = e; }
    }
    return best;
  };
  const platzNeben = (x, y, ringe = [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [-1, 1]]) =>
    ringe.map(([dx, dy]) => [x + dx, y + dy]).find(([px, py]) => frei4(px, py));

  // 3a. Farmen: zum Acker. Er reift in der Tagesabrechnung (ackerBestellen) - hier wird
  // nur sichtbar, dass jemand dort arbeitet. Kein Ertrag, sonst zaehlte er doppelt.
  if (a === "farmen" && typeof aecker === "function") {
    const ziel = naechstes(aecker(), e => [e.x, e.y]);
    if (ziel) { const pl = platzNeben(ziel.x, ziel.y); if (pl) wege.push(["acker", pl, 10]); }
  }
  // 3b. Kraeuter: zu einem Heilkraut, nicht zu irgendeiner Blume
  if (a === "kraeuter" && typeof istKraut === "function") {
    const weg3 = gepflueckt();
    const kraut = naechstes((welt.pflanzen || []).filter(q => istKraut(q.ding) && !weg3.includes(`${q.x},${q.y}`)),
                            e => [e.x, e.y]);
    if (kraut) { const pl = platzNeben(kraut.x, kraut.y); if (pl) wege.push(["kraut", pl, 10]); }
  }
  // 3c. Jagen: ans Wasser, wo die Fische sind - sonst an den Waldrand
  if (a === "jagen" && typeof wasserBei === "function") {
    let ufer = null, weit = Infinity;
    for (let y = 4; y < KARTE.hoehe - 2 && !ufer; y++) for (let x = 1; x < KARTE.breite - 1; x++) {
      if (!wasserBei(x, y)) continue;
      const pl = platzNeben(x, y);
      if (!pl) continue;
      const d2 = Math.hypot(pl[0] * TK - n.x, pl[1] * TK - n.y);
      if (d2 < weit) { weit = d2; ufer = pl; }
    }
    if (ufer) wege.push(["angeln", ufer, 10]);
  }
  // 3d. Zaehmen: zu einem Tier
  if (a === "zaehmen" && typeof TIERE !== "undefined") {
    const tier = naechstes((welt.pflanzen || []).filter(q => TIERE.includes(q.ding)), e => [e.x, e.y]);
    if (tier) { const pl = platzNeben(tier.x, tier.y); if (pl) wege.push(["tier", pl, 10]); }
  }
  // 3e. Schmieden: an die eigene Tuer - dort steht der Amboss. Ohne Haus in die Dorfmitte.
  if (a === "schmieden") {
    let pl = null;
    const tuer = typeof hausTuer === "function" ? hausTuer(n.id) : null;
    if (tuer) pl = platzNeben(Math.floor(tuer.mitteX / TK), Math.floor(tuer.vorY / TK), [[0, 0], [1, 0], [-1, 0], [2, 0]]);
    if (!pl && KARTE.start) pl = platzNeben(KARTE.start.x, KARTE.start.y, [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [2, 0], [-2, 0]]);
    if (pl) wege.push(["schmieden", pl, 10]);
  }
  // 3f. Bauen heisst bauen: an JEDE unfertige Baustelle im Dorf, nicht nur an die eigene.
  // Gemessen lief ein Bauer zu 100 Prozent Kiesel sammeln, weil sein eigenes Haus stand.
  if (a === "bauen" && typeof stand.bau === "object") {
    const offen = Object.entries(stand.bau)
      .filter(([, b2]) => b2 && b2.platz && !b2.fertig)
      .map(([, b2]) => b2.platz);
    const ziel = naechstes(offen, e => [e.x + 1, e.y + HH]);
    if (ziel && frei4(ziel.x + 1, ziel.y + HH)) wege.push(["bauen", [ziel.x + 1, ziel.y + HH], 10]);
  }
  // 3g. Baeume: fuer alle, die sonst nichts zu tun haben - aber der naechste, nicht irgendeiner
  if (!wege.length) {
    const baum = naechstes(welt.baeume || [], e => [e.x, e.y]);
    if (baum) {
      const platz = [[2, 2], [1, 2], [3, 2], [2, 3]].map(([dx, dy]) => [baum.x + dx, baum.y + dy]).find(([x, y]) => frei4(x, y));
      if (platz) wege.push(["baum", platz, 2]);
    }
  }
  if (!wege.length) return null;
  const gesamt = wege.reduce((sum, e) => sum + e[2], 0);    // gewichtet würfeln
  let wurf = Math.random() * gesamt;
  const [tat, ziel2] = wege.find(e => (wurf -= e[2]) <= 0) || wege[0];
  const weg = wegSuchen(hier, ziel2);
  if (!weg) return null;
  n.tat = tat;
  return weg;
}
// Am Ziel angekommen: die Arbeit wird sichtbar
function npcTat(n, t) {
  const tat = n.tat;
  n.tat = null;
  if (!tat || !stand) return;
  const nah = Math.hypot(spieler.x - n.x, spieler.y - n.y) < 160;   // nur was man sieht, hört man auch
  if (tat === "kiesel") {
    const p = welt.pflanzen.find(q => q.ding === "kiesel" && Math.hypot(q.x * TK + 8 - n.x, q.y * TK + 8 - n.y) < 28
                                      && !gepflueckt().includes(`${q.x},${q.y}`));
    if (!p) return;
    gepflueckt().push(`${p.x},${p.y}`);
    if (typeof dorf === "function" && dorf()) dorf().vorrat.stein += 1;
    if (nah) geraeusch("kiesel");
    n.emote = "punkte"; n.emoteBis = t + 1400;
    speichernBald();
  } else if (tat === "baum") {
    const b = (welt.baeume || []).find(q => Math.hypot(q.stammX - n.x, q.stammY - 6 - n.y) < 34);
    if (!b) return;
    b.wackelBis = t + 700;
    if (nah) geraeusch("rascheln");
    n.emote = "note"; n.emoteBis = t + 1400;
  } else if (tat === "bauen") {
    if (nah) geraeusch("haemmern");
    n.emote = "punkte"; n.emoteBis = t + 1600;
    n.blick = "N";
  } else if (tat === "acker") {
    // Der Acker reift in der Tagesabrechnung (ackerBestellen). Hier wird nur sichtbar, dass
    // jemand ihn bestellt - haette die Tat auch Ertrag, zaehlte der Tag doppelt.
    if (nah) geraeusch("rascheln");
    n.emote = "punkte"; n.emoteBis = t + 1600;
    n.blick = "S";
  } else if (tat === "kraut") {
    if (nah) geraeusch("rascheln");
    n.emote = "note"; n.emoteBis = t + 1400;
    n.blick = "S";
  } else if (tat === "angeln") {
    if (nah) geraeusch("wasser");
    n.emote = "punkte"; n.emoteBis = t + 2000;
    n.warte = 3000 + Math.random() * 3000;                 // am Wasser bleibt man laenger
    return;
  } else if (tat === "tier") {
    if (nah) geraeusch("rascheln");
    n.emote = "herz"; n.emoteBis = t + 1600;
  } else if (tat === "schmieden") {
    if (nah) geraeusch("haemmern");
    n.emote = "punkte"; n.emoteBis = t + 1800;
    n.blick = "N";
    n.warte = 2600 + Math.random() * 2600;                 // am Amboss bleibt man dabei
    return;
  }
  n.warte = 1400 + Math.random() * 1600;                   // kurz dabeibleiben
}

// Heimweg: Wer ein fertiges Haus hat, geht auch mal hin - je spaeter am Tag, desto eher.
// Bisher streiften alle bis zum Abend ueber die Lichtung, auch wenn ihr eigenes Haus zwei
// Schritte weiter stand; heimgegangen wurde nur unsichtbar, per Feierabend (aufenthalt()).
function npcHeimziel(n) {
  if (!welt || !welt.tueren || typeof bauStand !== "function"
      || typeof HAEUSER !== "object" || !HAEUSER[n.id]) return null;
  if (typeof dorfVon === "function" && dorfVon(n.id)) return null;   // Fremde haben ihr eigenes Land
  const b = bauStand(n.id);
  if (!b.fertig || !b.platz) return null;
  const tuer = welt.tueren.find(q => q.schluessel === n.id);
  if (!tuer) return null;
  const hier = zelleVon(n.x, n.y), vor = zelleVon(tuer.mitteX, tuer.vorY);
  if (Math.abs(hier[0] - vor[0]) <= 1 && Math.abs(hier[1] - vor[1]) <= 1) return null;  // steht schon davor
  const spaet = typeof tagFortschritt === "function" ? tagFortschritt() : 0.5;
  if (Math.random() > 0.06 + spaet * 0.44) return null;    // morgens selten, abends fast immer
  const weg = wegSuchen(hier, vor);
  if (weg) n.heimweg = true;
  return weg;
}

function neuesZiel(n) {
  if (!lieblingsorte) lieblingsorteFinden();
  const arbeit = npcArbeitsziel(n);                        // erst die Arbeit, dann der Heimweg
  if (arbeit) return arbeit;
  const heim = npcHeimziel(n);
  if (heim) return heim;
  let ziel;
  const fremdesDorf = typeof dorfVon === "function" ? dorfVon(n.id) : null;
  if (fremdesDorf) {                                       // Leute fremder Dörfer bleiben bei sich
    const felderN = fremdesDorf.felder;
    for (let i = 0; i < 20 && !ziel; i++) {
      const [px, py] = felderN[Math.floor(Math.random() * felderN.length)].split(",").map(Number);
      const x = px * PARZ + Math.floor(Math.random() * PARZ), y = py * PARZ + Math.floor(Math.random() * PARZ);
      if (frei4(x, y)) ziel = [x, y];
    }
    return ziel ? wegSuchen(zelleVon(n.x, n.y), ziel) : null;
  }
  // Ohne Arbeit und ohne Heimweg: ein Lieblingsort, sonst ein Fleck IM Dorf. Vorher wurde
  // die ganze Karte ausgewuerfelt - im Mittel 15 Kacheln weit und zu zwei Dritteln ausserhalb
  // des Dorfes. Das sah aus, als liefen alle kreuz und quer.
  if (Math.random() < 0.55) ziel = eine(lieblingsorte);
  if (!ziel) for (let i = 0; i < 25 && !ziel; i++) {
    const x = 1 + Math.floor(Math.random() * (KARTE.breite - 2)), y = 6 + Math.floor(Math.random() * (KARTE.hoehe - 7));
    if (!frei4(x, y)) continue;
    if (typeof istDorf === "function" && aktWelt && aktWelt.heim && !istDorf(x, y)) continue;
    ziel = [x, y];
  }
  // Gar kein Platz im Dorf gefunden: dann doch irgendwohin, sonst steht die Figur still
  if (!ziel) for (let i = 0; i < 12 && !ziel; i++) {
    const x = 1 + Math.floor(Math.random() * (KARTE.breite - 2)), y = 6 + Math.floor(Math.random() * (KARTE.hoehe - 7));
    if (frei4(x, y)) ziel = [x, y];
  }
  return ziel ? wegSuchen(zelleVon(n.x, n.y), ziel) : null;
}

function npcBewegen(n, dt, nah, t) {
  const vorher = n.bewegt;
  // Wer dich begleitet, bleibt an deiner Seite, statt eigene Wege zu gehen
  if (n.folgt) {
    const dx2 = spieler.x - n.x, dy2 = spieler.y - n.y, weit2 = Math.hypot(dx2, dy2);
    if (weit2 > 26) {
      const s2 = NPC_TEMPO * 1.35 * dt / 1000;
      const nx = n.x + dx2 / weit2 * s2, ny = n.y + dy2 / weit2 * s2;
      if (begehbar(nx, n.y, n)) n.x = nx;
      if (begehbar(n.x, ny, n)) n.y = ny;
      n.bewegt = true;
      n.blick = Math.abs(dx2) > Math.abs(dy2) ? (dx2 > 0 ? "O" : "W") : (dy2 > 0 ? "S" : "N");
    } else n.bewegt = false;
    n.weg = null;
    if (n.bewegt !== vorher) { n.bild = 0; n.seit = 0; }
    n.seit += dt;
    if (n.bewegt && n.seit >= GEHEN.zeiten[n.bild]) { n.seit = 0; n.bild = (n.bild + 1) % GEHEN.spalten.length; }
    return;
  }
  if (nah || (gespraech && gespraech.npc === n)) {
    n.bewegt = false;
    n.blick = Math.abs(spieler.x - n.x) > Math.abs(spieler.y - n.y) ? (spieler.x > n.x ? "O" : "W") : (spieler.y > n.y ? "S" : "N");
  } else if (n.weg && n.weg.length) {
    const p = n.weg[0], ex = p.x - n.x, ey = p.y - n.y, weit = NPC_TEMPO * dt / 1000;
    if (Math.hypot(ex, ey) < 1.5) {
      n.weg.shift();
      if (!n.weg.length) {
        n.bewegt = false;
        // Daheim bleibt man eine Weile, statt sofort wieder loszulaufen
        n.warte = n.heimweg ? 9000 + Math.random() * 9000 : 1500 + Math.random() * 4500;
        if (n.heimweg) { n.heimweg = false; n.blick = "S"; }
        if (n.tat) npcTat(n, t); else if (Math.random() < 0.3) n.blick = "S";
      }
    } else {
      const sx = Math.abs(ex) > 0.5 ? Math.sign(ex) : 0, sy = Math.abs(ey) > 0.5 ? Math.sign(ey) : 0;
      const kx = Math.min(weit, Math.abs(ex)), ky = Math.min(weit, Math.abs(ey));
      const okX = sx && begehbar(n.x + sx * kx, n.y, n), okY = sy && begehbar(n.x, n.y + sy * ky, n);
      if (okX) n.x += sx * kx;
      if (okY) n.y += sy * ky;
      if (okX || okY) { n.stau = 0; n.bewegt = true; n.blick = Math.abs(ex) > Math.abs(ey) ? (ex > 0 ? "O" : "W") : (ey > 0 ? "S" : "N"); }
      else {                                               // jemand steht im Weg: kurz warten, dann neu planen
        n.bewegt = false; n.stau = (n.stau || 0) + dt;
        if (n.stau > 800) { n.stau = 0; n.weg = null; n.warte = 200 + Math.random() * 600; }
      }
    }
  } else {
    n.bewegt = false;
    n.warte -= dt;
    if (n.warte <= 0) { n.tat = null; n.weg = neuesZiel(n); n.warte = n.weg ? 0 : 400; }
  }
  if (n.bewegt !== vorher) { n.bild = 0; n.seit = 0; }
  n.seit += dt;
  if (n.bewegt && n.seit >= GEHEN.zeiten[n.bild]) { n.seit = 0; n.bild = (n.bild + 1) % GEHEN.spalten.length; }
  // Kleine Gedankenblasen: Note, Punkte, Herz (gute Freunde in der Naehe)
  n.naechstesEmote -= dt;
  if (n.naechstesEmote <= 0 && !gespraech) {
    const freund = (stand.freundschaft[n.id] || 0) >= 5;
    n.emote = nah && freund ? "herz" : ["note", "note", "punkte"][Math.floor(Math.random() * 3)];
    n.emoteBis = t + 2200;
    n.naechstesEmote = 15000 + Math.random() * 25000;
  }
}

// ---------------------------------------------------------------------------
// Schlafen legen - so endet der Tag
// ---------------------------------------------------------------------------
// Im Dorfbuch gibt es kein "Tag beenden" mehr: Wer das Buch aufschlaegt, will nachsehen
// und einteilen, nicht die Nacht starten - ein Enter aus Gewohnheit hat sonst den Tag
// weggeworfen. Stattdessen legt man sich hin. Wer ein eigenes Bett hat, benutzt es
// (Leertaste in der Stube); wer keines hat, legt sich ins Gras, und das kostet.
function schlafenDraussen() {
  if (typeof innen !== "undefined" && innen)
    return meldung(t("Drinnen legst du dich in dein Bett."), 2600);
  if (typeof istNacht === "function" && istNacht()) return meldung(t("Es ist schon Nacht."), 2000);
  const d = typeof dorf === "function" ? dorf() : null;
  if (!d || d.ende) return;
  const eigenesBettDa = typeof hausTuer === "function" && !!hausTuer("spieler")
                        && typeof bauStand === "function" && bauStand("spieler").fertig;
  const satz = eigenesBettDa
    ? t("Hier im Freien schlafen? Dein Bett steht drinnen – dort schläfst du sicherer.")
    : t("Dich ins Gras legen? Ohne Dach über dem Kopf bist du der Nacht ausgeliefert.");
  frage(satz, [
    { text: t("Schlafen"), hell: true, tun: () => { if (typeof tagBeenden === "function") tagBeenden(); } },
    { text: t("Noch nicht"), tun: () => {} },
  ]);
}

// Emote-Blasen als Pixelbilder (5x5 innen), damit sie zur Grafik passen
const EMOTES = {
  note:   ["..XX.", "..X.X", "..X..", "XXX..", "XXX.."],
  punkte: [".....", ".....", "X.X.X", ".....", "....."],
  herz:   [".X.X.", "XXXXX", "XXXXX", ".XXX.", "..X.."],
  ausruf: ["..X..", "..X..", "..X..", ".....", "..X.."],
};
function zeichneEmote(c, n, t) {
  if (!n.emote || t > n.emoteBis) return;
  const x = Math.round(n.x + 3), y = Math.round(n.y - 40);
  c.fillStyle = "#1e120a"; c.fillRect(x - 1, y - 1, 9, 9); c.fillRect(x, y + 7, 2, 2);
  c.fillStyle = "#f6ead0"; c.fillRect(x, y, 7, 7);
  c.fillStyle = n.emote === "herz" || n.emote === "ausruf" ? "#c83c50" : "#3b2a1a";
  EMOTES[n.emote].forEach((z, j) => [...z].forEach((p, i) => p === "X" && c.fillRect(x + 1 + i, y + 1 + j, 1, 1)));
}

// ---------------------------------------------------------------------------
// Gespraeche: Seiten mit Tipp-Effekt und Plapperstimme, optional eine Auswahl
// ---------------------------------------------------------------------------
let gespraech = null;
const dialogEl = document.getElementById("dialog");
const text = t => t.replaceAll("{name}", stand.name);
const eine = liste => liste[Math.floor(Math.random() * liste.length)];

const SPRECHWEITE = 40;                          // px auf der Karte, gut zwei Kacheln
function naechsterNpc() {
  const [bx, by] = { S: [0, 1], N: [0, -1], O: [1, 0], W: [-1, 0] }[spieler.blick];
  let best = null, wert = Infinity;
  for (const n of wach()) {
    const dx = n.x - spieler.x, dy = n.y - spieler.y, a = Math.hypot(dx, dy);
    if (a > SPRECHWEITE) continue;
    const vorne = (dx * bx + dy * by) / (a || 1);                // 1 = genau in Blickrichtung
    const w2 = a - vorne * 12;
    if (w2 < wert) { best = n; wert = w2; }
  }
  return best;
}

function dialogStart(npc, seiten, { wahl = null, danach = null } = {}) {
  gespraech = { npc, seiten: seiten.map(text), seite: 0, getippt: 0, tippSeit: performance.now(), wahl, wahlIndex: 0, danach };
  tasten.clear(); spieler.bewegt = false;
  // Name: mitgegeben (Szenen), sonst aus dem Dorf, sonst aus den festen Leuten
  const wieHeisst = q => q && q.name ? q.name
    : q && typeof person2 === "function" && typeof leute === "function" && leute().includes(q.id) ? person2(q.id).name
    : q ? (person(q.id) || {}).name || t("Jemand") : t("Die Lichtung");
  // Der Kopf traegt die Farbe des Sprechers: rot, wenn du selbst redest
  const kopf = document.getElementById("dname");
  kopf.textContent = wieHeisst(npc);
  const selbst = npc && (npc.id === "spieler"
    || (typeof stand !== "undefined" && stand && npc.name === stand.name));
  kopf.classList.toggle("n-pc", !!selbst);
  kopf.classList.toggle("n-npc", !selbst && !!npc);
  dialogEl.hidden = false;
}
function weiterReden() {
  const g = gespraech;
  const seite = g.seiten[g.seite];
  if (g.getippt < seite.length) { g.getippt = seite.length; return; }        // erst ganz zeigen
  const letzte = g.seite === g.seiten.length - 1;
  if (letzte && g.wahl) {
    const w = g.wahl[g.wahlIndex];
    const mehr = w.tun ? w.tun() : null;                       // weitere Seiten, optional mit neuer Auswahl
    const neu = Array.isArray(mehr) ? { seiten: mehr } : mehr;
    if (neu && neu.seiten && neu.seiten.length) {
      Object.assign(g, { seiten: neu.seiten.map(text), seite: 0, getippt: 0, tippSeit: performance.now(), wahl: neu.wahl || null, wahlIndex: 0 });
      return;
    }
    return gespraechEnde();
  }
  if (letzte) return gespraechEnde();
  g.seite += 1; g.getippt = 0; g.tippSeit = performance.now();
}
function wahlBewegen(d) {
  const g = gespraech;
  if (!g || !g.wahl || g.seite !== g.seiten.length - 1 || g.getippt < g.seiten[g.seite].length) return false;
  g.wahlIndex = (g.wahlIndex + d + g.wahl.length) % g.wahl.length;
  return true;
}
function gespraechEnde() {
  const g = gespraech;
  gespraech = null;
  dialogEl.hidden = true;
  if (g && g.danach) g.danach();
}
// ---------------------------------------------------------------------------
// Namen in Gespraechen einfaerben
//
// Wer gerade genannt wird, soll ins Auge fallen: dein eigener Name rot, die der Bewohner
// navyblau. Das hilft in Saetzen wie "Hara hat Lumo verarztet" sofort zu sehen, wer gemeint
// ist - und es zeigt auf einen Blick, wann von DIR die Rede ist.
//
// Der Text laeuft schreibmaschinenartig ein (textContent, Zeichen fuer Zeichen). Fuer Farbe
// braucht es HTML, also wird nur der bereits sichtbare Teil ausgegeben und darin gefaerbt.
// Alles andere wird maskiert - ein Name mit spitzer Klammer wuerde die Anzeige sonst
// zerlegen, und Namen kommen aus einer Zufallsliste.
const maskiere = s2 => String(s2).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

// Alle Namen, die gerade vorkommen koennen, mit ihrer Farbrolle. Die laengsten zuerst -
// sonst faerbt "Ada" die ersten drei Buchstaben von "Adalgar".
function namensListe() {
  const liste = [];
  if (typeof stand !== "undefined" && stand && stand.name) liste.push({ name: stand.name, rolle: "pc" });
  if (typeof leute === "function" && typeof person2 === "function" && typeof dorf === "function" && dorf()) {
    for (const id of leute()) {
      if (id === "spieler") continue;
      const p = person2(id);
      if (p && p.name) liste.push({ name: p.name, rolle: "npc" });
    }
  }
  if (typeof PERSONEN === "object") for (const p of Object.values(PERSONEN))
    if (p && p.name && !liste.some(x => x.name === p.name)) liste.push({ name: p.name, rolle: "npc" });
  return liste.sort((a, b) => b.name.length - a.name.length);
}

// Namen im Text einfaerben. Gesucht wird auf Wortgrenzen, damit "Ada" nicht mitten in
// "Adams" trifft; Umlaute zaehlen dabei als Buchstaben.
function namenFaerben(text) {
  const liste = namensListe();
  if (!liste.length) return maskiere(text);
  const istBuchstabe = z => z && /[A-Za-zÄÖÜäöüß0-9]/.test(z);
  let aus = "", i = 0;
  while (i < text.length) {
    let treffer = null;
    for (const n of liste) {
      if (!text.startsWith(n.name, i)) continue;
      if (istBuchstabe(text[i - 1]) || istBuchstabe(text[i + n.name.length])) continue;
      treffer = n; break;
    }
    if (treffer) {
      aus += `<span class="n-${treffer.rolle}">${maskiere(treffer.name)}</span>`;
      i += treffer.name.length;
    } else { aus += maskiere(text[i]); i++; }
  }
  return aus;
}

let letzteWahl = "";
function dialogZeichnen() {
  const g = gespraech;
  if (!g) return;
  const seite = g.seiten[g.seite];
  const vorher = g.getippt;
  const reduziert = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  g.getippt = reduziert ? seite.length : Math.max(g.getippt, Math.min(seite.length, Math.floor((performance.now() - g.tippSeit) / 28)));
  if (g.getippt > vorher && g.npc) plappern(g.npc.id, seite.slice(vorher, g.getippt));
  document.getElementById("dtext").innerHTML = namenFaerben(seite.slice(0, g.getippt));
  const fertig = g.getippt >= seite.length, letzte = g.seite === g.seiten.length - 1;
  const zeigeWahl = fertig && letzte && !!g.wahl;
  const wahlEl = document.getElementById("dwahl");
  wahlEl.hidden = !zeigeWahl;
  const schluessel = zeigeWahl ? g.wahl.map(w => w.text).join("|") + g.wahlIndex : "";
  if (zeigeWahl && schluessel !== letzteWahl) {
    wahlEl.innerHTML = "";
    g.wahl.forEach((w, i) => { const d = document.createElement("div"); d.innerHTML = namenFaerben(w.text); d.className = i === g.wahlIndex ? "an" : ""; wahlEl.append(d); });
  }
  letzteWahl = schluessel;
  const w = document.getElementById("dweiter");
  w.textContent = !fertig ? "" : zeigeWahl ? t("↑↓ wählen · T") : letzte ? t("T Ende") : t("T ▼");
  w.classList.toggle("an", fertig && !letzte);
}

// Plapperstimme: kurze Toene je Buchstabe, Tonhoehe je Figur
let audio = null;
function plappern(id, neu) {
  if (!klangFrei || stumm || !LAUT.klang) return;
  try { audio = audio || new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
  const basis = 170 + hash(id) % 220;
  [...neu].forEach((z, i) => {
    if (!/[a-zäöüß]/i.test(z) || (z.charCodeAt(0) + i) % 2) return;
    const o = audio.createOscillator(), v = audio.createGain(), jetztA = audio.currentTime + i * 0.012;
    o.type = "triangle";
    o.frequency.value = basis * (1 + (z.toLowerCase().charCodeAt(0) % 9) / 18);
    v.gain.setValueAtTime(0.0001, jetztA);
    v.gain.exponentialRampToValueAtTime(0.05 * LAUT.klang, jetztA + 0.01);
    v.gain.exponentialRampToValueAtTime(0.0001, jetztA + 0.06);
    o.connect(v).connect(audio.destination);
    o.start(jetztA); o.stop(jetztA + 0.07);
  });
}

// Was ein NPC sagt: erste Begegnung, Alltag (taeglich wechselnd), Freundschaft, nochmal am
// selben Tag, nachts ein Satz vorweg, Geburtstag. Danach Tauschen oder Schenken zur Auswahl.
const GEBURTSTAGSSAETZE = ["Alles Gute zum Geburtstag, {name}!", "Heute ist dein Tag, {name}! Herzlichen Glückwunsch!",
  "Geburtstag? Dann feiern wir! Alles Liebe, {name}!"];
const GEBURTSTAGSGABE = { mila: "kranz", jonas: "vogelhaus" };
const KLEINE_GABEN = { mila: BLUMEN, jonas: ["ast", "eichel", "kiesel"] };
const LIEBLINGS_GABE = { mila: "strauss", jonas: "floete" };      // Gegengeschenk fuer ein Lieblingsding
// Was sie besonders gern bekommen (Geschenk zaehlt dreifach)
const LIEBLINGE = { mila: [...BLUMEN, "kranz", "strauss", "kette"], jonas: [...FISCHE, "floete", "vogelhaus", "kraeuterfisch", "eichel"] };
// Tauschangebote: eines pro Tag und Person, fest aus Datum und Name
const TAUSCH = {
  mila:  [{ will: ["kiesel", 3], gibt: ["lavendel", 2] }, { will: ["ast", 3], gibt: ["rosa", 3] },
          { will: ["rotauge", 1], gibt: ["kranz", 1] }, { will: ["floete", 1], gibt: ["strauss", 1] },
          { will: ["eichel", 2], gibt: ["ringel", 3] }],
  jonas: [{ will: ["rosa", 2], gibt: ["ast", 4] }, { will: ["kranz", 1], gibt: ["eichel", 3] },
          { will: ["kiesel", 2], gibt: ["barsch", 1] }, { will: ["gaense", 3], gibt: ["angel", 1] },
          { will: ["lavendel", 2], gibt: ["kiesel", 3] }],
};
const angebotHeute = n => { const l = TAUSCH[n.id]; return l ? l[(spielTagNummer() + hash(n.id)) % l.length] : null; };

// Aufstieg sichtbar machen: ein kleines Schild steigt ueber dem Kopf auf und verblasst
let aufstiege = [];
function aufstiegZeigen(id, text2) {
  const n = (kartenNpcs || []).find(q => q.id === id);
  const x = id === "spieler" ? spieler.x : n ? n.x : null;
  const y = id === "spieler" ? spieler.y : n ? n.y : null;
  if (x === null) return;
  aufstiege.push({ x, y, text: text2, t0: performance.now() });
  geraeusch("aufstieg");
}
function aufstiegeZeichnen(c, t) {
  aufstiege = aufstiege.filter(a => t - a.t0 < 1800);
  for (const a of aufstiege) {
    const anteil = (t - a.t0) / 1800;
    c.globalAlpha = Math.max(0, 1 - anteil);
    const y = Math.round(a.y - 40 - anteil * 14);
    c.fillStyle = "#1a120c";
    c.fillRect(Math.round(a.x) - 22, y - 8, 44, 11);
    c.fillStyle = "#e8b84a";
    c.fillRect(Math.round(a.x) - 21, y - 7, 42, 9);
    c.fillStyle = "#3b2a1a";
    c.font = "8px monospace"; c.textAlign = "center";
    c.fillText(a.text, Math.round(a.x), y);
    c.globalAlpha = 1;
  }
  c.textAlign = "left";
}

// ---------------------------------------------------------------------------
// Szenen: gescriptete Auftritte. Die Figuren gehen von selbst an ihre Plaetze und reden
// nacheinander - du schaust zu. Schritte: { geh: [id, x, y] } · { sagt: [id, "..."] }
// { warte: ms } · { tun: fn }. Laeuft in weltSchritt mit.
// ---------------------------------------------------------------------------
let szene = null;                        // { schritte, nr, bis }
function szeneLaeuft() { return !!szene; }
// ---------------------------------------------------------------------------
// Szenenbild: Abblende am Anfang und Ende, Kamera auf die Beteiligten, Gesten
// ---------------------------------------------------------------------------
// Beschluss V3: die Szenen sollen sich vom normalen Spiel absetzen. Die Kamera faehrt weich
// auf den, der gerade dran ist; Anfang und Ende blenden ab; Figuren koennen nicken, den Kopf
// senken oder sich zuwenden, statt nur zu gehen und zu reden.
let szeneKamera = null;                 // { x, y } - wohin die Kamera weich zieht
let szeneBlende = 0;                    // 0 = klar, 1 = ganz schwarz
let szeneBlendeZiel = 0;
const SZENE_BLENDE_TEMPO = 0.05;        // je Bild
// Gesten laufen ueber dem Kopf der Figur und dauern kurz
let gesten = [];                        // { id, art, t0, dauer }
const GESTE_DAUER = { nicken: 700, kopfsenken: 1100, zuwenden: 400, aufblicken: 800 };
function gesteZeigen(id, art) {
  gesten = gesten.filter(g => g.id !== id);
  gesten.push({ id, art, t0: performance.now(), dauer: GESTE_DAUER[art] || 700 });
}
const gesteVon = id => gesten.find(g => g.id === id && performance.now() - g.t0 < g.dauer) || null;
// Wie weit eine Geste fortgeschritten ist (0..1) - die Zeichnung verschiebt danach den Koerper
function gesteVersatz(id) {
  const g = gesteVon(id);
  if (!g) return 0;
  const k = (performance.now() - g.t0) / g.dauer;
  if (g.art === "nicken") return Math.sin(k * Math.PI * 2) * 2;        // zweimal kurz senken
  if (g.art === "kopfsenken") return Math.sin(Math.min(1, k) * Math.PI) * 3;
  if (g.art === "aufblicken") return -Math.sin(Math.min(1, k) * Math.PI) * 2;
  return 0;
}
function szeneBlendeSchritt() {
  if (szeneBlende < szeneBlendeZiel) szeneBlende = Math.min(szeneBlendeZiel, szeneBlende + SZENE_BLENDE_TEMPO);
  else if (szeneBlende > szeneBlendeZiel) szeneBlende = Math.max(szeneBlendeZiel, szeneBlende - SZENE_BLENDE_TEMPO);
}
// Die Kamera zieht weich zum Szenenziel, statt hart zu springen
function szeneKameraSchritt() {
  if (!szeneKamera) return;
  const nah = (a, b) => a + (b - a) * 0.12;
  szeneKamera.jetztX = nah(szeneKamera.jetztX === undefined ? szeneKamera.x : szeneKamera.jetztX, szeneKamera.x);
  szeneKamera.jetztY = nah(szeneKamera.jetztY === undefined ? szeneKamera.y : szeneKamera.jetztY, szeneKamera.y);
}

function szeneStarten(schritte, fertig = null) {
  if (szene) return;
  // Nicht mitten im Kampf und nicht nachts: die Szene legte sich sonst ueber das Kampfbild,
  // zwei Gespraeche liefen uebereinander, und die Tastatur gehoerte beiden gleichzeitig.
  if (typeof kampf !== "undefined" && kampf) return false;
  if (typeof dorf === "function" && dorf() && dorf().phase === "nacht") return false;
  szene = { schritte, nr: 0, bis: 0, fertig };
  tasten.clear(); spieler.bewegt = false;
  szeneWeiter();
}
function szeneWeiter() {
  if (!szene) return;
  const s2 = szene.schritte[szene.nr];
  if (!s2) {
    // Kamera und Blende freigeben. Ohne das trug die naechste Szene die Kameraposition der
    // vorigen als Startpunkt weiter (`szeneKamera ? szeneKamera.jetztX : ...`) und fuhr quer
    // ueber die Karte heran, statt weich am Ort einzusetzen.
    szeneKamera = null; szeneBlendeZiel = 0;
    const f = szene.fertig; szene = null; if (f) f(); return;
  }
  if (s2.geh) {
    const [id, zx, zy] = s2.geh;
    const n = kartenNpcs.find(q => q.id === id);
    if (!n) { szene.nr++; return szeneWeiter(); }            // wer fehlt, wird übersprungen
    n.tat = null;
    n.weg = wegSuchen(zelleVon(n.x, n.y), [zx, zy]) || [];
    n.warte = 0;
    szene.warteAuf = n;
    szene.frist = performance.now() + 7000;                 // kommt jemand nicht an, geht es trotzdem weiter
  } else if (s2.sagt) {
    const [id, text2] = s2.sagt;
    const n = id === "spieler" ? { id: "spieler", name: stand.name } : kartenNpcs.find(q => q.id === id)
      || { id, name: typeof person2 === "function" ? person2(id).name : t("Jemand") };
    dialogStart(n, [text2], { danach: () => { if (szene) { szene.nr++; szeneWeiter(); } } });
    szene.warteAuf = null;
  } else if (s2.warte) {
    szene.bis = performance.now() + s2.warte;
    szene.warteAuf = null;
  } else if (s2.tun) {
    s2.tun();
    szene.nr++;
    return szeneWeiter();
  } else if (s2.geste) {                                    // nicken, Kopf senken, aufblicken
    const [id, art] = s2.geste;
    gesteZeigen(id, art);
    szene.bis = performance.now() + (GESTE_DAUER[art] || 700);
    szene.warteAuf = null;
  } else if (s2.kamera) {                                   // die Kamera zieht weich hin
    const [x2, y2] = s2.kamera;
    szeneKamera = { x: x2, y: y2, jetztX: szeneKamera ? szeneKamera.jetztX : x2,
                    jetztY: szeneKamera ? szeneKamera.jetztY : y2 };
    szene.bis = performance.now() + (s2.dauer || 600);
    szene.warteAuf = null;
  } else if (s2.kameraAuf) {                                // auf eine Figur halten
    const id = s2.kameraAuf;
    const n = id === "spieler" ? spieler : kartenNpcs.find(q => q.id === id);
    if (!n) { szene.nr++; return szeneWeiter(); }
    szeneKamera = { x: n.x, y: n.y, jetztX: szeneKamera ? szeneKamera.jetztX : n.x,
                    jetztY: szeneKamera ? szeneKamera.jetztY : n.y };
    szene.bis = performance.now() + (s2.dauer || 600);
    szene.warteAuf = null;
  } else if (s2.blende !== undefined) {                     // 1 = zu, 0 = auf
    szeneBlendeZiel = s2.blende;
    szene.bis = performance.now() + (s2.dauer || 420);
    szene.warteAuf = null;
  } else {
    szene.nr++;                                             // unbekannter Schritt: weiter
    return szeneWeiter();
  }
}
function szeneSchritt() {
  if (!szene) return;
  if (szene.warteAuf) {                                      // wartet, bis die Figur angekommen ist
    const da = !szene.warteAuf.weg || !szene.warteAuf.weg.length;
    if (!da && performance.now() < (szene.frist || 0)) return;
    szene.warteAuf.weg = [];
    szene.warteAuf = null; szene.nr++; return szeneWeiter();
  }
  if (szene.bis) {
    if (performance.now() < szene.bis) return;
    szene.bis = 0; szene.nr++; return szeneWeiter();
  }
}

// Zueinander drehen
function drehenZu(n) {
  const dx = n.x - spieler.x, dy = n.y - spieler.y;
  spieler.blick = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "O" : "W") : (dy > 0 ? "S" : "N");
  spieler.bewegt = false; spieler.bild = 0;
}
// frischerSatz() wohnt in dorf.js (spaeter geladen) - darum zur Laufzeit nachschlagen.
const frisch = liste => (typeof frischerSatz === "function" ? frischerSatz(null, liste) : eine(liste));
function redenMit(n) {
  if (!n) return;
  if (!NPC_INFO[n.id] && typeof dorfReden === "function" && leute().includes(n.id)) { drehenZu(n); return dorfReden(n); }
  if (!NPC_INFO[n.id]) { drehenZu(n); return fremdReden(n); }
  drehenZu(n);
  // Sieben der neun Eintraege in npcs.json sind reine Schmiede-Vorlagen (aktiv:false) und
  // haben gar keine Dialoge. Auf die Karte kommen sie nicht - aber wenn doch je eine dort
  // landet, stuerzt der Zugriff auf d.alltag ab, und das Bild bleibt stehen.
  const d = NPC_INFO[n.id].dialoge;
  if (!d || !d.alltag || !d.alltag.length) { return typeof fremdReden === "function" ? fremdReden(n) : undefined; }
  const heute = spielTagSchluessel(), jahr = jetzt().getFullYear();
  const f = stand.freundschaft[n.id] || 0, schonHeute = stand.gesprochen[n.id] === heute;
  const seiten = [], gaben = [];
  if (istNacht() && !schonHeute && d.nacht) seiten.push(t(d.nacht));
  if (geburtstagHeute()) {
    seiten.push(t(frisch(GEBURTSTAGSSAETZE)));
    if (stand.geburtstagsgaben[n.id] !== jahr && GEBURTSTAGSGABE[n.id]) {
      stand.geburtstagsgaben[n.id] = jahr;
      gaben.push(GEBURTSTAGSGABE[n.id]);
      seiten.push(t("Hier, das hab ich für dich gemacht: {0}!", menge(GEBURTSTAGSGABE[n.id], 1)));
    }
  }
  if (!f && !stand.gesprochen[n.id]) {
    seiten.push(...tAlle(d.erstes));
    if (n.id === "mila") {                                  // etwas zum Tauschen fuer den Anfang
      gaben.push("gans", "gans");
      seiten.push(t("Hier – nimm zwei von meinen Gänsen. Mit irgendwas muss man ja tauschen können!"));
    }
  }
  else if (schonHeute) seiten.push(t(frisch(d.nochmal)));
  else if (f >= 5 && spielTagNummer() % 3 === 0) seiten.push(...tAlle(d.freund));
  else seiten.push(...tAlle(d.alltag[(spielTagNummer() + hash(n.id)) % d.alltag.length]));
  if (!schonHeute && HAEUSER[n.id]) { bauFortschritt(); seiten.push(...bauSaetze(n.id)); }
  if (!schonHeute) {
    stand.freundschaft[n.id] = f + 1; stand.gesprochen[n.id] = heute;
    // Jede vierte Begegnung bringt eine Kleinigkeit
    if ((f + 1) % 4 === 0) {
      const g = eine(KLEINE_GABEN[n.id] || BLUMEN);
      gaben.push(g);
      seiten.push(t("Ach, warte – das hier ist für dich, {name}: {0}.", menge(g, 1)));
    }
  }
  // Auswahl am Ende: Tauschangebot des Tages, etwas schenken, verabschieden
  const wahl = [], angebot = angebotHeute(n);
  if (angebot && stand.getauscht[n.id] !== heute) {
    const [wd, wn] = angebot.will, [gd, gn] = angebot.gibt;
    seiten.push(t("Übrigens – hättest du {0} für mich? Dafür bekommst du {1}.", menge(wd, wn), menge(gd, gn)));
    wahl.push({ text: t("Tauschen: {0} gegen {1}", menge(wd, wn), menge(gd, gn)), tun: () => tauschen(n, angebot) });
  }
  if (stand.geschenkt[n.id] !== heute && stand.taschen.some(Boolean)) wahl.push({ text: t("Etwas schenken"), tun: () => schenkenWahl(n) });
  if (HAEUSER[n.id] && !bauStand(n.id).fertig)
    wahl.push({ text: t("Beim Hausbau helfen"), tun: () => { setTimeout(() => { bauWahl = 0; bauplatzZeigen(true, n.id); }, 0); return null; } });
  if (wahl.length) wahl.push({ text: t("Tschüss!") });
  speichernBald();
  dialogStart(n, seiten, { wahl: wahl.length ? wahl : null, danach: () => {
    const nichtPlatz = gaben.filter(g => !einstecken(g));
    if (gaben.length) meldung(nichtPlatz.length ? t("Deine Taschen sind voll – ein Geschenk bleibt liegen.")
                                                : t("{0} bekommen", gaben.map(g => menge(g, 1)).join(", ")));
    speichernBald();
    meilensteinePruefen();
  } });
}
function tauschen(n, a) {
  const [wd, wn] = a.will, [gd, gn] = a.gibt;
  const da = anzahlIn([wd]);
  const ergebnis = umtauschen([[[wd], wn]], [[gd, gn]]);
  if (ergebnis === "fehlt") return [da ? t("Du hast erst {0} dabei. Kein Problem – das Angebot gilt den ganzen Tag!", menge(wd, da))
                                      : t("Du hast gerade keine {0} dabei. Komm einfach wieder!", dingName(wd, true))];
  if (ergebnis === "voll") return [t("Deine Taschen sind ja randvoll, {name}! Mach erst ein bisschen Platz.")];
  stand.getauscht[n.id] = spielTagSchluessel();
  zaehle("getauscht");
  geraeusch("tausch");
  speichernBald();
  setTimeout(meilensteinePruefen, 0);
  meldung(t("{0} gegen {1} getauscht", menge(wd, wn), menge(gd, gn)));
  return [t("Abgemacht! Danke, {name} – so kommen wir beide weiter.")];
}
function schenkenWahl(n) {
  const arten = [...new Set(stand.taschen.filter(Boolean).map(f => f.ding))].slice(0, 6);
  return { seiten: [t("Oh, du hast etwas für mich?")],
           wahl: [...arten.map(ding => ({ text: `${dingName(ding)} (${anzahlIn([ding])})`, tun: () => schenken(n, ding) })), { text: t("Doch nicht") }] };
}
function schenken(n, ding) {
  herausnehmen([ding], 1);
  stand.geschenkt[n.id] = spielTagSchluessel();
  zaehle("verschenkt");
  const lieb = (LIEBLINGE[n.id] || []).includes(ding);
  const punkte = ding === "stiefel" ? 0 : lieb ? 3 : 1;
  stand.freundschaft[n.id] = (stand.freundschaft[n.id] || 0) + punkte;
  speichernBald();
  setTimeout(meilensteinePruefen, 0);
  // Wer etwas bekommt, schenkt immer etwas zurueck - fuer Lieblingsdinge etwas Besonderes
  const zurueck = lieb ? (LIEBLINGS_GABE[n.id] || "kranz") : eine(KLEINE_GABEN[n.id] || ["rosa", "ringel", "lavendel", "eichel"]);
  const dank = t("Warte – das hier ist für dich: {0}!", menge(zurueck, 1));
  setTimeout(() => meldung(einstecken(zurueck) ? t("{0} bekommen", menge(zurueck, 1))
                                               : t("Deine Taschen sind voll – das Geschenk bleibt liegen.")), 0);
  if (ding === "stiefel") return [t("Ein … alter Stiefel? Hm. Danke, {name}. Glaube ich."), dank];
  if (lieb) { n.emote = "herz"; n.emoteBis = performance.now() + 3000;
    return [t((NPC_INFO[n.id] && NPC_INFO[n.id].dialoge.geschenk) || "Oh, genau so etwas mag ich! Danke, {name}!"), dank]; }
  return [t("{0}? Danke dir, {name}! Das ist lieb von dir.", dingName(ding)), dank];
}
// Erste Ankunft: was hier gespielt wird. Der Text nannte lange nur Sammeln, Bauen und
// Reden - also das gemuetliche Spiel, das mit K1 abgewaehlt wurde. Die Nacht, die Monster
// und der Tod kamen darin nicht vor, obwohl die erste Nacht am selben Abend beginnt.
function ankunft() {
  const nachbarn2 = leute().filter(id => id !== "spieler").map(id => person2(id).name);
  dialogStart(null, [
    t("Du wachst im Wald auf, ohne zu wissen wie. Zwischen den Bäumen liegt eine Lichtung, und auf der Lichtung stehen zwei Menschen, die auch gerade erst angekommen sind."),
    t("{0} heißen sie. Bleib doch – zu dritt ist es leichter als allein.", nachbarn2.join(t(" und "))),
    // Keine Klasse am Anfang: was du kannst, zeigt sich bei der Arbeit, nicht in einer
    // Ansage. Wer es nachlesen will, findet es im Dorfbuch und im Faehigkeitsbaum.
    t("Was du kannst, weißt du noch nicht. Das findet sich, sobald du anfängst."),
    // Der Kern des Spiels, bevor der Spieler ihn ungewarnt erlebt
    t("Eines solltest du wissen: Wenn es dunkel wird, kommen sie aus dem Wald. Jede Nacht mehr. Wer kein Dach über dem Kopf hat, liegt im Freien, wenn sie da sind."),
    t("Das Dorf hält, solange ihr zusammen haltet – eine Mauer (Z), ein Bett für jeden, und Leute, die nicht verwundet sind. Gewinnen kann man hier nicht. Man kann nur überstehen, wie viele Nächte es eben werden."),
    t("Heute kommst du erst einmal an: Sammle Zweige und Kiesel (P), steck dir mit B ein Haus ab, bau im Werkbuch (H), was du brauchst."),
    t("Steht ein Fels, Stumpf oder Farn im Weg: dreimal P, dann ist er weg und die Stelle frei zum Bauen."),
    // Reden ist die Kernmechanik - und die Einfuehrung nannte die Taste dafuer als einzige
    // nicht. Man konnte das halbe Spiel spielen, ohne je mit jemandem gesprochen zu haben.
    t("Sprich die Leute an (T). Daraus kommen Aufträge, Tauschgeschäfte und alles, was du über sie erfährst – Reden ist hier keine Nebensache."),
    t("Das Dorfbuch (V) zeigt, wie es allen geht, und beendet den Tag. Deine Fähigkeiten stehen unter L, die Karte des Dorfes unter E."),
  ], { danach: () => { stand.eingefuehrt = true; speichernBald(); } });
}

// ---------------------------------------------------------------------------
// P: Blume oder Kiesel vor den Fuessen aufheben, Baum schuetteln, am Teich angeln
// ---------------------------------------------------------------------------
const RICHTUNG = { S: [0, 1], N: [0, -1], O: [1, 0], W: [-1, 0] };
const wasserBei = (x, y) => !!(KARTE.wasser && KARTE.wasser[y] && KARTE.wasser[y][x]);
// Wasser bis zwei Kacheln vor der Figur. Der Schwimmer landet so weit draussen wie moeglich
// (nach Norden bis drei Kacheln, damit er nicht hinter dem Kopf der Figur liegt)
function wasserVorMir() {
  const [bx, by] = RICHTUNG[spieler.blick], [zx, zy] = zelleVon(spieler.x, spieler.y);
  for (let d = 1; d <= 2; d++) {
    if (!wasserBei(zx + bx * d, zy + by * d)) continue;
    let weit = d;
    while (weit < d + (spieler.blick === "N" ? 3 : 1) && wasserBei(zx + bx * (weit + 1), zy + by * (weit + 1))) weit++;
    return { x: (zx + bx * weit) * TK + TK / 2, y: (zy + by * weit) * TK + TK / 2 };
  }
  return null;
}
function vorMir() {
  const [dx, dy] = { S: [0, 8], N: [0, -10], O: [9, -3], W: [-9, -3] }[spieler.blick];
  const punkte = [[spieler.x + dx, spieler.y + dy], [spieler.x, spieler.y - 3]];
  const bp = bauplatzVorMir(punkte);
  if (bp) return { art: "bau", bp };
  const tuer = tuerVorMir(punkte);
  if (tuer) return { art: "tuer", tuer };
  if (typeof graeber === "function") {                       // vor einem Grabstein: innehalten
    for (const [px, py] of punkte) {
      const x = Math.floor(px / TK), y = Math.floor(py / TK);
      const grab = graeber().find(q => q.x === x && q.y === y);
      if (grab) return { art: "grab", grab };
    }
  }
  const weg = gepflueckt();
  for (const [px, py] of punkte) {
    const x = Math.floor(px / TK), y = Math.floor(py / TK);
    const p = welt.pflanzen.find(q => q.x === x && q.y === y && !weg.includes(`${x},${y}`));
    if (p) return { art: "pflanze", p };
  }
  for (const [px, py] of punkte) {                           // Fels, Stumpf oder Farn vor den Fuessen
    const h = welt.hindernisZelle && welt.hindernisZelle.get(`${Math.floor(px / TK)},${Math.floor(py / TK)}`);
    if (h) return { art: "hindernis", h };
  }
  // Baum von jeder Seite: Stamm in Reichweite und die Figur schaut ungefaehr zu ihm hin
  const [bx, by] = RICHTUNG[spieler.blick];
  const b = welt.baeume.find(q => {
    const dx = q.stammX - spieler.x, dy = q.stammY - 6 - spieler.y, d = Math.hypot(dx, dy);
    return d < 24 && (d < 6 || (dx * bx + dy * by) / d > 0.35);
  });
  if (b) return { art: "baum", b };
  const w = wasserVorMir();
  if (w) return { art: "wasser", w };
  return null;
}
// Was gerade vom Baum faellt: erst sichtbar herunter, dann eingesammelt
let fallend = [];
function fallendZeichnen(c, zeit) {
  fallend = fallend.filter(f => {
    const k = Math.min(1, (zeit - f.t0) / 420), y = f.y0 + (f.y1 - f.y0) * k * k;
    if (zeit - f.t0 > 700) {
      if (!einstecken(f.ding)) meldung(t("{0} liegt da – aber deine Taschen sind voll.", menge(f.ding, 1)));
      else meldung(t("{0} – eingesteckt!", f.ding === "ast" ? t("Ein Zweig") : t("Eine Eichel")));
      speichernBald();
      return false;
    }
    if (k === 1 && !f.gelandet) { f.gelandet = true; geraeusch("landen"); }
    c.drawImage(dingBild(f.ding), 0, 0, DING_TK, DING_TK, Math.round(f.x - 8), Math.round(y - 14), TK, TK);
    return true;
  });
}
// Drei Schlaege, dann ist das Hindernis weg: etwas in die Tasche, vier Kacheln Bauplatz frei.
// Geraeumtes steht dauerhaft im Weltspeicher - anders als Kiesel waechst es nicht nach.
const HINDERNIS_NAME = { fels: "Fels", stumpf: "Baumstumpf", farn: "Farn" };
const HINDERNIS = {
  fels:   { klang: "kiesel",   ding: "kiesel",   anzahl: 3,
            dabei: "Du klopfst auf den Fels. Er bekommt Risse.",
            fertig: "Der Fels zerbricht – {0}, und wo er stand, ist jetzt Platz.",
            voll: "Der Fels zerbricht – aber deine Taschen sind voll." },
  stumpf: { klang: "rascheln", ding: "ast",      anzahl: 3,
            dabei: "Du brichst am Stumpf herum. Das morsche Holz gibt nach.",
            fertig: "Der Stumpf fällt auseinander – {0}, und Platz obendrein.",
            voll: "Der Stumpf fällt auseinander – aber deine Taschen sind voll." },
  farn:   { klang: "pfluecken", ding: "lavendel", anzahl: 2,
            dabei: "Du reißt am Farn. Die Wurzeln sitzen fest.",
            fertig: "Der Farn ist heraus – {0} wuchs darunter, und die Stelle ist frei.",
            voll: "Der Farn ist heraus – aber deine Taschen sind voll." },
};
function hindernisAnfassen(h) {
  const jetzt = performance.now();
  if (jetzt < h.ruheBis) return;
  const a = HINDERNIS[h.art] || HINDERNIS.fels;
  h.wackelBis = jetzt + 280; h.ruheBis = jetzt + 560;
  geraeusch(a.klang);
  if (++h.schlaege < 3) return meldung(t(a.dabei), 1600);
  hindernisRaeumen(h);
}
function hindernisRaeumen(h) {
  const a = HINDERNIS[h.art] || HINDERNIS.fels;
  const ws = weltSpeicher();
  ws.geraeumt = ws.geraeumt || (ws.felsWeg || []);
  if (!ws.geraeumt.includes(h.id)) ws.geraeumt.push(h.id);
  h.zellen.forEach(([x, y]) => {
    if (KARTE.fest[y] && !KARTE.grund[y][x]) KARTE.fest[y][x] = 0;
    if (welt.grundFest[y] && !KARTE.grund[y][x]) welt.grundFest[y][x] = 0;
    welt.hindernisZelle.delete(`${x},${y}`);
  });
  welt.hindernisse = welt.hindernisse.filter(q => q !== h);
  welt.sprites = welt.sprites.filter(sp => sp.hindernis !== h);
  let hab = 0;
  for (let i = 0; i < a.anzahl; i++) if (einstecken(a.ding)) hab++;
  geraeusch("landen");
  zaehle("geraeumt");
  speichernBald();
  meilensteinePruefen();
  meldung(hab ? t(a.fertig, menge(a.ding, hab)) : t(a.voll), 3200);
}
function aufheben() {
  if (angeln) return einholen();
  const ziel = vorMir();
  if (!ziel) return;
  if (ziel.art === "grab") {                                 // einen Moment stehenbleiben
    const g = ziel.grab;
    return meldung(t("{0} · gefallen in der {1}. Nacht.", g.name, g.nacht), 4000);
  }
  if (ziel.art === "hindernis") return hindernisAnfassen(ziel.h);
  if (ziel.art === "bau") { bauWahl = 0; return bauplatzZeigen(true, ziel.bp.wer); }
  if (ziel.art === "tuer") return hausBetreten(ziel.tuer);
  if (ziel.art === "wasser") {
    if (!anzahlIn(["angel"])) return meldung(t("Hier könnte man angeln – eine Angel baust du im Werkbuch (H) aus 4 Zweigen."), 3500);
    return auswerfen(ziel.w);
  }
  if (ziel.art === "baum") {
    // Jedes Schuetteln laesst etwas fallen: meist einen Zweig, manchmal eine Eichel (kurze Pause je Baum)
    const b = ziel.b, t = performance.now();
    if (t < (b.ruheBis || 0)) return;
    b.wackelBis = t + 450; b.ruheBis = t + 900;
    geraeusch("rascheln");
    zaehle("geschuettelt");
    const fund = Math.random() < 0.65 ? "ast" : "eichel", seite = Math.random() < 0.5 ? -1 : 1;
    fallend.push({ ding: fund, x: b.stammX + seite * (8 + Math.random() * 10), y0: b.stammY - 44, y1: b.stammY + 2 + Math.random() * 6, t0: t });
    speichernBald();
    return meilensteinePruefen();
  }
  const { p } = ziel;
  if (!einstecken(p.ding)) return meldung(t("Deine Taschen sind voll!"));
  if (p.gelegt) {                                            // selbst Abgelegtes verschwindet aus der Ablage
    const ablage = weltSpeicher();
    ablage.gelegt = (ablage.gelegt || []).filter(q => !(q.x === p.x && q.y === p.y && q.ding === p.ding));
    welt.pflanzen = welt.pflanzen.filter(q => q !== p);
  }
  gepflueckt().push(`${p.x},${p.y}`);
  if (typeof tagSchritt === "function") tagSchritt();
  geraeusch(p.ding === "kiesel" ? "kiesel" : "pfluecken");
  zaehle("gepflueckt");
  speichernBald();
  meldung(t("{0} eingesteckt", dingName(p.ding)));
  meilensteinePruefen();
}

// ---------------------------------------------------------------------------
// Angeln: P am Teich wirft aus. Nach 2-7 s beisst etwas ("!") - dann bleibt knapp
// eine Sekunde, um mit P einzuholen. Zu frueh: nichts dran.
// Hechte beissen lieber in der Daemmerung und nachts.
// ---------------------------------------------------------------------------
let angeln = null;
function auswerfen(ort) {
  const t = performance.now();
  angeln = { ...ort, seit: t, biss: t + 2000 + Math.random() * 5000, phase: "wartet" };
  geraeusch("wusch");
  tasten.clear(); spieler.bewegt = false;
  plopp(0.6);
}
function fangWuerfeln() {
  const h = stunde(), daemmerung = h < 8 || h > 18;
  const topf = [["rotauge", 34], ["barsch", 26], ["karpfen", 18], ["hecht", daemmerung ? 12 : 4], ["stiefel", 6]];
  let z = Math.random() * topf.reduce((s2, [, w]) => s2 + w, 0);
  for (const [ding, w] of topf) if ((z -= w) < 0) return ding;
  return "rotauge";
}
function einholen() {
  const a = angeln;
  angeln = null;
  if (a.phase !== "biss") return meldung(t("Zu früh gezogen – nichts dran."));
  const fang = fangWuerfeln();
  if (!einstecken(fang)) return meldung(t("{0} – aber deine Taschen sind voll. Du lässt ihn wieder schwimmen.", menge(fang, 1)), 3000);
  geraeusch(fang === "stiefel" ? "platschen" : "fang");
  if (fang === "stiefel") meldung(t("Ein alter Stiefel. Na ja … eingesteckt."));
  else { zaehle("gefangen"); if (fang === "hecht") zaehle("hecht"); meldung(t("{0} gefangen!", dingName(fang))); }
  speichernBald();
  meilensteinePruefen();
}
function angelnSchritt(zeit) {
  if (!angeln) return;
  if (angeln.phase === "wartet" && zeit >= angeln.biss) { angeln.phase = "biss"; angeln.bisBis = zeit + 900; plopp(1); }
  else if (angeln.phase === "biss" && zeit > angeln.bisBis) { angeln = null; meldung(t("Weg ist er – der Fisch hat den Köder stibitzt.")); }
}
function angelZeichnen(c, t) {
  if (!angeln) return;
  const a = angeln, biss = a.phase === "biss";
  const x = Math.round(a.x), y = Math.round(a.y + (biss ? 2 : Math.sin((t - a.seit) / 400)));
  // Rute vom Koerper schraeg nach vorn, Schnur mit etwas Durchhang zum Schwimmer
  const [hx, hy] = { S: [7, -12], N: [-7, -34], O: [15, -30], W: [-15, -30] }[spieler.blick];
  const [gx, gy] = [spieler.x + Math.sign(hx) * 4, spieler.y - 16];
  const linie = (x0, y0, x1, y1, bogen, farbe) => {
    c.fillStyle = farbe;
    const n = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0)));
    for (let i = 0; i <= n; i++) { const k = i / n; c.fillRect(Math.round(x0 + (x1 - x0) * k), Math.round(y0 + (y1 - y0) * k + Math.sin(k * Math.PI) * bogen), 1, 1); }
  };
  const [sx, sy] = [spieler.x + hx, spieler.y + hy];
  linie(sx, sy, x, y - 2, biss ? 1 : 6, "rgba(236, 230, 214, .75)");
  linie(gx, gy, sx, sy, 0, "#5a3418");
  // Wellenringe, beim Biss kraeftiger
  const phase = (t - a.seit) % 1800;
  if (biss || phase < 700) {
    const r = biss ? 5 + Math.sin(t / 60) : 3 + phase / 350;
    c.fillStyle = `rgba(220, 240, 255, ${biss ? .8 : .5})`;
    for (let w = 0; w < 20; w++) { const k = w / 20 * Math.PI * 2; c.fillRect(Math.round(x + Math.cos(k) * r), Math.round(y + 1 + Math.sin(k) * r * 0.45), 1, 1); }
  }
  c.fillStyle = "#1e120a"; c.fillRect(x - 2, y - 3, 4, 5);
  c.fillStyle = "#c83c3c"; c.fillRect(x - 1, y - 2, 2, 2);
  c.fillStyle = "#ece6d6"; c.fillRect(x - 1, y, 2, 1);
  if (biss) zeichneEmote(c, { x: spieler.x, y: spieler.y, emote: "ausruf", emoteBis: Infinity }, t);
}
// Kurzes "Plopp" (Auswerfen, Biss, Herstellen)
function plopp(staerke = 1, hoch = 520) {
  if (!klangFrei || stumm || !LAUT.klang) return;
  try { audio = audio || new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
  const o = audio.createOscillator(), v = audio.createGain(), a = audio.currentTime;
  o.type = "sine";
  o.frequency.setValueAtTime(hoch, a); o.frequency.exponentialRampToValueAtTime(hoch * 0.35, a + 0.12);
  v.gain.setValueAtTime(0.0001, a); v.gain.exponentialRampToValueAtTime(0.12 * staerke * LAUT.klang, a + 0.01); v.gain.exponentialRampToValueAtTime(0.0001, a + 0.16);
  o.connect(v).connect(audio.destination); o.start(a); o.stop(a + 0.18);
}

// ---------------------------------------------------------------------------
// Werkbuch (H): Rezepte aus gesammelten Dingen. zutaten: [[liste, anzahl], ...]
// ---------------------------------------------------------------------------
const REZEPTE = [
  { ding: "angel",         zutaten: [[["ast"], 4]],                                         wozu: "Damit angelst du am Teich." },
  { ding: "floete",        zutaten: [[["ast"], 2]],                                         wozu: "Klingt ein bisschen schief. Genau richtig." },
  { ding: "kranz",         zutaten: [[BLUMEN, 3]],                                          wozu: "Aus drei beliebigen Blumen geflochten." },
  { ding: "strauss",       zutaten: [[["rosa"], 1], [["ringel"], 1], [["gaense"], 1], [["lavendel"], 1]], wozu: "Von jeder Blume eine." },
  { ding: "kette",         zutaten: [[["kiesel"], 3], [["lavendel"], 1]],                   wozu: "Kiesel an einem Lavendelstängel aufgefädelt." },
  { ding: "vogelhaus",     zutaten: [[["ast"], 5], [["eichel"], 2]],                        wozu: "Für die Vögel, die beim Schütteln so beleidigt gucken." },
  { ding: "kraeuterfisch", zutaten: [[FISCHE, 1], [["lavendel"], 1]],                       wozu: "Ein Fisch mit Kräutern – duftet nach Sommer." },
  // Moebel: leere Stuben füllt man selbst. Aufstellen im Haus mit B.
  { ding: "hocker",        zutaten: [[["ast"], 3]],                                         wozu: "Zum Sitzen. Stell ihn mit B in dein Haus." },
  { ding: "korb",          zutaten: [[["ast"], 4]],                                         wozu: "Geflochten. Steht gut in einer Ecke." },
  { ding: "topf",          zutaten: [[["kiesel"], 4]],                                      wozu: "Aus Ton gebrannt." },
  { ding: "krug",          zutaten: [[["kiesel"], 3], [["ast"], 1]],                        wozu: "Für Wasser vom Teich." },
  { ding: "stuhl",         zutaten: [[["ast"], 6]],                                         wozu: "Mit Lehne, damit es gemütlich wird." },
  { ding: "sessel",        zutaten: [[["ast"], 6], [BLUMEN, 2]],                            wozu: "Weich gepolstert mit Blütenblättern." },
  { ding: "truhe",         zutaten: [[["ast"], 6], [["kiesel"], 2]],                        wozu: "Für alles, was du aufheben willst." },
  { ding: "kommode",       zutaten: [[["ast"], 8], [["kiesel"], 1]],                        wozu: "Zwei Schubladen, einmal Ordnung." },
  { ding: "regal",         zutaten: [[["ast"], 9]],                                         wozu: "Platz für Fundstücke." },
  { ding: "schrank",       zutaten: [[["ast"], 12], [["kiesel"], 2]],                       wozu: "Der große Schrank. Dauert, lohnt sich." },
  { ding: "tisch",         zutaten: [[["ast"], 10], [["kiesel"], 2]],                       wozu: "Ein Tisch macht aus vier Wänden ein Zuhause." },
  { ding: "bett",          zutaten: [[["ast"], 12], [BLUMEN, 3]],                           wozu: "Mit Blütenblättern gepolstert. Endlich schlafen." },
  { ding: "schaukel",      zutaten: [[["ast"], 8]],                                         wozu: "Wippt, wenn man sich hineinsetzt." },
];
const zutatName = (liste, n) => liste === BLUMEN ? t(n === 1 ? "{0} Blume" : "{0} Blumen", n)
  : liste === FISCHE ? t("{0} Fisch", n) : menge(liste[0], n);
const machbar = r => r.zutaten.every(([liste, n]) => anzahlIn(liste) >= n);
const REZEPT_PRO_SEITE = 8;                                  // zwei Spalten x vier Zeilen, nichts scrollt
let werkbuchOffen = false, werkbuchWahl = 0;
const menueOffen = () => taschenOffen || tagebuchOffen || werkbuchOffen || spielmenueOffen || handelOffen || reiseOffen || bauOffen
  || dorfbuchOffen || kampfOffen || tastenOffen || (typeof lagerOffen !== "undefined" && lagerOffen) || (typeof karteOffen !== "undefined" && karteOffen)
  || (typeof skillOffen !== "undefined" && skillOffen) || (typeof questOffen !== "undefined" && questOffen)
  || (typeof einstellungenOffen !== "undefined" && einstellungenOffen);
// Menues (Taschen, Werkbuch, Tagebuch, Menue, Tauschbrett, Weltkarte) erscheinen anstelle der Karte - nie darueber, nie zwei zugleich
function menueWechsel(name) {
  // Waehrend eines Kampfes darf nichts anderes aufgehen: menueWechsel wuerde den Kampf
  // schliessen, und wieder geoeffnet wird er nur aus naechsterKampf/befehlAusfuehren -
  // die Nacht bliebe fuer immer stehen.
  if (typeof kampf !== "undefined" && kampf && !kampf.fertig && name && name !== "kampf") {
    meldung(t("Erst den Kampf zu Ende bringen."), 2200);
    return false;
  }
  if (name && !(name === "werkbuch" && werkbuchOffen) && !(name === "spielmenue" && spielmenueOffen)) geraeusch("papier");
  if (name) { if (gespraech) gespraechEnde(); angeln = null; tasten.clear(); spieler.bewegt = false; }
  for (const [id, zu] of [["taschen", () => { taschenOffen = false; }], ["werkbuch", () => { werkbuchOffen = false; }],
                          ["meilen", () => { tagebuchOffen = false; }], ["spielmenue", () => { spielmenueOffen = false; }],
                          ["handel", () => { handelOffen = false; }], ["reisekarte", () => { reiseOffen = false; }],
                          ["bauplatz", () => { bauOffen = false; }], ["dorfbuch", () => { dorfbuchOffen = false; }],
                          ["kampf", () => { kampfOffen = false; }], ["dorfkarte", () => { karteOffen = false; }], ["skills", () => { skillOffen = false; }], ["quests", () => { questOffen = false; }],
                          ["tasten", () => { tastenOffen = false; }],
                          ["bauprojekte", () => { if (typeof baulisteOffen !== "undefined") baulisteOffen = false; }],
                          ["lager", () => { if (typeof lagerOffen !== "undefined") lagerOffen = false; }],
                          ["ablage", () => { if (typeof ablageOffen !== "undefined") ablageOffen = null; }],
                          ["einstellungen", () => { if (typeof einstellungenOffen !== "undefined") einstellungenOffen = false; }]])
    if (id !== name) { zu(); document.getElementById(id).hidden = true; }
  document.getElementById("karte").hidden = !!name;
  return true;
}
function werkbuchZeigen(an) {
  if (menueWechsel(an ? "werkbuch" : null) === false) return;
  werkbuchOffen = an;
  document.getElementById("werkbuch").hidden = !an;
  if (!an) return;
  const liste = document.getElementById("rezepte");
  liste.innerHTML = "";
  const seite = Math.floor(werkbuchWahl / REZEPT_PRO_SEITE), von = seite * REZEPT_PRO_SEITE;
  REZEPTE.slice(von, von + REZEPT_PRO_SEITE).forEach((r, j) => {
    const i = von + j;
    const zeile = document.createElement("div");
    zeile.className = "rezept" + (i === werkbuchWahl ? " an" : "") + (machbar(r) ? " geht" : "");
    const bild = document.createElement("canvas"); bild.width = bild.height = DING_TK;
    bild.getContext("2d").drawImage(dingBild(r.ding), 0, 0);
    const zut = r.zutaten.map(([l, n]) => `<span class="${anzahlIn(l) >= n ? "" : "fehlt"}">${zutatName(l, n)} (${Math.min(anzahlIn(l), n)}/${n})</span>`).join(" · ");
    zeile.innerHTML = `<b>${t(dingName(r.ding))}</b><small>${zut}</small><small class="wozu">${t(r.wozu)}</small><em>${machbar(r) ? t("bereit") : t("fehlt noch")}</em>`;
    zeile.prepend(bild);
    zeile.addEventListener("click", () => {                  // erst auswaehlen, erst der zweite Klick stellt her
      const schon = werkbuchWahl === i; werkbuchWahl = i;
      if (schon) herstellen(r); else werkbuchZeigen(true);
    });
    liste.append(zeile);
  });
  const seiten = Math.ceil(REZEPTE.length / REZEPT_PRO_SEITE);
  document.getElementById("werkbuchseite").textContent = seiten > 1 ? t("Seite {0} von {1}", seite + 1, seiten) : "";
}
function herstellen(r) {
  if (!machbar(r)) return meldung(t("Da fehlt noch etwas – schau aufs Rezept."));
  // Herstellen kostet keine der drei Tageshandlungen (10.10.): das Material ist schon
  // gesammelt, der Handgriff selbst soll den Tag nicht aufbrauchen. Vorher stand hier
  // ein grosseHandlung()-Riegel samt Rueckbuchung, wenn der Tausch doch scheiterte.
  const ergebnis = umtauschen(r.zutaten, [[r.ding, 1]]);
  if (ergebnis === "fehlt" || ergebnis === "voll") {
    meldung(ergebnis === "fehlt" ? t("Da fehlt noch etwas – schau aufs Rezept.")
                                 : t("Deine Taschen sind voll – mach erst Platz."));
  }
  else {
    zaehle("hergestellt");
    meldung(t("{0} hergestellt!", dingName(r.ding)));
    geraeusch("haemmern");
    speichernBald();
    meilensteinePruefen();
  }
  werkbuchZeigen(true);
}

// ---------------------------------------------------------------------------
// Anzeigen: Uhr, Hinweis, Meldung, Taschen
// ---------------------------------------------------------------------------
let meldungBis = 0;
function meldung(satz, dauer = 2200) {
  const el = document.getElementById("meldung");
  el.textContent = satz; el.hidden = false; meldungBis = performance.now() + dauer;
}
// Tagesgruss beim ersten Besuch eines Tages
function tagesgruss() {
  const d = jetzt(), h = stunde(d);
  const gruss = h < 5 ? t("Gute Nacht") : h < 11 ? t("Guten Morgen") : h < 18 ? t("Guten Tag") : t("Guten Abend");
  const wochentag = t(["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"][d.getDay()]);
  meldung(t("{0}, {1}! Heute ist {2}, der {3}. {4}.", gruss, stand.name, wochentag, d.getDate(), MONATE[d.getMonth()]), 4500);
}
// Der Himmel in der Kopfleiste: die Sonne wandert ueber den Bogen, nachts steht der Mond da
const TAGESZEIT = [[8, "Früher Morgen"], [11, "Vormittag"], [14, "Mittag"], [17, "Nachmittag"], [19.5, "Abend"], [24, "Nacht"]];
const tagesZeitName = () => (TAGESZEIT.find(([h]) => stunde() < h) || TAGESZEIT[5])[1];
function himmelMalen() {
  const c = document.getElementById("himmel");
  if (!c) return;
  const g = c.getContext("2d"), b = c.width, h = c.height;
  const st = stunde(), nacht = istNacht();
  const k = Math.max(0, Math.min(1, (st - 6) / 13));         // 0 = Sonnenaufgang, 1 = Untergang
  const daemmer = !nacht && (k < 0.14 || k > 0.86);
  g.clearRect(0, 0, b, h);
  // Himmel in Baendern, damit es nach Pixelgrafik aussieht statt nach Verlauf
  const baender = nacht ? ["#12162c", "#1b2340", "#273053"]
    : daemmer ? ["#3e4a7a", "#b86a3c", "#e7a35a"]
    : ["#5aa6dc", "#79bde8", "#a6d6f2"];
  baender.forEach((f, i) => { g.fillStyle = f; g.fillRect(0, Math.round(i * h / 3), b, Math.ceil(h / 3)); });
  // Bahn der Sonne als feine Punktreihe
  g.fillStyle = nacht ? "rgba(246,234,208,.18)" : "rgba(255,255,255,.35)";
  for (let x = 3; x < b - 3; x += 3) {
    const y = h - 5 - Math.sin((x - 3) / (b - 6) * Math.PI) * (h - 11);
    g.fillRect(x, Math.round(y), 1, 1);
  }
  if (nacht) {
    [[6, 5], [14, 9], [23, 4], [38, 7], [47, 4], [53, 11]].forEach(([x, y], i) => {   // Sterne, leicht flackernd
      const hell = 0.45 + 0.55 * Math.abs(Math.sin(performance.now() / (900 + i * 220) + i));
      g.fillStyle = `rgba(246,234,208,${hell.toFixed(2)})`;
      g.fillRect(x, y, 1, 1);
    });
    const mx = Math.round(b / 2), my = 8;                    // Mondsichel
    g.fillStyle = "#f6ead0";
    g.fillRect(mx - 3, my - 2, 6, 5); g.fillRect(mx - 2, my - 3, 4, 7);
    g.fillStyle = baender[1];
    g.fillRect(mx - 1, my - 3, 4, 7); g.fillRect(mx, my - 4, 3, 9);
  } else {
    const sx = Math.round(3 + k * (b - 6)), sy = Math.round(h - 5 - Math.sin(k * Math.PI) * (h - 11));
    g.fillStyle = daemmer ? "rgba(248,170,80,.30)" : "rgba(248,216,128,.28)";   // Hof um die Sonne
    g.fillRect(sx - 5, sy - 3, 11, 7); g.fillRect(sx - 3, sy - 5, 7, 11);
    g.fillStyle = daemmer ? "#f8a048" : "#f8d880";
    g.fillRect(sx - 3, sy - 2, 7, 5); g.fillRect(sx - 2, sy - 3, 5, 7);
    g.fillStyle = daemmer ? "#f8d880" : "#fff6cc";
    g.fillRect(sx - 1, sy - 2, 3, 5); g.fillRect(sx - 2, sy - 1, 5, 3);
    if (!daemmer) {                                          // vier kurze Strahlen
      g.fillStyle = "rgba(255,246,204,.75)";
      g.fillRect(sx, sy - 6, 1, 2); g.fillRect(sx, sy + 5, 1, 2);
      g.fillRect(sx - 6, sy, 2, 1); g.fillRect(sx + 5, sy, 2, 1);
    }
  }
  // Huegelsilhouette und Boden, damit der Himmel einen Horizont hat
  g.fillStyle = nacht ? "#0d1120" : daemmer ? "#2a2417" : "#2f4a22";
  for (let x = 0; x < b; x++) {
    const hoehe = 3 + Math.round(1.6 * Math.sin(x / 7) + 1.2 * Math.sin(x / 3.1 + 1));
    g.fillRect(x, h - hoehe, 1, hoehe);
  }
  if (regenTag()) {                                          // Regen als schraege Striche
    g.fillStyle = "rgba(200,220,255,.5)";
    for (let i = 0; i < 9; i++) {
      const x = (i * 7 + Math.floor(performance.now() / 90) % 7) % b;
      const y = (i * 5 + Math.floor(performance.now() / 60)) % (h - 4);
      g.fillRect(x, y, 1, 2);
    }
  }
}

// Kopfzeile rechts: Dorfname, Tag und ueberlebte Naechte
function ortZeile() {
  const d = stand && stand.dorf ? stand.dorf : null;
  const el = document.getElementById("ortname");
  if (!el || !aktWelt) return;
  el.textContent = d ? t("{0} · Tag {1} · {2} {3}", aktWelt.name, d.tag, d.naechte, d.naechte === 1 ? t("Nacht") : t("Nächte"))
                     : `${aktWelt.name} · ${biomVon(aktWelt.biom).name}`;
}
const hudAlt = {};
function setzeText(id, wert) { if (hudAlt[id] !== wert) { hudAlt[id] = wert; document.getElementById(id).textContent = wert; } }
function hudAktualisieren(zeit) {
  himmelMalen();
  const d = jetzt();
  const uebrig = typeof handlungenUebrig === "function" && typeof arbeitstag === "function" && arbeitstag()
    ? ` · ${t("{0} Handlungen", handlungenUebrig())}` : "";
  setzeText("uhrdatum", `${t(tagesZeitName())}${regenTag() ? ` · ${t("Regen")}` : ""}${geburtstagHeute() ? ` · ${t("Geburtstag!")}` : ""}${uebrig}`);
  if (zeit > meldungBis) document.getElementById("meldung").hidden = true;
  ortZeile();
  if (stand && stand.dorf && zeit - (hudAlt.einrichten || 0) > 2000) {   // Bewohner richten sich ein
    hudAlt.einrichten = zeit;
    if (typeof npcEinrichten === "function") npcEinrichten();
  }
  // Was die Tasten gerade tun: ein kleines Schild ueber der Figur (keine Leiste mehr)
  const frei = !gespraech && !menueOffen() && !innen;
  const ziel = frei ? vorMir() : null;
  const partner = frei ? naechsterNpc() : null;
  let html = "";
  if (mauerModus) {
    const k2 = mauerPlatz(), passt = grenzKacheln().includes(k2);
    html = !passt ? t("Nur an der Grenze des Dorfes")
      : natuerlich(k2) ? t("Wasser und Fels halten von allein")
      : `<kbd>←</kbd><kbd>→</kbd> ${MAUER_NAME[mauerModus.art]} (${Object.entries(MAUER_KOSTEN[mauerModus.art])
            .map(([v, n2]) => `${n2} ${VORRAT_NAME[v]}`).join(", ")}) &nbsp; <kbd>${t("Enter")}</kbd> ${t("setzen")}`;
  } else if (abstecken) {
    const q = absteckPlatz(), grund = platzPruefen(q.x, q.y, "spieler");
    html = `<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> ${t(ABSTECK_SEITE[absteckRichtung])} &nbsp; `
      + (grund ? grund : `${t("passt!")} <kbd>${t("Enter")}</kbd>`);
  } else if (angeln) {
    html = angeln.phase === "biss" ? `${t("Jetzt!")} <kbd>P</kbd>` : t("Warte …");
  } else if (innen) {
    const st = moebelVorMir();
    html = stellen ? `<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd> ${t("schieben")} &nbsp; <kbd>${t("Leer")}</kbd> ${dingName(stellen.liste[stellen.nr])} &nbsp; <kbd>${t("Enter")}</kbd> ${t("hinstellen")}`
      // Liegt jemand im Bett, hat der Kranke Vorrang - genau so entscheidet es auch
      // moebelAufheben(). Vorher versprach das Schild "P Bett aufheben", und P gab ein Kraut.
      : typeof liegenderVorMir === "function" && liegenderVorMir()
        ? (typeof istHeiler === "function" && istHeiler("spieler")
            ? `<kbd>P</kbd> ${t("{0} ein Kraut geben", person2(liegenderVorMir()).name)}`
            : `<kbd>P</kbd> ${t("nach {0} sehen", person2(liegenderVorMir()).name)}`)
      : st && st.art === "bett" ? `<kbd>${t("Leer")}</kbd> ${t("schlafen (Tag beenden)")} &nbsp; <kbd>P</kbd> ${t("Bett aufheben")}`
      : typeof liegenderVorMir === "function" && liegenderVorMir()
        ? (typeof istHeiler === "function" && istHeiler("spieler")
            ? `<kbd>P</kbd> ${t("{0} ein Kraut geben", person2(liegenderVorMir()).name)}`
            : `<kbd>P</kbd> ${t("nach {0} sehen", person2(liegenderVorMir()).name)}`)
      : st && SITZBAR[st.art] ? `<kbd>${t("Leer")}</kbd> ${t("hinsetzen")} &nbsp; <kbd>P</kbd> ${t("{0} aufheben", dingName(st.art))}`
      // Jedes Möbel kann etwas - das Schild sagt jetzt auch, was
      : st && typeof MOEBEL_TAT === "object" && MOEBEL_TAT[st.art]
        ? `<kbd>${t("Leer")}</kbd> ${t(MOEBEL_TAT[st.art])} &nbsp; <kbd>P</kbd> ${t("{0} aufheben", dingName(st.art))}`
      : st ? `<kbd>P</kbd> ${t("{0} aufheben", dingName(st.art))}`
      : anDerTuer() ? `<kbd>P</kbd> ${t("hinaus")}`
      : moebelInTasche().length ? `<kbd>B</kbd> ${t("einrichten")}` : "";
  } else if (ziel) {
    const was = ziel.art === "grab" ? t("{0} ({1}. Nacht)", ziel.grab.name, ziel.grab.nacht)
      : ziel.art === "tuer" ? (ziel.tuer.schluessel === "spieler" ? t("nach Hause") : ziel.tuer.name)
      : ziel.art === "bau" ? t("{0}: abgeben", HAEUSER[ziel.bp.wer].name)
      : ziel.art === "wasser" ? (anzahlIn(["angel"]) ? t("angeln") : t("Angel fehlt"))
      : ziel.art === "baum" ? t("schütteln")
      // Ohne diesen Zweig griff die Zeile darunter auf ziel.p.ding zu - bei einem Hindernis
      // gibt es kein ziel.p. Der TypeError flog aus hudAktualisieren heraus, und weil die
      // Schleife das naechste Bild erst NACH weltSchritt() anfordert, blieb das Spiel
      // stehen, sobald man vor einem Fels stand. Alle 110 Testfaelle waren dabei gruen.
      : ziel.art === "hindernis" ? t("{0} wegräumen", t(HINDERNIS_NAME[ziel.h.art] || "Hindernis"))
      : !ziel.p ? ""
      : ziel.p.ding === "kiesel" ? t("{0} aufheben", dingName(ziel.p.ding)) : t("{0} pflücken", dingName(ziel.p.ding));
    html = `<kbd>P</kbd> ${was}`;
  } else if (frei && !partner && !spieler.sprung) {
    const sz = sprungZiel();
    if (sz) html = `<kbd>${t("Leertaste")}</kbd> ${sz.y < spieler.y - 4 ? t("hinauf") : sz.y > spieler.y + 4 ? t("hinunter") : t("hinüber")}`;
  }
  hinweisSchild(html);
}

let taschenOffen = false;
const TASCHEN_SPALTEN = 5;                                   // wie im Raster (index.html)
let taschenWahl = 0;
// Was mit dem gewaehlten Ding geht: drinnen aufstellen, jemandem schenken, sonst ablegen
function taschenTat() {
  const f = stand.taschen[taschenWahl];
  if (!f) return null;
  if (f.ding === "floete") return { text: t("spielen"), tun: () => floeteSpielen() };
  // Ein eingetauschtes Tier wird zum Haustier eines Bewohners (Beschluss D5)
  if (typeof TIERE !== "undefined" && TIERE.includes(f.ding) && HAUSTIERE[f.ding]) {
    const n2 = naechsterNpc();
    if (n2 && typeof leute === "function" && leute().includes(n2.id) && !wert(n2.id).haustier)
      return { text: t("{0} anvertrauen", person2(n2.id).name), tun: () => tierGeben(n2, f.ding) };
  }
  if (innen && DINGE[f.ding].moebel !== undefined) return { text: t("hinstellen"), tun: () => { taschenZeigen(false); stellenStarten(f.ding); } };
  const n = naechsterNpc();
  if (n) return { text: t("{0} schenken", person2(n.id).name), tun: () => {
    taschenZeigen(false);
    const antwort = schenken(n, f.ding);
    if (antwort) dialogStart(n, antwort);                   // der Beschenkte sagt auch etwas dazu
  } };
  return { text: t("ablegen"), tun: () => ablegen(f.ding) };
}
// Ein Tier aus den Taschen wird jemandes Haustier - es öffnet ihm den vierten Zweig im Baum
function tierGeben(n, ding) {
  taschenZeigen(false);
  // Vorher gab herausnehmen() nichts zurueck: das Tier verschwand aus der Tasche, und der
  // Beschenkte bekam es nie - der vierte Zweig im Baum war auf diesem Weg unerreichbar.
  if (anzahlIn([ding]) < 1) return meldung(t("Das Tier hast du nicht dabei."), 2600);
  herausnehmen([ding], 1);
  wert(n.id).haustier = ding;
  dorf().bericht.push(t("{0} hat jetzt {1} – {2}.", person2(n.id).name, HAUSTIERE[ding].name, HAUSTIERE[ding].text));
  geraeusch("fanfare");
  meldung(t("{0} nimmt {1} zu sich (L: neuer Zweig).", person2(n.id).name, HAUSTIERE[ding].name), 4000);
  stand.freundschaft[n.id] = (stand.freundschaft[n.id] || 0) + 2;
  speichernBald();
}

// Auf der Holzfloete spielen: die Falter kommen herbeigeflattert, und wer in der Naehe steht, hoert zu
function floeteSpielen() {
  taschenZeigen(false);
  geraeusch("floete");
  for (const p of teilchen) {
    if (p.art !== "falter") continue;
    p.rastBis = 0;
    p.ziel = { x: spieler.x + (Math.random() * 2 - 1) * 26, y: spieler.y - 6 + (Math.random() * 2 - 1) * 18 };
    p.tempo = 34 + Math.random() * 14;                        // sie haben es auf einmal eilig
  }
  if (typeof dorf === "function" && dorf() && !istNacht()) dorf().floeteTag = dorf().tag;   // die Wachen hören es noch
  const n = naechsterNpc();
  if (n && !floeteGehoert[n.id]) {                            // einmal am Tag freut sich jeder darüber
    floeteGehoert[n.id] = true;
    stand.freundschaft[n.id] = (stand.freundschaft[n.id] || 0) + 1;
    meldung(t("{0} hört zu und lächelt.", person2(n.id).name), 2600);
    speichernBald();
  } else {
    meldung(t("Eine schiefe kleine Weise."), 2000);
  }
}
let floeteGehoert = {};                                       // wer die Weise heute schon gehört hat
function taschenBewegen(d) {
  const n = stand.taschen.length;
  taschenWahl = (taschenWahl + d + n) % n;
  taschenZeigen(true);
}
// Ding vor die Fuesse legen: es liegt dann wieder da und kann aufgehoben werden
function ablegen(ding) {
  const [dx, dy] = RICHTUNG[spieler.blick];
  const x = Math.floor((spieler.x + dx * TK) / TK), y = Math.floor((spieler.y - 2 + dy * TK) / TK);
  if (innen || x < 1 || y < 1 || x >= KARTE.breite - 1 || y >= KARTE.hoehe - 1 || istFest(x * TK + 8, y * TK + 8))
    return meldung(t("Hier ist kein Platz dafür."), 2200);
  // Auf einem Weg wächst nichts - dort wird gegangen
  if (typeof BLUMEN !== "undefined" && BLUMEN.includes(ding)
      && KARTE.untergrund[y] && KARTE.untergrund[y][x])
    return meldung(t("Auf dem Weg wächst nichts – leg sie daneben."), 2600);
  herausnehmen([ding], 1);
  const ablage = weltSpeicher();
  ablage.gelegt = ablage.gelegt || [];
  ablage.gelegt.push({ x, y, ding });
  welt.pflanzen.push({ x, y, ding, g: 0, gelegt: true });
  geraeusch("plumps");
  meldung(t("{0} abgelegt.", dingName(ding)), 1800);
  speichernBald();
  taschenZeigen(true);
}
function taschenZeigen(an) {
  if (menueWechsel(an ? "taschen" : null) === false) return;
  taschenOffen = an;
  document.getElementById("taschen").hidden = !an;
  if (!an) return;
  if (!stand.taschen[taschenWahl]) taschenWahl = Math.max(0, stand.taschen.findIndex(Boolean));
  const raster = document.getElementById("taschenraster");
  raster.innerHTML = "";
  stand.taschen.forEach((f, i) => {
    const fach = document.createElement("div");
    fach.className = "fach" + (i === taschenWahl ? " an" : "");
    fach.addEventListener("click", () => {                   // erst waehlen, erst der zweite Klick tut etwas
      const schon = taschenWahl === i; taschenWahl = i;
      if (schon) { const tat = taschenTat(); if (tat) return tat.tun(); }
      taschenZeigen(true);
    });
    if (f) {
      const c = document.createElement("canvas");
      c.width = c.height = DING_TK;
      c.getContext("2d").drawImage(dingBild(f.ding), 0, 0);
      fach.title = menge(f.ding, f.anzahl);
      const z = document.createElement("span"); z.className = "anzahl"; z.textContent = f.anzahl > 1 ? f.anzahl : "";
      fach.append(c, z);
    }
    raster.append(fach);
  });
  const frei = stand.taschen.filter(f => !f).length;
  const f = stand.taschen[taschenWahl], tat = taschenTat();
  // Wozu ein Ding gut ist, stand bisher nirgends - man hob etwas auf und wusste nicht, warum.
  const wozu = f && DINGE[f.ding] && DINGE[f.ding].wozu ? DINGE[f.ding].wozu : null;
  document.getElementById("taschenwahl").innerHTML = f
    ? `<b>${menge(f.ding, f.anzahl)}</b>${tat ? ` &nbsp; <kbd>${t("Enter")}</kbd> ${tat.text}` : ""}`
      + (wozu ? `<small class="wozu">${t(wozu)}</small>` : "")
    : t("Noch nichts eingesammelt.");
  document.getElementById("taschenfrei").textContent = t("{0} von {1} Fächern frei", frei, TASCHENPLAETZE);
}

// ---------------------------------------------------------------------------
// Namensschilder (HTML ueber der Karte, gestochen scharf)
// ---------------------------------------------------------------------------
const schilder = new Map();
function schild(n, sichtbar, ansprechbar = false) {
  let el = schilder.get(n.id);
  if (!el) {
    el = document.createElement("div"); el.className = "schild";
    const info = person(n.id);
    el.innerHTML = `${info.name}<kbd>T</kbd>`;
    document.getElementById("karte").append(el); schilder.set(n.id, el);
  }
  el.hidden = !sichtbar;
  el.querySelector("kbd").hidden = !ansprechbar;
  el.style.left = `${weltVersatz.x + (n.x - kamera.x) * weltZoom}px`;
  el.style.top = `${weltVersatz.y + (n.y - 34 - kamera.y) * weltZoom}px`;
}

// Hinweis ueber dem eigenen Kopf: zeigt, was die naechste Taste tut
const hinweisAlt = { html: null };
function hinweisSchild(html) {
  const el = document.getElementById("hinweisschild");
  if (!el) return;
  if (hinweisAlt.html !== html) { hinweisAlt.html = html; el.innerHTML = html; el.hidden = !html; }
  if (!html) return;
  if (innen) { el.style.left = "50%"; el.style.top = "86%"; return; }
  el.style.left = `${weltVersatz.x + (spieler.x - kamera.x) * weltZoom}px`;
  el.style.top = `${weltVersatz.y + (spieler.y - 40 - kamera.y) * weltZoom}px`;
}

// ---------------------------------------------------------------------------
// Teilchen: Schmetterlinge am Tag, Gluehwuermchen in der Nacht
// ---------------------------------------------------------------------------
let teilchen = [];
function teilchenAnlegen() {
  const r = zufallsquelle(tagNummer());
  const frei = () => {
    for (let i = 0; i < 40; i++) {
      const x = 2 + r() * (KARTE.breite - 4), y = 7 + r() * (KARTE.hoehe - 9);
      if (!KARTE.fest[Math.floor(y)][Math.floor(x)]) return { x: x * TK, y: y * TK };
    }
    return { x: 320, y: 240 };
  };
  const farben = ["#f6ead0", "#e8b84a", "#d2a1bd", "#8ac3d9"];
  teilchen = [
    ...Array.from({ length: 6 }, (_, i) => ({ art: "falter", ...frei(), phase: r() * 6, farbe: farben[i % farben.length],
                                              sorte: Math.floor(r() * 4), ziel: frei(), rastBis: 0, tempo: 20 + r() * 12 })),
    ...Array.from({ length: 26 }, () => ({ art: "gluehen", ...frei(), phase: r() * 6, vx: 0, vy: 0 })),
  ];
}
function teilchenZeichnen(c, t, dt, nacht) {
  for (const p of teilchen) {
    if (p.art === "falter" && !nacht) {
      // Flattern statt Gleiten: kurze Schuebe zum naechsten Ziel, Seitwaertswackeln, hin und wieder eine Rast
      const rastet = t < p.rastBis;
      if (!rastet) {
        const ex = p.ziel.x - p.x, ey = p.ziel.y - p.y, a = Math.hypot(ex, ey);
        if (a < 5) {
          if (Math.random() < 0.45) p.rastBis = t + 1200 + Math.random() * 2000;   // kurz auf einer Bluete sitzen
          p.ziel = { x: Math.max(24, Math.min(616, p.x + (Math.random() * 2 - 1) * 70)),
                     y: Math.max(96, Math.min(424, p.y + (Math.random() * 2 - 1) * 46)) };
        } else {
          const schub = 0.45 + 0.75 * Math.abs(Math.sin(t / 95 + p.phase));        // jeder Fluegelschlag treibt an
          const dx = ex / a, dy = ey / a, s = p.tempo * schub * dt / 1000;
          p.x += dx * s - dy * Math.sin(t / 130 + p.phase) * 0.55;                 // Wackeln quer zur Flugrichtung
          p.y += dy * s + dx * Math.sin(t / 130 + p.phase) * 0.55 + Math.sin(t / 210 + p.phase) * 0.35;
        }
      }
      const x = Math.round(p.x), y = Math.round(p.y - 10);
      if (welt.falter) {                                       // Wiesenfalter (Assets/14_nachbau_mana/insekten.py), 8x8, 4 Bilder
        const bild = rastet ? 2 : Math.floor(t / 85 + p.phase) % 4;
        c.drawImage(welt.falter, bild * 8, (p.sorte % 4) * 8, 8, 8, x - 4, y - 4, 8, 8);
      } else {
        const offen = Math.floor(t / 120 + p.phase) % 2 === 0;
        c.fillStyle = "#1e120a"; c.fillRect(x, y, 1, 3);
        c.fillStyle = p.farbe;
        if (offen) { c.fillRect(x - 2, y, 2, 2); c.fillRect(x + 1, y, 2, 2); } else { c.fillRect(x - 1, y, 1, 2); c.fillRect(x + 1, y, 1, 2); }
      }
    } else if (p.art === "gluehen" && nacht) {
      p.vx += (Math.random() - 0.5) * 0.02; p.vy += (Math.random() - 0.5) * 0.02;
      p.vx *= 0.98; p.vy *= 0.98; p.x += p.vx * dt / 16; p.y += p.vy * dt / 16;
      const hell = 0.5 + 0.5 * Math.sin(t / 500 + p.phase);
      if (hell < 0.15) continue;
      const x = Math.round(p.x), y = Math.round(p.y - 10);
      c.fillStyle = `rgba(200, 255, 120, ${0.25 * hell})`; c.fillRect(x - 1, y - 1, 3, 3);
      c.fillStyle = `rgba(240, 255, 170, ${hell})`; c.fillRect(x, y, 1, 1);
    }
  }
}

// ---------------------------------------------------------------------------
// Ein Bild der Welt
// ---------------------------------------------------------------------------
// Kamera: Canvas so gross wie das Feld (in Kartenpixeln), Ausschnitt folgt der Figur
const kamera = { x: 0, y: 0, b: 0, h: 0 };
// Der Ausschnitt wird in ganzen Pixeln vergroessert (WELT_MASS), damit keine Kachel breiter
// wird als ihre Nachbarin. Das Canvas ist so gross wie die Anzeige geteilt durch diesen Zoom.
const WELT_MASS = 3;
let weltZoom = 3, weltVersatz = { x: 0, y: 0 };
function kameraAnpassen() {
  const feld = document.getElementById("karte"), B = feld.clientWidth, H = feld.clientHeight;
  if (B && H && (B !== kamera.b || H !== kamera.h)) {
    kamera.b = B; kamera.h = H;
    const dpr = Math.min(2, Math.round(window.devicePixelRatio || 1));
    const mass = (typeof zoomStufe !== "undefined" ? zoomStufe : WELT_MASS) * dpr;
    weltCanvas.width = Math.min(640, Math.ceil(B * dpr / mass));
    weltCanvas.height = Math.min(448, Math.ceil(H * dpr / mass));
    weltZoom = mass / dpr;                                   // Anzeigepixel je Weltpixel
    weltCanvas.style.width = `${weltCanvas.width * weltZoom}px`;
    weltCanvas.style.height = `${weltCanvas.height * weltZoom}px`;
    weltVersatz = { x: (B - weltCanvas.width * weltZoom) / 2, y: (H - weltCanvas.height * weltZoom) / 2 };
    wctx.imageSmoothingEnabled = false;
  }
  const vw = weltCanvas.width, vh = weltCanvas.height;
  // Kamera und Figur muessen auf dieselbe gerundete Stelle schauen, sonst zappelt die Figur:
  // die Kamera rundet sonst anders als das Bild, und bei dreifacher Vergroesserung sieht man
  // jeden dieser Ein-Pixel-Spruenge dreifach.
  // Vorrang: laufende Szene, dann Zusehen, sonst die eigene Figur
  szeneKameraSchritt();
  const ziel = (szeneKamera && szene ? { x: szeneKamera.jetztX, y: szeneKamera.jetztY } : null)
    || zuschauenZiel() || spieler;
  const px = Math.round(ziel.x), py = Math.round(ziel.y);
  kamera.x = Math.max(0, Math.min(640 - vw, px - Math.round(vw / 2)));
  kamera.y = Math.max(0, Math.min(448 - vh, py - 20 - Math.round(vh / 2)));
}

// ---------------------------------------------------------------------------
// Zusehen: der Arbeit eines Bewohners folgen (aus dem Dorfbuch, Taste Y)
// ---------------------------------------------------------------------------
let zuschauen = null;                           // { id, seit }
const zuschauenZiel = () => (zuschauen ? kartenNpcs.find(n => n.id === zuschauen.id) || null : null);
// Was tut er gerade? Aus Aufgabe und laufender Handlung zusammengesetzt
function zuschauenText(id) {
  const n = kartenNpcs.find(q => q.id === id);
  const w = typeof wert === "function" ? wert(id) : null;
  const name = person2(id).name;
  if (!n) return t("{0} ist gerade nicht zu sehen.", name);
  if (n.tat === "kiesel") return t("{0} sammelt Kiesel.", name);
  if (n.tat === "baum") return t("{0} schüttelt einen Baum.", name);
  if (n.tat === "bauen") return t("{0} arbeitet am Bauplatz.", name);
  if (n.tat === "heilen") return t("{0} sitzt bei den Kranken.", name);
  if (w && w.aufgabe && typeof AUFGABEN !== "undefined" && AUFGABEN[w.aufgabe])
    return t("{0}: {1}.", name, AUFGABEN[w.aufgabe].name);
  return t("{0} geht seinen Weg.", name);
}
function zuschauenStarten(id) {
  if (!kartenNpcs.some(n => n.id === id))
    return meldung(t("{0} ist gerade nicht auf der Karte.", person2(id).name), 2600);
  zuschauen = { id, seit: performance.now() };
  if (typeof dorfbuchZeigen === "function") dorfbuchZeigen(false);
  meldung(t("{0} – eine Taste, und du bist wieder bei dir.", zuschauenText(id)), 4000);
}
function zuschauenEnde() {
  if (!zuschauen) return false;
  zuschauen = null;
  meldung(t("Du bist wieder bei dir."), 1800);
  return true;
}
// Am Kartenrand ist Schluss. Hier stand einmal der Weg in die Weltkarte - das Reisen ist
// seit K2 stillgelegt, ein Lauf spielt auf einer Karte. Der Kommentar versprach die Weltkarte
// noch Tage danach.
function ausgangPruefen(dx, dy) {
  if (!dx && !dy) return;
  const x = Math.floor(spieler.x / TK), y = Math.floor((spieler.y - 2) / TK);
  const rand = (x <= 0 && dx < 0) || (x >= KARTE.breite - 1 && dx > 0) || (y >= KARTE.hoehe - 1 && dy > 0);
  if (!rand) return;
  spieler.x -= dx * 3; spieler.y -= dy * 3;                   // hier endet die Karte
  if (!hudAlt.randhinweis || performance.now() - hudAlt.randhinweis > 8000) {
    hudAlt.randhinweis = performance.now();
    meldung(t("Weiter geht es nicht – dein Dorf liegt hier."), 2600);
  }
}
function weltSchritt() {
  // Eigene Zeitmessung statt des rAF-Zeitstempels: der ist nicht in jeder Umgebung fortlaufend
  const zeit = performance.now();
  const dt = Math.min(50, zeit - (zuletzt || zeit)); zuletzt = zeit;
  if (innen) return innenSchritt(zeit, dt);                  // drinnen zeichnet die Stube (haus.js)
  // Bewegung
  szeneSchritt();                                            // laeuft eine Szene, fuehrt sie Regie
  if (typeof questOrtePruefen === "function") questOrtePruefen();
  let dx = 0, dy = 0, blick = null;
  if (!gespraech && !menueOffen() && !szeneLaeuft()) for (const k of tasten) if (GEHTASTEN[k]) { dx += GEHTASTEN[k][0]; dy += GEHTASTEN[k][1]; blick = GEHTASTEN[k][2]; }
  dx = Math.sign(dx); dy = Math.sign(dy);
  if (spieler.sprungWeg) { dx = 0; dy = 0; }                                     // Kantensprung: der Bogen fuehrt
  if (angeln && (dx || dy)) { angeln = null; meldung(t("Angel eingeholt.")); }   // Weggehen holt die Angel ein
  angelnSchritt(zeit);
  const vorher = spieler.bewegt;
  spieler.rennt = tasten.has("Shift");
  spieler.bewegt = !!(dx || dy);
  if (blick) spieler.blick = dx && dy ? (dx > 0 ? "O" : "W") : blick;
  const weit = (spieler.rennt ? 92 : 50) * dt / 1000 / (dx && dy ? Math.SQRT2 : 1);
  if (dx && begehbar(spieler.x + dx * weit, spieler.y)
         && !fremdesDorfSperrt(spieler.x + dx * weit, spieler.y)) spieler.x += dx * weit;
  if (dy && begehbar(spieler.x, spieler.y + dy * weit)
         && !fremdesDorfSperrt(spieler.x, spieler.y + dy * weit)) spieler.y += dy * weit;
  if (spieler.bewegt !== vorher) { spieler.bild = 0; spieler.seit = 0; if (!spieler.bewegt) ortMerken(); }
  ausgangPruefen(dx, dy);
  trampeln();
  const anim = spieler.rennt ? RENNEN : GEHEN;
  spieler.seit += dt;
  if (spieler.bewegt && spieler.seit >= anim.zeiten[spieler.bild]) {
    spieler.seit = 0; spieler.bild = (spieler.bild + 1) % anim.spalten.length;
    if (AUFTRITT.includes(spieler.bild)) schrittLaut(bodenArt(), spieler.rennt);   // Fuss setzt auf
  }

  // Kamera: die Karte fuellt das ganze Feld; was nicht passt, zeigt ein Ausschnitt um die Figur
  kameraAnpassen();
  wctx.setTransform(1, 0, 0, 1, -kamera.x, -kamera.y);
  // Boden, Pflanzen, Wasserglitzern
  wctx.imageSmoothingEnabled = false;
  wctx.drawImage(welt.boden, 0, 0);
  // Ausgetretene Pfade: jede Kachel schaut, welche Nachbarn auch Weg sind, und waehlt danach
  // das passende Stueck - Rand, Ecke oder Mitte (Beschluss M1). Vorher bekam jede Kachel
  // dieselbe gid, weshalb Wege nebeneinanderlagen, statt sich zu verbinden.
  for (const p of welt.wege) { const g = wegKachel(p.x, p.y); if (g) welt.kachel(wctx, g, p.x, p.y); }
  const weg = gepflueckt();
  for (const p of welt.pflanzen) {
    if (weg.includes(`${p.x},${p.y}`)) continue;
    if (p.gelegt) wctx.drawImage(dingBild(p.ding), 0, 0, DING_TK, DING_TK, p.x * TK, p.y * TK, TK, TK);   // selbst hingelegt
    else welt.kachel(wctx, p.g, p.x, p.y);
  }
  for (const { g, x, y } of welt.glitzer) {
    const f = KARTE.animationen[g];
    let akt = g;
    if (f) {
      let rest = (zeit + x * 97 + y * 53) % f.reduce((s2, [, d]) => s2 + d, 0);
      for (const [gid, d] of f) { if (rest < d) { akt = gid; break; } rest -= d; }
    }
    welt.kachel(wctx, akt, x, y);
  }
  // Alles mit Hoehe nach Unterkante sortiert, dazu Schatten unter den Figuren
  const reihe = welt.sprites.slice();
  const ansprechbar = gespraech ? null : naechsterNpc();
  const schatten = (x, y) => { wctx.fillStyle = "rgba(20, 30, 10, .28)"; wctx.fillRect(Math.round(x) - 6, Math.round(y) - 1, 13, 2); wctx.fillRect(Math.round(x) - 4, Math.round(y) + 1, 9, 1); };
  for (const n of kartenNpcs) {
    if (schlaeft(n)) { schild(n, false); continue; }
    const nah = Math.hypot(n.x - spieler.x, n.y - spieler.y) < SPRECHWEITE;
    npcBewegen(n, dt, nah, zeit);
    const img = npcBlaetter[n.id];
    const [zeile, spalte] = n.bewegt ? [4 + BLICK[n.blick], GEHEN.spalten[n.bild]] : [BLICK[n.blick], 0];
    reihe.push({ unten: n.y, male: c => {
      schatten(n.x, n.y);
      // Gesten senken oder heben den Koerper ein paar Pixel (Szenen, Beschluss V3)
      const gv = typeof gesteVersatz === "function" ? gesteVersatz(n.id) : 0;
      if (img) c.drawImage(img, spalte * 64, zeile * 64, 64, 64,
                           Math.round(n.x - FUSS.x), Math.round(n.y - FUSS.y + gv), 64, 64);
      zeichneEmote(c, n, zeit);
    } });
    schild(n, nah && !(gespraech && gespraech.npc === n), n === ansprechbar);
  }
  const r = BLICK[spieler.blick], sprung = sprungBild(zeit);
  const [zeile, spalte] = sprung ? [r, sprung.spalte] : spieler.bewegt ? [4 + r, anim.spalten[spieler.bild]] : [r, 0];
  const hoch = sprung ? sprung.hoehe * (spieler.sprungWeg ? 1.8 : 1) : 0;
  reihe.push({ unten: spieler.y, male: c => {
    schatten(spieler.x, spieler.y);
    // Gesten galten nur fuer NPCs - eine Szene, die dem Spieler "aufblicken" gibt, hielt
    // 800 ms an, ohne dass etwas zu sehen war.
    const gv = typeof gesteVersatz === "function" ? gesteVersatz("spieler") : 0;
    const hub = spieler.bewegt ? gehHub(spieler.bild) : 0;
    c.drawImage(figurFrame("p1", zeile, spalte), Math.round(spieler.x - FUSS.x), Math.round(spieler.y - FUSS.y - hoch + gv + hub));
  } });
  reihe.sort((a, b) => a.unten - b.unten).forEach(sp => sp.male(wctx, zeit));
  angelZeichnen(wctx, zeit);
  fallendZeichnen(wctx, zeit);
  tuerEffekte(wctx, zeit, dt);
  if (abstecken) absteckUmriss(wctx, zeit);

  wctx.setTransform(1, 0, 0, 1, 0, 0);
  // Tageslicht, dann Gluehwuermchen (leuchten ueber der Nacht); an Regentagen Regen statt Faltern
  const nacht = istNacht(), regen = regenTag();
  const inWelt = f => { wctx.setTransform(1, 0, 0, 1, -kamera.x, -kamera.y); f(); wctx.setTransform(1, 0, 0, 1, 0, 0); };
  if (!nacht && !regen) inWelt(() => teilchenZeichnen(wctx, zeit, dt, false));
  const [lr, lg, lb, la] = licht(stunde());
  if (la > 0.005) { wctx.fillStyle = `rgba(${lr | 0}, ${lg | 0}, ${lb | 0}, ${la})`; wctx.fillRect(0, 0, weltCanvas.width, weltCanvas.height); }
  if (regen) regenZeichnen(wctx, zeit);
  if (nacht && !regen) inWelt(() => teilchenZeichnen(wctx, zeit, dt, true));

  blendeZeichnen(wctx, zeit);
  // Szenenblende: legt sich ueber alles, auch ueber das Licht (Beschluss V3)
  szeneBlendeSchritt();
  if (szeneBlende > 0.004) {
    wctx.fillStyle = `rgba(10, 8, 6, ${szeneBlende})`;
    wctx.fillRect(0, 0, weltCanvas.width, weltCanvas.height);
  }
  weltLaute(zeit);
  dialogZeichnen();
  aufstiegeZeichnen(wctx, zeit);                             // Stufenaufstiege über den Köpfen
  hudAktualisieren(zeit);
  klangAktualisieren();
}

// ---------------------------------------------------------------------------
// Tasten in der Welt (von spiel.js weitergereicht)
// ---------------------------------------------------------------------------
// Spielmenue (Esc oder Knopf oben): erscheint wie die anderen Menues anstelle der Karte
let spielmenueOffen = false, menueWahl = 0;
// Alle Tasten auf einen Blick - es sind inzwischen viele
const TASTENHILFE = [
  ["Unterwegs", [
    ["← → ↑ ↓", "gehen"], ["⇧", "rennen"], ["Leer", "springen"], ["P", "aufheben, angeln, räumen"],
    ["T", "reden"], ["B", "Haus abstecken"], ["Z", "Mauer setzen"], ["O", "Tore auf und zu"],
    ["N", "schlafen legen – der Tag endet"],
    ["X", "Falle stellen"], ["F", "Vollbild"], ["M", "Klang aus"], ["Esc", "Menü"],
  ]],
  ["Bücher", [
    ["G", "Lager"], ["V", "Dorfbuch"], ["E", "Dorfkarte"], ["Q", "Aufträge"], ["C", "Bauprojekte"],
    ["L", "Fähigkeiten"], ["I", "Taschen"], ["H", "Werkbuch"], ["J", "Tagebuch · Chronik"],
    ["?", "diese Übersicht"],
  ]],
  ["Im Dorfbuch", [
    ["↑ ↓", "Bewohner wählen"], ["← →", "Aufgabe"], ["T", "Arbeit verteilen"], ["Z", "Gruppe mitziehen"],
    ["1–4", "Gruppe"], ["C", "Klasse wechseln"], ["G", "Wache"], ["K", "Krankenlager"],
    ["R", "alles zurücksetzen"], ["U", "Vorschlag übernehmen"], ["N", "Nacht beginnen"],
  ]],
  ["In der Stube", [
    ["B", "Möbel aufstellen"], ["← → ↑ ↓", "schieben"],
    ["Leer", "Möbel wechseln · sitzen · schlafen"], ["Enter", "hinstellen"],
    ["P", "aufheben · Kraut geben"],
  ]],
  ["In der Nacht", [
    ["↑ ↓", "Ziel wählen"], ["← →", "Befehl"], ["Enter", "ausführen"], ["X", "alle greifen an"],
  ]],
  // Diese drei Zeichen standen seit jeher im Dorfbuch und wurden nirgends erklärt
  ["Zeichen im Dorfbuch", [
    ["★", "sein Fach – bringt mehr"], ["⚔", "als Wache eingeteilt"], ["✚", "verwundet"],
  ]],
  // Was eine Gruppe überhaupt ist, stand bisher nur in einem Kommentar im Quelltext
  ["Was Gruppen sind", [
    ["1–4", "Bewohner einer Gruppe zuteilen"], ["Z", "die ganze Gruppe auf eine Aufgabe"],
    ["⚔", "nachts kämpft eine Gruppe zusammen"], ["+", "wer oft zusammen kämpft, trifft besser"],
  ]],
];
let tastenOffen = false;
function tastenZeigen(an) {
  if (menueWechsel(an ? "tasten" : null) === false) return;
  tastenOffen = an;
  document.getElementById("tasten").hidden = !an;
  if (!an) return;
  document.getElementById("tastenliste").innerHTML = TASTENHILFE.map(([titel, zeilen]) =>
    `<div class="tastengruppe"><h4>${t(titel)}</h4>` + zeilen.map(([k, was]) =>
      `<div class="tastenzeile"><b>${k.split(" ").map(x => `<kbd>${t(x)}</kbd>`).join("")}</b><span>${t(was)}</span></div>`).join("")
    + `</div>`).join("");
}

const MENUEPUNKTE = [
  { text: () => t("Weiterspielen"), tun: () => spielmenueZeigen(false) },
  { text: () => t("Taschen"), taste: "I", tun: () => taschenZeigen(true) },
  { text: () => t("Werkbuch"), taste: "H", tun: () => werkbuchZeigen(true) },
  { text: () => t("Meilensteine"), taste: "J", tun: () => tagebuchZeigen(true) },
  { text: () => t("Dorfbuch"), taste: "V", tun: () => dorfbuchZeigen(true) },
  { text: () => t("Dorfkarte"), taste: "E", tun: () => dorfkarteZeigen(true) },
  { text: () => t("Bauprojekte"), taste: "C", tun: () => { spielmenueZeigen(false); baulisteZeigen(true); } },
  { text: () => t("Tastenübersicht"), taste: "?", tun: () => { spielmenueZeigen(false); tastenZeigen(true); } },
  { text: () => t("Mauer bauen"), taste: "Z", tun: () => { spielmenueZeigen(false); mauerStarten(); } },
  { text: () => t("Tore: {0}", typeof toreZu === "function" && toreZu() ? t("verriegelt") : t("offen")), taste: "O",
    tun: () => { toreSchalten(); spielmenueZeigen(true); } },
  { text: () => (typeof fallenBereit === "function" && fallenBereit()
      ? t("Falle stellen ({0} scharf)", fallenBereit()) : t("Falle stellen")),
    taste: "X", tun: () => { spielmenueZeigen(false); falleSetzen(); } },
  { text: () => t("Fähigkeiten"), taste: "L", tun: () => skillsZeigen(true, "spieler") },
  { text: () => (typeof offeneQuests === "function" && offeneQuests().length
      ? t("Aufträge ({0})", offeneQuests().length) : t("Aufträge")),
    taste: "Q", tun: () => questsZeigen(true) },
  { text: () => t("Dorf umbenennen: {0}", heimat().name), tun: () => dorfNameFragen() },
  // Ton und Bild stehen jetzt in den Einstellungen - hier lagen sie zwischen den Spielbefehlen
  { text: () => t("Einstellungen"), taste: "N",
    tun: () => { spielmenueZeigen(false); einstellungenZeigen(true, "welt"); } },
  { text: () => t("Speichern und zum Titelbild"), tun: () => {
    // Auch aus einer Stube heraus erreichbar - dann muss "innen" mit aufgeloest werden.
    // Sonst rendert die Schleife beim Weiterspielen weiter die Stube, waehrend die Figur
    // schon auf ihren Aussenkoordinaten steht: sie steckt fest, und die Tuer kommt nie.
    if (typeof innen !== "undefined" && innen) innen = null;
    ortMerken(true); Speicher.schreiben(stand); spielmenueZeigen(false); titelZeichnen(); startKnoepfe("haupt"); grundspracheHerstellen(); zeige("start");
  } },
];
// Die kurze Steuerungsliste im Spielmenue kommt aus TASTENHILFE - eine Quelle statt zweier,
// die auseinanderlaufen (A18). Gezeigt wird die Gruppe "Unterwegs": genau das, was man
// draussen braucht, samt Mauer, Tor und Falle. Alles Weitere steht unter ?.
function steuerungKurzFuellen() {
  const dl = document.getElementById("steuerkurz");
  if (!dl || typeof TASTENHILFE === "undefined") return;
  const gruppe = TASTENHILFE.find(g => g[0] === "Unterwegs");
  if (!gruppe) return;
  dl.innerHTML = gruppe[1].map(([k, was]) =>
    `<dt>${k.split(" ").map(x => `<kbd>${t(x)}</kbd>`).join("")}</dt><dd>${t(was)}</dd>`).join("");
}
function spielmenueZeigen(an) {
  if (menueWechsel(an ? "spielmenue" : null) === false) return;   // im Kampf bleibt alles zu
  spielmenueOffen = an;
  document.getElementById("spielmenue").hidden = !an;
  if (!an) return;
  steuerungKurzFuellen();
  const liste = document.getElementById("menueliste");
  liste.innerHTML = "";
  MENUEPUNKTE.forEach((m, i) => {
    const b = document.createElement("button");
    b.type = "button"; b.className = "menuepunkt" + (i === menueWahl ? " an" : "");
    // Ton und Bild sind ins Einstellungsmenue gewandert - hier lag noch die wortgleiche
    // Reglerlogik, die kein Menuepunkt mehr ausloest.
    b.innerHTML = `<span>${m.text()}</span>${m.taste ? `<kbd>${m.taste}</kbd>` : ""}`;
    b.addEventListener("click", () => { menueWahl = i; m.tun(); });
    liste.append(b);
  });
}
document.getElementById("menueknopf").addEventListener("click", () => {
  if (bildschirm === "welt") spielmenueZeigen(!spielmenueOffen);
});

// ---------------------------------------------------------------------------
// Tasten in der Welt (von spiel.js weitergereicht)
// ---------------------------------------------------------------------------
function weltTaste(ev, k) {
  if (abstecken && !gespraech && !menueOffen()) {
    if (innen) abstecken = false;                            // drinnen steckt man nichts ab
    else {
      const drehen = { ArrowUp: "N", ArrowDown: "S", ArrowRight: "O", ArrowLeft: "W" }[k];
      if (drehen) { absteckRichtung = drehen; return true; }
      if (k === "Enter" || k === "p") return absteckenBestaetigen() || true;
      if (k === "Escape" || k === "b") { abstecken = false; meldung(t("Abstecken abgebrochen.")); return true; }
      abstecken = false;                                     // alles andere beendet das Abstecken
    }
  }
  if (kampfOffen) return kampfTaste(k);
  if (karteOffen) return karteTaste(k);
  if (skillOffen) return skillTaste(k);
  if (questOffen) return questTaste(k);
  if (typeof baulisteOffen !== "undefined" && baulisteOffen) return baulisteTaste(k);
  if (mauerModus && !gespraech && !menueOffen()) {            // Mauer setzen laeuft wie das Abstecken
    if (k === "Enter" || k === "p") return mauerSetzen() || true;
    if (k === "ArrowLeft" || k === "ArrowRight") {           // Palisade -> Tor -> Steinmauer
      const i = MAUER_ARTEN.indexOf(mauerModus.art), d2 = k === "ArrowLeft" ? -1 : 1;
      mauerModus.art = MAUER_ARTEN[(i + d2 + MAUER_ARTEN.length) % MAUER_ARTEN.length];
      return true;
    }
    if (k === "Escape" || k === "z") { mauerModus = null; meldung(t("Mauerbau beendet."), 1800); return true; }
  }
  if (typeof einstellungenOffen !== "undefined" && einstellungenOffen) return einstellungenTaste(k) || true;
  if (dorfbuchOffen) return dorfTaste(k);
  if (handelOffen) return handelTaste(k);
  if (bauOffen) return bauTaste(k);
  if (reiseOffen) return reiseTaste(k);
  if (spielmenueOffen) {
    if (k === "Escape") spielmenueZeigen(false);
    else if (k === "ArrowUp" || k === "ArrowDown" || k === "w" || k === "s") {
      menueWahl = (menueWahl + (k === "ArrowUp" || k === "w" ? -1 : 1) + MENUEPUNKTE.length) % MENUEPUNKTE.length;
      spielmenueZeigen(true);
    } else if (k === "Enter" || k === " ") {
      MENUEPUNKTE[menueWahl].tun();
    }
    else { const m = MENUEPUNKTE.find(x => x.taste && x.taste.toLowerCase() === k); if (m) m.tun(); }
    return true;
  }
  if (tagebuchOffen) {
    if (k === "j" || k === "Escape") tagebuchZeigen(false);
    // Hoch/runter wechselt die Seite, links/rechts blättert in der Chronik
    else if (k === "ArrowUp" || k === "ArrowDown" || k === "w" || k === "s") {
      chronikStart = 0;
      tagebuchZeigen(true, tagebuchSeite === "chronik" ? "meilen" : "chronik");
    } else if (tagebuchSeite === "chronik" && (k === "ArrowLeft" || k === "a")) chronikBlaettern(-1);
    else if (tagebuchSeite === "chronik" && (k === "ArrowRight" || k === "d")) chronikBlaettern(1);
    return true;
  }
  if (taschenOffen) {
    if (k === "i" || k === "Escape" || k === "Tab") taschenZeigen(false);
    else if (k === "ArrowLeft" || k === "a") taschenBewegen(-1);
    else if (k === "ArrowRight" || k === "d") taschenBewegen(1);
    else if (k === "ArrowUp" || k === "w") taschenBewegen(-TASCHEN_SPALTEN);
    else if (k === "ArrowDown" || k === "s") taschenBewegen(TASCHEN_SPALTEN);
    else if (k === "Enter" || k === " ") { const tat = taschenTat(); if (tat) tat.tun(); }
    return true;
  }
  // Beim Zusehen holt jede Taste die Kamera zurueck
  if (zuschauen) { zuschauenEnde(); return true; }
  if (typeof lagerOffen !== "undefined" && lagerOffen) return lagerTaste(k);
  if (typeof ablageOffen !== "undefined" && ablageOffen && ablageTaste(k)) return true;
  if (tastenOffen) {                                         // Tastenuebersicht: nur wieder zu
    if (k === "?" || k === "ß" || k === "Escape") tastenZeigen(false);
    return true;
  }
  if (werkbuchOffen) {
    if (k === "h" || k === "Escape") werkbuchZeigen(false);
    else if (k === "ArrowUp" || k === "ArrowDown" || k === "w" || k === "s") {
      werkbuchWahl = (werkbuchWahl + (k === "ArrowUp" || k === "w" ? -1 : 1) + REZEPTE.length) % REZEPTE.length;
      werkbuchZeigen(true);
    } else if (k === "Enter" || k === " ") herstellen(REZEPTE[werkbuchWahl]);
    return true;
  }
  if (gespraech) {
    if (k === "ArrowUp" || k === "ArrowDown") return wahlBewegen(k === "ArrowUp" ? -1 : 1) || true;
    if (k === "t" || k === "Enter" || k === " ") weiterReden();
    else if (k === "Escape") { if (gespraech.wahl) gespraech.wahlIndex = gespraech.wahl.length - 1; gespraechEnde(); }
    return true;
  }
  if (innen) {                                               // in der Stube: einrichten, aufheben, hinaus
    if (stellen) {
      const schieben = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1],
                         a: [-1, 0], d: [1, 0], w: [0, -1], s: [0, 1] }[k];
      // Kippen und Spiegeln gibt es nicht mehr: die Perspektive der Blaetter laesst sich
      // nicht ehrlich drehen. Moebel stehen so, wie sie gezeichnet sind.
      if (schieben) { stellenSchieben(schieben[0], schieben[1]); return true; }
      if (k === " ") { stellenWechseln(1); return true; }
      if (k === "Enter") { stellenBestaetigen(); return true; }
      if (k === "Escape" || k === "b" || k === "p") { stellen = null; meldung(t("Fertig eingerichtet."), 1600); return true; }
      return true;                                           // nichts anderes stört das Einrichten
    }
    if (k === "t") {                                     // drinnen jemanden ansprechen
      const gast = typeof gastVorMir === "function" ? gastVorMir() : null;
      if (gast) { const n = { id: gast, name: person2(gast).name }; dorfReden(n); }
      else meldung(t("Hier ist niemand, mit dem du reden könntest."), 2200);
      return true;
    }
    if (k === " ") { moebelBenutzen(); return true; }     // Bett: schlafen · Stuhl: hinsetzen
    if (k === "b") { stellenStarten(); return true; }
    if (k === "p" || k === "Enter") {
      if (moebelAufheben()) return true;
      if (anDerTuer()) hausVerlassen();
      else meldung(t("Zum Hinausgehen zur Tür – mit B stellst du Möbel auf."), 2600);
      return true;
    }
    if (k === "Escape") { spielmenueZeigen(true); return true; }
    if (k === "i" || k === "Tab") { taschenZeigen(true); return true; }
    if (k === "h") { werkbuchZeigen(true); return true; }
    if (k === "j") { tagebuchZeigen(true); return true; }
    if (k === "l") { skillsZeigen(true, "spieler"); return true; }
    if (k === "m") { stumm = !stumm; meldung(stumm ? t("Klang aus") : t("Klang an")); klangAktualisieren(); return true; }
    if (k === "f") { vollbild(); return true; }
    if (GEHTASTEN[k] || k === "Shift") { tasten.add(k); return true; }
    return true;                                             // sonst passiert drinnen nichts
  }
  if (angeln && k !== "p" && !GEHTASTEN[k] && k !== "Shift") { angeln = null; meldung(t("Angel eingeholt.")); }
  if (k === "t" || k === "Enter") {
    const n = naechsterNpc();
    if (n) redenMit(n);
    else meldung(t("Hier ist niemand zum Reden – geh näher an jemanden heran."));
  }
  else if (k === "p") aufheben();
  else if (k === " ") springen();
  else if (k === "Escape") spielmenueZeigen(true);
  else if (k === "i" || k === "Tab") taschenZeigen(true);
  else if (k === "j") tagebuchZeigen(true);
  else if (k === "h") werkbuchZeigen(true);
  else if (k === "v") dorfbuchZeigen(true);
  else if (k === "e") dorfkarteZeigen(true);
  else if (k === "z") mauerStarten();
  else if (k === "n") schlafenDraussen();                  // Tag beenden: sich hinlegen
  else if (k === "g") lagerZeigen(true);                   // Lager: Gesammeltes abgeben
  else if (k === "?" || k === "ß") tastenZeigen(true);      // Tastenübersicht (auf deutscher Tastatur ⇧ß)
  else if (k === "o") toreSchalten();                      // Tore auf und zu
  else if (k === "x") falleSetzen();                       // Falle an der Grenze
  else if (k === "l") skillsZeigen(true, "spieler");
  else if (k === "q") questsZeigen(true);
  else if (k === "c") baulisteZeigen(true);                // was gerade gebaut wird
  else if (k === "f") vollbild();
  else if (k === "b") absteckenStarten();
  else if (k === "m") { stumm = !stumm; meldung(stumm ? t("Klang aus") : t("Klang an")); klangAktualisieren(); }
  else if (GEHTASTEN[k] || k === "Shift") tasten.add(k);
  else return false;
  return true;
}
document.addEventListener("keyup", ev => tasten.delete(ev.key.length === 1 ? ev.key.toLowerCase() : ev.key));
window.addEventListener("blur", () => tasten.clear());

// ---------------------------------------------------------------------------
// Regen: schraege Pixelstriche und ein grauer Schleier
// ---------------------------------------------------------------------------
const TROPFEN = Array.from({ length: 140 }, (_, i) => ({ x: (i * 97) % 640, y: (i * 53) % 448, v: 170 + (i * 31) % 90 }));
function regenZeichnen(c, t) {
  c.fillStyle = "rgba(60, 70, 90, .18)"; c.fillRect(0, 0, 640, 448);
  c.fillStyle = "rgba(190, 210, 235, .55)";
  for (const d of TROPFEN) {
    const y = (d.y + t * d.v / 1000) % 470 - 20, x = (d.x - y * 0.25 + 640) % 640;
    c.fillRect(Math.round(x), Math.round(y), 1, 4);
  }
}

// ---------------------------------------------------------------------------
// Zaehler und Meilensteine
// ---------------------------------------------------------------------------
function zaehle(was, n = 1) {
  stand.zaehler = stand.zaehler || {};
  stand.zaehler[was] = (stand.zaehler[was] || 0) + n;
}
const nachbarn = () => (typeof leute === "function" ? leute().filter(id => id !== "spieler") : []);
const zahl = was => (stand.zaehler && stand.zaehler[was]) || 0;
// Meilensteine in Ketten - wie eine Fortschrittstafel: jede Kette baut aufeinander auf
const MEILENSTEINE = [
  { kette: "Ankunft", id: "angekommen", name: "Angekommen",      text: "Im Dorf angekommen",                 ok: () => true },
  { kette: "Ankunft", id: "blume",      name: "Erste Beute",     text: "Das Erste aufgehoben",               ok: () => zahl("gepflueckt") >= 1 },
  { kette: "Ankunft", id: "strauss",    name: "Sammelfreude",    text: "50 Dinge gesammelt",                 ok: () => zahl("gepflueckt") >= 50 },
  { kette: "Ankunft", id: "stein",      name: "Platz geschaffen", text: "Fünf Hindernisse geräumt",           ok: () => zahl("geraeumt") >= 5 },
  { kette: "Handwerk", id: "werk",      name: "Erstes Werkstück", text: "Etwas im Werkbuch hergestellt",     ok: () => zahl("hergestellt") >= 1 },
  { kette: "Handwerk", id: "moebel",    name: "Möbelbauer",      text: "Ein Möbel aufgestellt",              ok: () => zahl("moebel") >= 1 },
  // Gezaehlt wird, was dasteht - nicht, wie oft je etwas hingestellt wurde. Vorher
  // reichte ein einziger Hocker, fuenfmal hin- und weggeraeumt.
  { kette: "Handwerk", id: "stube",     name: "Wohnlich",        text: "Fünf Möbel in deiner Stube",         ok: () => ((stand.moebel && stand.moebel.spieler) || []).length >= 5 },
  { kette: "Dach", id: "haus",          name: "Eigenes Dach",    text: "Das eigene Haus gebaut",             ok: () => !!(stand.bau && stand.bau.spieler && stand.bau.spieler.fertig) },
  { kette: "Dach", id: "dorf",          name: "Ein Dorf",        text: "Alle Bewohner haben ein Haus",       ok: () => leute().every(w => stand.bau && stand.bau[w] && stand.bau[w].fertig) },
  { kette: "Dach", id: "gast",          name: "Zu Gast",         text: "Ein fremdes Haus betreten",          ok: () => zahl("besucht") >= 1 },
  { kette: "Wehr", id: "mauer1",        name: "Erster Pfahl",    text: "Ein Mauerstück gesetzt",             ok: () => Object.keys((stand.dorf && stand.dorf.mauerteile) || {}).length >= 1 },
  { kette: "Wehr", id: "mauer2",        name: "Umfriedet",       text: "Die Grenze ganz gedeckt",            ok: () => typeof deckung === "function" && deckung() >= 0.99 },
  { kette: "Wehr", id: "mauer3",        name: "Bollwerk",        text: "Eine Nacht ohne Durchbruch",         ok: () => zahl("ohnedurchbruch") >= 1 },
  { kette: "Nächte", id: "nacht1",      name: "Durchgehalten",   text: "Die erste Nacht überstanden",        ok: () => dorf().naechte >= 1 },
  { kette: "Nächte", id: "nacht7",      name: "Eine Woche",      text: "Sieben Nächte überstanden",          ok: () => dorf().naechte >= 7 },
  { kette: "Nächte", id: "nacht20",     name: "Standhaft",       text: "Zwanzig Nächte überstanden",         ok: () => dorf().naechte >= 20 },
  { kette: "Kampf", id: "kampf1",       name: "Erster Sieg",     text: "Ein Monster erlegt",                 ok: () => zahl("monster") >= 1 },
  { kette: "Kampf", id: "kampf10",      name: "Geübt",           text: "Zehn Monster erlegt",                ok: () => zahl("monster") >= 10 },
  { kette: "Kampf", id: "anfuehrer",    name: "Kopf der Horde",  text: "Einen Anführer besiegt",             ok: () => zahl("anfuehrer") >= 1 },
  { kette: "Land", id: "feld1",         name: "Erstes Feld",     text: "Eine Parzelle eingenommen",          ok: () => typeof felder === "function" && felder().length > START_FELDER },
  { kette: "Land", id: "feld12",        name: "Gewachsen",       text: "Zwölf Parzellen",                    ok: () => typeof felder === "function" && felder().length >= 12 },
  { kette: "Land", id: "karte",         name: "Die ganze Karte", text: "Alles gehört dem Dorf",              ok: () => typeof felder === "function" && felder().length >= parzAnzahl() },
  { kette: "Leute", id: "freund",       name: "Beste Freunde",   text: "Mit jemandem richtig befreundet",    ok: () => Object.values(stand.freundschaft || {}).some(v => v >= 10) },
  { kette: "Leute", id: "volk",         name: "Zulauf",          text: "Acht Bewohner im Dorf",              ok: () => lebende().length >= 8 },
  { kette: "Leute", id: "meister",      name: "Meisterschaft",   text: "Jemand auf Klassenstufe 5",          ok: () => lebende().some(id => Object.values(wert(id).stufe || {}).some(v => v >= 5)) },
];
function meilensteinePruefen() {
  stand.meilensteine = stand.meilensteine || [];
  const neu = MEILENSTEINE.filter(m => !stand.meilensteine.includes(m.id) && m.ok());
  if (!neu.length) return;
  neu.forEach(m => stand.meilensteine.push(m.id));
  speichernBald();
  setTimeout(() => { meldung(t("Meilenstein: {0}!", neu[neu.length - 1].name), 3000); geraeusch("fanfare"); }, 900);
}

// ---------------------------------------------------------------------------
// Lichtungs-Tagebuch (J): Nachbarn mit Herzen, Meilensteine
// ---------------------------------------------------------------------------
let tagebuchOffen = false;
// Das Tagebuch hat zwei Seiten: die Meilensteine und die Dorfchronik (Vorschlag V16).
// Zwei Seiten statt einer neuen Taste - die Tastatur ist voll genug.
let tagebuchSeite = "meilen";
function tagebuchZeigen(an, seite) {
  if (seite) tagebuchSeite = seite;
  menueWechsel(an ? "meilen" : null);
  tagebuchOffen = an;
  document.getElementById("meilen").hidden = !an;
  if (!an) return;
  const baum = document.getElementById("meilenbaum");
  baum.innerHTML = "";
  document.getElementById("meilentitel").textContent = tagebuchSeite === "chronik" ? t("Dorfchronik") : t("Meilensteine");
  if (tagebuchSeite === "chronik") return chronikMalen(baum);
  const erreicht = stand.meilensteine || [];
  const ketten = [...new Set(MEILENSTEINE.map(m => m.kette))];
  for (const kette of ketten) {
    const reihe = document.createElement("div");
    reihe.className = "mkette";
    MEILENSTEINE.filter(m => m.kette === kette).forEach((m, i, liste) => {
      const hat = erreicht.includes(m.id);
      const offen = !hat && (i === 0 || erreicht.includes(liste[i - 1].id));
      const z = document.createElement("div");
      z.className = "mstein" + (hat ? " erreicht" : offen ? " offen" : "");
      z.innerHTML = `<b>${hat || offen ? m.name : "???"}</b><small>${hat || offen ? m.text : t("erst das Feld davor")}</small>`;
      reihe.append(z);
    });
    baum.append(reihe);
  }
  // Wen man draussen getroffen hat. Diese Liste wurde bei jedem Gespraech mitgeschrieben
  // und nirgends gelesen - jetzt steht sie hier, wo sie hingehoert.
  const bekannte = Object.values(stand.bekannte || {});
  if (bekannte.length) {
    const welten = [...new Set(bekannte.map(b => b.welt))];
    const reihe = document.createElement("div");
    reihe.className = "mkette bekannte";
    const kopf = document.createElement("div");
    kopf.className = "mstein erreicht";
    kopf.innerHTML = `<b>${t("Bekannte")}</b><small>${
      // "in 2 Welten" stammte aus der Reisezeit - es sind Nachbardoerfer auf derselben Karte.
      bekannte.length === 1 ? t("eine Person aus dem Nachbardorf")
                            : welten.length === 1 ? t("{0} Leute aus dem Nachbardorf", bekannte.length)
                            : t("{0} Leute aus {1} Nachbardörfern", bekannte.length, welten.length)}</small>`;
    reihe.append(kopf);
    for (const b of bekannte.slice(-4)) {
      const z = document.createElement("div");
      z.className = "mstein offen";
      z.innerHTML = `<b>${b.name}</b><small>${b.welt}</small>`;
      reihe.append(z);
    }
    baum.append(reihe);
  }
  document.getElementById("meilenfuss").textContent =
    t("{0} von {1} erreicht · {2} gesammelt · {3} Monster erlegt",
      erreicht.length, MEILENSTEINE.length, zahl("gepflueckt"), zahl("monster"));
}
// Die Chronik, neuestes zuerst - so liest man von der Gegenwart rückwärts. Seitenweise:
// nichts scrollt, und wie viel auf eine Seite passt, wird gemessen statt geraten - ein Block
// ist je nach Ereignissen unterschiedlich hoch, und das Feld je nach Fenster verschieden groß.
let chronikStart = 0;
function chronikBlock(e) {
  const block = document.createElement("div");
  block.className = "chronikblock" + (e.art === "nacht" ? " nacht" : "");
  const kopf = e.art === "nacht"
    ? (e.naechte === 1 ? t("Nach der 1. Nacht") : t("Nach der {0}. Nacht", e.naechte))
    : t("Tag {0}", e.tag);
  block.innerHTML = `<b>${kopf}</b>` + e.zeilen.map(z => `<span>${z}</span>`).join("");
  return block;
}
function chronikMalen(baum) {
  const d = typeof dorf === "function" ? dorf() : null;
  const alle = ((d && d.chronik) || []).slice().reverse();      // neuestes zuerst
  const fuss = document.getElementById("meilenfuss");
  if (!alle.length) {
    const z = document.createElement("div");
    z.className = "mstein offen";
    z.innerHTML = `<b>${t("Noch nichts geschehen")}</b><small>${t("Hier steht, was das Dorf erlebt hat – ab dem ersten Abend.")}</small>`;
    baum.append(z);
    fuss.textContent = t("noch keine Einträge");
    return;
  }
  chronikStart = Math.max(0, Math.min(chronikStart, alle.length - 1));
  let i = chronikStart;
  while (i < alle.length) {
    const block = chronikBlock(alle[i]);
    baum.append(block);
    if (baum.scrollHeight > baum.clientHeight && i > chronikStart) { block.remove(); break; }
    i++;
  }
  chronikEnde = i;                                             // erster Abschnitt, der nicht mehr passte
  const seite = chronikStart === 0 ? 1 : 2;                    // grob: erste Seite oder weiter hinten
  fuss.textContent = i >= alle.length && chronikStart === 0
    ? t("{0} Abschnitte · ältestes zuletzt", alle.length)
    : t("{0}–{1} von {2} · ältestes zuletzt", chronikStart + 1, i, alle.length);
}
let chronikEnde = 0;
// Weiter heißt: älter. Zurück füllt von hinten, damit die Seiten nicht verrutschen.
function chronikBlaettern(richtung) {
  const d = typeof dorf === "function" ? dorf() : null;
  const anzahl = ((d && d.chronik) || []).length;
  if (richtung > 0) { if (chronikEnde < anzahl) chronikStart = chronikEnde; }
  else if (chronikStart > 0) {
    // So weit zurück, wie auf eine Seite passt: probeweise von hinten füllen
    const baum = document.getElementById("meilenbaum");
    const alle = ((d && d.chronik) || []).slice().reverse();
    const merk = baum.innerHTML;
    baum.innerHTML = "";
    let j = chronikStart - 1;
    while (j >= 0) {
      const block = chronikBlock(alle[j]);
      baum.prepend(block);
      if (baum.scrollHeight > baum.clientHeight && j < chronikStart - 1) { block.remove(); j++; break; }
      j--;
    }
    baum.innerHTML = merk;
    chronikStart = Math.max(0, j);
  }
  tagebuchZeigen(true, "chronik");
}

// ---------------------------------------------------------------------------
// Dorf umbenennen (im Menue, Eingabefeld anstelle der Liste) und Vollbild
// ---------------------------------------------------------------------------
function dorfNameFragen() {
  const liste = document.getElementById("menueliste");
  liste.innerHTML = "";
  const form = document.createElement("form");
  form.className = "dorfform";
  form.innerHTML = `<label for="dorfname">${t("Wie soll dein Dorf heißen?")}</label>
    <input id="dorfname" maxlength="20" autocomplete="off" spellcheck="false">
    <div class="knopfreihe"><button class="knopf" type="submit">${t("Übernehmen")}</button><button class="knopf hell" type="button">${t("Abbrechen")}</button></div>`;
  const feld = form.querySelector("input");
  feld.value = heimat().name;
  const zurueck = () => { spielmenueZeigen(true); };
  form.addEventListener("submit", ev => {
    ev.preventDefault();
    const name = feld.value.trim().replace(/\s+/g, " ");
    if (!name) return feld.focus();
    stand.dorfname = name;
    heimat().name = name;
    if (aktWelt && aktWelt.heim) document.getElementById("ortname").textContent = `${name} · ${biomVon(aktWelt.biom).name}`;
    speichernBald();
    meldung(t("Dein Dorf heißt jetzt {0}.", name));
    zurueck();
  });
  form.querySelector("button[type=button]").addEventListener("click", zurueck);
  feld.addEventListener("keydown", ev => { if (ev.key === "Escape") { ev.preventDefault(); zurueck(); } });
  liste.append(form);
  setTimeout(() => { feld.focus(); feld.select(); }, 0);
}
function vollbild() {
  const el = document.documentElement;
  try {
    if (document.fullscreenElement) document.exitFullscreen();
    else if (el.requestFullscreen) el.requestFullscreen().catch(() => meldung(t("Vollbild ist hier nicht erlaubt – im Browser F11 (Mac: Ctrl+Cmd+F) hilft.")));
    else meldung(t("Vollbild wird von diesem Browser nicht unterstützt."));
  } catch (e) { meldung(t("Vollbild ist hier nicht erlaubt.")); }
}
