From c223f1ba2e70dfe10bebe6a0c0daddd9e0be34f1 Mon Sep 17 00:00:00 2001 From: arch_agent Date: Sat, 25 Jul 2026 23:23:51 +0200 Subject: [PATCH] =?UTF-8?q?v2.3=20=E2=80=94=20Vollst=C3=A4ndige=20D&D=205e?= =?UTF-8?q?=20Status-Effekte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 32 Status-Effekte (alle D&D 5e Conditions): Schaden über Zeit: Gift🐍, Brennen🔥, Blutung🩸, Verätzung⚗️, Nekrotisch💀 Debuffs: Schwäche💜, Erschöpfung😫, Furcht😱, Verlangsamt🐌, Geblendet🙈, Taub🔇, Gelähmt⚡, Betäubt💫, Gefesselt🔗, Niedergestreckt🤕, Versteinert🗿, Erschöpft🥵 Buffs: Regeneration💚, Gesegnet✨, Wutanfall😠, Glaubensschild🛡️, Beschleunigt⚡, Steinhaut🪨, Baumhaut🌳, Heldenmut🦸, Bewegungsfreiheit🕊️, Wahrer Streich🎯, Magische Rüstung🔮, Göttliche Gunst🙏, Vergrößert📈 Heilung: Verband🩹, Entgiftung🌿 Monster-Inflicts (nach Typ): - Spinne/Naga: Gift, Drache/Feuer: Brennen, Vampir/Bestie: Blutung - Hexe/Fluch: Schwäche, Geist/Phantom: Furcht - Schleim/Säure: Verätzung, Untot/Skelett: Nekrotisch - Medusa/Basilisk: Blenden, Oger/Troll: Betäubung - Eis/Frost: Verlangsamung, Wüste/Hitze: Erschöpfung Combat: - Gelähmt/Betäubt/Versteinert → kann nicht angreifen - Geblendet/Gefesselt/Niedergestreckt/Furcht → Attack Disadvantage - Effekte ticken jede Runde (Schaden/Heilung) Shop: - Verbände (15G): Stoppt Blutung/Brennen, heilt 1d4+2, gibt Regeneration - Gegengift (25G): Entfernt alle negativen Effekte - Trank-Anzeige: ❤️ ✨ 💖 🌿 🩹 --- src/dnd.py | 146 ++++++++++++++++++++++++++++++++++++-------------- src/engine.py | 100 +++++++++++++++++++++++++--------- src/main.py | 4 +- 3 files changed, 185 insertions(+), 65 deletions(-) diff --git a/src/dnd.py b/src/dnd.py index 64819ce..01b2356 100644 --- a/src/dnd.py +++ b/src/dnd.py @@ -249,12 +249,61 @@ class DnDCharacter: self.spell_slots -= 1 # === Status Effects === - def add_effect(self, effect_type: str, duration: int, power: int, name: str): + # Complete D&D 5e condition list adapted for auto-RPG + EFFECT_DEFINITIONS = { + # Damage over time + "poison": {"icon": "🐍", "tick": "damage", "desc": "Vergiftet"}, + "burn": {"icon": "🔥", "tick": "damage", "desc": "Brennend"}, + "bleed": {"icon": "🩸", "tick": "damage", "desc": "Blutung"}, + "acid": {"icon": "⚗️", "tick": "damage", "desc": "Verätzt"}, + "necrotic": {"icon": "💀", "tick": "damage", "desc": "Nekrotisch"}, + # Debuffs + "curse_weakness":{"icon": "💜", "tick": "none", "desc": "Schwäche", "stat": "attack", "mod": -2}, + "curse_fatigue": {"icon": "😫", "tick": "none", "desc": "Erschöpfung", "stat": "all", "mod": -1}, + "fear": {"icon": "😱", "tick": "skip_chance", "desc": "Verängstigt", "skip_pct": 50}, + "slow": {"icon": "🐌", "tick": "none", "desc": "Verlangsamt", "stat": "ac", "mod": -2}, + "blinded": {"icon": "🙈", "tick": "disadvantage", "desc": "Geblendet"}, + "deafened": {"icon": "🔇", "tick": "none", "desc": "Taub"}, + "paralyzed": {"icon": "⚡", "tick": "skip_all", "desc": "Gelähmt"}, + "stunned": {"icon": "💫", "tick": "skip_all", "desc": "Betäubt"}, + "restrained": {"icon": "🔗", "tick": "disadvantage", "desc": "Gefesselt"}, + "prone": {"icon": "🤕", "tick": "disadvantage", "desc": "Niedergestreckt"}, + "petrified": {"icon": "🗿", "tick": "skip_all", "desc": "Versteinert"}, + "exhaustion": {"icon": "🥵", "tick": "none", "desc": "Erschöpft", "stat": "all", "mod": -2}, + # Buffs + "regen": {"icon": "💚", "tick": "heal", "desc": "Regeneration"}, + "bless": {"icon": "✨", "tick": "none", "desc": "Gesegnet", "stat": "attack", "mod": 2}, + "rage": {"icon": "😠", "tick": "none", "desc": "Wutanfall", "stat": "attack", "mod": 3}, + "shield_of_faith":{"icon": "🛡️", "tick": "none", "desc": "Glaubensschild", "stat": "ac", "mod": 3}, + "haste": {"icon": "⚡", "tick": "extra_attack", "desc": "Beschleunigt"}, + "stoneskin": {"icon": "🪨", "tick": "none", "desc": "Steinhaut", "stat": "ac", "mod": 4}, + "barkskin": {"icon": "🌳", "tick": "none", "desc": "Baumhaut", "stat": "ac", "mod": 2}, + "heroism": {"icon": "🦸", "tick": "heal", "desc": "Heldenmut"}, + "freedom": {"icon": "🕊️", "tick": "none", "desc": "Bewegungsfreiheit"}, + "true_strike": {"icon": "🎯", "tick": "advantage", "desc": "Wahrer Streich"}, + "mage_armor": {"icon": "🔮", "tick": "none", "desc": "Magische Rüstung", "stat": "ac", "mod": 3}, + "divine_favor": {"icon": "🙏", "tick": "none", "desc": "Göttliche Gunst", "stat": "attack", "mod": 2}, + "enlarge": {"icon": "📈", "tick": "none", "desc": "Vergrößert", "stat": "attack", "mod": 2}, + # Bandages / healing items + "bandaged": {"icon": "🩹", "tick": "heal", "desc": "Verbunden"}, + "antidote_effect":{"icon": "🌿", "tick": "cleanse_tick", "desc": "Entgiftet"}, + } + + NEGATIVE_EFFECTS = [ + "poison", "burn", "bleed", "acid", "necrotic", + "curse_weakness", "curse_fatigue", "fear", "slow", + "blinded", "deafened", "paralyzed", "stunned", + "restrained", "prone", "petrified", "exhaustion", + ] + + def add_effect(self, effect_type: str, duration: int, power: int, name: str = ""): """Fügt einen Statuseffekt hinzu.""" - # Remove existing effect of same type self.status_effects = [e for e in self.status_effects if e["type"] != effect_type] + definition = self.EFFECT_DEFINITIONS.get(effect_type, {"icon": "❓", "desc": name}) self.status_effects.append({ - "type": effect_type, "duration": duration, "power": power, "name": name + "type": effect_type, "duration": duration, "power": power, + "name": name or definition.get("desc", effect_type), + "icon": definition.get("icon", "❓"), }) def has_effect(self, effect_type: str) -> bool: @@ -267,65 +316,88 @@ class DnDCharacter: return None def tick_effects(self) -> list: - """Tickt alle Effekte, gibt Liste der abgelaufenen und gewirkten zurück.""" + """Tickt alle Effekte pro Runde, gibt Liste der Ereignisse zurück.""" results = [] for effect in self.status_effects[:]: effect["duration"] -= 1 + etype = effect["type"] + power = effect["power"] + icon = effect.get("icon", "❓") # Apply per-turn effects - if effect["type"] == "poison": - dmg = effect["power"] - self.hp = max(0, self.hp - dmg) - results.append(f"[green]🐍 Gift schadet {dmg} HP (HP: {self.hp}/{self.max_hp})[/]") - elif effect["type"] == "burn": - dmg = effect["power"] - self.hp = max(0, self.hp - dmg) - results.append(f"[red]🔥 Brennen schadet {dmg} HP[/]") - elif effect["type"] == "regen": - heal = effect["power"] - self.hp = min(self.max_hp, self.hp + heal) - results.append(f"[green]💚 Regeneration heilt {heal} HP[/]") - elif effect["type"] == "bleed": - dmg = effect["power"] - self.hp = max(0, self.hp - dmg) - results.append(f"[red]🩸 Blutung schadet {dmg} HP[/]") + if etype in ("poison", "burn", "bleed", "acid", "necrotic"): + self.hp = max(0, self.hp - power) + results.append(f"[{self._effect_color(etype)}]{icon} {effect['name']} schadet {power} HP (HP: {self.hp}/{self.max_hp})[/]") + elif etype == "regen" or etype == "heroism": + self.hp = min(self.max_hp, self.hp + power) + results.append(f"[green]{icon} Regeneration heilt {power} HP (HP: {self.hp}/{self.max_hp})[/]") + elif etype == "bandaged": + self.hp = min(self.max_hp, self.hp + power) + results.append(f"[green]{icon} Verband heilt {power} HP[/]") + elif etype == "antidote_effect": + # Ticks: removes one negative effect per turn + for neg in self.NEGATIVE_EFFECTS: + if self.has_effect(neg) and neg != "antidote_effect": + self.status_effects = [e for e in self.status_effects if e["type"] != neg] + results.append(f"[green]{icon} Entgiftung entfernt {neg}[/]") + break if effect["duration"] <= 0: self.status_effects.remove(effect) - results.append(f"[dim]✨ {effect['name']} ist abgeklungen.[/]") + results.append(f"[dim]{icon} {effect['name']} ist abgeklungen.[/]") return results + def _effect_color(self, etype: str) -> str: + colors = { + "poison": "green", "burn": "red", "bleed": "red", + "acid": "yellow", "necrotic": "bold red", + } + return colors.get(etype, "red") + + def is_immobilized(self) -> bool: + """True wenn der Charakter nicht handeln kann.""" + return self.has_effect("paralyzed") or self.has_effect("stunned") or self.has_effect("petrified") + + def has_attack_disadvantage(self) -> bool: + return self.has_effect("blinded") or self.has_effect("restrained") or self.has_effect("prone") or self.has_effect("fear") + def cleanse(self) -> int: """Entfernt alle negativen Effekte. Gibt Anzahl entfernter zurück.""" - negative = ["poison", "burn", "bleed", "curse_weakness", "curse_fatigue", "fear", "slow"] removed = 0 - for neg in negative: + for neg in self.NEGATIVE_EFFECTS: before = len(self.status_effects) self.status_effects = [e for e in self.status_effects if e["type"] != neg] removed += before - len(self.status_effects) return removed + def apply_bandage(self) -> int: + """Verband anwenden — heilt und fügt regen-Effekt hinzu.""" + heal = roll(4, 1, 2)["total"] # 1d4+2 + self.hp = min(self.max_hp, self.hp + heal) + self.add_effect("bandaged", 3, 2, "Verband") + return heal + @property def effective_ac(self) -> int: """AC mit Buffs/Debuffs.""" ac = self.ac - if self.has_effect("shield_of_faith"): - ac += self.get_effect("shield_of_faith")["power"] - if self.has_effect("curse_weakness"): - ac -= 2 + for effect in self.status_effects: + definition = self.EFFECT_DEFINITIONS.get(effect["type"], {}) + if definition.get("stat") == "ac": + ac += definition.get("mod", 0) + effect.get("power", 0) return max(1, ac) @property def effective_attack(self) -> int: """Attack Bonus mit Buffs/Debuffs.""" bonus = self.attack_bonus - if self.has_effect("bless"): - bonus += 2 - if self.has_effect("curse_weakness"): - bonus -= 2 - if self.has_effect("rage"): - bonus += self.get_effect("rage")["power"] + for effect in self.status_effects: + definition = self.EFFECT_DEFINITIONS.get(effect["type"], {}) + if definition.get("stat") == "attack": + bonus += definition.get("mod", 0) + effect.get("power", 0) + elif definition.get("stat") == "all": + bonus += definition.get("mod", 0) return max(0, bonus) @property @@ -333,15 +405,9 @@ class DnDCharacter: """Kurze Zusammenfassung der aktiven Effekte für UI.""" if not self.status_effects: return "[dim]keine[/]" - icons = { - "poison": "🐍", "burn": "🔥", "regen": "💚", "bleed": "🩸", - "bless": "✨", "rage": "😠", "shield_of_faith": "🛡️", - "curse_weakness": "💜", "curse_fatigue": "😫", "fear": "😱", "slow": "🐌", - "haste": "⚡", "strength": "💪", - } parts = [] for e in self.status_effects: - icon = icons.get(e["type"], "❓") + icon = e.get("icon", "❓") parts.append(f"{icon}{e['duration']}") return " ".join(parts) diff --git a/src/engine.py b/src/engine.py index cef80ff..0477b91 100644 --- a/src/engine.py +++ b/src/engine.py @@ -67,18 +67,41 @@ class DnDMonster: # Status effects the monster can inflict (based on type) self.inflicts = [] - monster_type = name.lower() - if any(w in monster_type for w in ["spinne", "naga", "schlang", "skorpion", "gift"]): + mt = name.lower() + # Poison + if any(w in mt for w in ["spinne", "naga", "schlang", "skorpion", "gift", "nattern"]): self.inflicts.append({"type": "poison", "chance": 30, "duration": 3, "power": max(1, level // 2), "name": "Giftbiss"}) - if any(w in monster_type for w in ["drache", "feuer", "dämon", "phoenix", "flamme"]): + # Burn + if any(w in mt for w in ["drache", "feuer", "dämon", "phoenix", "flamme", "elementar"]): self.inflicts.append({"type": "burn", "chance": 25, "duration": 2, "power": max(1, level // 2), "name": "Feueratem"}) - if any(w in monster_type for w in ["vampir", "blut", "schnitter", "todes"]): + # Bleed + if any(w in mt for w in ["vampir", "blut", "schnitter", "todes", "werwolf", "bestie"]): self.inflicts.append({"type": "bleed", "chance": 35, "duration": 3, "power": max(1, level // 3), "name": "Blutung"}) - if any(w in monster_type for w in ["hexe", "fluch", "verflucht", "schatten", "dunkel"]): + # Curse + if any(w in mt for w in ["hexe", "fluch", "verflucht", "schatten", "dunkel", "necroman"]): self.inflicts.append({"type": "curse_weakness", "chance": 20, "duration": 3, "power": 2, "name": "Schwächungsfluch"}) - if any(w in monster_type for w in ["geist", "gespenst", "furcht", "schrecken"]): + # Fear + if any(w in mt for w in ["geist", "gespenst", "furcht", "schrecken", "phantom", "wraith"]): self.inflicts.append({"type": "fear", "chance": 20, "duration": 2, "power": 2, "name": "Furcht"}) - # Sometimes random poison for variety + # Acid + if any(w in mt for w in ["schleim", "säure", "gallert", "ozy", "schlam"]): + self.inflicts.append({"type": "acid", "chance": 30, "duration": 3, "power": max(1, level // 2), "name": "Verätzung"}) + # Necrotic + if any(w in mt for w in ["untot", "skelett", "wiederg", "ghoul", "leiche", "necro"]): + self.inflicts.append({"type": "necrotic", "chance": 20, "duration": 2, "power": max(1, level // 2), "name": "Nekrotische Aura"}) + # Blinded + if any(w in mt for w in ["medusa", "basilisk", "blitz", "licht", "sonne"]): + self.inflicts.append({"type": "blinded", "chance": 25, "duration": 2, "power": 1, "name": "Blendender Blick"}) + # Stunned + if any(w in mt for w in ["ogre", "ries", "troll", "donner", "schlag"]): + self.inflicts.append({"type": "stunned", "chance": 10, "duration": 1, "power": 1, "name": "Betäubender Schlag"}) + # Slow + if any(w in mt for w in ["eis", "frost", "kälte", "winter"]): + self.inflicts.append({"type": "slow", "chance": 25, "duration": 3, "power": 2, "name": "Verlangsamende Kälte"}) + # Exhaustion + if any(w in mt for w in ["wüste", "sand", "hitz", "phönix"]): + self.inflicts.append({"type": "exhaustion", "chance": 15, "duration": 3, "power": 2, "name": "Erschöpfung"}) + # Random poison for variety if not self.inflicts and random.randint(1, 4) == 1: self.inflicts.append({"type": "poison", "chance": 15, "duration": 2, "power": 1, "name": "Gift"}) @@ -296,26 +319,25 @@ class GameEngine: self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]") return {"result": "death", "rounds": rounds, "monster": monster.name} - # Use cleanse potion if badly afflicted - if self.character.has_effect("poison") and self.character.potions.get("healing", 0) > 0 and self.character.hp < self.character.max_hp * 0.5: - # Use healing potion which also cleanses (simplified) - pass # potion use happens in _use_potions_if_needed before combat - - # Caster tries spell first (50% chance per round) - if rounds == 1 and self.character.max_mp > 0 and random.random() < 0.5: - if self._cast_spell_if_possible(monster): + # Check immobilization + if self.character.is_immobilized(): + self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") + else: + # Caster tries spell first (50% chance per round) + if rounds == 1 and self.character.max_mp > 0 and random.random() < 0.5: + if self._cast_spell_if_possible(monster): + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]") + break + + # Player attacks + result = self._player_attack(monster) + if result["hit"]: + monster.hp = max(0, monster.hp - result["damage"]) if not monster.is_alive: - self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]") + self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") break - # Player attacks - result = self._player_attack(monster) - if result["hit"]: - monster.hp = max(0, monster.hp - result["damage"]) - if not monster.is_alive: - self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") - break - # Monster attacks if monster.is_alive: m_result = monster.attack(self.character.effective_ac, self.dice_log.entries) @@ -518,6 +540,17 @@ class GameEngine: self._add_loot_log(f"[green]🧪 Kaufe Gegengift (-{antidote_price}G)[/]") bought_something = True + # 8. Bandages (Verbände) — heal + stop bleeding + bandage_price = 15 + max_bandages = 5 + while c.potions.get("bandage", 0) < max_bandages and c.gold - bandage_price >= gold_reserve: + c.gold -= bandage_price + c.potions["bandage"] = c.potions.get("bandage", 0) + 1 + c.gold_spent += bandage_price + c.items_bought += 1 + self._add_loot_log(f"[green]🩹 Kaufe Verband (-{bandage_price}G)[/]") + bought_something = True + if bought_something: self._add_log(f"[yellow]🏪 Shop besucht — Ausrüstung und Tränke gekauft[/]") self.dice_log.add(f"[yellow]🏪 Shop: {c.items_bought} Items, Rest-Gold: {c.gold}G[/]") @@ -534,6 +567,25 @@ class GameEngine: self.dice_log.add(f"[green]🧪 Gegengift benutzt! {removed} Effekte entfernt.[/]") self._add_log(f"[green]🧪 Gegengift benutzt — Vergiftung geheilt![/]") + # Bandage if bleeding or burning + if c.has_effect("bleed") and c.potions.get("bandage", 0) > 0: + heal = c.apply_bandage() + c.potions["bandage"] -= 1 + c.potions_used += 1 + # Remove bleed + c.status_effects = [e for e in c.status_effects if e["type"] != "bleed"] + self.dice_log.add(f"[green]🩹 Verband benutzt! Blutung gestoppt, +{heal} HP[/]") + self._add_log(f"[green]🩹 Verband — Blutung gestoppt (+{heal} HP)[/]") + + # Bandage also helps with burn (smother flames) + if c.has_effect("burn") and c.potions.get("bandage", 0) > 0 and not c.has_effect("bleed"): + heal = c.apply_bandage() + c.potions["bandage"] -= 1 + c.potions_used += 1 + c.status_effects = [e for e in c.status_effects if e["type"] != "burn"] + self.dice_log.add(f"[green]🩹 Verband löscht Feuer! +{heal} HP[/]") + self._add_log(f"[green]🩹 Verband — Brennen gelöscht (+{heal} HP)[/]") + # Antidote also removes curses if desperate if c.has_effect("curse_weakness") and c.potions.get("antidote", 0) > 0 and c.hp < c.max_hp * 0.3: removed = c.cleanse() diff --git a/src/main.py b/src/main.py index 61c4182..989e914 100644 --- a/src/main.py +++ b/src/main.py @@ -109,7 +109,9 @@ def render_game(engine: GameEngine) -> Layout: # Potions pot_str = f"❤️{c.potions.get('healing',0)} " \ f"✨{c.potions.get('mana',0)} " \ - f"💖{c.potions.get('greater_healing',0)}" + f"💖{c.potions.get('greater_healing',0)} " \ + f"🌿{c.potions.get('antidote',0)} " \ + f"🩹{c.potions.get('bandage',0)}" char_table.add_row("Tränke", pot_str) # Status effects