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:
+32
@@ -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
|
||||
|
||||
|
||||
+65
-20
@@ -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![/]")
|
||||
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.[/]")
|
||||
# Death penalty: lose 10% gold, respawn at safe location
|
||||
lost_gold = max(1, self.character.gold // 10)
|
||||
self.character.gold -= lost_gold
|
||||
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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user