extends Node class_name RoundManager enum RoundState { WARMUP, FREEZE_TIME, ACTIVE, ROUND_END } signal round_state_changed(new_state: RoundState) signal round_started(round_number: int) signal round_ended(winner: TeamManager.Team) signal player_killed(victim_id: int, killer_id: int, weapon_id: int, kill_tick: int) @export var freeze_time: float = 5.0 @export var round_time: float = 115.0 @export var round_end_time: float = 5.0 @export var max_rounds: int = 24 @export var rounds_to_win: int = 13 var state: RoundState = RoundState.WARMUP var timer: float = 0.0 var round_number: int = 0 var score: Dictionary = { TeamManager.Team.T: 0, TeamManager.Team.CT: 0 } var freeze_end_tick: int = -1 var active_end_tick: int = -1 var round_end_tick: int = -1 # Tracking alive players per round (synced to clients via RPC) var _alive_players: Dictionary = {} # peer_id -> bool var _last_winner: TeamManager.Team = TeamManager.Team.NONE var team_manager: TeamManager var bomb: Bomb func _ready(): team_manager = $"../TeamManager" bomb = get_tree().current_scene.get_node_or_null("Network/Bomb") set_process(false) NetworkEvents.on_peer_leave.connect(_handle_peer_leave) func start_match(): if not multiplayer.is_server(): return round_number = 0 score = { TeamManager.Team.T: 0, TeamManager.Team.CT: 0 } _sync_score.rpc(score[TeamManager.Team.T], score[TeamManager.Team.CT]) start_round() func start_round(): if not multiplayer.is_server(): return round_number += 1 freeze_end_tick = NetworkTime.tick + int(freeze_time * float(NetworkTime.tickrate)) active_end_tick = freeze_end_tick + int(round_time * float(NetworkTime.tickrate)) round_end_tick = -1 _mark_all_alive() _change_state(RoundState.FREEZE_TIME, freeze_time) _sync_round_start.rpc(round_number, freeze_end_tick, active_end_tick) func _process(delta: float): # Local timer countdown for HUD display only if timer > 0: timer -= delta # Only server handles state transitions if not multiplayer.is_server(): return var current_tick := NetworkTime.tick # Tick-based transitions for gameplay-critical boundaries if state == RoundState.FREEZE_TIME and current_tick >= freeze_end_tick: _change_state(RoundState.ACTIVE, round_time) elif state == RoundState.ACTIVE and current_tick >= active_end_tick: # Time ran out — CTs win (only if bomb not planted) if bomb and (bomb.state == Bomb.BombState.PLANTED or bomb.state == Bomb.BombState.DEFUSING): pass # Round continues — bomb is ticking else: end_round(TeamManager.Team.CT) elif state == RoundState.ROUND_END and timer <= 0: if _check_match_over(): return start_round() func report_kill(victim_id: int, killer_id: int, weapon_id: int, kill_tick: int = -1): if not multiplayer.is_server(): return var check_tick := kill_tick if kill_tick >= 0 else NetworkTime.tick if round_end_tick >= 0 and check_tick > round_end_tick: return if check_tick < freeze_end_tick: return if check_tick >= active_end_tick: return _alive_players[victim_id] = false _sync_player_alive.rpc(victim_id, false) # Emit kill event for economy, stats, kill-feed player_killed.emit(victim_id, killer_id, weapon_id, check_tick) # Check if a whole team is eliminated var t_alive := _count_alive(TeamManager.Team.T) var ct_alive := _count_alive(TeamManager.Team.CT) var bomb_planted := bomb and (bomb.state == Bomb.BombState.PLANTED or bomb.state == Bomb.BombState.DEFUSING) if ct_alive == 0: end_round(TeamManager.Team.T) elif t_alive == 0 and not bomb_planted: # All Ts dead + bomb NOT planted → CT win end_round(TeamManager.Team.CT) elif t_alive == 0 and bomb_planted: pass # Round continues — bomb is planted func end_round(winner: TeamManager.Team): if not multiplayer.is_server(): return if round_end_tick >= 0: return round_end_tick = NetworkTime.tick _last_winner = winner score[winner] += 1 _sync_score.rpc(score[TeamManager.Team.T], score[TeamManager.Team.CT]) _sync_round_end.rpc(winner, round_end_tick) _change_state(RoundState.ROUND_END, round_end_time) round_ended.emit(winner) func is_player_alive(peer_id: int) -> bool: return _alive_players.get(peer_id, false) func sync_to_peer(peer_id: int): _sync_state.rpc_id(peer_id, state, timer) _sync_score.rpc_id(peer_id, score[TeamManager.Team.T], score[TeamManager.Team.CT]) _sync_round_start.rpc_id(peer_id, round_number, freeze_end_tick, active_end_tick) for pid in _alive_players: _sync_player_alive.rpc_id(peer_id, pid, _alive_players[pid]) if state == RoundState.ROUND_END and _last_winner != TeamManager.Team.NONE: _sync_round_end.rpc_id(peer_id, _last_winner, round_end_tick) func _count_alive(team: TeamManager.Team) -> int: var count := 0 for peer_id in team_manager.get_team_members(team): if _alive_players.get(peer_id, false): count += 1 return count func _mark_all_alive(): _alive_players.clear() for peer_id in team_manager.assignments: if team_manager.get_team(peer_id) != TeamManager.Team.NONE: _alive_players[peer_id] = true for pid in _alive_players: _sync_player_alive.rpc(pid, _alive_players[pid]) func _change_state(new_state: RoundState, duration: float): state = new_state timer = duration set_process(true) _sync_state.rpc(new_state, duration) round_state_changed.emit(new_state) func _handle_peer_leave(peer_id: int) -> void: _alive_players.erase(peer_id) func _check_match_over() -> bool: if score[TeamManager.Team.T] >= rounds_to_win or score[TeamManager.Team.CT] >= rounds_to_win: return true if round_number >= max_rounds: return true return false # --- RPCs: Server -> All Clients --- @rpc("authority", "call_local", "reliable") func _sync_state(new_state: int, duration: float) -> void: state = new_state as RoundState timer = duration set_process(true) round_state_changed.emit(state) @rpc("authority", "call_local", "reliable") func _sync_score(t_score: int, ct_score: int) -> void: score[TeamManager.Team.T] = t_score score[TeamManager.Team.CT] = ct_score @rpc("authority", "call_local", "reliable") func _sync_round_start(round_num: int, _freeze_end: int, _active_end: int) -> void: round_number = round_num freeze_end_tick = _freeze_end active_end_tick = _active_end round_end_tick = -1 round_started.emit(round_number) @rpc("authority", "call_local", "reliable") func _sync_round_end(winner: int, _round_end_tick: int) -> void: round_end_tick = _round_end_tick round_ended.emit(winner as TeamManager.Team) @rpc("authority", "call_local", "reliable") func _sync_player_alive(peer_id: int, alive: bool) -> void: _alive_players[peer_id] = alive