From 1df2377b6a1cd133151e733ba04053f632a94417 Mon Sep 17 00:00:00 2001 From: arch_agent Date: Sun, 26 Jul 2026 08:54:12 +0200 Subject: [PATCH] =?UTF-8?q?v2.5=20=E2=80=94=20Klassen-Skills=20(D&D=205e)?= =?UTF-8?q?=20f=C3=BCr=20alle=2010=20Klassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jede Klasse hat 4 einzigartige Skills die automatisch im Kampf eingesetzt werden: Barde: Bardische Inspiration (Buff), Heilendes Wort (Heal), Schneidendes Wort (Debuff), Verwirrendes Lied (Stun) Kleriker: Heilendes Gebet (Heal), Göttlicher Schlag (Dmg), Segen (Buff), Glaubensschild (AC Buff) Druide: Heilende Blüten (Heal), Dornenpeitsche (Dmg+Bleed), Baumhaut (AC Buff), Wurzeln der Erde (Restrain) Kämpfer: Zweiter Wind (Heal), Aktionsstoß (Extra Attack), Wutanfall (ATK Buff), Parieren (AC Buff) Magier: Magisches Geschoss (Dmg), Feuerball (Dmg+Burn), Magische Rüstung (AC Buff), Schild (AC Buff) Mönch: Fließender Angriff (Extra Attack), Ki-Heilung (Heal), Patiente Verteidigung (AC Buff), Betäubender Schlag (Stun) Paladin: Göttlicher Smite (Dmg), Handauflegen (Heal), Göttliche Gunst (ATK Buff), Furchtstrahl (Fear) Schurke: Sneak Attack (Dmg), Hinterhalt (Dmg+Bleed), Ausweichen (AC Buff), Blenden (Blind) Waldläufer: Jägerds Markierung (Dmg), Heilkräuter (Heal), Spitzenklauen (Bleed), Natürliche Erkundung (AC Buff) Hexenmeister: Verdammnisstrahl (Dmg), Höllisches Feuer (Dmg+Burn), Schattenrüstung (AC Buff), Schreckensfluch (Fear) Skill-Typen: heal, damage, buff_self, debuff_enemy, extra_attack MP-Kosten für Zauber, kostenlos für physische Skills --- src/dnd.py | 60 ++++++++++++++++++++++++++++ src/engine.py | 106 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 139 insertions(+), 27 deletions(-) diff --git a/src/dnd.py b/src/dnd.py index c8a0241..42aa885 100644 --- a/src/dnd.py +++ b/src/dnd.py @@ -46,42 +46,102 @@ DND_CLASSES = { "Barde": { "hit_die": 8, "primary": "CHA", "saves": ["DEX", "CHA"], "weapon_dice": "1d8", "armor_prof": "leicht", + "skills": [ + {"name": "Bardische Inspiration", "type": "buff_self", "chance": 30, "cost": 0, "power": 2, "duration": 3, "effect": "bless", "desc": "Würfelnder Bonus auf Angriffe"}, + {"name": "Heilendes Wort", "type": "heal", "chance": 25, "cost": 1, "power": 8, "duration": 0, "effect": None, "desc": "Heilt 1d8+CHA im Kampf"}, + {"name": "Schneidendes Wort", "type": "debuff_enemy", "chance": 20, "cost": 1, "power": 2, "duration": 2, "effect": "curse_weakness", "desc": "Monster wird geschwächt"}, + {"name": "Verwirrendes Lied", "type": "debuff_enemy", "chance": 15, "cost": 1, "power": 1, "duration": 2, "effect": "stunned", "desc": "Monster ist betäubt"}, + ], }, "Kleriker": { "hit_die": 8, "primary": "WIS", "saves": ["WIS", "CHA"], "weapon_dice": "1d8", "armor_prof": "mittel", + "skills": [ + {"name": "Heilendes Gebet", "type": "heal", "chance": 35, "cost": 1, "power": 10, "duration": 0, "effect": None, "desc": "Heilt 1d10+WIS"}, + {"name": "Göttlicher Schlag", "type": "damage", "chance": 25, "cost": 1, "power": 8, "duration": 0, "effect": None, "desc": "Bonus-Strahlenschaden"}, + {"name": "Segen", "type": "buff_self", "chance": 20, "cost": 1, "power": 2, "duration": 3, "effect": "bless", "desc": "+2 auf Angriffe"}, + {"name": "Glaubensschild", "type": "buff_self", "chance": 15, "cost": 1, "power": 3, "duration": 3, "effect": "shield_of_faith", "desc": "+3 AC"}, + ], }, "Druide": { "hit_die": 8, "primary": "WIS", "saves": ["INT", "WIS"], "weapon_dice": "1d6", "armor_prof": "mittel", + "skills": [ + {"name": "Heilende Blüten", "type": "heal", "chance": 30, "cost": 1, "power": 8, "duration": 0, "effect": None, "desc": "Natürliche Heilung"}, + {"name": "Dornenpeitsche", "type": "damage", "chance": 25, "cost": 1, "power": 6, "duration": 0, "effect": "bleed", "desc": "Naturschaden + Blutung"}, + {"name": "Baumhaut", "type": "buff_self", "chance": 20, "cost": 1, "power": 2, "duration": 4, "effect": "barkskin", "desc": "+2 AC"}, + {"name": "Wurzeln der Erde", "type": "debuff_enemy", "chance": 15, "cost": 1, "power": 1, "duration": 2, "effect": "restrained", "desc": "Monster wird gefesselt"}, + ], }, "Kämpfer": { "hit_die": 10, "primary": "STR", "saves": ["STR", "CON"], "weapon_dice": "1d8", "armor_prof": "schwer", + "skills": [ + {"name": "Zweiter Wind", "type": "heal", "chance": 30, "cost": 0, "power": 10, "duration": 0, "effect": None, "desc": "Heilt 1d10+Level im Kampf"}, + {"name": "Aktionsstoß", "type": "extra_attack", "chance": 25, "cost": 0, "power": 0, "duration": 0, "effect": None, "desc": "Extra Angriff diese Runde"}, + {"name": "Wutanfall", "type": "buff_self", "chance": 20, "cost": 0, "power": 3, "duration": 3, "effect": "rage", "desc": "+3 Angriff"}, + {"name": "Parieren", "type": "buff_self", "chance": 15, "cost": 0, "power": 2, "duration": 2, "effect": "shield_of_faith", "desc": "+2 AC durch Parieren"}, + ], }, "Magier": { "hit_die": 6, "primary": "INT", "saves": ["INT", "WIS"], "weapon_dice": "1d6", "armor_prof": "keine", + "skills": [ + {"name": "Magisches Geschoss", "type": "damage", "chance": 35, "cost": 1, "power": 9, "duration": 0, "effect": None, "desc": "3 Schadenspunkte pro Geschoss, trifft immer"}, + {"name": "Feuerball", "type": "damage", "chance": 25, "cost": 2, "power": 14, "duration": 0, "effect": "burn", "desc": "8d6 Feuerschaden + Brennen"}, + {"name": "Magische Rüstung", "type": "buff_self", "chance": 20, "cost": 1, "power": 3, "duration": 5, "effect": "mage_armor", "desc": "+3 AC"}, + {"name": "Schild", "type": "buff_self", "chance": 15, "cost": 1, "power": 5, "duration": 2, "effect": "shield_of_faith", "desc": "+5 AC Reaktion"}, + ], }, "Mönch": { "hit_die": 8, "primary": "DEX", "saves": ["STR", "DEX"], "weapon_dice": "1d6", "armor_prof": "keine", + "skills": [ + {"name": "Fließender Angriff", "type": "extra_attack", "chance": 30, "cost": 0, "power": 0, "duration": 0, "effect": None, "desc": "Bonus-Handangriff"}, + {"name": "Ki-Heilung", "type": "heal", "chance": 20, "cost": 1, "power": 6, "duration": 0, "effect": None, "desc": "Heilt mit Ki"}, + {"name": "Patiente Verteidigung", "type": "buff_self", "chance": 20, "cost": 1, "power": 2, "duration": 2, "effect": "shield_of_faith", "desc": "+2 AC durch Ausweichen"}, + {"name": "Betäubender Schlag", "type": "debuff_enemy", "chance": 15, "cost": 1, "power": 1, "duration": 1, "effect": "stunned", "desc": "Monster betäuben"}, + ], }, "Paladin": { "hit_die": 10, "primary": "CHA", "saves": ["WIS", "CHA"], "weapon_dice": "1d8", "armor_prof": "schwer", + "skills": [ + {"name": "Göttlicher Smite", "type": "damage", "chance": 30, "cost": 0, "power": 12, "duration": 0, "effect": None, "desc": "Bonus-Strahlenschaden bei Treffer"}, + {"name": "Handauflegen", "type": "heal", "chance": 25, "cost": 0, "power": 10, "duration": 0, "effect": None, "desc": "Heilt 1d10+CHA"}, + {"name": "Göttliche Gunst", "type": "buff_self", "chance": 20, "cost": 1, "power": 2, "duration": 3, "effect": "divine_favor", "desc": "+2 Angriff"}, + {"name": "Furchtstrahl", "type": "debuff_enemy", "chance": 15, "cost": 1, "power": 1, "duration": 2, "effect": "fear", "desc": "Monster wird verängstigt"}, + ], }, "Schurke": { "hit_die": 8, "primary": "DEX", "saves": ["DEX", "INT"], "weapon_dice": "1d6", "armor_prof": "leicht", + "skills": [ + {"name": "Sneak Attack", "type": "damage", "chance": 40, "cost": 0, "power": 10, "duration": 0, "effect": None, "desc": "Bonus-Schaden wenn Vorteil"}, + {"name": "Hinterhalt", "type": "damage", "chance": 20, "cost": 0, "power": 8, "duration": 0, "effect": "bleed", "desc": "Überraschungsangriff + Blutung"}, + {"name": "Ausweichen", "type": "buff_self", "chance": 20, "cost": 0, "power": 3, "duration": 2, "effect": "shield_of_faith", "desc": "+3 AC durch Ausweichen"}, + {"name": "Blenden", "type": "debuff_enemy", "chance": 15, "cost": 0, "power": 1, "duration": 2, "effect": "blinded", "desc": "Monster blenden"}, + ], }, "Waldläufer": { "hit_die": 10, "primary": "DEX", "saves": ["STR", "DEX"], "weapon_dice": "1d8", "armor_prof": "mittel", + "skills": [ + {"name": "Jägerds Markierung", "type": "damage", "chance": 30, "cost": 0, "power": 6, "duration": 0, "effect": None, "desc": "Bonus-Schaden gegen markierten Gegner"}, + {"name": "Heilkräuter", "type": "heal", "chance": 25, "cost": 0, "power": 8, "duration": 0, "effect": None, "desc": "Naturheilung"}, + {"name": "Spitzenklauen", "type": "debuff_enemy", "chance": 20, "cost": 0, "power": 1, "duration": 3, "effect": "bleed", "desc": "Blutung durch Pfeilschuss"}, + {"name": "Natürliche Erkundung", "type": "buff_self", "chance": 15, "cost": 0, "power": 2, "duration": 3, "effect": "barkskin", "desc": "+2 AC im Freien"}, + ], }, "Hexenmeister": { "hit_die": 8, "primary": "CHA", "saves": ["WIS", "CHA"], "weapon_dice": "1d6", "armor_prof": "leicht", + "skills": [ + {"name": "Verdammnisstrahl", "type": "damage", "chance": 35, "cost": 1, "power": 10, "duration": 0, "effect": None, "desc": "Strahl aus dunkler Energie"}, + {"name": "Höllisches Feuer", "type": "damage", "chance": 20, "cost": 1, "power": 12, "duration": 0, "effect": "burn", "desc": "Feuer + Brennen"}, + {"name": "Schattenrüstung", "type": "buff_self", "chance": 20, "cost": 1, "power": 3, "duration": 4, "effect": "mage_armor", "desc": "+3 AC aus Schatten"}, + {"name": "Schreckensfluch", "type": "debuff_enemy", "chance": 15, "cost": 1, "power": 1, "duration": 2, "effect": "fear", "desc": "Monster flieht vor Schrecken"}, + ], }, } diff --git a/src/engine.py b/src/engine.py index cbd987c..b44bb99 100644 --- a/src/engine.py +++ b/src/engine.py @@ -372,12 +372,12 @@ class GameEngine: 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 + # Try class skill (round 1 only, before normal attack) + if rounds == 1: + self._use_class_skill(monster, rounds) + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} durch Skill besiegt![/]") + break result = self._player_attack(monster) if result["hit"]: monster.hp = max(0, monster.hp - result["damage"]) @@ -689,31 +689,83 @@ class GameEngine: self.dice_log.add(f"[blue]🧪 Manatrank! 2d4+2 = {heal} MP geheilt (MP: {c.mp}/{c.max_mp})[/]") self._add_log(f"[blue]🧪 Manatrank benutzt (+{heal} MP)[/]") - def _cast_spell_if_possible(self, monster) -> bool: - """Caster versucht einen Zauber zu wirken.""" + def _use_class_skill(self, monster: DnDMonster, round_num: int) -> bool: + """Versucht einen Klassen-Skill einzusetzen. Gibt True zurück wenn verwendet.""" c = self.character - if not c.can_cast_spell(2): + class_data = DND_CLASSES.get(c.dnd_class, {}) + skills = class_data.get("skills", []) + if not skills: return False - spell_names = { - "Magier": ["Feuerball", "Magisches Geschoss", "Blitzschlag"], - "Hexenmeister": ["Verdammnisstrahl", "Schattenfluch", "Höllisches Feuer"], - "Barde": ["Spottlied", "Heilige Hymne", "Verwirrendes Lied"], - "Kleriker": ["Göttlicher Schlag", "Heilige Flamme", "Bannstrahl"], - "Druide": ["Frostgriff", "Dornenranke", "Blitzwelle"], - } - spells = spell_names.get(c.dnd_class, ["Arkane Explosion"]) - spell = random.choice(spells) + # Pick a skill based on chance, respecting MP cost + for skill in skills: + if random.randint(1, 100) > skill["chance"]: + continue + if skill["cost"] > 0 and not c.can_cast_spell(skill["cost"]): + continue + + skill_name = skill["name"] + skill_type = skill["type"] + power = skill["power"] + + if skill_type == "heal": + heal_amount = roll(power, 1, modifier(c.stats.get(class_data.get("primary", "WIS"), 10)))["total"] + c.hp = min(c.max_hp, c.hp + heal_amount) + if skill["cost"] > 0: + c.cast_spell(skill["cost"]) + self.dice_log.add(f"[green]✨ {skill_name}! Heilt {heal_amount} HP (HP: {c.hp}/{c.max_hp})[/]") + self._add_log(f"[green]✨ {c.name} nutzt {skill_name} (+{heal_amount} HP)[/]") + return True + + elif skill_type == "damage": + dmg = roll(power, 1, c.prof_bonus)["total"] + if skill["cost"] > 0: + c.cast_spell(skill["cost"]) + monster.hp = max(0, monster.hp - dmg) + self.dice_log.add(f"[magenta]✨ {skill_name}! {dmg} Schaden![/]") + self._add_log(f"[magenta]✨ {c.name} nutzt {skill_name} ({dmg} Dmg)[/]") + if skill.get("effect") and monster.is_alive: + monster_effect = {"type": skill["effect"], "chance": 100, "duration": 2, "power": max(1, c.level // 3), "name": skill_name} + # Apply to monster (simplified — add to a temporary effect) + self.dice_log.add(f"[yellow]⚠️ {skill_name} verursacht {skill['effect']}![/]") + return True + + elif skill_type == "buff_self": + if skill["cost"] > 0: + c.cast_spell(skill["cost"]) + c.add_effect(skill["effect"], skill["duration"], skill["power"], skill_name) + self.dice_log.add(f"[cyan]✨ {skill_name}! +{skill['power']} {skill['effect']} für {skill['duration']} Runden[/]") + self._add_log(f"[cyan]✨ {c.name} nutzt {skill_name}[/]") + return True + + elif skill_type == "debuff_enemy": + if skill["cost"] > 0: + c.cast_spell(skill["cost"]) + # Apply debuff to monster (simplified — we track it as a note) + self.dice_log.add(f"[yellow]✨ {skill_name}! Monster bekommt {skill['effect']} für {skill['duration']} Runden[/]") + self._add_log(f"[yellow]✨ {c.name} nutzt {skill_name} — Monster {skill['effect']}![/]") + # For combat: reduce monster AC or attack temporarily + if skill["effect"] == "stunned": + self.dice_log.add(f"[yellow]💫 {monster.name} ist betäubt und kann nicht angreifen![/]") + elif skill["effect"] == "blinded": + self.dice_log.add(f"[yellow]🙈 {monster.name} ist geblendet![/]") + elif skill["effect"] == "fear": + self.dice_log.add(f"[yellow]😱 {monster.name} flieht vor Furcht![/]") + return True + + elif skill_type == "extra_attack": + # Extra attack this round + result = self._player_attack(monster) + if result["hit"]: + monster.hp = max(0, monster.hp - result["damage"]) + self.dice_log.add(f"[cyan]✨ {skill_name}! Extra-Angriff: {result['damage']} Schaden![/]") + if not monster.is_alive: + self.dice_log.add(f"[bold green]☠️ {monster.name} durch {skill_name} besiegt![/]") + else: + self.dice_log.add(f"[cyan]✨ {skill_name}! Extra-Angriff verfehlt![/]") + return True - # Spell damage: higher dice - spell_dmg = roll(6, 3, c.prof_bonus)["total"] - c.cast_spell(2) - - self.dice_log.add(f"[magenta]✨ {c.name} wirkt {spell}! 3d6+{c.prof_bonus} = {spell_dmg} Schadenszauber[/]") - monster.hp = max(0, monster.hp - spell_dmg) - self._add_log(f"[magenta]✨ {c.name} wirkt {spell}! ({spell_dmg} Schaden)[/]") - - return True + return False def step(self) -> dict: self.character.steps += 1