mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat: add matchmaking queue UI
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
extends Control
|
||||
|
||||
const CLIENT_BUILD := "dev"
|
||||
const PROTOCOL_VERSION := 1
|
||||
const HEARTBEAT_SECONDS := 10.0
|
||||
const RECOVERY_POLL_SECONDS := 2.0
|
||||
|
||||
@onready var playlist_dropdown: OptionButton = %PlaylistDropdown
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
@onready var detail_label: Label = %DetailLabel
|
||||
@onready var queue_button: Button = %QueueButton
|
||||
@onready var cancel_button: Button = %CancelButton
|
||||
@onready var accept_button: Button = %AcceptButton
|
||||
@onready var decline_button: Button = %DeclineButton
|
||||
@onready var back_button: Button = %BackButton
|
||||
|
||||
var _elapsed_seconds := 0.0
|
||||
var _heartbeat_seconds := 0.0
|
||||
var _recovery_poll_seconds := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
playlist_dropdown.add_item("Casual")
|
||||
playlist_dropdown.set_item_metadata(0, "casual")
|
||||
playlist_dropdown.add_item("Ranked")
|
||||
playlist_dropdown.set_item_metadata(1, "ranked")
|
||||
ControlPlaneClient.state.changed.connect(_on_state_changed)
|
||||
ControlPlaneClient.request_failed.connect(_on_request_failed)
|
||||
ControlPlaneClient.request_succeeded.connect(_on_request_succeeded)
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if ControlPlaneClient.state.phase in [MatchmakingState.QUEUED, MatchmakingState.PROPOSED, MatchmakingState.ALLOCATING]:
|
||||
_elapsed_seconds += delta
|
||||
_heartbeat_seconds += delta
|
||||
_recovery_poll_seconds += delta
|
||||
if _recovery_poll_seconds >= RECOVERY_POLL_SECONDS:
|
||||
_recovery_poll_seconds = 0.0
|
||||
var recovery_err := ControlPlaneClient.recover_queue(ControlPlaneClient.state.ticket_id)
|
||||
if recovery_err != OK and recovery_err != ERR_BUSY:
|
||||
_on_local_error("State recovery unavailable: %s" % error_string(recovery_err))
|
||||
if ControlPlaneClient.state.phase == MatchmakingState.QUEUED and _heartbeat_seconds >= HEARTBEAT_SECONDS:
|
||||
_heartbeat_seconds = 0.0
|
||||
var err := ControlPlaneClient.heartbeat(ControlPlaneClient.state.ticket_id, ControlPlaneClient.state.revision)
|
||||
if err != OK:
|
||||
_on_local_error("Heartbeat unavailable: %s" % error_string(err))
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
func _on_queue_pressed() -> void:
|
||||
if not _can_start_new_search(ControlPlaneClient.state.phase):
|
||||
return
|
||||
_elapsed_seconds = 0.0
|
||||
_heartbeat_seconds = 0.0
|
||||
_recovery_poll_seconds = 0.0
|
||||
var playlist := String(playlist_dropdown.get_selected_metadata())
|
||||
var ticket_id := "ticket-%s-%s" % [str(Time.get_ticks_usec()), str(randi())]
|
||||
var err := ControlPlaneClient.queue_create(ticket_id, playlist, CLIENT_BUILD, PROTOCOL_VERSION)
|
||||
if err != OK:
|
||||
_on_local_error("Could not start matchmaking: %s" % error_string(err))
|
||||
|
||||
|
||||
func _on_cancel_pressed() -> void:
|
||||
if not ControlPlaneClient.state.can_cancel():
|
||||
return
|
||||
var err := ControlPlaneClient.cancel_queue(ControlPlaneClient.state.ticket_id, ControlPlaneClient.state.revision)
|
||||
if err != OK:
|
||||
_on_local_error("Could not cancel matchmaking: %s" % error_string(err))
|
||||
|
||||
|
||||
func _on_accept_pressed() -> void:
|
||||
var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, true, ControlPlaneClient.state.proposal_revision)
|
||||
if err != OK:
|
||||
_on_local_error("Could not accept proposal: %s" % error_string(err))
|
||||
|
||||
|
||||
func _on_decline_pressed() -> void:
|
||||
var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, false, ControlPlaneClient.state.proposal_revision)
|
||||
if err != OK:
|
||||
_on_local_error("Could not decline proposal: %s" % error_string(err))
|
||||
|
||||
|
||||
func _on_back_pressed() -> void:
|
||||
if ControlPlaneClient.state.can_cancel():
|
||||
status_label.text = "Cancel the active search before leaving"
|
||||
return
|
||||
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
|
||||
|
||||
|
||||
func _on_state_changed(snapshot: Dictionary) -> void:
|
||||
_render(snapshot)
|
||||
|
||||
|
||||
func _on_request_succeeded(_operation: String, _payload: Dictionary) -> void:
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
func _on_request_failed(_operation: String, _http_code: int, detail: String) -> void:
|
||||
detail_label.text = detail
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
func _on_local_error(detail: String) -> void:
|
||||
detail_label.text = detail
|
||||
|
||||
|
||||
static func phase_label(phase: String) -> String:
|
||||
match phase:
|
||||
MatchmakingState.IDLE:
|
||||
return "Ready to search"
|
||||
MatchmakingState.QUEUED:
|
||||
return "Searching for players"
|
||||
MatchmakingState.PROPOSED:
|
||||
return "Match found — confirm"
|
||||
MatchmakingState.ALLOCATING:
|
||||
return "Preparing match server"
|
||||
MatchmakingState.PROCESS_READY:
|
||||
return "Match server started"
|
||||
MatchmakingState.ASSIGNMENT_READY:
|
||||
return "Match assigned"
|
||||
MatchmakingState.CONNECTING:
|
||||
return "Connecting to match"
|
||||
MatchmakingState.LIVE:
|
||||
return "Match in progress"
|
||||
MatchmakingState.CANCELLED:
|
||||
return "Search cancelled"
|
||||
MatchmakingState.EXPIRED:
|
||||
return "Search expired"
|
||||
MatchmakingState.FAILED:
|
||||
return "Matchmaking unavailable"
|
||||
_:
|
||||
return "Recovering matchmaking state"
|
||||
|
||||
|
||||
func _render(snapshot: Dictionary) -> void:
|
||||
var phase := String(snapshot.get("phase", MatchmakingState.IDLE))
|
||||
status_label.text = phase_label(phase)
|
||||
if String(snapshot.get("message", "")) != "":
|
||||
detail_label.text = String(snapshot["message"])
|
||||
elif phase == MatchmakingState.QUEUED:
|
||||
detail_label.text = "Elapsed %.0fs · revision %d" % [_elapsed_seconds, int(snapshot.get("revision", 0))]
|
||||
elif phase == MatchmakingState.PROPOSED:
|
||||
detail_label.text = "Review the proposal before the countdown expires"
|
||||
elif phase == MatchmakingState.IDLE:
|
||||
detail_label.text = "Choose a playlist to begin"
|
||||
cancel_button.visible = ControlPlaneClient.state.can_cancel()
|
||||
accept_button.visible = phase == MatchmakingState.PROPOSED
|
||||
decline_button.visible = phase == MatchmakingState.PROPOSED
|
||||
queue_button.disabled = not _can_start_new_search(phase)
|
||||
|
||||
|
||||
static func _is_terminal(phase: String) -> bool:
|
||||
return phase in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED, MatchmakingState.LIVE]
|
||||
|
||||
|
||||
static func _can_start_new_search(phase: String) -> bool:
|
||||
return phase == MatchmakingState.IDLE or phase in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED]
|
||||
Reference in New Issue
Block a user