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:
|
def needs_healing(self) -> bool:
|
||||||
return self.hp < self.max_hp * self.potion_threshold
|
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:
|
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
|
||||||
|
|
||||||
|
|||||||
+65
-20
@@ -150,6 +150,12 @@ class GameEngine:
|
|||||||
self.is_traveling = False
|
self.is_traveling = False
|
||||||
self.travel_destination = ""
|
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()
|
self._fill_cache()
|
||||||
|
|
||||||
def _fill_cache(self):
|
def _fill_cache(self):
|
||||||
@@ -512,6 +518,32 @@ class GameEngine:
|
|||||||
self.character.steps += 1
|
self.character.steps += 1
|
||||||
events = []
|
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 ===
|
# === Travel phase ===
|
||||||
if self.is_traveling:
|
if self.is_traveling:
|
||||||
self.travel_progress += random.uniform(0.15, 0.35)
|
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"[blue]🌍 Ankunft in: {self.current_location}[/]")
|
||||||
self._add_log(f"[cyan]📜 Neue Quest: {self.current_quest}[/]")
|
self._add_log(f"[cyan]📜 Neue Quest: {self.current_quest}[/]")
|
||||||
self._add_log(f"[dim]Ziel: Besiege {self.quest_target} Gegner[/]")
|
self._add_log(f"[dim]Ziel: Besiege {self.quest_target} Gegner[/]")
|
||||||
# Visit shop on arrival
|
|
||||||
self._visit_shop()
|
self._visit_shop()
|
||||||
else:
|
else:
|
||||||
# Still traveling, no combat this step
|
|
||||||
return {"events": events, "traveling": True}
|
return {"events": events, "traveling": True}
|
||||||
|
|
||||||
# === Random travel between fights ===
|
# === Random travel between fights ===
|
||||||
@@ -550,6 +580,15 @@ class GameEngine:
|
|||||||
events.append(("victory", f"[green]⚔️ Besiegst {monster.name} ({result['rounds']} Runden)[/]"))
|
events.append(("victory", f"[green]⚔️ Besiegst {monster.name} ({result['rounds']} Runden)[/]"))
|
||||||
self._add_log(events[-1][1])
|
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
|
# XP + Gold
|
||||||
self.character.gold += monster.gold
|
self.character.gold += monster.gold
|
||||||
leveled = self.character.gain_xp(monster.xp)
|
leveled = self.character.gain_xp(monster.xp)
|
||||||
@@ -578,26 +617,24 @@ class GameEngine:
|
|||||||
|
|
||||||
elif result["result"] == "death":
|
elif result["result"] == "death":
|
||||||
self.character.deaths += 1
|
self.character.deaths += 1
|
||||||
# Death saving throw
|
self.dice_log.add(f"[bold red]💀 Du wurdest von {monster.name} besiegt![/]")
|
||||||
save = self.character.saving_throw("CON")
|
|
||||||
self.dice_log.add(f"[yellow]🎲 Todesschaden-Save: d20{save['rolls']}+{save['modifier']} = {save['total']} (DC 10)[/]")
|
|
||||||
|
|
||||||
if save["success"]:
|
# Death penalty: lose 10% gold, respawn at safe location
|
||||||
self.dice_log.add(f"[green]💚 Du stabilisierst dich![/]")
|
lost_gold = max(1, self.character.gold // 10)
|
||||||
self._add_log(f"[yellow]💀 {monster.name} besiegt dich, aber du überlebst![/]")
|
self.character.gold -= lost_gold
|
||||||
lost_gold = max(1, self.character.gold // 10)
|
self.character.hp = max(1, self.character.max_hp // 4) # respawn with 25% HP
|
||||||
self.character.gold -= lost_gold
|
self.character.mp = max(0, self.character.max_mp // 4)
|
||||||
self.character.heal()
|
self._add_log(f"[bold red]💀 Von {monster.name} besiegt! Du erwachst mit 25% HP am nächsten Schrein. (-{lost_gold}G)[/]")
|
||||||
self._add_log(f"[red]Verlierst {lost_gold} Gold.[/]")
|
self.dice_log.add(f"[red]💀 Respawn: -{lost_gold}G, HP {self.character.hp}/{self.character.max_hp}[/]")
|
||||||
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.[/]")
|
|
||||||
|
|
||||||
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}
|
return {"events": events}
|
||||||
|
|
||||||
@@ -614,6 +651,10 @@ class GameEngine:
|
|||||||
"is_traveling": self.is_traveling,
|
"is_traveling": self.is_traveling,
|
||||||
"travel_progress": self.travel_progress,
|
"travel_progress": self.travel_progress,
|
||||||
"travel_destination": self.travel_destination,
|
"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):
|
def save(self, path: str):
|
||||||
@@ -638,4 +679,8 @@ class GameEngine:
|
|||||||
engine.is_traveling = d.get("is_traveling", False)
|
engine.is_traveling = d.get("is_traveling", False)
|
||||||
engine.travel_progress = d.get("travel_progress", 0.0)
|
engine.travel_progress = d.get("travel_progress", 0.0)
|
||||||
engine.travel_destination = d.get("travel_destination", "")
|
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
|
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"HP [{hp_bar}] {c.hp}/{c.max_hp} ", style="red")
|
||||||
bars.append(f"XP [{xp_bar}] {c.xp}/{c.xp_to_next} ", style="green")
|
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_len = 20
|
||||||
travel_filled = int(engine.travel_progress * travel_len)
|
travel_filled = int(engine.travel_progress * travel_len)
|
||||||
travel_bar = "█" * travel_filled + "░" * (travel_len - travel_filled)
|
travel_bar = "█" * travel_filled + "░" * (travel_len - travel_filled)
|
||||||
|
|||||||
Reference in New Issue
Block a user