extends Node # GoalsCycleManager - Handles 60-second goal cycles, scoring, and goal regeneration # Also handles global match timer that ends the game const CYCLE_DURATION: float = 30.0 const BASE_SCORE: int = 100 const TIME_BONUS_MULTIPLIER: float = 2.0 # Cycle timer state (30-second cycles) var current_cycle_timer: float = 0.0 var is_cycle_active: bool = false # Global match timer state var global_match_timer: float = 0.0 var match_duration: float = 180.0 # Default 3 minutes var is_match_active: bool = false # Score tracking: peer_id -> score var player_scores: Dictionary = {} # Reference to main scene var main_scene: Node = null signal cycle_started() signal cycle_ended() signal timer_updated(time_remaining: float) signal score_updated(peer_id: int, new_score: int) signal leaderboard_updated(sorted_scores: Array) # Global match signals signal match_started() signal match_ended() signal global_timer_updated(time_remaining: float) func _ready(): set_process(false) func initialize(main: Node): main_scene = main func _process(delta): # Update global match timer if active if is_match_active: global_match_timer -= delta if global_match_timer <= 0: global_match_timer = 0 _on_match_end() else: emit_signal("global_timer_updated", global_match_timer) # Server broadcasts global timer sync every second if multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta): rpc("sync_global_timer", global_match_timer) # Update cycle timer if cycle is active if not is_cycle_active: return current_cycle_timer -= delta if current_cycle_timer <= 0: current_cycle_timer = 0 _on_cycle_end() else: emit_signal("timer_updated", current_cycle_timer) # Server broadcasts timer sync every second if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): rpc("sync_timer", current_cycle_timer) # ============================================================================= # Global Match Control # ============================================================================= func start_match(duration_seconds: float): """Start the global match timer. Called by server when game starts.""" match_duration = duration_seconds global_match_timer = duration_seconds is_match_active = true set_process(true) emit_signal("match_started") if multiplayer.is_server(): rpc("sync_match_start", duration_seconds) # Also start the first cycle start_cycle() func _on_match_end(): """Called when global match timer reaches zero - game over!""" is_match_active = false is_cycle_active = false emit_signal("match_ended") if multiplayer.is_server(): rpc("sync_match_end") @rpc("authority", "call_local", "reliable") func sync_match_start(duration_seconds: float): match_duration = duration_seconds global_match_timer = duration_seconds is_match_active = true set_process(true) emit_signal("match_started") @rpc("authority", "call_local", "reliable") func sync_match_end(): is_match_active = false is_cycle_active = false emit_signal("match_ended") @rpc("authority", "call_local", "unreliable") func sync_global_timer(time_remaining: float): global_match_timer = time_remaining emit_signal("global_timer_updated", time_remaining) func get_global_time_remaining() -> float: return global_match_timer func is_match_running() -> bool: return is_match_active # ============================================================================= # Cycle Control # ============================================================================= func start_cycle(): current_cycle_timer = CYCLE_DURATION is_cycle_active = true set_process(true) emit_signal("cycle_started") if multiplayer.is_server(): # Initialize scores for all connected players _initialize_player_scores() rpc("sync_cycle_start") func _initialize_player_scores(): """Initialize scores for all connected players to 0.""" var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var peer_id = player.get_multiplayer_authority() if not player_scores.has(peer_id): player_scores[peer_id] = 0 _update_leaderboard() @rpc("authority", "call_local", "reliable") func sync_cycle_start(): current_cycle_timer = CYCLE_DURATION is_cycle_active = true set_process(true) emit_signal("cycle_started") @rpc("authority", "call_local", "unreliable") func sync_timer(time_remaining: float): current_cycle_timer = time_remaining emit_signal("timer_updated", current_cycle_timer) func _on_cycle_end(): is_cycle_active = false emit_signal("cycle_ended") if multiplayer.is_server(): # Clear all playerboards and convert matches to score _process_cycle_end_for_all_players() rpc("sync_cycle_end") # Only start new cycle if match is still active if is_match_active: await get_tree().create_timer(2.0).timeout if is_match_active: # Check again in case match ended during delay start_cycle() @rpc("authority", "call_local", "reliable") func sync_cycle_end(): is_cycle_active = false emit_signal("cycle_ended") # ============================================================================= # Goal Completion & Scoring # ============================================================================= func on_goal_completed(player: Node, time_remaining: float): """Called when a player completes their goal pattern.""" if not multiplayer.is_server(): return var peer_id = player.get_multiplayer_authority() # Calculate score: base + time bonus var time_bonus = int(time_remaining * TIME_BONUS_MULTIPLIER) var score_earned = BASE_SCORE + time_bonus # Update player score if not player_scores.has(peer_id): player_scores[peer_id] = 0 player_scores[peer_id] += score_earned emit_signal("score_updated", peer_id, player_scores[peer_id]) _update_leaderboard() # Sync score to all clients rpc("sync_player_score", peer_id, player_scores[peer_id]) # Clear playerboard tiles (they convert to powerup bar reward) player.playerboard.fill(-1) # Use main scene's RPC which properly looks up player by ID on each client if main_scene: main_scene.rpc("sync_playerboard", peer_id, player.playerboard) # Regenerate goals for this player regenerate_goals_for_player(player) # Randomize 9 tiles around player _randomize_tiles_around_player(player) print("[GoalsCycle] Player %d completed goal! +%d points (base: %d, time bonus: %d)" % [peer_id, score_earned, BASE_SCORE, time_bonus]) @rpc("authority", "call_local", "reliable") func sync_player_score(peer_id: int, total_score: int): player_scores[peer_id] = total_score emit_signal("score_updated", peer_id, total_score) _update_leaderboard() func _update_leaderboard(): # Sort players by score (descending) var sorted_scores = [] for peer_id in player_scores.keys(): sorted_scores.append({"peer_id": peer_id, "score": player_scores[peer_id]}) sorted_scores.sort_custom(func(a, b): return a.score > b.score) emit_signal("leaderboard_updated", sorted_scores) # ============================================================================= # Cycle End Processing # ============================================================================= func _process_cycle_end_for_all_players(): """Server-side: Clear playerboards and convert matching tiles to score.""" var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var peer_id = player.get_multiplayer_authority() var match_score = _calculate_match_score(player) if match_score > 0: if not player_scores.has(peer_id): player_scores[peer_id] = 0 player_scores[peer_id] += match_score rpc("sync_player_score", peer_id, player_scores[peer_id]) # Clear playerboard player.playerboard.fill(-1) # Use main scene's RPC which properly looks up player by ID if main_scene: main_scene.rpc("sync_playerboard", peer_id, player.playerboard) # Generate new goals regenerate_goals_for_player(player) _update_leaderboard() func _calculate_match_score(player: Node) -> int: """Calculate score from matching tiles in playerboard to goals.""" var matching_tiles = 0 var goals = player.goals var playerboard = player.playerboard # Check center 3x3 of playerboard against goals for i in range(3): for j in range(3): var goal_idx = i * 3 + j var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board if goal_idx < goals.size() and board_idx < playerboard.size(): if goals[goal_idx] != -1 and playerboard[board_idx] == goals[goal_idx]: matching_tiles += 1 # 10 points per matching tile return matching_tiles * 10 # ============================================================================= # Goal Regeneration # ============================================================================= func regenerate_goals_for_player(player: Node): """Generate new random goals for a player.""" if not multiplayer.is_server(): return var new_goals = GoalManager.initialize_random_goals(9, 7, 10, 1.0) var int_goals: Array[int] = [] for g in new_goals: int_goals.append(g) player.goals = int_goals # Use main scene's RPC which properly looks up player by ID on each client var peer_id = player.get_multiplayer_authority() if main_scene: main_scene.rpc("sync_player_goals", peer_id, int_goals) # ============================================================================= # Tile Randomization # ============================================================================= func _randomize_tiles_around_player(player: Node): """Randomize 9 tiles in 3x3 area around player position.""" if not main_scene: return var enhanced_gridmap = main_scene.get_node_or_null("EnhancedGridMap") if not enhanced_gridmap: return var center = player.current_position var rng = RandomNumberGenerator.new() rng.randomize() # 3x3 area around player for dx in range(-1, 2): for dz in range(-1, 2): var pos = Vector2i(center.x + dx, center.y + dz) var cell = Vector3i(pos.x, 1, pos.y) # Check if position is valid if not enhanced_gridmap.is_position_valid(pos): continue # Check if there are tiles nearby or if empty var current_item = enhanced_gridmap.get_cell_item(cell) # Decide: delete, spawn, or randomize var action = rng.randi() % 3 match action: 0: # Delete tile if current_item != -1: main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) 1: # Spawn new tile if current_item == -1: var new_tile = rng.randi_range(7, 10) main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile) 2: # Randomize existing if current_item != -1: var new_tile = rng.randi_range(7, 10) while new_tile == current_item: new_tile = rng.randi_range(7, 10) main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile) # ============================================================================= # Score Getters # ============================================================================= func get_player_score(peer_id: int) -> int: return player_scores.get(peer_id, 0) func get_leaderboard() -> Array: var sorted_scores = [] for peer_id in player_scores.keys(): sorted_scores.append({"peer_id": peer_id, "score": player_scores[peer_id]}) sorted_scores.sort_custom(func(a, b): return a.score > b.score) return sorted_scores func get_time_remaining() -> float: return current_cycle_timer func reset_scores(): player_scores.clear() _update_leaderboard()