Fix build stamp visibility, add create world UI, and add Save as New World to editor
checkpoint / checkpoint (push) Successful in 5m54s
checkpoint / checkpoint (push) Successful in 5m54s
This commit is contained in:
@@ -22,6 +22,7 @@ Session="*res://scripts/session.gd"
|
|||||||
window/size/viewport_width=1920
|
window/size/viewport_width=1920
|
||||||
window/size/viewport_height=1080
|
window/size/viewport_height=1080
|
||||||
window/stretch/mode="canvas_items"
|
window/stretch/mode="canvas_items"
|
||||||
|
window/stretch/aspect="keep"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ horizontal_alignment = 1
|
|||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_font_sizes/font_size = 28
|
theme_override_font_sizes/font_size = 28
|
||||||
|
|
||||||
|
[node name="CreateButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
|
layout_mode = 3
|
||||||
|
text = "+ Create World"
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
|
||||||
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
text = "Refresh"
|
text = "Refresh"
|
||||||
@@ -75,4 +80,5 @@ horizontal_alignment = 1
|
|||||||
theme_override_font_sizes/font_size = 18
|
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/BackButton" to="." method="_on_back_button_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HeaderHBox/CreateButton" to="." method="_on_create_button_pressed"]
|
||||||
[connection signal="pressed" from="VBoxContainer/HeaderHBox/RefreshButton" to="." method="_on_refresh_button_pressed"]
|
[connection signal="pressed" from="VBoxContainer/HeaderHBox/RefreshButton" to="." method="_on_refresh_button_pressed"]
|
||||||
|
|||||||
@@ -247,6 +247,19 @@ func get_worlds(skip: int = 0, limit: int = 50, visibility: String = "") -> Dict
|
|||||||
return await request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
return await request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
||||||
|
|
||||||
|
|
||||||
|
## Create a new world owned by the current user (requires auth).
|
||||||
|
## On success, result.data is the created World object containing {"id", "name", ...}.
|
||||||
|
func create_world(world_name: String, description: String = "", visibility: String = "public", category: String = "Building", tags: Array = []) -> Dictionary:
|
||||||
|
var payload: Dictionary = {
|
||||||
|
"name": world_name,
|
||||||
|
"description": description,
|
||||||
|
"visibility": visibility,
|
||||||
|
"category": category,
|
||||||
|
"tags": tags,
|
||||||
|
}
|
||||||
|
return await request(HTTPClient.METHOD_POST, "/api/worlds/", JSON.stringify(payload), "application/json", true)
|
||||||
|
|
||||||
|
|
||||||
## Ping the backend health endpoint (no auth).
|
## Ping the backend health endpoint (no auth).
|
||||||
func health_check() -> Dictionary:
|
func health_check() -> Dictionary:
|
||||||
return await request(HTTPClient.METHOD_GET, "/api/health", "", "application/json", false)
|
return await request(HTTPClient.METHOD_GET, "/api/health", "", "application/json", false)
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ var _inspector: InspectorPanel
|
|||||||
var _viewport: EditorViewport
|
var _viewport: EditorViewport
|
||||||
var _close_button: Button
|
var _close_button: Button
|
||||||
var _save_button: Button
|
var _save_button: Button
|
||||||
|
var _save_as_button: Button
|
||||||
var _save_status_label: Label
|
var _save_status_label: Label
|
||||||
var _instruction_label: Label
|
var _instruction_label: Label
|
||||||
|
var _save_as_modal: Control = null
|
||||||
|
|
||||||
|
|
||||||
## Save-path state (world_persistence_impl.md §8.1/§8.2), set via
|
## Save-path state (world_persistence_impl.md §8.1/§8.2), set via
|
||||||
## `setup_persistence()`. `_save_service == null` means there is nothing to
|
## `setup_persistence()`. `_save_service == null` means there is nothing to
|
||||||
@@ -103,11 +106,17 @@ func _build_ui() -> void:
|
|||||||
|
|
||||||
_save_button = Button.new()
|
_save_button = Button.new()
|
||||||
_save_button.text = "Save (Ctrl+S)"
|
_save_button.text = "Save (Ctrl+S)"
|
||||||
_save_button.disabled = (_save_service == null)
|
_save_button.tooltip_text = "Save changes to server (Ctrl+S)"
|
||||||
_save_button.tooltip_text = "Save changes to server (Ctrl+S)" if _save_service != null else "Saving not available in scratch mode"
|
|
||||||
_save_button.pressed.connect(_on_save_pressed)
|
_save_button.pressed.connect(_on_save_pressed)
|
||||||
top_bar.add_child(_save_button)
|
top_bar.add_child(_save_button)
|
||||||
|
|
||||||
|
_save_as_button = Button.new()
|
||||||
|
_save_as_button.text = "Save as New World"
|
||||||
|
_save_as_button.tooltip_text = "Save this creation as a new world you own on the server"
|
||||||
|
_save_as_button.pressed.connect(_on_save_as_pressed)
|
||||||
|
top_bar.add_child(_save_as_button)
|
||||||
|
|
||||||
|
|
||||||
_close_button = Button.new()
|
_close_button = Button.new()
|
||||||
_close_button.text = "Close (F2 / Esc)"
|
_close_button.text = "Close (F2 / Esc)"
|
||||||
_close_button.tooltip_text = "Exit build mode and return to game"
|
_close_button.tooltip_text = "Exit build mode and return to game"
|
||||||
@@ -285,8 +294,13 @@ func _on_close_pressed() -> void:
|
|||||||
## (§8.2/§5.4) -- this just reflects the outcome and, per §6 D4, surfaces
|
## (§8.2/§5.4) -- this just reflects the outcome and, per §6 D4, surfaces
|
||||||
## the `live_session` flag as a warning instead of ignoring it.
|
## the `live_session` flag as a warning instead of ignoring it.
|
||||||
func _on_save_pressed() -> void:
|
func _on_save_pressed() -> void:
|
||||||
if _save_service == null or _saving:
|
if _saving:
|
||||||
return
|
return
|
||||||
|
if _save_service == null:
|
||||||
|
_set_save_status("No world selected to save to. Choose 'Save as New World'.", Color(1.0, 0.8, 0.25))
|
||||||
|
_show_save_as_modal()
|
||||||
|
return
|
||||||
|
|
||||||
_saving = true
|
_saving = true
|
||||||
_save_button.disabled = true
|
_save_button.disabled = true
|
||||||
_set_save_status("Saving…", Color(0.85, 0.85, 0.85))
|
_set_save_status("Saving…", Color(0.85, 0.85, 0.85))
|
||||||
@@ -307,7 +321,174 @@ func _on_save_pressed() -> void:
|
|||||||
else:
|
else:
|
||||||
_set_save_status(msg, Color(0.55, 1.0, 0.55))
|
_set_save_status(msg, Color(0.55, 1.0, 0.55))
|
||||||
else:
|
else:
|
||||||
_set_save_status("Save failed: %s" % str(result.get("detail", "unknown error")), Color(1.0, 0.45, 0.45))
|
var status_code := int(result.get("status", 0))
|
||||||
|
var detail := str(result.get("detail", "unknown error"))
|
||||||
|
if status_code == 403 or "owner" in detail.to_lower():
|
||||||
|
_set_save_status("Save failed: You do not own world %s. Use 'Save as New World' to save your work." % _world_id, Color(1.0, 0.45, 0.45))
|
||||||
|
_show_save_as_modal()
|
||||||
|
else:
|
||||||
|
_set_save_status("Save failed: %s" % detail, Color(1.0, 0.45, 0.45))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save_as_pressed() -> void:
|
||||||
|
_show_save_as_modal()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_save_as_modal() -> void:
|
||||||
|
if _save_as_modal != null and is_instance_valid(_save_as_modal):
|
||||||
|
_save_as_modal.queue_free()
|
||||||
|
|
||||||
|
var overlay := ColorRect.new()
|
||||||
|
overlay.name = "SaveAsOverlay"
|
||||||
|
overlay.color = Color(0, 0, 0, 0.75)
|
||||||
|
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.add_child(center)
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = Vector2(460, 0)
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.18, 0.18, 0.22, 1.0)
|
||||||
|
style.border_color = Color(0.4, 0.6, 0.9, 1.0)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 24
|
||||||
|
style.content_margin_right = 24
|
||||||
|
style.content_margin_top = 20
|
||||||
|
style.content_margin_bottom = 20
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
center.add_child(panel)
|
||||||
|
|
||||||
|
var form := VBoxContainer.new()
|
||||||
|
form.add_theme_constant_override("separation", 12)
|
||||||
|
panel.add_child(form)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Save as New World"
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title.add_theme_font_size_override("font_size", 20)
|
||||||
|
form.add_child(title)
|
||||||
|
|
||||||
|
var hint := Label.new()
|
||||||
|
hint.text = "Creates a new world owned by you and saves your current build into it."
|
||||||
|
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
hint.add_theme_font_size_override("font_size", 13)
|
||||||
|
hint.add_theme_color_override("font_color", Color(0.75, 0.75, 0.75))
|
||||||
|
form.add_child(hint)
|
||||||
|
|
||||||
|
var name_lbl := Label.new()
|
||||||
|
name_lbl.text = "World Name:"
|
||||||
|
name_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
|
form.add_child(name_lbl)
|
||||||
|
|
||||||
|
var name_input := LineEdit.new()
|
||||||
|
name_input.placeholder_text = "Enter world name..."
|
||||||
|
name_input.text = "My Creation" if (_world_id.is_empty() or _world_id == "0") else "Remix of World %s" % _world_id
|
||||||
|
form.add_child(name_input)
|
||||||
|
|
||||||
|
var desc_lbl := Label.new()
|
||||||
|
desc_lbl.text = "Description:"
|
||||||
|
desc_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
|
form.add_child(desc_lbl)
|
||||||
|
|
||||||
|
var desc_input := LineEdit.new()
|
||||||
|
desc_input.placeholder_text = "Describe your world..."
|
||||||
|
desc_input.text = "Created in editor"
|
||||||
|
form.add_child(desc_input)
|
||||||
|
|
||||||
|
var err_label := Label.new()
|
||||||
|
err_label.add_theme_font_size_override("font_size", 13)
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
err_label.visible = false
|
||||||
|
form.add_child(err_label)
|
||||||
|
|
||||||
|
var btn_row := HBoxContainer.new()
|
||||||
|
btn_row.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
btn_row.add_theme_constant_override("separation", 10)
|
||||||
|
form.add_child(btn_row)
|
||||||
|
|
||||||
|
var cancel_btn := Button.new()
|
||||||
|
cancel_btn.text = "Cancel"
|
||||||
|
cancel_btn.pressed.connect(func(): overlay.queue_free())
|
||||||
|
btn_row.add_child(cancel_btn)
|
||||||
|
|
||||||
|
var save_btn := Button.new()
|
||||||
|
save_btn.text = "Save New World"
|
||||||
|
btn_row.add_child(save_btn)
|
||||||
|
|
||||||
|
save_btn.pressed.connect(func() -> void:
|
||||||
|
var wname := name_input.text.strip_edges()
|
||||||
|
if wname.is_empty():
|
||||||
|
err_label.text = "World name cannot be empty."
|
||||||
|
err_label.visible = true
|
||||||
|
return
|
||||||
|
save_btn.disabled = true
|
||||||
|
cancel_btn.disabled = true
|
||||||
|
err_label.text = "Creating world..."
|
||||||
|
err_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||||
|
err_label.visible = true
|
||||||
|
|
||||||
|
var api := _api_client()
|
||||||
|
if api == null:
|
||||||
|
save_btn.disabled = false
|
||||||
|
cancel_btn.disabled = false
|
||||||
|
err_label.text = "API client not available."
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
return
|
||||||
|
|
||||||
|
var res: Dictionary = await api.create_world(wname, desc_input.text.strip_edges())
|
||||||
|
if not res.get("ok", false):
|
||||||
|
save_btn.disabled = false
|
||||||
|
cancel_btn.disabled = false
|
||||||
|
err_label.text = "Failed to create world: %s" % str(res.get("detail", "unknown error"))
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
return
|
||||||
|
|
||||||
|
var new_world: Dictionary = res.get("data", {})
|
||||||
|
var new_id: int = int(new_world.get("id", 0))
|
||||||
|
var new_id_str: String = str(new_id)
|
||||||
|
|
||||||
|
err_label.text = "Saving chunk data..."
|
||||||
|
if _save_service == null:
|
||||||
|
_save_service = ChunkSaveService.new()
|
||||||
|
add_child(_save_service)
|
||||||
|
_save_service.world_id = new_id_str
|
||||||
|
_save_service.room_slug = "main"
|
||||||
|
|
||||||
|
_world_id = new_id_str
|
||||||
|
_room_slug = "main"
|
||||||
|
_coord = Vector3i.ZERO
|
||||||
|
_base_revision = 0
|
||||||
|
var sess := _session()
|
||||||
|
if sess != null:
|
||||||
|
sess.world_id = new_id
|
||||||
|
|
||||||
|
core.model.payload.world_id = _world_id
|
||||||
|
core.model.payload.room_id = _room_slug
|
||||||
|
core.model.payload.coord = _coord
|
||||||
|
|
||||||
|
|
||||||
|
var chunk_res: Dictionary = await _save_service.save_chunk(core.model, _coord, 0)
|
||||||
|
if not chunk_res.get("ok", false):
|
||||||
|
save_btn.disabled = false
|
||||||
|
cancel_btn.disabled = false
|
||||||
|
err_label.text = "World created (ID: %d) but chunk save failed: %s" % [new_id, str(chunk_res.get("detail", "unknown error"))]
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
return
|
||||||
|
|
||||||
|
_base_revision = int(chunk_res.get("revision", 1))
|
||||||
|
_save_button.disabled = false
|
||||||
|
_save_button.tooltip_text = "Save chunk changes to backend (Ctrl+S)"
|
||||||
|
_set_save_status("Saved successfully as new world '%s' (ID: %d, revision %d)!" % [wname, new_id, _base_revision], Color(0.55, 1.0, 0.55))
|
||||||
|
overlay.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
_save_as_modal = overlay
|
||||||
|
add_child(overlay)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _set_save_status(text: String, color: Color) -> void:
|
func _set_save_status(text: String, color: Color) -> void:
|
||||||
@@ -316,3 +497,12 @@ func _set_save_status(text: String, color: Color) -> void:
|
|||||||
_save_status_label.text = text
|
_save_status_label.text = text
|
||||||
_save_status_label.add_theme_color_override("font_color", color)
|
_save_status_label.add_theme_color_override("font_color", color)
|
||||||
_save_status_label.visible = true
|
_save_status_label.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func _api_client() -> Node:
|
||||||
|
return get_node_or_null("/root/ApiClient")
|
||||||
|
|
||||||
|
|
||||||
|
func _session() -> Node:
|
||||||
|
return get_node_or_null("/root/Session")
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -224,21 +224,22 @@ func _show_build_stamp() -> void:
|
|||||||
add_child(layer)
|
add_child(layer)
|
||||||
var lbl := Label.new()
|
var lbl := Label.new()
|
||||||
lbl.text = "build %s" % _build_label()
|
lbl.text = "build %s" % _build_label()
|
||||||
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35, 0.85))
|
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35, 1.0))
|
||||||
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
|
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1.0))
|
||||||
lbl.add_theme_constant_override("outline_size", 4)
|
lbl.add_theme_constant_override("outline_size", 6)
|
||||||
lbl.add_theme_font_size_override("font_size", 14)
|
lbl.add_theme_font_size_override("font_size", 18)
|
||||||
# Pinned to the BOTTOM-right so it never blocks top menu or editor toolbars.
|
|
||||||
lbl.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
|
||||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
||||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
|
||||||
lbl.offset_left = 12
|
|
||||||
lbl.offset_right = -16
|
|
||||||
lbl.offset_top = -32
|
|
||||||
lbl.offset_bottom = -8
|
|
||||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
layer.add_child(lbl)
|
layer.add_child(lbl)
|
||||||
|
|
||||||
|
var reposition := func() -> void:
|
||||||
|
if is_instance_valid(lbl) and is_inside_tree():
|
||||||
|
var vp := get_viewport().get_visible_rect().size
|
||||||
|
lbl.position = Vector2(20, vp.y - 40)
|
||||||
|
lbl.size = Vector2(vp.x - 40, 32)
|
||||||
|
reposition.call()
|
||||||
|
get_viewport().size_changed.connect(reposition)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
# Backstop for any exit path that didn't go through _leave_world (e.g. the
|
# Backstop for any exit path that didn't go through _leave_world (e.g. the
|
||||||
|
|||||||
+170
-3
@@ -12,6 +12,7 @@ extends Control
|
|||||||
@onready var error_label: Label = $VBoxContainer/ErrorLabel
|
@onready var error_label: Label = $VBoxContainer/ErrorLabel
|
||||||
@onready var refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton
|
@onready var refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton
|
||||||
@onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton
|
@onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton
|
||||||
|
@onready var create_button: Button = $VBoxContainer/HeaderHBox/CreateButton
|
||||||
@onready var empty_label: Label = $VBoxContainer/EmptyLabel
|
@onready var empty_label: Label = $VBoxContainer/EmptyLabel
|
||||||
|
|
||||||
## Cached so each world card can decide whether to show "Edit World"
|
## Cached so each world card can decide whether to show "Edit World"
|
||||||
@@ -20,16 +21,20 @@ extends Control
|
|||||||
## round trip per card. -1 until resolved / on failure, in which case no
|
## round trip per card. -1 until resolved / on failure, in which case no
|
||||||
## card shows the edit entry point.
|
## card shows the edit entry point.
|
||||||
var _current_user_id: int = -1
|
var _current_user_id: int = -1
|
||||||
|
var _create_modal: Control = null
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
refresh_button.disabled = false
|
refresh_button.disabled = false
|
||||||
back_button.disabled = false
|
back_button.disabled = false
|
||||||
|
create_button.disabled = false
|
||||||
loading_label.visible = false
|
loading_label.visible = false
|
||||||
error_label.visible = false
|
error_label.visible = false
|
||||||
empty_label.visible = false
|
empty_label.visible = false
|
||||||
world_list.visible = false
|
world_list.visible = false
|
||||||
|
|
||||||
|
_show_build_stamp()
|
||||||
|
|
||||||
var me_result: Dictionary = await ApiClient.get_me()
|
var me_result: Dictionary = await ApiClient.get_me()
|
||||||
if me_result.ok and me_result.data is Dictionary:
|
if me_result.ok and me_result.data is Dictionary:
|
||||||
_current_user_id = int((me_result.data as Dictionary).get("id", -1))
|
_current_user_id = int((me_result.data as Dictionary).get("id", -1))
|
||||||
@@ -37,6 +42,157 @@ func _ready() -> void:
|
|||||||
_refresh_worlds()
|
_refresh_worlds()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _show_build_stamp() -> void:
|
||||||
|
var lbl := Label.new()
|
||||||
|
var f := FileAccess.open("res://assets/build_info.json", FileAccess.READ)
|
||||||
|
var stamp := "dev"
|
||||||
|
if f != null:
|
||||||
|
var d = JSON.parse_string(f.get_as_text())
|
||||||
|
if d is Dictionary:
|
||||||
|
stamp = "%s %s" % [d.get("checkpoint", "?"), d.get("commit", "?")]
|
||||||
|
lbl.text = "build %s" % stamp
|
||||||
|
lbl.add_theme_color_override("font_color", Color(1, 1, 0.35, 1.0))
|
||||||
|
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1.0))
|
||||||
|
lbl.add_theme_constant_override("outline_size", 6)
|
||||||
|
lbl.add_theme_font_size_override("font_size", 18)
|
||||||
|
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(lbl)
|
||||||
|
|
||||||
|
var reposition := func() -> void:
|
||||||
|
if is_instance_valid(lbl) and is_inside_tree():
|
||||||
|
var vp := get_viewport().get_visible_rect().size
|
||||||
|
lbl.position = Vector2(20, vp.y - 40)
|
||||||
|
lbl.size = Vector2(vp.x - 40, 32)
|
||||||
|
reposition.call()
|
||||||
|
get_viewport().size_changed.connect(reposition)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_create_button_pressed() -> void:
|
||||||
|
if _create_modal != null and is_instance_valid(_create_modal):
|
||||||
|
_create_modal.queue_free()
|
||||||
|
_create_modal = _build_create_world_modal()
|
||||||
|
add_child(_create_modal)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_create_world_modal() -> Control:
|
||||||
|
var overlay := ColorRect.new()
|
||||||
|
overlay.name = "CreateWorldOverlay"
|
||||||
|
overlay.color = Color(0, 0, 0, 0.75)
|
||||||
|
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.add_child(center)
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = Vector2(480, 0)
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.18, 0.18, 0.22, 1.0)
|
||||||
|
style.border_color = Color(0.4, 0.55, 0.8, 1.0)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 24
|
||||||
|
style.content_margin_right = 24
|
||||||
|
style.content_margin_top = 20
|
||||||
|
style.content_margin_bottom = 20
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
center.add_child(panel)
|
||||||
|
|
||||||
|
var form := VBoxContainer.new()
|
||||||
|
form.add_theme_constant_override("separation", 12)
|
||||||
|
panel.add_child(form)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Create New World"
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title.add_theme_font_size_override("font_size", 22)
|
||||||
|
title.add_theme_color_override("font_color", Color(1.0, 1.0, 1.0))
|
||||||
|
form.add_child(title)
|
||||||
|
|
||||||
|
var name_lbl := Label.new()
|
||||||
|
name_lbl.text = "World Name:"
|
||||||
|
name_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
|
form.add_child(name_lbl)
|
||||||
|
|
||||||
|
var name_input := LineEdit.new()
|
||||||
|
name_input.placeholder_text = "Enter world name..."
|
||||||
|
name_input.text = "My New World"
|
||||||
|
form.add_child(name_input)
|
||||||
|
|
||||||
|
var desc_lbl := Label.new()
|
||||||
|
desc_lbl.text = "Description:"
|
||||||
|
desc_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
|
form.add_child(desc_lbl)
|
||||||
|
|
||||||
|
var desc_input := LineEdit.new()
|
||||||
|
desc_input.placeholder_text = "Describe your world..."
|
||||||
|
desc_input.text = "A creative build world"
|
||||||
|
form.add_child(desc_input)
|
||||||
|
|
||||||
|
var err_label := Label.new()
|
||||||
|
err_label.add_theme_font_size_override("font_size", 13)
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
err_label.visible = false
|
||||||
|
form.add_child(err_label)
|
||||||
|
|
||||||
|
var btn_row := HBoxContainer.new()
|
||||||
|
btn_row.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
btn_row.add_theme_constant_override("separation", 10)
|
||||||
|
form.add_child(btn_row)
|
||||||
|
|
||||||
|
var cancel_btn := Button.new()
|
||||||
|
cancel_btn.text = "Cancel"
|
||||||
|
cancel_btn.pressed.connect(func(): overlay.queue_free())
|
||||||
|
btn_row.add_child(cancel_btn)
|
||||||
|
|
||||||
|
var create_btn := Button.new()
|
||||||
|
create_btn.text = "Create World"
|
||||||
|
btn_row.add_child(create_btn)
|
||||||
|
|
||||||
|
var create_edit_btn := Button.new()
|
||||||
|
create_edit_btn.text = "Create & Edit Now"
|
||||||
|
btn_row.add_child(create_edit_btn)
|
||||||
|
|
||||||
|
var handle_create := func(open_editor: bool) -> void:
|
||||||
|
var wname := name_input.text.strip_edges()
|
||||||
|
if wname.is_empty():
|
||||||
|
err_label.text = "World name cannot be empty."
|
||||||
|
err_label.visible = true
|
||||||
|
return
|
||||||
|
create_btn.disabled = true
|
||||||
|
create_edit_btn.disabled = true
|
||||||
|
cancel_btn.disabled = true
|
||||||
|
err_label.text = "Creating world..."
|
||||||
|
err_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||||
|
err_label.visible = true
|
||||||
|
|
||||||
|
var res: Dictionary = await ApiClient.create_world(wname, desc_input.text.strip_edges())
|
||||||
|
if not res.get("ok", false):
|
||||||
|
create_btn.disabled = false
|
||||||
|
create_edit_btn.disabled = false
|
||||||
|
cancel_btn.disabled = false
|
||||||
|
err_label.text = "Failed: %s" % str(res.get("detail", "unknown error"))
|
||||||
|
err_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
return
|
||||||
|
|
||||||
|
var new_world: Dictionary = res.get("data", {})
|
||||||
|
var new_world_id: int = int(new_world.get("id", 0))
|
||||||
|
overlay.queue_free()
|
||||||
|
|
||||||
|
if open_editor and new_world_id > 0:
|
||||||
|
Session.begin_edit(new_world_id)
|
||||||
|
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||||
|
else:
|
||||||
|
_refresh_worlds()
|
||||||
|
|
||||||
|
create_btn.pressed.connect(handle_create.bind(false))
|
||||||
|
create_edit_btn.pressed.connect(handle_create.bind(true))
|
||||||
|
|
||||||
|
return overlay
|
||||||
|
|
||||||
|
|
||||||
func _on_refresh_button_pressed() -> void:
|
func _on_refresh_button_pressed() -> void:
|
||||||
_refresh_worlds()
|
_refresh_worlds()
|
||||||
|
|
||||||
@@ -45,6 +201,7 @@ func _on_back_button_pressed() -> void:
|
|||||||
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Fetch the world list from the backend and update the UI.
|
## Fetch the world list from the backend and update the UI.
|
||||||
func _refresh_worlds() -> void:
|
func _refresh_worlds() -> void:
|
||||||
# Reset UI state
|
# Reset UI state
|
||||||
@@ -121,13 +278,24 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
|||||||
card.add_theme_constant_override("separation", 4)
|
card.add_theme_constant_override("separation", 4)
|
||||||
panel.add_child(card)
|
panel.add_child(card)
|
||||||
|
|
||||||
|
var owner_id := int(world_dict.get("owner_id", -1))
|
||||||
|
var is_owner := (_current_user_id != -1 and owner_id == _current_user_id)
|
||||||
|
|
||||||
var name_label := Label.new()
|
var name_label := Label.new()
|
||||||
name_label.text = world_dict.get("name", "Unnamed World")
|
var raw_name: String = world_dict.get("name", "Unnamed World")
|
||||||
|
if is_owner:
|
||||||
|
name_label.text = "%s ★ [Your World]" % raw_name
|
||||||
|
name_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4, 1.0))
|
||||||
|
style.border_color = Color(0.7, 0.6, 0.2, 0.8)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
else:
|
||||||
|
name_label.text = raw_name
|
||||||
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
name_label.size_flags_horizontal = Control.SIZE_FILL
|
name_label.size_flags_horizontal = Control.SIZE_FILL
|
||||||
name_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
name_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
name_label.add_theme_font_size_override("font_size", 18)
|
name_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
|
||||||
|
|
||||||
var desc_label := Label.new()
|
var desc_label := Label.new()
|
||||||
var description: String = str(world_dict.get("description", ""))
|
var description: String = str(world_dict.get("description", ""))
|
||||||
desc_label.text = description if description != "" else "No description."
|
desc_label.text = description if description != "" else "No description."
|
||||||
@@ -166,8 +334,7 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
|||||||
# shown for the world's owner, matching the write-side authorization the
|
# shown for the world's owner, matching the write-side authorization the
|
||||||
# save path itself enforces (§5.3), so a non-owner never sees a button
|
# save path itself enforces (§5.3), so a non-owner never sees a button
|
||||||
# that would just 403 on the first save.
|
# that would just 403 on the first save.
|
||||||
var owner_id := int(world_dict.get("owner_id", -1))
|
if is_owner:
|
||||||
if _current_user_id != -1 and owner_id == _current_user_id:
|
|
||||||
var edit_button := Button.new()
|
var edit_button := Button.new()
|
||||||
edit_button.text = "Edit World"
|
edit_button.text = "Edit World"
|
||||||
edit_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
edit_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
|
|||||||
Reference in New Issue
Block a user