extends Node ## Single authoritative phase coordinator; the presentation never ticks combat. signal updated signal battle_started signal wave_settled(reports: Array) const AI_SCRIPT = preload("res://scripts/ai/simple_ai.gd") const BUILD_FIRST := 45.0 const BUILD_DURATION := 30.0 const RESULTS_DURATION := 4.0 var battle: Node3D var remaining: float = 0.0 var ai = AI_SCRIPT.new() var _public_snapshots: Array = [{}, {}] var _wave_core_damage: Array = [0, 0] func attach_battle(controller: Node3D) -> void: battle = controller func start_match() -> void: if not is_instance_valid(battle): push_error("WaveManager needs an attached BattleController.") return GameState.reset_match() EconomyManager.reset_match() _public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)] GameState.wave = 1 _open_build() func _open_build() -> void: EconomyManager.open_build(GameState.wave) GameState.set_phase(GameState.Phase.BUILD) remaining = BUILD_FIRST if GameState.wave == 1 else BUILD_DURATION # The rival uses the previous completed formation, never a live player queue. ai.plan(1, GameState.wave, _public_snapshots[0], GameState.last_reports[1], GameState.ai_level) battle.show_formation(_rosters()) updated.emit() func _rosters() -> Array: return [EconomyManager.get_team(0).roster, EconomyManager.get_team(1).roster] func ready_for_battle() -> void: if GameState.phase == GameState.Phase.RESULTS: _next_wave() return if GameState.phase != GameState.Phase.BUILD: return _public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)] var outgoing: Array = EconomyManager.freeze(GameState.wave) var attackers: Array = [ DataCatalog.get_attackers(GameState.wave), DataCatalog.get_attackers(GameState.wave) ] for sender in range(2): for send_id in outgoing[sender]: attackers[1 - sender].append(DataCatalog.get_send(str(send_id), GameState.wave)) _wave_core_damage = [0, 0] battle.begin_battle(_rosters(), attackers, GameState.wave) remaining = 90.0 GameState.set_phase(GameState.Phase.COMBAT) battle_started.emit() updated.emit() func _physics_process(delta: float) -> void: if GameState.paused: return # 2x repeats the same fixed step rather than doubling its integration delta. for _substep in range(GameState.speed): advance(delta) func advance(delta: float) -> void: if delta <= 0.0 or GameState.paused: return match GameState.phase: GameState.Phase.BUILD: if not GameState.timed_build: return remaining = maxf(0.0, remaining - delta) if remaining <= 0.0: ready_for_battle() GameState.Phase.COMBAT: var report: Dictionary = battle.step(delta) remaining = maxf(0.0, 90.0 - float(report.elapsed)) var leak_damage: Array = report.leak_damage if leak_damage[0] > 0 or leak_damage[1] > 0: for team in range(2): _wave_core_damage[team] += int(leak_damage[team]) GameState.apply_core_damage(leak_damage) for lane: int in range(2): if leak_damage[lane] > 0: Soundscape.play("leak", lane) var winner: int = GameState.decide_winner(false) if winner != GameState.ONGOING: _finish_match(winner) elif report.finished: _settle(report) GameState.Phase.RESULTS: if not GameState.timed_build: return remaining = maxf(0.0, remaining - delta) if remaining <= 0.0: _next_wave() func _settle(combat_report: Dictionary) -> void: var reports: Array = [] for team in range(2): var report: Dictionary = EconomyManager.settle( team, GameState.wave, int(combat_report.bounties[team])) report["leaks"] = combat_report.leaks[team] report["core_damage"] = _wave_core_damage[team] report["combat_seconds"] = combat_report.elapsed report["natural_count"] = DataCatalog.get_attackers(GameState.wave).size() report["natural_leaks"] = combat_report.get("natural_leaks", combat_report.leaks)[team] report["front_lifetime"] = combat_report.get("front_lifetime", [90.0, 90.0])[team] reports.append(report) GameState.last_reports = reports wave_settled.emit(reports) if GameState.wave >= DataCatalog.wave_count(): _finish_match(GameState.decide_winner(true)) else: remaining = RESULTS_DURATION GameState.set_phase(GameState.Phase.RESULTS) updated.emit() func _next_wave() -> void: if GameState.phase != GameState.Phase.RESULTS: return GameState.wave += 1 _open_build() func _finish_match(winner: int) -> void: GameState.outcome = winner GameState.paused = false remaining = 0.0 GameState.set_phase(GameState.Phase.GAME_OVER) updated.emit() func rival_snapshot() -> Dictionary: return _public_snapshots[1].duplicate(true)