Files
tekton/scripts/managers/goals_cycle_manager.gd
T

372 lines
11 KiB
GDScript

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()