extends Node # GoalsCycleManager - Handles 60-second goal cycles, scoring, and goal regeneration const CYCLE_DURATION: float = 60.0 const BASE_SCORE: int = 100 const TIME_BONUS_MULTIPLIER: float = 2.0 # Timer state var current_cycle_timer: float = 0.0 var is_cycle_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) func _ready(): set_process(false) func initialize(main: Node): main_scene = main func _process(delta): 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) # ============================================================================= # 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(): rpc("sync_cycle_start") @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 set_process(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") # Start new cycle after a brief delay await get_tree().create_timer(2.0).timeout start_cycle() @rpc("authority", "call_local", "reliable") func sync_cycle_end(): is_cycle_active = false set_process(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]) # 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) player.rpc("sync_playerboard", 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 player.rpc("sync_goals", 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()