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:
|
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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user