extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_desktop_hides_touch_controls.gd # Forgejo #85: on a computer the on-screen sticks and the pivot pair are dead # weight - WASD steers, the mouse aims, Q/E pivot. # Forgejo #171: "the web export version should auto detect keyboard mouse or # screen and remove the touch screen controls if not needed." # # #85 asked DisplayServer.is_touchscreen_available() and, if it said yes, kept # the controls for ever. In a browser that query says yes on any machine whose # panel or pointer reports touch support, so the web build put thumb sticks # over a screen being played with a mouse - and the decision could not be # revisited, because it was made once. # # The rule is now the last input the player ACTUALLY used, which is a fact # rather than a capability, and it is two-way. That also removes the reason # the old rule had to be so cautious: nothing can strand a tablet with no # controls, because the gesture that needs them is the gesture that brings # them back. # # Covers: # A. they all start visible, so this is testing a change and not a default # B. a keyboard takes all four away, together # C. a touch brings all four back, together - on any device, whatever the # display claims # D. a mouse takes them away again, so the detection is not one-shot # E. the manufactured event is not believed: Godot makes a mouse click out # of every touch, and one arriving straight after a touch must not read # as "this player has a mouse" # F. the two consumers agree - the same reading gates mouse aiming, so the # sticks and the guns can never disagree about what is being held const CONTROLS: Array = ["UI/Joystick", "UI/FireStick", "UI/PivotCWButton", "UI/PivotCCWButton"] var game_state: Node var main: Node var controller: Node var mouse: Node var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: game_state = root.get_node("GameState") game_state.reset_progression() main = load("res://world/Main.tscn").instantiate() root.add_child(main) current_scene = main controller = main.get_node("GameController") if main.has_node("GameController") else main mouse = main.get_node_or_null("MouseCombatController") func _all_visible() -> bool: for path in CONTROLS: if not main.get_node(path).visible: return false return true func _any_visible() -> bool: for path in CONTROLS: if main.get_node(path).visible: return true return false # A touch far enough in the past that the grace window has expired. Ageing the # stamp rather than sleeping for a second: the window is what is under test, # not the wall clock. func _forget_touch() -> void: game_state.last_touch_msec = -1 func _process(delta: float) -> bool: elapsed += delta if stage != 0: return false if elapsed < 0.3: return false stage = 1 # --- A. everything starts on -------------------------------------- if not _all_visible(): return _fail("a touch control was already hidden before any input") print("all four touch controls start visible") # --- B. a keyboard takes them away -------------------------------- _forget_touch() controller._note_keyboard() if _any_visible(): return _fail("using a keyboard left a touch control on screen") if game_state.pointer_is_touch: return _fail("a keyboard did not change the reading to \"not touch\"") print("using a keyboard hides all four together") # --- C. and a touch brings them back ------------------------------ # The case the old one-shot rule could not do at all, and the reason the # new rule is safe to apply on every platform rather than only where the # display says there is no touchscreen. controller._note_touch() if not _all_visible(): return _fail("touching the screen did not bring the touch controls back") if not game_state.pointer_is_touch: return _fail("a touch did not change the reading to \"touch\"") print("a touch brings all four back") # --- E. the manufactured click is not believed -------------------- # Godot turns every touch into a click (emulate_mouse_from_touch), so the # click below is what a phone produces one frame after the touch above. # Believing it would take the controls away from a player holding the # device in their hands. controller._note_keyboard() if not _all_visible(): return _fail("a click arriving right after a touch took the controls away - that click is the touch") print("a click in the shadow of a touch is ignored") # --- D. a real mouse, later, does take them away ------------------ _forget_touch() controller._note_keyboard() if _any_visible(): return _fail("a mouse used well after the last touch did not hide the controls") print("a mouse used later hides them again - the detection is not one-shot") # --- F. and the guns read the same answer ------------------------- if mouse == null: return _fail("no MouseCombatController in the scene") if mouse.pointer_mode != mouse.PointerMode.AUTO: return _fail("the scene ships with an overridden pointer mode, so what a real device does is not what was tested") if not mouse.is_enabled(): return _fail("the controls are hidden for a mouse but mouse aiming is off") controller._note_touch() if mouse.is_enabled(): return _fail("a touch put the thumb controls back but left mouse firing armed - a tap would fire the guns") print("mouse aiming follows the same reading the controls do") print("TEST_DESKTOP_HIDES_TOUCH_CONTROLS: PASS") quit(0) return true func _fail(message: String) -> bool: print("TEST_DESKTOP_HIDES_TOUCH_CONTROLS: FAIL (%s)" % message) quit(1) return true