411 lines
17 KiB
Python
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
|
|
} |