fix: Rast-System — kein dauerhaftes 0 HP mehr

- Lange Rast bei <10% HP (Lagerfeuer/Gasthaus/Waldlichtung)
- Volle Heilung nach Rast (HP + MP + Spell Slots)
- Nach Tod: Respawn mit 25% HP + Rast am Schrein/Tempel
- Nach knappem Sieg: Sofortige Rast wenn HP <10%
- HP wird nie 0 gelassen — immer max(1, hp)
- Rest-Progress-Balken: 🏕️ [████░░░░] Lagerfeuer
- Tränke werden weiterhin bei <40% HP benutzt
This commit is contained in:
arch_agent
2026-07-25 23:16:43 +02:00
parent 36cb713b86
commit cd69fa1ef0
3 changed files with 103 additions and 21 deletions
+32
View File
@@ -206,6 +206,38 @@ class DnDCharacter:
def needs_healing(self) -> bool:
return self.hp < self.max_hp * self.potion_threshold
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)
def can_fight(self) -> bool:
"""True wenn der Character kampffähig ist."""
return self.hp >= self.max_hp * 0.10
def short_rest(self, dice_log: list = None) -> int:
"""Kurze Rast (D&D 5e): Hit Dice verbrauchen um HP zu heilen."""
con_mod = modifier(self.stats["CON"])
# Heal 1 Hit Die + CON mod per rest, simplified
heal = self.hit_die + con_mod
self.hp = min(self.max_hp, self.hp + heal)
# Also restore some MP
if self.max_mp > 0:
self.mp = min(self.max_mp, self.mp + self.max_mp // 2)
if dice_log is not None:
dice_log.append(f"[yellow]🔥 Kurze Rast: Heile d{self.hit_die}+{con_mod} = {heal} HP (HP: {self.hp}/{self.max_hp})[/]")
return heal
def long_rest(self, dice_log: list = None) -> int:
"""Lange Rast (D&D 5e): Volle HP und MP Wiederherstellung."""
old_hp = self.hp
self.hp = self.max_hp
self.mp = self.max_mp
# 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
def can_cast_spell(self, cost: int = 1) -> bool:
return self.mp >= cost and self.spell_slots > 0
+63 -18
View File
@@ -150,6 +150,12 @@ class GameEngine:
self.is_traveling = False
self.travel_destination = ""
# Rest progress
self.is_resting = False
self.rest_progress = 0.0
self.rest_type = "" # "short" or "long"
self.rest_location = "" # Lagerfeuer, Gasthaus etc.
self._fill_cache()
def _fill_cache(self):
@@ -512,6 +518,32 @@ class GameEngine:
self.character.steps += 1
events = []
# === Rest phase ===
if self.is_resting:
self.rest_progress += random.uniform(0.2, 0.4)
if self.rest_progress >= 1.0:
self.is_resting = False
self.rest_progress = 0.0
if self.rest_type == "long":
healed = self.character.long_rest(self.dice_log.entries)
self._add_log(f"[bold green]🏕️ Lange Rast beendet — voll geheilt! (+{healed} HP)[/]")
else:
healed = self.character.short_rest(self.dice_log.entries)
self._add_log(f"[green]🔥 Kurze Rast beendet (+{healed} HP, +MP)[/]")
self._add_loot_log(f"[green]🏕️ Rast am {self.rest_location} — HP {self.character.hp}/{self.character.max_hp}[/]")
else:
return {"events": events, "resting": True}
# === Check if rest is needed (before travel or fight) ===
if self.character.needs_rest() and not self.is_resting:
self.rest_type = "long"
self.rest_location = random.choice(["Lagerfeuer", "Gasthaus zum Eber", "Waldlichtung", "Rastplatz"])
self.is_resting = True
self.rest_progress = 0.0
self._add_log(f"[bold green]🏕️ Du bist schwer verwundet — lange Rast am {self.rest_location}...[/]")
self.dice_log.add(f"[bold green]🏕️ Lange Rast — HP {self.character.hp}/{self.character.max_hp}, volle Erholung nötig[/]")
return {"events": events, "resting": True}
# === Travel phase ===
if self.is_traveling:
self.travel_progress += random.uniform(0.15, 0.35)
@@ -522,10 +554,8 @@ class GameEngine:
self._add_log(f"[blue]🌍 Ankunft in: {self.current_location}[/]")
self._add_log(f"[cyan]📜 Neue Quest: {self.current_quest}[/]")
self._add_log(f"[dim]Ziel: Besiege {self.quest_target} Gegner[/]")
# Visit shop on arrival
self._visit_shop()
else:
# Still traveling, no combat this step
return {"events": events, "traveling": True}
# === Random travel between fights ===
@@ -550,6 +580,15 @@ class GameEngine:
events.append(("victory", f"[green]⚔️ Besiegst {monster.name} ({result['rounds']} Runden)[/]"))
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
self.rest_type = "long"
self.rest_location = random.choice(["Lagerfeuer", "Gasthaus zum Eber", "Waldlichtung"])
self.is_resting = True
self.rest_progress = 0.0
self._add_log(f"[bold green]🏕️ Kaum überlebt — Rast am {self.rest_location}...[/]")
# XP + Gold
self.character.gold += monster.gold
leveled = self.character.gain_xp(monster.xp)
@@ -578,26 +617,24 @@ class GameEngine:
elif result["result"] == "death":
self.character.deaths += 1
# Death saving throw
save = self.character.saving_throw("CON")
self.dice_log.add(f"[yellow]🎲 Todesschaden-Save: d20{save['rolls']}+{save['modifier']} = {save['total']} (DC 10)[/]")
self.dice_log.add(f"[bold red]💀 Du wurdest von {monster.name} besiegt![/]")
if save["success"]:
self.dice_log.add(f"[green]💚 Du stabilisierst dich![/]")
self._add_log(f"[yellow]💀 {monster.name} besiegt dich, aber du überlebst![/]")
# Death penalty: lose 10% gold, respawn at safe location
lost_gold = max(1, self.character.gold // 10)
self.character.gold -= lost_gold
self.character.heal()
self._add_log(f"[red]Verlierst {lost_gold} Gold.[/]")
else:
self.dice_log.add(f"[red]💀 Rettungswurf fehlgeschlagen![/]")
self._add_log(f"[bold red]💀 Von {monster.name} besiegt! Respawn...[/]")
lost_gold = max(1, self.character.gold // 10)
self.character.gold -= lost_gold
self.character.heal()
self._add_log(f"[red]Verlierst {lost_gold} Gold beim Sterben.[/]")
self.character.hp = max(1, self.character.max_hp // 4) # respawn with 25% HP
self.character.mp = max(0, self.character.max_mp // 4)
self._add_log(f"[bold red]💀 Von {monster.name} besiegt! Du erwachst mit 25% HP am nächsten Schrein. (-{lost_gold}G)[/]")
self.dice_log.add(f"[red]💀 Respawn: -{lost_gold}G, HP {self.character.hp}/{self.character.max_hp}[/]")
events.append(("death", f"[bold red]💀 Niederlage gegen {monster.name}![/]"))
# After death, always rest to recover
self.rest_type = "long"
self.rest_location = random.choice(["Seelenstein-Schrein", "Heiliger Tempel", "Sicherer Außenposten"])
self.is_resting = True
self.rest_progress = 0.0
self._add_log(f"[bold green]🏕️ Du suchst Zuflucht am {self.rest_location} zur Erholung...[/]")
events.append(("death", f"[bold red]💀 Niederlage gegen {monster.name}! (-{lost_gold}G)[/]"))
return {"events": events}
@@ -614,6 +651,10 @@ class GameEngine:
"is_traveling": self.is_traveling,
"travel_progress": self.travel_progress,
"travel_destination": self.travel_destination,
"is_resting": self.is_resting,
"rest_progress": self.rest_progress,
"rest_type": self.rest_type,
"rest_location": self.rest_location,
}
def save(self, path: str):
@@ -638,4 +679,8 @@ class GameEngine:
engine.is_traveling = d.get("is_traveling", False)
engine.travel_progress = d.get("travel_progress", 0.0)
engine.travel_destination = d.get("travel_destination", "")
engine.is_resting = d.get("is_resting", False)
engine.rest_progress = d.get("rest_progress", 0.0)
engine.rest_type = d.get("rest_type", "")
engine.rest_location = d.get("rest_location", "")
return engine
+6 -1
View File
@@ -196,7 +196,12 @@ def render_game(engine: GameEngine) -> Layout:
bars.append(f"HP [{hp_bar}] {c.hp}/{c.max_hp} ", style="red")
bars.append(f"XP [{xp_bar}] {c.xp}/{c.xp_to_next} ", style="green")
if engine.is_traveling:
if engine.is_resting:
rest_len = 20
rest_filled = int(engine.rest_progress * rest_len)
rest_bar = "" * rest_filled + "" * (rest_len - rest_filled)
bars.append(f"🏕️ [{rest_bar}] {engine.rest_location}", style="bold green")
elif engine.is_traveling:
travel_len = 20
travel_filled = int(engine.travel_progress * travel_len)
travel_bar = "" * travel_filled + "" * (travel_len - travel_filled)