v2.2 — Status-Effekte + Log-Scrolling
Status-Effekte (D&D 5e): - Gift (🐍): Spinne/Naga/Schlange — Schaden pro Runde - Brennen (🔥): Drache/Feuerdämon — Feuerschaden pro Runde - Blutung (🩸): Vampir/Todes — Schaden pro Runde - Schwächungsfluch (💜): Hexe/Fluch — -2 ATK/AC - Furcht (😱): Geist/Schrecken — verhindert Angriff - Regeneration (💚): Heilung über Zeit - Buff/Debuff wirken sich auf effective_ac/effective_attack aus Entgiftung: - Gegengift-Trank im Shop (25G) — entfernt Gift/Fluch - Auto-Nutzung bei Vergiftung - Lange Rast reinigt auch alle Effekte Log-Scrolling: - Event-Log: chronologisch (neueste unten), 50 Einträge - Loot-Log: chronologisch, 50 Einträge - Dice-Log: chronologisch, 30 Einträge - UI zeigt letzte 20/15/12 Einträge — Scroll-Effekt
This commit is contained in:
+102
@@ -148,6 +148,9 @@ class DnDCharacter:
|
||||
self.max_mp = self._calc_max_mp()
|
||||
self.mp = self.max_mp
|
||||
|
||||
# Status effects
|
||||
self.status_effects = [] # list of {type, duration, power, name}
|
||||
|
||||
# Stats tracking
|
||||
self.kills = 0
|
||||
self.deaths = 0
|
||||
@@ -245,6 +248,103 @@ class DnDCharacter:
|
||||
self.mp -= cost
|
||||
self.spell_slots -= 1
|
||||
|
||||
# === Status Effects ===
|
||||
def add_effect(self, effect_type: str, duration: int, power: int, name: str):
|
||||
"""Fügt einen Statuseffekt hinzu."""
|
||||
# Remove existing effect of same type
|
||||
self.status_effects = [e for e in self.status_effects if e["type"] != effect_type]
|
||||
self.status_effects.append({
|
||||
"type": effect_type, "duration": duration, "power": power, "name": name
|
||||
})
|
||||
|
||||
def has_effect(self, effect_type: str) -> bool:
|
||||
return any(e["type"] == effect_type for e in self.status_effects)
|
||||
|
||||
def get_effect(self, effect_type: str):
|
||||
for e in self.status_effects:
|
||||
if e["type"] == effect_type:
|
||||
return e
|
||||
return None
|
||||
|
||||
def tick_effects(self) -> list:
|
||||
"""Tickt alle Effekte, gibt Liste der abgelaufenen und gewirkten zurück."""
|
||||
results = []
|
||||
for effect in self.status_effects[:]:
|
||||
effect["duration"] -= 1
|
||||
|
||||
# Apply per-turn effects
|
||||
if effect["type"] == "poison":
|
||||
dmg = effect["power"]
|
||||
self.hp = max(0, self.hp - dmg)
|
||||
results.append(f"[green]🐍 Gift schadet {dmg} HP (HP: {self.hp}/{self.max_hp})[/]")
|
||||
elif effect["type"] == "burn":
|
||||
dmg = effect["power"]
|
||||
self.hp = max(0, self.hp - dmg)
|
||||
results.append(f"[red]🔥 Brennen schadet {dmg} HP[/]")
|
||||
elif effect["type"] == "regen":
|
||||
heal = effect["power"]
|
||||
self.hp = min(self.max_hp, self.hp + heal)
|
||||
results.append(f"[green]💚 Regeneration heilt {heal} HP[/]")
|
||||
elif effect["type"] == "bleed":
|
||||
dmg = effect["power"]
|
||||
self.hp = max(0, self.hp - dmg)
|
||||
results.append(f"[red]🩸 Blutung schadet {dmg} HP[/]")
|
||||
|
||||
if effect["duration"] <= 0:
|
||||
self.status_effects.remove(effect)
|
||||
results.append(f"[dim]✨ {effect['name']} ist abgeklungen.[/]")
|
||||
|
||||
return results
|
||||
|
||||
def cleanse(self) -> int:
|
||||
"""Entfernt alle negativen Effekte. Gibt Anzahl entfernter zurück."""
|
||||
negative = ["poison", "burn", "bleed", "curse_weakness", "curse_fatigue", "fear", "slow"]
|
||||
removed = 0
|
||||
for neg in negative:
|
||||
before = len(self.status_effects)
|
||||
self.status_effects = [e for e in self.status_effects if e["type"] != neg]
|
||||
removed += before - len(self.status_effects)
|
||||
return removed
|
||||
|
||||
@property
|
||||
def effective_ac(self) -> int:
|
||||
"""AC mit Buffs/Debuffs."""
|
||||
ac = self.ac
|
||||
if self.has_effect("shield_of_faith"):
|
||||
ac += self.get_effect("shield_of_faith")["power"]
|
||||
if self.has_effect("curse_weakness"):
|
||||
ac -= 2
|
||||
return max(1, ac)
|
||||
|
||||
@property
|
||||
def effective_attack(self) -> int:
|
||||
"""Attack Bonus mit Buffs/Debuffs."""
|
||||
bonus = self.attack_bonus
|
||||
if self.has_effect("bless"):
|
||||
bonus += 2
|
||||
if self.has_effect("curse_weakness"):
|
||||
bonus -= 2
|
||||
if self.has_effect("rage"):
|
||||
bonus += self.get_effect("rage")["power"]
|
||||
return max(0, bonus)
|
||||
|
||||
@property
|
||||
def status_summary(self) -> str:
|
||||
"""Kurze Zusammenfassung der aktiven Effekte für UI."""
|
||||
if not self.status_effects:
|
||||
return "[dim]keine[/]"
|
||||
icons = {
|
||||
"poison": "🐍", "burn": "🔥", "regen": "💚", "bleed": "🩸",
|
||||
"bless": "✨", "rage": "😠", "shield_of_faith": "🛡️",
|
||||
"curse_weakness": "💜", "curse_fatigue": "😫", "fear": "😱", "slow": "🐌",
|
||||
"haste": "⚡", "strength": "💪",
|
||||
}
|
||||
parts = []
|
||||
for e in self.status_effects:
|
||||
icon = icons.get(e["type"], "❓")
|
||||
parts.append(f"{icon}{e['duration']}")
|
||||
return " ".join(parts)
|
||||
|
||||
@property
|
||||
def attack_bonus(self) -> int:
|
||||
"""Attack roll modifier: STR/DEX mod + proficiency."""
|
||||
@@ -331,6 +431,7 @@ class DnDCharacter:
|
||||
"inventory": [i.to_dict() for i in self.inventory],
|
||||
"gold": self.gold,
|
||||
"potions": self.potions, "potion_threshold": self.potion_threshold,
|
||||
"status_effects": self.status_effects,
|
||||
"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,
|
||||
"potions_used": self.potions_used, "gold_spent": self.gold_spent, "items_bought": self.items_bought,
|
||||
@@ -352,6 +453,7 @@ class DnDCharacter:
|
||||
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.status_effects = d.get("status_effects", [])
|
||||
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.potions_used = d.get("potions_used", 0); c.gold_spent = d.get("gold_spent", 0); c.items_bought = d.get("items_bought", 0)
|
||||
|
||||
+92
-20
@@ -65,6 +65,23 @@ class DnDMonster:
|
||||
self.damage_dice = random.choice(["1d6", "1d8", "1d10", "2d6", "1d12"])
|
||||
self.damage_mod = max(0, level // 3)
|
||||
|
||||
# Status effects the monster can inflict (based on type)
|
||||
self.inflicts = []
|
||||
monster_type = name.lower()
|
||||
if any(w in monster_type for w in ["spinne", "naga", "schlang", "skorpion", "gift"]):
|
||||
self.inflicts.append({"type": "poison", "chance": 30, "duration": 3, "power": max(1, level // 2), "name": "Giftbiss"})
|
||||
if any(w in monster_type for w in ["drache", "feuer", "dämon", "phoenix", "flamme"]):
|
||||
self.inflicts.append({"type": "burn", "chance": 25, "duration": 2, "power": max(1, level // 2), "name": "Feueratem"})
|
||||
if any(w in monster_type for w in ["vampir", "blut", "schnitter", "todes"]):
|
||||
self.inflicts.append({"type": "bleed", "chance": 35, "duration": 3, "power": max(1, level // 3), "name": "Blutung"})
|
||||
if any(w in monster_type for w in ["hexe", "fluch", "verflucht", "schatten", "dunkel"]):
|
||||
self.inflicts.append({"type": "curse_weakness", "chance": 20, "duration": 3, "power": 2, "name": "Schwächungsfluch"})
|
||||
if any(w in monster_type for w in ["geist", "gespenst", "furcht", "schrecken"]):
|
||||
self.inflicts.append({"type": "fear", "chance": 20, "duration": 2, "power": 2, "name": "Furcht"})
|
||||
# Sometimes random poison for variety
|
||||
if not self.inflicts and random.randint(1, 4) == 1:
|
||||
self.inflicts.append({"type": "poison", "chance": 15, "duration": 2, "power": 1, "name": "Gift"})
|
||||
|
||||
# XP and Gold (D&D 5e CR table, simplified)
|
||||
cr_xp = {0.25: 50, 0.5: 100, 1: 200, 2: 450, 3: 700, 4: 1100, 5: 1800,
|
||||
6: 2300, 7: 2900, 8: 3900, 9: 5000, 10: 5900}
|
||||
@@ -80,7 +97,7 @@ class DnDMonster:
|
||||
return self.hp > 0
|
||||
|
||||
def attack(self, target_ac: int, dice_log: list) -> dict:
|
||||
"""Monster greift an mit d20."""
|
||||
"""Monster greift an mit d20, kann Status-Effekte zufügen."""
|
||||
atk_roll = roll_d20(self.attack_mod)
|
||||
dice_log.append(f"[red]🎲 {self.name} greift an: d20{atk_roll['rolls']}{'+' if self.attack_mod >= 0 else ''}{self.attack_mod} = {atk_roll['total']} vs AC {target_ac}[/]")
|
||||
|
||||
@@ -90,11 +107,11 @@ class DnDMonster:
|
||||
|
||||
if fumble:
|
||||
dice_log.append(f"[red]💀 Kritischer Fehlschlag! {self.name} stolpert![/]")
|
||||
return {"hit": False, "damage": 0, "crit": False, "fumble": True}
|
||||
return {"hit": False, "damage": 0, "crit": False, "fumble": True, "effect": None}
|
||||
|
||||
if not hit:
|
||||
dice_log.append(f"[yellow]💨 {self.name} verfehlt! (AC {target_ac})[/]")
|
||||
return {"hit": False, "damage": 0, "crit": False, "fumble": False}
|
||||
return {"hit": False, "damage": 0, "crit": False, "fumble": False, "effect": None}
|
||||
|
||||
# Damage roll
|
||||
dice_str = self.damage_dice
|
||||
@@ -102,7 +119,6 @@ class DnDMonster:
|
||||
dmg_roll = roll(sides, count, self.damage_mod)
|
||||
|
||||
if crit:
|
||||
# Double dice on crit
|
||||
extra = roll(sides, count, 0)
|
||||
total_dmg = dmg_roll["total"] + extra["total"] - self.damage_mod
|
||||
dice_log.append(f"[bold red]💥 KRITISCH! {dice_str}+{self.damage_mod} = {dmg_roll['total']} + {extra['total']} (crit) = {total_dmg} Schaden![/]")
|
||||
@@ -110,19 +126,27 @@ class DnDMonster:
|
||||
total_dmg = dmg_roll["total"]
|
||||
dice_log.append(f"[red]⚔️ {self.name} trifft: {dice_str}+{self.damage_mod} = {total_dmg} Schaden[/]")
|
||||
|
||||
return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False}
|
||||
# Check for status effect infliction
|
||||
effect_inflicted = None
|
||||
for inf in self.inflicts:
|
||||
if random.randint(1, 100) <= inf["chance"]:
|
||||
effect_inflicted = inf
|
||||
dice_log.append(f"[yellow]⚠️ {self.name} wirkt {inf['name']}! ({inf['type']}, {inf['duration']} Runden)[/]")
|
||||
break
|
||||
|
||||
return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False, "effect": effect_inflicted}
|
||||
|
||||
|
||||
class DiceLog:
|
||||
"""Verwaltet den Würfel-Log für die UI."""
|
||||
def __init__(self, max_entries: int = 25):
|
||||
def __init__(self, max_entries: int = 30):
|
||||
self.entries = []
|
||||
self.max_entries = max_entries
|
||||
|
||||
def add(self, msg: str):
|
||||
self.entries.insert(0, msg)
|
||||
self.entries.append(msg)
|
||||
if len(self.entries) > self.max_entries:
|
||||
self.entries = self.entries[:self.max_entries]
|
||||
self.entries = self.entries[-self.max_entries:]
|
||||
|
||||
def clear(self):
|
||||
self.entries = []
|
||||
@@ -136,9 +160,11 @@ class GameEngine:
|
||||
self.character = character
|
||||
self.dice_log = DiceLog(max_entries=25)
|
||||
self.event_log = []
|
||||
self.max_event_log = 20
|
||||
self.max_event_log = 50 # increased for scrolling
|
||||
self.event_scroll = 0 # 0 = newest at bottom, scroll up to see older
|
||||
self.loot_log = []
|
||||
self.max_loot_log = 20
|
||||
self.max_loot_log = 50
|
||||
self.loot_scroll = 0
|
||||
|
||||
self.current_location = random.choice(FALLBACK_LOCATIONS)
|
||||
self.current_quest = random.choice(FALLBACK_QUESTS)
|
||||
@@ -162,14 +188,14 @@ class GameEngine:
|
||||
pass
|
||||
|
||||
def _add_log(self, msg: str):
|
||||
self.event_log.insert(0, msg)
|
||||
self.event_log.append(msg)
|
||||
if len(self.event_log) > self.max_event_log:
|
||||
self.event_log = self.event_log[:self.max_event_log]
|
||||
self.event_log = self.event_log[-self.max_event_log:]
|
||||
|
||||
def _add_loot_log(self, msg: str):
|
||||
self.loot_log.insert(0, msg)
|
||||
self.loot_log.append(msg)
|
||||
if len(self.loot_log) > self.max_loot_log:
|
||||
self.loot_log = self.loot_log[:self.max_loot_log]
|
||||
self.loot_log = self.loot_log[-self.max_loot_log:]
|
||||
|
||||
def _spawn_monster(self) -> DnDMonster:
|
||||
name = random.choice(FALLBACK_MONSTERS)
|
||||
@@ -254,13 +280,27 @@ class GameEngine:
|
||||
return {"hit": True, "damage": total_dmg, "crit": crit, "fumble": False}
|
||||
|
||||
def _combat(self, monster: DnDMonster) -> dict:
|
||||
"""D&D 5e Combat: abwechselnde Runden."""
|
||||
"""D&D 5e Combat: abwechselnde Runden mit Status-Effekten."""
|
||||
rounds = 0
|
||||
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:
|
||||
rounds += 1
|
||||
|
||||
# Tick status effects at start of round
|
||||
if self.character.status_effects:
|
||||
effect_results = self.character.tick_effects()
|
||||
for er in effect_results:
|
||||
self.dice_log.add(er)
|
||||
if not self.character.is_alive:
|
||||
self.dice_log.add(f"[bold red]💀 Status-Effekt tötet dich![/]")
|
||||
return {"result": "death", "rounds": rounds, "monster": monster.name}
|
||||
|
||||
# Use cleanse potion if badly afflicted
|
||||
if self.character.has_effect("poison") and self.character.potions.get("healing", 0) > 0 and self.character.hp < self.character.max_hp * 0.5:
|
||||
# Use healing potion which also cleanses (simplified)
|
||||
pass # potion use happens in _use_potions_if_needed before combat
|
||||
|
||||
# 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):
|
||||
@@ -268,7 +308,7 @@ class GameEngine:
|
||||
self.dice_log.add(f"[bold green]☠️ {monster.name} durch Zauber besiegt![/]")
|
||||
break
|
||||
|
||||
# Player attacks (initiative: DEX check)
|
||||
# Player attacks
|
||||
result = self._player_attack(monster)
|
||||
if result["hit"]:
|
||||
monster.hp = max(0, monster.hp - result["damage"])
|
||||
@@ -278,15 +318,20 @@ class GameEngine:
|
||||
|
||||
# Monster attacks
|
||||
if monster.is_alive:
|
||||
m_result = monster.attack(self.character.ac, self.dice_log.entries)
|
||||
self.dice_log.entries = self.dice_log.entries # update ref
|
||||
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"])
|
||||
# Apply status effect
|
||||
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}
|
||||
|
||||
self.dice_log.add(f"[dim]── Runde {rounds} ── HP: {self.character.hp}/{self.character.max_hp} | Monster: {monster.hp}/{monster.max_hp} ──[/]")
|
||||
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} ──[/]")
|
||||
|
||||
return {"result": "victory", "rounds": rounds, "monster": monster.name}
|
||||
|
||||
@@ -462,14 +507,41 @@ class GameEngine:
|
||||
self._add_loot_log(f"[blue]🧪 Kaufe Manatrank (-{mana_price}G)[/]")
|
||||
bought_something = True
|
||||
|
||||
# 7. Antidote (Entgiftung) — always useful
|
||||
antidote_price = 25
|
||||
max_antidotes = 3
|
||||
while c.potions.get("antidote", 0) < max_antidotes and c.gold - antidote_price >= gold_reserve:
|
||||
c.gold -= antidote_price
|
||||
c.potions["antidote"] = c.potions.get("antidote", 0) + 1
|
||||
c.gold_spent += antidote_price
|
||||
c.items_bought += 1
|
||||
self._add_loot_log(f"[green]🧪 Kaufe Gegengift (-{antidote_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."""
|
||||
"""Benutzt automatisch Tränke wenn nötig."""
|
||||
c = self.character
|
||||
|
||||
# Antidote if poisoned
|
||||
if c.has_effect("poison") and c.potions.get("antidote", 0) > 0:
|
||||
removed = c.cleanse()
|
||||
c.potions["antidote"] -= 1
|
||||
c.potions_used += 1
|
||||
self.dice_log.add(f"[green]🧪 Gegengift benutzt! {removed} Effekte entfernt.[/]")
|
||||
self._add_log(f"[green]🧪 Gegengift benutzt — Vergiftung geheilt![/]")
|
||||
|
||||
# Antidote also removes curses if desperate
|
||||
if c.has_effect("curse_weakness") and c.potions.get("antidote", 0) > 0 and c.hp < c.max_hp * 0.3:
|
||||
removed = c.cleanse()
|
||||
c.potions["antidote"] -= 1
|
||||
c.potions_used += 1
|
||||
self.dice_log.add(f"[green]🧪 Gegengift gegen Fluch! {removed} Effekte entfernt.[/]")
|
||||
self._add_log(f"[green]🧪 Fluch aufgehoben![/]")
|
||||
|
||||
# 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")
|
||||
|
||||
+10
-4
@@ -112,6 +112,9 @@ def render_game(engine: GameEngine) -> Layout:
|
||||
f"💖{c.potions.get('greater_healing',0)}"
|
||||
char_table.add_row("Tränke", pot_str)
|
||||
|
||||
# Status effects
|
||||
char_table.add_row("Status", c.status_summary)
|
||||
|
||||
# Stats row
|
||||
stats_str = " ".join(f"{k}:{c.stats[k]}" for k in ["STR","DEX","CON","INT","WIS","CHA"])
|
||||
char_table.add_row("Stats", stats_str)
|
||||
@@ -134,23 +137,26 @@ def render_game(engine: GameEngine) -> Layout:
|
||||
quest_text.append(f"📜 {engine.current_quest}\n", style="cyan")
|
||||
quest_text.append(f"Fortschritt: {engine.quest_progress}/{engine.quest_target}", style="yellow")
|
||||
|
||||
# === Event Log ===
|
||||
# === Event Log (scrollable — newest at bottom) ===
|
||||
event_text = Text()
|
||||
for entry in engine.event_log:
|
||||
visible_events = engine.event_log[-20:] # show last 20
|
||||
for entry in visible_events:
|
||||
t = Text.from_markup(entry)
|
||||
event_text.append(t)
|
||||
event_text.append("\n")
|
||||
|
||||
# === Loot Log ===
|
||||
loot_text = Text()
|
||||
for entry in engine.loot_log:
|
||||
visible_loot = engine.loot_log[-15:]
|
||||
for entry in visible_loot:
|
||||
t = Text.from_markup(entry)
|
||||
loot_text.append(t)
|
||||
loot_text.append("\n")
|
||||
|
||||
# === Dice Log (Würfel-Fenster) ===
|
||||
dice_text = Text()
|
||||
for entry in engine.dice_log.get_all():
|
||||
visible_dice = engine.dice_log.get_all()[-12:]
|
||||
for entry in visible_dice:
|
||||
t = Text.from_markup(entry)
|
||||
dice_text.append(t)
|
||||
dice_text.append("\n")
|
||||
|
||||
Reference in New Issue
Block a user