v2.1 — Shop, Tränke, Spells, Weg-Progress
Shop-System (klassenspezifisch):
- Waffen-Upgrades: Langschwert, Magierstab, Dolch etc. pro Klasse
- Rüstung-Upgrades: Kettenhemd, Magierrobe etc.
- Schilde für Tanks (Kämpfer/Paladin)
- Heiltränke (2d4+2), Große Heiltränke (4d4+4) ab Stufe 5
- Manatränke für Caster
- 10% Gold-Reserve wird immer behalten
Trank-System:
- Auto-Benutzung bei HP < 40% (Heiltrank) / HP < 20% (Großer)
- Manatrank bei MP < 30% für Caster
- Würfel-Log zeigt jeden Trankwurf
Zauber-System:
- Caster wirken Spells in Runde 1 (50% Chance)
- Feuerball, Verdammnisstrahl, Göttlicher Schlag etc.
- Kosten 2 MP + 1 Spell Slot
- 3d6 + Proficiency Bonus Schaden
Weg-Progress:
- Travel-Phasen zwischen Quests mit Progress-Balken
- 🚶 [████░░░░] Zielort im Bottom-Panel
- Shop-Besuch bei Ankunft
This commit is contained in:
+61
@@ -140,6 +140,14 @@ class DnDCharacter:
|
|||||||
self.inventory = []
|
self.inventory = []
|
||||||
self.gold = roll(6, 3)["total"] * 10 # Starting gold 3d6 * 10
|
self.gold = roll(6, 3)["total"] * 10 # Starting gold 3d6 * 10
|
||||||
|
|
||||||
|
# Potions
|
||||||
|
self.potions = {"healing": 2, "mana": 0, "greater_healing": 0, "strength": 0}
|
||||||
|
self.potion_threshold = 0.4 # Use healing potion when HP < 40% of max
|
||||||
|
|
||||||
|
# MP / Spell points
|
||||||
|
self.max_mp = self._calc_max_mp()
|
||||||
|
self.mp = self.max_mp
|
||||||
|
|
||||||
# Stats tracking
|
# Stats tracking
|
||||||
self.kills = 0
|
self.kills = 0
|
||||||
self.deaths = 0
|
self.deaths = 0
|
||||||
@@ -148,6 +156,9 @@ class DnDCharacter:
|
|||||||
self.crit_hits = 0
|
self.crit_hits = 0
|
||||||
self.crit_misses = 0
|
self.crit_misses = 0
|
||||||
self.nat_20s = 0
|
self.nat_20s = 0
|
||||||
|
self.potions_used = 0
|
||||||
|
self.gold_spent = 0
|
||||||
|
self.items_bought = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ac(self) -> int:
|
def ac(self) -> int:
|
||||||
@@ -161,6 +172,47 @@ class DnDCharacter:
|
|||||||
return max(0, self.level) # simplified: 1 slot per level
|
return max(0, self.level) # simplified: 1 slot per level
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def _calc_max_mp(self) -> int:
|
||||||
|
"""MP für Zauber — basierend auf Klasse und INT/WIS/CHA."""
|
||||||
|
if self.dnd_class in ("Magier", "Hexenmeister", "Barde", "Kleriker", "Druide"):
|
||||||
|
primary = DND_CLASSES.get(self.dnd_class, {}).get("primary", "INT")
|
||||||
|
return 10 + self.level * 2 + modifier(self.stats.get(primary, 10)) * 2
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def use_potion(self, potion_type: str) -> int:
|
||||||
|
"""Benutzt einen Trank. Gibt geheilte Menge zurück, 0 wenn keine da."""
|
||||||
|
if self.potions.get(potion_type, 0) <= 0:
|
||||||
|
return 0
|
||||||
|
self.potions[potion_type] -= 1
|
||||||
|
self.potions_used += 1
|
||||||
|
|
||||||
|
if potion_type == "healing":
|
||||||
|
heal = roll(4, 2, 2)["total"] # 2d4+2
|
||||||
|
self.hp = min(self.max_hp, self.hp + heal)
|
||||||
|
return heal
|
||||||
|
elif potion_type == "greater_healing":
|
||||||
|
heal = roll(8, 4, 4)["total"] # 4d4+4
|
||||||
|
self.hp = min(self.max_hp, self.hp + heal)
|
||||||
|
return heal
|
||||||
|
elif potion_type == "mana":
|
||||||
|
heal = roll(4, 2, 2)["total"]
|
||||||
|
self.mp = min(self.max_mp, self.mp + heal)
|
||||||
|
return heal
|
||||||
|
elif potion_type == "strength":
|
||||||
|
# Temporary buff: +2 to next attack (simplified — just return 0)
|
||||||
|
return 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def needs_healing(self) -> bool:
|
||||||
|
return self.hp < self.max_hp * self.potion_threshold
|
||||||
|
|
||||||
|
def can_cast_spell(self, cost: int = 1) -> bool:
|
||||||
|
return self.mp >= cost and self.spell_slots > 0
|
||||||
|
|
||||||
|
def cast_spell(self, cost: int = 1):
|
||||||
|
self.mp -= cost
|
||||||
|
self.spell_slots -= 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack_bonus(self) -> int:
|
def attack_bonus(self) -> int:
|
||||||
"""Attack roll modifier: STR/DEX mod + proficiency."""
|
"""Attack roll modifier: STR/DEX mod + proficiency."""
|
||||||
@@ -211,6 +263,8 @@ class DnDCharacter:
|
|||||||
self.hp = self.max_hp
|
self.hp = self.max_hp
|
||||||
self.prof_bonus = self._calc_prof_bonus()
|
self.prof_bonus = self._calc_prof_bonus()
|
||||||
self.spell_slots = self._calc_spell_slots()
|
self.spell_slots = self._calc_spell_slots()
|
||||||
|
self.max_mp = self._calc_max_mp()
|
||||||
|
self.mp = self.max_mp
|
||||||
# ASI: +2 to one stat every 4 levels
|
# ASI: +2 to one stat every 4 levels
|
||||||
if self.level % 4 == 0:
|
if self.level % 4 == 0:
|
||||||
best_stat = max(self.stats, key=lambda k: self.stats[k])
|
best_stat = max(self.stats, key=lambda k: self.stats[k])
|
||||||
@@ -238,13 +292,16 @@ class DnDCharacter:
|
|||||||
"hit_die": self.hit_die, "save_prof": self.save_prof,
|
"hit_die": self.hit_die, "save_prof": self.save_prof,
|
||||||
"weapon_dice": self.weapon_dice,
|
"weapon_dice": self.weapon_dice,
|
||||||
"max_hp": self.max_hp, "hp": self.hp,
|
"max_hp": self.max_hp, "hp": self.hp,
|
||||||
|
"max_mp": self.max_mp, "mp": self.mp,
|
||||||
"base_ac": self.base_ac, "armor_bonus": self.armor_bonus, "shield_bonus": self.shield_bonus,
|
"base_ac": self.base_ac, "armor_bonus": self.armor_bonus, "shield_bonus": self.shield_bonus,
|
||||||
"prof_bonus": self.prof_bonus, "spell_slots": self.spell_slots,
|
"prof_bonus": self.prof_bonus, "spell_slots": self.spell_slots,
|
||||||
"equipped": {k: v.to_dict() if v else None for k, v in self.equipped.items()},
|
"equipped": {k: v.to_dict() if v else None for k, v in self.equipped.items()},
|
||||||
"inventory": [i.to_dict() for i in self.inventory],
|
"inventory": [i.to_dict() for i in self.inventory],
|
||||||
"gold": self.gold,
|
"gold": self.gold,
|
||||||
|
"potions": self.potions, "potion_threshold": self.potion_threshold,
|
||||||
"kills": self.kills, "deaths": self.deaths, "quests_done": self.quests_done,
|
"kills": self.kills, "deaths": self.deaths, "quests_done": self.quests_done,
|
||||||
"steps": self.steps, "crit_hits": self.crit_hits, "crit_misses": self.crit_misses, "nat_20s": self.nat_20s,
|
"steps": self.steps, "crit_hits": self.crit_hits, "crit_misses": self.crit_misses, "nat_20s": self.nat_20s,
|
||||||
|
"potions_used": self.potions_used, "gold_spent": self.gold_spent, "items_bought": self.items_bought,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -255,13 +312,17 @@ class DnDCharacter:
|
|||||||
c.hit_die = d["hit_die"]; c.save_prof = d["save_prof"]
|
c.hit_die = d["hit_die"]; c.save_prof = d["save_prof"]
|
||||||
c.weapon_dice = d["weapon_dice"]
|
c.weapon_dice = d["weapon_dice"]
|
||||||
c.max_hp = d["max_hp"]; c.hp = d["hp"]
|
c.max_hp = d["max_hp"]; c.hp = d["hp"]
|
||||||
|
c.max_mp = d.get("max_mp", 0); c.mp = d.get("mp", 0)
|
||||||
c.base_ac = d["base_ac"]; c.armor_bonus = d["armor_bonus"]; c.shield_bonus = d["shield_bonus"]
|
c.base_ac = d["base_ac"]; c.armor_bonus = d["armor_bonus"]; c.shield_bonus = d["shield_bonus"]
|
||||||
c.prof_bonus = d["prof_bonus"]; c.spell_slots = d["spell_slots"]
|
c.prof_bonus = d["prof_bonus"]; c.spell_slots = d["spell_slots"]
|
||||||
c.equipped = {k: Item.from_dict(v) if v else None for k, v in d.get("equipped", {}).items()}
|
c.equipped = {k: Item.from_dict(v) if v else None for k, v in d.get("equipped", {}).items()}
|
||||||
c.inventory = [Item.from_dict(i) for i in d.get("inventory", [])]
|
c.inventory = [Item.from_dict(i) for i in d.get("inventory", [])]
|
||||||
c.gold = d["gold"]
|
c.gold = d["gold"]
|
||||||
|
c.potions = d.get("potions", {"healing": 2, "mana": 0, "greater_healing": 0, "strength": 0})
|
||||||
|
c.potion_threshold = d.get("potion_threshold", 0.4)
|
||||||
c.kills = d["kills"]; c.deaths = d["deaths"]; c.quests_done = d["quests_done"]
|
c.kills = d["kills"]; c.deaths = d["deaths"]; c.quests_done = d["quests_done"]
|
||||||
c.steps = d["steps"]; c.crit_hits = d["crit_hits"]; c.crit_misses = d["crit_misses"]; c.nat_20s = d["nat_20s"]
|
c.steps = d["steps"]; c.crit_hits = d["crit_hits"]; c.crit_misses = d["crit_misses"]; c.nat_20s = d["nat_20s"]
|
||||||
|
c.potions_used = d.get("potions_used", 0); c.gold_spent = d.get("gold_spent", 0); c.items_bought = d.get("items_bought", 0)
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+210
-11
@@ -145,11 +145,15 @@ class GameEngine:
|
|||||||
self.quest_progress = 0
|
self.quest_progress = 0
|
||||||
self.quest_target = random.randint(3, 8)
|
self.quest_target = random.randint(3, 8)
|
||||||
|
|
||||||
|
# Travel progress
|
||||||
|
self.travel_progress = 0.0 # 0.0 to 1.0
|
||||||
|
self.is_traveling = False
|
||||||
|
self.travel_destination = ""
|
||||||
|
|
||||||
self._fill_cache()
|
self._fill_cache()
|
||||||
|
|
||||||
def _fill_cache(self):
|
def _fill_cache(self):
|
||||||
"""Füllt Cache initial."""
|
pass
|
||||||
pass # LLM wird asynchron in refill_async geladen
|
|
||||||
|
|
||||||
def _add_log(self, msg: str):
|
def _add_log(self, msg: str):
|
||||||
self.event_log.insert(0, msg)
|
self.event_log.insert(0, msg)
|
||||||
@@ -251,6 +255,13 @@ class GameEngine:
|
|||||||
while monster.is_alive and self.character.is_alive:
|
while monster.is_alive and self.character.is_alive:
|
||||||
rounds += 1
|
rounds += 1
|
||||||
|
|
||||||
|
# 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 (initiative: DEX check)
|
# Player attacks (initiative: DEX check)
|
||||||
result = self._player_attack(monster)
|
result = self._player_attack(monster)
|
||||||
if result["hit"]:
|
if result["hit"]:
|
||||||
@@ -288,16 +299,17 @@ class GameEngine:
|
|||||||
self._add_log(f"[bold yellow]⬆️ Stufe {self.character.level} erreicht![/]")
|
self._add_log(f"[bold yellow]⬆️ Stufe {self.character.level} erreicht![/]")
|
||||||
self.dice_log.add(f"[bold yellow]⬆️ LEVEL UP! Stufe {self.character.level}[/]")
|
self.dice_log.add(f"[bold yellow]⬆️ LEVEL UP! Stufe {self.character.level}[/]")
|
||||||
self.character.heal()
|
self.character.heal()
|
||||||
|
self.character.mp = self.character.max_mp
|
||||||
|
|
||||||
self._add_log(f"[blue]🌍 Du machst dich auf den Weg...[/]")
|
# Start travel to new location
|
||||||
self.current_location = random.choice(FALLBACK_LOCATIONS)
|
self.is_traveling = True
|
||||||
self._add_log(f"[blue]🌍 Ankunft in: {self.current_location}[/]")
|
self.travel_progress = 0.0
|
||||||
|
self.travel_destination = random.choice(FALLBACK_LOCATIONS)
|
||||||
|
self._add_log(f"[blue]🌍 Du machst dich auf den Weg nach {self.travel_destination}...[/]")
|
||||||
|
|
||||||
self.current_quest = random.choice(FALLBACK_QUESTS)
|
self.current_quest = random.choice(FALLBACK_QUESTS)
|
||||||
self.quest_progress = 0
|
self.quest_progress = 0
|
||||||
self.quest_target = random.randint(3, 8)
|
self.quest_target = random.randint(3, 8)
|
||||||
self._add_log(f"[cyan]📜 Neue Quest: {self.current_quest}[/]")
|
|
||||||
self._add_log(f"[dim]Ziel: Besiege {self.quest_target} Gegner[/]")
|
|
||||||
|
|
||||||
def _auto_equip(self, item: Item):
|
def _auto_equip(self, item: Item):
|
||||||
slot = None
|
slot = None
|
||||||
@@ -334,22 +346,203 @@ class GameEngine:
|
|||||||
if sold > 0:
|
if sold > 0:
|
||||||
self._add_loot_log(f"[dim]💰 {sold} Schrott verkauft (+{total_gold}G)[/]")
|
self._add_loot_log(f"[dim]💰 {sold} Schrott verkauft (+{total_gold}G)[/]")
|
||||||
|
|
||||||
|
def _visit_shop(self):
|
||||||
|
"""Charakter kauft Ausrüstung und Tränke im Shop (klassenspezifisch)."""
|
||||||
|
c = self.character
|
||||||
|
bought_something = False
|
||||||
|
|
||||||
|
is_caster = c.dnd_class in ("Magier", "Hexenmeister", "Barde", "Kleriker", "Druide")
|
||||||
|
is_tank = c.dnd_class in ("Kämpfer", "Paladin")
|
||||||
|
|
||||||
|
# 1. Buy better weapon first (most impact)
|
||||||
|
weapon_price = 30 + c.level * 15
|
||||||
|
current_weapon = c.equipped.get("weapon")
|
||||||
|
desired_power = c.level + 2
|
||||||
|
if c.gold >= weapon_price and (not current_weapon or current_weapon.power < desired_power):
|
||||||
|
weapon_names = {
|
||||||
|
"Kämpfer": ["Langschwert", "Kampfaxt", "Streitkolben"],
|
||||||
|
"Paladin": ["Langschwert", "Heilige Klinge", "Göttlicher Hammer"],
|
||||||
|
"Schurke": ["Dolch", "Kurzschwert", "Wurfmesser"],
|
||||||
|
"Mönch": ["Kampfstab", "Waffenlose Faust", "Nunchaku"],
|
||||||
|
"Magier": ["Magierstab", "Zauberfokus", "Kristallkugel"],
|
||||||
|
"Hexenmeister": ["Verdammnisstab", "Schattenfokus"],
|
||||||
|
"Barde": ["Rapier", "Laute der Macht", "Elfenklinge"],
|
||||||
|
"Kleriker": ["Streitkolben", "Heiliges Symbol", "Göttlicher Schild"],
|
||||||
|
"Druide": ["Eichenstab", "Dornenpeitsche", "Naturfokus"],
|
||||||
|
"Waldläufer": ["Langbogen", "Kurzschwert", "Jagdspeer"],
|
||||||
|
}
|
||||||
|
names = weapon_names.get(c.dnd_class, ["Langschwert"])
|
||||||
|
rarity = "selten" if c.level >= 5 else "gewöhnlich"
|
||||||
|
w_name = random.choice(names)
|
||||||
|
w = Item(w_name, "weapon", desired_power, weapon_price // 2, rarity)
|
||||||
|
c.gold -= weapon_price
|
||||||
|
c.gold_spent += weapon_price
|
||||||
|
c.items_bought += 1
|
||||||
|
if current_weapon:
|
||||||
|
c.inventory.append(current_weapon)
|
||||||
|
c.equipped["weapon"] = w
|
||||||
|
self._add_loot_log(f"[cyan]⚔️ Kaufe {w} (-{weapon_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
# 2. Buy armor
|
||||||
|
armor_price = 40 + c.level * 10
|
||||||
|
current_armor = c.equipped.get("armor")
|
||||||
|
desired_armor = c.level // 2 + 1
|
||||||
|
if c.gold >= armor_price and (not current_armor or current_armor.power < desired_armor):
|
||||||
|
armor_names = {
|
||||||
|
"Kämpfer": ["Kettenhemd", "Plattenpanzer", "Schwere Rüstung"],
|
||||||
|
"Paladin": ["Plattenpanzer", "Heilige Rüstung", "Göttlicher Harnisch"],
|
||||||
|
"Schurke": ["Lederrüstung", "Schattenmantel", "Wendiger Umhang"],
|
||||||
|
"Magier": ["Magierrobe", "Zauberergewand", "Arkanes Gewand"],
|
||||||
|
"default": ["Lederrüstung", "Kettenhemd", "Schuppengewand"],
|
||||||
|
}
|
||||||
|
names = armor_names.get(c.dnd_class, armor_names["default"])
|
||||||
|
a_name = random.choice(names)
|
||||||
|
a = Item(a_name, "armor", desired_armor, armor_price // 2, "gewöhnlich")
|
||||||
|
c.gold -= armor_price
|
||||||
|
c.gold_spent += armor_price
|
||||||
|
c.items_bought += 1
|
||||||
|
c.armor_bonus = desired_armor
|
||||||
|
if current_armor:
|
||||||
|
c.inventory.append(current_armor)
|
||||||
|
c.equipped["armor"] = a
|
||||||
|
self._add_loot_log(f"[cyan]🛡️ Kaufe {a} (-{armor_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
# 3. Buy shield for tanks
|
||||||
|
if is_tank and not c.equipped.get("shield"):
|
||||||
|
shield_price = 25
|
||||||
|
if c.gold >= shield_price:
|
||||||
|
s = Item("Eisenschild", "shield", 2, 12, "gewöhnlich")
|
||||||
|
c.gold -= shield_price
|
||||||
|
c.gold_spent += shield_price
|
||||||
|
c.items_bought += 1
|
||||||
|
c.shield_bonus = 2
|
||||||
|
c.equipped["shield"] = s
|
||||||
|
self._add_loot_log(f"[cyan]🛡️ Kaufe {s} (-{shield_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
# 4. Buy healing potions (keep 10% gold reserve)
|
||||||
|
heal_price = 50
|
||||||
|
max_healing = 5 if is_tank else 3
|
||||||
|
gold_reserve = c.gold * 0.1
|
||||||
|
while c.potions["healing"] < max_healing and c.gold - heal_price >= gold_reserve:
|
||||||
|
c.gold -= heal_price
|
||||||
|
c.potions["healing"] += 1
|
||||||
|
c.gold_spent += heal_price
|
||||||
|
c.items_bought += 1
|
||||||
|
self._add_loot_log(f"[green]🧪 Kaufe Heiltrank (-{heal_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
# 5. Greater healing potions at higher levels
|
||||||
|
if c.level >= 5:
|
||||||
|
greater_price = 150
|
||||||
|
while c.potions["greater_healing"] < 2 and c.gold - greater_price >= gold_reserve:
|
||||||
|
c.gold -= greater_price
|
||||||
|
c.potions["greater_healing"] += 1
|
||||||
|
c.gold_spent += greater_price
|
||||||
|
c.items_bought += 1
|
||||||
|
self._add_loot_log(f"[green]🧪 Kaufe Großen Heiltrank (-{greater_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
# 6. Mana potions for casters
|
||||||
|
if is_caster:
|
||||||
|
mana_price = 50
|
||||||
|
while c.potions["mana"] < 3 and c.gold - mana_price >= gold_reserve:
|
||||||
|
c.gold -= mana_price
|
||||||
|
c.potions["mana"] += 1
|
||||||
|
c.gold_spent += mana_price
|
||||||
|
c.items_bought += 1
|
||||||
|
self._add_loot_log(f"[blue]🧪 Kaufe Manatrank (-{mana_price}G)[/]")
|
||||||
|
bought_something = True
|
||||||
|
|
||||||
|
if bought_something:
|
||||||
|
self._add_log(f"[yellow]🏪 Shop besucht — Ausrüstung und Tränke gekauft[/]")
|
||||||
|
self.dice_log.add(f"[yellow]🏪 Shop: {c.items_bought} Items, Rest-Gold: {c.gold}G[/]")
|
||||||
|
|
||||||
|
def _use_potions_if_needed(self):
|
||||||
|
"""Benutzt automatisch Heiltränke wenn HP niedrig ist."""
|
||||||
|
c = self.character
|
||||||
|
|
||||||
|
# Use greater healing first if very low
|
||||||
|
if c.hp < c.max_hp * 0.2 and c.potions.get("greater_healing", 0) > 0:
|
||||||
|
heal = c.use_potion("greater_healing")
|
||||||
|
self.dice_log.add(f"[green]🧪 Großer Heiltrank! 4d4+4 = {heal} HP geheilt (HP: {c.hp}/{c.max_hp})[/]")
|
||||||
|
self._add_log(f"[green]🧪 Großen Heiltrank benutzt (+{heal} HP)[/]")
|
||||||
|
|
||||||
|
# Regular healing potion
|
||||||
|
elif c.needs_healing() and c.potions.get("healing", 0) > 0:
|
||||||
|
heal = c.use_potion("healing")
|
||||||
|
self.dice_log.add(f"[green]🧪 Heiltrank! 2d4+2 = {heal} HP geheilt (HP: {c.hp}/{c.max_hp})[/]")
|
||||||
|
self._add_log(f"[green]🧪 Heiltrank benutzt (+{heal} HP)[/]")
|
||||||
|
|
||||||
|
# Mana potion for casters
|
||||||
|
if c.max_mp > 0 and c.mp < c.max_mp * 0.3 and c.potions.get("mana", 0) > 0:
|
||||||
|
heal = c.use_potion("mana")
|
||||||
|
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."""
|
||||||
|
c = self.character
|
||||||
|
if not c.can_cast_spell(2):
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
def step(self) -> dict:
|
def step(self) -> dict:
|
||||||
self.character.steps += 1
|
self.character.steps += 1
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
# Random travel
|
# === Travel phase ===
|
||||||
if random.randint(1, 50) == 1:
|
if self.is_traveling:
|
||||||
|
self.travel_progress += random.uniform(0.15, 0.35)
|
||||||
|
if self.travel_progress >= 1.0:
|
||||||
|
self.is_traveling = False
|
||||||
|
self.current_location = self.travel_destination
|
||||||
|
self.travel_progress = 0.0
|
||||||
|
self._add_log(f"[blue]🌍 Ankunft in: {self.current_location}[/]")
|
||||||
|
self._add_log(f"[cyan]📜 Neue Quest: {self.current_quest}[/]")
|
||||||
|
self._add_log(f"[dim]Ziel: Besiege {self.quest_target} Gegner[/]")
|
||||||
|
# Visit shop on arrival
|
||||||
|
self._visit_shop()
|
||||||
|
else:
|
||||||
|
# Still traveling, no combat this step
|
||||||
|
return {"events": events, "traveling": True}
|
||||||
|
|
||||||
|
# === Random travel between fights ===
|
||||||
|
if random.randint(1, 50) == 1 and not self.is_traveling:
|
||||||
self.current_location = random.choice(FALLBACK_LOCATIONS)
|
self.current_location = random.choice(FALLBACK_LOCATIONS)
|
||||||
events.append(("travel", f"[blue]🌍 Du reist nach: {self.current_location}[/]"))
|
events.append(("travel", f"[blue]🌍 Du reist nach: {self.current_location}[/]"))
|
||||||
self._add_log(events[-1][1])
|
self._add_log(events[-1][1])
|
||||||
|
|
||||||
# Spawn monster
|
# === Use potions if needed (before fight) ===
|
||||||
|
self._use_potions_if_needed()
|
||||||
|
|
||||||
|
# === Spawn monster ===
|
||||||
monster = self._spawn_monster()
|
monster = self._spawn_monster()
|
||||||
events.append(("encounter", f"[red]⚔️ {monster.name} (AC {monster.ac}, HP {monster.hp}) erscheint![/]"))
|
events.append(("encounter", f"[red]⚔️ {monster.name} (AC {monster.ac}, HP {monster.hp}) erscheint![/]"))
|
||||||
self._add_log(events[-1][1])
|
self._add_log(events[-1][1])
|
||||||
|
|
||||||
# Fight
|
# === Fight ===
|
||||||
result = self._combat(monster)
|
result = self._combat(monster)
|
||||||
|
|
||||||
if result["result"] == "victory":
|
if result["result"] == "victory":
|
||||||
@@ -418,6 +611,9 @@ class GameEngine:
|
|||||||
"event_log": self.event_log,
|
"event_log": self.event_log,
|
||||||
"loot_log": self.loot_log,
|
"loot_log": self.loot_log,
|
||||||
"dice_log": self.dice_log.get_all(),
|
"dice_log": self.dice_log.get_all(),
|
||||||
|
"is_traveling": self.is_traveling,
|
||||||
|
"travel_progress": self.travel_progress,
|
||||||
|
"travel_destination": self.travel_destination,
|
||||||
}
|
}
|
||||||
|
|
||||||
def save(self, path: str):
|
def save(self, path: str):
|
||||||
@@ -439,4 +635,7 @@ class GameEngine:
|
|||||||
engine.event_log = d.get("event_log", [])
|
engine.event_log = d.get("event_log", [])
|
||||||
engine.loot_log = d.get("loot_log", [])
|
engine.loot_log = d.get("loot_log", [])
|
||||||
engine.dice_log.entries = d.get("dice_log", [])
|
engine.dice_log.entries = d.get("dice_log", [])
|
||||||
|
engine.is_traveling = d.get("is_traveling", False)
|
||||||
|
engine.travel_progress = d.get("travel_progress", 0.0)
|
||||||
|
engine.travel_destination = d.get("travel_destination", "")
|
||||||
return engine
|
return engine
|
||||||
+17
-3
@@ -99,12 +99,19 @@ def render_game(engine: GameEngine) -> Layout:
|
|||||||
char_table.add_row("Stufe", str(c.level))
|
char_table.add_row("Stufe", str(c.level))
|
||||||
char_table.add_row("XP", f"{c.xp}/{c.xp_to_next}")
|
char_table.add_row("XP", f"{c.xp}/{c.xp_to_next}")
|
||||||
char_table.add_row("HP", f"{c.hp}/{c.max_hp}")
|
char_table.add_row("HP", f"{c.hp}/{c.max_hp}")
|
||||||
|
char_table.add_row("MP", f"{c.mp}/{c.max_mp}" if c.max_mp > 0 else "[dim]—[/]")
|
||||||
char_table.add_row("AC", str(c.ac))
|
char_table.add_row("AC", str(c.ac))
|
||||||
char_table.add_row("Prof", f"+{c.prof_bonus}")
|
char_table.add_row("Prof", f"+{c.prof_bonus}")
|
||||||
char_table.add_row("ATK", f"+{c.attack_bonus}")
|
char_table.add_row("ATK", f"+{c.attack_bonus}")
|
||||||
char_table.add_row("DMG", f"{c.weapon_dice}+{c.damage_bonus}")
|
char_table.add_row("DMG", f"{c.weapon_dice}+{c.damage_bonus}")
|
||||||
char_table.add_row("Gold", f"{c.gold}G")
|
char_table.add_row("Gold", f"{c.gold}G")
|
||||||
|
|
||||||
|
# Potions
|
||||||
|
pot_str = f"❤️{c.potions.get('healing',0)} " \
|
||||||
|
f"✨{c.potions.get('mana',0)} " \
|
||||||
|
f"💖{c.potions.get('greater_healing',0)}"
|
||||||
|
char_table.add_row("Tränke", pot_str)
|
||||||
|
|
||||||
# Stats row
|
# Stats row
|
||||||
stats_str = " ".join(f"{k}:{c.stats[k]}" for k in ["STR","DEX","CON","INT","WIS","CHA"])
|
stats_str = " ".join(f"{k}:{c.stats[k]}" for k in ["STR","DEX","CON","INT","WIS","CHA"])
|
||||||
char_table.add_row("Stats", stats_str)
|
char_table.add_row("Stats", stats_str)
|
||||||
@@ -184,12 +191,19 @@ def render_game(engine: GameEngine) -> Layout:
|
|||||||
Layout(Panel(quest_text, title="[bold]Aktuelle Quest[/]"), name="quest"),
|
Layout(Panel(quest_text, title="[bold]Aktuelle Quest[/]"), name="quest"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bottom: bars
|
# Bottom: bars + travel
|
||||||
bars = Text()
|
bars = Text()
|
||||||
bars.append(f"HP [{hp_bar}] {c.hp}/{c.max_hp} ", style="red")
|
bars.append(f"HP [{hp_bar}] {c.hp}/{c.max_hp} ", style="red")
|
||||||
bars.append(f"XP [{xp_bar}] {c.xp}/{c.xp_to_next} ", style="green")
|
bars.append(f"XP [{xp_bar}] {c.xp}/{c.xp_to_next} ", style="green")
|
||||||
bars.append(f"🌍 {engine.current_location} ", style="blue")
|
|
||||||
bars.append(f"Schritte: {c.steps}", style="dim")
|
if engine.is_traveling:
|
||||||
|
travel_len = 20
|
||||||
|
travel_filled = int(engine.travel_progress * travel_len)
|
||||||
|
travel_bar = "█" * travel_filled + "░" * (travel_len - travel_filled)
|
||||||
|
bars.append(f"🚶 [{travel_bar}] {engine.travel_destination}", style="blue")
|
||||||
|
else:
|
||||||
|
bars.append(f"🌍 {engine.current_location} ", style="blue")
|
||||||
|
bars.append(f"Schritte: {c.steps}", style="dim")
|
||||||
|
|
||||||
layout["bottom"].update(Panel(bars, style="dim"))
|
layout["bottom"].update(Panel(bars, style="dim"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user