Add Alt+F to toggle fullscreen and window mode

New remappable action `toggle_fullscreen` (Alt+F) handled in main.gd::_input(),
so it works in the menu, during a match with the pointer captured and in the boss
sandbox; the event is marked handled so the key does not reach the rest of the game.

toggle_fullscreen() switches between WINDOW_MODE_FULLSCREEN and WINDOW_MODE_WINDOWED;
the state lives in main.fullscreen (initialised from the real window mode) instead of
querying the driver on every press, so the toggle is deterministic and testable. On
Web it is a no-op because fullscreen belongs to the page.

Desktop HUD hint updated. New tools/test_fullscreen_shortcut.gd: 8 checks (action and
key mapping, alternation, bare F inert, works mid-match without freeing the cursor).
Verified on a real display: window_get_mode 0 -> 3 -> 0.
This commit is contained in:
enne2
2026-10-24 12:55:15 +02:00
parent cb14fd5400
commit 4af402f8bf
6 changed files with 138 additions and 1 deletions
+16
View File
@@ -51,9 +51,24 @@ sulla schermata iniziale e i pulsanti non erano cliccabili.
| `scripts/start_screen.gd` | campo del nome sempre mostrato e vuoto, `reset_for_new_player()`, `GIOCA` bloccato a campo vuoto | | `scripts/start_screen.gd` | campo del nome sempre mostrato e vuoto, `reset_for_new_player()`, `GIOCA` bloccato a campo vuoto |
| `scripts/main.gd` | `start_game()` deriva id e record dal nome e chiama `player.begin_play()`; `_on_player_died()` sblocca il profilo e invia il nome della partita conclusa | | `scripts/main.gd` | `start_game()` deriva id e record dal nome e chiama `player.begin_play()`; `_on_player_died()` sblocca il profilo e invia il nome della partita conclusa |
| `scripts/player.gd` | cursore visibile al menu, `begin_play()` con la cattura, flag `playing` | | `scripts/player.gd` | cursore visibile al menu, `begin_play()` con la cattura, flag `playing` |
| `scripts/game_hud.gd` | promemoria dei comandi desktop aggiornato con `Alt+F` |
| `tools/test_player_profile.gd` | 30 controlli (campo libero, id derivato, record per nome, game over) | | `tools/test_player_profile.gd` | 30 controlli (campo libero, id derivato, record per nome, game over) |
| `tools/test_start_screen.gd` | 15 controlli (campo sempre richiesto, partenza bloccata a campo vuoto, campo liberato dopo il game over, cursore libero al menu e catturato solo in partita) | | `tools/test_start_screen.gd` | 15 controlli (campo sempre richiesto, partenza bloccata a campo vuoto, campo liberato dopo il game over, cursore libero al menu e catturato solo in partita) |
## 1c. Scorciatoia Alt+F (finestra / schermo intero)
* Azione in `project.godot`: `toggle_fullscreen` = `Alt+F` (rimappabile dal pannello
Input Map come le altre).
* Gestita in `main.gd::_input()`, quindi vale **sempre**: menu, partita (anche con il
cursore catturato) e sandbox del boss. L'evento viene marcato come gestito, così il
tasto non arriva al resto del gioco.
* `main.gd::toggle_fullscreen()` alterna `WINDOW_MODE_FULLSCREEN` e
`WINDOW_MODE_WINDOWED`; lo stato vive in `main.fullscreen`, inizializzato dalla
modalità reale della finestra. Nel browser non fa nulla (il fullscreen è della pagina).
* Verificato su display reale: `window_get_mode` 0 (finestra) → 3 (schermo intero) → 0.
* `tools/test_fullscreen_shortcut.gd`: 8 controlli (azione e tasto mappati, alternanza,
F da solo inerte, funzionamento in partita senza liberare il cursore).
## 2. Verifiche eseguite ## 2. Verifiche eseguite
```bash ```bash
@@ -66,6 +81,7 @@ G="$HOME/.local/opt/godot-4.7.1/Godot_v4.7.1-stable_linux.x86_64"
"$G" --headless --path . --script res://tools/test_spawn_windows.gd "$G" --headless --path . --script res://tools/test_spawn_windows.gd
"$G" --headless --path . --script res://tools/test_wave_growth.gd "$G" --headless --path . --script res://tools/test_wave_growth.gd
"$G" --headless --path . --script res://tools/test_gameplay_improvements.gd "$G" --headless --path . --script res://tools/test_gameplay_improvements.gd
"$G" --headless --path . --script res://tools/test_fullscreen_shortcut.gd # 8/8 PASS
python3 tools/test_scores_api.py # 42/42 python3 tools/test_scores_api.py # 42/42
``` ```
+6
View File
@@ -83,6 +83,12 @@ pause={
] ]
} }
toggle_fullscreen={
"deadzone": 0.5,
"events": [Object(InputEventKey, "resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
grenade={ grenade={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventMouseButton, "button_index":2)] "events": [Object(InputEventMouseButton, "button_index":2)]
+1 -1
View File
@@ -40,7 +40,7 @@ func _ready() -> void:
grenade_value.offset_right = 160 grenade_value.offset_right = 160
grenade_value.offset_top = 44 grenade_value.offset_top = 44
grenade_value.offset_bottom = 66 grenade_value.offset_bottom = 66
help = _label(root, "WASD: muovi | mouse: mira | R: ricarica | destro: granata", 15, Vector2.ZERO) help = _label(root, "WASD: muovi | mouse: mira | R: ricarica | destro: granata | Alt+F: schermo intero", 15, Vector2.ZERO)
help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
help.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM) help.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
help.offset_left = -420 help.offset_left = -420
+20
View File
@@ -36,9 +36,13 @@ var simulation_finished := false
var simulation_restarting := false var simulation_restarting := false
var simulation_label: Label var simulation_label: Label
var simulation_retry: Button var simulation_retry: Button
## Stato dello schermo intero, tenuto qui e non letto dal driver a ogni pressione:
## cosi' Alt+F alterna davvero e il comportamento e' verificabile anche in headless.
var fullscreen := false
func _ready() -> void: func _ready() -> void:
smoke = "--smoke" in OS.get_cmdline_user_args() smoke = "--smoke" in OS.get_cmdline_user_args()
fullscreen = DisplayServer.window_get_mode() != DisplayServer.WINDOW_MODE_WINDOWED
if not _configure_simulation(): if not _configure_simulation():
return return
# A simulation must never read/write the real player's profile or public scores. # A simulation must never read/write the real player's profile or public scores.
@@ -412,11 +416,27 @@ func _setup_simulation_ui() -> void:
canvas.add_child(simulation_retry) canvas.add_child(simulation_retry)
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
# Alt+F: finestra <-> schermo intero. Vale sempre (menu, partita, simulazione),
# anche a cursore catturato, perche' _input arriva prima della UI.
if event.is_action_pressed("toggle_fullscreen"):
get_viewport().set_input_as_handled()
toggle_fullscreen()
return
# Only the completed native simulation has this shortcut. No menu click-to-start. # Only the completed native simulation has this shortcut. No menu click-to-start.
if boss_simulation and simulation_finished and event.is_action_pressed("reload"): if boss_simulation and simulation_finished and event.is_action_pressed("reload"):
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
_restart_simulation() _restart_simulation()
## Alterna finestra e schermo intero. Restituisce il nuovo stato (true = schermo intero).
## Nel browser il fullscreen e' della pagina, non della finestra Godot: li' non si tocca.
func toggle_fullscreen() -> bool:
if OS.has_feature("web"):
return fullscreen
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED if fullscreen
else DisplayServer.WINDOW_MODE_FULLSCREEN)
fullscreen = not fullscreen
return fullscreen
func _restart_simulation() -> void: func _restart_simulation() -> void:
if simulation_restarting: if simulation_restarting:
return return
+94
View File
@@ -0,0 +1,94 @@
extends SceneTree
## Verifica la scorciatoia Alt+F (finestra <-> schermo intero):
## - l'azione "toggle_fullscreen" esiste nella mappa dei comandi ed e' Alt+F;
## - ogni pressione alterna lo stato, anche a partita avviata e a cursore catturato;
## - l'evento viene marcato come gestito (il tasto non arriva al gioco);
## - Alt+F senza Alt (o F da solo) NON cambia nulla.
## Avviare con: --headless --path . --script res://tools/test_fullscreen_shortcut.gd
## Con un display reale si usa: --rendering-method gl_compatibility
const ACTIONS := ["toggle_fullscreen"]
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])
func run_test() -> void:
# --- mappa dei comandi ---
check("l'azione toggle_fullscreen esiste nella mappa", InputMap.has_action("toggle_fullscreen"))
var alt_f := false
for event in InputMap.action_get_events("toggle_fullscreen"):
if event is InputEventKey and event.alt_pressed \
and (event.physical_keycode == KEY_F or event.keycode == KEY_F):
alt_f = true
check("l'azione corrisponde ad Alt+F", alt_f, str(InputMap.action_get_events("toggle_fullscreen")))
world = load("res://scenes/main.tscn").instantiate()
root.add_child(world)
await process_frame
await process_frame
var start_state: bool = world.fullscreen
_press_f(true)
await process_frame
check("Alt+F capovolge lo stato dello schermo intero",
world.fullscreen != start_state, "prima=%s dopo=%s" % [str(start_state), str(world.fullscreen)])
_press_f(true)
await process_frame
check("una seconda pressione riporta allo stato iniziale",
world.fullscreen == start_state, "dopo=%s atteso=%s" % [str(world.fullscreen), str(start_state)])
# F senza Alt non deve cambiare nulla
var before: bool = world.fullscreen
_press_f(false)
await process_frame
check("F da solo (senza Alt) non cambia lo schermo", world.fullscreen == before,
"prima=%s dopo=%s" % [str(before), str(world.fullscreen)])
# --- in partita, a cursore catturato ---
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 = "AltF"
if gioca != null:
gioca.pressed.emit()
await process_frame
await process_frame
check("la partita e' iniziata (il cursore e' catturato o headless)",
world.player.playing == true, "playing=%s" % str(world.player.playing))
var during_play: bool = world.fullscreen
_press_f(true)
await process_frame
check("Alt+F funziona anche in partita, a cursore catturato",
world.fullscreen != during_play, "prima=%s dopo=%s" % [str(during_play), str(world.fullscreen)])
check("Alt+F non interrompe la partita ne' libera il cursore",
world.player.playing == true and world.player.death_started == false,
"playing=%s morte=%s" % [str(world.player.playing), str(world.player.death_started)])
print("%d controlli superati, %d falliti" % [checks - failures, failures])
quit(1 if failures > 0 else 0)
## Pressione + rilascio di F, con o senza Alt: stesso percorso di un tasto reale.
func _press_f(alt: bool) -> void:
for pressed in [true, false]:
var event := InputEventKey.new()
event.physical_keycode = KEY_F
event.keycode = KEY_F
event.alt_pressed = alt
event.pressed = pressed
Input.parse_input_event(event)
Input.flush_buffered_events()
+1
View File
@@ -0,0 +1 @@
uid://bdaw8q5cc5owc