Every button in the web build was dead - reported as "Play does nothing".
Diagnosed on the deployed build with a temporary on-screen input probe,
because none of it reproduces here: the suite passes, and pressing Play under
the real gl_compatibility renderer (the same path WebGL2 takes) reaches
Main.tscn fine. The probe reported, from a phone browser:
#6 touch down @ 362,781
play rect 296,725 128x128
hit=YES
Three taps, six events, every one a DOWN and not a single UP - and the button
left drawn in its bright held state, pressed_down stuck true because nothing
ever came to clear it. So touches arrive, their coordinates are correct, and
they land inside the button; the browser simply never delivers the release,
in _gui_input or in raw _input.
IconButton emits `pressed` only on release, deliberately, so that dragging
off a button cancels it. With no release that means it never emits at all -
on every button in the game, not just Play. Which also explains why the
earlier suspects went nowhere: the scene change was never reached, so it was
never the scene change, and hit=YES ruled out coordinate mapping.
fire_on_press defaults to OS.has_feature("web"). There, a press completes the
tap itself and does NOT latch, since nothing is coming to unlatch it. Every
other platform is untouched and keeps drag-off-to-cancel.
The cost, stated plainly: a web tap can no longer be cancelled by sliding off
before lifting, and that applies to destructive buttons too. The alternative -
keeping release-firing and treating a new press as completing the previous
one - preserves cancellation but leaves the very first tap dead, which is
worse.
tests/test_icon_button_press_fires.gd drives the flag directly, since a
headless Linux run cannot BE a web build: one tap emits exactly once, a late
release does not emit again, the button does not latch, and with the flag off
the release still drives the press and dragging off still cancels.
MainMenu keeps the load/start bisection introduced while chasing this - a
failed change_scene_to_packed otherwise leaves the menu up, which looks
exactly like an unpressed button.
Full suite green at 100/100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
5.6 KiB
GDScript
147 lines
5.6 KiB
GDScript
extends Node2D
|
|
|
|
# The menu stacks whatever entries are actually available, rather than
|
|
# trusting the positions authored in the scene.
|
|
#
|
|
# Two of the three entries only exist when the game is run from Godot, so a
|
|
# shipped build shows one entry and an editor run shows three. There is no
|
|
# Quit: a web build has nothing to quit TO, and on Android the system back
|
|
# gesture already does it. Fixed
|
|
# offsets would leave holes where the hidden entries used to be - the same
|
|
# failure the HUD's button columns had, where a literal y meant "below the
|
|
# thing above me" and stopped being true when that thing moved. Here the
|
|
# stack is measured instead, and centred on whatever it ends up containing.
|
|
|
|
const ROW_BUTTON_HEIGHT: float = 128.0
|
|
const ROW_LABEL_OFFSET: float = 134.0
|
|
const ROW_LABEL_HEIGHT: float = 56.0
|
|
const ROW_HEIGHT: float = ROW_LABEL_OFFSET + ROW_LABEL_HEIGHT
|
|
const ROW_GAP: float = 20.0
|
|
const ROW_PITCH: float = ROW_HEIGHT + ROW_GAP
|
|
# The stack is centred on this rather than on the screen, to leave the title
|
|
# above it room to breathe.
|
|
const STACK_CENTER_Y: float = 40.0
|
|
const STATUS_GAP: float = 20.0
|
|
const STATUS_HEIGHT: float = 70.0
|
|
|
|
@onready var play_button: IconButton = $UI/PlayButton
|
|
@onready var play_label: Label = $UI/PlayLabel
|
|
@onready var editor_button: IconButton = $UI/EditorButton
|
|
@onready var editor_label: Label = $UI/EditorLabel
|
|
@onready var dev_button: IconButton = $UI/DevModeButton
|
|
@onready var dev_label: Label = $UI/DevModeLabel
|
|
@onready var status_label: Label = $UI/StatusLabel
|
|
|
|
|
|
func _ready() -> void:
|
|
play_button.pressed.connect(_on_play_pressed)
|
|
editor_button.pressed.connect(_on_editor_pressed)
|
|
editor_button.enabled = true
|
|
dev_button.pressed.connect(_on_dev_mode_pressed)
|
|
dev_button.enabled = true
|
|
|
|
# Dev mode and the map editor are development tools, not features of the
|
|
# game: they are for building and play-testing it, and a player who
|
|
# installed a build has no use for either. `editor` is the feature tag
|
|
# Godot sets when running from the editor and clears in an export, which
|
|
# is exactly the "started from Godot" distinction wanted here.
|
|
var developing: bool = is_developer_build()
|
|
editor_button.visible = developing
|
|
editor_label.visible = developing
|
|
dev_button.visible = developing
|
|
dev_label.visible = developing
|
|
|
|
_layout_stack()
|
|
|
|
|
|
# Wrapped so tests can state the rule they are checking rather than
|
|
# duplicating the feature-tag string.
|
|
func is_developer_build() -> bool:
|
|
return OS.has_feature("editor")
|
|
|
|
|
|
func _layout_stack() -> void:
|
|
var rows: Array = []
|
|
for row in [[play_button, play_label], [editor_button, editor_label],
|
|
[dev_button, dev_label]]:
|
|
if row[0].visible:
|
|
rows.append(row)
|
|
if rows.is_empty():
|
|
return
|
|
|
|
# The last row carries no trailing gap, so the block is one pitch per row
|
|
# minus that gap.
|
|
var block_height: float = float(rows.size()) * ROW_PITCH - ROW_GAP
|
|
var top: float = STACK_CENTER_Y - block_height * 0.5
|
|
for row in rows:
|
|
var button: Control = row[0]
|
|
var label: Label = row[1]
|
|
button.offset_top = top
|
|
button.offset_bottom = top + ROW_BUTTON_HEIGHT
|
|
label.offset_top = top + ROW_LABEL_OFFSET
|
|
label.offset_bottom = top + ROW_LABEL_OFFSET + ROW_LABEL_HEIGHT
|
|
top += ROW_PITCH
|
|
|
|
# Status sits under the whole stack, where a message about any entry can
|
|
# be read without it shifting the entries themselves.
|
|
var status_top: float = top - ROW_GAP + STATUS_GAP
|
|
status_label.offset_top = status_top
|
|
status_label.offset_bottom = status_top + STATUS_HEIGHT
|
|
|
|
|
|
func _on_play_pressed() -> void:
|
|
GameState.reset_new_game()
|
|
_start("res://Main.tscn")
|
|
|
|
|
|
# "Nothing happens when I press Play" covers three different faults, and they
|
|
# are indistinguishable from the outside: the press never reaching this
|
|
# handler, the scene change refusing, or the scene change reporting success
|
|
# and not taking effect. Each writes a DIFFERENT line here, so one look at
|
|
# the screen says which one it is.
|
|
#
|
|
# (nothing at all) - the press never got here
|
|
# "Loading Main.tscn..." - it got here but the scene never finished
|
|
# loading
|
|
# "Could not load ..." - the load returned nothing
|
|
# "Starting Main.tscn..." - loaded and the change reported OK, yet the
|
|
# menu is somehow still up
|
|
# "Could not start (...)" - the change refused, with the error number
|
|
#
|
|
# The scene is also loaded HERE rather than left to change_scene_to_file(),
|
|
# which loads internally and reports a single error for both steps. Doing it
|
|
# in two makes the failure specific, and takes whatever that call does about
|
|
# background loading out of the picture - the web export is built without
|
|
# thread support, so anything expecting a loader thread is worth removing
|
|
# from the path rather than trusting.
|
|
func _start(scene_path: String) -> void:
|
|
status_label.text = "Loading %s..." % scene_path.get_file()
|
|
var packed: PackedScene = load(scene_path)
|
|
if packed == null:
|
|
status_label.text = "Could not load %s" % scene_path.get_file()
|
|
return
|
|
status_label.text = "Starting %s..." % scene_path.get_file()
|
|
var result: int = get_tree().change_scene_to_packed(packed)
|
|
if result != OK:
|
|
status_label.text = "Could not start (%s, error %d)" % [scene_path.get_file(), result]
|
|
|
|
|
|
func _on_dev_mode_pressed() -> void:
|
|
# Guarded as well as hidden. Visibility is presentation; this is the rule.
|
|
if not is_developer_build():
|
|
status_label.text = "Dev mode only runs from Godot"
|
|
return
|
|
GameState.start_dev_game()
|
|
get_tree().change_scene_to_file("res://Main.tscn")
|
|
|
|
|
|
func _on_editor_pressed() -> void:
|
|
if not is_developer_build():
|
|
status_label.text = "Map editor only runs from Godot"
|
|
return
|
|
if editor_button.enabled:
|
|
_start("res://PlanetSetupEditor.tscn")
|
|
return
|
|
status_label.text = "Map editor coming soon"
|
|
|
|
|