fix: is_alive war Methode statt Property — Tod wurde nie erkannt!

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
This commit is contained in:
arch_agent
2026-07-25 23:37:55 +02:00
parent c223f1ba2e
commit d24f0fd950
2 changed files with 44 additions and 30 deletions
+9 -5
View File
@@ -211,7 +211,7 @@ class DnDCharacter:
def needs_rest(self) -> bool: def needs_rest(self) -> bool:
"""True wenn HP oder MP so niedrig sind dass eine Rast nötig ist.""" """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: def can_fight(self) -> bool:
"""True wenn der Character kampffähig ist.""" """True wenn der Character kampffähig ist."""
@@ -231,15 +231,18 @@ class DnDCharacter:
return heal return heal
def long_rest(self, dice_log: list = None) -> int: 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 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 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 # Refill some spell slots
self.spell_slots = self._calc_spell_slots() self.spell_slots = self._calc_spell_slots()
if dice_log is not None: 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}[/]") dice_log.append(f"[bold green]🏕️ Lange Rast: HP {old_hp}{self.hp}/{self.max_hp}, MP→{self.max_mp}, Status gereinigt[/]")
return self.max_hp - old_hp return self.hp - old_hp
def can_cast_spell(self, cost: int = 1) -> bool: def can_cast_spell(self, cost: int = 1) -> bool:
return self.mp >= cost and self.spell_slots > 0 return self.mp >= cost and self.spell_slots > 0
@@ -477,6 +480,7 @@ class DnDCharacter:
return True return True
return False return False
@property
def is_alive(self) -> bool: def is_alive(self) -> bool:
return self.hp > 0 return self.hp > 0
+35 -25
View File
@@ -53,16 +53,16 @@ class DnDMonster:
self.level = level self.level = level
self.challenge_rating = max(0.25, level / 4) 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.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 self.max_hp = self.hp
# Attack: d20 + prof + STR/DEX # Attack: d20 + prof + STR/DEX
self.attack_mod = level // 2 + 2 + random.randint(0, 2) self.attack_mod = level // 2 + 2 + random.randint(0, 2)
# Damage: weapon dice + mod # 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) self.damage_mod = max(0, level // 3)
# Status effects the monster can inflict (based on type) # 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})[/]") dice_log.append(f"[yellow]💨 {self.name} verfehlt! (AC {target_ac})[/]")
return {"hit": False, "damage": 0, "crit": False, "fumble": False, "effect": None} return {"hit": False, "damage": 0, "crit": False, "fumble": False, "effect": None}
# Damage roll # Damage roll — higher variance for danger
dice_str = self.damage_dice dice_str = self.damage_dice
count, sides = map(int, dice_str.split("d")) count, sides = map(int, dice_str.split("d"))
dmg_roll = roll(sides, count, self.damage_mod) dmg_roll = roll(sides, count, self.damage_mod)
if crit: if crit:
extra = roll(sides, count, 0) 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![/]") dice_log.append(f"[bold red]💥 KRITISCH! {dice_str}+{self.damage_mod} = {dmg_roll['total']} + {extra['total']} (crit) = {total_dmg} Schaden![/]")
else: else:
total_dmg = dmg_roll["total"] 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 # Check for status effect infliction
effect_inflicted = None effect_inflicted = None
@@ -224,10 +230,12 @@ class GameEngine:
name = random.choice(FALLBACK_MONSTERS) name = random.choice(FALLBACK_MONSTERS)
# Add adjective sometimes # Add adjective sometimes
if random.randint(1, 3) == 1: 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}" 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) return DnDMonster(name, level, self.character.level)
def _generate_loot(self, monster: DnDMonster) -> Item: 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![/]") self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]")
return {"result": "death", "rounds": rounds, "monster": monster.name} 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 # Check immobilization
if self.character.is_immobilized(): if self.character.is_immobilized():
self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") 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"]) monster.hp = max(0, monster.hp - result["damage"])
if not monster.is_alive: if not monster.is_alive:
self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") 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 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} ──[/]") 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} return {"result": "victory", "rounds": rounds, "monster": monster.name}
@@ -705,8 +716,7 @@ class GameEngine:
self._add_log(events[-1][1]) self._add_log(events[-1][1])
# If barely survived, start resting immediately # If barely survived, start resting immediately
if self.character.hp < self.character.max_hp * 0.10: if self.character.hp < self.character.max_hp * 0.10 and self.character.hp > 0:
self.character.hp = max(1, self.character.hp) # ensure not 0
self.rest_type = "long" self.rest_type = "long"
self.rest_location = random.choice(["Lagerfeuer", "Gasthaus zum Eber", "Waldlichtung"]) self.rest_location = random.choice(["Lagerfeuer", "Gasthaus zum Eber", "Waldlichtung"])
self.is_resting = True self.is_resting = True