Wire browse-worlds screen to GET /api/worlds #36
@@ -21,7 +21,12 @@ class World(Base):
|
||||
description = Column(Text, nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
visibility = Column(
|
||||
ENUM(WorldVisibility, name="world_visibility", create_type=False),
|
||||
ENUM(
|
||||
WorldVisibility,
|
||||
name="world_visibility",
|
||||
create_type=False,
|
||||
values_callable=lambda enum_cls: [e.value for e in enum_cls],
|
||||
),
|
||||
default=WorldVisibility.PUBLIC,
|
||||
)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Worlds router - CRUD for game worlds."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
@@ -40,7 +41,7 @@ class WorldResponse(BaseModel):
|
||||
description: str | None
|
||||
visibility: WorldVisibility
|
||||
owner_id: int
|
||||
created_at: str | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
+72
-7
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[gd_scene load_steps=2 format=3 uid="uid://main_menu_scene"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main_menu.gd" id="1_main"]
|
||||
|
||||
@@ -29,21 +29,86 @@ text = "AGP Game"
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 72
|
||||
|
||||
[node name="AuthPanel" type="PanelContainer" parent="CenterContainer/VBoxContainer"]
|
||||
layout_mode = 1
|
||||
visible = false
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/VBoxContainer/AuthPanel"]
|
||||
layout_mode = 1
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 24
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 24
|
||||
|
||||
[node name="AuthVBox" type="VBoxContainer" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer"]
|
||||
layout_mode = 1
|
||||
theme_override_constants/separation = 8
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="AuthModeLabel" type="Label" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = "Login"
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 24
|
||||
|
||||
[node name="UsernameEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
placeholder_text = "Username"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="PasswordEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
placeholder_text = "Password"
|
||||
size_flags_horizontal = 3
|
||||
echo_password = true
|
||||
|
||||
[node name="EmailEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
visible = false
|
||||
placeholder_text = "Email"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="DOBEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
visible = false
|
||||
placeholder_text = "Date of Birth (YYYY-MM-DD)"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ModeToggle" type="CheckButton" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = "Register"
|
||||
|
||||
[node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = "Login"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="AuthStatusLabel" type="Label" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = ""
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
visible = false
|
||||
|
||||
[node name="WelcomeLabel" type="Label" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
visible = false
|
||||
text = ""
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||
layout_mode = 1
|
||||
text = "Play"
|
||||
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"]
|
||||
layout_mode = 1
|
||||
text = "Quit"
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[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="toggled" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle" to="." method="_on_mode_toggle_toggled"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton" to="." method="_on_auth_button_pressed"]
|
||||
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit" to="." method="_on_auth_button_pressed"]
|
||||
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit" to="." method="_on_auth_button_pressed"]
|
||||
|
||||
@@ -71,3 +71,6 @@ text = ""
|
||||
visible = false
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 18
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderHBox/BackButton" to="." method="_on_back_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderHBox/RefreshButton" to="." method="_on_refresh_button_pressed"]
|
||||
|
||||
+203
-10
@@ -1,19 +1,212 @@
|
||||
extends Control
|
||||
## Main menu with login/register UI wired to the backend auth API.
|
||||
##
|
||||
## The AuthPanel is hidden by default. When the user clicks "Login" or "Register"
|
||||
## (the existing Play/Quit buttons), the auth panel slides in. The ModeToggle
|
||||
## switches between Login and Register modes, showing/hiding the email and DOB
|
||||
## fields accordingly.
|
||||
##
|
||||
## On successful auth the token is stored via ApiClient, the welcome label
|
||||
## shows the username, and the Play button becomes enabled.
|
||||
|
||||
|
||||
func _ready():
|
||||
pass
|
||||
# ── UI references ────────────────────────────────────────────────────────────
|
||||
|
||||
var _auth_panel: PanelContainer
|
||||
var _auth_mode_label: Label
|
||||
var _username_edit: LineEdit
|
||||
var _password_edit: LineEdit
|
||||
var _email_edit: LineEdit
|
||||
var _dob_edit: LineEdit
|
||||
var _mode_toggle: CheckButton
|
||||
var _auth_button: Button
|
||||
var _status_label: Label
|
||||
var _welcome_label: Label
|
||||
var _play_button: Button
|
||||
var _quit_button: Button
|
||||
|
||||
var _is_register_mode: bool = false
|
||||
var _is_authenticating: bool = false
|
||||
|
||||
|
||||
func _on_play_button_pressed():
|
||||
# Navigate to the world list / browse screen.
|
||||
# ── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _ready() -> void:
|
||||
# Cache node references.
|
||||
_auth_panel = $CenterContainer/VBoxContainer/AuthPanel
|
||||
_auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel
|
||||
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
|
||||
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
|
||||
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
|
||||
_dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit
|
||||
_mode_toggle = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle
|
||||
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
|
||||
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
||||
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
||||
_play_button = $CenterContainer/VBoxContainer/PlayButton
|
||||
_quit_button = $CenterContainer/VBoxContainer/QuitButton
|
||||
|
||||
# Initial state.
|
||||
_auth_panel.visible = false
|
||||
_status_label.visible = false
|
||||
_welcome_label.visible = false
|
||||
_email_edit.visible = false
|
||||
_dob_edit.visible = false
|
||||
_play_button.disabled = true # Gate play behind successful auth.
|
||||
_update_auth_mode_ui()
|
||||
|
||||
|
||||
# ── Play / Quit ──────────────────────────────────────────────────────────────
|
||||
|
||||
func _on_play_button_pressed() -> void:
|
||||
if _play_button.disabled:
|
||||
return
|
||||
# Browse worlds is the first real screen past login -- actual gameplay
|
||||
# (joining a world) doesn't exist yet, browsing does.
|
||||
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
|
||||
|
||||
|
||||
func _on_browse_button_pressed():
|
||||
# Same as Play — browse worlds is the primary action.
|
||||
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
|
||||
|
||||
|
||||
func _on_quit_button_pressed():
|
||||
func _on_quit_button_pressed() -> void:
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
# ── Auth panel toggle ────────────────────────────────────────────────────────
|
||||
|
||||
func _on_login_button_pressed() -> void:
|
||||
"""Show the auth panel (called from a future Login button on the main menu)."""
|
||||
_auth_panel.visible = true
|
||||
_auth_panel.grab_focus()
|
||||
|
||||
|
||||
func _on_register_button_pressed() -> void:
|
||||
"""Show the auth panel in register mode."""
|
||||
_auth_panel.visible = true
|
||||
_is_register_mode = true
|
||||
_mode_toggle.button_pressed = true
|
||||
_update_auth_mode_ui()
|
||||
_clear_status()
|
||||
|
||||
|
||||
# ── Mode toggle (Login / Register) ───────────────────────────────────────────
|
||||
|
||||
func _on_mode_toggle_toggled(button_pressed: bool) -> void:
|
||||
_is_register_mode = button_pressed
|
||||
_update_auth_mode_ui()
|
||||
_clear_status()
|
||||
|
||||
|
||||
func _update_auth_mode_ui() -> void:
|
||||
"""Update labels and field visibility based on login vs register mode."""
|
||||
if _is_register_mode:
|
||||
_auth_mode_label.text = "Register"
|
||||
_auth_button.text = "Register"
|
||||
_email_edit.visible = true
|
||||
_dob_edit.visible = true
|
||||
else:
|
||||
_auth_mode_label.text = "Login"
|
||||
_auth_button.text = "Login"
|
||||
_email_edit.visible = false
|
||||
_dob_edit.visible = false
|
||||
|
||||
|
||||
# ── Auth submission ──────────────────────────────────────────────────────────
|
||||
|
||||
func _on_auth_button_pressed() -> void:
|
||||
"""Handle login or register button press."""
|
||||
if _is_authenticating:
|
||||
return
|
||||
|
||||
# Validate inputs.
|
||||
var username := _username_edit.text.strip_edges()
|
||||
var password := _password_edit.text
|
||||
|
||||
if username == "":
|
||||
_show_status("Username is required.")
|
||||
return
|
||||
if password == "":
|
||||
_show_status("Password is required.")
|
||||
return
|
||||
|
||||
if _is_register_mode:
|
||||
var email := _email_edit.text.strip_edges()
|
||||
var dob := _dob_edit.text.strip_edges()
|
||||
|
||||
if email == "":
|
||||
_show_status("Email is required.")
|
||||
return
|
||||
if dob == "":
|
||||
_show_status("Date of birth is required.")
|
||||
return
|
||||
# Basic YYYY-MM-DD format check.
|
||||
if not _is_valid_date(dob):
|
||||
_show_status("Date of birth must be in YYYY-MM-DD format.")
|
||||
return
|
||||
|
||||
_auth_button.disabled = true
|
||||
_auth_button.text = "Registering..."
|
||||
_is_authenticating = true
|
||||
_clear_status()
|
||||
|
||||
var result := await ApiClient.register(username, email, password, dob)
|
||||
_on_auth_complete(result)
|
||||
else:
|
||||
_auth_button.disabled = true
|
||||
_auth_button.text = "Logging in..."
|
||||
_is_authenticating = true
|
||||
_clear_status()
|
||||
|
||||
var result := await ApiClient.login(username, password)
|
||||
_on_auth_complete(result)
|
||||
|
||||
|
||||
func _on_auth_complete(result: Dictionary) -> void:
|
||||
"""Handle the result of a login or register call."""
|
||||
_auth_button.disabled = false
|
||||
_auth_button.text = "Register" if _is_register_mode else "Login"
|
||||
_is_authenticating = false
|
||||
|
||||
if result.ok:
|
||||
# Success — show welcome, enable Play.
|
||||
_welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges()
|
||||
_welcome_label.visible = true
|
||||
_status_label.visible = false
|
||||
_play_button.disabled = false
|
||||
print("Auth successful. Token stored.")
|
||||
else:
|
||||
# Failure — show backend error detail.
|
||||
_show_status(result.detail)
|
||||
|
||||
|
||||
# ── Status helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
_status_label.text = message
|
||||
_status_label.visible = true
|
||||
|
||||
|
||||
func _clear_status() -> void:
|
||||
_status_label.text = ""
|
||||
_status_label.visible = false
|
||||
|
||||
|
||||
# ── Validation ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _is_valid_date(date_str: String) -> bool:
|
||||
"""Check if a string looks like a valid YYYY-MM-DD date."""
|
||||
if date_str.length() != 10:
|
||||
return false
|
||||
var parts := date_str.split("-")
|
||||
if parts.size() != 3:
|
||||
return false
|
||||
var year := parts[0].to_int()
|
||||
var month := parts[1].to_int()
|
||||
var day := parts[2].to_int()
|
||||
if month < 1 or month > 12:
|
||||
return false
|
||||
if day < 1 or day > 31:
|
||||
return false
|
||||
# Rough month-day validation.
|
||||
var days_in_month := [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
if day > days_in_month[month]:
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
extends SceneTree
|
||||
## Headless smoke test for the world list screen.
|
||||
##
|
||||
## Run with:
|
||||
## godot --headless --path . --script res://scripts/smoke_test_world_list.gd
|
||||
##
|
||||
## Requires the backend to be running (default base URL
|
||||
## `application/agp/api_base_url` -> http://127.0.0.1:8000, override with
|
||||
## AGP_API_BASE_URL).
|
||||
##
|
||||
## Unlike a pure API-level test, this actually instantiates scenes/
|
||||
## world_list.tscn as a real scene and adds it to the tree -- catches
|
||||
## @onready node-path bugs (a node reference pointing at the wrong path
|
||||
## errors right here, not just "looks right" from reading the script) that
|
||||
## an API-only test would miss entirely.
|
||||
##
|
||||
## Tests:
|
||||
## 1. Register + log in a fresh user (so get_worlds() has a valid token).
|
||||
## 2. Instantiate world_list.tscn, add to tree -- @onready resolution
|
||||
## must not error.
|
||||
## 3. Wait for _refresh_worlds()'s awaited request to complete.
|
||||
## 4. Confirm the screen reached a real end state (world cards, or the
|
||||
## empty-list message -- either is fine, a crash/stuck-loading isn't).
|
||||
##
|
||||
## Exits 0 on full pass, 1 on any failure.
|
||||
|
||||
const WORLD_LIST_SCENE := "res://scenes/world_list.tscn"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
var api := root.get_node("ApiClient")
|
||||
assert(api != null, "ApiClient autoload not found")
|
||||
|
||||
var passed: int = 0
|
||||
var failed: int = 0
|
||||
|
||||
# --- Setup: register + log in so the world list has a valid token ---
|
||||
print("\n=== Setup: register + login ===")
|
||||
randomize()
|
||||
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
||||
var username := "worldlist_smoke_%d" % unique_id
|
||||
var email := "worldlist_smoke_%d@test.example.com" % unique_id
|
||||
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", "2000-01-01")
|
||||
if not _check("setup: register succeeded", reg_result.ok):
|
||||
print(" detail: ", reg_result.detail)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# --- Test 1: Instantiate the real scene ---
|
||||
print("\n=== Test 1: Instantiate world_list.tscn ===")
|
||||
var packed_scene: PackedScene = load(WORLD_LIST_SCENE)
|
||||
if not _check("scene loads", packed_scene != null):
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var instance: Control = packed_scene.instantiate()
|
||||
if not _check("scene instantiates", instance != null):
|
||||
quit(1)
|
||||
return
|
||||
|
||||
root.add_child(instance)
|
||||
# One frame for _ready() (and its @onready resolution) to run. A wrong
|
||||
# node path in an @onready var errors during this, not silently.
|
||||
await process_frame
|
||||
|
||||
if _check("scene added to tree without error", is_instance_valid(instance)):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 2: Wait for the world fetch to resolve, check a real end state ---
|
||||
print("\n=== Test 2: World fetch reaches a real end state ===")
|
||||
# _refresh_worlds() runs from _ready(); give its awaited HTTP call time
|
||||
# to complete rather than a fixed sleep -- poll for either end state.
|
||||
var world_list_node: VBoxContainer = instance.get_node("VBoxContainer/ScrollContainer/WorldList")
|
||||
var empty_label: Label = instance.get_node("VBoxContainer/EmptyLabel")
|
||||
var error_label: Label = instance.get_node("VBoxContainer/ErrorLabel")
|
||||
|
||||
var settled := false
|
||||
for i in range(50): # up to ~5s at 100ms/frame-ish via process_frame waits
|
||||
await process_frame
|
||||
if world_list_node.visible or empty_label.visible or error_label.visible:
|
||||
settled = true
|
||||
break
|
||||
|
||||
if _check("reached a real end state (cards, empty, or error) not stuck loading", settled):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
print(" world_list.visible=%s empty_label.visible=%s error_label.visible=%s" % [
|
||||
world_list_node.visible, empty_label.visible, error_label.visible
|
||||
])
|
||||
|
||||
if error_label.visible:
|
||||
printerr(" Note: ended in error state: ", error_label.text)
|
||||
elif world_list_node.visible:
|
||||
print(" Note: rendered %d world card(s)" % world_list_node.get_child_count())
|
||||
elif empty_label.visible:
|
||||
print(" Note: ended in empty-list state")
|
||||
|
||||
# --- Summary ---
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
if failed > 0:
|
||||
quit(1)
|
||||
else:
|
||||
print("SMOKE RESULT: ALL PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> bool:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
return true
|
||||
else:
|
||||
printerr(" FAIL: ", description)
|
||||
return false
|
||||
@@ -10,8 +10,8 @@ extends Control
|
||||
@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 refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton
|
||||
@onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton
|
||||
@onready var empty_label: Label = $VBoxContainer/EmptyLabel
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ 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
|
||||
card.add_theme_constant_override("separation", 4)
|
||||
|
||||
var name_label := Label.new()
|
||||
name_label.text = world_dict.get("name", "Unnamed World")
|
||||
@@ -101,7 +101,7 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
||||
name_label.add_theme_font_size_override("font_size", 18)
|
||||
|
||||
var desc_label := Label.new()
|
||||
var description := world_dict.get("description", "")
|
||||
var description: String = str(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
|
||||
@@ -110,8 +110,8 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
||||
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", "")
|
||||
var visibility: String = str(world_dict.get("visibility", "public"))
|
||||
var created: String = str(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
|
||||
|
||||
Reference in New Issue
Block a user