import math import json import time import random from .game_data import BUILDING_DEFINITIONS, HERO_CLASSES # In-memory storage for active raid sessions # session_id -> RaidSession ACTIVE_RAIDS = {} def get_distance(x1, y1, x2, y2): return max(abs(x1 - x2), abs(y1 - y2)) # Chebyshev distance for grid movement/targeting def has_line_of_sight(grid, x1, y1, x2, y2, ignore_b_id=None): # Bresenham-like line check for projectile line of sight dx = abs(x2 - x1) dy = abs(y2 - y1) sx = 1 if x1 < x2 else -1 sy = 1 if y1 < y2 else -1 err = dx - dy curr_x, curr_y = x1, y1 while (curr_x, curr_y) != (x2, y2): e2 = 2 * err if e2 > -dy: err -= dy curr_x += sx if e2 < dx: err += dx curr_y += sy if (curr_x, curr_y) != (x2, y2): # Check if obstacle in between b = grid.get((curr_x, curr_y)) if b and b["status"] == "active" and b["id"] != ignore_b_id: defn = BUILDING_DEFINITIONS.get(b["b_type"], {}) if not defn.get("is_hidden", False): return False # Line of sight blocked return True class RaidSession: def __init__(self, session_id: str, attacker: dict, defender: dict, buildings: list, grid_w: int, grid_h: int): self.session_id = session_id self.attacker_id = attacker["id"] self.attacker_name = attacker["username"] self.defender_id = defender["id"] self.defender_name = defender["username"] self.grid_w = grid_w self.grid_h = grid_h self.turn = 1 self.max_turns = 20 self.state = "active" # "active", "victory", "defeat" self.log = [] # Convert buildings list to dict by coords and ID self.buildings = {} self.core_building_id = None for b in buildings: b_dict = { "id": b["id"], "b_type": b["b_type"], "level": b["level"], "x": b["x"], "y": b["y"], "hp": b["hp"], "max_hp": b["max_hp"], "status": "active", "revealed": not BUILDING_DEFINITIONS.get(b["b_type"], {}).get("is_hidden", False) } self.buildings[b["id"]] = b_dict if b["b_type"] == "core": self.core_building_id = b["id"] # Initialize Heroes self.heroes = { "tank": { "id": "tank", "name": HERO_CLASSES["tank"]["name"], "hp": HERO_CLASSES["tank"]["max_hp"], "max_hp": HERO_CLASSES["tank"]["max_hp"], "atk": HERO_CLASSES["tank"]["atk"], "range": HERO_CLASSES["tank"]["range"], "max_move": HERO_CLASSES["tank"]["move"], "move_left": HERO_CLASSES["tank"]["move"], "has_attacked": False, "x": 1, "y": max(0, grid_h // 2 - 1), "alive": True, "buff_shieldwall": False, "poison_turns": 0, "skill_cd": 0 }, "archer": { "id": "archer", "name": HERO_CLASSES["archer"]["name"], "hp": HERO_CLASSES["archer"]["max_hp"], "max_hp": HERO_CLASSES["archer"]["max_hp"], "atk": HERO_CLASSES["archer"]["atk"], "range": HERO_CLASSES["archer"]["range"], "max_move": HERO_CLASSES["archer"]["move"], "move_left": HERO_CLASSES["archer"]["move"], "has_attacked": False, "x": 1, "y": max(0, grid_h // 2 + 1), "alive": True, "buff_shieldwall": False, "poison_turns": 0, "skill_cd": 0 }, "rogue": { "id": "rogue", "name": HERO_CLASSES["rogue"]["name"], "hp": HERO_CLASSES["rogue"]["max_hp"], "max_hp": HERO_CLASSES["rogue"]["max_hp"], "atk": HERO_CLASSES["rogue"]["atk"], "range": HERO_CLASSES["rogue"]["range"], "max_move": HERO_CLASSES["rogue"]["move"], "move_left": HERO_CLASSES["rogue"]["move"], "has_attacked": False, "x": 0, "y": max(0, grid_h // 2), "alive": True, "buff_shieldwall": False, "poison_turns": 0, "skill_cd": 0 } } self.log.append(f"⚔️ Überfall auf die Festung von {self.defender_name} hat begonnen! Ziel: Plündere die Schatzkammer.") def get_building_at(self, x, y): for b in self.buildings.values(): if b["x"] == x and b["y"] == y and b["status"] == "active": return b return None def move_hero(self, hero_id: str, tx: int, ty: int): if self.state != "active": return {"error": "Schlacht ist bereits beendet."} hero = self.heroes.get(hero_id) if not hero or not hero["alive"]: return {"error": "Held ist kampfunfähig."} # Check bounds if tx < 0 or tx >= self.grid_w or ty < 0 or ty >= self.grid_h: return {"error": "Ziel außerhalb des Schlachtfeldes."} dist = get_distance(hero["x"], hero["y"], tx, ty) if dist > hero["move_left"]: return {"error": f"Nicht genügend Bewegungspunkte (Verbleibend: {hero['move_left']}, nötig: {dist})."} # Check obstacle target_b = self.get_building_at(tx, ty) if target_b: defn = BUILDING_DEFINITIONS.get(target_b["b_type"], {}) if not defn.get("is_hidden", False): return {"error": "Feld wird durch ein Gebäude/Mauer blockiert!"} # Check other hero for other_id, other_h in self.heroes.items(): if other_id != hero_id and other_h["alive"] and other_h["x"] == tx and other_h["y"] == ty: return {"error": "Feld ist bereits durch einen anderen Helden besetzt."} # Move hero["x"] = tx hero["y"] = ty hero["move_left"] -= dist self.log.append(f"👣 {hero['name']} rückt vor auf Position ({tx}, {ty}).") # Trap Trigger Check! if target_b and target_b["status"] == "active": defn = BUILDING_DEFINITIONS.get(target_b["b_type"], {}) if defn.get("is_hidden", False): target_b["revealed"] = True if target_b["b_type"] == "spike_trap": dmg = defn["damage"] hero["hp"] -= dmg self.log.append(f"💥 FALLE ZUGESCHNAPPT! {hero['name']} tritt in verborgene Stacheln (-{dmg} HP)!") elif target_b["b_type"] == "poison_trap": hero["poison_turns"] = defn["poison_duration"] self.log.append(f"☠️ GIFTFALLE AUSGELÖST! {hero['name']} wird für 3 Runden vergiftet!") target_b["status"] = "destroyed" if hero["hp"] <= 0: hero["hp"] = 0 hero["alive"] = False self.log.append(f"💀 {hero['name']} ist der Falle zum Opfer gefallen!") self._check_battle_state() return {"success": True, "state": self.to_dict()} def attack_target(self, hero_id: str, b_id: int): if self.state != "active": return {"error": "Schlacht ist bereits beendet."} hero = self.heroes.get(hero_id) if not hero or not hero["alive"]: return {"error": "Held ist nicht am Leben."} if hero["has_attacked"]: return {"error": "Held hat in dieser Runde bereits angegriffen."} b = self.buildings.get(b_id) if not b or b["status"] != "active": return {"error": "Ziel ist nicht verfügbar oder bereits zerstört."} dist = get_distance(hero["x"], hero["y"], b["x"], b["y"]) if dist > hero["range"]: return {"error": f"Ziel außer Reichweite (Reichweite: {hero['range']}, Distanz: {dist})."} dmg = hero["atk"] b["hp"] -= dmg hero["has_attacked"] = True hero["move_left"] = 0 # attacking ends movement defn = BUILDING_DEFINITIONS.get(b["b_type"], {}) b_name = defn.get("name", b["b_type"]) self.log.append(f"⚔️ {hero['name']} greift {b_name} an und verursacht {dmg} Schaden! ({max(0, b['hp'])}/{b['max_hp']} HP)") if b["hp"] <= 0: b["hp"] = 0 b["status"] = "destroyed" self.log.append(f"💥 {b_name} an Position ({b['x']}, {b['y']}) wurde ZERSTÖRT!") if b["id"] == self.core_building_id: self.state = "victory" self.log.append(f"🏆 SIEG! Die Schatzkammer wurde geplündert! Deine Helden sichern die Beute!") self._check_battle_state() return {"success": True, "state": self.to_dict()} def use_skill(self, hero_id: str, target_b_id: int = None): if self.state != "active": return {"error": "Schlacht ist beendet."} hero = self.heroes.get(hero_id) if not hero or not hero["alive"]: return {"error": "Held nicht verfügbar."} if hero["skill_cd"] > 0: return {"error": f"Fertigkeit noch in Abklingzeit ({hero['skill_cd']} Runden)."} if hero_id == "tank": hero["buff_shieldwall"] = True hero["skill_cd"] = HERO_CLASSES["tank"]["skill_cd"] self.log.append("🛡️ Schildbrecher aktiviert 'Schildwall'! Erleidet diese Runde 50% weniger Turmschaden.") elif hero_id == "archer": if not target_b_id: return {"error": "Ziel für Gezielter Schuss erforderlich."} b = self.buildings.get(target_b_id) if not b or b["status"] != "active": return {"error": "Ungültiges Ziel."} dist = get_distance(hero["x"], hero["y"], b["x"], b["y"]) if dist > 4: return {"error": "Ziel für Meisterschuss zu weit entfernt (max 4 Felder)."} dmg = 140 b["hp"] -= dmg hero["skill_cd"] = HERO_CLASSES["archer"]["skill_cd"] hero["has_attacked"] = True defn = BUILDING_DEFINITIONS.get(b["b_type"], {}) self.log.append(f"🎯 Waldläufer feuert 'Gezielten Schuss' ab! Durchschlägt {defn.get('name')} für {dmg} Schaden!") if b["hp"] <= 0: b["hp"] = 0 b["status"] = "destroyed" self.log.append(f"💥 {defn.get('name')} wurde durch den Meisterschuss ZERSTÖRT!") if b["id"] == self.core_building_id: self.state = "victory" self.log.append("🏆 SIEG! Schatzkammer zerstört!") elif hero_id == "rogue": # Reveal traps within radius 3 revealed_count = 0 for b in self.buildings.values(): if b["status"] == "active" and not b["revealed"]: if get_distance(hero["x"], hero["y"], b["x"], b["y"]) <= 3: b["revealed"] = True revealed_count += 1 hero["skill_cd"] = HERO_CLASSES["rogue"]["skill_cd"] self.log.append(f"👁️ Schatten-Schurke späht das Terrain aus: {revealed_count} verborgene Fallen aufgedeckt!") self._check_battle_state() return {"success": True, "state": self.to_dict()} def end_turn(self): if self.state != "active": return {"error": "Schlacht ist bereits beendet."} self.log.append(f"--- 🏰 Runde {self.turn}: Die Verteidigungsanlagen der Festung reagieren! ---") # 1. Tower Defense AI Phase living_heroes = [h for h in self.heroes.values() if h["alive"]] if living_heroes: for b in self.buildings.values(): if b["status"] != "active": continue defn = BUILDING_DEFINITIONS.get(b["b_type"]) if not defn or defn.get("category") != "tower": continue t_range = defn["range"] min_range = defn.get("range_min", 0) t_dmg = defn["damage"] + (b["level"] - 1) * defn.get("damage_per_lvl", 10) # Find valid hero targets in range in_range_heroes = [] for h in living_heroes: dist = get_distance(b["x"], b["y"], h["x"], h["y"]) if min_range <= dist <= t_range: in_range_heroes.append((dist, h)) if not in_range_heroes: continue # Sort by closest in_range_heroes.sort(key=lambda x: x[0]) target_dist, target_hero = in_range_heroes[0] # Ballista single target if b["b_type"] == "ballista": dmg = t_dmg if target_hero["buff_shieldwall"]: dmg = math.floor(dmg * 0.5) self.log.append(f"🛡️ Schildwall fängt Teil des Bolzens ab!") target_hero["hp"] -= dmg self.log.append(f"🏹 Ballistenturm feuert schweren Bolzen auf {target_hero['name']} (-{dmg} HP)!") # Catapult AoE elif b["b_type"] == "catapult": self.log.append(f"🔥 Brandkatapult schleudert flüssiges Pech auf {target_hero['name']}!") for other_h in living_heroes: aoe_dist = get_distance(target_hero["x"], target_hero["y"], other_h["x"], other_h["y"]) if aoe_dist <= 1: dmg = t_dmg if other_h["id"] == target_hero["id"] else math.floor(t_dmg * 0.6) if other_h["buff_shieldwall"]: dmg = math.floor(dmg * 0.5) other_h["hp"] -= dmg self.log.append(f" 💥 Brand-Flächenschaden an {other_h['name']} (-{dmg} HP)!") # Crypt Mage Tower elif b["b_type"] == "crypt": dmg = t_dmg target_hero["hp"] -= dmg target_hero["move_left"] = max(1, target_hero["move_left"] - 1) self.log.append(f"✨ Krypta-Magierturm durchdringt die Rüstung von {target_hero['name']} (-{dmg} HP) und verlangsamt ihn!") # Check casualties for h in living_heroes: if h["hp"] <= 0 and h["alive"]: h["hp"] = 0 h["alive"] = False self.log.append(f"💀 {h['name']} wurde von den Verteidigungsanlagen niedergestreckt!") # 2. Poison ticks for h in self.heroes.values(): if h["alive"] and h["poison_turns"] > 0: p_dmg = 30 h["hp"] -= p_dmg h["poison_turns"] -= 1 self.log.append(f"☠️ Gift verätzt {h['name']} (-{p_dmg} HP, noch {h['poison_turns']} Runden)!") if h["hp"] <= 0: h["hp"] = 0 h["alive"] = False self.log.append(f"💀 {h['name']} ist dem Gift erlegen!") # 3. Reset turn stats for heroes self.turn += 1 for h in self.heroes.values(): if h["alive"]: h["has_attacked"] = False h["move_left"] = h["max_move"] h["buff_shieldwall"] = False if h["skill_cd"] > 0: h["skill_cd"] -= 1 self._check_battle_state() return {"success": True, "state": self.to_dict()} def _check_battle_state(self): core = self.buildings.get(self.core_building_id) if core and core["status"] == "destroyed": self.state = "victory" return living = [h for h in self.heroes.values() if h["alive"]] if not living: self.state = "defeat" self.log.append("❌ NIEDERLAGE: Dein gesamter Trupp wurde von den Festungsanlagen vernichtet!") return if self.turn > self.max_turns: self.state = "defeat" self.log.append("⌛ ZEIT ABGELAUFEN: Nach 20 Runden musstet ihr den Rückzug antreten!") def to_dict(self): # Format for frontend buildings_list = [] for b in self.buildings.values(): defn = BUILDING_DEFINITIONS.get(b["b_type"], {}) # Only send revealed traps to the attacker if defn.get("is_hidden", False) and not b["revealed"]: continue buildings_list.append(b) return { "session_id": self.session_id, "attacker_id": self.attacker_id, "attacker_name": self.attacker_name, "defender_id": self.defender_id, "defender_name": self.defender_name, "turn": self.turn, "max_turns": self.max_turns, "state": self.state, "grid_w": self.grid_w, "grid_h": self.grid_h, "heroes": self.heroes, "buildings": buildings_list, "log": self.log[-12:] # latest 12 messages }