Files

411 lines
17 KiB
Python

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
}