feat: Implement a new lobby system with configurable match duration, game over screen, and core game state management.
This commit is contained in:
@@ -40,6 +40,11 @@ func remove_bot(bot_id: int):
|
||||
players.erase(bot_id)
|
||||
emit_signal("game_state_changed")
|
||||
|
||||
func end_game():
|
||||
"""End the current game and prepare for return to lobby."""
|
||||
game_started_flag = false
|
||||
emit_signal("game_state_changed")
|
||||
|
||||
func reset():
|
||||
players.clear()
|
||||
bots.clear()
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
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
|
||||
|
||||
# Timer state
|
||||
# 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 = {}
|
||||
|
||||
@@ -22,6 +28,11 @@ 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)
|
||||
|
||||
@@ -29,6 +40,21 @@ 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
|
||||
|
||||
@@ -44,6 +70,57 @@ func _process(delta):
|
||||
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
|
||||
# =============================================================================
|
||||
@@ -82,7 +159,6 @@ func sync_timer(time_remaining: float):
|
||||
|
||||
func _on_cycle_end():
|
||||
is_cycle_active = false
|
||||
set_process(false)
|
||||
emit_signal("cycle_ended")
|
||||
|
||||
if multiplayer.is_server():
|
||||
@@ -90,14 +166,15 @@ func _on_cycle_end():
|
||||
_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()
|
||||
# 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
|
||||
set_process(false)
|
||||
emit_signal("cycle_ended")
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -11,6 +11,10 @@ signal player_left(player_id: int)
|
||||
signal ready_state_changed(player_id: int, is_ready: bool)
|
||||
signal all_players_ready()
|
||||
signal game_starting()
|
||||
signal match_duration_changed(duration_seconds: int)
|
||||
signal character_changed(player_id: int, character_name: String)
|
||||
signal area_changed(area_name: String)
|
||||
signal player_list_changed()
|
||||
|
||||
# Room data structure
|
||||
var current_room: Dictionary = {}
|
||||
@@ -19,6 +23,15 @@ var available_rooms: Array = []
|
||||
var is_host: bool = false
|
||||
var local_player_name: String = "Player"
|
||||
|
||||
# Match duration in seconds (configurable in lobby by host)
|
||||
var match_duration: int = 180 # Default 3 minutes
|
||||
|
||||
# Character and area selection
|
||||
var available_characters: Array[String] = ["Bob", "Gatot", "Masbro", "Oldpop"]
|
||||
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
|
||||
var selected_area: String = "Desert" # Host-controlled
|
||||
var local_character_index: int = 0 # Local player's character index
|
||||
|
||||
# Ready to start game check
|
||||
var _all_ready: bool = false
|
||||
|
||||
@@ -133,6 +146,101 @@ func is_all_ready() -> bool:
|
||||
# Game Start
|
||||
# =============================================================================
|
||||
|
||||
func set_match_duration(duration_seconds: int) -> void:
|
||||
"""Host sets match duration. Syncs to all clients."""
|
||||
match_duration = duration_seconds
|
||||
if is_host:
|
||||
rpc("sync_match_duration", duration_seconds)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_match_duration(duration_seconds: int) -> void:
|
||||
"""Sync match duration from host to clients."""
|
||||
match_duration = duration_seconds
|
||||
emit_signal("match_duration_changed", duration_seconds)
|
||||
|
||||
func get_match_duration() -> int:
|
||||
return match_duration
|
||||
|
||||
# =============================================================================
|
||||
# Character Selection
|
||||
# =============================================================================
|
||||
|
||||
func get_local_character() -> String:
|
||||
"""Get the local player's current character name."""
|
||||
return available_characters[local_character_index]
|
||||
|
||||
func set_character(character_name: String) -> void:
|
||||
"""Set local player's character. Syncs to all peers."""
|
||||
var idx = available_characters.find(character_name)
|
||||
if idx == -1:
|
||||
push_error("Invalid character: " + character_name)
|
||||
return
|
||||
|
||||
local_character_index = idx
|
||||
var my_id = multiplayer.get_unique_id()
|
||||
|
||||
# Update local player data
|
||||
for player in players_in_room:
|
||||
if player["id"] == my_id:
|
||||
player["character"] = character_name
|
||||
break
|
||||
|
||||
# Sync to all peers
|
||||
rpc("sync_character", my_id, character_name)
|
||||
|
||||
func cycle_character(direction: int) -> void:
|
||||
"""Cycle through available characters. direction: -1 for left, +1 for right."""
|
||||
local_character_index = wrapi(local_character_index + direction, 0, available_characters.size())
|
||||
set_character(available_characters[local_character_index])
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_character(player_id: int, character_name: String) -> void:
|
||||
"""Sync character selection across all clients."""
|
||||
for player in players_in_room:
|
||||
if player["id"] == player_id:
|
||||
player["character"] = character_name
|
||||
break
|
||||
|
||||
emit_signal("character_changed", player_id, character_name)
|
||||
emit_signal("player_list_changed")
|
||||
|
||||
# =============================================================================
|
||||
# Area Selection (Host Only)
|
||||
# =============================================================================
|
||||
|
||||
func get_selected_area() -> String:
|
||||
return selected_area
|
||||
|
||||
func get_area_index() -> int:
|
||||
return available_areas.find(selected_area)
|
||||
|
||||
func set_area(area_name: String) -> void:
|
||||
"""Host sets the game area. Syncs to all clients."""
|
||||
if not is_host:
|
||||
push_warning("Only host can change area")
|
||||
return
|
||||
|
||||
if area_name not in available_areas:
|
||||
push_error("Invalid area: " + area_name)
|
||||
return
|
||||
|
||||
selected_area = area_name
|
||||
rpc("sync_area", area_name)
|
||||
|
||||
func cycle_area(direction: int) -> void:
|
||||
"""Host cycles through available areas. direction: -1 for left, +1 for right."""
|
||||
if not is_host:
|
||||
return
|
||||
var current_idx = available_areas.find(selected_area)
|
||||
var new_idx = wrapi(current_idx + direction, 0, available_areas.size())
|
||||
set_area(available_areas[new_idx])
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_area(area_name: String) -> void:
|
||||
"""Sync area selection from host to clients."""
|
||||
selected_area = area_name
|
||||
emit_signal("area_changed", area_name)
|
||||
|
||||
func start_game() -> void:
|
||||
"""Host triggers game start (transitions all players to main.tscn)."""
|
||||
if not is_host:
|
||||
@@ -143,6 +251,9 @@ func start_game() -> void:
|
||||
push_error("Not all players are ready")
|
||||
return
|
||||
|
||||
# Sync match duration to all clients before starting
|
||||
rpc("sync_match_duration", match_duration)
|
||||
|
||||
# Notify all clients to start
|
||||
rpc("_on_game_starting")
|
||||
|
||||
@@ -168,7 +279,8 @@ func _on_match_joined(match_id: String) -> void:
|
||||
var my_data = {
|
||||
"id": my_id,
|
||||
"name": local_player_name,
|
||||
"is_ready": false
|
||||
"is_ready": false,
|
||||
"character": available_characters[local_character_index]
|
||||
}
|
||||
players_in_room.append(my_data)
|
||||
|
||||
@@ -202,7 +314,8 @@ func _on_peer_connected(peer_id: int) -> void:
|
||||
var new_player = {
|
||||
"id": peer_id,
|
||||
"name": "Player %d" % peer_id,
|
||||
"is_ready": false
|
||||
"is_ready": false,
|
||||
"character": available_characters[0]
|
||||
}
|
||||
players_in_room.append(new_player)
|
||||
|
||||
@@ -247,3 +360,6 @@ func reset() -> void:
|
||||
available_rooms.clear()
|
||||
is_host = false
|
||||
_all_ready = false
|
||||
match_duration = 180 # Reset to default 3 minutes
|
||||
selected_area = "Desert"
|
||||
local_character_index = 0
|
||||
|
||||
@@ -22,7 +22,7 @@ signal player_banned(player_id: String)
|
||||
|
||||
# Player data cache
|
||||
var players: Array = []
|
||||
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
||||
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
||||
var is_admin: bool = false
|
||||
var is_host: bool = false
|
||||
|
||||
@@ -77,7 +77,7 @@ func _rpc_call(rpc_name: String, payload: Dictionary) -> Dictionary:
|
||||
push_error("[AdminPanel] Not connected to Nakama")
|
||||
return {"error": "Not connected"}
|
||||
|
||||
var result := await NakamaManager.client.rpc_async(
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
rpc_name,
|
||||
JSON.stringify(payload)
|
||||
@@ -185,10 +185,10 @@ func _update_action_buttons() -> void:
|
||||
|
||||
var idx: int = selected[0]
|
||||
var meta: Dictionary = player_list.get_item_metadata(idx)
|
||||
var is_player_host := meta.get("peer_id", 0) == 1
|
||||
var is_player_host: bool = meta.get("peer_id", 0) == 1
|
||||
|
||||
# Can't kick/ban the host or yourself
|
||||
var is_self := meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
||||
var is_self: bool = meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
||||
|
||||
kick_btn.disabled = is_player_host or is_self
|
||||
ban_btn.disabled = is_player_host or is_self or not is_admin
|
||||
@@ -267,8 +267,8 @@ func _create_ban_dialog(user_id: String, player_name: String) -> ConfirmationDia
|
||||
|
||||
var duration_input := SpinBox.new()
|
||||
duration_input.min_value = 0
|
||||
duration_input.max_value = 8760 # 1 year
|
||||
duration_input.value = 24 # Default 24 hours
|
||||
duration_input.max_value = 8760 # 1 year
|
||||
duration_input.value = 24 # Default 24 hours
|
||||
vbox.add_child(duration_input)
|
||||
|
||||
dialog.add_child(vbox)
|
||||
|
||||
@@ -15,7 +15,7 @@ extends Control
|
||||
|
||||
var update_manager: Node
|
||||
var update_info: Dictionary = {}
|
||||
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
|
||||
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
|
||||
|
||||
func _ready() -> void:
|
||||
# Get or create the update manager
|
||||
@@ -49,7 +49,7 @@ func _get_update_manager() -> Node:
|
||||
|
||||
# Otherwise, create instance
|
||||
var manager_script := load("res://scripts/managers/game_update_manager.gd")
|
||||
var manager := manager_script.new()
|
||||
var manager: Node = manager_script.new()
|
||||
manager.name = "GameUpdateManager"
|
||||
get_tree().root.add_child(manager)
|
||||
return manager
|
||||
|
||||
Reference in New Issue
Block a user