v2.4 — Echte D&D 5e Initiative + Überraschungsangriffe

Initiative-System:
- d20 + DEX-Modifier für Spieler und Monster
- Höherer Wurf greift zuerst an (pro Kampf, nicht pro Runde)
- Wurf wird im Würfel-Log angezeigt

Überraschungsangriffe (Surprise):
- 8% Chance: Monster überrascht Spieler → freie Attacke
- 8% Chance: Spieler überrascht Monster → freie Attacke
- Surprise-Attacke kann tödlich sein (0 Runden Kampf)
- Log: 😱 'Monster überfällt dich!' / 🗡️ 'Du überraschst Monster'

Fix: Keine Endlosschleife bei Respawn mit niedrigem HP
- Wenn HP < 15% → Rast statt Kampf
- Monster-AC min. 10 (war manchmal 8)
This commit is contained in:
arch_agent
2026-07-25 23:39:40 +02:00
parent d24f0fd950
commit aa73891245
+88 -13
View File
@@ -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,7 +367,42 @@ 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 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} 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
# 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]💀 Du wurdest besiegt![/]")
return {"result": "death", "rounds": rounds, "monster": monster.name}
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
@@ -341,24 +416,15 @@ class GameEngine:
self.dice_log.add(f"[bold red]💀 Du wurdest besiegt![/]")
return {"result": "death", "rounds": rounds, "monster": monster.name}
# Check immobilization
# Player attacks second
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 not monster.is_alive:
self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]")
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
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}
@@ -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)