feat(mvp): complete M13 clean-up milestone (real usernames, avatar studio redesign, pause menu, audio manager)
checkpoint / checkpoint (push) Successful in 6m22s

This commit is contained in:
2026-09-18 21:54:01 +00:00
parent 4c8a60fbc8
commit ee4006d217
31 changed files with 2394 additions and 262 deletions
+175 -25
View File
@@ -36,11 +36,15 @@ var _status_label: Label = null
## normal play"). Editing is in-memory only here; the save path to the
## backend is a sibling task (world_persistence_impl.md).
const ReportModal = preload("res://scripts/report_modal.gd")
const PauseMenu = preload("res://scripts/pause_menu.gd")
const ControlsModal = preload("res://scripts/controls_modal.gd")
var _build_editor: InGameEditor = null
var _chat_overlay: Control = null
var _friends_overlay: Control = null
var _report_modal: Control = null
var _pause_menu: Control = null
var _controls_modal: Control = null
var _hud_layer: CanvasLayer = null
var _overlay_layer: CanvasLayer = null
@@ -51,13 +55,13 @@ var _loaded_chunk_coords: Array[Vector3i] = []
func _ready() -> void:
_show_build_stamp()
_setup_in_game_hud()
_setup_chunk_streaming()
if Session.editing:
_open_editor_for_world()
else:
_setup_in_game_hud()
if Session.active:
_setup_networked()
_setup_chunk_streaming()
func _setup_chunk_streaming() -> void:
@@ -175,26 +179,22 @@ func _hide_default_test_props() -> void:
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_T or (event.keycode == KEY_ENTER and not (_chat_overlay != null and is_instance_valid(_chat_overlay))):
if not Session.editing and not (_build_editor != null and is_instance_valid(_build_editor)):
if not (_build_editor != null and is_instance_valid(_build_editor)) and not (_pause_menu != null and is_instance_valid(_pause_menu)):
get_viewport().set_input_as_handled()
_toggle_chat_overlay()
return
elif event.keycode == KEY_TAB or event.keycode == KEY_F1:
if not Session.editing and not (_build_editor != null and is_instance_valid(_build_editor)):
if not (_build_editor != null and is_instance_valid(_build_editor)) and not (_pause_menu != null and is_instance_valid(_pause_menu)):
get_viewport().set_input_as_handled()
_toggle_friends_overlay()
return
if event.is_action_pressed("toggle_build_mode"):
# world_persistence_impl.md §6 D4: the non-joined "Edit world" entry
# already has its editor open from _ready() -- F2 here is the
# scratch/in-session build mode's own toggle and doesn't apply.
if not Session.editing:
get_viewport().set_input_as_handled()
_toggle_build_mode()
get_viewport().set_input_as_handled()
_toggle_build_mode()
return
# ESC leaves the world back to the browser (or closes an open overlay/editor).
# ESC opens Pause Menu (or closes open modal/overlay/editor).
if event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
if _report_modal != null and is_instance_valid(_report_modal):
@@ -203,21 +203,19 @@ func _unhandled_input(event: InputEvent) -> void:
_close_chat_overlay()
elif _friends_overlay != null and is_instance_valid(_friends_overlay):
_close_friends_overlay()
elif Session.editing:
# Non-joined editing has no play session to fall back into --
# closing it means leaving, same as the Close button.
_exit_editor_to_browser()
elif _controls_modal != null and is_instance_valid(_controls_modal):
_close_controls_modal()
elif _pause_menu != null and is_instance_valid(_pause_menu):
_close_pause_menu()
elif _build_editor != null and is_instance_valid(_build_editor):
_close_build_mode()
else:
_leave_world()
_open_pause_menu()
# ─── In-Game HUD & Social Overlays ───────────────────────────────────────────
func _setup_in_game_hud() -> void:
if Session.editing:
return
if _hud_layer != null and is_instance_valid(_hud_layer):
return
_hud_layer = CanvasLayer.new()
@@ -274,13 +272,69 @@ func _setup_in_game_hud() -> void:
_close_chat_overlay()
elif _friends_overlay != null and is_instance_valid(_friends_overlay):
_close_friends_overlay()
elif _controls_modal != null and is_instance_valid(_controls_modal):
_close_controls_modal()
elif _pause_menu != null and is_instance_valid(_pause_menu):
_close_pause_menu()
elif _build_editor != null and is_instance_valid(_build_editor):
_close_build_mode()
else:
_leave_world()
_open_pause_menu()
)
bar.add_child(exit_btn)
# Bottom controls pill hint
var controls_pill := PanelContainer.new()
controls_pill.name = "ControlsPill"
var pill_style := StyleBoxFlat.new()
pill_style.bg_color = Color(0.08, 0.10, 0.14, 0.75)
pill_style.set_corner_radius_all(14)
pill_style.content_margin_left = 14
pill_style.content_margin_right = 14
pill_style.content_margin_top = 4
pill_style.content_margin_bottom = 4
controls_pill.add_theme_stylebox_override("panel", pill_style)
controls_pill.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
controls_pill.offset_top = -34
controls_pill.offset_bottom = -8
controls_pill.offset_left = 220
controls_pill.offset_right = -220
var hint_lbl := Label.new()
hint_lbl.text = "[W][A][S][D] Move [Space] Jump [E] Interact [F1] Friends [T] Chat [F2] Build [ESC] Menu"
hint_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hint_lbl.add_theme_font_size_override("font_size", 12)
hint_lbl.add_theme_color_override("font_color", Color(0.85, 0.9, 0.95, 0.85))
controls_pill.add_child(hint_lbl)
_hud_layer.add_child(controls_pill)
# If editing in creator test mode, show status badge
if Session.editing and Session.world_id != null and int(Session.world_id) > 0:
var creator_pill := PanelContainer.new()
creator_pill.name = "CreatorTestPill"
var c_style := StyleBoxFlat.new()
c_style.bg_color = Color(0.18, 0.14, 0.04, 0.85)
c_style.border_color = Color(0.9, 0.75, 0.2, 0.9)
c_style.set_border_width_all(1)
c_style.set_corner_radius_all(6)
c_style.content_margin_left = 10
c_style.content_margin_right = 10
c_style.content_margin_top = 4
c_style.content_margin_bottom = 4
creator_pill.add_theme_stylebox_override("panel", c_style)
creator_pill.set_anchors_preset(Control.PRESET_TOP_RIGHT)
creator_pill.offset_left = -280
creator_pill.offset_top = 16
creator_pill.offset_right = -20
creator_pill.offset_bottom = 46
var creator_lbl := Label.new()
creator_lbl.text = "🛠️ Creator Test Mode (World #%s)" % str(Session.world_id)
creator_lbl.add_theme_font_size_override("font_size", 12)
creator_lbl.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
creator_pill.add_child(creator_lbl)
_hud_layer.add_child(creator_pill)
func _toggle_report_modal() -> void:
if _report_modal != null and is_instance_valid(_report_modal):
@@ -451,6 +505,90 @@ func _close_friends_overlay() -> void:
_player._refresh_mouse_mode()
func _toggle_pause_menu() -> void:
if _pause_menu != null and is_instance_valid(_pause_menu):
_close_pause_menu()
else:
_open_pause_menu()
func _open_pause_menu() -> void:
if _chat_overlay != null and is_instance_valid(_chat_overlay):
_close_chat_overlay()
if _friends_overlay != null and is_instance_valid(_friends_overlay):
_close_friends_overlay()
if _controls_modal != null and is_instance_valid(_controls_modal):
_close_controls_modal()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_player.set_physics_process(false)
_player.set_process_unhandled_input(false)
if _overlay_layer == null or not is_instance_valid(_overlay_layer):
_overlay_layer = CanvasLayer.new()
_overlay_layer.name = "OverlayLayer"
_overlay_layer.layer = 20
add_child(_overlay_layer)
var can_edit := Session.editing or (Session.world_id != null and int(Session.world_id) > 0)
var title_str := "World #%s" % str(Session.world_id) if Session.world_id != null and int(Session.world_id) > 0 else "Sandbox World"
_pause_menu = PauseMenu.new(can_edit, title_str)
_pause_menu.resume_requested.connect(_close_pause_menu)
_pause_menu.edit_world_requested.connect(func():
_close_pause_menu()
_open_build_mode()
)
_pause_menu.friends_requested.connect(func():
_close_pause_menu()
_open_friends_overlay()
)
_pause_menu.chat_requested.connect(func():
_close_pause_menu()
_open_chat_overlay()
)
_pause_menu.controls_requested.connect(func():
_open_controls_modal()
)
_pause_menu.leave_world_confirmed.connect(func():
_close_pause_menu()
_leave_world()
)
_overlay_layer.add_child(_pause_menu)
func _close_pause_menu() -> void:
if _pause_menu != null and is_instance_valid(_pause_menu):
_pause_menu.queue_free()
_pause_menu = null
if _controls_modal != null and is_instance_valid(_controls_modal):
_controls_modal.queue_free()
_controls_modal = null
if _overlay_layer != null and is_instance_valid(_overlay_layer):
_overlay_layer.queue_free()
_overlay_layer = null
_player.set_physics_process(true)
_player.set_process_unhandled_input(true)
_player._refresh_mouse_mode()
func _open_controls_modal() -> void:
if _controls_modal != null and is_instance_valid(_controls_modal):
return
_controls_modal = ControlsModal.new()
_controls_modal.closed.connect(_close_controls_modal)
if _overlay_layer != null and is_instance_valid(_overlay_layer):
_overlay_layer.add_child(_controls_modal)
else:
add_child(_controls_modal)
func _close_controls_modal() -> void:
if _controls_modal != null and is_instance_valid(_controls_modal):
_controls_modal.queue_free()
_controls_modal = null
## Entry point into the in-game editor (docs/architecture/editor.md §7.2).
## Freezes the player and opens a fresh, empty in-memory chunk to edit --
## loading a *real* chunk from the backend for editing is the sibling
@@ -507,6 +645,7 @@ func _open_build_mode() -> void:
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, base_revision)
_build_editor.closed.connect(_close_build_mode)
_build_editor.exit_browser_requested.connect(_exit_editor_to_browser)
add_child(_build_editor)
@@ -578,7 +717,8 @@ func _open_editor_for_world() -> void:
_build_editor = InGameEditor.new(core)
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, base_revision)
_build_editor.closed.connect(_exit_editor_to_browser)
_build_editor.closed.connect(_close_build_mode)
_build_editor.exit_browser_requested.connect(_exit_editor_to_browser)
add_child(_build_editor)
@@ -728,13 +868,23 @@ func _sync_remotes(states: Dictionary, delta: float) -> void:
var st: Dictionary = states[pid]
var node: Node3D = _remotes.get(pid)
var loadout: Variant = st.get("loadout", null)
var uname: String = str(st.get("username", ""))
if uname.is_empty() and loadout is Dictionary:
uname = str(loadout.get("username", ""))
if uname.is_empty():
uname = "Player %d" % pid
if node == null:
node = _make_remote(pid, loadout)
node = _make_remote(pid, loadout, uname)
_remotes[pid] = node
add_child(node)
node.global_position = st.get("position", Vector3.ZERO)
elif loadout is Dictionary and not (loadout as Dictionary).is_empty() and _remote_loadouts.get(pid) != loadout:
_setup_remote_visual(node, pid, loadout)
else:
if loadout is Dictionary and not (loadout as Dictionary).is_empty() and _remote_loadouts.get(pid) != loadout:
_setup_remote_visual(node, pid, loadout)
var tag := node.get_node_or_null("NameTag") as Label3D
if tag != null and tag.text != uname and not uname.is_empty():
tag.text = uname
var target: Vector3 = st.get("position", node.global_position)
node.global_position = node.global_position.lerp(target, clampf(REMOTE_LERP * delta, 0.0, 1.0))
@@ -756,7 +906,7 @@ func _sync_remotes(states: Dictionary, delta: float) -> void:
_remote_loadouts.erase(pid)
func _make_remote(pid: int, loadout: Variant = null) -> Node3D:
func _make_remote(pid: int, loadout: Variant = null, uname: String = "") -> Node3D:
var root := Node3D.new()
root.name = "Remote_%d" % pid
@@ -764,7 +914,7 @@ func _make_remote(pid: int, loadout: Variant = null) -> Node3D:
var tag := Label3D.new()
tag.name = "NameTag"
tag.text = "Player %d" % pid
tag.text = uname if not uname.is_empty() else ("Player %d" % pid)
tag.position = Vector3(0, 2.1, 0)
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
tag.no_depth_test = true