WIP checkpoint: Implementation: Browse worlds screen wired to GET /api/worlds
This commit is contained in:
@@ -34,10 +34,16 @@ layout_mode = 1
|
|||||||
text = "Play"
|
text = "Play"
|
||||||
size_flags_horizontal = 4
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="BrowseButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||||
|
layout_mode = 1
|
||||||
|
text = "Browse Worlds"
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
[node name="QuitButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
[node name="QuitButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
text = "Quit"
|
text = "Quit"
|
||||||
size_flags_horizontal = 4
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"]
|
[connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"]
|
||||||
|
[connection signal="pressed" from="CenterContainer/VBoxContainer/BrowseButton" to="." method="_on_browse_button_pressed"]
|
||||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]
|
[connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/world_list.gd" id="1_wl"]
|
||||||
|
|
||||||
|
[node name="WorldList" type="Control"]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_wl")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
|
||||||
|
[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 3
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
layout_mode = 3
|
||||||
|
text = "< Back"
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
layout_mode = 3
|
||||||
|
text = "Browse Worlds"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
theme_override_font_sizes/font_size = 28
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
layout_mode = 3
|
||||||
|
text = "Refresh"
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 3
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
horizontal_mode = 2
|
||||||
|
vertical_mode = 2
|
||||||
|
|
||||||
|
[node name="ScrollVBox" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
|
||||||
|
layout_mode = 3
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
size_flags_vertical = 4
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="VBoxContainer"]
|
||||||
|
layout_mode = 3
|
||||||
|
text = ""
|
||||||
|
visible = false
|
||||||
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
@@ -6,8 +6,8 @@ func _ready():
|
|||||||
|
|
||||||
|
|
||||||
func _on_play_button_pressed():
|
func _on_play_button_pressed():
|
||||||
# No gameplay scene exists yet — wire this to the real scene once one lands.
|
# Navigate to the world list / browse screen.
|
||||||
print("Play pressed — no gameplay scene yet")
|
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
|
||||||
|
|
||||||
|
|
||||||
func _on_quit_button_pressed():
|
func _on_quit_button_pressed():
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
extends Control
|
||||||
|
## World list screen — browses public worlds via GET /api/worlds.
|
||||||
|
##
|
||||||
|
## Displays a scrollable list of world cards (name + description).
|
||||||
|
## Handles loading, empty list, errors, and refresh.
|
||||||
|
|
||||||
|
const WORLD_CARD_SCENE := preload("res://scenes/world_card.tscn")
|
||||||
|
|
||||||
|
# UI references
|
||||||
|
@onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer
|
||||||
|
@onready var world_list: VBoxContainer = $VBoxContainer/ScrollContainer/WorldList
|
||||||
|
@onready var loading_label: Label = $VBoxContainer/LoadingLabel
|
||||||
|
@onready var error_label: Label = $VBoxContainer/ErrorLabel
|
||||||
|
@onready var refresh_button: Button = $VBoxContainer/RefreshButton
|
||||||
|
@onready var back_button: Button = $VBoxContainer/BackButton
|
||||||
|
@onready var empty_label: Label = $VBoxContainer/EmptyLabel
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
refresh_button.disabled = false
|
||||||
|
back_button.disabled = false
|
||||||
|
loading_label.visible = false
|
||||||
|
error_label.visible = false
|
||||||
|
empty_label.visible = false
|
||||||
|
world_list.visible = false
|
||||||
|
_refresh_worlds()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_refresh_button_pressed() -> void:
|
||||||
|
_refresh_worlds()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_button_pressed() -> void:
|
||||||
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
## Fetch the world list from the backend and update the UI.
|
||||||
|
func _refresh_worlds() -> void:
|
||||||
|
# Reset UI state
|
||||||
|
loading_label.visible = true
|
||||||
|
error_label.visible = false
|
||||||
|
empty_label.visible = false
|
||||||
|
world_list.visible = false
|
||||||
|
refresh_button.disabled = true
|
||||||
|
|
||||||
|
# Clear previous world cards
|
||||||
|
for child in world_list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
var result := await ApiClient.get_worlds()
|
||||||
|
|
||||||
|
loading_label.visible = false
|
||||||
|
refresh_button.disabled = false
|
||||||
|
|
||||||
|
if not result.ok:
|
||||||
|
# Show error message (401, network failure, etc.)
|
||||||
|
error_label.text = "Error: %s" % result.detail
|
||||||
|
error_label.visible = true
|
||||||
|
empty_label.visible = false
|
||||||
|
world_list.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var data = result.data
|
||||||
|
if not data is Array:
|
||||||
|
error_label.text = "Unexpected response format."
|
||||||
|
error_label.visible = true
|
||||||
|
empty_label.visible = false
|
||||||
|
world_list.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
if data.is_empty():
|
||||||
|
empty_label.text = "No worlds yet. Check back later!"
|
||||||
|
empty_label.visible = true
|
||||||
|
error_label.visible = false
|
||||||
|
world_list.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
# Render world cards
|
||||||
|
world_list.visible = true
|
||||||
|
empty_label.visible = false
|
||||||
|
error_label.visible = false
|
||||||
|
|
||||||
|
for world_dict in data:
|
||||||
|
if not world_dict is Dictionary:
|
||||||
|
continue
|
||||||
|
var card := _create_world_card(world_dict)
|
||||||
|
world_list.add_child(card)
|
||||||
|
|
||||||
|
|
||||||
|
## Create a visual card for a single world entry.
|
||||||
|
func _create_world_card(world_dict: Dictionary) -> Control:
|
||||||
|
var card := VBoxContainer.new()
|
||||||
|
card.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
card.custom_minimum_size = Vector2(0, 80)
|
||||||
|
card.theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
var name_label := Label.new()
|
||||||
|
name_label.text = world_dict.get("name", "Unnamed World")
|
||||||
|
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
name_label.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
name_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
name_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
|
||||||
|
var desc_label := Label.new()
|
||||||
|
var description := world_dict.get("description", "")
|
||||||
|
desc_label.text = description if description != "" else "No description."
|
||||||
|
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
desc_label.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
desc_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
desc_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
desc_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1.0))
|
||||||
|
|
||||||
|
var meta_label := Label.new()
|
||||||
|
var visibility := world_dict.get("visibility", "public")
|
||||||
|
var created := world_dict.get("created_at", "")
|
||||||
|
meta_label.text = "Visibility: %s%s" % [visibility, " | Created: " + created if created != "" else ""]
|
||||||
|
meta_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
meta_label.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
meta_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
meta_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
meta_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1.0))
|
||||||
|
|
||||||
|
card.add_child(name_label)
|
||||||
|
card.add_child(desc_label)
|
||||||
|
card.add_child(meta_label)
|
||||||
|
|
||||||
|
# Subtle background for the card
|
||||||
|
var bg := ColorRect.new()
|
||||||
|
bg.color = Color(0.15, 0.15, 0.15, 1.0)
|
||||||
|
bg.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
bg.size_flags_vertical = Control.SIZE_FILL
|
||||||
|
bg.custom_minimum_size = Vector2(0, 80)
|
||||||
|
card.add_child(bg)
|
||||||
|
card.move_child(bg, 0)
|
||||||
|
|
||||||
|
return card
|
||||||
Reference in New Issue
Block a user