feat(godot): discovery & home UI with search, categories, sorting, pagination, and world entry
This commit is contained in:
@@ -0,0 +1,150 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/discovery_ui.gd" id="1_disc"]
|
||||||
|
|
||||||
|
[node name="DiscoveryUI" 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_disc")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = 24.0
|
||||||
|
offset_right = -32.0
|
||||||
|
offset_bottom = -24.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="Header" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="VBox/Header"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "< Back"
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBox/Header"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/font_size = 26
|
||||||
|
text = "Discover Experiences"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="VBox/Header"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="VBox"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 0.3, 0.3, 1)
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="Filters" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="SearchEdit" type="LineEdit" parent="VBox/Filters"]
|
||||||
|
custom_minimum_size = Vector2(240, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Search by name or tag..."
|
||||||
|
|
||||||
|
[node name="CategoryOption" type="OptionButton" parent="VBox/Filters"]
|
||||||
|
custom_minimum_size = Vector2(160, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SortOption" type="OptionButton" parent="VBox/Filters"]
|
||||||
|
custom_minimum_size = Vector2(140, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="FilterButton" type="Button" parent="VBox/Filters"]
|
||||||
|
custom_minimum_size = Vector2(90, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Filter"
|
||||||
|
|
||||||
|
[node name="Content" type="Control" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="LoadingLabel" type="Label" parent="VBox/Content"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 14
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_top = -12.0
|
||||||
|
offset_bottom = 12.0
|
||||||
|
text = "Loading experiences..."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ErrorLabel" type="Label" parent="VBox/Content"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 14
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_top = -12.0
|
||||||
|
offset_bottom = 12.0
|
||||||
|
theme_override_colors/font_color = Color(1, 0.4, 0.4, 1)
|
||||||
|
text = ""
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="EmptyLabel" type="Label" parent="VBox/Content"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 14
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_top = -12.0
|
||||||
|
offset_bottom = 12.0
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||||
|
text = "No experiences found."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="VBox/Content"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="WorldsVBox" type="VBoxContainer" parent="VBox/Content/Scroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="Pagination" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="PrevButton" type="Button" parent="VBox/Pagination"]
|
||||||
|
custom_minimum_size = Vector2(90, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "< Prev"
|
||||||
|
|
||||||
|
[node name="PageLabel" type="Label" parent="VBox/Pagination"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Page 1 of 1"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="NextButton" type="Button" parent="VBox/Pagination"]
|
||||||
|
custom_minimum_size = Vector2(90, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Next >"
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
extends Control
|
||||||
|
class_name DiscoveryUI
|
||||||
|
## Discovery & Home Screen for browsing, searching, and entering experiences.
|
||||||
|
##
|
||||||
|
## docs/planning/discovery.md (task b77bddf31ba5).
|
||||||
|
|
||||||
|
signal back_requested
|
||||||
|
signal world_selected(world_id: int)
|
||||||
|
signal worlds_loaded(count: int)
|
||||||
|
|
||||||
|
@onready var back_button: Button = $VBox/Header/BackButton
|
||||||
|
@onready var refresh_button: Button = $VBox/Header/RefreshButton
|
||||||
|
@onready var status_label: Label = $VBox/StatusLabel
|
||||||
|
|
||||||
|
# Filter Controls
|
||||||
|
@onready var search_edit: LineEdit = $VBox/Filters/SearchEdit
|
||||||
|
@onready var category_option: OptionButton = $VBox/Filters/CategoryOption
|
||||||
|
@onready var sort_option: OptionButton = $VBox/Filters/SortOption
|
||||||
|
@onready var filter_button: Button = $VBox/Filters/FilterButton
|
||||||
|
|
||||||
|
# Content
|
||||||
|
@onready var loading_label: Label = $VBox/Content/LoadingLabel
|
||||||
|
@onready var error_label: Label = $VBox/Content/ErrorLabel
|
||||||
|
@onready var empty_label: Label = $VBox/Content/EmptyLabel
|
||||||
|
@onready var worlds_container: VBoxContainer = $VBox/Content/Scroll/WorldsVBox
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
@onready var prev_button: Button = $VBox/Pagination/PrevButton
|
||||||
|
@onready var next_button: Button = $VBox/Pagination/NextButton
|
||||||
|
@onready var page_label: Label = $VBox/Pagination/PageLabel
|
||||||
|
|
||||||
|
var _api_client: Node = null
|
||||||
|
var _current_page: int = 0
|
||||||
|
var _page_size: int = 10
|
||||||
|
var _total_count: int = 0
|
||||||
|
var _loaded_worlds: Array[DiscoveryApiClient.WorldSummary] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_api_client = _get_api()
|
||||||
|
_wire_signals()
|
||||||
|
_setup_sort_options()
|
||||||
|
_setup_initial_ui_state()
|
||||||
|
|
||||||
|
# Populate categories then load initial worlds feed
|
||||||
|
await load_categories()
|
||||||
|
await load_worlds(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_api() -> Node:
|
||||||
|
if has_node("/root/ApiClient"):
|
||||||
|
return get_node("/root/ApiClient")
|
||||||
|
var api_script = load("res://scripts/api_client.gd")
|
||||||
|
if api_script:
|
||||||
|
var api = api_script.new()
|
||||||
|
add_child(api)
|
||||||
|
return api
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_initial_ui_state() -> void:
|
||||||
|
if loading_label: loading_label.visible = false
|
||||||
|
if error_label: error_label.visible = false
|
||||||
|
if empty_label: empty_label.visible = false
|
||||||
|
if prev_button: prev_button.disabled = true
|
||||||
|
if next_button: next_button.disabled = true
|
||||||
|
if page_label: page_label.text = "Page 1"
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_sort_options() -> void:
|
||||||
|
if not sort_option:
|
||||||
|
return
|
||||||
|
sort_option.clear()
|
||||||
|
sort_option.add_item("Newest", 0)
|
||||||
|
sort_option.add_item("Most Played", 1)
|
||||||
|
sort_option.add_item("Trending", 2)
|
||||||
|
sort_option.selected = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _wire_signals() -> void:
|
||||||
|
if back_button and not back_button.pressed.is_connected(_on_back_pressed):
|
||||||
|
back_button.pressed.connect(_on_back_pressed)
|
||||||
|
if refresh_button and not refresh_button.pressed.is_connected(_on_refresh_pressed):
|
||||||
|
refresh_button.pressed.connect(_on_refresh_pressed)
|
||||||
|
if filter_button and not filter_button.pressed.is_connected(_on_filter_pressed):
|
||||||
|
filter_button.pressed.connect(_on_filter_pressed)
|
||||||
|
if search_edit and not search_edit.text_submitted.is_connected(_on_search_submitted):
|
||||||
|
search_edit.text_submitted.connect(_on_search_submitted)
|
||||||
|
if category_option and not category_option.item_selected.is_connected(_on_category_selected):
|
||||||
|
category_option.item_selected.connect(_on_category_selected)
|
||||||
|
if sort_option and not sort_option.item_selected.is_connected(_on_sort_selected):
|
||||||
|
sort_option.item_selected.connect(_on_sort_selected)
|
||||||
|
if prev_button and not prev_button.pressed.is_connected(_on_prev_page_pressed):
|
||||||
|
prev_button.pressed.connect(_on_prev_page_pressed)
|
||||||
|
if next_button and not next_button.pressed.is_connected(_on_next_page_pressed):
|
||||||
|
next_button.pressed.connect(_on_next_page_pressed)
|
||||||
|
|
||||||
|
|
||||||
|
func load_categories() -> void:
|
||||||
|
if not category_option:
|
||||||
|
return
|
||||||
|
category_option.clear()
|
||||||
|
category_option.add_item("All Categories", 0)
|
||||||
|
|
||||||
|
if _api_client == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var res: Dictionary = await DiscoveryApiClient.get_categories(_api_client)
|
||||||
|
if res.ok and res.data is Array:
|
||||||
|
var idx := 1
|
||||||
|
for cat in res.data:
|
||||||
|
var cat_str := str(cat).strip_edges()
|
||||||
|
if cat_str != "":
|
||||||
|
category_option.add_item(cat_str, idx)
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
|
||||||
|
func load_worlds(page: int = 0) -> void:
|
||||||
|
_current_page = page
|
||||||
|
if loading_label: loading_label.visible = true
|
||||||
|
if error_label: error_label.visible = false
|
||||||
|
if empty_label: empty_label.visible = false
|
||||||
|
|
||||||
|
var q := search_edit.text.strip_edges() if search_edit else ""
|
||||||
|
var category := ""
|
||||||
|
if category_option and category_option.selected > 0:
|
||||||
|
category = category_option.get_item_text(category_option.selected)
|
||||||
|
|
||||||
|
var sort := "newest"
|
||||||
|
if sort_option:
|
||||||
|
match sort_option.selected:
|
||||||
|
0: sort = "newest"
|
||||||
|
1: sort = "most_played"
|
||||||
|
2: sort = "trending"
|
||||||
|
|
||||||
|
var skip := _current_page * _page_size
|
||||||
|
var res: Dictionary = await DiscoveryApiClient.get_worlds(
|
||||||
|
_api_client,
|
||||||
|
q,
|
||||||
|
[],
|
||||||
|
category,
|
||||||
|
sort,
|
||||||
|
skip,
|
||||||
|
_page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
if loading_label: loading_label.visible = false
|
||||||
|
|
||||||
|
if not res.ok:
|
||||||
|
if error_label:
|
||||||
|
error_label.text = "Error: %s" % res.detail
|
||||||
|
error_label.visible = true
|
||||||
|
_clear_container()
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: DiscoveryApiClient.DiscoveryResult = res.get("parsed")
|
||||||
|
if parsed == null and res.data is Dictionary:
|
||||||
|
parsed = DiscoveryApiClient.DiscoveryResult.from_dict(res.data)
|
||||||
|
|
||||||
|
if parsed == null:
|
||||||
|
if error_label:
|
||||||
|
error_label.text = "Invalid discovery response"
|
||||||
|
error_label.visible = true
|
||||||
|
_clear_container()
|
||||||
|
return
|
||||||
|
|
||||||
|
_total_count = parsed.total
|
||||||
|
_loaded_worlds = parsed.items
|
||||||
|
|
||||||
|
_render_worlds(_loaded_worlds)
|
||||||
|
_update_pagination()
|
||||||
|
worlds_loaded.emit(_loaded_worlds.size())
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_container() -> void:
|
||||||
|
if not worlds_container:
|
||||||
|
return
|
||||||
|
for c in worlds_container.get_children():
|
||||||
|
worlds_container.remove_child(c)
|
||||||
|
c.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _render_worlds(worlds: Array[DiscoveryApiClient.WorldSummary]) -> void:
|
||||||
|
_clear_container()
|
||||||
|
|
||||||
|
if worlds.is_empty():
|
||||||
|
if empty_label:
|
||||||
|
empty_label.text = "No experiences found matching your criteria."
|
||||||
|
empty_label.visible = true
|
||||||
|
return
|
||||||
|
|
||||||
|
if empty_label: empty_label.visible = false
|
||||||
|
|
||||||
|
for world in worlds:
|
||||||
|
var card := _create_world_card(world)
|
||||||
|
worlds_container.add_child(card)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_world_card(world: DiscoveryApiClient.WorldSummary) -> Control:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.custom_minimum_size = Vector2(0, 90)
|
||||||
|
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.14, 0.16, 0.20, 1.0)
|
||||||
|
style.corner_radius_top_left = 6
|
||||||
|
style.corner_radius_top_right = 6
|
||||||
|
style.corner_radius_bottom_left = 6
|
||||||
|
style.corner_radius_bottom_right = 6
|
||||||
|
style.content_margin_left = 16
|
||||||
|
style.content_margin_right = 16
|
||||||
|
style.content_margin_top = 10
|
||||||
|
style.content_margin_bottom = 10
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.add_theme_constant_override("separation", 4)
|
||||||
|
panel.add_child(vbox)
|
||||||
|
|
||||||
|
# Title & Category Row
|
||||||
|
var title_row := HBoxContainer.new()
|
||||||
|
var name_label := Label.new()
|
||||||
|
name_label.text = world.name
|
||||||
|
name_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
name_label.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0, 1.0))
|
||||||
|
title_row.add_child(name_label)
|
||||||
|
|
||||||
|
if world.category != "":
|
||||||
|
var cat_label := Label.new()
|
||||||
|
cat_label.text = " [%s]" % world.category
|
||||||
|
cat_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
cat_label.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0, 1.0))
|
||||||
|
title_row.add_child(cat_label)
|
||||||
|
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title_row.add_child(spacer)
|
||||||
|
|
||||||
|
# Enter World Button
|
||||||
|
var enter_button := Button.new()
|
||||||
|
enter_button.text = "Enter World"
|
||||||
|
enter_button.custom_minimum_size = Vector2(110, 30)
|
||||||
|
enter_button.pressed.connect(_on_enter_world_pressed.bind(world.id, enter_button))
|
||||||
|
title_row.add_child(enter_button)
|
||||||
|
vbox.add_child(title_row)
|
||||||
|
|
||||||
|
# Description
|
||||||
|
var desc_label := Label.new()
|
||||||
|
desc_label.text = world.description if world.description != "" else "No description provided."
|
||||||
|
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
desc_label.add_theme_font_size_override("font_size", 13)
|
||||||
|
desc_label.add_theme_color_override("font_color", Color(0.75, 0.75, 0.75, 1.0))
|
||||||
|
vbox.add_child(desc_label)
|
||||||
|
|
||||||
|
# Metadata Row: Tags, Plays, Owner
|
||||||
|
var meta_row := HBoxContainer.new()
|
||||||
|
meta_row.add_theme_constant_override("separation", 16)
|
||||||
|
|
||||||
|
var play_label := Label.new()
|
||||||
|
play_label.text = "%d plays" % world.play_count
|
||||||
|
play_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
play_label.add_theme_color_override("font_color", Color(0.6, 0.9, 0.6, 1.0))
|
||||||
|
meta_row.add_child(play_label)
|
||||||
|
|
||||||
|
if world.tags.size() > 0:
|
||||||
|
var tags_str: String = ""
|
||||||
|
for t in world.tags:
|
||||||
|
tags_str += "#" + str(t) + " "
|
||||||
|
var tags_label := Label.new()
|
||||||
|
tags_label.text = tags_str.strip_edges()
|
||||||
|
tags_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
tags_label.add_theme_color_override("font_color", Color(0.8, 0.7, 0.4, 1.0))
|
||||||
|
meta_row.add_child(tags_label)
|
||||||
|
|
||||||
|
if world.owner_username != "":
|
||||||
|
var owner_label := Label.new()
|
||||||
|
owner_label.text = "Creator: %s" % world.owner_username
|
||||||
|
owner_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
owner_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0))
|
||||||
|
meta_row.add_child(owner_label)
|
||||||
|
|
||||||
|
vbox.add_child(meta_row)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
func _update_pagination() -> void:
|
||||||
|
var total_pages := int(ceil(float(_total_count) / float(_page_size)))
|
||||||
|
if total_pages <= 0:
|
||||||
|
total_pages = 1
|
||||||
|
|
||||||
|
if page_label:
|
||||||
|
page_label.text = "Page %d of %d (%d experiences)" % [_current_page + 1, total_pages, _total_count]
|
||||||
|
|
||||||
|
if prev_button:
|
||||||
|
prev_button.disabled = (_current_page <= 0)
|
||||||
|
if next_button:
|
||||||
|
next_button.disabled = ((_current_page + 1) >= total_pages)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_enter_world_pressed(world_id: int, button: Button) -> void:
|
||||||
|
button.disabled = true
|
||||||
|
button.text = "Joining…"
|
||||||
|
world_selected.emit(world_id)
|
||||||
|
|
||||||
|
# 1. Record visit metric
|
||||||
|
if _api_client != null:
|
||||||
|
await DiscoveryApiClient.record_visit(_api_client, world_id)
|
||||||
|
|
||||||
|
# 2. Join flow (existing game server orchestration)
|
||||||
|
if _api_client != null and _api_client.has_method("join_world"):
|
||||||
|
var result: Dictionary = await _api_client.join_world(world_id)
|
||||||
|
if result.ok and result.data is Dictionary:
|
||||||
|
var d: Dictionary = result.data
|
||||||
|
if has_node("/root/Session"):
|
||||||
|
get_node("/root/Session").begin(world_id, str(d.get("host", "")), int(d.get("port", 0)))
|
||||||
|
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||||
|
return
|
||||||
|
|
||||||
|
if result.status == 503:
|
||||||
|
# Dev fallback: local single-player
|
||||||
|
if has_node("/root/Session"):
|
||||||
|
get_node("/root/Session").clear()
|
||||||
|
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||||
|
return
|
||||||
|
|
||||||
|
if status_label:
|
||||||
|
status_label.text = "Failed to join: " + str(result.detail)
|
||||||
|
status_label.visible = true
|
||||||
|
else:
|
||||||
|
# Fallback if standalone test or not wired
|
||||||
|
if has_node("/root/Session"):
|
||||||
|
get_node("/root/Session").clear()
|
||||||
|
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||||
|
return
|
||||||
|
|
||||||
|
button.disabled = false
|
||||||
|
button.text = "Enter World"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_filter_pressed() -> void:
|
||||||
|
load_worlds(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_submitted(_new_text: String) -> void:
|
||||||
|
load_worlds(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_category_selected(_index: int) -> void:
|
||||||
|
load_worlds(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_sort_selected(_index: int) -> void:
|
||||||
|
load_worlds(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_refresh_pressed() -> void:
|
||||||
|
load_worlds(_current_page)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_prev_page_pressed() -> void:
|
||||||
|
if _current_page > 0:
|
||||||
|
load_worlds(_current_page - 1)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_next_page_pressed() -> void:
|
||||||
|
var total_pages := int(ceil(float(_total_count) / float(_page_size)))
|
||||||
|
if (_current_page + 1) < total_pages:
|
||||||
|
load_worlds(_current_page + 1)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_pressed() -> void:
|
||||||
|
back_requested.emit()
|
||||||
|
if get_tree():
|
||||||
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test for DiscoveryUI.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
print("[SMOKE] Starting DiscoveryUI smoke test...")
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var scene = load("res://scenes/discovery_ui.tscn")
|
||||||
|
if not scene:
|
||||||
|
printerr("[FAIL] Could not load res://scenes/discovery_ui.tscn")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var ui: DiscoveryUI = scene.instantiate()
|
||||||
|
root.add_child(ui)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# 1. Verify node references
|
||||||
|
assert(ui.search_edit != null, "search_edit is null")
|
||||||
|
assert(ui.category_option != null, "category_option is null")
|
||||||
|
assert(ui.sort_option != null, "sort_option is null")
|
||||||
|
assert(ui.filter_button != null, "filter_button is null")
|
||||||
|
assert(ui.worlds_container != null, "worlds_container is null")
|
||||||
|
assert(ui.prev_button != null, "prev_button is null")
|
||||||
|
assert(ui.next_button != null, "next_button is null")
|
||||||
|
assert(ui.page_label != null, "page_label is null")
|
||||||
|
print("[PASS] Node references verified")
|
||||||
|
|
||||||
|
# 2. Verify sort options
|
||||||
|
assert(ui.sort_option.item_count == 3, "sort_option should have 3 items")
|
||||||
|
assert(ui.sort_option.get_item_text(0) == "Newest", "sort 0 should be Newest")
|
||||||
|
assert(ui.sort_option.get_item_text(1) == "Most Played", "sort 1 should be Most Played")
|
||||||
|
assert(ui.sort_option.get_item_text(2) == "Trending", "sort 2 should be Trending")
|
||||||
|
print("[PASS] Sort options verified")
|
||||||
|
|
||||||
|
# 3. Verify category dropdown initial state
|
||||||
|
assert(ui.category_option.item_count >= 1, "category_option should have at least All Categories")
|
||||||
|
assert(ui.category_option.get_item_text(0) == "All Categories", "category 0 should be All Categories")
|
||||||
|
print("[PASS] Category dropdown verified")
|
||||||
|
|
||||||
|
# 4. Test rendering of WorldSummary cards
|
||||||
|
var mock_w := DiscoveryApiClient.WorldSummary.new()
|
||||||
|
mock_w.id = 101
|
||||||
|
mock_w.name = "Cyber Nexus"
|
||||||
|
mock_w.category = "Action"
|
||||||
|
mock_w.tags = ["cyberpunk", "pvp"]
|
||||||
|
mock_w.play_count = 256
|
||||||
|
mock_w.description = "A futuristic arena"
|
||||||
|
mock_w.owner_username = "neon_builder"
|
||||||
|
|
||||||
|
var mock_worlds: Array[DiscoveryApiClient.WorldSummary] = [mock_w]
|
||||||
|
ui._render_worlds(mock_worlds)
|
||||||
|
assert(ui.worlds_container.get_child_count() == 1, "Should render 1 card")
|
||||||
|
assert(ui.empty_label.visible == false, "empty_label should be hidden when items present")
|
||||||
|
print("[PASS] World card rendering verified")
|
||||||
|
|
||||||
|
# 5. Test empty state
|
||||||
|
var empty_list: Array[DiscoveryApiClient.WorldSummary] = []
|
||||||
|
ui._render_worlds(empty_list)
|
||||||
|
assert(ui.worlds_container.get_child_count() == 0, "Container should have 0 children")
|
||||||
|
assert(ui.empty_label.visible == true, "empty_label should be visible when empty")
|
||||||
|
print("[PASS] Empty state verified")
|
||||||
|
|
||||||
|
# 6. Test pagination logic
|
||||||
|
ui._total_count = 25
|
||||||
|
ui._page_size = 10
|
||||||
|
ui._current_page = 0
|
||||||
|
ui._update_pagination()
|
||||||
|
assert(ui.prev_button.disabled == true, "Page 0 prev_button should be disabled")
|
||||||
|
assert(ui.next_button.disabled == false, "Page 0 next_button should be enabled")
|
||||||
|
assert(ui.page_label.text.contains("Page 1 of 3"), "Page 1 of 3 mismatch: %s" % ui.page_label.text)
|
||||||
|
|
||||||
|
ui._current_page = 1
|
||||||
|
ui._update_pagination()
|
||||||
|
assert(ui.prev_button.disabled == false, "Page 1 prev_button should be enabled")
|
||||||
|
assert(ui.next_button.disabled == false, "Page 1 next_button should be enabled")
|
||||||
|
|
||||||
|
ui._current_page = 2
|
||||||
|
ui._update_pagination()
|
||||||
|
assert(ui.prev_button.disabled == false, "Page 2 prev_button should be enabled")
|
||||||
|
assert(ui.next_button.disabled == true, "Page 2 next_button should be disabled")
|
||||||
|
print("[PASS] Pagination logic verified")
|
||||||
|
|
||||||
|
ui.queue_free()
|
||||||
|
print("[SMOKE] DiscoveryUI smoke test PASSED")
|
||||||
|
quit(0)
|
||||||
Reference in New Issue
Block a user