Files
Bullosseum/tests/touch_controls_test.gd
T
richard 7b6a141767 Reorganize the codebase into role-based folders (snake_case)
Move the 40-odd root-level scripts and scenes into a tree that groups by role,
per the approved roadmap (WS2/WS3): actors/{player,matador,bear}, systems/{combat,
fx,input,debug,render,crowd,config}, ui/, props/ (was tscn_s/). Rename the shell
scene.tscn -> levels/game_shell.tscn and every PascalCase scene to snake_case
(Player.tscn -> player.tscn, MainMenu.tscn -> main_menu.tscn, etc.) so the
case-insensitive web/macOS filesystems can't alias two files together.

Done as a scripted move: each file travels with its .uid; every quoted res:// path
across scenes, scripts, .tres, and project.godot's 9 autoloads is rewritten; the
.godot uid/class cache is rebuilt from scratch. Scenes still resolve by uid, so the
rewrite is belt-and-suspenders. Lint globs (run_tests.sh + both CI workflows) now
walk the new dirs; an orphan animation_tree.gd.uid and 7 pre-existing trailing-
whitespace lints (barrel/torch, never covered by the old root-only glob) are cleaned.

Full suite green on both sides. Asset consolidation (Assets/Animations/fonts ->
art/) is deferred — moving imported .glb/.png/.ttf is safer done in the editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 00:04:41 +03:00

124 lines
4.6 KiB
GDScript

extends SceneTree
## Guardrail for the on-screen joystick's self-healing. The reported bug is a stick that
## "disappears / goes non-responsive": a touchend dropped during a resize/orientation flip (or
## an app backgrounding) leaves _stick_active stuck true, so the ghost hint hides and no new
## stick can start — and the bull keeps coasting on the held move actions. touch_controls.gd
## now resets on relayout + focus-out, prunes orphaned touches, and lets the latest left-zone
## touch re-acquire the stick. This drives synthetic touch events and asserts the stick never
## stays stranded and never leaves movement held.
## Run: godot --headless --script res://tests/touch_controls_test.gd
const _MOVE := [&"move_forward", &"move_back", &"move_left", &"move_right"]
var _pass := 0
var _fail := 0
var _tc: CanvasLayer
func _init() -> void:
_run.call_deferred()
func _ok(c: bool, m: String) -> void:
if c:
_pass += 1
print(" PASS: ", m)
else:
_fail += 1
print(" FAIL: ", m)
func _press(index: int, pos: Vector2) -> void:
var e := InputEventScreenTouch.new()
e.index = index
e.position = pos
e.pressed = true
_tc._input(e)
func _release(index: int, pos: Vector2) -> void:
var e := InputEventScreenTouch.new()
e.index = index
e.position = pos
e.pressed = false
_tc._input(e)
func _drag(index: int, pos: Vector2) -> void:
var e := InputEventScreenDrag.new()
e.index = index
e.position = pos
_tc._input(e)
func _any_move_held() -> bool:
for a: StringName in _MOVE:
if Input.is_action_pressed(a):
return true
return false
# A point safely inside the left-hand joystick zone (independent of viewport size), plus the
# same point pushed a full radius "up" so the stick engages move_forward.
func _stick_origin() -> Vector2:
return _tc._stick_zone.position + _tc._stick_zone.size * Vector2(0.25, 0.5)
func _run() -> void:
var dp: Node = root.get_node("/root/DP")
var controls: Node = root.get_node("/root/Controls")
dp.call("set_value", "force_touch_controls", true)
_ok(controls.call("use_touch_ui"), "force flag makes use_touch_ui() true (touch UI live)")
# Headless boots a 64x64 root viewport, which collapses every UI rect on top of each other.
# Give it a real phone-ish landscape size so the joystick zone / buttons lay out sanely.
root.size = Vector2i(1152, 648)
_tc = (load("res://systems/input/touch_controls.gd") as Script).new()
root.add_child(_tc)
await process_frame # _ready builds _surface and runs the first _relayout
_tc._relayout() # ensure the layout reflects the size set above
var o := _stick_origin()
var up := o + Vector2(0.0, -_tc._stick_radius)
# (a) A resize mid-drag (dropped touchend) must not strand the stick or keep the bull moving.
_tc._reset_touch_state()
_press(0, o)
_drag(0, up)
_ok(_tc._stick_active and _any_move_held(), "drag engages the stick and holds movement")
_tc._relayout() # the resize/orientation flip that used to swallow the release
_ok(not _tc._stick_active, "a relayout mid-drag stands the stick down (not stranded)")
_ok(not _any_move_held(), "movement is released on relayout (bull doesn't coast)")
# (b) App backgrounded / focus lost — same self-heal via the notification hook.
_tc._reset_touch_state()
_press(0, o)
_drag(0, up)
_tc._notification(Node.NOTIFICATION_APPLICATION_FOCUS_OUT)
_ok(not _tc._stick_active and not _any_move_held(), "focus-out clears the stick and movement")
# (c) A stranded stick self-heals: with finger 0's release lost, a fresh finger 1 in the zone
# re-acquires the stick; finger 0's late release must NOT kill finger 1's stick.
_tc._reset_touch_state()
_press(0, o) # finger 0 owns the stick
_press(1, o + Vector2(20, 10)) # finger 1 lands while 0 is still (wrongly) held → takes over
_ok(_tc._stick_active and _tc._stick_index == 1, "latest left-zone touch re-acquires the stick")
_release(0, o) # the stranded finger finally reports up
_ok(_tc._stick_active and _tc._stick_index == 1, "a superseded finger's release doesn't end the stick")
_release(1, o)
_ok(not _tc._stick_active, "releasing the owning finger ends the stick")
# (d) Watchdog: a touch that vanishes from tracking (no release event at all) is pruned.
_tc._reset_touch_state()
_press(0, o)
_tc._touches.erase(0) # simulate the finger silently disappearing
_tc._process(0.016) # prune runs each frame
_ok(not _tc._stick_active and not _any_move_held(), "watchdog prunes a vanished touch and frees the stick")
_finish()
func _finish() -> void:
print("Results: %d passed, %d failed" % [_pass, _fail])
quit(1 if _fail > 0 else 0)