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:
+88
-13
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user