// Figurenschmiede: Figur aus den Atlanten zusammensetzen (Teil von index.html).
// Globale Namen werden von welt.js und spiel.js mitbenutzt (klassische Skripte).
// ---------------------------------------------------------------------------
// Daten: atlas/daten.js (erzeugt von baue_atlas.py). Je Seite und Layer-Gruppe
// ein Atlas, die Farbvarianten untereinander, je 512 px hoch.
// ---------------------------------------------------------------------------
const D = window.ATLAS;
const FRAME = 64, BLATT = 512;
const vorhanden = (seite, gruppe) => gruppe in (D.gruppen[seite] || {});

// Zeilen im Blatt: obere Haelfte Sued/Nord/Ost/West = 0-3, untere = 4-7
const RICHTUNGEN = [{ name: "Süd", z: 0 }, { name: "Ost", z: 2 }, { name: "Nord", z: 1 }, { name: "West", z: 3 }];

// Namen, Posen und NPC-Vorlagen kommen aus daten.js (Quelle: baue_atlas.py, npcs.json).
// Eine Pose ist eine Folge von Frames {c: Spalte, t: ms, dx: nach vorn, dy: nach unten}.
// Posen, die eine Waffe oder einen Schild brauchen, entfallen - Ausruestung gibt es in der Schmiede nicht
// Posen, die ohne Waffe und Schild sinnlos aussehen - die Schmiede zeigt keine
// Ausrüstung. "Kampf-Idle" stand bis zum 22.10. trotzdem an erster Stelle (Indie:
// "warum gibt es Kampf-Idle noch?"): eine Kampfhaltung mit leeren Händen, und als
// ERSTES, was man beim Öffnen der Schmiede sieht. Jetzt beginnt die Liste mit
// "Stehen". Die Pose wird nirgends gespeichert (START.pose = 0, und eine geladene
// Figur bekommt pose: 0), ein Verschieben der Liste bricht also keinen Spielstand.
const OHNE_AUSRUESTUNG = ["Kampf-Idle", "Waffe ziehen", "Parieren", "Schlag A", "Schlag B", "Stich", "Schildstoß"];
const POSEN = D ? D.posen.filter(p => !OHNE_AUSRUESTUNG.includes(p.name)) : [];
const HAUTTOENE = D ? D.hauttoene : [];
const HAARFARBEN = D ? D.haarfarben : [];
const FRISUREN = D ? D.frisuren : [];
const HUETE = D ? D.huete : [];
const WAFFEN = D ? D.waffen : [];
const SCHILDE = D ? D.schilde : [];
const NPCS = D ? D.npcs : [];
const farben = n => Array.from({ length: n }, (_, i) => `Farbe ${i + 1}`);
// Die fünf Farben je Kopfbedeckung hießen bis zum 22.10. "Farbe 1" bis "Farbe 5" —
// der Spieler sah also fünfmal dasselbe Wort und musste durchblättern, um zu wissen,
// was er wählt. Die Namen sind am Blatt GEMESSEN (häufigster Ton je Variante, Kontur
// ausgenommen): Haube #58544e #3f3d41 #706e6c #d2c4a8 #765436,
// Spitzhut #523c2a #baac8e #5c6c46 #aa7e3e #8e5838. Beide Hüte haben eigene Paletten,
// eine gemeinsame Liste kann es also nicht geben.
const HUTFARBEN = {
  pfht: ["Schiefer", "Anthrazit", "Steingrau", "Leinen", "Nussbraun"],
  pnty: ["Rinde", "Leinen", "Moos", "Ocker", "Lehm"],
};
const hutfarbenVon = form => HUTFARBEN[form] || farben(5);
const BERUFE = D ? D.berufe : [];

// Kleidung aus daten.js (baue_atlas.py): je Bereich Kleidungsstuecke mit benannten
// Farben, jede Farbe zeigt auf (Atlas-Gruppe, Index). "Komplett" nutzt die Outfit-Namen
// (0 = keins, 1 = Foerster, 2 = Bauer, ab 3 = Berufe), die Einzelteile eigene Namen;
// gleiche Form in anderen Farben ist ein Stueck (z. B. alle Stiefel).
const KEINS = { name: "keins", farben: [] };
const TEILE = [["ober", "Oberteil"], ["hose", "Hose"], ["schuhe", "Schuhe"]];
const OPTIONEN = D ? {
  outfit: [KEINS, ...D.komplett],
  ober: [KEINS, ...D.teile.ober], hose: [KEINS, ...D.teile.hose], schuhe: [KEINS, ...D.teile.schuhe],
  acc: [KEINS, ...D.teile.acc],
} : {};
const wahl = k => OPTIONEN[k][Math.min(s[k], OPTIONEN[k].length - 1)];
const quelle = (opt, farbe) => { const f = opt.farben[Math.min(farbe, opt.farben.length - 1)]; return [f.gruppe, f.index]; };

// ---------------------------------------------------------------------------
// Zustand und Eigenschaften
// ---------------------------------------------------------------------------
const START = { vorlage: 0, pose: 0, richtung: 0, haut: 3, unterw: 0,
  art: 0, outfit: 2, outfitFarbe: 1,
  ober: 2, oberFarbe: 1, hose: 2, hoseFarbe: 5, schuhe: 1, schuheFarbe: 12,
  acc: 0, accFarbe: 0,
  frisur: 2, haarfarbe: 10, hut: 0, hutfarbe: 1,
  waffe: 0, waffenfarbe: 0, schild: 0, schildfarbe: 0,          // Ausruestung ist nicht waehlbar
  ambiente: 1, schritte: 1 };
const s = { ...START };

const komplett = () => s.art === 0;
const berufKomplett = () => komplett() && !!wahl("outfit").beruf;
const hutOptionen = () => ["keine", ...HUETE.map(h => h[1]), ...BERUFE.map(b => `${b.name}-Hut`)];

const EIGENSCHAFTEN = [
  { gruppe: "Figur" },
  { key: "vorlage",   name: "Vorlage",       optionen: () => ["eigene", ...NPCS.map(n => n.name)] },
  { key: "pose",      name: "Pose",          optionen: () => POSEN.map(p => p.name) },
  // "Blickrichtung" und "Kopfbedeckung" passten nicht in die Namensspalte: gemessen am
  // 22.10. braucht "Blickrichtung" 142 px und "Kopfbedeckung" 152, sichtbar sind 118.
  // Der Spieler las BLICKRICHT… und KOPFBEDEC…. Die Spalte breiter zu machen ginge nur
  // auf Kosten der Werte (da steht mit "Kräuterfrau-Hut" schon das längste Wort knapp),
  // also heißen die beiden Zeilen jetzt kurz - in ihrer Gruppe ist beides eindeutig.
  { key: "richtung",  name: "Richtung",      optionen: () => RICHTUNGEN.map(r => r.name) },
  { key: "haut",      name: "Haut",          optionen: () => HAUTTOENE },
  { gruppe: "Kleidung" },
  { key: "art",       name: "Outfit",        optionen: () => ["Komplett", "Einzelteile"] },
  ...[["outfit", "Kleidung", komplett], ...TEILE.map(([k, n]) => [k, n, () => !komplett()]), ["acc", "Accessoire"]]
    .flatMap(([k, name, sicht]) => [
      { key: k, name, optionen: () => OPTIONEN[k].map(o => o.name), unter: k !== "acc", sichtbar: sicht },
      { key: k + "Farbe", name: "Farbe", unter: true, sichtbar: sicht,
        optionen: () => wahl(k).farben.map(f => f.name), aktiv: () => wahl(k).farben.length > 1,
        grund: () => wahl(k).beruf ? "durch Beruf" : "–" },
    ]),
  { gruppe: "Kopf" },
  { key: "frisur",    name: "Frisur",        optionen: () => ["keine", ...FRISUREN.map(f => f[1])] },
  { key: "haarfarbe", name: "Farbe",         optionen: () => HAARFARBEN, unter: true, aktiv: () => s.frisur > 0 },
  { key: "hut",       name: "Hut",           optionen: hutOptionen },
  { key: "hutfarbe",  name: "Farbe",
    optionen: () => hutfarbenVon(s.hut >= 1 && s.hut <= 2 ? HUETE[s.hut - 1][0] : null), unter: true,
    aktiv: () => s.hut === 1 || s.hut === 2, grund: () => s.hut > 2 ? "durch Beruf" : "–" },
  { gruppe: "Klang" },
  { key: "schritte",  name: "Schritte",      optionen: () => KLANG.schritte.map(k => k.name) },
];
const ZEILEN = EIGENSCHAFTEN.filter(e => e.key);
const sichtbar = e => !e.sichtbar || e.sichtbar();
const waehlbar = e => sichtbar(e) && (!e.aktiv || e.aktiv());

// ---------------------------------------------------------------------------
// Atlanten laden. Die Seite der aktuellen Pose zuerst, der Rest im Hintergrund.
// ---------------------------------------------------------------------------
const bilder = new Map();
function atlas(seite, gruppe) {
  const pfad = `atlas/${seite}_${gruppe}.png`;
  if (!bilder.has(pfad)) {
    const img = new Image();
    const eintrag = { img, fertig: false };
    eintrag.bereit = new Promise(ok => {
      img.onload = () => { eintrag.fertig = true; ok(); };
      img.onerror = () => { console.warn("Fehlt:", pfad); ok(); };
    });
    img.src = pfad;
    bilder.set(pfad, eintrag);
  }
  return bilder.get(pfad);
}
const ladeAnz = document.getElementById("laden");
async function seiteLaden(seite) {
  const alle = Object.keys(D.gruppen[seite]).map(g => atlas(seite, g));
  if (alle.every(e => e.fertig)) return;
  ladeAnz.classList.add("an");
  await Promise.all(alle.map(e => e.bereit));
  ladeAnz.classList.remove("an");
}

// ---------------------------------------------------------------------------
// Zeichnen. Ein Frame = Koerper (Haut, Unterwaesche, Kleidung, Haar, Hut) plus
// Waffe und Schild davor oder dahinter, je Frame laut Anleitungsblatt.
// ---------------------------------------------------------------------------
const puffer = () => { const c = document.createElement("canvas"); c.width = c.height = FRAME; return c.getContext("2d"); };
const koerperCtx = puffer(), teilCtx = puffer();

function ebene(ctx, seite, gruppe, index, zeile, spalte) {
  if (!vorhanden(seite, gruppe)) return false;
  const e = atlas(seite, gruppe);
  if (!e.fertig) return false;
  ctx.drawImage(e.img, spalte * FRAME, index * BLATT + zeile * FRAME, FRAME, FRAME, 0, 0, FRAME, FRAME);
  return true;
}

// Ebene nur innerhalb ("destination-in") oder ausserhalb ("destination-out") einer Maske zeichnen
function maskiert(ziel, seite, gruppe, index, zeile, spalte, maske, modus) {
  teilCtx.globalCompositeOperation = "source-over";
  teilCtx.clearRect(0, 0, FRAME, FRAME);
  if (!ebene(teilCtx, seite, gruppe, index, zeile, spalte)) return;
  if (maske) {
    teilCtx.globalCompositeOperation = modus;
    ebene(teilCtx, seite, maske[0], maske[1], zeile, spalte);
    teilCtx.globalCompositeOperation = "source-over";
  }
  ziel.drawImage(teilCtx.canvas, 0, 0);
}

function hutWahl() {
  if (!s.hut) return null;
  if (s.hut <= 2) { const f = HUETE[s.hut - 1][0]; return { gruppe: `hut_${f}`, index: s.hutfarbe, form: f }; }
  const b = BERUFE[s.hut - 3];
  return { gruppe: "hut_beruf", index: s.hut - 3, form: b.hut.slice(0, 4) };
}

function zeichneKoerper(seite, zeile, spalte) {
  const k = koerperCtx;
  k.clearRect(0, 0, FRAME, FRAME);
  ebene(k, seite, "haut", s.haut, zeile, spalte);
  // Keine Unterwaesche-Schicht mehr: der neue Figurenkoerper bringt die Shorts mit,
  // die Schicht lag seit dem 09.10. doppelt darueber.
  if (komplett()) {
    if (s.outfit) { const [g, i] = quelle(wahl("outfit"), s.outfitFarbe); ebene(k, seite, g, i, zeile, spalte); }
  } else {
    // Schuhe zuerst, Oberteil zuletzt (Mantel und Kleid liegen ueber der Hose).
    // Komplett-Outfits werden auf ihren Bereich zugeschnitten, neue Stuecke passen schon.
    [["schuhe", 2], ["hose", 1], ["ober", 0]].forEach(([t, m]) => {
      if (!s[t]) return;
      const opt = wahl(t), [g, i] = quelle(opt, s[t + "Farbe"]);
      if (opt.farben[Math.min(s[t + "Farbe"], opt.farben.length - 1)].maske)
        maskiert(k, seite, g, i, zeile, spalte, ["maske_teile", m], "destination-in");
      else ebene(k, seite, g, i, zeile, spalte);
    });
  }
  if (s.acc) { const [g, i] = quelle(wahl("acc"), s.accFarbe); ebene(k, seite, g, i, zeile, spalte); }
  const hut = hutWahl();
  if (s.frisur) {
    // Unter einem Hut verschwindet alles Haar, das oberhalb der Krempe hervorschauen wuerde
    const maske = hut ? ["maske_hut", HUETE.findIndex(h => h[0] === hut.form)] : null;
    maskiert(k, seite, `haar_${FRISUREN[s.frisur - 1][0]}`, s.haarfarbe, zeile, spalte, maske, "destination-out");
  }
  if (hut) ebene(k, seite, hut.gruppe, hut.index, zeile, spalte);
}

// Felskante fuer "Runterspringen": Oberkante unter den Fuessen der Startposition,
// Landung D.kante.tiefe px unter Normalboden. Von der Seite endet die Kante vor der Figur.
function zeichneKante(ctx, groesse, richtung) {
  const u = groesse / FRAME, K = D.kante, oben = 44 - K.hoehe, unten = 44 + K.tiefe;
  const r = (x, y, w, hh, farbe) => { ctx.fillStyle = farbe; ctx.fillRect(x * u, y * u, w * u, hh * u); };
  const name = RICHTUNGEN[richtung].name;
  const [x0, x1] = name === "Ost" ? [0, 37] : name === "West" ? [27, 64] : [0, 64];
  r(x0, oben, x1 - x0, 1, "#90c070");
  r(x0, oben + 1, x1 - x0, 1, "#608860");
  r(x0, oben + 2, x1 - x0, unten - oben - 2, "#806838");
  for (let x = x0 + 2; x < x1; x += 5) r(x, oben + 3, 1, unten - oben - 4, "#584020");
  r(x0, unten, x1 - x0, 1, "#181818");
  if (name === "Ost") r(x1 - 1, oben, 1, unten - oben, "#181818");
  if (name === "West") r(x0, oben, 1, unten - oben, "#181818");
}

// ---------------------------------------------------------------------------
// Pixel-Kulisse: Wiese (64x64, einmal vorgerendert), Schatten, Efeu am Rahmen.
// Fester Zufallsstartwert - sieht bei jedem Laden gleich aus.
// ---------------------------------------------------------------------------
function zufallsquelle(saat) {
  return () => { saat |= 0; saat = saat + 0x6D2B79F5 | 0; let t = Math.imul(saat ^ saat >>> 15, 1 | saat);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const WIESE = (() => {
  const c = document.createElement("canvas"); c.width = c.height = FRAME;
  const g = c.getContext("2d"), r = zufallsquelle(7);
  const px = (x, y, f) => { g.fillStyle = f; g.fillRect(x, y, 1, 1); };
  g.fillStyle = "#4e7d34"; g.fillRect(0, 0, FRAME, FRAME);
  for (let y = 0; y < FRAME; y++) for (let x = 0; x < FRAME; x++) {
    const z = r();
    if (z < .12) px(x, y, "#457230"); else if (z < .2) px(x, y, "#588a3b");
  }
  for (let i = 0; i < 38; i++) {             // Grasbueschel
    const x = Math.floor(r() * 62) + 1, y = Math.floor(r() * 62) + 1;
    px(x, y, "#6fa048"); px(x - 1, y + 1, "#355c24"); px(x + 1, y + 1, "#355c24"); px(x, y + 1, "#88b85a");
  }
  const blumen = ["#e8d25a", "#e8e0d0", "#d86a6a", "#b890e0"];
  for (let i = 0; i < 9; i++) {
    const x = Math.floor(r() * 60) + 2, y = Math.floor(r() * 60) + 2, f = blumen[i % blumen.length];
    px(x, y, f); px(x - 1, y, f); px(x + 1, y, f); px(x, y - 1, f); px(x, y + 1, "#2d5a27");
  }
  return c;
})();

function zeichneSchatten(ctx, u) {
  ctx.fillStyle = "rgba(20, 30, 10, .35)";
  [[26, 43, 13], [24, 44, 17], [26, 45, 13]].forEach(([x, y, w]) => ctx.fillRect(x * u, y * u, w * u, u));
}

// Efeu: Ranke entlang zweier Kanten ab einer Ecke, mit Blaettern und haengenden Trieben.
// Gezeichnet in Rahmen-Pixeln (3 px), damit er zur Pixeloptik passt.
const EFEU_P = 3;
function efeu(ziel, saat, ecken) {
  let c = ziel.querySelector(":scope > canvas.efeu");
  if (!c) { c = document.createElement("canvas"); c.className = "efeu"; ziel.prepend(c); }
  const rand = 4;                                   // Efeu darf ueber den Rand wachsen
  const W = Math.ceil(ziel.clientWidth / EFEU_P) + 2 * rand, H = Math.ceil(ziel.clientHeight / EFEU_P) + 2 * rand;
  c.width = W; c.height = H;
  Object.assign(c.style, { left: `${-rand * EFEU_P}px`, top: `${-rand * EFEU_P}px`,
    width: `${W * EFEU_P}px`, height: `${H * EFEU_P}px` });
  const g = c.getContext("2d"), r = zufallsquelle(saat);
  const px = (x, y, f) => { g.fillStyle = f; g.fillRect(Math.round(x), Math.round(y), 1, 1); };
  const blatt = (x, y) => {
    px(x, y - 1, "#86b85a"); px(x - 1, y, "#86b85a"); px(x, y, "#4f8a3b"); px(x + 1, y, "#2d5a27");
    px(x, y + 1, "#2d5a27"); px(x - 1, y - 1, "#1b2e14"); px(x + 1, y + 1, "#1b2e14");
  };
  const ranke = (x, y, dx, dy, laenge) => {
    let quer = 0;
    for (let i = 0; i < laenge; i++) {
      if (r() < .3) quer = Math.max(-1, Math.min(1, quer + (r() < .5 ? -1 : 1)));
      x += dx + (dy ? quer * .5 : 0); y += dy + (dx ? quer * .5 : 0);
      px(x, y, "#2a4a1e");
      if (i % 3 === 0 && r() < .85) {
        const s = r() < .5 ? -1 : 1;
        blatt(x + (dy ? s * 2 : 0), y + (dx ? s * 2 : 0));
      }
      if (dx && i % 11 === 5 && r() < .7) {           // haengender Trieb
        let hx = x, hy = y;
        const n = 3 + Math.floor(r() * 7);
        for (let k = 0; k < n; k++) { hy += 1; px(hx, hy, "#2a4a1e"); if (k % 3 === 2) blatt(hx + (k % 2 ? 1 : -1), hy); }
      }
    }
  };
  ecken.forEach(([ecke, quer, laengs]) => {
    const x = ecke.includes("l") ? rand + 1 : W - rand - 2, y = ecke.includes("o") ? rand + 1 : H - rand - 2;
    const sx = ecke.includes("l") ? 1 : -1, sy = ecke.includes("o") ? 1 : -1;
    ranke(x, y, sx, 0, Math.floor((W - 2 * rand) * quer));
    ranke(x, y, 0, sy, Math.floor((H - 2 * rand) * laengs));
    for (let i = 0; i < 5; i++) blatt(x + sx * (r() * 4), y + sy * (r() * 4));
  });
  // Nur auf dem Rand: was in den Inhalt waechst, wird weggeschnitten (nichts ueberdeckt Knoepfe/Menues)
  const st = getComputedStyle(ziel), p = k => Math.ceil(parseFloat(st[k]) / EFEU_P);
  const [l, o, re, u] = [p("paddingLeft"), p("paddingTop"), p("paddingRight"), p("paddingBottom")];
  g.clearRect(rand + l, rand + o, W - 2 * rand - l - re, H - 2 * rand - o - u);
}
function efeuZeichnen() {
  efeu(document.getElementById("rahmen"), 11, [["lo", .45, .35], ["ru", .2, .3], ["ro", .18, .12]]);
  efeu(document.getElementById("buehne"), 23, [["ro", .5, .4], ["lu", .3, .25]]);
}

function zeichneFrame(ctx, groesse, pose, richtung, frame) {
  const zeile = pose.halb + RICHTUNGEN[richtung].z;
  const seite = pose.seite, spalte = frame.c - 1;
  const u = groesse / FRAME;
  const vor = { Ost: 1, West: -1 }[RICHTUNGEN[richtung].name] || 0;
  const x = frame.dx * vor * u, y = frame.dy * u;
  ctx.imageSmoothingEnabled = false;
  ctx.drawImage(WIESE, 0, 0, groesse, groesse);
  if (pose.kulisse === "kante") zeichneKante(ctx, groesse, richtung);
  else zeichneSchatten(ctx, u);
  const bild = figurFrame(seite, zeile, spalte,
    pose.geraet ? { gruppe: pose.geraet, spalte: frame.g || 0 } : null);
  // Beim Sprung nicht die Frisur verlieren. "Runterspringen" hebt die Figur um bis zu
  // 14 Pixel (dy = -14), im 64er-Feld ist oben aber nur so viel Luft, wie der Kopf frei
  // laesst - gemessen am 17.10.: 184 Faelle, in denen ein bis drei Haarzeilen oben
  // abgeschnitten wurden, ueber alle Frisuren, Farben und Richtungen. Darum steigt die
  // Figur nur so weit, wie ihr eigener Scheitel es zulaesst.
  let yv = y;
  if (yv < 0) {
    const d = figurCtx.getImageData(0, 0, FRAME, FRAME).data;
    let scheitel = FRAME;
    suche: for (let yy = 0; yy < FRAME; yy++)
      for (let xx = 0; xx < FRAME; xx++)
        if (d[(yy * FRAME + xx) * 4 + 3]) { scheitel = yy; break suche; }
    yv = Math.max(y, -scheitel * u);
  }
  ctx.drawImage(bild, x, yv, groesse, groesse);
}

// Die Figur als ein 64er-Frame: Koerper plus Waffe/Schild davor oder dahinter
const figurCtx = puffer();
function figurFrame(seite, zeile, spalte, geraet) {
  zeichneKoerper(seite, zeile, spalte);
  figurCtx.clearRect(0, 0, FRAME, FRAME);
  const [hinten, vorn] = (D.ordnung[seite] || {})[`${zeile},${spalte}`] || [[], []];
  const ausruestung = g => {
    const w = g === "waffe" ? (s.waffe && [`waffe_${WAFFEN[s.waffe - 1][0]}`, s.waffenfarbe])
                            : (s.schild && [`schild_${SCHILDE[s.schild - 1][0]}`, s.schildfarbe]);
    if (!w) return;
    teilCtx.clearRect(0, 0, FRAME, FRAME);
    if (ebene(teilCtx, seite, w[0], w[1], zeile, spalte)) figurCtx.drawImage(teilCtx.canvas, 0, 0);
  };
  hinten.forEach(ausruestung);
  figurCtx.drawImage(koerperCtx.canvas, 0, 0);
  vorn.forEach(ausruestung);
  // Ein Gerät (bisher nur die Angel) liegt VOR der Figur und hat ein eigenes Blatt
  // mit eigener Spaltenzählung: p1_angel.png hat vier Bilder, der Körper steht
  // dreimal auf derselben Spalte. Darum kommt die Spalte aus dem Frame (g), nicht
  // aus der Körperspalte.
  if (geraet) {
    teilCtx.clearRect(0, 0, FRAME, FRAME);
    if (ebene(teilCtx, seite, geraet.gruppe, 0, zeile, geraet.spalte)) figurCtx.drawImage(teilCtx.canvas, 0, 0);
  }
  return figurCtx.canvas;
}

const haupt = document.getElementById("haupt");
const hctx = haupt.getContext("2d");
const mini = RICHTUNGEN.map((r, i) => {
  const fig = document.createElement("figure");
  const c = document.createElement("canvas");
  c.width = c.height = FRAME * 2;
  c.addEventListener("click", () => setze("richtung", i));
  const cap = document.createElement("figcaption");
  cap.textContent = t(r.name);
  fig.append(c, cap);
  document.getElementById("richtungen").append(fig);
  return { fig, ctx: c.getContext("2d"), groesse: c.width };
});

// Animationstakt: die Frames der Pose mit ihren eigenen Zeiten, endlos
let bild = 0, bildSeit = 0;
function schmiedeSchritt(t) {
  const pose = POSEN[s.pose];
  if (t - bildSeit >= pose.frames[bild].t) {
    bild = (bild + 1) % pose.frames.length; bildSeit = t;
    if (pose.kulisse === "kante" && bild === 6) vogelAufscheuchen();
  }
  const frame = pose.frames[bild];
  zeichneFrame(hctx, haupt.width, pose, s.richtung, frame);
  mini.forEach((m, i) => {
    zeichneFrame(m.ctx, m.groesse, pose, i, frame);
    m.fig.classList.toggle("aktiv", i === s.richtung);
  });
}

// ---------------------------------------------------------------------------
// Liste und Steuerung
// ---------------------------------------------------------------------------
const liste = document.getElementById("liste");
let cursor = ZEILEN.findIndex(e => e.key === "outfit");

function zeichneListe(blinkKey, richtung) {
  liste.innerHTML = "";
  EIGENSCHAFTEN.forEach(e => {
    if (e.gruppe) {
      const g = document.createElement("div");
      g.className = "gruppe"; g.textContent = t(e.gruppe);
      liste.append(g);
      return;
    }
    if (!sichtbar(e)) return;
    const i = ZEILEN.indexOf(e);
    const an = waehlbar(e);
    const opts = e.optionen();
    const z = document.createElement("div");
    z.className = "zeile" + (i === cursor ? " aktiv" : "") + (an ? "" : " aus") + (e.unter ? " unter" : "");
    if (e.key === blinkKey) { z.classList.add("blink"); z.style.setProperty("--dx", `${richtung * 8}px`); }
    const wert = an ? `${t(String(opts[s[e.key]]))}<small>${s[e.key] + 1}/${opts.length}</small>` : (e.grund ? t(e.grund()) : "–");
    z.innerHTML = `<span class="name">${t(e.name)}</span><span class="pf" data-d="-1"></span>` +
                  `<span class="wert">${wert}</span><span class="pf" data-d="1"></span>`;
    z.addEventListener("click", ev => {
      if (!an) return;
      cursor = i;
      const d = +ev.target.dataset.d;
      d ? blaettern(d) : zeichneListe();
    });
    liste.append(z);
  });
  zeigeInfo();
}

function zeigeInfo() {
  const pose = POSEN[s.pose];
  const teile = [];
  if (s.vorlage) { const n = NPCS[s.vorlage - 1]; teile.push(t("Vorlage {0} – wer etwas ändert, macht daraus eine eigene Figur.", n.name)); }
  // Ueber den Namen statt ueber einen Index-Abstand: "BERUFE[s.outfit - 3]" stimmte nur
  // solange beide Listen unveraendert blieben. Als am 09.10. elf ungenutzte Outfits aus
  // der Auswahl flogen, ergab das einen negativen Index - und das Spiel blieb beim Laden
  // stehen, weil zeigeInfo() auf undefined zugriff.
  else if (berufKomplett()) {
    const b = BERUFE.find(x => x.name === wahl("outfit").name);
    if (b) teile.push(`${t(b.name)}: ${t(b.info)}.`);
  }
  if (pose.kulisse === "kante")
    teile.push(t("Sprung von einer {0} px hohen Kante, Landung {1} px tiefer. Im Spiel die Figur je Frame um dx/dy versetzen.", D.kante.hoehe, D.kante.hoehe + D.kante.tiefe));
  if (pose.name === "Niederschlag" && !komplett())
    teile.push(t("Liegend lassen sich die Einzelteile nicht sauber trennen – dort zählt alles als Oberteil."));
  if (!klangFrei && (s.ambiente || s.schritte)) teile.push(t("Der Klang startet mit der ersten Taste oder dem ersten Klick."));
  document.getElementById("info").textContent = teile.join(" ");
  document.getElementById("posenname").textContent = t(pose.name);
}

// NPC-Vorlage (npcs.json) auf den Zustand uebertragen - Werte stehen dort als Namen
function vorlageAnwenden(npc) {
  const idx = (liste, name) => {
    const i = liste.indexOf(name);
    if (i < 0) console.warn(`Vorlage ${npc.id}: "${name}" unbekannt`);
    return Math.max(0, i);
  };
  const namen = k => OPTIONEN[k].map(o => o.name);
  const farbeVon = (k, f) => (f ? idx(wahl(k).farben.map(x => x.name), f) : 0);
  Object.assign(s, { unterw: 0, hut: 0, hutfarbe: 0, acc: 0, accFarbe: 0,
    waffe: 0, waffenfarbe: 0, schild: 0, schildfarbe: 0,
    haut: idx(HAUTTOENE, npc.haut), haarfarbe: idx(HAARFARBEN, npc.haarfarbe),
    frisur: npc.frisur ? 1 + idx(FRISUREN.map(f => f[1]), npc.frisur) : 0 });
  if (npc.komplett) {
    s.art = 0; s.outfit = idx(namen("outfit"), npc.komplett); s.outfitFarbe = farbeVon("outfit", npc.komplettFarbe);
  } else {
    s.art = 1;
    TEILE.forEach(([t]) => {
      const [n, f] = (npc.teile || {})[t] || ["keins"];
      s[t] = idx(namen(t), n); s[t + "Farbe"] = farbeVon(t, f);
    });
  }
  if (npc.acc) { s.acc = idx(namen("acc"), npc.acc[0]); s.accFarbe = farbeVon("acc", npc.acc[1]); }
  if (npc.hut) {
    s.hut = idx(hutOptionen(), npc.hut[0]);
    // Die Farbliste hängt am Hut: erst den Hut setzen, dann darin die Farbe suchen.
    if (npc.hut[1]) s.hutfarbe = idx(hutfarbenVon(s.hut >= 1 && s.hut <= 2 ? HUETE[s.hut - 1][0] : null), npc.hut[1]);
  }
}

// Dieselbe Vorlage, aber als Figur zurueckgegeben statt in den Schmiede-Zustand geschrieben.
// Bewusst ueber vorlageAnwenden() und nicht nachgebaut: ein Nachbau derselben Umrechnung
// hat im Videotool schon einmal 18 von 27 Figuren verfaelscht, weil er eine Kleinigkeit
// anders machte. Der Zustand wird vorher gesichert und danach zurueckgelegt.
function figurAusVorlage(npc) {
  if (!npc) return null;
  const merk = { ...s };
  try {
    vorlageAnwenden(npc);
    return { ...s, pose: 0, richtung: 0, vorlage: 0 };
  } finally {
    Object.keys(s).forEach(k => { delete s[k]; });
    Object.assign(s, merk);
  }
}

function setze(key, wert, richtung = 0) {
  const vorher = s[key];
  s[key] = wert;
  if (key === "vorlage" && wert > 0) vorlageAnwenden(NPCS[wert - 1]);
  else if (!["vorlage", "pose", "richtung", "ambiente", "schritte"].includes(key)) s.vorlage = 0;   // geaendert: eigene Figur
  if (key === "outfit") {
    // Beruf gewaehlt: passenden Hut gleich mit aufsetzen; Beruf verlassen: Berufshut ab
    if (s.outfit >= 3) s.hut = s.outfit;
    else if (vorher >= 3 && s.hut === vorher) s.hut = 0;
  }
  if (key === "art" && s.art === 1 && vorher === 0) {
    // Einzelteile starten mit dem bisherigen Komplett-Outfit - zunaechst aendert sich nichts
    const [g, i] = s.outfit ? quelle(wahl("outfit"), s.outfitFarbe) : [null, -1];
    TEILE.forEach(([t]) => {
      s[t] = 0; s[t + "Farbe"] = 0;
      OPTIONEN[t].forEach((opt, oi) => opt.farben.forEach((f, fi) => {
        if (f.gruppe === g && f.index === i) { s[t] = oi; s[t + "Farbe"] = fi; }
      }));
    });
  }
  if (key === "pose") { bild = 0; bildSeit = performance.now(); seiteLaden(POSEN[s.pose].seite); }
  klemmen();
  zeichneListe(key, richtung);
  klangAktualisieren();
  if (!["richtung", "pose", "ambiente", "schritte"].includes(key)) {
    haupt.classList.add("pop");
    setTimeout(() => haupt.classList.remove("pop"), 90);
  }
}

// Farbindex passend zur gewaehlten Kleidung halten (Lederjacke hat 3 Farben, Stoffe 9)
function klemmen() {
  Object.keys(OPTIONEN).forEach(k => {
    const n = wahl(k).farben.length;
    if (n && s[k + "Farbe"] >= n) s[k + "Farbe"] = n - 1;
  });
}

function blaettern(d) {
  const e = ZEILEN[cursor];
  if (!waehlbar(e)) return;
  const n = e.optionen().length;
  setze(e.key, (s[e.key] + d + n) % n, d);
}

function bewegeCursor(d) {
  let i = cursor;
  do { i = (i + d + ZEILEN.length) % ZEILEN.length; } while (!waehlbar(ZEILEN[i]));
  cursor = i;
  zeichneListe();
}

function zufall() {
  const r = n => Math.floor(Math.random() * n);
  const teil = k => 1 + r(OPTIONEN[k].length - 1);
  Object.assign(s, {
    vorlage: 0, haut: r(HAUTTOENE.length), unterw: 0, art: r(2),
    outfit: teil("outfit"), outfitFarbe: r(9),
    ober: teil("ober"), oberFarbe: r(9), hose: teil("hose"), hoseFarbe: r(9), schuhe: teil("schuhe"), schuheFarbe: r(5),
    acc: r(2) ? 0 : teil("acc"), accFarbe: r(9),
    frisur: r(FRISUREN.length + 1), haarfarbe: r(HAARFARBEN.length),
    hut: r(3) ? 0 : r(hutOptionen().length), hutfarbe: r(5),
  });
  Object.keys(OPTIONEN).forEach(k => { s[k + "Farbe"] = r(Math.max(1, wahl(k).farben.length)); });
  if (!waehlbar(ZEILEN[cursor])) bewegeCursor(1);
  setze("haut", s.haut);
}

// ---------------------------------------------------------------------------
// Klang (Assets/quellen/9_sounds, Web-Fassungen in atlas/klang): Ambiente (nur Welt, nach Tageszeit) als Schleife, Schritte
// nur in Gehposen (Rennen schneller), gelegentlich ein aufflatternder Vogel.
// Browser spielen erst nach der ersten Eingabe ab.
// ---------------------------------------------------------------------------
const KLANG = {
  ambiente: [{ name: "aus" }, { name: "Waldvögel", datei: "wald", voegel: true },
             { name: "Vogelgezwitscher", datei: "voegel", voegel: true }, { name: "Grillen (Nacht)", datei: "grillen", nacht: true }],
  schritte: [{ name: "aus" }, { name: "Laub", datei: "schritte_laub" }, { name: "Kies", datei: "schritte_kies" },
             { name: "Holzboden", datei: "schritte_holz" }, { name: "Schwer", datei: "schritte_schwer" }],
};
const SCHRITT_TEMPO = { "Gehen": 1, "Rennen": 1.45, "Kampfschritt": 0.8 };
const toene = {};
function ton(datei, schleife = true) {
  if (!toene[datei]) { const a = new Audio(`atlas/klang/${datei}.mp3`); a.loop = schleife; a.preload = "auto"; toene[datei] = a; }
  return toene[datei];
}
let klangFrei = false;
// Lautstaerke (0..1) fuer Musik und Geraeusche, im Menue einstellbar, je Browser gemerkt
const LAUT_SPEICHER = "bedrock-society-lautstaerke";
const LAUT = { musik: 0.8, klang: 0.8 };
try { Object.assign(LAUT, JSON.parse(localStorage.getItem(LAUT_SPEICHER)) || {}); } catch (e) { /* Standard */ }
function lautSetzen(art, wert) {
  LAUT[art] = Math.round(Math.max(0, Math.min(1, wert)) * 10) / 10;
  try { localStorage.setItem(LAUT_SPEICHER, JSON.stringify(LAUT)); } catch (e) { /* egal */ }
  klangAktualisieren();
}
function klangAktualisieren() {
  if (!klangFrei) return;
  if (stumm) { Object.values(toene).forEach(a => a.pause()); return; }
  const spiele = (liste, aktiv, lautstaerke, tempo = 1) => liste.forEach(k => {
    if (!k.datei) return;
    const a = ton(k.datei);
    if (k === aktiv) { a.volume = Math.min(1, lautstaerke * LAUT.klang); a.playbackRate = tempo; if (a.paused) a.play().catch(() => {}); }
    else if (toene[k.datei]) a.pause();
  });
  if (bildschirm === "welt") {
    // Welt: Ambiente nach Tageszeit, Schritte nach Boden (Weg = Kies, Wiese = Laub)
    spiele(KLANG.ambiente, weltAmbiente(), 0.16);
    spiele(KLANG.schritte, null, 0);   // in der Welt klingt jeder Tritt einzeln (geraeusche.js: schrittLaut)
    return;
  }
  spiele(KLANG.ambiente, null, 0);                       // Ambiente gibt es nur auf der Karte
  const tempo = SCHRITT_TEMPO[POSEN[s.pose].name];
  spiele(KLANG.schritte, tempo ? KLANG.schritte[s.schritte] : null, 0.2, tempo || 1);
}
function vogelAufscheuchen() {
  const amb = bildschirm === "welt" ? weltAmbiente() : null;
  if (!klangFrei || stumm || !amb || !amb.voegel) return;
  const a = ton("fluegel", false).cloneNode();
  a.volume = (0.35 + Math.random() * 0.3) * LAUT.klang;
  a.play().catch(() => {});
}
setInterval(() => { if (Math.random() < 1 / 30) vogelAufscheuchen(); }, 1000);

// ---------------------------------------------------------------------------
// Hintergrundmusik (liegt ausserhalb des Gesamtpakets: Bedrock Society/musik/web): je Tageszeit eine Auswahl,
// zwischen zwei Liedern eine kurze Pause, beim Wechsel (Tageszeit, Bildschirm, aus) weich
// ausblenden. Titel, Anmeldung und Schmiede spielen "Fantasy Folk Celebration".
// ---------------------------------------------------------------------------
const MUSIK = {
  morgen: [{ datei: "morgen_1", titel: "Glimmer of the Dawn" }, { datei: "morgen_2", titel: "Glimmer of the Dawn" },
           { datei: "morgen_3", titel: "Dawn's First Bow" }, { datei: "morgen_4", titel: "Dawn's First Bow" },
           { datei: "tag_3", titel: "Fantasy Folk Celebration" }],
  tag:    [{ datei: "tag_1", titel: "Polka of the Hidden Grove" }, { datei: "tag_2", titel: "Polka of the Hidden Grove" },
           { datei: "tag_3", titel: "Fantasy Folk Celebration" }, { datei: "tag_4", titel: "Lonesome Quest" },
           { datei: "morgen_3", titel: "Dawn's First Bow" }],
  nacht:  [{ datei: "nacht_1", titel: "Moonlit Quest" }, { datei: "nacht_2", titel: "Moonlit Quest" },
           { datei: "nacht_3", titel: "Lonesome Quest" }, { datei: "nacht_4", titel: "The Duelist's Omen" }],
  titel:  [{ datei: "tag_3", titel: "Fantasy Folk Celebration" }],
};
const MUSIK_LAUT = 0.3, MUSIK_SPEICHER = "bedrock-society-musik";
const MUSIK_ORDNER = "musik/";                 // von 6_character_creator/ aus; im Artifact: Assets/musik/
let musikAn = true;
try { musikAn = localStorage.getItem(MUSIK_SPEICHER) !== "aus"; } catch (e) { /* ohne Speicher: an */ }
// Ein einziges Audio-Element fuer alle Lieder: einmal per Eingabe freigeschaltet, spielt es
// auch spaeter weiter (Safari erlaubt neue Elemente sonst nur direkt nach einer Eingabe)
const musikEl = new Audio();
musikEl.preload = "auto";
const musik = { spielt: false, liste: null, lied: null, pauseBis: 0, zuletzt: [] };
// Naechstes Lied: erst die Titel durchspielen, die lange nicht dran waren - zwei Dateien
// desselben Titels zaehlen als ein Titel, sonst kaeme derselbe Song doppelt so oft.
function naechstesLied(soll) {
  const liste = MUSIK[soll];
  const titel = [...new Set(liste.map(l => l.titel))];
  const frisch = titel.filter(t => !musik.zuletzt.includes(t));
  const auswahl = frisch.length ? frisch : titel.filter(t => t !== musik.zuletzt[musik.zuletzt.length - 1]);
  const gewaehlt = (auswahl.length ? auswahl : titel)[Math.floor(Math.random() * (auswahl.length || titel.length))];
  musik.zuletzt = [...musik.zuletzt, gewaehlt].slice(-(Math.max(1, titel.length - 1)));
  const dateien = liste.filter(l => l.titel === gewaehlt);
  return dateien[Math.floor(Math.random() * dateien.length)];
}
function musikListe() {
  if (bildschirm !== "welt") return "titel";
  const h = stunde();
  return h >= 5 && h < 10 ? "morgen" : h >= 10 && h < 19 ? "tag" : "nacht";
}
function musikUmschalten() {
  musikAn = !musikAn;
  try { localStorage.setItem(MUSIK_SPEICHER, musikAn ? "an" : "aus"); } catch (e) { /* egal */ }
}
function musikTakt() {
  const soll = klangFrei && !stumm && musikAn ? musikListe() : null;
  const a = musikEl;
  if (musik.spielt && (soll !== musik.liste || a.ended)) {
    if (!a.ended && a.volume > 0.02) { a.volume = Math.max(0, a.volume - 0.02); return; }   // gut 1,5 s Ausblende
    a.pause();
    musik.spielt = false;
    musik.pauseBis = performance.now() + (a.ended ? 4000 + Math.random() * 6000 : 600);
  }
  if (!musik.spielt && soll && performance.now() >= musik.pauseBis) {
    const lied = naechstesLied(soll);
    a.src = `${MUSIK_ORDNER}${lied.datei}.mp3`;
    a.volume = 0;
    a.play().catch(() => {});
    Object.assign(musik, { spielt: true, liste: soll, lied });
  }
  // Einblenden bis zur eingestellten Lautstaerke; leiser gestellt: gleich absenken
  const ziel = MUSIK_LAUT * LAUT.musik;
  if (musik.spielt && soll === musik.liste && a.volume !== ziel)
    a.volume = a.volume < ziel ? Math.min(ziel, a.volume + 0.02) : ziel;
}
setInterval(musikTakt, 80);
function klangFreischalten() {
  if (klangFrei) return;
  klangFrei = true;
  klangAktualisieren();
  musikTakt();                                          // erstes Lied direkt in der Eingabe starten
  zeigeInfo();
}
document.addEventListener("pointerdown", () => klangFreischalten());

function schmiedeTaste(ev, k) {
  if (k === "Enter" && !ev.target.closest("button")) schmiedeFertig();
  else if (k === "ArrowUp")    bewegeCursor(-1);
  else if (k === "ArrowDown")  bewegeCursor(1);
  else if (k === "ArrowLeft")  blaettern(-1);
  else if (k === "ArrowRight") blaettern(1);
  else if (k === "z") zufall();
  else if (k === "0") { Object.assign(s, START); setze("pose", s.pose); }
  else return false;
  return true;
}

// Eingebettet (z. B. als Artifact) braucht das Fenster erst den Fokus
const fokus = document.getElementById("fokus");
const fokusPruefen = () => { fokus.hidden = document.hasFocus(); };
window.addEventListener("focus", fokusPruefen);
window.addEventListener("blur", fokusPruefen);
try { window.focus(); } catch (e) {}
fokusPruefen();
