Falkencode · 09.09.2026

Falkencode: engine.py — Die Spiellogik

engine.py ist das Herzstück — rund 60 Befehle und die komplette Spiellogik: Befehlsverteiler mit deutschen und englischen Aliasen, die geteilte Welt mit Respawn nach 5 Minuten, Rundenkampf, Rassen-Boni (Ork +2 Schaden, Elf-Zauberermäßigung …), Gilden, 10 Quests, Zauber mit Mana, Händler und Kneipen (Orks zahlen 10 % Aufschlag — das Dorf traut ihnen nicht), Bank mit todesfestem Guthaben, Beutel-Kapazität nach Stufe, Truhe mit 100 Fächern, Tag/Nacht-Zyklus und die Göttlichen Befehle für den Admin.

Zwei Stellen lohnen den Blick: faerbe() versieht jede Ausgabezeile je nach Typ mit klassischen ANSI-Farbcodes (Raumtitel gold, Schaden rot, Quests grün) — der Browser setzt sie in CSS-Farben um. Und _raum_ansehen() baut die Raumansicht zusammen und übersetzt sie je nach Spielersprache.

#!/usr/bin/env python3
"""Falkenmoor-MUD: Spiel-Engine (Multiuser, Persistenz, Händler, Türen)."""
import json, random, os, re, threading, time
import db
import uebersetz
BASE = os.path.dirname(os.path.abspath(__file__))
WORLD_PATH = os.path.join(BASE, "world.json")
RESPAWN_SEKUNDEN = 300  # besiegte Monster kehren nach 5 Minuten zurück
_richtung_aliase = {
    "n": "norden", "no": "nordosten", "nw": "nordwesten",
    "s": "sueden", "so": "suedosten", "sw": "suedwesten",
    "o": "osten", "w": "westen", "hoch": "oben", "tief": "runter",
    "north": "norden", "south": "sueden", "east": "osten", "west": "westen",
    "northeast": "nordosten", "northwest": "nordwesten",
    "southeast": "suedosten", "southwest": "suedwesten",
    "up": "oben", "down": "runter", "e": "osten",
}
_GODPORT_BEFEHLE = {"godport", "goport", "gottport", "teleport"}
_ADMIN_NAMEN = {"Hugo"}

# Emotes: befehl -> (selbst-Text, raum-Text, braucht Ziel?)
_EMOTES = {
    "lach": ("Du lachst herzlich.", "{name} lacht herzlich.", False),
    "lache": ("Du lachst herzlich.", "{name} lacht herzlich.", False),
    "weine": ("Eine Träne läuft dir über die Wange.", "{name} weint bitterlich.", False),
    "wein": ("Eine Träne läuft dir über die Wange.", "{name} weint bitterlich.", False),
    "tanz": ("Du drehst eine Runde.", "{name} tanzt ein einsames Reigentänzchen.", False),
    "tanze": ("Du drehst eine Runde.", "{name} tanzt ein einsames Reigentänzchen.", False),
    "seufz": ("Du seufzt tief.", "{name} seufzt tief aus der Seele.", False),
    "seufze": ("Du seufzt tief.", "{name} seufzt tief aus der Seele.", False),
    "jauchz": ("Du jauchzt vor Freude!", "{name} jauchzt laut vor Freude!", False),
    "jauchze": ("Du jauchzt vor Freude!", "{name} jauchzt laut vor Freude!", False),
    "zitter": ("Du zitterst am ganzen Körper.", "{name} zittert wie Espenlaub.", False),
    "zittre": ("Du zitterst am ganzen Körper.", "{name} zittert wie Espenlaub.", False),
    "schnarch": ("Du... schnarchst? Wach auf!", "{name} schnarcht laut.", False),
    "schnarche": ("Du... schnarchst? Wach auf!", "{name} schnarcht laut.", False),
    "verbeug": ("Du verbeugst dich tief.", "{name} verbeugt sich tief{ziel_zu}.", True),
    "verbeuge": ("Du verbeugst dich tief{ziel_vor}.", "{name} verbeugt sich tief{ziel_zu}.", True),
    "wink": ("Du winkst freundlich{ziel_zu}.", "{name} winkt freundlich{ziel_zu}.", True),
    "winke": ("Du winkst freundlich{ziel_zu}.", "{name} winkt freundlich{ziel_zu}.", True),
    "zwinker": ("Du zwinkerst{ziel_zu}.", "{name} zwinkert vielversprechend{ziel_zu}.", True),
    "zwinkere": ("Du zwinkerst{ziel_zu}.", "{name} zwinkert vielversprechend{ziel_zu}.", True),
    "gruess": ("Du grüßt{ziel_zu}.", "{name} grüßt{ziel_zu}.", True),
    "gruesse": ("Du grüßt{ziel_zu}.", "{name} grüßt{ziel_zu}.", True),
    "knuddel": ("Du knuddelst{ziel_zu}.", "{name} knuddelt{ziel_zu}.", True),
    "knudle": ("Du knuddelst{ziel_zu}.", "{name} knuddelt{ziel_zu}.", True),
    "klatsch": ("Du klatschst Beifall{ziel_zu}.", "{name} klatscht Beifall{ziel_zu}.", True),
    "klatsche": ("Du klatschst Beifall{ziel_zu}.", "{name} klatscht Beifall{ziel_zu}.", True),
}

# Tag/Nacht: 1 Spieltag = 2 echte Stunden (5 Minuten pro Spielstunde)
_SPIELSTUNDE_SEKUNDEN = 300
_TAGESZEITEN = {
    "nacht": "Die Nacht liegt dunkel über dem Land.",
    "morgengrauen": "Am Horizont graut der Morgen.",
    "tag": "Die Sonne steht hoch am Himmel.",
    "abenddaemmerung": "Der Himmel glüht in Abendrot.",
}

# Wappen-Grafik für den Startraum (Marktplatz)
_START_ART = """\
    ^v^                 ^v^
   |_|_|_|           |_|_|_|
   |_____|           |_____|
  _|_____|___________|_____|_
 |             ,---.         |
 |            |     |        |
 |____________|     |_________|
          FALKENMOOR"""

# Beutel-Kapazität: Basiswert + Zuwachs je Stufe
KAPAZITAET_BASIS = 20
KAPAZITAET_PRO_STUFE = 2
TRUHE_FAECHER = 100
HAUS_RAUM = "haus"

# ANSI-Farbcodes: Der Server markiert Zeilentypen, der Browser-Client
# setzt sie in CSS-Farben um (klassische MUD-Technik).
_ANSI_RESET = "\x1b[0m"
_FAERBUNG = [
    ("【", "\x1b[1;33m"),        # Raumtitel: gold + fett
    ("⚔", "\x1b[33m"),           # eigener Treffer: gelb
    ("💢", "\x1b[31m"),          # erlittener Schaden: rot
    ("☠", "\x1b[1;35m"),        # Sieg: magenta + fett
    ("💀", "\x1b[1;31m"),       # Todesfolge: rot + fett
    ("✨", "\x1b[36m"),          # Zauber: cyan
    ("★", "\x1b[1;32m"),        # Quest angenommen: grün + fett
    ("✔", "\x1b[1;32m"),        # Quest abgeschlossen: grün + fett
    ("Belohnung:", "\x1b[32m"),
    ("Beute:", "\x1b[33m"),
    ("💬", "\x1b[36m"),          # Chat: cyan
    ("📢", "\x1b[36m"),
    ("🛡", "\x1b[36m"),
    ("📬", "\x1b[34m"),          # Post: blau
    ("📨", "\x1b[34m"),
    ("✉", "\x1b[34m"),
    ("Du kaufst", "\x1b[33m"),
    ("Du verkaufst", "\x1b[33m"),
    ("Du trinkst", "\x1b[33m"),
    ("Du isst", "\x1b[33m"),
    ("Du nimmst alles", "\x1b[1;33m"),
    ("You take everything", "\x1b[1;33m"),
    ("Du nimmst", "\x1b[33m"),
    ("You take", "\x1b[33m"),
    ("You buy", "\x1b[33m"),
    ("You sell", "\x1b[33m"),
    ("You drink", "\x1b[33m"),
    ("You eat", "\x1b[33m"),
    ("You put down", "\x1b[33m"),
    ("Du sagst", "\x1b[36m"),
    ("Du rufst", "\x1b[36m"),
    ("Reward:", "\x1b[32m"),
    ("Loot:", "\x1b[33m"),
    ("Exits:", "\x1b[2m"),
    ("Assessment:", "\x1b[35m"),
    ("Condition:", "\x1b[2m"),
    ("Du sagst", "\x1b[36m"),
    ("Du rufst", "\x1b[36m"),
    ("Ausgänge:", "\x1b[2m"),   # dezent
    ("Einschätzung:", "\x1b[35m"),
    ("═══", "\x1b[1;33m"),      # Hilfe-Titel
]

def faerbe(text):
    """Färbt Zeilen nach ihrem führenden Kennzeichen. Bereits gefärbte
    Zeilen (beginnen mit Escape) bleiben unverändert."""
    zeilen = []
    for z in text.split("\n"):
        if z.startswith("\x1b"):
            zeilen.append(z)
            continue
        if re.match(r"^\([^)]*(?:Uhr|o'clock)\)", z):
            zeilen.append("\x1b[2m" + z + _ANSI_RESET)
            continue
        for praefix, code in _FAERBUNG:
            if z.startswith(praefix):
                zeilen.append(code + z + _ANSI_RESET)
                break
        else:
            zeilen.append(z)
    return "\n".join(zeilen)

# Zauber: name -> (ab Stufe, Mana-Kosten, Wirkung-Kürzel, Beschreibung)
_ZAUBER = {
    "heilung":   (1, 6,  "heil25",  "Heilt 25 Lebenspunkte"),
    "funke":     (1, 3,  "dmg8",    "Ein magischer Funke: 8+Schaden"),
    "feuerball": (3, 10, "dmg18",   "Ein Feuerball: 18+Schaden"),
    "blitz":     (5, 14, "dmg28",   "Ein Blitz: 28+Schaden"),
    "heimwind":  (4, 12, "port",    "Trägt dich zum Marktplatz"),
}

_GILDEN = {
    "krieger": "Krieger (+3 Schaden, +10 Max-HP)",
    "magier":  "Magier (+10 Max-Mana, Zauber −1 Mana)",
    "dieb":    "Dieb (findet doppelt so viel Gold bei Siegen)",
}
def _norm(s):
    return (s.lower().replace("ü", "ue").replace("ö", "oe").replace("ä", "ae")
            .replace("ß", "ss"))
def _raum_name(name):
    """Anzeigename ohne angehängte Raumnummer wie ' (7)'."""
    return re.sub(r"\s*\(\d+\)\s*$", "", str(name)).strip()

_RASSEN_ALIASE = {"zwerg": "zwerg", "dwarf": "zwerg", "elf": "elf",
                  "ork": "ork", "orc": "ork", "mensch": "mensch", "human": "mensch"}
class Welt:
    """Geteilte Welt für alle verbundene Spieler."""
    def __init__(self, pfad=WORLD_PATH):
        with open(pfad, encoding="utf-8") as f:
            data = json.load(f)
        self.titel = data["titel"]
        self.raeume = data["raeume"]
        self.items = data["items"]
        self.npcs = {n["id"]: dict(n, tot_seit=None) for n in data["npcs"].values()}
        self.start_raum = data["start_raum"]
        self.spieler_defaults = data["spieler"]
        self.lock = threading.RLock()
    def npc_in_raum(self, raum_id):
        """Lebende (respawnerte) NPCs im Raum."""
        jetzt = time.time()
        with self.lock:
            out = []
            for n in self.npcs.values():
                if n["raum"] != raum_id:
                    continue
                if n["hp"] <= 0:
                    if n["tot_seit"] and jetzt - n["tot_seit"] >= RESPAWN_SEKUNDEN:
                        n["hp"] = n["max_hp"]
                        n["tot_seit"] = None
                    else:
                        continue
                out.append(n)
            return out
ONLINE = {}  # name -> dict(spieler=Spieler, engine=Engine)
_online_lock = threading.Lock()
class Spieler:
    def __init__(self, name, welt):
        self.name = name
        self.hp = welt.spieler_defaults["hp"]
        self.max_hp = welt.spieler_defaults["max_hp"]
        self.basis_schaden = welt.spieler_defaults["schaden"]
        self.basis_verteidigung = welt.spieler_defaults["verteidigung"]
        self.gold = welt.spieler_defaults.get("start_gold", 10)
        self.bank = 0
        self.sprache = "de"
        self.truhe = []
        self.rasse = None
        self.raum = welt.start_raum
        self.inventar = []
        self.waffe = None
        self.ruestung = None
        self.xp = 0
        self.level = 1
        self.mana = 20
        self.max_mana = 20
        self.gilde = None
        self.quests_aktiv = {}
        self.quests_fertig = []
        self.kampf_ziel = None
    def kapazitaet(self):
        return KAPAZITAET_BASIS + (self.level - 1) * KAPAZITAET_PRO_STUFE
    @property
    def schaden(self):
        b = self.basis_schaden
        if self.waffe and self.waffe in self.inventar:
            b += SPIEL_WELT.items[self.waffe].get("schaden", 0)
        if self.gilde == "krieger":
            b += 3
        if self.rasse == "ork":
            b += 2
        return max(1, b + self.level - 1)
    @property
    def verteidigung(self):
        b = self.basis_verteidigung
        if self.ruestung and self.ruestung in self.inventar:
            b += SPIEL_WELT.items[self.ruestung].get("verteidigung", 0)
        return b
    def serialisieren(self):
        return {"hp": self.hp, "max_hp": self.max_hp, "gold": self.gold,
                "bank": self.bank, "sprache": self.sprache, "truhe": self.truhe,
                "rasse": self.rasse,
                "raum": self.raum, "inventar": self.inventar, "waffe": self.waffe,
                "ruestung": self.ruestung, "xp": self.xp, "level": self.level,
                "mana": self.mana, "max_mana": self.max_mana, "gilde": self.gilde,
                "quests_aktiv": self.quests_aktiv, "quests_fertig": self.quests_fertig}
    def laden(self, daten):
        for k in ("hp", "max_hp", "gold", "bank", "sprache", "truhe", "rasse", "raum", "inventar", "waffe", "ruestung",
                  "xp", "level", "mana", "max_mana", "gilde",
                  "quests_aktiv", "quests_fertig"):
            if k in daten:
                setattr(self, k, daten[k])
        if self.raum not in SPIEL_WELT.raeume:
            self.raum = SPIEL_WELT.start_raum
SPIEL_WELT = Welt()
class Engine:
    def __init__(self, spieler, sende_raum=None, sende_alle=None, beim_save=None):
        self.p = spieler
        self.w = SPIEL_WELT
        self.sende_raum = sende_raum or (lambda raum, text, ausser: None)
        self.sende_alle = sende_alle or (lambda text, ausser: None)
        self.save = beim_save or (lambda: None)
    # ---------- Tag/Nacht ----------
    @staticmethod
    def tageszeit():
        """Liefert (spielstunde 0-23, phasenname, atmosphaere-text)."""
        stunde = int(time.time() // _SPIELSTUNDE_SEKUNDEN) % 24
        if 6 <= stunde < 8:
            phase = "morgengrauen"
        elif 8 <= stunde < 19:
            phase = "tag"
        elif 19 <= stunde < 21:
            phase = "abenddaemmerung"
        else:
            phase = "nacht"
        return stunde, phase, _TAGESZEITEN[phase]

    def _nacht_ist(self):
        return self.tageszeit()[1] == "nacht"

    # ---------- Ausgabe ----------
    def _raum_ansehen(self):
        raum = self.w.raeume[self.p.raum]
        stunde, phase, atmosphaere = self.tageszeit()
        zeilen = [f"\n【 {_raum_name(raum['name'])} 】"]
        if self.p.raum == self.w.start_raum:
            zeilen.append(_START_ART)
        zeilen += [
                  f"({phase.capitalize()}, {stunde} Uhr) — {atmosphaere}",
                  raum["beschreibung"]]
        npcs = self.w.npc_in_raum(self.p.raum)
        if npcs:
            zeilen.append("Hier sind: " + ", ".join(
                f"{n['name']}{' ⚔' if n['aggressiv'] else ''}" for n in npcs))
        with _online_lock:
            spieler_hier = [n for n, e in ONLINE.items()
                            if e["spieler"].raum == self.p.raum and n != self.p.name]
        if spieler_hier:
            zeilen.append("Andere Helden: " + ", ".join(spieler_hier))
        if raum["items"]:
            zeilen.append("Am Boden liegen: " + ", ".join(
                self.w.items[i]["name"] for i in raum["items"]))
        if raum["exits"]:
            zeilen.append("Ausgänge: " + ", ".join(self._exit_anzeigen(k, v) for k, v in raum["exits"].items()))
        return faerbe(uebersetz.text("\n".join(zeilen), self.p.sprache))
    def _exit_anzeigen(self, richtung, ziel):
        if isinstance(ziel, dict) and ziel.get("verschlossen"):
            return f"{richtung} (verschlossen)"
        return richtung
    def _npc_finden(self, suchwort):
        suchwort = _norm(suchwort)
        for n in self.w.npc_in_raum(self.p.raum):
            if suchwort in _norm(n["name"]) or suchwort == n["id"]:
                return n
        return None
    def _item_finden_boden(self, suchwort):
        suchwort = _norm(suchwort)
        for i in self.w.raeume[self.p.raum]["items"]:
            if suchwort in _norm(self.w.items[i]["name"]):
                return i
        return None
    def _item_finden_inv(self, suchwort):
        suchwort = _norm(suchwort)
        for i in self.p.inventar:
            if suchwort in _norm(self.w.items[i]["name"]):
                return i
        return None
    def _rasse_xp(self, xp):
        return int(xp * 1.1) if self.p.rasse == "mensch" else xp
    def _xp_vergeben(self, xp):
        self.p.xp += self._rasse_xp(xp)
        while self.p.xp >= self.p.level * 100:
            self.p.xp -= self.p.level * 100
            self.p.level += 1
            self.p.max_hp += 10
            self.p.max_mana += 5
            self.p.hp = self.p.max_hp
            self.p.mana = self.p.max_mana
            return (f"✦ Du bist jetzt Stufe {self.p.level}! "
                    f"(Max-HP: {self.p.max_hp}, Max-Mana: {self.p.max_mana})")
        return None
    def _sende(self, text, ausser=None):
        self.sende_raum(self.p.raum, text, ausser)
    # ---------- Befehlsverteiler ----------
    KEIN_ARG = {"umschauen", "l", "schaue", "inv", "inventar", "i", "quests", "quest",
                "aufgaben", "status", "stat", "wer", "spieler", "hilfe", "?", "befehle",
                "fliehe", "flucht", "godport", "goport", "gottport", "teleport"}
    def verarbeite(self, eingabe):
        ergebnis = self._verarbeite_roh(eingabe)
        if not ergebnis:
            return ergebnis
        if self.p.sprache == "en":
            ergebnis = uebersetz.text(ergebnis, "en")
        return faerbe(ergebnis)
    def cmd_sprache(self, arg):
        wahl = _norm(arg).strip()
        if wahl in ("en", "englisch", "english"):
            self.p.sprache = "en"
            return ("🗣 Language: ENGLISH. Room/NPC/item names remain German for now "
                    "(world translation in progress). Type HELP for English commands.")
        if wahl in ("de", "deutsch", "german"):
            self.p.sprache = "de"
            return "🗣 Sprache: DEUTSCH. Tippe HILFE für alle Befehle."
        return (f"Sprache/Language: {self.p.sprache.upper()}. "
                "SPRACHE EN · SPRACHE DE")
    def _verarbeite_roh(self, eingabe):
        text = eingabe.strip()
        if not text:
            return ""
        teile = text.split(maxsplit=1)
        befehl = _richtung_aliase.get(teile[0].lower(), teile[0].lower())
        arg = teile[1].strip() if len(teile) > 1 else ""
        if _norm(befehl) in _GODPORT_BEFEHLE:
            ergebnis = self.cmd_godport()
            self.save()
            return ergebnis
        # Emote?
        if _norm(befehl) in _EMOTES:
            ergebnis = self.cmd_emote(befehl, arg)
            if ergebnis is not None:
                return ergebnis
        # Richtung direkt?
        if befehl in ("gehe", "gang", "lauf", "laufe", "beweg") and arg:
            worte = arg.split(maxsplit=1)
            befehl = _richtung_aliase.get(worte[0].lower(), worte[0].lower())
            arg = worte[1] if len(worte) > 1 else ""
        if befehl in self.w.raeume[self.p.raum]["exits"] or befehl in _richtung_aliase.values():
            ergebnis = self.cmd_gehe(befehl)
            self.save()
            return ergebnis
        tabelle = {
            "umschauen": (self.cmd_umschauen, False), "schaue": (self.cmd_schaue, True),
            "l": (self.cmd_umschauen, False), "look": (self.cmd_umschauen, False),
            "untersuche": (self.cmd_untersuche, True), "untersuch": (self.cmd_untersuche, True),
            "betrachte": (self.cmd_untersuche, True), "betracht": (self.cmd_untersuche, True),
            "x": (self.cmd_untersuche, True), "examine": (self.cmd_untersuche, True),
            "nimm": (self.cmd_nimm, True), "ablegen": (self.cmd_ablegen, True),
            "take": (self.cmd_nimm, True), "get": (self.cmd_nimm, True),
            "drop": (self.cmd_ablegen, True),
            "benutze": (self.cmd_benutze, True), "trinke": (self.cmd_benutze, True),
            "use": (self.cmd_benutze, True), "drink": (self.cmd_benutze, True),
            "esse": (self.cmd_benutze, True), "iss": (self.cmd_benutze, True),
            "eat": (self.cmd_benutze, True),
            "rueste": (self.cmd_rueste, True), "ruestung": (self.cmd_rueste, True),
            "wear": (self.cmd_rueste, True), "wield": (self.cmd_rueste, True),
            "greif": (self.cmd_angriff, True), "angriff": (self.cmd_angriff, True),
            "attackiere": (self.cmd_angriff, True), "toete": (self.cmd_angriff, True),
            "kill": (self.cmd_angriff, True), "attack": (self.cmd_angriff, True),
            "fliehe": (self.cmd_fliehe, False), "flucht": (self.cmd_fliehe, False),
            "flee": (self.cmd_fliehe, False),
            "sprich": (self.cmd_sprich, True), "rede": (self.cmd_sprich, True),
            "talk": (self.cmd_sprich, True),
            "gib": (self.cmd_gib, True), "give": (self.cmd_gib, True),
            "sprich": (self.cmd_sprich, True), "rede": (self.cmd_sprich, True),
            "gib": (self.cmd_gib, True),
            "kauf": (self.cmd_kauf, True), "kaufe": (self.cmd_kauf, True),
            "buy": (self.cmd_kauf, True),
            "verkauf": (self.cmd_verkauf, True), "verkaufe": (self.cmd_verkauf, True),
            "sell": (self.cmd_verkauf, True),
            "einzahlen": (self.cmd_einzahlen, True), "einzahl": (self.cmd_einzahlen, True),
            "deposit": (self.cmd_einzahlen, True),
            "abheben": (self.cmd_abheben, True), "abhebe": (self.cmd_abheben, True),
            "withdraw": (self.cmd_abheben, True),
            "kontostand": (self.cmd_kontostand, False), "bank": (self.cmd_kontostand, False),
            "balance": (self.cmd_kontostand, False),
            "truhe": (self.cmd_truhe, False), "chest": (self.cmd_truhe, False),
            "lege": (self.cmd_lege, True), "lagere": (self.cmd_lege, True),
            "store": (self.cmd_lege, True),
            "hol": (self.cmd_hol, True), "hole": (self.cmd_hol, True),
            "fetch": (self.cmd_hol, True),
            "sprache": (self.cmd_sprache, True), "language": (self.cmd_sprache, True),
            "rasse": (self.cmd_rasse, True), "race": (self.cmd_rasse, True),
            "verkauf": (self.cmd_verkauf, True), "verkaufe": (self.cmd_verkauf, True),
            "sag": (self.cmd_sagen, True), "sage": (self.cmd_sagen, True),
            "say": (self.cmd_sagen, True),
            "post": (self.cmd_post, True), "brief": (self.cmd_post, True),
            "mail": (self.cmd_post, True),
            "brett": (self.cmd_brett, True), "notiz": (self.cmd_brett, True),
            "board": (self.cmd_brett, True),
            "zauber": (self.cmd_zauber, True), "zaubere": (self.cmd_zauber, True), "z": (self.cmd_zauber, True),
            "spell": (self.cmd_zauber, True), "cast": (self.cmd_zauber, True),
            "gilde": (self.cmd_gilde, True), "guild": (self.cmd_gilde, True),
            "gildenruf": (self.cmd_gildenruf, True), "gchat": (self.cmd_gildenruf, True),
            "rufe": (self.cmd_rufen, True),
            "inv": (self.cmd_inventar, False), "inventar": (self.cmd_inventar, False),
            "inventory": (self.cmd_inventar, False),
            "i": (self.cmd_inventar, False),
            "quests": (self.cmd_quests, False), "quest": (self.cmd_quests, False),
            "aufgaben": (self.cmd_quests, False),
            "status": (self.cmd_status, False), "stat": (self.cmd_status, False),
            "spieler": (self.cmd_spieler, False),
            "players": (self.cmd_spieler, False), "who": (self.cmd_spieler, False),
            "hilfe": (self.cmd_hilfe, False), "?": (self.cmd_hilfe, False),
            "help": (self.cmd_hilfe, False),
            "befehle": (self.cmd_hilfe, False),
            "godport": (self.cmd_godport, False), "goport": (self.cmd_godport, False),
            "gottport": (self.cmd_godport, False), "teleport": (self.cmd_godport, False),
            "godmap": (self.cmd_godmap, False), "gottkarte": (self.cmd_godmap, False),
            "gottpost": (self.cmd_gottpost, False), "gottwer": (self.cmd_gottwer, False),
            "gottspruch": (self.cmd_gottspruch, True), "gottheil": (self.cmd_gottheil, False),
            "gottheilung": (self.cmd_gottheil, False),
            "gottgold": (self.cmd_gottgold, True),
            "gotttrank": (self.cmd_gotttrank, True),
            "gottzug": (self.cmd_gottzug, True), "gotthole": (self.cmd_gotthole, True),
            "gottzorn": (self.cmd_gottzorn, True),
            "gottitem": (self.cmd_gottitem, True), "gottgabe": (self.cmd_gottitem, True),
            "gottmonster": (self.cmd_gottmonster, True), "gottgeschöpf": (self.cmd_gottmonster, True),
        }
        eintrag = tabelle.get(befehl)
        if not eintrag:
            return f"Was meinst du mit '{eingabe}'? Tippe HILFE für alle Befehle."
        fn, braucht_arg = eintrag
        ergebnis = fn(arg) if braucht_arg else fn()
        self.save()
        return ergebnis
    # ---------- Bewegung ----------
    def cmd_gehe(self, richtung):
        raum = self.w.raeume[self.p.raum]
        ziel = raum["exits"].get(richtung)
        if ziel is None:
            if raum["exits"]:
                return "Dahin geht es nicht. Ausgänge: " + ", ".join(self._exit_anzeigen(k, v) for k, v in raum["exits"].items())
            return "Hier gibt es keine Ausgänge."
        if self.p.kampf_ziel:
            return "Du bist im Kampf! Erst FLIEHE oder siege."
        if isinstance(ziel, dict):
            if ziel.get("verschlossen"):
                schluessel = ziel.get("schluessel")
                if not schluessel or schluessel not in self.p.inventar:
                    return (f"Der Weg {richtung} ist versperrt: {ziel.get('tuer', 'eine Tür')} ist verschlossen. "
                            + (f"Du brauchst: {self.w.items[schluessel]['name']}." if schluessel else ""))
                ziel_id = ziel["raum"]
            else:
                ziel_id = ziel["raum"]
        else:
            ziel_id = ziel
        if ziel_id not in self.w.raeume:
            return "Dorthin führt kein Weg."
        self.p.mana = min(self.p.max_mana, self.p.mana + 1)
        self._sende(f"{self.p.name} geht Richtung {richtung}.")
        self.p.raum = ziel_id
        out = [self._raum_ansehen()]
        for n in self.w.npc_in_raum(ziel_id):
            if n["aggressiv"]:
                self.p.kampf_ziel = n["id"]
                out.append(f"⚔ {n['name']} fällt über dich her!")
                out.append(self._kampf_runde())
                break
        return "\n".join(out)
    def cmd_umschauen(self):
        return self._raum_ansehen()
    def cmd_schaue(self, arg):
        return self.cmd_untersuche(arg) if arg else self._raum_ansehen()
    def cmd_untersuche(self, arg):
        """UNTERSUCHE <ding>: Raum-Details, Gegenstände, NPCs (mit Kampfschätzung),
        Mitspieler. Verrät keine Werte, die die Spannung killen."""
        raum = self.w.raeume[self.p.raum]
        if not arg:
            dinge = sorted(s.replace("_", " ") for s in raum.get("details", {}))
            zeilen = ["Auffällig hier: " + (", ".join(dinge) if dinge else "nichts Besonderes.")]
            zeilen.append("Gegenstände und Personen hier kannst du ebenfalls untersuchen.")
            return "\n".join(zeilen)
        wort = _norm(arg)
        # 1) Raum-Details
        for schluessel, text in raum.get("details", {}).items():
            k = _norm(schluessel)
            if wort == k or (len(wort) >= 3 and (k.startswith(wort) or wort in k)):
                return f"Du schaust dir {schluessel.replace('_', ' ')} an: {text}"
        # 2) Gegenstand am Boden
        iid = self._item_finden_boden(arg)
        if iid:
            return self._item_text(iid, "liegt hier")
        # 3) Gegenstand im Inventar
        iid = self._item_finden_inv(arg)
        if iid:
            return self._item_text(iid, "führst du bei dir")
        # 4) NPC im Raum
        npc = self._npc_finden(arg)
        if npc:
            return self._npc_text(npc)
        # 5) Mitspieler im Raum
        with _online_lock:
            hier = [(n, e["spieler"]) for n, e in ONLINE.items()
                    if e["spieler"].raum == self.p.raum and n != self.p.name]
        treffer = [n for n, _ in hier if _norm(n).startswith(wort)]
        if len(treffer) == 1:
            _, sp = next((n, s) for n, s in hier if n == treffer[0])
            gilde = f", {sp.gilde.capitalize()}" if sp.gilde else ""
            kampf = " Gerade im Kampf!" if sp.kampf_ziel else ""
            return (f"{sp.name} — Stufe {sp.level}{gilde}. "
                    f"{'🩹 Angeschlagen.' if sp.hp < sp.max_hp // 3 else 'Frisch und munter.'}{kampf}")
        if len(treffer) > 1:
            return "Wen meinst du? " + ", ".join(treffer)
        return f"Etwas namens „{arg}“ kannst du hier nicht entdecken."
    def _item_text(self, iid, lage):
        it = self.w.items[iid]
        zeilen = [f"{it['name']} ({it['typ']}, {lage}): {it['beschreibung']}"]
        if it["typ"] == "Waffe":
            zeilen.append(f"Als Waffe führt sich das gut an: Schaden +{it.get('schaden', 0)}.")
        elif it["typ"] == "Rüstung":
            zeilen.append(f"Schützend: Verteidigung +{it.get('verteidigung', 0)}.")
        elif it["typ"] == "Trank":
            zeilen.append(f"Getrunken heilt es etwa {it.get('heilung', 20)} Lebenspunkte.")
        wert = it.get("wert")
        if wert:
            zeilen.append(f"Händlern ist das etwa {wert} Gold wert.")
        return "\n".join(zeilen)
    def _npc_text(self, npc):
        name = npc["name"]
        if npc["aggressiv"]:
            zeilen = [f"{name} — {npc['beschreibung']}"]
            zustand = npc["hp"] / max(1, npc["max_hp"])
            if zustand < 0.25:
                zeilen.append("Zustand: schwer verwundet.")
            elif zustand < 0.6:
                zeilen.append("Zustand: verwundet.")
            else:
                zeilen.append("Zustand: ungeschwächt.")
            zeilen.append(self._einschaetzen(npc))
            return "\n".join(zeilen)
        zeilen = [f"{name} wirkt friedlich."]
        if npc.get("laden"):
            zeilen.append("Ein Sortiment wird hier aufbewahrt — SPRICH mit dieser Person über Waren.")
        if npc.get("quest") and npc["quest"]["id"] in self.p.quests_aktiv:
            benoetigt = npc["quest"].get("anzahl", 1)
            zeilen.append(f"{name} erwartet noch etwas von dir "
                          f"({self.p.quests_aktiv[npc['quest']['id']]}/{benoetigt}).")
        elif npc.get("quest") and npc["quest"]["id"] not in self.p.quests_fertig:
            zeilen.append(f"{name} scheint dir etwas zu sagen zu haben.")
        return "\n".join(zeilen)
    def _einschaetzen(self, npc):
        """Grobe Gefahren-Einschätzung (klassisches 'consider') — ohne Zahlen."""
        pro_runde = max(1, self.p.schaden)
        runden_fuer_gegner = max(0.5, npc["hp"] / pro_runde)
        treffer_auf_mich = max(1, npc["schaden"] - self.p.verteidigung)
        runden_fuer_mich = max(0.5, max(1, self.p.hp) / treffer_auf_mich)
        if runden_fuer_mich >= runden_fuer_gegner * 3:
            return "Einschätzung: Ein Kinderspiel."
        if runden_fuer_mich >= runden_fuer_gegner * 1.4:
            return "Einschätzung: Wirkt schwächer als du."
        if runden_fuer_mich * 1.4 >= runden_fuer_gegner:
            return "Einschätzung: Etwa auf Augenhöhe — ein fairer Kampf."
        if runden_fuer_mich * 3 >= runden_fuer_gegner:
            return "Einschätzung: Wirkt gefährlich — besser gewappnet als du."
        return "Einschätzung: Lebensgefährlich! Ohne Vorbereitung wird das nichts."
    def cmd_godport(self):
        """Gott-Teleport: sofort zum Startraum."""
        ziel = self.w.start_raum
        if ziel not in self.w.raeume:
            return "Startraum ist unbekannt."
        if self.p.raum == ziel:
            return "Du bist bereits am Start.\n" + self._raum_ansehen()
        self.p.kampf_ziel = None
        self._sende(f"{self.p.name} verschwindet in einem Lichtblitz.")
        self.p.raum = ziel
        self._sende(f"{self.p.name} materialisiert sich.")
        return "✦ Du wirst zum Startraum versetzt.\n" + self._raum_ansehen()
    def cmd_godmap(self):
        """Gott-Karte: ASCII-Übersicht der Umgebung des Spielers."""
        radius = 2
        delta = {"norden": (0, -1), "sueden": (0, 1), "osten": (1, 0), "westen": (-1, 0),
                 "nordosten": (1, -1), "nordwesten": (-1, -1),
                 "suedosten": (1, 1), "suedwesten": (-1, 1)}
        pos = {(0, 0): self.p.raum}
        seen = {self.p.raum}
        frontier = [(self.p.raum, 0, 0)]
        while frontier:
            rid, x, y = frontier.pop(0)
            for richtung, ziel in self.w.raeume[rid]["exits"].items():
                zid = ziel["raum"] if isinstance(ziel, dict) else ziel
                if zid not in self.w.raeume or zid in seen:
                    continue
                d = delta.get(richtung)
                if not d:
                    continue
                nx, ny = x + d[0], y + d[1]
                if abs(nx) > radius or abs(ny) > radius or (nx, ny) in pos:
                    continue
                pos[(nx, ny)] = zid
                seen.add(zid)
                frontier.append((zid, nx, ny))
        zeilen = ["✦ GOTTKARTE (Norden ist oben) ✦"]
        for y in range(-radius, radius + 1):
            reihe = []
            for x in range(-radius, radius + 1):
                if (x, y) not in pos:
                    reihe.append("     ")
                elif (x, y) == (0, 0):
                    reihe.append(" [☠] ")
                else:
                    npcs = self.w.npc_in_raum(pos[(x, y)])
                    if any(n["aggressiv"] for n in npcs):
                        zeichen = "[!]"
                    elif npcs:
                        zeichen = "[o]"
                    else:
                        zeichen = "[·]"
                    reihe.append(f" {zeichen} ")
            zeilen.append("│".join(reihe))
        zeilen.append("Legende: [☠] = du · [·] = Raum · [o] = Ort mit NPC · [!] = Gegner")
        kompass = {}
        for (x, y), rid in pos.items():
            if (x, y) == (0, 0):
                continue
            if y == -1 and x == 0: r = "N"
            elif y == 1 and x == 0: r = "S"
            elif x == 1 and y == 0: r = "O"
            elif x == -1 and y == 0: r = "W"
            elif x > 0 and y < 0: r = "NO"
            elif x < 0 and y < 0: r = "NW"
            elif x > 0 and y > 0: r = "SO"
            else: r = "SW"
            kompass[r] = _raum_name(self.w.raeume[rid]["name"])
        umgebung = "; ".join(f"{r}: {name}" for r, name in
                             sorted(kompass.items(), key=lambda kv: "NNOOSOWW".find(kv[0][0]) * 2 + len(kv[0])))
        return "\n".join(zeilen) + "\nUmgebung: " + umgebung
    # ---------- Gegenstände ----------
    def cmd_nimm(self, arg):
        if arg and _norm(arg) in ("alles", "all"):
            with self.w.lock:
                boden = list(self.w.raeume[self.p.raum]["items"])
                if not boden:
                    return "Hier liegt nichts zum Nehmen."
                frei = max(0, self.p.kapazitaet() - len(self.p.inventar))
                nimm = boden[:frei]
                liegen = boden[frei:]
                self.w.raeume[self.p.raum]["items"] = liegen
                self.p.inventar.extend(nimm)
            if not nimm:
                return (f"Dein Beutel ist voll ({len(self.p.inventar)}/{self.p.kapazitaet()}). "
                        "VERKAUF, ABLEGEN oder die Truhe im Haus schafft Platz.")
            namen = ", ".join(self.w.items[i]["name"] for i in nimm)
            mehr = "Gegenstände" if len(nimm) != 1 else "Gegenstand"
            hinweis = (f" — Dein Beutel ist voll ({len(self.p.inventar)}/{self.p.kapazitaet()}), "
                       f"{len(liegen)} blieben liegen." if liegen else "")
            return f"Du nimmst alles: {namen}. ({len(nimm)} {mehr})" + hinweis
        if not arg:
            return "Was denn nehmen? NIMM <Gegenstand> · NIMM ALLES"
        with self.w.lock:
            if len(self.p.inventar) >= self.p.kapazitaet():
                return (f"Dein Beutel ist voll ({len(self.p.inventar)}/{self.p.kapazitaet()}). "
                        "VERKAUF, ABLEGEN oder die Truhe im Haus schafft Platz.")
            iid = self._item_finden_boden(arg)
            if not iid:
                return "So etwas liegt hier nicht."
            self.w.raeume[self.p.raum]["items"].remove(iid)
            self.p.inventar.append(iid)
        return f"Du nimmst {self.w.items[iid]['name']}."
    def cmd_ablegen(self, arg):
        if not arg:
            return "Was ablegen? ABLEGEN <Gegenstand>"
        iid = self._item_finden_inv(arg)
        if not iid:
            return "Das hast du nicht."
        self.p.inventar.remove(iid)
        if self.p.waffe == iid: self.p.waffe = None
        if self.p.ruestung == iid: self.p.ruestung = None
        with self.w.lock:
            self.w.raeume[self.p.raum]["items"].append(iid)
        return f"Du legst {self.w.items[iid]['name']} ab."
    def cmd_inventar(self):
        zeilen = [f"💰 {self.p.gold} Gold | ❤ {self.p.hp}/{self.p.max_hp} HP | Stufe {self.p.level} ({self.p.xp} XP)",
                  f"In deinem Beutel ({len(self.p.inventar)}/{self.p.kapazitaet()}):"]
        if not self.p.inventar:
            zeilen.append("  (leer)")
        for iid in self.p.inventar:
            it = self.w.items[iid]
            mark = " [geführt]" if self.p.waffe == iid else " [getragen]" if self.p.ruestung == iid else ""
            zeilen.append(f"  • {it['name']} ({it['typ']}){mark}")
        return "\n".join(zeilen)
    def cmd_benutze(self, arg):
        if not arg:
            return "Was benutzen? BENUTZE <Gegenstand>"
        iid = self._item_finden_inv(arg)
        if not iid:
            return "Das hast du nicht."
        it = self.w.items[iid]
        if it["typ"] == "Trank":
            self.p.inventar.remove(iid)
            heilung = it.get("heilung", 20)
            self.p.hp = min(self.p.max_hp, self.p.hp + heilung)
            return f"Du trinkst {it['name']} und heilst um {heilung} HP. ({self.p.hp}/{self.p.max_hp})"
        if it["typ"] == "Nahrung":
            self.p.inventar.remove(iid)
            self.p.hp = min(self.p.max_hp, self.p.hp + 5)
            return f"Du isst {it['name']} und fühlst dich gestärkt. ({self.p.hp}/{self.p.max_hp})"
        if it["typ"] == "Getränk":
            self.p.inventar.remove(iid)
            teile = [f"Du trinkst {it['name']}."]
            if it.get("heilung"):
                self.p.hp = min(self.p.max_hp, self.p.hp + it["heilung"])
                teile.append(f"+{it['heilung']} HP")
            if it.get("mana"):
                self.p.mana = min(self.p.max_mana, self.p.mana + it["mana"])
                teile.append(f"+{it['mana']} Mana")
            teile.append(f"({self.p.hp}/{self.p.max_hp} HP, {self.p.mana}/{self.p.max_mana} Mana)")
            return " ".join(teile)
        if it["typ"] == "Essen":
            self.p.inventar.remove(iid)
            heilung = it.get("heilung", 5)
            self.p.hp = min(self.p.max_hp, self.p.hp + heilung)
            return (f"Du isst {it['name']} und fühlst dich gestärkt. "
                    f"(+{heilung} HP, {self.p.hp}/{self.p.max_hp})")
        if it["typ"] in ("Waffe", "Rüstung"):
            return self.cmd_rueste(arg)
        return f"{it['name']}: {it['beschreibung']}"
    def cmd_rueste(self, arg):
        if not arg:
            return "Was ausrüsten? RÜSTE <Waffe/Rüstung>"
        iid = self._item_finden_inv(arg)
        if not iid:
            return "Das hast du nicht im Inventar."
        it = self.w.items[iid]
        if it["typ"] == "Waffe":
            self.p.waffe = iid
            return f"Du führst jetzt {it['name']} (Schaden +{it['schaden']})."
        if it["typ"] == "Rüstung":
            self.p.ruestung = iid
            return f"Du trägst jetzt {it['name']} (Verteidigung +{it['verteidigung']})."
        return f"{it['name']} kann man nicht ausrüsten."
    # ---------- Truhe im Haus am Markt ----------
    def _im_haus(self):
        return self.p.raum == HAUS_RAUM

    def cmd_truhe(self):
        if not self._im_haus():
            return "Deine Truhe steht in deinem Haus am Marktplatz (Ausgang: haus)."
        kopf = f"🧰 Deine Truhe ({len(self.p.truhe)}/{TRUHE_FAECHER} Fächer):"
        if not self.p.truhe:
            return kopf + "\n  (leer) — LEGE <x> lagert etwas ein."
        zeilen = [kopf] + [f"  • {self.w.items[i]['name']} ({self.w.items[i]['typ']})"
                           for i in self.p.truhe]
        zeilen.append("Herausnehmen: HOL <x> · Einlagern: LEGE <x>")
        return "\n".join(zeilen)
    def cmd_lege(self, arg):
        if not self._im_haus():
            return "Die Truhe ist in deinem Haus am Marktplatz (Ausgang: haus)."
        if not arg:
            return "Was einlagern? LEGE <Gegenstand>"
        iid = self._item_finden_inv(arg)
        if not iid:
            return "Das hast du nicht bei dir."
        if self.p.waffe == iid or self.p.ruestung == iid:
            return "Ausgerüstete Gegenstände erst ABLEGEN, dann einlagern."
        if len(self.p.truhe) >= TRUHE_FAECHER:
            return f"Deine Truhe ist voll ({TRUHE_FAECHER}/{TRUHE_FAECHER} Fächer)."
        self.p.inventar.remove(iid)
        self.p.truhe.append(iid)
        return f"Du legst {self.w.items[iid]['name']} in die Truhe. ({len(self.p.truhe)}/{TRUHE_FAECHER} Fächer belegt)"
    def cmd_hol(self, arg):
        if not self._im_haus():
            return "Die Truhe ist in deinem Haus am Marktplatz (Ausgang: haus)."
        if not arg:
            return "Was herausnehmen? HOL <Gegenstand>"
        wort = _norm(arg)
        treffer = [iid for iid in self.p.truhe if wort in _norm(self.w.items[iid]["name"])]
        if not treffer:
            return "Das liegt nicht in deiner Truhe. (TRUHE zeigt den Inhalt)"
        iid = treffer[0]
        if len(self.p.inventar) >= self.p.kapazitaet():
            return (f"Dein Beutel ist voll ({len(self.p.inventar)}/{self.p.kapazitaet()}). "
                    "VERKAUF oder ABLEGEN schafft Platz.")
        self.p.truhe.remove(iid)
        self.p.inventar.append(iid)
        return f"Du holst {self.w.items[iid]['name']} aus der Truhe. ({len(self.p.truhe)}/{TRUHE_FAECHER} Fächer belegt)"

    # ---------- Kampf ----------
    def cmd_angriff(self, arg):
        if not arg:
            return "Wen angreifen? GREIF <Name> AN"
        npc = self._npc_finden(arg.replace(" an", "").strip())
        if not npc:
            return "Hier kämpft niemand mit diesem Namen."
        with self.w.lock:
            self.p.kampf_ziel = npc["id"]
        self._sende(f"{self.p.name} greift {npc['name']} an!")
        return self._kampf_runde()
    def _kampf_runde(self):
        with self.w.lock:
            npc = self.w.npcs.get(self.p.kampf_ziel)
            if not npc or npc["hp"] <= 0:
                self.p.kampf_ziel = None
                return "Der Kampf ist vorbei."
            out = []
            schaden = max(1, self.p.schaden + random.randint(-1, 3) - npc["verteidigung"])
            npc["hp"] -= schaden
            out.append(f"⚔ Du triffst {npc['name']} für {schaden} Schaden. ({max(0,npc['hp'])}/{npc['max_hp']} HP)")
            if npc["hp"] <= 0:
                out.extend(self._npc_besiegen(npc))
                return "\n".join(out)
            sch = max(1, npc["schaden"] + random.randint(-1, 2) - self.p.verteidigung)
            self.p.hp -= sch
            out.append(f"💢 {npc['name']} trifft dich für {sch} Schaden. (Du: {max(0,self.p.hp)}/{self.p.max_hp} HP)")
            if self.p.hp <= 0:
                out.append(self._spieler_stirbt())
            return "\n".join(out)
    def _npc_besiegen(self, npc):
        """Gegner ist tot: XP, Beute, Diebesgold, Broadcast. Gibt Textzeilen zurück."""
        npc["tot_seit"] = time.time()
        raum = self.w.raeume[npc["raum"]]
        self.p.kampf_ziel = None
        xp = 10 + npc["max_hp"] // 2
        out = [f"☠ {npc['name']} ist besiegt! (+{self._rasse_xp(xp)} XP)"]
        lvl = self._xp_vergeben(xp)
        if lvl: out.append(lvl)
        if npc["beute"]:
            for iid in npc["beute"]:
                raum["items"].append(iid)
            out.append("Beute: " + ", ".join(self.w.items[i]["name"] for i in npc["beute"]))
        if self.p.gilde == "dieb":
            fund = max(2, npc["max_hp"] // 4)
            self.p.gold += fund
            out.append(f"🥷 Deine flinken Hände finden {fund} Gold in der Beute.")
        self._sende(f"{self.p.name} hat {npc['name']} besiegt!")
        return out
    def _spieler_stirbt(self):
        """Todesfolge: Auftachen auf dem Marktplatz mit 10 HP, halbes Bargeld weg —
        Bankguthaben bleibt unangetastet."""
        self.p.hp = 10
        self.p.raum = self.w.start_raum
        self.p.kampf_ziel = None
        verlust = self.p.gold // 2
        self.p.gold -= verlust
        hinten = (f" Dein Bankguthaben ({self.p.bank} Gold) ist sicher."
                  if self.p.bank else "")
        return (f"\n💀 Du bist gefallen... Der Dorfbader weckt dich auf dem Marktplatz. "
                f"Du verlierst {verlust} Gold (Bargeld)." + hinten)
    def cmd_fliehe(self):
        if not self.p.kampf_ziel:
            return "Vor wem denn fliehen?"
        npc = self.w.npcs[self.p.kampf_ziel]
        self.p.kampf_ziel = None
        raum = self.w.raeume[self.p.raum]
        if raum["exits"] and random.random() < 0.7:
            ziel = random.choice(list(raum["exits"].values()))
            if isinstance(ziel, dict):
                ziel = ziel["raum"]
            self.p.raum = ziel
            return f"Du entkommst {npc['name']} und fliehst.\n" + self._raum_ansehen()
        return "Die Flucht misslingt – du bleibst im Kampf!\n" + self._kampf_runde()
    # ---------- Dialog & Quests ----------
    def cmd_sprich(self, arg):
        npc = self._npc_finden(arg.replace(" mit", "").strip()) if arg else None
        if not npc:
            freundliche = [n for n in self.w.npc_in_raum(self.p.raum) if not n["aggressiv"]]
            if freundliche:
                npc = freundliche[0]
            else:
                return "Hier ist niemand, mit dem man reden könnte."
        out = [f'{npc["name"]} sagt: „{npc["beschreibung"]}“']
        if npc.get("laden"):
            out.append(f"📦 {npc['name']} handelt: KAUF <ware> · VERKAUF <gegenstand>")
            out.append("Sortiment: " + ", ".join(
                f"{self.w.items[i]['name']} ({p} G)" for i, p in npc["laden"]["sortiment"].items()))
        q = npc.get("quest")
        if q and q["id"] not in self.p.quests_fertig:
            if q["id"] in self.p.quests_aktiv:
                benoetigt = q.get("anzahl", 1)
                out.append(f"[Quest {q['name']}] Fortschritt: {self.p.quests_aktiv[q['id']]}/{benoetigt} "
                           f"({self.w.items[q['item']]['name']})")
            else:
                self.p.quests_aktiv[q["id"]] = 0
                out.append(f"★ NEUE QUEST: {q['name']} — {q['beschreibung']}")
                if q.get("uebergabe"):
                    self.p.inventar.append(q["uebergabe"])
                    out.append(f"(Du erhältst: {self.w.items[q['uebergabe']]['name']})")
        return "\n".join(out)
    def cmd_gib(self, arg):
        if not arg:
            return "Benutzung: GIB <Gegenstand> <NPC>"
        worte = arg.split()
        if len(worte) < 2:
            return "Benutzung: GIB <Gegenstand> <NPC>"
        npc = self._npc_finden(worte[-1])
        if not npc:
            return "Diese Person ist nicht hier."
        item_wort = " ".join(worte[:-1]).replace(" an ", " ")
        iid = self._item_finden_inv(item_wort)
        if not iid:
            return "So etwas hast du nicht."
        return self._quest_abgeben(npc, iid)
    def _quest_definition(self, qid):
        """Questdefinition anhand der ID in der ganzen Welt suchen."""
        for n in self.w.npcs.values():
            q = n.get("quest")
            if q and q["id"] == qid:
                return q
        return None
    def _quest_abgeben(self, npc, iid):
        # Alle aktiven Quests, die an diesen NPC gerichtet sind
        # (bei Selbstquests gilt: ziel == eigene ID).
        kandidaten = []
        for qid in self.p.quests_aktiv:
            q = self._quest_definition(qid)
            if q and q.get("ziel", npc["id"]) == npc["id"]:
                kandidaten.append(q)
        if not kandidaten:
            return (f"{npc['name']} schüttelt den Kopf. "
                    "(Das gehört zu keiner aktiven Quest – sieh mit QUESTS nach, wer was will.)")
        passende = [q for q in kandidaten if q["item"] == iid]
        if not passende:
            wollen = ", ".join(self.w.items[q["item"]]["name"] for q in kandidaten)
            if len(kandidaten) == 1:
                return f"{npc['name']} will {wollen} haben."
            return f"{npc['name']} erwartet: {wollen}. Was davon gibst du ihm?"
        q = passende[0]
        benoetigt = q.get("anzahl", 1)
        zaehler = self.p.quests_aktiv[q["id"]] + 1
        self.p.quests_aktiv[q["id"]] = zaehler
        self.p.inventar.remove(iid)
        if zaehler < benoetigt:
            return (f"{npc['name']} nimmt {self.w.items[iid]['name']} dankend an. "
                    f"({zaehler}/{benoetigt} abgegeben)")
        self.p.quests_fertig.append(q["id"])
        del self.p.quests_aktiv[q["id"]]
        out = [f"✔ QUEST ABGESCHLOSSEN: {q['name']}! ({npc['name']} dankt dir.)"]
        for belohnung in q["belohnung"]:
            self.p.inventar.append(belohnung)
            it = self.w.items[belohnung]
            zusatz = (f" (Schaden +{it['schaden']})" if it["typ"] == "Waffe" else
                      f" (Verteidigung +{it['verteidigung']})" if it["typ"] == "Rüstung" else "")
            out.append(f"  Belohnung: {it['name']}{zusatz}")
        self.p.gold += q.get("gold", 0)
        if q.get("gold"):
            out.append(f"  (+{q['gold']} Gold)")
        lvl = self._xp_vergeben(q.get("xp", 25))
        out.append(f"  (+{self._rasse_xp(q.get('xp', 25))} XP)")
        if lvl: out.append(lvl)
        return "\n".join(out)
    def cmd_quests(self):
        if not self.p.quests_aktiv:
            return "Du hast keine aktiven Quests. Sprich mit den Dorfbewohnern (SPRICH <Name>)!"
        out = ["Deine Quests:"]
        for qid, zaehler in self.p.quests_aktiv.items():
            q = next(n["quest"] for n in self.w.npcs.values() if n.get("quest") and n["quest"]["id"] == qid)
            ziel = next((n["name"] for n in self.w.npcs.values() if n["id"] == q["ziel"]), "?")
            out.append(f"  ★ {q['name']}: {q['beschreibung']} → abgeben bei: {ziel} "
                       f"({zaehler}/{q.get('anzahl', 1)})")
        return "\n".join(out)
    # ---------- Handel ----------
    def _haendler(self):
        for n in self.w.npc_in_raum(self.p.raum):
            if n.get("laden"):
                return n
        return None
    def cmd_kauf(self, arg):
        npc = self._haendler()
        if not npc:
            return "Hier gibt niemandem etwas zu kaufen."
        if self._nacht_ist() and not npc["laden"].get("nacht") and npc["id"] != "kraeuterhexe2":
            return ("Die Läden haben geschlossen – nur die Kräuterhexe in Falkenmoor "
                    "und die Kneipen verkaufen bei Nacht. Kehre im Morgenlicht zurück!")
        if not arg:
            return "Was kaufen? KAUF <Ware>"
        for iid, preis in npc["laden"]["sortiment"].items():
            if _norm(arg) in _norm(self.w.items[iid]["name"]):
                if self.p.rasse == "ork":
                    preis = (preis * 11 + 9) // 10  # Das Dorf traut Orks nicht über den Weg (10 % Aufschlag)
                if self.p.gold < preis:
                    return f"{self.w.items[iid]['name']} kostet {preis} Gold – so viel hast du nicht."
                self.p.gold -= preis
                self.p.inventar.append(iid)
                return f"Du kaufst {self.w.items[iid]['name']} für {preis} Gold. ({self.p.gold} G übrig)"
        return "Das führt man hier nicht."
    def cmd_verkauf(self, arg):
        npc = self._haendler()
        if not npc:
            return "Hier kauft niemand etwas."
        if not arg:
            return "Was verkaufen? VERKAUF <Gegenstand>"
        iid = self._item_finden_inv(arg)
        if not iid:
            return "Das hast du nicht."
        it = self.w.items[iid]
        if it["typ"] in ("Quest",):
            return "Questgegenstände kannst du nicht verkaufen."
        preis = max(1, it.get("wert", 5) // 2)
        self.p.inventar.remove(iid)
        if self.p.waffe == iid: self.p.waffe = None
        if self.p.ruestung == iid: self.p.ruestung = None
        self.p.gold += preis
        return f"Du verkaufst {it['name']} für {preis} Gold. ({self.p.gold} G)"
    # ---------- Gott-Administration (nur _ADMIN_NAMEN) ----------
    def _ist_gott(self):
        return self.p.name in _ADMIN_NAMEN

    def _kein_gott(self):
        return ("Ein Blitz zuckt über den Himmel und eine Stimme grollt: "
                "„Diese Macht steht dir nicht zu.“")

    def cmd_gottpost(self):
        if not self._ist_gott():
            return self._kein_gott()
        briefe = db.mail_an_gott()
        if not briefe:
            return "🕊 Kein Sterblicher hat Gott zuletzt geschrieben."
        out = ["🕊 POST AN GOTT (neueste zuerst):"]
        for b in briefe:
            out.append(f"  ✉ {b['absender']} ({b['zeit']}): {b['text']}")
        return "\n".join(out)

    def cmd_gottwer(self):
        if not self._ist_gott():
            return self._kein_gott()
        out = ["🕊 ALLWISSENHEIT — Helden dieses Reiches:"]
        for name in db.alle_spieler():
            daten = db.laden(name)
            raum = _raum_name(self.w.raeume[daten.get("raum", self.w.start_raum)]["name"]) \
                if daten.get("raum") in self.w.raeume else "?"
            out.append(f"  • {name} — Stufe {daten.get('level', 1)}, "
                       f"{daten.get('hp', '?')}/{daten.get('max_hp', '?')} HP, "
                       f"{daten.get('gold', 0)} Gold, Ort: {raum}"
                       + (" (online)" if name in ONLINE else ""))
        return "\n".join(out)

    def cmd_gottspruch(self, arg):
        if not self._ist_gott():
            return self._kein_gott()
        worte = arg.split(maxsplit=1)
        if len(worte) < 2:
            return "Benutzung: GOTTSPRUCH <name> <text> — Gott spricht zu einem Helden."
        empfaenger_wort, text = worte[0], worte[1].strip()[:400]
        treffer = [n for n in db.alle_spieler() if _norm(n).startswith(_norm(empfaenger_wort))]
        if not treffer:
            return "Ein solcher Held ist nicht registriert."
        if len(treffer) > 1:
            return "Name nicht eindeutig: " + ", ".join(treffer)
        db.mail_senden("Gott", treffer[0], text)
        self.sende_alle(f"🌩 Ein Donnergrollen hallt über Falkenmoor — Gott hat gesprochen.", ausser=None)
        return f"🕊 Gott spricht zu {treffer[0]}: „{text}“ (im Postfach des Helden)"

    def cmd_gottheil(self):
        if not self._ist_gott():
            return self._kein_gott()
        self.p.hp = self.p.max_hp
        self._sende(f"Ein warmes Licht erfüllt {self.p.name}.")
        return f"✨ Göttliche Heilung: {self.p.hp}/{self.p.max_hp} HP."

    def _gott_spieler(self, wort):
        """Registrierten Spieler per Präfix finden; gibt (name, daten, fehler) zurück."""
        treffer = [n for n in db.alle_spieler() if _norm(n).startswith(_norm(wort))]
        if not treffer:
            return None, None, "Ein solcher Held ist nicht registriert."
        if len(treffer) > 1:
            return None, None, "Name nicht eindeutig: " + ", ".join(treffer)
        name = treffer[0]
        return name, db.laden(name), None

    def _gott_fluestern(self, name, text):
        """Direkte Nachricht an einen online-Spieler."""
        eintrag = ONLINE.get(name)
        if eintrag and eintrag.get("ws"):
            try:
                eintrag["ws"].send(text)
            except Exception:
                pass

    def _gott_speichern(self, name, daten):
        db.speichern(name, daten)

    def cmd_gottgold(self, arg):
        if not self._ist_gott():
            return self._kein_gott()
        worte = arg.split()
        if len(worte) != 2 or not worte[1].lstrip("-").isdigit():
            return "Benutzung: GOTTGOLD <name> <menge> — Gold aus dem Himmel (auch negativ möglich)"
        name, daten, fehler = self._gott_spieler(worte[0])
        if fehler:
            return fehler
        menge = int(worte[1])
        daten["gold"] = max(0, daten.get("gold", 0) + menge)
        self._gott_speichern(name, daten)
        if name in ONLINE:
            ONLINE[name]["spieler"].gold = daten["gold"]
        self._gott_fluestern(name, f"🌩 Ein Goldregen fällt vom Himmel: {menge:+d} Gold! (Neu: {daten['gold']})")
        return f"💰 {name} erhält {menge:+d} Gold (Stand: {daten['gold']})."

    def cmd_gotttrank(self, arg):
        if not self._ist_gott():
            return self._kein_gott()
        worte = arg.split()
        if len(worte) != 2 or not worte[1].isdigit():
            return "Benutzung: GOTTTRANK <name> <anzahl> — Heiltränke für einen Helden"
        name, daten, fehler = self._gott_spieler(worte[0])
        if fehler:
            return fehler
        anzahl = max(1, min(20, int(worte[1])))
        daten.setdefault("inventar", []).extend(["i_heiltrank"] * anzahl)
        self._gott_speichern(name, daten)
        if name in ONLINE:
            ONLINE[name]["spieler"].inventar.extend(["i_heiltrank"] * anzahl)
        self._gott_fluestern(name, f"✨ Göttliche Gabe: {anzahl} Heiltränke erscheinen in deinem Beutel!")
        return f"🧪 {name} erhält {anzahl} Heiltränke."

    def cmd_gottzug(self, arg):
        """Spieler sanft zum Start zurückholen (befreit aus jeder Lage)."""
        if not self._ist_gott():
            return self._kein_gott()
        if not arg:
            return "Benutzung: GOTTZUG <name> — holt den Helden zum Marktplatz zurück"
        name, daten, fehler = self._gott_spieler(arg.split()[0])
        if fehler:
            return fehler
        daten["raum"] = self.w.start_raum
        daten["hp"] = max(daten.get("hp", 10), 10)
        self._gott_speichern(name, daten)
        if name in ONLINE:
            ONLINE[name]["spieler"].raum = self.w.start_raum
            ONLINE[name]["spieler"].kampf_ziel = None
        self._gott_fluestern(name, "🌩 Eine göttliche Hand hebt dich sanft auf und trägt dich zum Marktplatz.")
        return f"🌪 {name} wurde zum Marktplatz versetzt."

    def cmd_gotthole(self, arg):
        """Spieler in den eigenen Raum holen."""
        if not self._ist_gott():
            return self._kein_gott()
        if not arg:
            return "Benutzung: GOTTHOLE <name> — holt den Helden zu dir"
        name, daten, fehler = self._gott_spieler(arg.split()[0])
        if fehler:
            return fehler
        daten["raum"] = self.p.raum
        self._gott_speichern(name, daten)
        if name in ONLINE:
            ONLINE[name]["spieler"].raum = self.p.raum
            ONLINE[name]["spieler"].kampf_ziel = None
        self._gott_fluestern(name, f"🌩 Du wirst von einem Lichtwirbel erfasst und erscheinst bei {self.p.name}.")
        return f"🌪 {name} erscheint in deinem Raum."

    def cmd_gottzorn(self, arg):
        """Blitzschlag: halbiert die HP des Ziels (mindestens 1 lassen)."""
        if not self._ist_gott():
            return self._kein_gott()
        if not arg:
            return "Benutzung: GOTTZORN <name> — ein Blitz straft den Helden (halbe HP)"
        name, daten, fehler = self._gott_spieler(arg.split()[0])
        if fehler:
            return fehler
        schaden = max(1, daten.get("hp", 20) // 2)
        daten["hp"] = max(1, daten.get("hp", 20) - schaden)
        self._gott_speichern(name, daten)
        if name in ONLINE:
            ONLINE[name]["spieler"].hp = daten["hp"]
        self.sende_alle(f"🌩 Ein Blitz zuckt vom Himmel und trifft {name}! Der Boden bebt.", ausser=None)
        self._gott_fluestern(name, f"🌩 Göttlicher Zorn trifft dich: -{schaden} HP! (Du: {daten['hp']}/{daten.get('max_hp', '?')})")
        return f"⚡ {name} vom Blitz getroffen: -{schaden} HP (übrig: {daten['hp']})."

    def cmd_gottitem(self, arg):
        """Item aus dem Himmel (eigenes Inventar)."""
        if not self._ist_gott():
            return self._kein_gott()
        if not arg:
            return ("Benutzung: GOTTITEM <gegenstand> — lässt einen Gegenstand vom Himmel fallen "
                    "(z.B. gottitem langschwert)")
        iid = self._item_finden_himmel(arg)
        if not iid:
            return "So ein Ding existiert nicht in dieser Welt. (Tipp: gottitem drachenschwert)"
        self.p.inventar.append(iid)
        self._sende(f"Ein Gegenstand fällt in einem Lichtstrahl vom Himmel.")
        return f"🌟 Vom Himmel gefallen: {self.w.items[iid]['name']}."

    def _item_finden_himmel(self, suchwort):
        suchwort = _norm(suchwort)
        for iid, it in self.w.items.items():
            if suchwort in _norm(it["name"]):
                return iid
        return None

    def cmd_gottmonster(self, arg):
        """Erschafft ein Monster im aktuellen Raum (Vorlage per Namenssuche oder zufällig)."""
        if not self._ist_gott():
            return self._kein_gott()
        vorlagen = [n for n in self.w.npcs.values() if n["aggressiv"] and not n["id"].startswith("gott_")]
        if arg:
            sw = _norm(arg)
            vorlagen = [n for n in vorlagen if sw in _norm(n["name"])] or vorlagen
        vorlage = random.choice(vorlagen)
        with self.w.lock:
            nr = 1
            while f"gott_monster_{nr}" in self.w.npcs:
                nr += 1
            nid = f"gott_monster_{nr}"
            self.w.npcs[nid] = {"id": nid, "name": vorlage["name"], "raum": self.p.raum,
                                "hp": vorlage["max_hp"], "max_hp": vorlage["max_hp"],
                                "schaden": vorlage["schaden"], "verteidigung": vorlage["verteidigung"],
                                "aggressiv": True, "beschreibung": "Aus nichts erschaffen – ein Werk göttlichen Zorns.",
                                "quest": None, "beute": vorlage.get("beute", []), "tot_seit": None}
            self.w.raeume[self.p.raum]["npcs"].append(nid)
        self._sende(f"Der Himmel reißt auf – {vorlage['name']} materialisiert sich in einem Blitz!")
        return f"🐉 Erschaffen: {vorlage['name']} (HP {vorlage['max_hp']}) ist nun in diesem Raum."

    # ---------- Magie ----------
    def cmd_zauber(self, arg):
        if not arg:
            out = [f"✨ Zauber (Mana: {self.p.mana}/{self.p.max_mana}):"]
            for name, (lvl, kosten, _, beschreibung) in _ZAUBER.items():
                offen = "✔" if self.p.level >= lvl else f"✗ (ab Stufe {lvl})"
                kosten_eff = kosten - 1 if (self.p.gilde == "magier" or self.p.rasse == "elf") and kosten > 1 else kosten
                out.append(f"  [{name}] {beschreibung} — {kosten_eff} Mana {offen}")
            out.append("Anwenden: ZAUBER <name> [gegner]")
            return "\n".join(out)
        worte = arg.split(maxsplit=1)
        name = _norm(worte[0])
        ziel_arg = worte[1].strip() if len(worte) > 1 else ""
        treffer = [z for z in _ZAUBER if z.startswith(name)]
        if not treffer:
            return "Diesen Zauber kennst du nicht. (ZAUBER zeigt alle)"
        name = treffer[0]
        lvl, kosten, wirkung, _ = _ZAUBER[name]
        if self.p.level < lvl:
            return f"⟡ {name.capitalize()} lernt man erst ab Stufe {lvl}."
        kosten = kosten - 1 if (self.p.gilde == "magier" or self.p.rasse == "elf") and kosten > 1 else kosten
        if self.p.mana < kosten:
            return f"⟡ Nicht genug Mana ({self.p.mana}/{kosten}). Zeit heilt… oder ein Heiltrank aus dem nächsten Kampf."
        self.p.mana -= kosten
        if wirkung == "port":
            self.p.kampf_ziel = None
            self._sende(f"{self.p.name} löst sich in Wirbelwind auf.")
            self.p.raum = self.w.start_raum
            return "✨ Der Heimwind trägt dich zum Marktplatz.\n" + self._raum_ansehen()
        if wirkung == "heil25":
            self.p.hp = min(self.p.max_hp, self.p.hp + 25)
            self._sende(f"Ein sanfter Glanz umhüllt {self.p.name}.")
            return f"✨ Heilung: {self.p.hp}/{self.p.max_hp} HP."
        npc = self._npc_finden(ziel_arg) if ziel_arg else None
        if ziel_arg and not npc:
            self.p.mana += kosten
            return "Hier ist kein solches Ziel."
        if not npc:
            # ohne Ziel: erstbesten Gegner im Raum
            npc = next((n for n in self.w.npc_in_raum(self.p.raum) if n["aggressiv"]), None)
        if not npc:
            self.p.mana += kosten  # Mana zurück, kein Ziel vorhanden
            return "Hier gibt es nichts zu bekämpfen."
        # Angriffszauber: Magieschaden trifft sofort; lebt der Gegner noch,
        # schlägt er zurück (der Zauber war die Aktion dieser Runde).
        schaden = int(wirkung[3:]) + self.p.level
        self.p.kampf_ziel = npc["id"]
        self._sende(f"{self.p.name} webt einen Zauber gegen {npc['name']}!")
        with self.w.lock:
            npc["hp"] -= schaden
            if npc["hp"] <= 0:
                out = [f"✨ {name.capitalize()} trifft {npc['name']} für {schaden} Schaden."]
                out.extend(self._npc_besiegen(npc))
                return "\n".join(out)
            sch = max(1, npc["schaden"] + random.randint(-1, 2) - self.p.verteidigung)
            self.p.hp -= sch
        out = [f"✨ {name.capitalize()} trifft {npc['name']} für {schaden} Schaden. "
               f"({npc['hp']}/{npc['max_hp']} HP)",
               f"💢 {npc['name']} trifft dich für {sch} Schaden. (Du: {max(0,self.p.hp)}/{self.p.max_hp} HP)"]
        if self.p.hp <= 0:
            out.append(self._spieler_stirbt())
        return "\n".join(out)

    # ---------- Gilden ----------
    def cmd_gilde(self, arg):
        if not arg:
            out = ["⚔ Die Gilden von Falkenmoor:"]
            for g, info in _GILDEN.items():
                out.append(f"  • {g.capitalize()}: {info}")
            out.append("Beitreten (am Gildenhaus im Rathaus): GILDE BEITRETEN <name>")
            out.append("Gilden-Chat: GILDENRUF <text>")
            if self.p.gilde:
                out.append(f"Du bist: {self.p.gilde.capitalize()}")
            return "\n".join(out)
        worte = arg.split(maxsplit=1)
        if _norm(worte[0]) in ("beitreten", "beitret", "beigehen") and len(worte) > 1:
            name = _norm(worte[1].split()[0])
            treffer = [g for g in _GILDEN if g.startswith(name)]
            if not treffer:
                return "Eine solche Gilde gibt es nicht. (GILDE zeigt alle)"
            if len(treffer) > 1:
                return "Name nicht eindeutig: " + ", ".join(treffer)
            if not any(n["id"] == "gildenmeister" for n in self.w.npc_in_raum(self.p.raum)):
                return "Der Gildenmeister erwartet dich im Rathaus von Falkenmoor."
            gilde = treffer[0]
            if self.p.gilde == gilde:
                return f"Du bist bereits {gilde.capitalize()}."
            if self.p.gilde:
                return (f"Du bist schon {self.p.gilde.capitalize()}. Ein Gildenwechsel "
                        "kostet 100 Gold — sag GILDE WECHSELN <name>, wenn du sicher bist.")
            self.p.gilde = gilde
            if gilde == "krieger":
                self.p.max_hp += 10; self.p.hp = self.p.max_hp
            if gilde == "magier":
                self.p.max_mana += 10; self.p.mana = self.p.max_mana
            self._sende(f"{self.p.name} tritt feierlich der Gilde der {gilde.capitalize()} bei!")
            return (f"⚔ Willkommen in der Gilde der {gilde.capitalize()}! "
                    f"(HP: {self.p.hp}/{self.p.max_hp}, Mana: {self.p.mana}/{self.p.max_mana})")
        if _norm(worte[0]) == "wechseln" and len(worte) > 1:
            if not self.p.gilde:
                return "Du bist in keiner Gilde."
            if self.p.gold < 100:
                return "Ein Gildenwechsel kostet 100 Gold — so viel hast du nicht."
            name = _norm(worte[1].split()[0])
            treffer = [g for g in _GILDEN if g.startswith(name)]
            if not treffer or len(treffer) > 1 or treffer[0] == self.p.gilde:
                return "So geht das nicht. (GILDE zeigt die Gilden)"
            self.p.gold -= 100
            alt = self.p.gilde
            self._gilde_bonus_entfernen(alt)
            self.p.gilde = None
            ergebnis = self.cmd_gilde("beitreten " + treffer[0])
            if "Willkommen" in ergebnis:
                return (f"Du wechselst von der Gilde der {alt.capitalize()} "
                        f"(Bonus entfernt) — {ergebnis}")
            return ergebnis
        return "Benutzung: GILDE · GILDE BEITRETEN <name> · GILDE WECHSELN <name> · GILDENRUF <text>"

    def _gilde_bonus_entfernen(self, gilde):
        """Gildenbonus beim Verlassen zurückgeben (HP/Mana nie unter 1 fallen lassen)."""
        if gilde == "krieger":
            self.p.max_hp = max(10, self.p.max_hp - 10)
            self.p.hp = min(self.p.hp, self.p.max_hp)
        elif gilde == "magier":
            self.p.max_mana = max(0, self.p.max_mana - 10)
            self.p.mana = min(self.p.mana, self.p.max_mana)

    def cmd_gildenruf(self, arg):
        if not self.p.gilde:
            return "Nur Gildenmitglieder können den Gildenruf nutzen."
        if not arg:
            return "Was rufen? GILDENRUF <text>"
        with _online_lock:
            namen = [n for n, e in ONLINE.items()
                     if e["spieler"].gilde == self.p.gilde and n != self.p.name]
        for n in namen:
            eintrag = ONLINE.get(n)
            if eintrag and eintrag.get("ws"):
                try:
                    eintrag["ws"].send(f"🛡 [{self.p.gilde.capitalize()}] {self.p.name}: {arg}")
                except Exception:
                    pass
        return f"🛡 [{self.p.gilde.capitalize()}] Du: {arg}"

    # ---------- Emotes ----------
    def cmd_emote(self, befehl, arg):
        selbst, raum_text, braucht_ziel = _EMOTES[_norm(befehl)]
        ziel = ""
        if braucht_ziel and arg:
            ziel_name = arg.split()[0]
            with _online_lock:
                hier = [n for n, e in ONLINE.items() if n != self.p.name]
            hier += [n["name"] for n in self.w.npc_in_raum(self.p.raum)]
            treffer = [n for n in hier if _norm(n).startswith(_norm(ziel_name))]
            if len(treffer) > 1:
                return "Wen meinst du? " + ", ".join(treffer)
            if not treffer:
                return f"{ziel_name} ist nicht hier."
            ziel = treffer[0]
        elif braucht_ziel:
            ziel = "in die Runde"
        ziel_zu = f" {ziel} zu" if ziel and ziel != "in die Runde" else (" allen zu" if ziel == "in die Runde" else "")
        ziel_vor = f" vor {ziel}" if ziel else ""
        try:
            self._sende(raum_text.format(name=self.p.name, ziel_zu=ziel_zu))
        except KeyError:
            self._sende(raum_text.format(name=self.p.name))
        return (selbst.format(ziel_zu=ziel_zu.replace(" zu", ""), ziel_vor=ziel_vor)
                if "{" in selbst else selbst)

    # ---------- Anschlagbrett ----------
    def _am_brett(self):
        return any(n["id"] == "anschlagbrett" for n in self.w.npc_in_raum(self.p.raum))

    def cmd_brett(self, arg):
        if not self._am_brett():
            return "Hier hängt kein Anschlagbrett. Am Marktplatz von Falkenmoor findest du eins."
        if not arg:
            eintraege = db.brett_lesen()
            if not eintraege:
                return ("📋 Das Anschlagbrett ist leer.\n"
                        "Notiz anheften: BRETT <text> · entfernen: BRETT LOESCHE <nr>")
            out = ["📋 ANSCHLAGBRETT (neueste zuerst):"]
            for i, e in enumerate(eintraege, 1):
                out.append(f"  [{i}] {e['autor']} ({e['zeit']}): {e['text']}")
            out.append("Notiz: BRETT <text> · eigene entfernen: BRETT LOESCHE <nr>")
            return "\n".join(out)
        worte = arg.split(maxsplit=1)
        if _norm(worte[0]) in ("loesche", "loesch", "entferne", "weg"):
            if len(worte) < 2 or not worte[1].strip().isdigit():
                return "Benutzung: BRETT LOESCHE <nummer>"
            nr = int(worte[1])
            eintraege = db.brett_lesen()
            if nr < 1 or nr > len(eintraege):
                return f"Es gibt keinen Eintrag Nr. {nr} (siehe BRETT)."
            return db.brett_loeschen(eintraege[nr - 1]["id"], self.p.name,
                                     ist_gott=self._ist_gott())
        text = arg.strip()[:300]
        db.brett_schreiben(self.p.name, text)
        self._sende(f"{self.p.name} heftet eine Notiz an das Anschlagbrett.")
        return "📋 Deine Notiz hängt am Brett."

    # ---------- Postsystem ----------
    def _am_briefkasten(self):
        return any(n["id"] == "briefkasten" for n in self.w.npc_in_raum(self.p.raum))

    def cmd_post(self, arg):
        if not self._am_briefkasten():
            return ("Hier steht kein Briefkasten. Am Marktplatz von Falkenmoor, "
                    "direkt beim Brunnen, findest du einen.")
        if not arg:
            briefe = db.mail_postfach(self.p.name)
            out = []
            if briefe:
                out.append("📬 Dein Postfach:")
                for b in briefe[-10:]:
                    out.append(f"  ✉ Von {b['absender']} ({b['zeit']}): {b['text']}")
            else:
                out.append("📬 Dein Postfach ist leer.")
            out.append("Registrierte Helden: " + ", ".join(db.alle_spieler()) + ", Gott")
            out.append("Schreiben: POST <name> <text>")
            return "\n".join(out)
        worte = arg.split(maxsplit=1)
        if len(worte) < 2 or not worte[1].strip():
            return "Benutzung: POST <name> <text>"
        empfaenger_wort, text = worte[0], worte[1].strip()[:400]
        if _norm(empfaenger_wort) == "gott":
            db.mail_senden(self.p.name, "Gott", text)
            # Hugo erhält automatisch eine Kopie jeder Botschaft an Gott
            if "Hugo" in db.alle_spieler() and self.p.name != "Hugo":
                db.mail_senden(f"{self.p.name} → Gott", "Hugo", text)
            db.mail_senden("Gott", self.p.name,
                           "Dein Gebet wurde erhört. Gott hat deine Botschaft gelesen "
                           "und wird sie im rechten Moment bedenken. 🕊")
            return ("📨 Dein Bote reitet gen Himmel – Gott empfängt jede Botschaft. "
                    "Eine Antwort ist bereits in deinem Postfach.")
        namen = db.alle_spieler()
        exakt = [n for n in namen if n.lower() == empfaenger_wort.lower()]
        treffer = exakt or [n for n in namen if _norm(n).startswith(_norm(empfaenger_wort))]
        if not treffer:
            return (f"Kein Held namens '{empfaenger_wort}' ist registriert. "
                    "Registrierte Helden: " + (", ".join(namen) or "(keine)") + ", Gott")
        if len(treffer) > 1:
            return "Name nicht eindeutig – gemeint sein könnten: " + ", ".join(treffer)
        empfaenger = treffer[0]
        if empfaenger == self.p.name:
            return "Dir selbst kannst du keine Post schicken – sprich mit dir selbst, das hilft auch."
        db.mail_senden(self.p.name, empfaenger, text)
        return f"📨 Dein Bote macht sich auf den Weg zu {empfaenger}!"

    # ---------- Bank (Stadtkasse im Rathaus) ----------
    def _am_schalter(self):
        return any(n["id"] == "bankier" for n in self.w.npc_in_raum(self.p.raum))

    def _betrag(self, arg):
        """Betrag aus Argument lesen: Zahl oder 'alles'. None bei Unsinn."""
        wort = _norm(arg.strip())
        if wort == "alles":
            return "alles"
        if wort.isdigit() and int(wort) > 0:
            return int(wort)
        return None

    def cmd_einzahlen(self, arg):
        if not self._am_schalter():
            return "Hier ist kein Bankschalter. Die Stadtkasse ist im Rathaus von Falkenmoor."
        betrag = self._betrag(arg)
        if betrag is None:
            return "Benutzung: EINZAHLEN <x> (oder EINZAHLEN ALLES)"
        menge = self.p.gold if betrag == "alles" else betrag
        if menge <= 0 or menge > self.p.gold:
            return "So viel Gold hast du nicht in der Tasche."
        self.p.gold -= menge
        self.p.bank += menge
        return (f"🏦 Du zahlst {menge} Gold in die Stadtkasse ein. "
                f"(Bargeld: {self.p.gold} G · Guthaben: {self.p.bank} G)")
    def cmd_abheben(self, arg):
        if not self._am_schalter():
            return "Hier ist kein Bankschalter. Die Stadtkasse ist im Rathaus von Falkenmoor."
        betrag = self._betrag(arg)
        if betrag is None:
            return "Benutzung: ABHEBEN <x> (oder ABHEBEN ALLES)"
        menge = self.p.bank if betrag == "alles" else betrag
        if menge <= 0 or menge > self.p.bank:
            return "So viel Gold ist nicht auf deinem Konto."
        self.p.bank -= menge
        self.p.gold += menge
        return (f"🏦 Du hebst {menge} Gold ab. "
                f"(Bargeld: {self.p.gold} G · Guthaben: {self.p.bank} G)")
    def cmd_kontostand(self):
        return (f"🏦 Bankguthaben: {self.p.bank} Gold · Bargeld: {self.p.gold} Gold. "
                f"Einzahlen und Abheben geht an der Stadtkasse im Rathaus.")

    # ---------- Rassen ----------
    _RASSEN_LORE = {
        "zwerg": {"de": "🛡 Du bist ein ZWERG aus dem Graukamm — Steinhatz und Brauerei-Meister. "
                        "Dein Körper ist Fels: +10 Max-HP, +1 Verteidigung. Dafür bleibt der "
                        "Zauberstab dir fremd: −5 Max-Mana.",
                  "en": "🛡 You are a DWARF of the Greycrag — hewn from stone itself: "
                        "+10 max HP, +1 defense. Magic, however, stays foreign: −5 max mana."},
        "elf": {"de": "✨ Du bist ein ELF aus den Alten Wäldern. Magie fließt durch dich: "
                       "+10 Max-Mana, Zauber kosten 1 weniger. Dein Leib ist zart: −5 Max-HP.",
                "en": "✨ You are an ELF of the Ancient Woods. Magic flows through you: "
                      "+10 max mana, spells cost 1 less. Your frame is frail: −5 max HP."},
        "ork": {"de": "⚔ Du bist ein ORK der Graufluss-Höhlen. Deine Faust spricht: +2 Schaden. "
                      "Dafür ist dein Körper grob geschmiedet: −5 Max-HP — und das Dorf traut dir "
                      "nicht über den Weg (Händler: +10 % Preise).",
                "en": "⚔ You are an ORC of the Greyriver caves. Your fist does the talking: "
                      "+2 damage. Your body is roughly forged: −5 max HP — and the village "
                      "doesn't trust you (shop prices +10%)."},
        "mensch": {"de": "🌾 Du bist ein MENSCH aus Falkenmoor — anpassungsfähig wie alle dein Stamm: "
                         "+10 Gold Siedlerbonus und +10 % XP. Nichts mehr, nichts weniger.",
                   "en": "🌾 You are a HUMAN of Falkenmoor — adaptable like all your kin: "
                         "+10 gold settler's bonus and +10% XP. Nothing more, nothing less."},
    }

    def cmd_rasse(self, arg):
        en = self.p.sprache == "en"
        if self.p.rasse:
            return self._RASSEN_LORE[self.p.rasse].get(
                self.p.sprache, self._RASSEN_LORE[self.p.rasse]["de"])
        if not arg:
            if en:
                return ("Choose your race — once, and for all time:\n"
                        "  🛡 ZWERG (dwarf): +10 max HP, +1 defense · −5 max mana\n"
                        "  ✨ ELF: +10 max mana, spells cost 1 less · −5 max HP\n"
                        "  ⚔ ORK (orc): +2 damage · −5 max HP · shop prices +10%\n"
                        "  🌾 MENSCH (human): +10 gold bonus · +10% XP\n"
                        "Choose: RASSE <zwerg|elf|ork|mensch>")
            return ("Wähle deine Rasse — einmal, für immer:\n"
                    "  🛡 ZWERG: +10 Max-HP, +1 Verteidigung · −5 Max-Mana\n"
                    "  ✨ ELF: +10 Max-Mana, Zauber kosten 1 weniger · −5 Max-HP\n"
                    "  ⚔ ORK: +2 Schaden · −5 Max-HP · Händler +10 % Preise\n"
                    "  🌾 MENSCH: +10 Gold Siedlerbonus · +10 % XP\n"
                    "Wählen: RASSE <zwerg|elf|ork|mensch>")
        wahl = _RASSEN_ALIASE.get(_norm(arg).split()[0], "")
        if not wahl:
            return ("Diese Rasse kennt Falkenmoor nicht. "
                    "Wählbar: zwerg, elf, ork, mensch." if not en else
                    "Falkenmoor doesn't know that race. Available: zwerg (dwarf), elf, ork, mensch (human).")
        # Boni anwenden — einmalig
        if wahl == "zwerg":
            self.p.max_hp += 10; self.p.hp += 10
            self.p.basis_verteidigung += 1
            self.p.max_mana = max(0, self.p.max_mana - 5)
            self.p.mana = min(self.p.mana, self.p.max_mana)
        elif wahl == "elf":
            self.p.max_mana += 10; self.p.mana = self.p.max_mana
            self.p.max_hp = max(10, self.p.max_hp - 5)
            self.p.hp = min(self.p.hp, self.p.max_hp)
        elif wahl == "ork":
            self.p.max_hp = max(10, self.p.max_hp - 5)
            self.p.hp = min(self.p.hp, self.p.max_hp)
        elif wahl == "mensch":
            self.p.gold += 10
        self.p.rasse = wahl
        return self._RASSEN_LORE[wahl].get(self.p.sprache, self._RASSEN_LORE[wahl]["de"])

    # ---------- Kommunikation ----------
    def cmd_sagen(self, arg):
        if not arg:
            return "Was sagen? SAG <Text>"
        self._sende(f"💬 {self.p.name}: {arg}")
        return f'Du sagst: „{arg}“'
    def cmd_rufen(self, arg):
        if not arg:
            return "Was rufen? RUFE <Text>"
        self.sende_alle(f"📢 {self.p.name} ruft: {arg}", ausser=self.p.name)
        return f"Du rufst: „{arg}“"
    def cmd_spieler(self):
        with _online_lock:
            gegenwaertig = [(name, e["spieler"].level) for name, e in ONLINE.items()]
        if not gegenwaertig:
            return "Außer dir ist gerade niemand hier."
        return "Helden online: " + ", ".join(
            f"{name} (Stufe {lvl})" for name, lvl in gegenwaertig)
    # ---------- Status & Hilfe ----------
    def cmd_status(self):
        waffe = self.w.items[self.p.waffe]["name"] if self.p.waffe else "Fäuste"
        ruestung = self.w.items[self.p.ruestung]["name"] if self.p.ruestung else "nur Stoff"
        rasse_txt = f" · Rasse: {self.p.rasse.capitalize()}" if self.p.rasse else ""
        return (f"👤 {self.p.name} — Stufe {self.p.level} ({self.p.xp} XP){rasse_txt}\n"
                f"❤ {self.p.hp}/{self.p.max_hp} HP  |  ⚔ Schaden {self.p.schaden}  |  🛡 Verteidigung {self.p.verteidigung}\n"
                f"Waffe: {waffe}  |  Rüstung: {ruestung}  |  💰 {self.p.gold} Gold  |  🏦 {self.p.bank} Gold (Bank)\n"
                f"Ort: {_raum_name(self.w.raeume[self.p.raum]['name'])}")
    def cmd_hilfe(self):
        if self.p.sprache == "en":
            out = uebersetz.hilfe("en")
            if self._ist_gott():
                out += ("\n\n🕊 GODLY POWER (only yours):\nGOTTPOST · GOTTWER · GOTTSPRUCH "
                        "<name> <text> · GOTTHEIL · GODMAP · GOTTGOLD <name> <±amount> · "
                        "GOTTTRANK · GOTTITEM · GOTTZUG · GOTTHOLE · GOTTZORN · GOTTMONSTER")
            return out
        return (
            "═══ FALKENMOOR — BEFEHLE ═══\n"
            "Bewegung:  NORDEN/SÜDEN/OSTEN/WESTEN (auch n/s/o/w) · UMSCHAUEN\n"
            "Dinge:     UNTERSUCHE <x> · BETRACHTE <x> · SCHAUE <x> — Details (auch: x <x>)\n"
            "Gegenstände: NIMM <x> · NIMM ALLES · ABLEGEN <x> · BENUTZE/TRINKE <x> · RÜSTE <x> · INVENTAR\n"
            "Haus:      TRUHE · LEGE <x> · HOL <x> — dein Haus am Markt (Ausgang: haus)\n"
            "Kampf:     GREIF <name> AN · FLIEHE\n"
            "Rede:      SPRICH <name> · GIB <gegenstand> <npc>\n"
            "Handel:    KAUF <ware> · VERKAUF <gegenstand> (bei Händlern)\n"
            "Bank:      EINZAHLEN <x> · ABHEBEN <x> · KONTOSTAND — Stadtkasse im Rathaus\n"
            "Sprache:   SPRACHE EN · SPRACHE DE — englische Befehle: take, buy, kill, flee …\n"
            "Chat:      SAG <text> (Raum) · RUFE <text> (alle) · SPIELER (wer online ist)\n"
            "Post:      POST (Postfach) · POST <name> <text> — am Briefkasten auf dem Marktplatz\n"
            "Brett:     BRETT (lesen) · BRETT <text> (Notiz) — am Anschlagbrett auf dem Marktplatz\n"
            "Emotes:    LACH · WEINE · TANZ · VERBEUGE [name] · ZWINKERE <name> · GRUESS [name] …\n"
            "Magie:     ZAUBER (Liste) · ZAUBER <name> [gegner] — kostet Mana (regeneriert beim Reisen)\n"
            "Gilden:    GILDE · GILDE BEITRETEN <name> (im Rathaus) · GILDENRUF <text>\n"
            "Rasse:     RASSE · RASSE <zwerg|elf|ork|mensch> — einmalig wählbar\n"
            "Quests:    QUESTS · STATUS · HILFE · QUIT (Abmelden)\n"
            "Tipp: Nachts sind die Läden geschlossen — nur die Kräuterhexe und die Kneipen "
            "schenken bei Nacht aus. Verschlossene Wege brauchen den passenden Schlüssel!"
            + ("\n🕊 GOTTESMACHT (nur für dich):\n"
               "GOTTPOST (Post an Gott) · GOTTWER (alle Helden) · "
               "GOTTSPRUCH <name> <text> · GOTTHEIL (Heilung) · GODMAP (Karte)\n"
               "GOTTGOLD <name> <±menge> · GOTTTRANK <name> <anzahl> · GOTTITEM <gegenstand>\n"
               "GOTTZUG <name> (zum Markt) · GOTTHOLE <name> (zu dir) · "
               "GOTTZORN <name> (Blitz) · GOTTMONSTER [suchwort] (erschaffen)"
               if self._ist_gott() else "")
        )

← Zurück zum Lehrpfad