Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.2 KiB
GDScript
101 lines
4.2 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_icon_button_press_fires.gd
|
|
# A browser on a phone delivers the touch DOWN and never delivers the
|
|
# release. Measured on the deployed web build: presses arrived with correct
|
|
# coordinates, landed inside the button, the button latched into its held
|
|
# state, and no release ever followed. IconButton emits `pressed` only on
|
|
# release, so on the web nothing ever fired - the "Play does nothing" bug.
|
|
#
|
|
# fire_on_press exists for exactly that platform. This test does not care
|
|
# whether THIS run is a web build; it exercises both settings directly, which
|
|
# is the only way a headless Linux run can cover the web path at all.
|
|
#
|
|
# Covers:
|
|
# A. with fire_on_press, a press alone emits - and emits ONCE even if a
|
|
# release does arrive after it
|
|
# B. with fire_on_press, the button does not stay latched in its held
|
|
# state (nothing is coming to clear it)
|
|
# C. without it, the release still drives the press, and dragging off
|
|
# still cancels - the behaviour every other platform keeps
|
|
# D. the default follows OS.has_feature("web")
|
|
#
|
|
# There is deliberately NO test for bypassing Godot's GUI routing. An earlier
|
|
# attempt did that, on the theory that touches were not reaching Controls at
|
|
# all - the device then reported `held=y`, proving the button had been
|
|
# receiving presses through ordinary routing the whole time. The bypass was
|
|
# solving a problem that did not exist, and its set_input_as_handled() call
|
|
# swallowed the very events the on-screen probe was reading.
|
|
|
|
var presses: int = 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
var button_script: GDScript = load("res://ui/IconButton.gd")
|
|
|
|
# --- D. the default is the platform rule --------------------------
|
|
var default_button: Control = button_script.new()
|
|
default_button.size = Vector2(120, 120)
|
|
if default_button.fire_on_press != OS.has_feature("web"):
|
|
return _fail("fire_on_press defaults to %s, expected OS.has_feature('web') = %s" \
|
|
% [default_button.fire_on_press, OS.has_feature("web")])
|
|
default_button.free()
|
|
print("fire_on_press defaults to the web feature flag (%s here)" % OS.has_feature("web"))
|
|
|
|
# --- A + B. the web path ------------------------------------------
|
|
var web_button: Control = button_script.new()
|
|
web_button.size = Vector2(120, 120)
|
|
web_button.fire_on_press = true
|
|
web_button.pressed.connect(_count)
|
|
root.add_child(web_button)
|
|
|
|
presses = 0
|
|
web_button._handle_press(true, Vector2(60, 60))
|
|
if presses != 1:
|
|
return _fail("a press alone emitted %d times with fire_on_press, expected 1" % presses)
|
|
if web_button.pressed_down:
|
|
return _fail("the button latched held with fire_on_press - nothing will ever clear it")
|
|
# A release may still arrive on a platform that sends one; it must not
|
|
# produce a second press for the same tap.
|
|
web_button._handle_press(false, Vector2(60, 60))
|
|
if presses != 1:
|
|
return _fail("a release after a fired press emitted again (%d total)" % presses)
|
|
print("fire_on_press: one tap emits exactly once and does not latch")
|
|
|
|
# --- C. every other platform --------------------------------------
|
|
var native_button: Control = button_script.new()
|
|
native_button.size = Vector2(120, 120)
|
|
native_button.fire_on_press = false
|
|
native_button.pressed.connect(_count)
|
|
root.add_child(native_button)
|
|
|
|
presses = 0
|
|
native_button._handle_press(true, Vector2(60, 60))
|
|
if presses != 0:
|
|
return _fail("a press emitted without a release while fire_on_press was off")
|
|
if not native_button.pressed_down:
|
|
return _fail("the button did not latch held while waiting for its release")
|
|
native_button._handle_press(false, Vector2(60, 60))
|
|
if presses != 1:
|
|
return _fail("the release did not emit (%d)" % presses)
|
|
|
|
# Dragging off before lifting still cancels.
|
|
presses = 0
|
|
native_button._handle_press(true, Vector2(60, 60))
|
|
native_button._handle_press(false, Vector2(400, 400))
|
|
if presses != 0:
|
|
return _fail("releasing outside the button still counted as a press")
|
|
print("release path unchanged: release fires, dragging off cancels")
|
|
|
|
print("TEST_ICON_BUTTON_PRESS_FIRES: PASS")
|
|
quit(0)
|
|
|
|
|
|
func _count() -> void:
|
|
presses += 1
|
|
|
|
|
|
func _fail(message: String) -> void:
|
|
print("TEST_ICON_BUTTON_PRESS_FIRES: FAIL (%s)" % message)
|
|
quit(1)
|