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:
+108
-33
@@ -54,7 +54,7 @@ class DnDMonster:
|
|||||||
self.challenge_rating = max(0.25, level / 4)
|
self.challenge_rating = max(0.25, level / 4)
|
||||||
|
|
||||||
# Stats basierend auf Challenge Rating — balanced
|
# 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.hp = max(1, int((level * 6) + random.randint(0, 8)))
|
||||||
self.max_hp = self.hp
|
self.max_hp = self.hp
|
||||||
|
|
||||||
@@ -311,8 +311,48 @@ class GameEngine:
|
|||||||
return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False}
|
return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False}
|
||||||
|
|
||||||
def _combat(self, monster: DnDMonster) -> dict:
|
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
|
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}) ━━━[/]")
|
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:
|
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![/]")
|
self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]")
|
||||||
return {"result": "death", "rounds": rounds, "monster": monster.name}
|
return {"result": "death", "rounds": rounds, "monster": monster.name}
|
||||||
|
|
||||||
# Monster attacks FIRST (initiative disadvantage for player)
|
if player_first:
|
||||||
if monster.is_alive:
|
# Player attacks first this round
|
||||||
m_result = monster.attack(self.character.effective_ac, self.dice_log.entries)
|
if self.character.is_immobilized():
|
||||||
self.dice_log.entries = self.dice_log.entries
|
self.dice_log.add(f"[yellow]💫 Du bist gelähmt/betäubt — kannst nicht angreifen![/]")
|
||||||
if m_result["hit"]:
|
else:
|
||||||
self.character.hp = max(0, self.character.hp - m_result["damage"])
|
# Caster tries spell first (50% chance, round 1 only)
|
||||||
if m_result.get("effect"):
|
if rounds == 1 and self.character.max_mp > 0 and random.random() < 0.5:
|
||||||
eff = m_result["effect"]
|
if self._cast_spell_if_possible(monster):
|
||||||
self.character.add_effect(eff["type"], eff["duration"], eff["power"], eff["name"])
|
if not monster.is_alive:
|
||||||
self._add_log(f"[yellow]⚠️ {eff['name']} erhalten! ({eff['duration']} Runden)[/]")
|
self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]")
|
||||||
if not self.character.is_alive:
|
break
|
||||||
self.dice_log.add(f"[bold red]💀 Du wurdest besiegt![/]")
|
result = self._player_attack(monster)
|
||||||
return {"result": "death", "rounds": rounds, "monster": monster.name}
|
if result["hit"]:
|
||||||
|
monster.hp = max(0, monster.hp - result["damage"])
|
||||||
# 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 not monster.is_alive:
|
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
|
break
|
||||||
|
|
||||||
# Player attacks
|
# Monster attacks second
|
||||||
result = self._player_attack(monster)
|
if monster.is_alive:
|
||||||
if result["hit"]:
|
m_result = monster.attack(self.character.effective_ac, self.dice_log.entries)
|
||||||
monster.hp = max(0, monster.hp - result["damage"])
|
self.dice_log.entries = self.dice_log.entries
|
||||||
if not monster.is_alive:
|
if m_result["hit"]:
|
||||||
self.dice_log.add(f"[bold green]☠️ {monster.name} besiegt![/]")
|
self.character.hp = max(0, self.character.hp - m_result["damage"])
|
||||||
# Check if player also died from monster's last hit
|
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:
|
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}
|
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} ──[/]")
|
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[/]")
|
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}
|
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 ===
|
# === 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user