From 9872cb993052baee7ea3347f0b8b400c79e98080 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 14:13:51 +0200 Subject: [PATCH] Draw the neon components in the editor, not just at runtime Nothing under ui/ or vfx/ was marked @tool, so every hand-drawn control rendered as an empty rect in the editor. BuildMenu.tscn positions its buttons with hand-tuned pixel offsets (offset_left = -204) around a glyph identified only as `icon = 8` - laying out invisible boxes by number. @tool on the three scene-placed components that are safe to run in the editor, covering 23 of the 34 placements: IconButton (20 scenes), TouchJoystick (2), NeonPanel (1). Two of them mutate persisted properties on _ready, which under @tool would write computed values back into the .tscn every time the scene was opened, so both are guarded with Engine.is_editor_hint(): NeonPanel._fit_to_screen() rewrites all four offsets TouchJoystick._relayout() overwrites max_radius, which is @export IconButton's _gui_input is guarded too, so clicking a button on the editor canvas does not emit `pressed`. The autoload-heavy drawers (ResourceBar, PriceTag, QuestPanel, ResourceTransferList, SkillTreeView, FormationEditor - 6 to 16 GameState./Quests. references each) are left alone; they need their call sites guarded first. Verified: 172/172 headless, and a --headless --editor pass loads the project with no script errors. That pass only proves the scripts parse under @tool - it never opens a scene, so it does not exercise the in-editor _draw. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD --- ui/IconButton.gd | 6 ++++++ ui/NeonPanel.gd | 7 +++++++ ui/TouchJoystick.gd | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/ui/IconButton.gd b/ui/IconButton.gd index 7b9fdad..75ca542 100644 --- a/ui/IconButton.gd +++ b/ui/IconButton.gd @@ -1,3 +1,4 @@ +@tool class_name IconButton extends Control @@ -142,6 +143,11 @@ func _blocked_by_modal() -> bool: func _gui_input(event: InputEvent) -> void: + # @tool runs this in the editor too, where a click on the canvas would + # emit `pressed` at whatever it is wired to. The glyph is what we want + # visible there, not the behaviour. + if Engine.is_editor_hint(): + return if _blocked_by_modal(): return if event is InputEventScreenTouch: diff --git a/ui/NeonPanel.gd b/ui/NeonPanel.gd index 55803ab..1fb1e36 100644 --- a/ui/NeonPanel.gd +++ b/ui/NeonPanel.gd @@ -1,3 +1,4 @@ +@tool class_name NeonPanel extends Control @@ -39,6 +40,12 @@ var design_centre: Vector2 = Vector2.ZERO func _ready() -> void: + # _fit_to_screen() below rewrites offset_left/right/top/bottom. Under + # @tool that runs against the editor viewport, overwriting the authored + # layout and dirtying the scene, so the card previews at its authored + # size and is only ever fitted at runtime. + if Engine.is_editor_hint(): + return # Fitting adjusts the four offsets around a fixed centre, which is only # meaningful for a card pinned to a point - both anchor pairs collapsed, # as a centred dialog's are. A panel stretched between different anchors diff --git a/ui/TouchJoystick.gd b/ui/TouchJoystick.gd index 78056a6..a16c48e 100644 --- a/ui/TouchJoystick.gd +++ b/ui/TouchJoystick.gd @@ -1,3 +1,4 @@ +@tool class_name TouchJoystick extends Control @@ -40,6 +41,11 @@ var knob_pos: Vector2 = Vector2.ZERO func _ready() -> void: + # _relayout() writes max_radius, which is @export - under @tool that + # would persist a computed value into the scene. The preview uses the + # authored radius instead; only a running game refits. + if Engine.is_editor_hint(): + return # Dialogs release every spring-loaded stick through one compositional # contract, without NarrationBanner knowing scene paths or joystick roles. add_to_group("touch_joysticks")