443 lines
14 KiB
GDScript
443 lines
14 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 = 1000
|
|
const TIME_BONUS_MULTIPLIER: float = 0.0 # User implied flat 1000, setting bonus to 0 effectively or I can just ignore it in calc.
|
|
|
|
# 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 = {}
|
|
var player_goal_counts: Dictionary = {} # peer_id -> count
|
|
|
|
# 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 goal_count_updated(peer_id: int, count: 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
|
|
if LobbyManager:
|
|
LobbyManager.enable_cycle_timer_changed.connect(_on_enable_cycle_timer_changed)
|
|
|
|
func _on_enable_cycle_timer_changed(enabled: bool):
|
|
# If disabled mid-cycle, the timer will just freeze in _process
|
|
# If enabled mid-cycle, it will resume
|
|
pass
|
|
|
|
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
|
|
|
|
# Skip countdown if timer is disabled
|
|
var lobby_manager = get_tree().get_root().get_node_or_null("Main/LobbyManager")
|
|
# Note: LobbyManager is an Autoload, so we can access it directly via 'LobbyManager'
|
|
|
|
if LobbyManager and not LobbyManager.get_enable_cycle_timer():
|
|
# If timer is disabled, we just don't decrement.
|
|
# But we still keep is_cycle_active = true so the phase is "active" (allowing actions)
|
|
# just without the clock ticking down.
|
|
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
|
|
|
|
if multiplayer.is_server():
|
|
# FINAL SCORING: Process any points remaining on board
|
|
_process_cycle_end_for_all_players()
|
|
# Sync final scores THEN end match on clients
|
|
rpc("sync_final_scores")
|
|
else:
|
|
emit_signal("match_ended")
|
|
|
|
@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_final_scores():
|
|
"""Called by server at match end. Signals clients to stop."""
|
|
is_match_active = false
|
|
is_cycle_active = false
|
|
|
|
# Request final leaderboard refresh
|
|
_update_leaderboard()
|
|
|
|
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:
|
|
# Use name.to_int() for ID because bots share authority 1 but have unique node names
|
|
var peer_id = player.name.to_int()
|
|
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
|
|
|
|
# Use name.to_int() for ID because bots share authority 1
|
|
var peer_id = player.name.to_int()
|
|
|
|
# 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
|
|
|
|
# Update goal count
|
|
if not player_goal_counts.has(peer_id):
|
|
player_goal_counts[peer_id] = 0
|
|
player_goal_counts[peer_id] += 1
|
|
|
|
emit_signal("score_updated", peer_id, player_scores[peer_id])
|
|
emit_signal("goal_count_updated", peer_id, player_goal_counts[peer_id])
|
|
_update_leaderboard()
|
|
|
|
# Sync score to all clients
|
|
rpc("sync_player_score", peer_id, player_scores[peer_id])
|
|
rpc("sync_goal_count", peer_id, player_goal_counts[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()
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func sync_goal_count(peer_id: int, count: int):
|
|
player_goal_counts[peer_id] = count
|
|
emit_signal("goal_count_updated", peer_id, count)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func request_add_score(amount: int):
|
|
"""RPC for clients to request score addition (trusted)."""
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
var sender_id = multiplayer.get_remote_sender_id()
|
|
# If called locally by server, sender_id might be 0 or 1.
|
|
if sender_id == 0: sender_id = 1
|
|
|
|
add_score(sender_id, amount)
|
|
|
|
func add_score(peer_id: int, amount: int):
|
|
"""Add points to a specific player (Server only)."""
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
if not player_scores.has(peer_id):
|
|
player_scores[peer_id] = 0
|
|
|
|
player_scores[peer_id] += amount
|
|
|
|
# Sync
|
|
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
|
rpc("sync_player_score", peer_id, player_scores[peer_id])
|
|
print("[GoalsCycle] Added %d points to Player %d. Total: %d" % [amount, peer_id, player_scores[peer_id]])
|
|
|
|
|
|
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:
|
|
# Use name.to_int() for ID because bots share authority 1
|
|
var peer_id = player.name.to_int()
|
|
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.name.to_int()
|
|
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()
|