diff --git a/BuildController.gd b/BuildController.gd index f976de6..109f753 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -1276,6 +1276,12 @@ func _layout_hangar() -> void: func _layout_hangar_section(slots: Array, labels: Array, top: float, columns: int, cell_width: float, cell_height: float, slot_size: float, label_height: float, width: float) -> void: + # A zero here is fatal on the web and silent everywhere else: wasm TRAPS + # on integer % 0 ("remainder by zero") and kills the engine mid-frame, + # while a native build shrugs it off. That difference is why this class + # of bug survives every test and only appears in a browser. + if columns <= 0: + return for i in range(slots.size()): var column: int = i % columns var row: int = i / columns diff --git a/IconButton.gd b/IconButton.gd index c7515d2..b3789e8 100644 --- a/IconButton.gd +++ b/IconButton.gd @@ -110,24 +110,6 @@ func _blocked_by_modal() -> bool: return false -# Fire on the PRESS instead of the release. -# -# A browser on a phone delivers the touch down and then never delivers the -# release. Measured directly on the deployed build: presses arrive with -# correct coordinates, land inside the button, and the button visibly latches -# into its held state - and no release ever follows, in _gui_input or in raw -# _input. A button that only emits on release therefore never emits at all, -# which is exactly the "Play does nothing" report. -# -# The cost is the drag-off-to-cancel affordance below, which is worth much -# less than the button working. Everywhere else keeps it, because everywhere -# else delivers releases. -# -# Settable rather than a constant so a test can exercise both paths without -# needing to be a web build. -@export var fire_on_press: bool = OS.has_feature("web") - - func _gui_input(event: InputEvent) -> void: if _blocked_by_modal(): return @@ -139,14 +121,28 @@ func _gui_input(event: InputEvent) -> void: accept_event() +# Complete the press immediately instead of waiting for a release. +# +# Measured on the device, in the web build: an on-screen probe reported +# `touch=1 mouse=1 held=y` after one tap - one touch down, one emulated mouse +# down, the button latched into its held state through ordinary GUI routing, +# and then nothing. The release is never delivered. A button that only emits +# on release therefore never emits at all. +# +# GUI routing itself works fine; `held=y` is the proof. An earlier attempt to +# bypass it made things worse and hid the evidence. +# +# The cost is the drag-off-to-cancel affordance below. Everywhere that +# delivers releases keeps it. +@export var fire_on_press: bool = OS.has_feature("web") + + func _handle_press(is_pressed: bool, at_position: Vector2) -> void: if is_pressed: pressed_down = true queue_redraw() if fire_on_press: - # Nothing is coming to complete this press, so complete it here. - # Not latched: with no release to clear it, the button would - # otherwise stay drawn in its held state forever. + # Not latched: nothing is coming to clear it. pressed_down = false pressed.emit() elif pressed_down: diff --git a/MainMenu.gd b/MainMenu.gd index c9cd8a3..b4f334a 100644 --- a/MainMenu.gd +++ b/MainMenu.gd @@ -94,36 +94,23 @@ func _on_play_pressed() -> void: _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. +# Loaded, built and added by hand rather than through change_scene_to_file(). +# That call does all three in one opaque step, so when the web build hung +# there was no way to tell which part failed. Split out, each step can say so +# - and a scene change that fails otherwise leaves the menu up, which looks +# exactly like an unpressed button. 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] + var scene: Node = packed.instantiate() + if scene == null: + status_label.text = "Could not build %s" % scene_path.get_file() + return + get_tree().root.add_child(scene) + get_tree().current_scene = scene + queue_free() func _on_dev_mode_pressed() -> void: @@ -132,7 +119,7 @@ func _on_dev_mode_pressed() -> void: status_label.text = "Dev mode only runs from Godot" return GameState.start_dev_game() - get_tree().change_scene_to_file("res://Main.tscn") + _start("res://Main.tscn") func _on_editor_pressed() -> void: diff --git a/PlanetAtmosphereController.gd b/PlanetAtmosphereController.gd index 8faca25..3f3599a 100644 --- a/PlanetAtmosphereController.gd +++ b/PlanetAtmosphereController.gd @@ -54,8 +54,15 @@ func _ready() -> void: # a second before asteroids/the planet visibly pop in. Force an # immediate synchronous populate now that the fleet is correctly # positioned, so nothing is missing on the very first visible frame. - asteroid_field.update_chunks() - planet_field.update_chunks() + # DEFERRED, not called straight through. Everything above runs inside the + # single frame that builds Main.tscn, and these two populate 25 and 49 + # chunks of world on top of it - 150-odd asteroids plus planets and their + # visuals. On a phone browser that frame is where the scene appeared to + # hang forever: nothing can paint until it ends, so a slow frame and a + # freeze look identical. One frame later is still immediate to the eye, + # and it lets the scene draw first. + asteroid_field.update_chunks.call_deferred() + planet_field.update_chunks.call_deferred() func _process(delta: float) -> void: diff --git a/PlanetSetupEditor.gd b/PlanetSetupEditor.gd index 0f31e15..6d9b6aa 100644 --- a/PlanetSetupEditor.gd +++ b/PlanetSetupEditor.gd @@ -318,6 +318,8 @@ func _on_reward_pressed() -> void: if current_setup == null: return var index: int = REWARD_OPTIONS.find(int(current_setup.research_point_reward)) + if REWARD_OPTIONS.is_empty(): + return current_setup.research_point_reward = REWARD_OPTIONS[(index + 1) % REWARD_OPTIONS.size()] _refresh_reward_label() @@ -330,6 +332,8 @@ func _refresh_reward_label() -> void: func _on_profile_pressed() -> void: + if _profile_ids.is_empty(): + return if _profile_ids.is_empty(): return _profile_cursor = (_profile_cursor + 1) % _profile_ids.size() diff --git a/WorldGen.gd b/WorldGen.gd index d051788..4345ef1 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -152,7 +152,11 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: definition_id = forced_definition_id var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names - var planet_name: String = names[rng.randi() % names.size()] + # Guarded for the same reason as the hangar grid: an empty names array + # would be a modulo by zero, which traps on wasm and takes the whole + # engine down rather than raising a script error. + var planet_name: String = "Unnamed" if names.is_empty() \ + else names[rng.randi() % names.size()] var planet_id: String = "chunk:%d:%d" % [chunk.x, chunk.y] return {"planet_id": planet_id, "local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, "definition_id": definition_id} diff --git a/export_presets.cfg b/export_presets.cfg index 500261f..97c30fb 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -257,7 +257,7 @@ variant/thread_support=false vram_texture_compression/for_desktop=true vram_texture_compression/for_mobile=true html/export_icon=true -html/custom_html_shell="" +html/custom_html_shell="res://web_shell.html" html/head_include="" html/canvas_resize_policy=2 html/focus_canvas_on_start=true diff --git a/tests/_web_play_probe.gd b/tests/_web_play_probe.gd deleted file mode 100644 index e384286..0000000 --- a/tests/_web_play_probe.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends Node2D - -# Probe, not a test - presses Play on the main menu under the REAL -# gl_compatibility renderer, which is the same path the web export takes -# (WebGL2 == GLES3 compatibility). --headless uses a dummy renderer and so -# cannot surface a failure that only happens once something actually has to -# be rendered or a shader has to compile. -# -# bash tests/screenshot.sh res://tests/_web_play_probe.tscn /tmp/play.png 45 720x1280 - -var menu: Node -var frames: int = 0 -var pressed: bool = false - - -func _ready() -> void: - menu = load("res://MainMenu.tscn").instantiate() - add_child(menu) - - -func _process(_delta: float) -> void: - frames += 1 - if frames == 8 and not pressed: - pressed = true - print("PLAY_PROBE: pressing Play") - menu._on_play_pressed() - if frames == 30: - var scene: Node = get_tree().current_scene - print("PLAY_PROBE: current_scene=%s" % [scene.name if scene != null else ""]) diff --git a/tests/_web_play_probe.gd.uid b/tests/_web_play_probe.gd.uid deleted file mode 100644 index 84ce0ce..0000000 --- a/tests/_web_play_probe.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dtudtqerivj84 diff --git a/tests/_web_play_probe.tscn b/tests/_web_play_probe.tscn deleted file mode 100644 index e63c2a9..0000000 --- a/tests/_web_play_probe.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://tests/_web_play_probe.gd" id="1"] - -[node name="WebPlayProbe" type="Node2D"] -script = ExtResource("1") diff --git a/tests/test_icon_button_press_fires.gd b/tests/test_icon_button_press_fires.gd index 0675d27..24e8f09 100644 --- a/tests/test_icon_button_press_fires.gd +++ b/tests/test_icon_button_press_fires.gd @@ -20,6 +20,13 @@ extends SceneTree # 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 diff --git a/tests/test_main_menu.gd b/tests/test_main_menu.gd index 5a86a7a..97af240 100644 --- a/tests/test_main_menu.gd +++ b/tests/test_main_menu.gd @@ -50,7 +50,9 @@ func _process(delta: float) -> bool: stage = 1 return false 1: - if elapsed < 0.2: + # _start awaits a frame so its status text paints before the + # blocking load, so the change takes a couple of frames. + if elapsed < 1.0: return false var on_editor: bool = current_scene != null and current_scene.name == "PlanetSetupEditor" if not on_editor: @@ -78,7 +80,7 @@ func _process(delta: float) -> bool: stage = 3 return false 3: - if elapsed < 0.3: + if elapsed < 1.0: return false var on_main: bool = current_scene != null and current_scene.name == "Main" # A normal game now starts with NOTHING banked - the opening diff --git a/web_shell.html b/web_shell.html new file mode 100644 index 0000000..6f35eb6 --- /dev/null +++ b/web_shell.html @@ -0,0 +1,253 @@ + + + + + + Spaceship + + + + + + + + Your browser does not support the canvas tag. + + + + +
+ + +
+
+ + + + + + + +