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/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..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,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 +## dragging the finger keeps aiming. Lifting the finger throws. +## [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,12 @@ 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 + +## 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. @@ -93,12 +102,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: - 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 + # Ignore the mouse motion that Godot emulates from touch, for example + # when dragging the virtual joystick. + if _event.device == InputEvent.DEVICE_ID_EMULATION: + return + _aim_at(get_global_mouse_position()) return # When aiming with keyboard, do not change the hook angle immediately if one of these actions @@ -132,6 +145,36 @@ func _unhandled_input(_event: InputEvent) -> void: return +## 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: + _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)) + elif event.index == _touch_index: + _touch_index = -1 + _aim_at(_screen_to_global(event.position)) + _touch_throw_requested = true + + +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") @@ -218,6 +261,9 @@ 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 + _touch_throw_requested = false clear_pointing_at_area() State.AIMING_PAUSED: clear_pointing_at_area() @@ -236,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() diff --git a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd index c85fdaeba9..a196dddf47 100644 --- a/scenes/game_logic/walk_behaviors/input_walk_behavior.gd +++ b/scenes/game_logic/walk_behaviors/input_walk_behavior.gd @@ -52,13 +52,17 @@ 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 + # 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() > 0.99 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/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() 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/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() 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 deleted file mode 100644 index 954f21f08c..0000000000 --- a/scenes/menus/title/components/touchscreen_warning.tscn +++ /dev/null @@ -1,43 +0,0 @@ -[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"] - -[node name="TouchscreenWarning" type="Control" unique_id=2046863085] -layout_mode = 3 -anchors_preset = 0 -script = ExtResource("1_smfbs") -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/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"] 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..fac1e3efe7 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,16 +22,14 @@ 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) 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 @@ -86,15 +83,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..f7c5cc91af --- /dev/null +++ b/scenes/ui_elements/touch_controls/components/touch_controls.gd @@ -0,0 +1,89 @@ +# 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 + +var player_interaction: PlayerInteraction +var player_repel: PlayerRepel +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 + + +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, 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) + Transitions.finished.connect(update_visibility) + + +func _notification(what: int) -> void: + match what: + NOTIFICATION_PAUSED, NOTIFICATION_UNPAUSED: + 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() + and not get_tree().paused + and not Transitions.is_running() + 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 and not interact_button.visible 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..f5578d21e4 --- /dev/null +++ b/scenes/ui_elements/touch_controls/touch_controls.tscn @@ -0,0 +1,99 @@ +[gd_scene format=3 uid="uid://ba3fvp0yravi4"] + +[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://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) + +[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 +mouse_filter = 2 +script = ExtResource("1_mjavl") +metadata/_edit_horizontal_guides_ = [593.0] + +[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 +mouse_filter = 2 + +[node name="CenterContainer" type="BoxContainer" parent="Control" unique_id=759928299] +layout_mode = 1 +mouse_filter = 2 + +[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") +shape = SubResource("RectangleShape2D_fhnvg") +action = "pause" + +[node name="BottomLeftContainer" type="BoxContainer" parent="." unique_id=1803916095] +custom_minimum_size = Vector2(250, 250) +layout_mode = 1 +anchors_preset = 2 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_top = -250.0 +offset_right = 250.0 +grow_vertical = 0 + +[node name="VirtualJoystick" type="VirtualJoystick" parent="BottomLeftContainer" unique_id=1528042620] +unique_name_in_owner = true +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="BottomRightContainer" type="Control" parent="." unique_id=1744849055] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -216.0 +offset_top = -175.0 +offset_right = -216.0 +offset_bottom = -175.0 +grow_horizontal = 0 +grow_vertical = 0 +mouse_filter = 2 + +[node name="RepelButton" type="TouchScreenButton" parent="BottomRightContainer" unique_id=675930575] +unique_name_in_owner = true +position = Vector2(30, -16) +texture_normal = ExtResource("4_pk8s8") +texture_pressed = ExtResource("5_dtb3f") +shape = SubResource("RectangleShape2D_fhnvg") +action = "repel" + +[node name="InteractButton" type="TouchScreenButton" parent="BottomRightContainer" unique_id=1674418296] +unique_name_in_owner = true +position = Vector2(30, -16) +texture_normal = ExtResource("6_pt1mb") +texture_pressed = ExtResource("7_qk0o5") +shape = SubResource("RectangleShape2D_fhnvg") +action = "interact" diff --git a/web/full-size.html b/web/full-size.html index 7c9b24b02d..a47ddfe070 100644 --- a/web/full-size.html +++ b/web/full-size.html @@ -174,11 +174,18 @@ 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. + // 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);