class_name Game extends RefCounted ## Rundenlogik: 8 Spieler (1 Mensch, 7 Bots), geteilter Pool, Paarungen, Kämpfe. enum Phase { PLANNING, COMBAT, RESULT, GAMEOVER } const BOT_NAMES := [ "Ignis", "Nerea", "Zephyr", "Volt", "Terra", "Frost", "Umbra", ] var players: Array = [] var pool: Pool var rand := RandomNumberGenerator.new() var round_no: int = 0 var phase: int = Phase.PLANNING var human: PlayerState var bots := {} ## pid -> BotAI var pairings: Array = [] ## [{a, b, ghost}] var player_combat: Combat = null var pending: Array = [] ## Ergebnisse der Bot-Kämpfe var last_round_log: Array = [] var plan_left: float = Data.PLAN_SECONDS var timer_enabled: bool = true var human_recap: Dictionary = {} ## Rückblick auf den letzten eigenen Kampf ## Zählt, wie oft der Mensch gewonnen bzw. verloren hat (für Sound und Anzeige). var human_last_result: int = 0 ## 1 Sieg, -1 Niederlage, 0 unentschieden/keine func start(seed_value: int = 0) -> void: rand.seed = seed_value if seed_value != 0 else randi() pool = Pool.new() players.clear() human = PlayerState.new(0, "Du", false) players.append(human) for i in BOT_NAMES.size(): var b := PlayerState.new(i + 1, BOT_NAMES[i], true) players.append(b) var ai := BotAI.new() ai.setup(rand) bots[b.pid] = ai round_no = 1 for p in players: p.roll_shop(pool, rand) pairings = _make_pairings() _run_bot_turns() plan_left = Data.PLAN_SECONDS phase = Phase.PLANNING func alive_players() -> Array: var out := [] for p in players: if p.alive: out.append(p) return out # --- Planungsphase --------------------------------------------------------- func _run_bot_turns() -> void: for p in players: if p.is_bot and p.alive: bots[p.pid].take_turn(p, pool, rand) bots[p.pid].arrange(p, opponent_of(p)) ## Planungsuhr. Läuft sie ab, startet der Kampf von selbst. ## Gibt true zurück, wenn dadurch die Phase gewechselt hat. func tick_planning(dt: float) -> bool: if phase != Phase.PLANNING or not timer_enabled: return false plan_left -= dt if plan_left <= 0.0: plan_left = 0.0 start_combat() return true return false # --- Kampfphase ------------------------------------------------------------ func start_combat() -> void: if phase != Phase.PLANNING: return # Abbilder erst jetzt kopieren, damit sie den aktuellen Stand zeigen. for pr in pairings: if pr["ghost"]: pr["b"] = _make_ghost(pr["ghost_src"]) pending.clear() player_combat = null for pr in pairings: if pr["a"] == human or pr["b"] == human: player_combat = Combat.new(pr["a"], pr["b"], rand.randi()) player_combat.record_hits = true pending.append({"pair": pr, "combat": player_combat}) else: var c := Combat.new(pr["a"], pr["b"], rand.randi()) c.run_to_end() pending.append({"pair": pr, "combat": c}) phase = Phase.COMBAT if player_combat == null: resolve_round() func update_combat(dt: float) -> void: if phase != Phase.COMBAT or player_combat == null: return player_combat.step(dt) if player_combat.finished: resolve_round() func skip_combat() -> void: if phase == Phase.COMBAT and player_combat != null: player_combat.run_to_end() resolve_round() func _make_pairings() -> Array: var alive := alive_players() alive.shuffle() var out := [] var i := 0 while i + 1 < alive.size(): out.append({"a": alive[i], "b": alive[i + 1], "ghost": false}) i += 2 if i < alive.size(): # Ungerade Anzahl: der Rest kämpft gegen ein Abbild eines Gegners. var lone: PlayerState = alive[i] var others := alive.filter(func(p): return p != lone) if others.is_empty(): return out var src: PlayerState = others[rand.randi_range(0, others.size() - 1)] out.append({"a": lone, "b": _make_ghost(src), "ghost": true, "ghost_src": src}) return out func _make_ghost(src: PlayerState) -> PlayerState: var g := PlayerState.new(-1, src.pname + " (Abbild)", true) for hex in src.board: g.board[hex] = src.board[hex] return g # --- Auswertung ------------------------------------------------------------ func resolve_round() -> void: last_round_log.clear() for item in pending: var pr: Dictionary = item["pair"] var c: Combat = item["combat"] if not c.finished: c.run_to_end() var a: PlayerState = pr["a"] var b: PlayerState = pr["b"] var base := Data.round_base_damage(round_no) var dmg := base + c.survivor_stars if a == human or b == human: human_last_result = 0 if c.winner < 0 else (1 if pr["a" if c.winner == 0 else "b"] == human else -1) human_recap = c.recap(0 if a == human else 1) human_recap["opponent"] = (b if a == human else a).pname human_recap["result"] = human_last_result human_recap["damage"] = dmg if c.winner >= 0 else base if c.winner == 0: _apply_result(a, b, dmg, pr["ghost"]) elif c.winner == 1: _apply_result(b, a, dmg, pr["ghost"]) else: a.register_result(false) a.take_damage(base) if not pr["ghost"]: b.register_result(false) b.take_damage(base) last_round_log.append("%s vs %s: unentschieden (-%d)" % [a.pname, b.pname, base]) pending.clear() player_combat = null _check_eliminations() if alive_players().size() <= 1: phase = Phase.GAMEOVER for p in alive_players(): p.placement = 1 return _next_round() func _apply_result(winner: PlayerState, loser: PlayerState, dmg: int, ghost: bool) -> void: winner.register_result(true) if loser.pid >= 0: loser.register_result(false) loser.take_damage(dmg) last_round_log.append("%s schlägt %s (-%d Leben)" % [winner.pname, loser.pname, dmg]) func _check_eliminations() -> void: var still := alive_players().size() for p in players: if p.hp <= 0 and p.alive: p.alive = false p.placement = still still -= 1 last_round_log.append(p.pname + " ist ausgeschieden") func _next_round() -> void: round_no += 1 for p in players: if not p.alive: continue p.gold += p.round_income() p.gain_xp(Data.XP_PER_ROUND) p.new_round_shop(pool, rand) pairings = _make_pairings() _run_bot_turns() plan_left = Data.PLAN_SECONDS phase = Phase.PLANNING # --- Hilfen für die UI ---------------------------------------------------- ## Gegner eines Spielers in der aktuellen Paarung (Abbilder liefern ihre Quelle). func opponent_of(p: PlayerState) -> PlayerState: for pr in pairings: if pr["a"] == p: return pr["ghost_src"] if pr["ghost"] else pr["b"] if pr["b"] == p: return pr["a"] return null func human_opponent() -> PlayerState: for pr in pairings: if pr["a"] == human: return pr["b"] if pr["b"] == human: return pr["a"] return null func human_team_in_combat() -> int: for pr in pairings: if pr["a"] == human: return 0 if pr["b"] == human: return 1 return 0 func ranking() -> Array: var sorted := players.duplicate() sorted.sort_custom(func(a, b): if a.alive != b.alive: return a.alive return a.hp > b.hp) return sorted