diff --git a/src/dnd.py b/src/dnd.py index c66aec3..df58846 100644 --- a/src/dnd.py +++ b/src/dnd.py @@ -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 diff --git a/src/engine.py b/src/engine.py index ae66c46..e9e2f2d 100644 --- a/src/engine.py +++ b/src/engine.py @@ -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 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 0c393d8..5f07f18 100644 --- a/src/main.py +++ b/src/main.py @@ -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)