## RoundReplicator — Client-side round state synced from RoundManager Self-RPCs. ## ## The server's round_manager signals arrive on clients via netfox Self-RPC ## (@rpc with call_local). This replicator listens to those signals and ## maintains a local round state snapshot for rollback-accessible use. ## ## This script is ONLY loaded from client scenes. extends Node # --------------------------------------------------------------------------- # Signals (mirrors server RoundManager for client-side consumers) # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Round state (mirrors server RoundManager) # --------------------------------------------------------------------------- enum RoundState { INACTIVE = 0, WAITING_FOR_PLAYERS = 1, WARMUP = 2, LIVE = 3, POST_ROUND = 4, MATCH_END = 5, } # Current round state (populated from server broadcasts) var round_state: int = RoundState.INACTIVE var round_number: int = 0 var team_a_score: int = 0 var team_b_score: int = 0 var time_remaining: float = 0.0 var round_time_seconds: float = 600.0 var win_limit: int = 3 # --------------------------------------------------------------------------- # Rollback-friendly state (used by rollback_tick in player scripts) # --------------------------------------------------------------------------- ## Whether the round is in freeze time (before round start). var is_frozen: bool = false ## The network tick when freeze ends (for rollback checks). var freeze_end_tick: int = -1 # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: # Connect to RoundManager signals (which now arrive via Self-RPC) if Engine.has_singleton(&"RoundManager"): var rm = Engine.get_singleton(&"RoundManager") if rm.round_started.is_connected(_on_round_start): return # Already connected rm.round_started.connect(_on_round_start) rm.round_ended.connect(_on_round_end) rm.match_ended.connect(_on_match_end) rm.score_changed.connect(_on_score_change) else: push_warning("[RoundReplicator] RoundManager not available") func _on_connected() -> void: print("[RoundReplicator] Connected to server — awaiting round state") # --------------------------------------------------------------------------- # Signal handlers # --------------------------------------------------------------------------- func _on_round_start(round_num: int) -> void: round_number = round_num # Read round_time_seconds from RoundManager (replicated via _rpc_round_started) if Engine.has_singleton(&"RoundManager"): var rm = Engine.get_singleton(&"RoundManager") round_time_seconds = rm.round_time_seconds if rm.round_time_seconds > 0 else 600 time_remaining = round_time_seconds round_state = RoundState.LIVE # Freeze time handling (10 seconds for first round, 5 for subsequent) var freeze_time: float = 10.0 if round_num <= 1 else 5.0 is_frozen = true var nt = Engine.get_singleton(&"NetworkTime") if nt: freeze_end_tick = nt.tick + int(freeze_time * 64.0) # 64 tick-rate assumption else: freeze_end_tick = -1 print("[RoundReplicator] Round %d started (%.0fs, frozen for %.0fs)" % [round_num, round_time_seconds, freeze_time]) func _on_round_end(winner_team: int, reason: String) -> void: # Read scores from RoundManager (replicated by Self-RPC) if Engine.has_singleton(&"RoundManager"): var rm = Engine.get_singleton(&"RoundManager") team_a_score = rm.team_a_score team_b_score = rm.team_b_score is_frozen = false round_state = RoundState.POST_ROUND print("[RoundReplicator] Round ended: %s wins (%s). Score: %d-%d" % [_team_name(winner_team), reason, team_a_score, team_b_score]) func _on_match_end(winner_team: int) -> void: # Read scores from RoundManager if Engine.has_singleton(&"RoundManager"): var rm = Engine.get_singleton(&"RoundManager") team_a_score = rm.team_a_score team_b_score = rm.team_b_score round_state = RoundState.MATCH_END print("[RoundReplicator] MATCH OVER: %s wins %d-%d" % [_team_name(winner_team), team_a_score, team_b_score]) func _on_score_change(a_score: int, b_score: int) -> void: team_a_score = a_score team_b_score = b_score print("[RoundReplicator] Score updated: %d-%d" % [a_score, b_score]) func _process(delta: float) -> void: # Update time remaining (for HUD display) if round_state == RoundState.LIVE: time_remaining -= delta if time_remaining < 0.0: time_remaining = 0.0 # Update freeze state if is_frozen and freeze_end_tick >= 0: var nt = Engine.get_singleton(&"NetworkTime") if nt: if nt.tick >= freeze_end_tick: is_frozen = false # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- ## Get a snapshot of the current round state (for HUD/UI). func get_snapshot() -> Dictionary: return { "state": round_state, "round_number": round_number, "team_a_score": team_a_score, "team_b_score": team_b_score, "time_remaining": time_remaining, "is_frozen": is_frozen, } ## Check if the player can move (not frozen, not dead, round is LIVE). func can_player_move(player_is_alive: bool) -> bool: return player_is_alive and round_state == RoundState.LIVE and not is_frozen ## Check if the round is currently active (LIVE or WARMUP). func is_round_active() -> bool: return round_state == RoundState.LIVE or round_state == RoundState.WARMUP ## Get the human-readable team name. static func _team_name(team: int) -> String: match team: 0: return "Draw" 1: return "Team A" 2: return "Team B" _: return "Unknown"