Rough character selection screen + API
This commit is contained in:
@@ -24,6 +24,7 @@ bus_layout="res://audio/bus_layout.tres"
|
||||
MenuMusic="*res://scenes/UI/menu_music.tscn"
|
||||
MenuSfx="*res://scenes/UI/menu_sfx.tscn"
|
||||
AuthState="*res://scenes/UI/auth_state.gd"
|
||||
CharacterService="*res://scenes/UI/character_service.gd"
|
||||
|
||||
[dotnet]
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
extends Control
|
||||
|
||||
const AUTH_LOGOUT_URL := "https://pauth.ranaze.com/api/Auth/logout"
|
||||
|
||||
@onready var _status_label: Label = %StatusLabel
|
||||
@onready var _character_list: ItemList = %CharacterList
|
||||
@onready var _name_input: LineEdit = %NameInput
|
||||
@onready var _logout_request: HTTPRequest = %LogoutRequest
|
||||
|
||||
var _characters: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
if not AuthState.is_logged_in:
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
return
|
||||
_load_characters()
|
||||
|
||||
func _log_failure(action: String, response: Dictionary) -> void:
|
||||
push_warning("%s failed: status=%s error=%s body=%s" % [
|
||||
action,
|
||||
response.get("status", "n/a"),
|
||||
response.get("error", ""),
|
||||
response.get("body", "")
|
||||
])
|
||||
|
||||
func _load_characters() -> void:
|
||||
_status_label.text = "Loading characters..."
|
||||
var response := await CharacterService.list_characters()
|
||||
if not response.get("ok", false):
|
||||
_log_failure("List characters", response)
|
||||
_status_label.text = "Failed to load characters."
|
||||
return
|
||||
|
||||
var parsed: Variant = JSON.parse_string(String(response.get("body", "")))
|
||||
if typeof(parsed) != TYPE_ARRAY:
|
||||
_status_label.text = "Unexpected character response."
|
||||
return
|
||||
|
||||
_characters = parsed
|
||||
_character_list.clear()
|
||||
for character in _characters:
|
||||
var character_name := String(character.get("name", "Unnamed"))
|
||||
_character_list.add_item(character_name)
|
||||
|
||||
if _characters.is_empty():
|
||||
_status_label.text = "No characters yet. Add one below."
|
||||
else:
|
||||
_status_label.text = ""
|
||||
|
||||
func _on_add_button_pressed() -> void:
|
||||
var character_name := _name_input.text.strip_edges()
|
||||
if character_name.is_empty():
|
||||
_status_label.text = "Enter a character name first."
|
||||
return
|
||||
|
||||
_status_label.text = "Creating character..."
|
||||
var response := await CharacterService.create_character(character_name)
|
||||
if not response.get("ok", false):
|
||||
_log_failure("Create character", response)
|
||||
_status_label.text = "Failed to create character."
|
||||
return
|
||||
|
||||
var parsed: Variant = JSON.parse_string(String(response.get("body", "")))
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
_characters.append(parsed)
|
||||
_character_list.add_item(String(parsed.get("name", character_name)))
|
||||
_name_input.text = ""
|
||||
_status_label.text = "Character added."
|
||||
else:
|
||||
_status_label.text = "Character created, but response was unexpected."
|
||||
|
||||
func _on_delete_button_pressed() -> void:
|
||||
var selected := _character_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
_status_label.text = "Select a character to delete."
|
||||
return
|
||||
|
||||
var index := selected[0]
|
||||
if index < 0 or index >= _characters.size():
|
||||
_status_label.text = "Invalid selection."
|
||||
return
|
||||
|
||||
var character: Dictionary = _characters[index]
|
||||
var character_id := String(character.get("id", character.get("Id", "")))
|
||||
if character_id.is_empty():
|
||||
_status_label.text = "Missing character id."
|
||||
return
|
||||
|
||||
_status_label.text = "Deleting character..."
|
||||
var response := await CharacterService.delete_character(character_id)
|
||||
if not response.get("ok", false):
|
||||
_log_failure("Delete character", response)
|
||||
_status_label.text = "Failed to delete character."
|
||||
return
|
||||
|
||||
_characters.remove_at(index)
|
||||
_character_list.remove_item(index)
|
||||
_status_label.text = "Character deleted."
|
||||
|
||||
func _on_refresh_button_pressed() -> void:
|
||||
_load_characters()
|
||||
|
||||
func _on_back_button_pressed() -> void:
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
|
||||
func _on_logout_button_pressed() -> void:
|
||||
_request_logout()
|
||||
|
||||
func _request_logout() -> void:
|
||||
if AuthState.access_token.is_empty():
|
||||
AuthState.clear_session()
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
return
|
||||
|
||||
var headers := PackedStringArray([
|
||||
"Authorization: Bearer %s" % AuthState.access_token,
|
||||
])
|
||||
var err := _logout_request.request(AUTH_LOGOUT_URL, headers, HTTPClient.METHOD_POST)
|
||||
if err != OK:
|
||||
push_warning("Failed to send logout request: %s" % err)
|
||||
AuthState.clear_session()
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
|
||||
func _on_logout_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var body_text := body.get_string_from_utf8()
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
push_warning("Logout failed (%s/%s): %s" % [result, response_code, body_text])
|
||||
AuthState.clear_session()
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2y7ftq2k3v4x
|
||||
@@ -0,0 +1,131 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://cw3b7n7k2c4h6"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/UI/character_screen.gd" id="1_0p3qk"]
|
||||
[ext_resource type="Texture2D" uid="uid://dhuosr0p605gj" path="res://assets/images/pp_start_bg.png" id="2_5g2t1"]
|
||||
[ext_resource type="Theme" uid="uid://tn8qndst18d6" path="res://themes/title_font_theme.tres" id="3_k2j6k"]
|
||||
[ext_resource type="Theme" uid="uid://wpxmub0n2dr3" path="res://themes/button_theme.tres" id="4_5b3b7"]
|
||||
|
||||
[node name="CharacterScreen" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_0p3qk")
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
texture = ExtResource("2_5g2t1")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 80
|
||||
theme_override_constants/margin_top = 40
|
||||
theme_override_constants/margin_right = 80
|
||||
theme_override_constants/margin_bottom = 40
|
||||
|
||||
[node name="ContentCenter" type="CenterContainer" parent="MarginContainer"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="ContentVBox" type="VBoxContainer" parent="MarginContainer/ContentCenter"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
theme_override_constants/separation = 18
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="MarginContainer/ContentCenter/ContentVBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme = ExtResource("3_k2j6k")
|
||||
text = "YOUR CHARACTERS"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="MarginContainer/ContentCenter/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CharacterList" type="ItemList" parent="MarginContainer/ContentCenter/ContentVBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
custom_minimum_size = Vector2(520, 240)
|
||||
|
||||
[node name="AddHBox" type="HBoxContainer" parent="MarginContainer/ContentCenter/ContentVBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="NameInput" type="LineEdit" parent="MarginContainer/ContentCenter/ContentVBox/AddHBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "character name"
|
||||
|
||||
[node name="AddButton" type="Button" parent="MarginContainer/ContentCenter/ContentVBox/AddHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
theme = ExtResource("4_5b3b7")
|
||||
text = "ADD"
|
||||
text_alignment = 1
|
||||
|
||||
[node name="ActionHBox" type="HBoxContainer" parent="MarginContainer/ContentCenter/ContentVBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="RefreshButton" type="Button" parent="MarginContainer/ContentCenter/ContentVBox/ActionHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme = ExtResource("4_5b3b7")
|
||||
text = "REFRESH"
|
||||
text_alignment = 1
|
||||
|
||||
[node name="DeleteButton" type="Button" parent="MarginContainer/ContentCenter/ContentVBox/ActionHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme = ExtResource("4_5b3b7")
|
||||
text = "DELETE"
|
||||
text_alignment = 1
|
||||
|
||||
[node name="BackButton" type="Button" parent="MarginContainer/ContentCenter/ContentVBox/ActionHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme = ExtResource("4_5b3b7")
|
||||
text = "BACK"
|
||||
text_alignment = 1
|
||||
|
||||
[node name="LogoutButton" type="Button" parent="MarginContainer/ContentCenter/ContentVBox/ActionHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
theme = ExtResource("4_5b3b7")
|
||||
text = "LOG OUT"
|
||||
text_alignment = 1
|
||||
|
||||
[node name="LogoutRequest" type="HTTPRequest" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[connection signal="pressed" from="MarginContainer/ContentCenter/ContentVBox/AddHBox/AddButton" to="." method="_on_add_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/ContentCenter/ContentVBox/ActionHBox/RefreshButton" to="." method="_on_refresh_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/ContentCenter/ContentVBox/ActionHBox/DeleteButton" to="." method="_on_delete_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/ContentCenter/ContentVBox/ActionHBox/BackButton" to="." method="_on_back_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/ContentCenter/ContentVBox/ActionHBox/LogoutButton" to="." method="_on_logout_button_pressed"]
|
||||
[connection signal="request_completed" from="LogoutRequest" to="." method="_on_logout_request_completed"]
|
||||
@@ -0,0 +1,58 @@
|
||||
extends Node
|
||||
|
||||
const CHARACTER_API_URL := "https://pcharacter.ranaze.com/api/Characters"
|
||||
|
||||
func list_characters() -> Dictionary:
|
||||
return await _request(HTTPClient.METHOD_GET, CHARACTER_API_URL)
|
||||
|
||||
func create_character(character_name: String) -> Dictionary:
|
||||
var payload := JSON.stringify({
|
||||
"name": character_name
|
||||
})
|
||||
return await _request(HTTPClient.METHOD_POST, CHARACTER_API_URL, payload)
|
||||
|
||||
func delete_character(character_id: String) -> Dictionary:
|
||||
var url := "%s/%s" % [CHARACTER_API_URL, character_id]
|
||||
return await _request(HTTPClient.METHOD_DELETE, url)
|
||||
|
||||
func _request(method: int, url: String, body: String = "") -> Dictionary:
|
||||
var request := HTTPRequest.new()
|
||||
add_child(request)
|
||||
|
||||
var headers := PackedStringArray()
|
||||
if not AuthState.access_token.is_empty():
|
||||
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||
if method == HTTPClient.METHOD_POST or method == HTTPClient.METHOD_PUT:
|
||||
headers.append("Content-Type: application/json")
|
||||
|
||||
var err := request.request(url, headers, method, body)
|
||||
if err != OK:
|
||||
request.queue_free()
|
||||
return {
|
||||
"ok": false,
|
||||
"status": 0,
|
||||
"error": "Failed to send request (%s)." % err,
|
||||
"body": ""
|
||||
}
|
||||
|
||||
var result: Array = await request.request_completed
|
||||
request.queue_free()
|
||||
|
||||
var result_code: int = result[0]
|
||||
var response_code: int = result[1]
|
||||
var response_body: String = result[3].get_string_from_utf8()
|
||||
|
||||
if result_code != HTTPRequest.RESULT_SUCCESS:
|
||||
return {
|
||||
"ok": false,
|
||||
"status": response_code,
|
||||
"error": "Network error (%s)." % result_code,
|
||||
"body": response_body
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": response_code >= 200 and response_code < 300,
|
||||
"status": response_code,
|
||||
"error": "",
|
||||
"body": response_body
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8kchv0e77yw4
|
||||
@@ -37,7 +37,7 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
var token := String(response.get("accessToken", ""))
|
||||
var username := String(response.get("username", ""))
|
||||
AuthState.set_session(username, token)
|
||||
get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn")
|
||||
get_tree().change_scene_to_file("res://scenes/UI/character_screen.tscn")
|
||||
else:
|
||||
_show_error("Login failed. Check your credentials.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user