From d24f0fd9506d06ffcdeb79d6859c5fce158e6ea2 Mon Sep 17 00:00:00 2001 From: arch_agent Date: Sat, 25 Jul 2026 23:37:55 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20is=5Falive=20war=20Methode=20statt=20Pro?= =?UTF-8?q?perty=20=E2=80=94=20Tod=20wurde=20nie=20erkannt!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KRITISCHER BUG: DnDCharacter.is_alive war eine Methode, kein @property. 'not self.character.is_alive' war immer False (method object is truthy). Der Spieler konnte nie sterben — HP blieb bei 0 aber Kampf ging weiter. Fix: @property decorator hinzugefügt. Jetzt funktioniert der Tod. Weitere Balance-Änderungen: - Monster greifen zuerst an (Initiative-Nachteil für Spieler) - Lange Rast heilt nur 75% statt 100% + reinigt Status-Effekte - needs_rest bei 15% HP (vorher 5%) - Monster-Stats moderat (nicht zu stark, nicht zu schwach) - 10% Chance für Monster Bonus-Schaden-Spike - Beide sterben im selben Zug → Spieler stirbt --- src/dnd.py | 14 +++++++----- src/engine.py | 60 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/dnd.py b/src/dnd.py index 01b2356..659ff7e 100644 --- a/src/dnd.py +++ b/src/dnd.py @@ -211,7 +211,7 @@ class DnDCharacter: def needs_rest(self) -> bool: """True wenn HP oder MP so niedrig sind dass eine Rast nötig ist.""" - return self.hp < self.max_hp * 0.10 or (self.max_mp > 0 and self.mp < self.max_mp * 0.15) + return self.hp < self.max_hp * 0.15 or (self.max_mp > 0 and self.mp < self.max_mp * 0.15) def can_fight(self) -> bool: """True wenn der Character kampffähig ist.""" @@ -231,15 +231,18 @@ class DnDCharacter: return heal def long_rest(self, dice_log: list = None) -> int: - """Lange Rast (D&D 5e): Volle HP und MP Wiederherstellung.""" + """Lange Rast (D&D 5e): Heilt 75% HP und volle MP.""" old_hp = self.hp - self.hp = self.max_hp + heal_target = int(self.max_hp * 0.75) + self.hp = max(self.hp, heal_target) # heal to at least 75% self.mp = self.max_mp + # Remove negative status effects on rest + self.status_effects = [e for e in self.status_effects if e["type"] not in self.NEGATIVE_EFFECTS] # Refill some spell slots self.spell_slots = self._calc_spell_slots() if dice_log is not None: - dice_log.append(f"[bold green]🏕️ Lange Rast am Lagerfeuer: HP {old_hp}→{self.max_hp}, MP→{self.max_mp}[/]") - return self.max_hp - old_hp + dice_log.append(f"[bold green]🏕️ Lange Rast: HP {old_hp}→{self.hp}/{self.max_hp}, MP→{self.max_mp}, Status gereinigt[/]") + return self.hp - old_hp def can_cast_spell(self, cost: int = 1) -> bool: return self.mp >= cost and self.spell_slots > 0 @@ -477,6 +480,7 @@ class DnDCharacter: return True return False + @property def is_alive(self) -> bool: return self.hp > 0 diff --git a/src/engine.py b/src/engine.py index 0477b91..93b5ebf 100644 --- a/src/engine.py +++ b/src/engine.py @@ -53,16 +53,16 @@ class DnDMonster: self.level = level self.challenge_rating = max(0.25, level / 4) - # Stats basierend auf Challenge Rating + # Stats basierend auf Challenge Rating — balanced self.ac = 10 + level + random.randint(0, 3) - self.hp = max(1, int((level * 8) + random.randint(0, 10))) + self.hp = max(1, int((level * 6) + random.randint(0, 8))) self.max_hp = self.hp # Attack: d20 + prof + STR/DEX self.attack_mod = level // 2 + 2 + random.randint(0, 2) # Damage: weapon dice + mod - self.damage_dice = random.choice(["1d6", "1d8", "1d10", "2d6", "1d12"]) + self.damage_dice = random.choice(["1d6", "1d8", "1d10", "2d6"]) self.damage_mod = max(0, level // 3) # Status effects the monster can inflict (based on type) @@ -136,18 +136,24 @@ class DnDMonster: dice_log.append(f"[yellow]💨 {self.name} verfehlt! (AC {target_ac})[/]") return {"hit": False, "damage": 0, "crit": False, "fumble": False, "effect": None} - # Damage roll + # Damage roll — higher variance for danger dice_str = self.damage_dice count, sides = map(int, dice_str.split("d")) dmg_roll = roll(sides, count, self.damage_mod) if crit: extra = roll(sides, count, 0) - total_dmg = dmg_roll["total"] + extra["total"] - self.damage_mod + total_dmg = dmg_roll["total"] + extra["total"] dice_log.append(f"[bold red]💥 KRITISCH! {dice_str}+{self.damage_mod} = {dmg_roll['total']} + {extra['total']} (crit) = {total_dmg} Schaden![/]") else: total_dmg = dmg_roll["total"] - dice_log.append(f"[red]⚔️ {self.name} trifft: {dice_str}+{self.damage_mod} = {total_dmg} Schaden[/]") + # 10% chance for bonus damage spike + if random.randint(1, 100) <= 10: + bonus = roll(sides, 1, 0)["total"] + total_dmg += bonus + dice_log.append(f"[red]⚔️ {self.name} trifft hart: {dice_str}+{self.damage_mod}+{bonus} = {total_dmg} Schaden![/]") + else: + dice_log.append(f"[red]⚔️ {self.name} trifft: {dice_str}+{self.damage_mod} = {total_dmg} Schaden[/]") # Check for status effect infliction effect_inflicted = None @@ -224,10 +230,12 @@ class GameEngine: name = random.choice(FALLBACK_MONSTERS) # Add adjective sometimes if random.randint(1, 3) == 1: - adj = random.choice(["Wütender", "Verirrter", "Verfluchter", "Hungriger", "Müder", "Brüllender"]) + adj = random.choice(["Wütender", "Verirrter", "Verfluchter", "Hungriger", "Müder", "Brüllender", "Stärkerer", "Alter", "Junger"]) name = f"{adj} {name}" - level = max(1, self.character.level + random.randint(-1, 1)) + # Monster level: sometimes higher than player for danger + level_roll = random.randint(-1, 2) + level = max(1, self.character.level + level_roll) return DnDMonster(name, level, self.character.level) def _generate_loot(self, monster: DnDMonster) -> Item: @@ -319,6 +327,20 @@ class GameEngine: self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]") return {"result": "death", "rounds": rounds, "monster": monster.name} + # Monster attacks FIRST (initiative disadvantage for player) + if monster.is_alive: + m_result = monster.attack(self.character.effective_ac, self.dice_log.entries) + self.dice_log.entries = self.dice_log.entries + if m_result["hit"]: + self.character.hp = max(0, self.character.hp - m_result["damage"]) + if m_result.get("effect"): + eff = m_result["effect"] + self.character.add_effect(eff["type"], eff["duration"], eff["power"], eff["name"]) + self._add_log(f"[yellow]⚠️ {eff['name']} erhalten! ({eff['duration']} Runden)[/]") + if not self.character.is_alive: + self.dice_log.add(f"[bold red]💀 Du wurdest besiegt![/]") + return {"result": "death", "rounds": rounds, "monster": monster.name} + # Check immobilization if self.character.is_immobilized(): self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") @@ -336,23 +358,12 @@ class GameEngine: monster.hp = max(0, monster.hp - result["damage"]) if not monster.is_alive: self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") + # Check if player also died from monster's last hit + if not self.character.is_alive: + self.dice_log.add(f"[bold red]💀 Beide fallen — aber du stirbst![/]") + return {"result": "death", "rounds": rounds, "monster": monster.name} break - # Monster attacks - if monster.is_alive: - m_result = monster.attack(self.character.effective_ac, self.dice_log.entries) - self.dice_log.entries = self.dice_log.entries - if m_result["hit"]: - self.character.hp = max(0, self.character.hp - m_result["damage"]) - # Apply status effect - if m_result.get("effect"): - eff = m_result["effect"] - self.character.add_effect(eff["type"], eff["duration"], eff["power"], eff["name"]) - self._add_log(f"[yellow]⚠️ {eff['name']} erhalten! ({eff['duration']} Runden)[/]") - if not self.character.is_alive: - self.dice_log.add(f"[bold red]💀 Du wurdest besiegt![/]") - return {"result": "death", "rounds": rounds, "monster": monster.name} - self.dice_log.add(f"[dim]── Runde {rounds} ── HP: {self.character.hp}/{self.character.max_hp} | Monster: {monster.hp}/{monster.max_hp} | Status: {self.character.status_summary} ──[/]") return {"result": "victory", "rounds": rounds, "monster": monster.name} @@ -705,8 +716,7 @@ class GameEngine: self._add_log(events[-1][1]) # If barely survived, start resting immediately - if self.character.hp < self.character.max_hp * 0.10: - self.character.hp = max(1, self.character.hp) # ensure not 0 + if self.character.hp < self.character.max_hp * 0.10 and self.character.hp > 0: self.rest_type = "long" self.rest_location = random.choice(["Lagerfeuer", "Gasthaus zum Eber", "Waldlichtung"]) self.is_resting = True