mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat: display backend ranked profile
This commit is contained in:
@@ -13,6 +13,7 @@ const PERSIST_PATH := "user://matchmaking_state.cfg"
|
||||
var base_url := DEFAULT_BASE_URL
|
||||
var access_token := ""
|
||||
var state: MatchmakingState
|
||||
var ranked_profile: RankedProfileState
|
||||
|
||||
var _request: HTTPRequest
|
||||
var _operation := ""
|
||||
@@ -20,6 +21,7 @@ var _operation := ""
|
||||
|
||||
func _ready() -> void:
|
||||
state = MatchmakingState.new()
|
||||
ranked_profile = RankedProfileState.new()
|
||||
_load_persisted_state()
|
||||
state.changed.connect(_persist_state)
|
||||
_request = HTTPRequest.new()
|
||||
@@ -61,6 +63,10 @@ func recover_proposal(proposal_id: String) -> Error:
|
||||
return _start_request("proposal_recover", HTTPClient.METHOD_GET, "/v1/proposals/" + proposal_id, {}, "")
|
||||
|
||||
|
||||
func fetch_ranked_profile() -> Error:
|
||||
return _start_request("ranked_profile", HTTPClient.METHOD_GET, "/v1/profile/ranked", {}, "")
|
||||
|
||||
|
||||
func heartbeat(ticket_id: String, expected_revision: int) -> Error:
|
||||
if ticket_id.is_empty() or expected_revision < 0:
|
||||
return ERR_INVALID_PARAMETER
|
||||
@@ -114,7 +120,9 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
var operation := _operation
|
||||
_operation = ""
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
if operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
if operation == "ranked_profile":
|
||||
ranked_profile.set_error("Ranked profile request failed")
|
||||
elif operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
state.fail("Control-plane request failed")
|
||||
else:
|
||||
state.set_notice("Control-plane request failed; retrying is safe")
|
||||
@@ -122,7 +130,9 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
return
|
||||
var parsed = JSON.parse_string(body.get_string_from_utf8())
|
||||
if not parsed is Dictionary:
|
||||
if operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
if operation == "ranked_profile":
|
||||
ranked_profile.set_error("Ranked profile returned invalid JSON")
|
||||
elif operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
state.fail("Control-plane returned invalid JSON")
|
||||
else:
|
||||
state.set_notice("Control-plane returned invalid JSON; retrying is safe")
|
||||
@@ -130,7 +140,9 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
return
|
||||
if response_code < 200 or response_code >= 300:
|
||||
var detail := String(parsed.get("error", "request rejected"))
|
||||
if operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
if operation == "ranked_profile":
|
||||
ranked_profile.set_error(detail)
|
||||
elif operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
state.fail(detail)
|
||||
else:
|
||||
state.set_notice(detail)
|
||||
@@ -143,6 +155,10 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
state.apply_ticket_update(normalize_ticket(payload))
|
||||
elif operation.begins_with("proposal_"):
|
||||
state.apply_proposal_update(payload)
|
||||
elif operation == "ranked_profile":
|
||||
if not ranked_profile.apply(payload):
|
||||
request_failed.emit(operation, response_code, ranked_profile.error_message)
|
||||
return
|
||||
request_succeeded.emit(operation, payload)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 ranked_profile_label: Label = %RankedProfileLabel
|
||||
@onready var queue_button: Button = %QueueButton
|
||||
@onready var cancel_button: Button = %CancelButton
|
||||
@onready var accept_button: Button = %AcceptButton
|
||||
@@ -24,9 +25,11 @@ func _ready() -> void:
|
||||
playlist_dropdown.set_item_metadata(0, "casual")
|
||||
playlist_dropdown.add_item("Ranked")
|
||||
playlist_dropdown.set_item_metadata(1, "ranked")
|
||||
playlist_dropdown.item_selected.connect(_on_playlist_selected)
|
||||
ControlPlaneClient.state.changed.connect(_on_state_changed)
|
||||
ControlPlaneClient.request_failed.connect(_on_request_failed)
|
||||
ControlPlaneClient.request_succeeded.connect(_on_request_succeeded)
|
||||
_refresh_ranked_profile()
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
@@ -81,6 +84,20 @@ func _on_decline_pressed() -> void:
|
||||
_on_local_error("Could not decline proposal: %s" % error_string(err))
|
||||
|
||||
|
||||
func _on_playlist_selected(_index: int) -> void:
|
||||
_refresh_ranked_profile()
|
||||
|
||||
|
||||
func _refresh_ranked_profile() -> void:
|
||||
var ranked := String(playlist_dropdown.get_selected_metadata()) == "ranked"
|
||||
ranked_profile_label.visible = ranked
|
||||
if not ranked:
|
||||
return
|
||||
var err := ControlPlaneClient.fetch_ranked_profile()
|
||||
if err != OK and err != ERR_BUSY:
|
||||
ranked_profile_label.text = "Ranked profile unavailable: %s" % error_string(err)
|
||||
|
||||
|
||||
func _on_back_pressed() -> void:
|
||||
if ControlPlaneClient.state.can_cancel():
|
||||
status_label.text = "Cancel the active search before leaving"
|
||||
@@ -93,11 +110,13 @@ func _on_state_changed(snapshot: Dictionary) -> void:
|
||||
|
||||
|
||||
func _on_request_succeeded(_operation: String, _payload: Dictionary) -> void:
|
||||
ranked_profile_label.text = ControlPlaneClient.ranked_profile.display_text()
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
func _on_request_failed(_operation: String, _http_code: int, detail: String) -> void:
|
||||
detail_label.text = detail
|
||||
ranked_profile_label.text = ControlPlaneClient.ranked_profile.display_text()
|
||||
_render(ControlPlaneClient.state.snapshot())
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
class_name RankedProfileState
|
||||
extends RefCounted
|
||||
|
||||
# Read-only server projection. The client deliberately stores no tier bands
|
||||
# or rating formula: it displays the backend's committed view verbatim after
|
||||
# validating the shape and numeric safety of the response.
|
||||
|
||||
var available := false
|
||||
var rating := 0.0
|
||||
var rd := 0.0
|
||||
var volatility := 0.0
|
||||
var ranked_games := 0
|
||||
var tier := ""
|
||||
var provisional := false
|
||||
var season_id := ""
|
||||
var error_message := ""
|
||||
|
||||
|
||||
func apply(payload: Dictionary) -> bool:
|
||||
var required := ["rating", "rd", "volatility", "ranked_games", "tier", "provisional"]
|
||||
for key in required:
|
||||
if not payload.has(key):
|
||||
return _reject("Profile response is missing " + key)
|
||||
if not (payload["rating"] is int or payload["rating"] is float) or not (payload["rd"] is int or payload["rd"] is float) or not (payload["volatility"] is int or payload["volatility"] is float) or not payload["ranked_games"] is int or not payload["tier"] is String or not payload["provisional"] is bool:
|
||||
return _reject("Profile response contains invalid types")
|
||||
var next_rating := float(payload["rating"])
|
||||
var next_rd := float(payload["rd"])
|
||||
var next_volatility := float(payload["volatility"])
|
||||
var next_games := int(payload["ranked_games"])
|
||||
var next_tier := String(payload["tier"])
|
||||
if not is_finite(next_rating) or not is_finite(next_rd) or not is_finite(next_volatility) or next_rating < 0.0 or next_rd < 0.0 or next_volatility < 0.0 or next_games < 0 or next_tier.is_empty():
|
||||
return _reject("Profile response contains invalid values")
|
||||
rating = next_rating
|
||||
rd = next_rd
|
||||
volatility = next_volatility
|
||||
ranked_games = next_games
|
||||
tier = next_tier
|
||||
provisional = bool(payload["provisional"])
|
||||
season_id = String(payload.get("season_id", ""))
|
||||
available = true
|
||||
error_message = ""
|
||||
return true
|
||||
|
||||
|
||||
func set_error(reason: String) -> void:
|
||||
available = false
|
||||
error_message = reason
|
||||
|
||||
|
||||
func display_text() -> String:
|
||||
if not available:
|
||||
return error_message if not error_message.is_empty() else "Ranked profile unavailable"
|
||||
var status := "Provisional" if provisional else tier
|
||||
return "%s · %d ranked game%s" % [status, ranked_games, "" if ranked_games == 1 else "s"]
|
||||
|
||||
|
||||
func _reject(reason: String) -> bool:
|
||||
available = false
|
||||
error_message = reason
|
||||
return false
|
||||
Reference in New Issue
Block a user