diff --git a/src/engine.py b/src/engine.py index 93b5ebf..cbd987c 100644 --- a/src/engine.py +++ b/src/engine.py @@ -54,7 +54,7 @@ class DnDMonster: self.challenge_rating = max(0.25, level / 4) # Stats basierend auf Challenge Rating — balanced - self.ac = 10 + level + random.randint(0, 3) + self.ac = max(10, 10 + level + random.randint(0, 3)) self.hp = max(1, int((level * 6) + random.randint(0, 8))) self.max_hp = self.hp @@ -311,8 +311,48 @@ class GameEngine: return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False} def _combat(self, monster: DnDMonster) -> dict: - """D&D 5e Combat: abwechselnde Runden mit Status-Effekten.""" + """D&D 5e Combat: Initiative-Wurf, Surprise, abwechselnde Runden mit Status-Effekten.""" rounds = 0 + + # === Initiative === + player_init = roll_d20(modifier(self.character.stats["DEX"])) + monster_init = roll_d20(modifier({"STR": 0, "DEX": monster.attack_mod, "CON": 0, "INT": 0, "WIS": 0, "CHA": 0}.get("DEX", 0))) + + player_first = player_init["total"] >= monster_init["total"] + self.dice_log.add(f"[cyan]🎲 Initiative: Du d20{player_init['rolls']}+{modifier(self.character.stats['DEX'])}={player_init['total']} vs {monster.name} d20{monster_init['rolls']}={monster_init['total']} → {'Du zuerst' if player_first else 'Monster zuerst'}[/]") + + # === Surprise check (15% chance either side) === + surprise_roll = random.randint(1, 100) + player_surprised = surprise_roll <= 8 # 8% chance monster surprises player + monster_surprised = surprise_roll > 92 # 8% chance player surprises monster + + if player_surprised: + self.dice_log.add(f"[bold red]😱 Überraschungsangriff! {monster.name} hat dich überrascht — eine freie Runde![/]") + self._add_log(f"[bold red]😱 {monster.name} überfällt dich! Überraschungsangriff![/]") + # Monster gets a free attack + 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 im Überraschungsangriff besiegt![/]") + return {"result": "death", "rounds": 0, "monster": monster.name} + + elif monster_surprised: + self.dice_log.add(f"[bold green]🗡️ Du überraschst {monster.name}! Freie Runde![/]") + self._add_log(f"[bold green]🗡️ Du überraschst {monster.name} — freie Runde![/]") + # Player gets a free attack + result = self._player_attack(monster) + if result["hit"]: + monster.hp = max(0, monster.hp - result["damage"]) + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} im Überraschungsangriff besiegt![/]") + return {"result": "victory", "rounds": 0, "monster": monster.name} + self.dice_log.add(f"[bold]━━━ Kampf: {self.character.name} vs {monster.name} (AC {monster.ac}, HP {monster.hp}) ━━━[/]") while monster.is_alive and self.character.is_alive: @@ -327,42 +367,68 @@ class GameEngine: self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]") 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 - if self.character.is_immobilized(): - self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") - else: - # Caster tries spell first (50% chance per round) - if rounds == 1 and self.character.max_mp > 0 and random.random() < 0.5: - if self._cast_spell_if_possible(monster): + if player_first: + # Player attacks first this round + if self.character.is_immobilized(): + self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") + else: + # Caster tries spell first (50% chance, round 1 only) + if rounds == 1 and self.character.max_mp > 0 and random.random() < 0.5: + if self._cast_spell_if_possible(monster): + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]") + break + result = self._player_attack(monster) + if result["hit"]: + monster.hp = max(0, monster.hp - result["damage"]) if not monster.is_alive: - self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]") + self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") + 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 - # Player attacks - result = self._player_attack(monster) - if result["hit"]: - monster.hp = max(0, monster.hp - result["damage"]) - if not monster.is_alive: - self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") - # Check if player also died from monster's last hit + # Monster attacks second + 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]💀 Beide fallen — aber du stirbst![/]") + self.dice_log.add(f"[bold red]💀 Du wurdest besiegt![/]") return {"result": "death", "rounds": rounds, "monster": monster.name} - break + else: + # Monster attacks first this round + 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} + + # Player attacks second + if self.character.is_immobilized(): + self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]") + else: + result = self._player_attack(monster) + if result["hit"]: + monster.hp = max(0, monster.hp - result["damage"]) + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]") + 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 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} ──[/]") @@ -679,6 +745,15 @@ class GameEngine: 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} + # If HP too low to fight, rest instead + if self.character.hp < self.character.max_hp * 0.15 and not self.is_resting: + 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]🏕️ Zu schwach zum Kämpfen — Rast am {self.rest_location}...[/]") + return {"events": events, "resting": True} + # === Travel phase === if self.is_traveling: self.travel_progress += random.uniform(0.15, 0.35)