From a40abdb475e7875b23cf63db7e462ff49f5f4e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 15:22:25 -0300 Subject: [PATCH 01/16] Remove touchscreen warning Manual revert of 40bb70f2 Helps https://github.com/endlessm/threadbare/issues/806 --- .../title/components/touchscreen_warning.gd | 26 ---------------- .../components/touchscreen_warning.gd.uid | 1 - .../title/components/touchscreen_warning.tscn | 31 +++++++++++++++++-- scenes/menus/title/title_screen.tscn | 7 ----- 4 files changed, 29 insertions(+), 36 deletions(-) delete mode 100644 scenes/menus/title/components/touchscreen_warning.gd delete mode 100644 scenes/menus/title/components/touchscreen_warning.gd.uid diff --git a/scenes/menus/title/components/touchscreen_warning.gd b/scenes/menus/title/components/touchscreen_warning.gd deleted file mode 100644 index 8c0ec12e9e..0000000000 --- a/scenes/menus/title/components/touchscreen_warning.gd +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-FileCopyrightText: The Threadbare Authors -# SPDX-License-Identifier: MPL-2.0 -extends Control - -signal dismissed - - -func _ready() -> void: - if _should_show_touchscreen_warning(): - show() - - -func _should_show_touchscreen_warning() -> bool: - var has_touch: bool = DisplayServer.is_touchscreen_available() - var has_keyboard: bool - if OS.has_feature("web_android") or OS.has_feature("web_ios"): - # DisplayServer.has_hardware_keyboard() is not implemented on the web. - # Assuming mobile web browsers don't have a hardware keyboard. - has_keyboard = false - else: - has_keyboard = DisplayServer.has_hardware_keyboard() - return has_touch and not has_keyboard - - -func _on_dismiss_button_pressed() -> void: - dismissed.emit() diff --git a/scenes/menus/title/components/touchscreen_warning.gd.uid b/scenes/menus/title/components/touchscreen_warning.gd.uid deleted file mode 100644 index 08fb6ebf0e..0000000000 --- a/scenes/menus/title/components/touchscreen_warning.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dx4alphqyxbwv diff --git a/scenes/menus/title/components/touchscreen_warning.tscn b/scenes/menus/title/components/touchscreen_warning.tscn index 954f21f08c..e5adc63a8c 100644 --- a/scenes/menus/title/components/touchscreen_warning.tscn +++ b/scenes/menus/title/components/touchscreen_warning.tscn @@ -1,11 +1,38 @@ [gd_scene format=3 uid="uid://cbf7ft3j4hemp"] -[ext_resource type="Script" uid="uid://dx4alphqyxbwv" path="res://scenes/menus/title/components/touchscreen_warning.gd" id="1_smfbs"] +[sub_resource type="GDScript" id="GDScript_4hx3q"] +script/source = "# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +extends Control + +signal dismissed + + +func _ready() -> void: + if _should_show_touchscreen_warning(): + show() + + +func _should_show_touchscreen_warning() -> bool: + var has_touch: bool = DisplayServer.is_touchscreen_available() + var has_keyboard: bool + if OS.has_feature(\"web_android\") or OS.has_feature(\"web_ios\"): + # DisplayServer.has_hardware_keyboard() is not implemented on the web. + # Assuming mobile web browsers don't have a hardware keyboard. + has_keyboard = false + else: + has_keyboard = DisplayServer.has_hardware_keyboard() + return has_touch and not has_keyboard + + +func _on_dismiss_button_pressed() -> void: + dismissed.emit() +" [node name="TouchscreenWarning" type="Control" unique_id=2046863085] layout_mode = 3 anchors_preset = 0 -script = ExtResource("1_smfbs") +script = SubResource("GDScript_4hx3q") metadata/_tab_index = 0 [node name="PanelContainer" type="PanelContainer" parent="." unique_id=479124586] diff --git a/scenes/menus/title/title_screen.tscn b/scenes/menus/title/title_screen.tscn index c61351eab3..2a5c07bb7f 100644 --- a/scenes/menus/title/title_screen.tscn +++ b/scenes/menus/title/title_screen.tscn @@ -6,7 +6,6 @@ [ext_resource type="AudioStream" uid="uid://kc657macgib4" path="res://assets/first_party/music/Threadbare_Main_Loud.ogg" id="5_5uobg"] [ext_resource type="Texture2D" uid="uid://lg5dl13njsg3" path="res://assets/first_party/tiles/Grass_And_Sand_Tiles.png" id="5_c6kyi"] [ext_resource type="PackedScene" uid="uid://6s70kur03rjk" path="res://scenes/menus/title/components/credits.tscn" id="6_2ijyo"] -[ext_resource type="PackedScene" uid="uid://cbf7ft3j4hemp" path="res://scenes/menus/title/components/touchscreen_warning.tscn" id="6_5uobg"] [ext_resource type="PackedScene" uid="uid://dkeb0yjgcfi86" path="res://scenes/menus/options/options.tscn" id="8_5uobg"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2ijyo"] @@ -64,15 +63,9 @@ layout_mode = 2 size_flags_vertical = 3 metadata/_tab_index = 2 -[node name="TouchscreenWarning" parent="Pages" unique_id=2046863085 instance=ExtResource("6_5uobg")] -visible = false -layout_mode = 2 -metadata/_tab_index = 3 - [connection signal="continue_pressed" from="Pages/MainMenu" to="." method="_on_main_menu_continue_pressed"] [connection signal="credits_pressed" from="Pages/MainMenu" to="." method="_on_main_menu_credits_pressed"] [connection signal="options_pressed" from="Pages/MainMenu" to="." method="_on_main_menu_options_pressed"] [connection signal="start_pressed" from="Pages/MainMenu" to="." method="_on_start_pressed"] [connection signal="back" from="Pages/Credits" to="." method="_on_credits_back"] [connection signal="back" from="Pages/Options" to="." method="_on_options_back"] -[connection signal="dismissed" from="Pages/TouchscreenWarning" to="Pages/MainMenu" method="show"] From 05064b0acced5d4ba0488afda552026049c60fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 19:06:38 -0300 Subject: [PATCH 02/16] Fix fullscreen setting getting out of sync --- scenes/globals/settings/settings.gd | 6 +++++- scenes/menus/options/components/settings_toggle.gd | 3 +++ web/full-size.html | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scenes/globals/settings/settings.gd b/scenes/globals/settings/settings.gd index b20eaadeab..ffdccd7227 100644 --- a/scenes/globals/settings/settings.gd +++ b/scenes/globals/settings/settings.gd @@ -110,7 +110,11 @@ func set_volume(bus: String, volume_linear: float) -> void: func is_fullscreen() -> bool: - return DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN + # On the web, Godot reports the non-exclusive fullscreen mode instead. + return ( + DisplayServer.window_get_mode() + in [DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN, DisplayServer.WINDOW_MODE_FULLSCREEN] + ) func set_fullscreen(toggled_on: bool) -> void: diff --git a/scenes/menus/options/components/settings_toggle.gd b/scenes/menus/options/components/settings_toggle.gd index 8ac5d6a50f..f9dec54367 100644 --- a/scenes/menus/options/components/settings_toggle.gd +++ b/scenes/menus/options/components/settings_toggle.gd @@ -33,6 +33,9 @@ func _ready() -> void: # another in the pause overlay. At most one is displayed at a time, so we can keep them in # synch by reading the setting each time each toggle is displayed. visibility_changed.connect(_refresh) + # Fullscreen can also change outside the game, for example when the player + # presses Esc in a browser. The window is resized when that happens. + get_window().size_changed.connect(_refresh) _refresh() toggled.connect(_on_toggled) diff --git a/web/full-size.html b/web/full-size.html index 7c9b24b02d..a523288be2 100644 --- a/web/full-size.html +++ b/web/full-size.html @@ -174,7 +174,10 @@ if (!document.fullscreenEnabled) { return; } - document.documentElement.requestFullscreen().then(() => { + // Make the canvas fullscreen, not the whole page, because Godot only + // tracks fullscreen changes of its own canvas. Otherwise the game + // wouldn't know it's fullscreen. + document.getElementById('canvas').requestFullscreen().then(() => { // Browsers only allow locking the orientation in fullscreen. Safari // and desktop browsers don't support it at all. return screen.orientation?.lock?.('landscape').catch((err) => { From 1c95fe0d37ec8eb430b4ce0b72f41f4cf857a283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 19:08:52 -0300 Subject: [PATCH 03/16] WIP --- .../user_interface/touch-button.png | 3 + .../user_interface/touch-button.png.import | 40 ++++++ .../user_interface/touch-button_pressed.png | 3 + .../touch-button_pressed.png.import | 40 ++++++ scenes/ui_elements/hud/components/hud.gd | 21 +++ scenes/ui_elements/hud/hud.tscn | 4 + .../input_hud/components/input_hud.gd | 16 +-- .../components/touch_controls.gd | 27 ++++ .../components/touch_controls.gd.uid | 1 + .../touch_controls/touch_controls.tscn | 122 ++++++++++++++++++ 10 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 assets/first_party/user_interface/touch-button.png create mode 100644 assets/first_party/user_interface/touch-button.png.import create mode 100644 assets/first_party/user_interface/touch-button_pressed.png create mode 100644 assets/first_party/user_interface/touch-button_pressed.png.import create mode 100644 scenes/ui_elements/touch_controls/components/touch_controls.gd create mode 100644 scenes/ui_elements/touch_controls/components/touch_controls.gd.uid create mode 100644 scenes/ui_elements/touch_controls/touch_controls.tscn diff --git a/assets/first_party/user_interface/touch-button.png b/assets/first_party/user_interface/touch-button.png new file mode 100644 index 0000000000..ff919e5902 --- /dev/null +++ b/assets/first_party/user_interface/touch-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faaf8e09b41d3eb529ef6f69ffb908f3ef0b6d307a532cc519074f2537e9436e +size 366 diff --git a/assets/first_party/user_interface/touch-button.png.import b/assets/first_party/user_interface/touch-button.png.import new file mode 100644 index 0000000000..1f417d2498 --- /dev/null +++ b/assets/first_party/user_interface/touch-button.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ijhmisl4foqa" +path="res://.godot/imported/touch-button.png-573bfdf492ca68ead333f9cc90e542fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/touch-button.png" +dest_files=["res://.godot/imported/touch-button.png-573bfdf492ca68ead333f9cc90e542fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/touch-button_pressed.png b/assets/first_party/user_interface/touch-button_pressed.png new file mode 100644 index 0000000000..4c6a85d192 --- /dev/null +++ b/assets/first_party/user_interface/touch-button_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55324998e869b85b5c60486e492321098749a9227658db68fe16875aa521ed99 +size 535 diff --git a/assets/first_party/user_interface/touch-button_pressed.png.import b/assets/first_party/user_interface/touch-button_pressed.png.import new file mode 100644 index 0000000000..a62636b6e3 --- /dev/null +++ b/assets/first_party/user_interface/touch-button_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dm8jbpjptaw00" +path="res://.godot/imported/touch-button_pressed.png-daf3b6540e79f17c4258784e61ee0203.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/touch-button_pressed.png" +dest_files=["res://.godot/imported/touch-button_pressed.png-daf3b6540e79f17c4258784e61ee0203.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scenes/ui_elements/hud/components/hud.gd b/scenes/ui_elements/hud/components/hud.gd index acabf57b25..2dfd8102b0 100644 --- a/scenes/ui_elements/hud/components/hud.gd +++ b/scenes/ui_elements/hud/components/hud.gd @@ -6,9 +6,13 @@ extends CanvasLayer @onready var zone_name_display: PanelContainer = %ZoneNameDisplay @onready var zone_unlocked_display: CanvasLayer = %ZoneUnlockedDisplay @onready var input_hud: InputHud = %InputHud +@onready var touch_controls: Control = %TouchControls func _ready() -> void: + DialogueManager.dialogue_started.connect(_on_dialogue_started) + DialogueManager.dialogue_ended.connect(_on_dialogue_ended) + GameState.cleared.connect(_on_gamestate_cleared) _on_gamestate_cleared() @@ -36,6 +40,23 @@ func _on_scene_changed() -> void: story_quest_progress.player = player story_quest_progress.sokoban_ruleset = sokoban_ruleset story_quest_progress.update_visibility() + touch_controls.player = player + touch_controls.sokoban_ruleset = sokoban_ruleset + touch_controls.update_visibility() + + +func _on_dialogue_started(_resource: DialogueResource) -> void: + input_hud.displaying_dialogue = true + input_hud.update_visibility() + touch_controls.displaying_dialogue = true + touch_controls.update_visibility() + + +func _on_dialogue_ended(_resource: DialogueResource) -> void: + input_hud.displaying_dialogue = false + input_hud.update_visibility() + touch_controls.displaying_dialogue = false + touch_controls.update_visibility() ## Force a refresh of the StoryQuest progress. diff --git a/scenes/ui_elements/hud/hud.tscn b/scenes/ui_elements/hud/hud.tscn index 9c69665d8c..1c73f4ef08 100644 --- a/scenes/ui_elements/hud/hud.tscn +++ b/scenes/ui_elements/hud/hud.tscn @@ -5,6 +5,7 @@ [ext_resource type="PackedScene" uid="uid://cps4im7j5unff" path="res://scenes/ui_elements/zone_activation/zone_name_display.tscn" id="2_isa5i"] [ext_resource type="PackedScene" uid="uid://ipgq27vmhsmc" path="res://scenes/ui_elements/zone_activation/zone_unlocked_display.tscn" id="4_isa5i"] [ext_resource type="PackedScene" uid="uid://dfu3rocpande8" path="res://scenes/ui_elements/input_hud/input_hud.tscn" id="5_138yv"] +[ext_resource type="PackedScene" uid="uid://ba3fvp0yravi4" path="res://scenes/ui_elements/touch_controls/touch_controls.tscn" id="6_xdab2"] [node name="HUD" type="CanvasLayer" unique_id=1426591993] script = ExtResource("1_dwpju") @@ -31,3 +32,6 @@ unique_name_in_owner = true [node name="InputHud" parent="." unique_id=176382299 instance=ExtResource("5_138yv")] unique_name_in_owner = true + +[node name="TouchControls" parent="." unique_id=980451545 instance=ExtResource("6_xdab2")] +unique_name_in_owner = true diff --git a/scenes/ui_elements/input_hud/components/input_hud.gd b/scenes/ui_elements/input_hud/components/input_hud.gd index 27c52bebb0..adcbe41ff7 100644 --- a/scenes/ui_elements/input_hud/components/input_hud.gd +++ b/scenes/ui_elements/input_hud/components/input_hud.gd @@ -5,13 +5,12 @@ extends Control @export var player: CharacterBody2D @export var sokoban_ruleset: RuleEngine +@export var displaying_dialogue: bool var player_interaction: PlayerInteraction var player_repel: PlayerRepel var player_hook: PlayerHook -var displaying_dialogue: bool - @onready var normal_controls := %NormalControls @onready var interact_input_hint: LabeledInputHint = %InteractInputHint @onready var aim_input_hint := %AimInputHint @@ -23,9 +22,6 @@ var displaying_dialogue: bool func _ready() -> void: - DialogueManager.dialogue_started.connect(_on_dialogue_started) - DialogueManager.dialogue_ended.connect(_on_dialogue_ended) - Transitions.started.connect(update_visibility) Transitions.finished.connect(update_visibility) @@ -86,15 +82,5 @@ func _update_player_state() -> void: aim_input_hint.visible = player_hook and player_hook.visible -func _on_dialogue_started(_resource: DialogueResource) -> void: - displaying_dialogue = true - update_visibility() - - -func _on_dialogue_ended(_resource: DialogueResource) -> void: - displaying_dialogue = false - update_visibility() - - func _display_skip() -> void: skip_input_hint.visible = true diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd new file mode 100644 index 0000000000..01e3d8058b --- /dev/null +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +extends Control + +@export var player: CharacterBody2D +@export var sokoban_ruleset: RuleEngine +@export var displaying_dialogue: bool + + +func _ready() -> void: + Transitions.started.connect(update_visibility) + Transitions.finished.connect(update_visibility) + + +func _notification(what: int) -> void: + match what: + NOTIFICATION_PAUSED, NOTIFICATION_UNPAUSED: + update_visibility() + + +func update_visibility() -> void: + visible = ( + not get_tree().paused + and not Transitions.is_running() + and not displaying_dialogue + and (player or sokoban_ruleset) + ) diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd.uid b/scenes/ui_elements/touch_controls/components/touch_controls.gd.uid new file mode 100644 index 0000000000..4fb1a6a3b1 --- /dev/null +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd.uid @@ -0,0 +1 @@ +uid://dyyshnmovujje diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn new file mode 100644 index 0000000000..a04cb55f2f --- /dev/null +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -0,0 +1,122 @@ +[gd_scene format=3 uid="uid://ba3fvp0yravi4"] + +[ext_resource type="Texture2D" uid="uid://ijhmisl4foqa" path="res://assets/first_party/user_interface/touch-button.png" id="1_1t1pq"] +[ext_resource type="Script" uid="uid://dyyshnmovujje" path="res://scenes/ui_elements/touch_controls/components/touch_controls.gd" id="1_mjavl"] +[ext_resource type="Texture2D" uid="uid://dm8jbpjptaw00" path="res://assets/first_party/user_interface/touch-button_pressed.png" id="2_mjavl"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_fhnvg"] +size = Vector2(128, 128) + +[node name="TouchControls" type="Control" unique_id=980451545] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_mjavl") + +[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1944684301] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer" unique_id=1528042620] +layout_mode = 2 +size_flags_horizontal = 3 +joystick_mode = 1 +joystick_size = 192.0 +tip_size = 64.0 +action_left = &"move_left" +action_right = &"move_right" +action_up = &"move_up" +action_down = &"move_down" + +[node name="Control" type="Control" parent="HBoxContainer" unique_id=138367453] +custom_minimum_size = Vector2(148, 0) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/Control" unique_id=123091378] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 + +[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=2006198045] +custom_minimum_size = Vector2(148, 0) +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control" unique_id=83058655] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control/CenterContainer" unique_id=1553460435] +layout_mode = 2 + +[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control/CenterContainer/Control" unique_id=1825489206] +position = Vector2(-64, -64) +texture_normal = ExtResource("1_1t1pq") +texture_pressed = ExtResource("2_mjavl") +shape = SubResource("RectangleShape2D_fhnvg") +action = "pause" + +[node name="Control2" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=212784272] +custom_minimum_size = Vector2(148, 0) +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control2" unique_id=1274423800] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control2/CenterContainer" unique_id=1744849055] +layout_mode = 2 + +[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control2/CenterContainer/Control" unique_id=675930575] +position = Vector2(-64, -64) +texture_normal = ExtResource("1_1t1pq") +texture_pressed = ExtResource("2_mjavl") +shape = SubResource("RectangleShape2D_fhnvg") +action = "repel" + +[node name="Control3" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=911935960] +custom_minimum_size = Vector2(148, 0) +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control3" unique_id=677807753] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control3/CenterContainer" unique_id=645350051] +layout_mode = 2 + +[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control3/CenterContainer/Control" unique_id=1674418296] +position = Vector2(-64, -64) +texture_normal = ExtResource("1_1t1pq") +texture_pressed = ExtResource("2_mjavl") +shape = SubResource("RectangleShape2D_fhnvg") +action = "interact" From 70bbc1586e20a90d38f52d30bc124b7b00c1c95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 19:38:07 -0300 Subject: [PATCH 04/16] WIP --- project.godot | 4 ++-- .../props/hook_control/components/hook_control.gd | 4 ++++ scenes/globals/mouse_manager/mouse_manager.gd | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/project.godot b/project.godot index d425142dbe..a46fbbe936 100644 --- a/project.godot +++ b/project.godot @@ -267,14 +267,14 @@ repel={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":true,"script":null) -, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(53, 19),"global_position":Vector2(62, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) +, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(53, 19),"global_position":Vector2(62, 67),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null) ] } throw={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":120,"location":0,"echo":false,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null) -, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(92, 25),"global_position":Vector2(92, 54),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null) +, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(92, 25),"global_position":Vector2(92, 54),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null) ] } champ_blink={ diff --git a/scenes/game_elements/props/hook_control/components/hook_control.gd b/scenes/game_elements/props/hook_control/components/hook_control.gd index f311ba60d8..cb0235f715 100644 --- a/scenes/game_elements/props/hook_control/components/hook_control.gd +++ b/scenes/game_elements/props/hook_control/components/hook_control.gd @@ -94,6 +94,10 @@ var _pointing_at_area: HookableArea func _unhandled_input(_event: InputEvent) -> void: if _event is InputEventMouseMotion: + # Ignore the mouse motion that Godot emulates from touch, for example + # when dragging the virtual joystick. + if _event.device == InputEvent.DEVICE_ID_EMULATION: + return mouse_aiming_timer.start() var axis := get_global_mouse_position() - global_position if not axis.is_zero_approx(): diff --git a/scenes/globals/mouse_manager/mouse_manager.gd b/scenes/globals/mouse_manager/mouse_manager.gd index dc37b2e319..27b9545bd8 100644 --- a/scenes/globals/mouse_manager/mouse_manager.gd +++ b/scenes/globals/mouse_manager/mouse_manager.gd @@ -20,7 +20,9 @@ func _ready() -> void: func _input(event: InputEvent) -> void: - if event is InputEventMouseMotion: + # Only show the cursor for a real mouse, not for the mouse motion that Godot + # emulates from touch. + if event is InputEventMouseMotion and event.device != InputEvent.DEVICE_ID_EMULATION: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE mode_changed.emit() hide_timer.start() From 723c749adfa184800dff4cbb1cc1e02326973854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 19:43:07 -0300 Subject: [PATCH 05/16] WIP --- assets/first_party/user_interface/touch-button.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/first_party/user_interface/touch-button.png b/assets/first_party/user_interface/touch-button.png index ff919e5902..efdc801af2 100644 --- a/assets/first_party/user_interface/touch-button.png +++ b/assets/first_party/user_interface/touch-button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:faaf8e09b41d3eb529ef6f69ffb908f3ef0b6d307a532cc519074f2537e9436e -size 366 +oid sha256:59b7df1c831a27c66fa1304a80ab8938e3fee17030a5a20cc3ede8b3ccd720d1 +size 1288 From c51c5d8a327e56af4bd095bf4f1d707a0e5e2f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Tue, 13 Oct 2026 22:40:20 -0300 Subject: [PATCH 06/16] WIP --- .../game_logic/walk_behaviors/input_walk_behavior.gd | 10 ++++++---- scenes/ui_elements/input_hud/components/input_hud.gd | 3 ++- .../touch_controls/components/touch_controls.gd | 3 ++- scenes/ui_elements/touch_controls/touch_controls.tscn | 1 - 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd index c85fdaeba9..a9d75ef80e 100644 --- a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd +++ b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd @@ -52,13 +52,15 @@ func _ready() -> void: speeds = CharacterSpeeds.new() -func _unhandled_input(_event: InputEvent) -> void: +func _physics_process(delta: float) -> void: var axis := Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") - var speed := speeds.run_speed if Input.is_action_pressed(&"running") else speeds.walk_speed + var is_input_running := ( + Input.is_action_pressed(&"running") + or (axis.length_squared() == 1.0 and DisplayServer.is_touchscreen_available()) + ) + var speed := speeds.run_speed if is_input_running else speeds.walk_speed input_vector = axis * speed - -func _physics_process(delta: float) -> void: var step := ( speeds.stopping_step if character.velocity.length_squared() > input_vector.length_squared() diff --git a/scenes/ui_elements/input_hud/components/input_hud.gd b/scenes/ui_elements/input_hud/components/input_hud.gd index adcbe41ff7..fac1e3efe7 100644 --- a/scenes/ui_elements/input_hud/components/input_hud.gd +++ b/scenes/ui_elements/input_hud/components/input_hud.gd @@ -28,7 +28,8 @@ func _ready() -> void: func update_visibility() -> void: visible = ( - Settings.show_input_hud + not DisplayServer.is_touchscreen_available() + and Settings.show_input_hud and not get_tree().paused and not Transitions.is_running() and not displaying_dialogue diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index 01e3d8058b..015634f18d 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -20,7 +20,8 @@ func _notification(what: int) -> void: func update_visibility() -> void: visible = ( - not get_tree().paused + DisplayServer.is_touchscreen_available() + and not get_tree().paused and not Transitions.is_running() and not displaying_dialogue and (player or sokoban_ruleset) diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index a04cb55f2f..cab0cf3454 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -27,7 +27,6 @@ grow_vertical = 2 [node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer" unique_id=1528042620] layout_mode = 2 size_flags_horizontal = 3 -joystick_mode = 1 joystick_size = 192.0 tip_size = 64.0 action_left = &"move_left" From 1e2d96c34bfafc27594bb7a40871cd2635be8b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 09:58:51 -0300 Subject: [PATCH 07/16] WIP --- .../user_interface/interact-touch-button.png | 3 + .../interact-touch-button.png.import | 40 +++++ .../interact-touch-button_pressed.png | 3 + .../interact-touch-button_pressed.png.import | 40 +++++ .../user_interface/pause-touch-button.png | 3 + .../pause-touch-button.png.import | 40 +++++ .../pause-touch-button_pressed.png | 3 + .../pause-touch-button_pressed.png.import | 40 +++++ .../user_interface/repel-touch-button.png | 3 + .../repel-touch-button.png.import | 40 +++++ .../repel-touch-button_pressed.png | 3 + .../repel-touch-button_pressed.png.import | 40 +++++ .../components/touch_controls.gd | 38 +++++ .../touch_controls/touch_controls.tscn | 138 +++++++++--------- 14 files changed, 368 insertions(+), 66 deletions(-) create mode 100644 assets/first_party/user_interface/interact-touch-button.png create mode 100644 assets/first_party/user_interface/interact-touch-button.png.import create mode 100644 assets/first_party/user_interface/interact-touch-button_pressed.png create mode 100644 assets/first_party/user_interface/interact-touch-button_pressed.png.import create mode 100644 assets/first_party/user_interface/pause-touch-button.png create mode 100644 assets/first_party/user_interface/pause-touch-button.png.import create mode 100644 assets/first_party/user_interface/pause-touch-button_pressed.png create mode 100644 assets/first_party/user_interface/pause-touch-button_pressed.png.import create mode 100644 assets/first_party/user_interface/repel-touch-button.png create mode 100644 assets/first_party/user_interface/repel-touch-button.png.import create mode 100644 assets/first_party/user_interface/repel-touch-button_pressed.png create mode 100644 assets/first_party/user_interface/repel-touch-button_pressed.png.import diff --git a/assets/first_party/user_interface/interact-touch-button.png b/assets/first_party/user_interface/interact-touch-button.png new file mode 100644 index 0000000000..f2a29129e7 --- /dev/null +++ b/assets/first_party/user_interface/interact-touch-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87759982804ee6c527265bcf409e713ae345764dcb9ff0e6110a2d69f718846b +size 4852 diff --git a/assets/first_party/user_interface/interact-touch-button.png.import b/assets/first_party/user_interface/interact-touch-button.png.import new file mode 100644 index 0000000000..54c264d93b --- /dev/null +++ b/assets/first_party/user_interface/interact-touch-button.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ns6755757cje" +path="res://.godot/imported/interact-touch-button.png-7e3bd5c77ce05bfadea8bbec1012522e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/interact-touch-button.png" +dest_files=["res://.godot/imported/interact-touch-button.png-7e3bd5c77ce05bfadea8bbec1012522e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/interact-touch-button_pressed.png b/assets/first_party/user_interface/interact-touch-button_pressed.png new file mode 100644 index 0000000000..8f7c742195 --- /dev/null +++ b/assets/first_party/user_interface/interact-touch-button_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7791818acad2e097db61168284116a8731e7be240ec15d29c21f89ff0a7c6019 +size 4792 diff --git a/assets/first_party/user_interface/interact-touch-button_pressed.png.import b/assets/first_party/user_interface/interact-touch-button_pressed.png.import new file mode 100644 index 0000000000..8c8dee0ef1 --- /dev/null +++ b/assets/first_party/user_interface/interact-touch-button_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://iwooycswl1bb" +path="res://.godot/imported/interact-touch-button_pressed.png-4f1015b221802045bc3183355312d88c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/interact-touch-button_pressed.png" +dest_files=["res://.godot/imported/interact-touch-button_pressed.png-4f1015b221802045bc3183355312d88c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/pause-touch-button.png b/assets/first_party/user_interface/pause-touch-button.png new file mode 100644 index 0000000000..bf325d5952 --- /dev/null +++ b/assets/first_party/user_interface/pause-touch-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aa675b1e985d7708a2ca48b56f3b206547847ff550a3d8fa230298d76c31436 +size 1846 diff --git a/assets/first_party/user_interface/pause-touch-button.png.import b/assets/first_party/user_interface/pause-touch-button.png.import new file mode 100644 index 0000000000..3fb064ba87 --- /dev/null +++ b/assets/first_party/user_interface/pause-touch-button.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djsqefyd16tyg" +path="res://.godot/imported/pause-touch-button.png-fe592e54ffb11e5c5f4598de9767b24d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/pause-touch-button.png" +dest_files=["res://.godot/imported/pause-touch-button.png-fe592e54ffb11e5c5f4598de9767b24d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/pause-touch-button_pressed.png b/assets/first_party/user_interface/pause-touch-button_pressed.png new file mode 100644 index 0000000000..b59201ac58 --- /dev/null +++ b/assets/first_party/user_interface/pause-touch-button_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bd8ab11f5bd064ca069a6cb3021e73512c2c5c105c971ccdb99f3ee647ce0e7 +size 1860 diff --git a/assets/first_party/user_interface/pause-touch-button_pressed.png.import b/assets/first_party/user_interface/pause-touch-button_pressed.png.import new file mode 100644 index 0000000000..7b8dfa5f0d --- /dev/null +++ b/assets/first_party/user_interface/pause-touch-button_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dv5gfkbecnvas" +path="res://.godot/imported/pause-touch-button_pressed.png-74f7b845f9d96faaf843ab2c4313774b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/pause-touch-button_pressed.png" +dest_files=["res://.godot/imported/pause-touch-button_pressed.png-74f7b845f9d96faaf843ab2c4313774b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/repel-touch-button.png b/assets/first_party/user_interface/repel-touch-button.png new file mode 100644 index 0000000000..e6773a93e5 --- /dev/null +++ b/assets/first_party/user_interface/repel-touch-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0ad2217d78e356779bc5fc4b4d7ba5a233beb4d04ea8f4cb8815a0f53118320 +size 6066 diff --git a/assets/first_party/user_interface/repel-touch-button.png.import b/assets/first_party/user_interface/repel-touch-button.png.import new file mode 100644 index 0000000000..cfda3ccf3e --- /dev/null +++ b/assets/first_party/user_interface/repel-touch-button.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://byra1c7yc54mg" +path="res://.godot/imported/repel-touch-button.png-79ecb9d4a242ef90e0ef33cb07527472.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/repel-touch-button.png" +dest_files=["res://.godot/imported/repel-touch-button.png-79ecb9d4a242ef90e0ef33cb07527472.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/repel-touch-button_pressed.png b/assets/first_party/user_interface/repel-touch-button_pressed.png new file mode 100644 index 0000000000..d51b295cd3 --- /dev/null +++ b/assets/first_party/user_interface/repel-touch-button_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:285582a58fd81e50d83b96273b03d59c45dcb90315e16c61ff60a59421c05f0f +size 5976 diff --git a/assets/first_party/user_interface/repel-touch-button_pressed.png.import b/assets/first_party/user_interface/repel-touch-button_pressed.png.import new file mode 100644 index 0000000000..fafe6a63a6 --- /dev/null +++ b/assets/first_party/user_interface/repel-touch-button_pressed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cttum8v2mb62v" +path="res://.godot/imported/repel-touch-button_pressed.png-74661814a87f36012eabd5e398a676c6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/first_party/user_interface/repel-touch-button_pressed.png" +dest_files=["res://.godot/imported/repel-touch-button_pressed.png-74661814a87f36012eabd5e398a676c6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index 015634f18d..a185e65769 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -6,6 +6,13 @@ extends Control @export var sokoban_ruleset: RuleEngine @export var displaying_dialogue: bool +var player_interaction: PlayerInteraction +var player_repel: PlayerRepel +var player_hook: PlayerHook + +@onready var interact_button: TouchScreenButton = %InteractButton +@onready var repel_button: TouchScreenButton = %RepelButton + func _ready() -> void: Transitions.started.connect(update_visibility) @@ -26,3 +33,34 @@ func update_visibility() -> void: and not displaying_dialogue and (player or sokoban_ruleset) ) + + if player: + player_interaction = player.get("player_interaction") as PlayerInteraction + if ( + player_interaction + and not player_interaction.interact_action_changed.is_connected(_update_player_state) + ): + player_interaction.interact_action_changed.connect(_update_player_state) + + player_repel = player.get("player_repel") as PlayerRepel + if player_repel and not player_repel.visibility_changed.is_connected(_update_player_state): + player_repel.visibility_changed.connect(_update_player_state) + + player_hook = player.get("player_hook") as PlayerHook + if player_hook and not player_hook.visibility_changed.is_connected(_update_player_state): + player_hook.visibility_changed.connect(_update_player_state) + + _update_player_state() + + +func _update_player_state() -> void: + if player_interaction: + var action := player_interaction.get_interact_action() + interact_button.visible = not action.is_empty() + else: + interact_button.visible = false + + repel_button.visible = player_repel and player_repel.visible + + # throw_input_hint.visible = player_hook and player_hook.visible + # aim_input_hint.visible = player_hook and player_hook.visible diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index cab0cf3454..da4c4567a8 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -1,8 +1,12 @@ [gd_scene format=3 uid="uid://ba3fvp0yravi4"] -[ext_resource type="Texture2D" uid="uid://ijhmisl4foqa" path="res://assets/first_party/user_interface/touch-button.png" id="1_1t1pq"] [ext_resource type="Script" uid="uid://dyyshnmovujje" path="res://scenes/ui_elements/touch_controls/components/touch_controls.gd" id="1_mjavl"] -[ext_resource type="Texture2D" uid="uid://dm8jbpjptaw00" path="res://assets/first_party/user_interface/touch-button_pressed.png" id="2_mjavl"] +[ext_resource type="Texture2D" uid="uid://djsqefyd16tyg" path="res://assets/first_party/user_interface/pause-touch-button.png" id="2_pt1mb"] +[ext_resource type="Texture2D" uid="uid://dv5gfkbecnvas" path="res://assets/first_party/user_interface/pause-touch-button_pressed.png" id="3_qk0o5"] +[ext_resource type="Texture2D" uid="uid://byra1c7yc54mg" path="res://assets/first_party/user_interface/repel-touch-button.png" id="4_pk8s8"] +[ext_resource type="Texture2D" uid="uid://cttum8v2mb62v" path="res://assets/first_party/user_interface/repel-touch-button_pressed.png" id="5_dtb3f"] +[ext_resource type="Texture2D" uid="uid://ns6755757cje" path="res://assets/first_party/user_interface/interact-touch-button.png" id="6_pt1mb"] +[ext_resource type="Texture2D" uid="uid://iwooycswl1bb" path="res://assets/first_party/user_interface/interact-touch-button_pressed.png" id="7_qk0o5"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_fhnvg"] size = Vector2(128, 128) @@ -15,8 +19,18 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_mjavl") +metadata/_edit_horizontal_guides_ = [545.0] -[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1944684301] +[node name="Control" type="Control" parent="." unique_id=2006198045] +custom_minimum_size = Vector2(148, 148) +layout_mode = 1 +anchors_preset = 0 +offset_right = 156.0 +offset_bottom = 148.0 +size_flags_horizontal = 8 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="Control" unique_id=83058655] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -24,98 +38,90 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer" unique_id=1528042620] +[node name="Control" type="Control" parent="Control/CenterContainer" unique_id=1553460435] layout_mode = 2 -size_flags_horizontal = 3 -joystick_size = 192.0 -tip_size = 64.0 -action_left = &"move_left" -action_right = &"move_right" -action_up = &"move_up" -action_down = &"move_down" -[node name="Control" type="Control" parent="HBoxContainer" unique_id=138367453] -custom_minimum_size = Vector2(148, 0) -layout_mode = 2 -size_flags_horizontal = 3 +[node name="TouchScreenButton" type="TouchScreenButton" parent="Control/CenterContainer/Control" unique_id=1825489206] +position = Vector2(-64, -64) +texture_normal = ExtResource("2_pt1mb") +texture_pressed = ExtResource("3_qk0o5") +shape = SubResource("RectangleShape2D_fhnvg") +action = "pause" -[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/Control" unique_id=123091378] +[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1944684301] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer" unique_id=621688913] +layout_mode = 2 size_flags_horizontal = 3 +size_flags_stretch_ratio = 0.6 -[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=2006198045] -custom_minimum_size = Vector2(148, 0) +[node name="Control" type="Control" parent="HBoxContainer/VBoxContainer" unique_id=1054363867] layout_mode = 2 -size_flags_horizontal = 8 size_flags_vertical = 3 -[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control" unique_id=83058655] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control/CenterContainer" unique_id=1553460435] +[node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer/VBoxContainer" unique_id=1528042620] layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +joystick_mode = 1 +joystick_size = 192.0 +tip_size = 64.0 +action_left = &"move_left" +action_right = &"move_right" +action_up = &"move_up" +action_down = &"move_down" -[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control/CenterContainer/Control" unique_id=1825489206] -position = Vector2(-64, -64) -texture_normal = ExtResource("1_1t1pq") -texture_pressed = ExtResource("2_mjavl") -shape = SubResource("RectangleShape2D_fhnvg") -action = "pause" - -[node name="Control2" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=212784272] +[node name="Control" type="Control" parent="HBoxContainer" unique_id=138367453] custom_minimum_size = Vector2(148, 0) layout_mode = 2 -size_flags_horizontal = 8 -size_flags_vertical = 3 +size_flags_horizontal = 3 -[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control2" unique_id=1274423800] +[node name="Control" type="Control" parent="HBoxContainer/Control" unique_id=1744849055] layout_mode = 1 -anchors_preset = 15 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control2/CenterContainer" unique_id=1744849055] -layout_mode = 2 - -[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control2/CenterContainer/Control" unique_id=675930575] +offset_left = -216.0 +offset_top = -175.0 +offset_right = -216.0 +offset_bottom = -175.0 +grow_horizontal = 0 +grow_vertical = 0 + +[node name="RepelButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control" unique_id=675930575] +unique_name_in_owner = true position = Vector2(-64, -64) -texture_normal = ExtResource("1_1t1pq") -texture_pressed = ExtResource("2_mjavl") +texture_normal = ExtResource("4_pk8s8") +texture_pressed = ExtResource("5_dtb3f") shape = SubResource("RectangleShape2D_fhnvg") action = "repel" -[node name="Control3" type="Control" parent="HBoxContainer/Control/VBoxContainer" unique_id=911935960] -custom_minimum_size = Vector2(148, 0) -layout_mode = 2 -size_flags_horizontal = 8 -size_flags_vertical = 3 - -[node name="CenterContainer" type="CenterContainer" parent="HBoxContainer/Control/VBoxContainer/Control3" unique_id=677807753] +[node name="Control2" type="Control" parent="HBoxContainer/Control" unique_id=645350051] layout_mode = 1 -anchors_preset = 15 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="Control" type="Control" parent="HBoxContainer/Control/VBoxContainer/Control3/CenterContainer" unique_id=645350051] -layout_mode = 2 - -[node name="TouchScreenButton" type="TouchScreenButton" parent="HBoxContainer/Control/VBoxContainer/Control3/CenterContainer/Control" unique_id=1674418296] +offset_left = -143.0 +offset_top = -343.0 +offset_right = -143.0 +offset_bottom = -343.0 +grow_horizontal = 0 +grow_vertical = 0 + +[node name="InteractButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control2" unique_id=1674418296] +unique_name_in_owner = true position = Vector2(-64, -64) -texture_normal = ExtResource("1_1t1pq") -texture_pressed = ExtResource("2_mjavl") +texture_normal = ExtResource("6_pt1mb") +texture_pressed = ExtResource("7_qk0o5") shape = SubResource("RectangleShape2D_fhnvg") action = "interact" From 51172f3d9996ecdb0ab6c27369c2cff9b53f3cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 14:33:57 -0300 Subject: [PATCH 08/16] WIP --- .../components/touch_controls.gd | 19 +++++++++++++++++++ .../touch_controls/touch_controls.tscn | 1 + 2 files changed, 20 insertions(+) diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index a185e65769..3dc1df19c3 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -12,9 +12,11 @@ var player_hook: PlayerHook @onready var interact_button: TouchScreenButton = %InteractButton @onready var repel_button: TouchScreenButton = %RepelButton +@onready var virtual_joystick: VirtualJoystick = %VirtualJoystick func _ready() -> void: + visibility_changed.connect(_on_visibility_changed) Transitions.started.connect(update_visibility) Transitions.finished.connect(update_visibility) @@ -25,6 +27,23 @@ func _notification(what: int) -> void: update_visibility() +func _on_visibility_changed() -> void: + if not is_visible_in_tree(): + _reset_virtual_joystick() + + +## Release the virtual joystick and the movement actions it keeps pressed. +## [br][br] +## A hidden [VirtualJoystick] doesn't receive the touch release, so if it's +## hidden while touched, for example by a transition, the player would keep +## walking. It has no public method to reset it, but changing its size resets +## it, so change the size and put it back. +func _reset_virtual_joystick() -> void: + var tip_size := virtual_joystick.tip_size + virtual_joystick.tip_size = tip_size + 1.0 + virtual_joystick.tip_size = tip_size + + func update_visibility() -> void: visible = ( DisplayServer.is_touchscreen_available() diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index da4c4567a8..7c201cf79d 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -66,6 +66,7 @@ layout_mode = 2 size_flags_vertical = 3 [node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer/VBoxContainer" unique_id=1528042620] +unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 From ec9336d67f08464f63967544c8fdd8a1a2215bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 14:54:23 -0300 Subject: [PATCH 09/16] WIP --- .../hook_control/components/hook_control.gd | 49 +++++++++++++++++-- .../components/touch_controls.gd | 6 +++ .../touch_controls/touch_controls.tscn | 10 ++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/scenes/game_elements/props/hook_control/components/hook_control.gd b/scenes/game_elements/props/hook_control/components/hook_control.gd index cb0235f715..1475c042ab 100644 --- a/scenes/game_elements/props/hook_control/components/hook_control.gd +++ b/scenes/game_elements/props/hook_control/components/hook_control.gd @@ -15,6 +15,9 @@ extends Node2D ## Additionally, the mouse movement can also be used for aiming, ## but the mouse not considered the primary input. ## [br][br] +## On touchscreens, touching the screen aims towards the touch position and +## throws, like clicking with the mouse. +## [br][br] ## Aiming is effectively rotating the [member ray_cast_2d]. ## [br][br] ## Visually it displays the [member sprite_2d] texture pointing in the @@ -77,6 +80,9 @@ var _hook_angle: float var _pointing_at_area: HookableArea +## The index of the finger touching the screen to aim and throw, or -1. +var _touch_index: int = -1 + ## The visual representation of the [member ray_cast_2d] direction. ## It also indicates if the ray is hitting a [HookableArea] by displaying ## the sprite solid, or semitransparent if not. @@ -93,16 +99,16 @@ var _pointing_at_area: HookableArea func _unhandled_input(_event: InputEvent) -> void: + if _event is InputEventScreenTouch or _event is InputEventScreenDrag: + _handle_touch(_event) + return + if _event is InputEventMouseMotion: # Ignore the mouse motion that Godot emulates from touch, for example # when dragging the virtual joystick. if _event.device == InputEvent.DEVICE_ID_EMULATION: return - mouse_aiming_timer.start() - var axis := get_global_mouse_position() - global_position - if not axis.is_zero_approx(): - _hook_angle = axis.angle() - _hook_angle_set = true + _aim_at(get_global_mouse_position()) return # When aiming with keyboard, do not change the hook angle immediately if one of these actions @@ -136,6 +142,37 @@ func _unhandled_input(_event: InputEvent) -> void: return +## Touching the screen aims towards the touch position and throws, like +## clicking with the mouse. Dragging the finger keeps aiming. Only one finger +## is followed at a time. +func _handle_touch(event: InputEvent) -> void: + if event is InputEventScreenDrag: + if event.index == _touch_index: + _aim_at(_screen_to_global(event.position)) + elif event.pressed: + if _touch_index != -1: + return + _touch_index = event.index + _aim_at(_screen_to_global(event.position)) + pressing_throw_action = true + elif event.index == _touch_index: + _touch_index = -1 + pressing_throw_action = false + throw_failed_while_pressing = false + + +func _screen_to_global(screen_position: Vector2) -> Vector2: + return get_canvas_transform().affine_inverse() * screen_position + + +func _aim_at(target: Vector2) -> void: + mouse_aiming_timer.start() + var axis := target - global_position + if not axis.is_zero_approx(): + _hook_angle = axis.angle() + _hook_angle_set = true + + func _update_hook_angle() -> void: var axis: Vector2 axis = Input.get_vector(&"aim_left", &"aim_right", &"aim_up", &"aim_down") @@ -222,6 +259,8 @@ func _set_state(new_state: State) -> void: rotation = 0 pressing_throw_action = false throw_failed_while_pressing = false + # A disabled control doesn't get the touch release. + _touch_index = -1 clear_pointing_at_area() State.AIMING_PAUSED: clear_pointing_at_area() diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index 3dc1df19c3..263948e35a 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -17,6 +17,12 @@ var player_hook: PlayerHook func _ready() -> void: visibility_changed.connect(_on_visibility_changed) + # TouchScreenButton doesn't mark its touches as handled, so the game would + # also get them. For example, tapping the interact button would throw the + # grappling hook too. + for button: TouchScreenButton in [interact_button, repel_button]: + button.pressed.connect(get_viewport().set_input_as_handled) + button.released.connect(get_viewport().set_input_as_handled) Transitions.started.connect(update_visibility) Transitions.finished.connect(update_visibility) diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index 7c201cf79d..58e7d9c8c0 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -18,6 +18,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +mouse_filter = 2 script = ExtResource("1_mjavl") metadata/_edit_horizontal_guides_ = [545.0] @@ -29,6 +30,7 @@ offset_right = 156.0 offset_bottom = 148.0 size_flags_horizontal = 8 size_flags_vertical = 3 +mouse_filter = 2 [node name="CenterContainer" type="CenterContainer" parent="Control" unique_id=83058655] layout_mode = 1 @@ -37,9 +39,11 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +mouse_filter = 2 [node name="Control" type="Control" parent="Control/CenterContainer" unique_id=1553460435] layout_mode = 2 +mouse_filter = 2 [node name="TouchScreenButton" type="TouchScreenButton" parent="Control/CenterContainer/Control" unique_id=1825489206] position = Vector2(-64, -64) @@ -55,15 +59,18 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +mouse_filter = 2 [node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer" unique_id=621688913] layout_mode = 2 size_flags_horizontal = 3 size_flags_stretch_ratio = 0.6 +mouse_filter = 2 [node name="Control" type="Control" parent="HBoxContainer/VBoxContainer" unique_id=1054363867] layout_mode = 2 size_flags_vertical = 3 +mouse_filter = 2 [node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer/VBoxContainer" unique_id=1528042620] unique_name_in_owner = true @@ -82,6 +89,7 @@ action_down = &"move_down" custom_minimum_size = Vector2(148, 0) layout_mode = 2 size_flags_horizontal = 3 +mouse_filter = 2 [node name="Control" type="Control" parent="HBoxContainer/Control" unique_id=1744849055] layout_mode = 1 @@ -96,6 +104,7 @@ offset_right = -216.0 offset_bottom = -175.0 grow_horizontal = 0 grow_vertical = 0 +mouse_filter = 2 [node name="RepelButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control" unique_id=675930575] unique_name_in_owner = true @@ -118,6 +127,7 @@ offset_right = -143.0 offset_bottom = -343.0 grow_horizontal = 0 grow_vertical = 0 +mouse_filter = 2 [node name="InteractButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control2" unique_id=1674418296] unique_name_in_owner = true From d217c1ef4827d293411ce67c5a45ea37bd6e5943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 16:23:56 -0300 Subject: [PATCH 10/16] WIP --- scenes/menus/splash/components/splash.gd | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scenes/menus/splash/components/splash.gd b/scenes/menus/splash/components/splash.gd index dc6a7cbc98..c8107e1a69 100644 --- a/scenes/menus/splash/components/splash.gd +++ b/scenes/menus/splash/components/splash.gd @@ -15,6 +15,15 @@ func _ready() -> void: scene_switch_timer.timeout.connect(switch_to_intro) +func _input(event: InputEvent) -> void: + # On touchscreens, a tap anywhere skips the splash. This is in _input() + # rather than _unhandled_input() because the controls of the splash would + # otherwise take the touch first. + if event is InputEventScreenTouch and event.pressed: + get_viewport().set_input_as_handled() + switch_to_intro() + + func _unhandled_input(event: InputEvent) -> void: if ( event.is_action_pressed(&"dialogue_next") @@ -26,5 +35,8 @@ func _unhandled_input(event: InputEvent) -> void: func switch_to_intro() -> void: + # A second tap or key press during the transition would switch again. + if not scene_switch_timer.timeout.is_connected(switch_to_intro): + return scene_switch_timer.timeout.disconnect(switch_to_intro) scene_link.switch() From 01ca07856a76ea7e3e341200513c2e3ef9a7caf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 18:35:24 -0300 Subject: [PATCH 11/16] WIP --- .../hook_control/components/hook_control.gd | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scenes/game_elements/props/hook_control/components/hook_control.gd b/scenes/game_elements/props/hook_control/components/hook_control.gd index 1475c042ab..5aa1632e7c 100644 --- a/scenes/game_elements/props/hook_control/components/hook_control.gd +++ b/scenes/game_elements/props/hook_control/components/hook_control.gd @@ -15,8 +15,8 @@ extends Node2D ## Additionally, the mouse movement can also be used for aiming, ## but the mouse not considered the primary input. ## [br][br] -## On touchscreens, touching the screen aims towards the touch position and -## throws, like clicking with the mouse. +## On touchscreens, touching the screen aims towards the touch position, and +## dragging the finger keeps aiming. Lifting the finger throws. ## [br][br] ## Aiming is effectively rotating the [member ray_cast_2d]. ## [br][br] @@ -83,6 +83,9 @@ var _pointing_at_area: HookableArea ## The index of the finger touching the screen to aim and throw, or -1. var _touch_index: int = -1 +## True if the finger was lifted, so the next frame should throw. +var _touch_throw_requested: bool = false + ## The visual representation of the [member ray_cast_2d] direction. ## It also indicates if the ray is hitting a [HookableArea] by displaying ## the sprite solid, or semitransparent if not. @@ -142,9 +145,9 @@ func _unhandled_input(_event: InputEvent) -> void: return -## Touching the screen aims towards the touch position and throws, like -## clicking with the mouse. Dragging the finger keeps aiming. Only one finger -## is followed at a time. +## Touching the screen aims towards the touch position, and dragging the +## finger keeps aiming. Lifting the finger throws. Only one finger is followed +## at a time. func _handle_touch(event: InputEvent) -> void: if event is InputEventScreenDrag: if event.index == _touch_index: @@ -154,11 +157,10 @@ func _handle_touch(event: InputEvent) -> void: return _touch_index = event.index _aim_at(_screen_to_global(event.position)) - pressing_throw_action = true elif event.index == _touch_index: _touch_index = -1 - pressing_throw_action = false - throw_failed_while_pressing = false + _aim_at(_screen_to_global(event.position)) + _touch_throw_requested = true func _screen_to_global(screen_position: Vector2) -> Vector2: @@ -261,6 +263,7 @@ func _set_state(new_state: State) -> void: throw_failed_while_pressing = false # A disabled control doesn't get the touch release. _touch_index = -1 + _touch_throw_requested = false clear_pointing_at_area() State.AIMING_PAUSED: clear_pointing_at_area() @@ -279,13 +282,25 @@ func _process(_delta: float) -> void: if state == State.DISABLED: return rotation = _get_hook_angle() + var touch_throw := _touch_throw_requested + _touch_throw_requested = false + if touch_throw: + # The finger may have been lifted somewhere else than the last drag, so + # update the ray now instead of waiting for the next physics frame. + ray_cast_2d.force_raycast_update() if state == State.AIMING: _update_pointing_at_area() sprite_2d.modulate = Color.WHITE if _pointing_at_area else Color(Color.WHITE, 0.5) if hooked_to: # Currently the control can be hooked to a single area. return - if state != State.AIMING_PAUSED and pressing_throw_action and not throw_failed_while_pressing: + if state == State.AIMING_PAUSED: + return + if touch_throw: + _throw() + # Unlike holding the throw action, a touch throws only once. + throw_failed_while_pressing = false + elif pressing_throw_action and not throw_failed_while_pressing: _throw() From 1f32ecb71e923b2bd9bd369e696d9ac85f7d5fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Wed, 14 Oct 2026 18:43:56 -0300 Subject: [PATCH 12/16] WIP --- .../components/touch_controls.gd | 5 +- .../touch_controls/touch_controls.tscn | 74 +++++-------------- 2 files changed, 18 insertions(+), 61 deletions(-) diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index 263948e35a..31779b04ed 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -85,7 +85,4 @@ func _update_player_state() -> void: else: interact_button.visible = false - repel_button.visible = player_repel and player_repel.visible - - # throw_input_hint.visible = player_hook and player_hook.visible - # aim_input_hint.visible = player_hook and player_hook.visible + repel_button.visible = player_repel and player_repel.visible and not interact_button.visible diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index 58e7d9c8c0..b39052767a 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -20,7 +20,7 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 script = ExtResource("1_mjavl") -metadata/_edit_horizontal_guides_ = [545.0] +metadata/_edit_horizontal_guides_ = [593.0] [node name="Control" type="Control" parent="." unique_id=2006198045] custom_minimum_size = Vector2(148, 148) @@ -32,47 +32,28 @@ size_flags_horizontal = 8 size_flags_vertical = 3 mouse_filter = 2 -[node name="CenterContainer" type="CenterContainer" parent="Control" unique_id=83058655] +[node name="CenterContainer" type="BoxContainer" parent="Control" unique_id=759928299] layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 mouse_filter = 2 -[node name="Control" type="Control" parent="Control/CenterContainer" unique_id=1553460435] -layout_mode = 2 -mouse_filter = 2 - -[node name="TouchScreenButton" type="TouchScreenButton" parent="Control/CenterContainer/Control" unique_id=1825489206] -position = Vector2(-64, -64) +[node name="TouchScreenButton" type="TouchScreenButton" parent="Control/CenterContainer" unique_id=1825489206] +position = Vector2(16, 16) texture_normal = ExtResource("2_pt1mb") texture_pressed = ExtResource("3_qk0o5") shape = SubResource("RectangleShape2D_fhnvg") action = "pause" -[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1944684301] +[node name="BottomLeftContainer" type="BoxContainer" parent="." unique_id=1803916095] +custom_minimum_size = Vector2(250, 250) layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 +anchors_preset = 2 +anchor_top = 1.0 anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer" unique_id=621688913] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_stretch_ratio = 0.6 -mouse_filter = 2 - -[node name="Control" type="Control" parent="HBoxContainer/VBoxContainer" unique_id=1054363867] -layout_mode = 2 -size_flags_vertical = 3 -mouse_filter = 2 +offset_top = -250.0 +offset_right = 250.0 +grow_vertical = 0 -[node name="VirtualJoystick" type="VirtualJoystick" parent="HBoxContainer/VBoxContainer" unique_id=1528042620] +[node name="VirtualJoystick" type="VirtualJoystick" parent="BottomLeftContainer" unique_id=1528042620] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 @@ -85,13 +66,7 @@ action_right = &"move_right" action_up = &"move_up" action_down = &"move_down" -[node name="Control" type="Control" parent="HBoxContainer" unique_id=138367453] -custom_minimum_size = Vector2(148, 0) -layout_mode = 2 -size_flags_horizontal = 3 -mouse_filter = 2 - -[node name="Control" type="Control" parent="HBoxContainer/Control" unique_id=1744849055] +[node name="BottomRightContariner" type="Control" parent="." unique_id=1744849055] layout_mode = 1 anchors_preset = 3 anchor_left = 1.0 @@ -106,32 +81,17 @@ grow_horizontal = 0 grow_vertical = 0 mouse_filter = 2 -[node name="RepelButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control" unique_id=675930575] +[node name="RepelButton" type="TouchScreenButton" parent="BottomRightContariner" unique_id=675930575] unique_name_in_owner = true -position = Vector2(-64, -64) +position = Vector2(30, -16) texture_normal = ExtResource("4_pk8s8") texture_pressed = ExtResource("5_dtb3f") shape = SubResource("RectangleShape2D_fhnvg") action = "repel" -[node name="Control2" type="Control" parent="HBoxContainer/Control" unique_id=645350051] -layout_mode = 1 -anchors_preset = 3 -anchor_left = 1.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = -143.0 -offset_top = -343.0 -offset_right = -143.0 -offset_bottom = -343.0 -grow_horizontal = 0 -grow_vertical = 0 -mouse_filter = 2 - -[node name="InteractButton" type="TouchScreenButton" parent="HBoxContainer/Control/Control2" unique_id=1674418296] +[node name="InteractButton" type="TouchScreenButton" parent="BottomRightContariner" unique_id=1674418296] unique_name_in_owner = true -position = Vector2(-64, -64) +position = Vector2(30, -16) texture_normal = ExtResource("6_pt1mb") texture_pressed = ExtResource("7_qk0o5") shape = SubResource("RectangleShape2D_fhnvg") From f6bb27ac1d32c1c45fde46da87b7132a5f65111d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Thu, 15 Oct 2026 10:41:01 -0300 Subject: [PATCH 13/16] WIP --- .../user_interface/touch-button.png | 3 -- .../user_interface/touch-button.png.import | 40 ------------------- .../user_interface/touch-button_pressed.png | 3 -- .../touch-button_pressed.png.import | 40 ------------------- 4 files changed, 86 deletions(-) delete mode 100644 assets/first_party/user_interface/touch-button.png delete mode 100644 assets/first_party/user_interface/touch-button.png.import delete mode 100644 assets/first_party/user_interface/touch-button_pressed.png delete mode 100644 assets/first_party/user_interface/touch-button_pressed.png.import diff --git a/assets/first_party/user_interface/touch-button.png b/assets/first_party/user_interface/touch-button.png deleted file mode 100644 index efdc801af2..0000000000 --- a/assets/first_party/user_interface/touch-button.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:59b7df1c831a27c66fa1304a80ab8938e3fee17030a5a20cc3ede8b3ccd720d1 -size 1288 diff --git a/assets/first_party/user_interface/touch-button.png.import b/assets/first_party/user_interface/touch-button.png.import deleted file mode 100644 index 1f417d2498..0000000000 --- a/assets/first_party/user_interface/touch-button.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://ijhmisl4foqa" -path="res://.godot/imported/touch-button.png-573bfdf492ca68ead333f9cc90e542fd.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://assets/first_party/user_interface/touch-button.png" -dest_files=["res://.godot/imported/touch-button.png-573bfdf492ca68ead333f9cc90e542fd.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/assets/first_party/user_interface/touch-button_pressed.png b/assets/first_party/user_interface/touch-button_pressed.png deleted file mode 100644 index 4c6a85d192..0000000000 --- a/assets/first_party/user_interface/touch-button_pressed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:55324998e869b85b5c60486e492321098749a9227658db68fe16875aa521ed99 -size 535 diff --git a/assets/first_party/user_interface/touch-button_pressed.png.import b/assets/first_party/user_interface/touch-button_pressed.png.import deleted file mode 100644 index a62636b6e3..0000000000 --- a/assets/first_party/user_interface/touch-button_pressed.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://dm8jbpjptaw00" -path="res://.godot/imported/touch-button_pressed.png-daf3b6540e79f17c4258784e61ee0203.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://assets/first_party/user_interface/touch-button_pressed.png" -dest_files=["res://.godot/imported/touch-button_pressed.png-daf3b6540e79f17c4258784e61ee0203.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 From 2890dc3592b36a88ab41e1fb75de807e10fdfdf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Thu, 15 Oct 2026 10:54:38 -0300 Subject: [PATCH 14/16] WIP --- web/full-size.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/full-size.html b/web/full-size.html index a523288be2..a47ddfe070 100644 --- a/web/full-size.html +++ b/web/full-size.html @@ -179,9 +179,13 @@ // wouldn't know it's fullscreen. document.getElementById('canvas').requestFullscreen().then(() => { // Browsers only allow locking the orientation in fullscreen. Safari - // and desktop browsers don't support it at all. + // doesn't have it at all. Desktop browsers have it, but there is no + // way to tell that they don't support it other than calling it, so + // that error is expected and not reported. return screen.orientation?.lock?.('landscape').catch((err) => { - console.warn('Could not lock the orientation:', err); + if (err.name !== 'NotSupportedError') { + console.warn('Could not lock the orientation:', err); + } }); }, (err) => { console.warn('Could not enter fullscreen:', err); From 195a4718c3afe4607c12862ce26e43777b9947fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Thu, 15 Oct 2026 11:01:35 -0300 Subject: [PATCH 15/16] WIP --- .../title/components/touchscreen_warning.tscn | 70 ------------------- .../touch_controls/touch_controls.tscn | 6 +- 2 files changed, 3 insertions(+), 73 deletions(-) delete mode 100644 scenes/menus/title/components/touchscreen_warning.tscn diff --git a/scenes/menus/title/components/touchscreen_warning.tscn b/scenes/menus/title/components/touchscreen_warning.tscn deleted file mode 100644 index e5adc63a8c..0000000000 --- a/scenes/menus/title/components/touchscreen_warning.tscn +++ /dev/null @@ -1,70 +0,0 @@ -[gd_scene format=3 uid="uid://cbf7ft3j4hemp"] - -[sub_resource type="GDScript" id="GDScript_4hx3q"] -script/source = "# SPDX-FileCopyrightText: The Threadbare Authors -# SPDX-License-Identifier: MPL-2.0 -extends Control - -signal dismissed - - -func _ready() -> void: - if _should_show_touchscreen_warning(): - show() - - -func _should_show_touchscreen_warning() -> bool: - var has_touch: bool = DisplayServer.is_touchscreen_available() - var has_keyboard: bool - if OS.has_feature(\"web_android\") or OS.has_feature(\"web_ios\"): - # DisplayServer.has_hardware_keyboard() is not implemented on the web. - # Assuming mobile web browsers don't have a hardware keyboard. - has_keyboard = false - else: - has_keyboard = DisplayServer.has_hardware_keyboard() - return has_touch and not has_keyboard - - -func _on_dismiss_button_pressed() -> void: - dismissed.emit() -" - -[node name="TouchscreenWarning" type="Control" unique_id=2046863085] -layout_mode = 3 -anchors_preset = 0 -script = SubResource("GDScript_4hx3q") -metadata/_tab_index = 0 - -[node name="PanelContainer" type="PanelContainer" parent="." unique_id=479124586] -layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -150.5 -offset_top = -132.5 -offset_right = 150.5 -offset_bottom = 132.5 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer" unique_id=1554237287] -layout_mode = 2 -alignment = 1 - -[node name="Label" type="Label" parent="PanelContainer/VBoxContainer" unique_id=969003221] -custom_minimum_size = Vector2(640, 0) -layout_mode = 2 -text = "You need a gamepad or a keyboard to play Threadbare. Threadbare is not playable with a touchscreen only." -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="DismissButton" type="Button" parent="PanelContainer/VBoxContainer" unique_id=1452001342] -layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 -text = "Dismiss " -icon_alignment = 1 - -[connection signal="pressed" from="PanelContainer/VBoxContainer/DismissButton" to="." method="_on_dismiss_button_pressed"] diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index b39052767a..3a7d7b9e07 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -66,7 +66,7 @@ action_right = &"move_right" action_up = &"move_up" action_down = &"move_down" -[node name="BottomRightContariner" type="Control" parent="." unique_id=1744849055] +[node name="BottomRightContainer" type="Control" parent="." unique_id=1744849055] layout_mode = 1 anchors_preset = 3 anchor_left = 1.0 @@ -81,7 +81,7 @@ grow_horizontal = 0 grow_vertical = 0 mouse_filter = 2 -[node name="RepelButton" type="TouchScreenButton" parent="BottomRightContariner" unique_id=675930575] +[node name="RepelButton" type="TouchScreenButton" parent="BottomRightContainer" unique_id=675930575] unique_name_in_owner = true position = Vector2(30, -16) texture_normal = ExtResource("4_pk8s8") @@ -89,7 +89,7 @@ texture_pressed = ExtResource("5_dtb3f") shape = SubResource("RectangleShape2D_fhnvg") action = "repel" -[node name="InteractButton" type="TouchScreenButton" parent="BottomRightContariner" unique_id=1674418296] +[node name="InteractButton" type="TouchScreenButton" parent="BottomRightContainer" unique_id=1674418296] unique_name_in_owner = true position = Vector2(30, -16) texture_normal = ExtResource("6_pt1mb") From 6098bb24a987460bd37cd7ca715d3759230b22cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Thu, 15 Oct 2026 11:06:15 -0300 Subject: [PATCH 16/16] WIP --- scenes/game_logic/walk_behaviors/input_walk_behavior.gd | 4 +++- .../ui_elements/touch_controls/components/touch_controls.gd | 3 ++- scenes/ui_elements/touch_controls/touch_controls.tscn | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd index a9d75ef80e..a196dddf47 100644 --- a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd +++ b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd @@ -54,9 +54,11 @@ func _ready() -> void: func _physics_process(delta: float) -> void: var axis := Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down") + # On touchscreens, pushing the virtual joystick all the way runs. The length + # of the axis may fall slightly short of 1, so compare with a threshold. var is_input_running := ( Input.is_action_pressed(&"running") - or (axis.length_squared() == 1.0 and DisplayServer.is_touchscreen_available()) + or (axis.length_squared() > 0.99 and DisplayServer.is_touchscreen_available()) ) var speed := speeds.run_speed if is_input_running else speeds.walk_speed input_vector = axis * speed diff --git a/scenes/ui_elements/touch_controls/components/touch_controls.gd b/scenes/ui_elements/touch_controls/components/touch_controls.gd index 31779b04ed..f7c5cc91af 100644 --- a/scenes/ui_elements/touch_controls/components/touch_controls.gd +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -12,6 +12,7 @@ var player_hook: PlayerHook @onready var interact_button: TouchScreenButton = %InteractButton @onready var repel_button: TouchScreenButton = %RepelButton +@onready var pause_button: TouchScreenButton = %PauseButton @onready var virtual_joystick: VirtualJoystick = %VirtualJoystick @@ -20,7 +21,7 @@ func _ready() -> void: # TouchScreenButton doesn't mark its touches as handled, so the game would # also get them. For example, tapping the interact button would throw the # grappling hook too. - for button: TouchScreenButton in [interact_button, repel_button]: + for button: TouchScreenButton in [interact_button, repel_button, pause_button]: button.pressed.connect(get_viewport().set_input_as_handled) button.released.connect(get_viewport().set_input_as_handled) Transitions.started.connect(update_visibility) diff --git a/scenes/ui_elements/touch_controls/touch_controls.tscn b/scenes/ui_elements/touch_controls/touch_controls.tscn index 3a7d7b9e07..f5578d21e4 100644 --- a/scenes/ui_elements/touch_controls/touch_controls.tscn +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -36,7 +36,8 @@ mouse_filter = 2 layout_mode = 1 mouse_filter = 2 -[node name="TouchScreenButton" type="TouchScreenButton" parent="Control/CenterContainer" unique_id=1825489206] +[node name="PauseButton" type="TouchScreenButton" parent="Control/CenterContainer" unique_id=1825489206] +unique_name_in_owner = true position = Vector2(16, 16) texture_normal = ExtResource("2_pt1mb") texture_pressed = ExtResource("3_qk0o5")