mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
105 lines
4.2 KiB
GDScript
105 lines
4.2 KiB
GDScript
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 season_ends_at_unix := 0
|
|
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 payload["ranked_games"] is float) 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 not _valid_nonnegative_integer(payload["ranked_games"]) or next_rating < 0.0 or next_rd < 0.0 or next_volatility < 0.0 or next_games < 0 or not _valid_tier(next_tier):
|
|
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 = ""
|
|
if payload.has("season_id"):
|
|
if not payload["season_id"] is String or not is_valid_opaque_id(String(payload["season_id"])):
|
|
return _reject("Profile response contains invalid season identifier")
|
|
season_id = String(payload["season_id"])
|
|
season_ends_at_unix = 0
|
|
if payload.has("season_ends_at"):
|
|
if not payload["season_ends_at"] is String or not is_valid_season_timestamp(String(payload["season_ends_at"])):
|
|
return _reject("Profile response contains invalid season expiry")
|
|
var parsed_season_end := Time.get_unix_time_from_datetime_string(String(payload["season_ends_at"]))
|
|
if parsed_season_end < 0:
|
|
return _reject("Profile response contains invalid season expiry")
|
|
season_ends_at_unix = int(parsed_season_end)
|
|
available = true
|
|
error_message = ""
|
|
return true
|
|
|
|
|
|
static func is_valid_season_timestamp(value: String) -> bool:
|
|
if value.is_empty():
|
|
return false
|
|
var timestamp_pattern := RegEx.create_from_string("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$")
|
|
return timestamp_pattern.search(value) != null
|
|
|
|
|
|
static func is_valid_opaque_id(value: String) -> bool:
|
|
if value.length() < 16 or value.length() > 128:
|
|
return false
|
|
var id_pattern := RegEx.create_from_string("^[A-Za-z0-9_-]+$")
|
|
return id_pattern.search(value) != null
|
|
|
|
|
|
static func _valid_nonnegative_integer(value: Variant) -> bool:
|
|
if value is int:
|
|
return int(value) >= 0
|
|
if value is float:
|
|
return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value))
|
|
return false
|
|
|
|
|
|
static func _valid_tier(value: String) -> bool:
|
|
return value in ["PROVISIONAL", "BRONZE", "SILVER", "GOLD", "PLATINUM", "DIAMOND"]
|
|
|
|
|
|
func set_error(reason: String) -> void:
|
|
available = false
|
|
error_message = reason
|
|
|
|
|
|
func display_text(now_unix: int = -1) -> String:
|
|
if not available:
|
|
return error_message if not error_message.is_empty() else "Ranked profile unavailable"
|
|
var status := "Provisional" if provisional else tier
|
|
var text := "%s · %d ranked game%s" % [status, ranked_games, "" if ranked_games == 1 else "s"]
|
|
if season_ends_at_unix > 0:
|
|
var current_unix := int(Time.get_unix_time_from_system()) if now_unix < 0 else now_unix
|
|
var remaining_days := maxi(0, int(ceil(float(season_ends_at_unix - current_unix) / 86400.0)))
|
|
text += " · Season ends in %dd" % remaining_days
|
|
return text
|
|
|
|
|
|
func _reject(reason: String) -> bool:
|
|
available = false
|
|
error_message = reason
|
|
return false
|