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:
+9
-5
@@ -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
|
||||
|
||||
|
||||
+35
-25
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user