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)