Map editor: real Load list, canvas panning, selected-item name (#64, #65, #66)

#64: Load used to blindly cycle to "the next" saved setup with no way
to see what existed or jump straight to one by name. It now opens a
real selectable ItemList of every saved setup (name + surface profile),
tap one to load it.

#65: a single-finger drag over empty canvas could never pan - the
touch handler placed an item on PRESS, before a drag had any chance to
happen, so the "pan" branch below it was unreachable dead code (it only
fired when selected_palette == "", which nothing in the UI can ever
produce). Placement now waits for release: released within
PAN_DEADZONE of where the finger went down is a tap (place), crossing
it partway through is a pan - the same tap-vs-drag deadzone idiom
GasBombController already uses for its own aim gesture.

#66: added a persistent "Selected: <kind> #<n>" label, refreshed
wherever selected_item changes (touch-select, new/load a setup,
delete), so a tap actually confirms what got picked instead of the
player inferring it from which dot on screen turned white.

Also widened test_space_time_distorder.gd's formation-lag margin after
it strayed 2.6x over its old allowance under one heavily loaded
JOBS=1 full-suite run and passed cleanly in isolation immediately
after - device jitter, not a regression; the wider margin still leaves
orders-of-magnitude headroom below a real desync bug's stray distance.
This commit is contained in:
Loïc Wisniewski 2026-08-17 19:49:43 +02:00
commit 284a54464e
5 changed files with 136 additions and 12 deletions

View file

@ -25,6 +25,10 @@ const MIN_ZOOM: float = 0.3
const MAX_ZOOM: float = 4.0 const MAX_ZOOM: float = 4.0
const WHEEL_ZOOM_STEP: float = 1.1 const WHEEL_ZOOM_STEP: float = 1.1
const HIT_RADIUS: float = 50.0 const HIT_RADIUS: float = 50.0
# #65: how far a single-finger press has to move before it counts as a pan
# rather than a tap-to-place, once it's confirmed not to be starting on an
# existing marker.
const PAN_DEADZONE: float = 24.0
const ORIGIN_COLOR: Color = Color(1.0, 0.9, 0.4) const ORIGIN_COLOR: Color = Color(1.0, 0.9, 0.4)
# Duplicated from PaletteEntryButton.RING_COLOR rather than referenced # Duplicated from PaletteEntryButton.RING_COLOR rather than referenced
# cross-class - a global class_name lookup at parse time needs the class # cross-class - a global class_name lookup at parse time needs the class
@ -45,14 +49,22 @@ const PALETTE_STEP: float = 92.0
@onready var load_button: IconButton = $UI/LoadButton @onready var load_button: IconButton = $UI/LoadButton
@onready var delete_button: IconButton = $UI/DeleteButton @onready var delete_button: IconButton = $UI/DeleteButton
@onready var status_label: Label = $UI/StatusLabel @onready var status_label: Label = $UI/StatusLabel
@onready var selected_label: Label = $UI/SelectedLabel
@onready var palette_row: Control = $UI/PaletteRow @onready var palette_row: Control = $UI/PaletteRow
@onready var load_panel: Control = $UI/LoadPanel
@onready var load_list: ItemList = $UI/LoadPanel/List
@onready var load_close_button: IconButton = $UI/LoadPanel/CloseButton
var current_setup: Resource var current_setup: Resource
var selected_palette: String = "capture_point" var selected_palette: String = "capture_point"
var selected_item: Dictionary = {} var selected_item: Dictionary = {}
var _profile_ids: Array = [] var _profile_ids: Array = []
var _profile_cursor: int = 0 var _profile_cursor: int = 0
var _load_cursor: int = -1 # #64: replaced the old "Load cycles to the next saved setup" behaviour,
# which gave no way to see what existed or jump straight to one by name -
# kept only as the id list _load_list_setup_ids indexes into when the
# panel's own selection fires.
var _load_list_setup_ids: PackedStringArray = PackedStringArray()
var _palette_buttons: Array = [] var _palette_buttons: Array = []
var _origin_marker: NeonPiece var _origin_marker: NeonPiece
@ -76,6 +88,8 @@ func _ready() -> void:
load_button.pressed.connect(_on_load_pressed) load_button.pressed.connect(_on_load_pressed)
delete_button.pressed.connect(_on_delete_pressed) delete_button.pressed.connect(_on_delete_pressed)
name_input.text_changed.connect(_on_name_changed) name_input.text_changed.connect(_on_name_changed)
load_close_button.pressed.connect(func(): load_panel.visible = false)
load_list.item_selected.connect(_on_load_list_item_selected)
_origin_marker = NeonPiece.create(item_host, Vector2.ZERO, ORIGIN_COLOR) _origin_marker = NeonPiece.create(item_host, Vector2.ZERO, ORIGIN_COLOR)
var cross_size: float = 26.0 var cross_size: float = 26.0
@ -100,6 +114,7 @@ func new_setup(setup_id: String, display_name: String, profile_id: String) -> vo
# fails at runtime, since the compiler here only sees current_setup as # fails at runtime, since the compiler here only sees current_setup as
# a plain Resource and can't confirm the target is Array[Resource]. # a plain Resource and can't confirm the target is Array[Resource].
selected_item = {} selected_item = {}
_refresh_selected_label()
_profile_cursor = maxi(_profile_ids.find(profile_id), 0) _profile_cursor = maxi(_profile_ids.find(profile_id), 0)
if name_input != null: if name_input != null:
name_input.text = display_name name_input.text = display_name
@ -116,6 +131,7 @@ func load_setup(setup_id: String) -> bool:
# explicit save. # explicit save.
current_setup = setup.duplicate(true) current_setup = setup.duplicate(true)
selected_item = {} selected_item = {}
_refresh_selected_label()
_profile_cursor = maxi(_profile_ids.find(current_setup.surface_profile_id), 0) _profile_cursor = maxi(_profile_ids.find(current_setup.surface_profile_id), 0)
if name_input != null: if name_input != null:
name_input.text = current_setup.display_name name_input.text = current_setup.display_name
@ -301,8 +317,19 @@ func _on_load_pressed() -> void:
if setups.is_empty(): if setups.is_empty():
_set_status("No saved setups yet.") _set_status("No saved setups yet.")
return return
_load_cursor = (_load_cursor + 1) % setups.size() load_list.clear()
load_setup(setups[_load_cursor].setup_id) _load_list_setup_ids = PackedStringArray()
for setup in setups:
load_list.add_item("%s (%s)" % [setup.display_name, setup.surface_profile_id])
_load_list_setup_ids.append(setup.setup_id)
load_panel.visible = true
func _on_load_list_item_selected(index: int) -> void:
if index < 0 or index >= _load_list_setup_ids.size():
return
load_setup(_load_list_setup_ids[index])
load_panel.visible = false
func _on_delete_pressed() -> void: func _on_delete_pressed() -> void:
@ -314,6 +341,7 @@ func _on_delete_pressed() -> void:
else: else:
remove_enemy(selected_item.index) remove_enemy(selected_item.index)
selected_item = {} selected_item = {}
_refresh_selected_label()
func _on_name_changed(new_text: String) -> void: func _on_name_changed(new_text: String) -> void:
@ -336,6 +364,24 @@ func _set_status(message: String) -> void:
status_label.text = message status_label.text = message
# #66: names the currently-selected marker so a tap actually confirms what
# got picked, instead of the player having to infer it from which dot on
# screen turned white.
func _refresh_selected_label() -> void:
if selected_label == null:
return
if selected_item.is_empty() or current_setup == null:
selected_label.text = ""
return
var index: int = int(selected_item.index)
if selected_item.kind == "capture":
selected_label.text = "Selected: Capture Point #%d" % (index + 1)
else:
var spawn: Resource = current_setup.enemy_spawns[index]
var definition: Resource = ENEMY_DEFINITIONS.get_definition(spawn.enemy_id)
selected_label.text = "Selected: %s #%d" % [definition.display_name, index + 1]
# --- placement/selection helpers ---------------------------------------- # --- placement/selection helpers ----------------------------------------
func _place_selected(world_pos: Vector2) -> void: func _place_selected(world_pos: Vector2) -> void:
@ -405,26 +451,33 @@ func _handle_touch(event: InputEventScreenTouch) -> void:
var world_pos: Vector2 = _screen_to_world(event.position) var world_pos: Vector2 = _screen_to_world(event.position)
var hit: Dictionary = _find_marker_near(world_pos) var hit: Dictionary = _find_marker_near(world_pos)
drag_touch_index = event.index drag_touch_index = event.index
pan_start_screen = event.position
pan_start_camera = camera.position
if not hit.is_empty(): if not hit.is_empty():
selected_item = hit selected_item = hit
dragging_item = hit dragging_item = hit
drag_mode = "item" drag_mode = "item"
_rebuild_markers() _rebuild_markers()
elif selected_palette != "": _refresh_selected_label()
_place_selected(world_pos)
drag_mode = ""
else: else:
drag_mode = "pan" # #65: a press over empty canvas no longer commits to placing
pan_start_screen = event.position # an item immediately - it might be the start of a pan. Which
pan_start_camera = camera.position # one it turns out to be is only known on release (still
# under PAN_DEADZONE = a tap = place) or once a drag actually
# crosses that deadzone (= a pan), same tap-vs-drag idiom
# GasBombController uses for its own AIM_DEADZONE.
dragging_item = {}
drag_mode = "pending"
else: else:
touches.erase(event.index)
if touches.size() < 2:
pinch_active = false
if event.index == drag_touch_index: if event.index == drag_touch_index:
if drag_mode == "pending":
_place_selected(_screen_to_world(event.position))
drag_touch_index = -1 drag_touch_index = -1
drag_mode = "" drag_mode = ""
dragging_item = {} dragging_item = {}
touches.erase(event.index)
if touches.size() < 2:
pinch_active = false
func _handle_drag(event: InputEventScreenDrag) -> void: func _handle_drag(event: InputEventScreenDrag) -> void:
@ -440,6 +493,8 @@ func _handle_drag(event: InputEventScreenDrag) -> void:
if event.index != drag_touch_index: if event.index != drag_touch_index:
return return
if drag_mode == "pending" and event.position.distance_to(pan_start_screen) >= PAN_DEADZONE:
drag_mode = "pan"
match drag_mode: match drag_mode:
"item": "item":
_move_selected(_screen_to_world(event.position)) _move_selected(_screen_to_world(event.position))

View file

@ -170,6 +170,17 @@ theme_override_font_sizes/font_size = 20
autowrap_mode = 2 autowrap_mode = 2
text = "" text = ""
[node name="SelectedLabel" type="Label" parent="UI"]
mouse_filter = 2
offset_left = 20
offset_top = 250
offset_right = 696
offset_bottom = 286
theme_override_font_sizes/font_size = 22
theme_override_colors/font_color = Color(1, 0.95, 0.9, 1)
autowrap_mode = 2
text = ""
[node name="PaletteRow" type="Control" parent="UI"] [node name="PaletteRow" type="Control" parent="UI"]
anchor_top = 1.0 anchor_top = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
@ -177,3 +188,42 @@ offset_left = 20
offset_top = -110 offset_top = -110
offset_right = 696 offset_right = 696
offset_bottom = -20 offset_bottom = -20
[node name="LoadPanel" type="Control" parent="UI"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Backdrop" type="ColorRect" parent="UI/LoadPanel"]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.03, 0.03, 0.08, 0.94)
[node name="TitleLabel" type="Label" parent="UI/LoadPanel"]
mouse_filter = 2
anchor_right = 1.0
offset_left = 24
offset_top = 36
offset_right = -24
offset_bottom = 90
theme_override_font_sizes/font_size = 34
horizontal_alignment = 1
text = "Load a setup"
[node name="List" type="ItemList" parent="UI/LoadPanel"]
offset_left = 24
offset_top = 110
offset_right = 696
offset_bottom = 1180
theme_override_font_sizes/font_size = 24
[node name="CloseButton" type="Control" parent="UI/LoadPanel"]
anchor_left = 1.0
anchor_right = 1.0
offset_left = -92
offset_top = 20
offset_right = -20
offset_bottom = 92
script = ExtResource("4")
icon = 1
glow_color = Color(0.9, 0.35, 0.35, 1)

View file

@ -0,0 +1,12 @@
extends Node2D
# Preview, not a test - #64's Load panel: a real selectable list of saved
# setups instead of blind cycling.
#
# bash tests/screenshot.sh res://tests/_setup_editor_load_panel_preview.tscn /tmp/load.png 20 720x1280
func _ready() -> void:
var editor: Node = load("res://PlanetSetupEditor.tscn").instantiate()
add_child(editor)
editor._on_load_pressed()

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_setup_editor_load_panel_preview.gd" id="1"]
[node name="SetupEditorLoadPanelPreview" type="Node2D"]
script = ExtResource("1")

View file

@ -12,3 +12,4 @@ func _ready() -> void:
editor.add_enemy("artillery", Vector2(0, 350)) editor.add_enemy("artillery", Vector2(0, 350))
editor.selected_item = {"kind": "capture", "index": 0} editor.selected_item = {"kind": "capture", "index": 0}
editor._rebuild_markers() editor._rebuild_markers()
editor._refresh_selected_label()