- project.godot: joypad events on the ten existing actions (left stick move, A jump, right trigger/RB fire, X reload, B/LB grenade, Start pause, Back fullscreen), four new look_* actions on the right stick (deadzone 0.2) and ui_accept/ui_cancel redefined with the pad buttons A/B while keeping Enter/KP Enter/Space/Esc: the 4.7.1 engine maps no pad button on those. - scripts/player.gd: gamepad_mode turns on by itself on the first pad input; _can_aim() replaces the "mouse captured" gate of shoot and grenade (the pad never captures the pointer, same trap already fixed for touch); _handle_gamepad_look() converts the right stick into equivalent relative mouse pixels (900 px/s at full deflection, m^1.8 response) and still goes through apply_look(); restart accepts A/X and the death text follows the device in use. - scripts/start_screen.gd: pad hint shown only when a controller is connected, refreshed on Input.joy_connection_changed. - tools/test_gamepad.gd: 33 checks on map structure, keyboard preservation, real event pipeline, gamepad_mode and analog look. - docs/GAMEPAD.md: mapping, code map, verification, known limit (kiosk name entry still needs a keyboard: on-screen keyboard is the next step). Verified: test_gamepad 33/33, test_fullscreen_shortcut 8/8, test_touch_controls 14/14 (--touch), test_grenade 24/24, test_boss 50/50, test_rebalance_branding 14/14, test_player_profile 30/30. test_start_screen keeps its 2 pre-existing headless failures (identical on HEAD).
197 lines
7.2 KiB
GDScript
197 lines
7.2 KiB
GDScript
extends SceneTree
|
|
## Verifica il supporto gamepad:
|
|
## - ogni azione di gioco ha almeno un binding joypad, con i tasti/gli assi attesi;
|
|
## - gli assi di movimento sono sullo stick sinistro, la visuale sullo stick destro;
|
|
## - ui_accept/ui_cancel accettano A/B del pad SENZA perdere Invio/Spazio/Esc;
|
|
## - un pad reale (device != -1) attiva le azioni mappate con device -1;
|
|
## - l'attivita' del pad accende player.gamepad_mode, il puntatore non viene
|
|
## catturato e la visuale gira con lo stick destro passando da apply_look().
|
|
##
|
|
## Avviare con: --headless --path . --script res://tools/test_gamepad.gd
|
|
## Con un display reale: --rendering-method gl_compatibility
|
|
|
|
## azione -> eventi joypad attesi come stringhe leggibili (il formato del controllo)
|
|
const EXPECTED := {
|
|
"move_forward": "axis1-1.0",
|
|
"move_back": "axis1+1.0",
|
|
"move_left": "axis0-1.0",
|
|
"move_right": "axis0+1.0",
|
|
"jump": "btn0",
|
|
"reload": "btn2",
|
|
"pause": "btn6",
|
|
"toggle_fullscreen": "btn4",
|
|
"grenade": "btn1+btn9",
|
|
"shoot": "axis5+1.0+btn10",
|
|
"look_left": "axis2-1.0",
|
|
"look_right": "axis2+1.0",
|
|
"look_up": "axis3-1.0",
|
|
"look_down": "axis3+1.0",
|
|
}
|
|
|
|
var world: Node3D
|
|
var failures := 0
|
|
var checks := 0
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
func check(label: String, condition: bool, detail: String = "") -> void:
|
|
checks += 1
|
|
if condition:
|
|
print("PASS: " + label)
|
|
else:
|
|
failures += 1
|
|
print("FAIL: %s %s" % [label, detail])
|
|
|
|
## Rappresentazione compatta degli eventi joypad di un'azione, ordinata.
|
|
func _joypad_signature(action: String) -> String:
|
|
var parts: Array[String] = []
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventJoypadButton:
|
|
parts.append("btn%d" % event.button_index)
|
|
elif event is InputEventJoypadMotion:
|
|
parts.append("axis%d%+.1f" % [event.axis, event.axis_value])
|
|
parts.sort()
|
|
return "+".join(parts)
|
|
|
|
func _has_key(action: String, keycode: int) -> bool:
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventKey and (event.keycode == keycode or event.physical_keycode == keycode):
|
|
return true
|
|
return false
|
|
|
|
func _joypad_devices(action: String) -> Array[int]:
|
|
var devices: Array[int] = []
|
|
for event in InputMap.action_get_events(action):
|
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
|
devices.append(event.device)
|
|
return devices
|
|
|
|
## Pressione/rilascio di un pulsante del pad, come un controller vero.
|
|
func _pad_button(index: int, pressed: bool, device := 0) -> void:
|
|
var event := InputEventJoypadButton.new()
|
|
event.device = device
|
|
event.button_index = index
|
|
event.pressed = pressed
|
|
Input.parse_input_event(event)
|
|
Input.flush_buffered_events()
|
|
|
|
## Movimento di un asse del pad (valore fra -1 e 1).
|
|
func _pad_axis(axis: int, value: float, device := 0) -> void:
|
|
var event := InputEventJoypadMotion.new()
|
|
event.device = device
|
|
event.axis = axis
|
|
event.axis_value = value
|
|
Input.parse_input_event(event)
|
|
Input.flush_buffered_events()
|
|
|
|
func _frames(count: int) -> void:
|
|
for i in count:
|
|
await process_frame
|
|
|
|
func _steps(count: int) -> void:
|
|
for i in count:
|
|
await physics_frame
|
|
|
|
func run_test() -> void:
|
|
# --- mappa dei comandi ---
|
|
for action: String in EXPECTED:
|
|
check("\"%s\" ha i binding joypad attesi" % action,
|
|
_joypad_signature(action) == EXPECTED[action],
|
|
"trovato \"%s\" atteso \"%s\"" % [_joypad_signature(action), EXPECTED[action]])
|
|
|
|
var all_any := true
|
|
var offenders: Array[String] = []
|
|
for action: String in EXPECTED:
|
|
for device in _joypad_devices(action):
|
|
if device != -1:
|
|
all_any = false
|
|
offenders.append("%s(device %d)" % [action, device])
|
|
check("i binding joypad accettano qualunque pad (device -1)", all_any, str(offenders))
|
|
|
|
# ui_*: il motore 4.7.1 non mappa alcun pulsante su ui_accept/ui_cancel, quindi
|
|
# vanno aggiunti a mano tenendo i tasti di tastiera gia' presenti.
|
|
check("ui_accept accetta il pulsante A del pad",
|
|
_joypad_signature("ui_accept") == "btn0", _joypad_signature("ui_accept"))
|
|
check("ui_accept conserva Invio", _has_key("ui_accept", KEY_ENTER))
|
|
check("ui_accept conserva Invio del tastierino", _has_key("ui_accept", KEY_KP_ENTER))
|
|
check("ui_accept conserva Spazio", _has_key("ui_accept", KEY_SPACE))
|
|
check("ui_cancel accetta il pulsante B del pad",
|
|
_joypad_signature("ui_cancel") == "btn1", _joypad_signature("ui_cancel"))
|
|
check("ui_cancel conserva Esc", _has_key("ui_cancel", KEY_ESCAPE))
|
|
|
|
# --- pipeline eventi: un pad qualsiasi (device 0) deve attivare le azioni ---
|
|
_pad_button(JOY_BUTTON_A, true)
|
|
await _frames(1)
|
|
check("A del pad attiva l'azione \"jump\"", Input.is_action_pressed("jump"))
|
|
check("A del pad attiva ui_accept", Input.is_action_pressed("ui_accept"))
|
|
_pad_button(JOY_BUTTON_A, false)
|
|
await _frames(1)
|
|
check("il rilascio di A spegne \"jump\"", not Input.is_action_pressed("jump"))
|
|
|
|
_pad_axis(JOY_AXIS_LEFT_Y, -1.0)
|
|
await _frames(1)
|
|
check("stick sinistro in alto attiva \"move_forward\"",
|
|
Input.is_action_pressed("move_forward"))
|
|
_pad_axis(JOY_AXIS_LEFT_Y, 0.0)
|
|
await _frames(1)
|
|
|
|
_pad_axis(JOY_AXIS_RIGHT_X, 1.0)
|
|
await _frames(1)
|
|
check("stick destro a destra attiva \"look_right\"", Input.is_action_pressed("look_right"))
|
|
_pad_axis(JOY_AXIS_RIGHT_X, 0.0)
|
|
await _frames(1)
|
|
|
|
# --- mondo di gioco: gamepad_mode e visuale analogica ---
|
|
world = load("res://scenes/main.tscn").instantiate()
|
|
root.add_child(world)
|
|
await _frames(2)
|
|
var player: Node = world.player
|
|
var screen = world.start_screen
|
|
var gioca: Button = null
|
|
if screen != null:
|
|
for button in screen.find_children("*", "Button", true, false):
|
|
if button.text == "GIOCA":
|
|
gioca = button
|
|
if screen != null and screen.get("name_field") != null:
|
|
screen.name_field.text = "PadTest"
|
|
if gioca != null:
|
|
gioca.pressed.emit()
|
|
await _frames(2)
|
|
check("la partita e' iniziata", player.playing == true, "playing=%s" % str(player.playing))
|
|
check("senza attivita' di pad gamepad_mode resta falso", player.gamepad_mode == false)
|
|
|
|
_pad_button(JOY_BUTTON_A, true)
|
|
_pad_button(JOY_BUTTON_A, false)
|
|
await _frames(1)
|
|
check("un pulsante del pad accende player.gamepad_mode", player.gamepad_mode == true)
|
|
|
|
var yaw_before: float = player.rotation.y
|
|
_pad_axis(JOY_AXIS_RIGHT_X, 1.0)
|
|
await _steps(12)
|
|
_pad_axis(JOY_AXIS_RIGHT_X, 0.0)
|
|
await _steps(2)
|
|
check("lo stick destro fa ruotare la visuale verso destra",
|
|
player.rotation.y < yaw_before - 0.01,
|
|
"prima=%.3f dopo=%.3f" % [yaw_before, player.rotation.y])
|
|
|
|
var pitch_before: float = player.camera.rotation.x
|
|
_pad_axis(JOY_AXIS_RIGHT_Y, 1.0)
|
|
await _steps(8)
|
|
_pad_axis(JOY_AXIS_RIGHT_Y, 0.0)
|
|
await _steps(2)
|
|
check("lo stick destro in basso abbassa l'inclinazione della camera",
|
|
player.camera.rotation.x < pitch_before,
|
|
"prima=%.3f dopo=%.3f" % [pitch_before, player.camera.rotation.x])
|
|
check("l'inclinazione resta dentro i limiti di apply_look",
|
|
player.camera.rotation.x >= -1.4 - 0.001, "pitch=%.3f" % player.camera.rotation.x)
|
|
|
|
# Il pad non deve catturare il puntatore (in headless non e' verificabile):
|
|
# quello che conta e' che il gate di sparo/granata non dipenda dal mouse.
|
|
check("con gamepad_mode il gate di mira e' aperto anche senza mouse catturato",
|
|
player._can_aim() == true and player.touch_mode == false,
|
|
"can_aim=%s touch=%s" % [str(player._can_aim()), str(player.touch_mode)])
|
|
|
|
print("%d controlli superati, %d falliti" % [checks - failures, failures])
|
|
quit(1 if failures > 0 else 0)
|