Classifica globale: API dei punteggi, schermata iniziale e nome memorizzato
- services/scores-api: microservizio (solo stdlib) con SQLite WAL, idempotenza per submission_id, limiti di frequenza e validazione; immagine Docker non-root. - nginx: reverse proxy su /api/zombie-fps/ (stessa origine del gioco, niente CORS). - scripts/leaderboard.gd: client HTTP con retry idempotente; su desktop la classifica resta disattivata senza --api=, cosi' i test non sporcano la classifica pubblica. - scripts/player_profile.gd: nome, id anonimo e miglior punteggio in user:// e in window.localStorage; il nome scelto non viene piu' richiesto; riparazione dei profili con la stringa "null". - scripts/start_screen.gd: menu con nome, GIOCA e classifica; la partita parte solo dal pulsante (via la rete di sicurezza sull'input, Invio nel campo non avvia). - scripts/main.gd, player.gd: avvio esplicito, segnale died, nota dell'invio. - tools: test_scores_api.py (42), test_leaderboard (14), test_player_profile (21), test_start_screen (10); suite esistenti adeguate all'avvio manuale. - docs/HIGHSCORES.md + docs/WEB-DEPLOY.md aggiornati.
This commit is contained in:
+74
-32
@@ -2,6 +2,9 @@ extends Node3D
|
||||
const PlayerScript = preload("res://scripts/player.gd")
|
||||
const ZombieScript = preload("res://scripts/zombie.gd")
|
||||
const RoomScript = preload("res://scripts/room.gd")
|
||||
const LeaderboardScript = preload("res://scripts/leaderboard.gd")
|
||||
const StartScreenScript = preload("res://scripts/start_screen.gd")
|
||||
const Profile = preload("res://scripts/player_profile.gd")
|
||||
const MUSIC_PATH := "res://assets/audio/music/zombie_industrial_loop.ogg"
|
||||
const MUSIC_VOLUME_DB := -11.0
|
||||
const TouchControls = preload("res://scripts/touch_controls.gd")
|
||||
@@ -18,12 +21,15 @@ var alive := 0
|
||||
var spawning := false
|
||||
var wave_pending := false
|
||||
var smoke := false
|
||||
var awaiting_web_start := false
|
||||
var start_label: Label
|
||||
var last_window := -1
|
||||
var leaderboard: Node
|
||||
var start_screen: CanvasLayer
|
||||
var profile := {}
|
||||
var score_submitted := false
|
||||
|
||||
func _ready() -> void:
|
||||
smoke = "--smoke" in OS.get_cmdline_user_args()
|
||||
profile = Profile.load_profile()
|
||||
room = Node3D.new()
|
||||
room.set_script(RoomScript)
|
||||
add_child(room)
|
||||
@@ -74,44 +80,80 @@ func _ready() -> void:
|
||||
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
_setup_music()
|
||||
_setup_touch()
|
||||
leaderboard = Node.new()
|
||||
leaderboard.set_script(LeaderboardScript)
|
||||
leaderboard.player_id = profile["player_id"]
|
||||
add_child(leaderboard)
|
||||
leaderboard.leaderboard_ready.connect(_on_leaderboard_ready)
|
||||
leaderboard.leaderboard_failed.connect(_on_leaderboard_failed)
|
||||
leaderboard.score_submitted.connect(_on_score_submitted)
|
||||
leaderboard.submit_failed.connect(_on_submit_failed)
|
||||
player.died.connect(_on_player_died)
|
||||
start_screen = CanvasLayer.new()
|
||||
start_screen.set_script(StartScreenScript)
|
||||
start_screen.player = player
|
||||
start_screen.leaderboard = leaderboard
|
||||
start_screen.profile = profile
|
||||
add_child(start_screen)
|
||||
start_screen.started.connect(_on_start_pressed)
|
||||
if OS.has_feature("web"):
|
||||
awaiting_web_start = true
|
||||
if touch != null:
|
||||
touch.enabled = false # il primo tocco/click avvia la partita
|
||||
start_label = Label.new()
|
||||
var howto := "trascina a sinistra: muovi · destra: guarda · pulsanti: fuoco/granata/ricarica/salto" if player.touch_mode else "WASD: movimento · clic sinistro: spara · destro: granata · R: ricarica"
|
||||
start_label.text = ("TOCCA PER INIZIARE" if player.touch_mode else "CLICCA PER INIZIARE") + "\n" + howto
|
||||
start_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
start_label.add_theme_font_size_override("font_size", 24)
|
||||
start_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
canvas.add_child(start_label)
|
||||
start_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
else:
|
||||
_start_music()
|
||||
_next_wave()
|
||||
touch.enabled = false # i tocchi servono alla schermata iniziale
|
||||
if smoke:
|
||||
get_tree().create_timer(12).timeout.connect(func():
|
||||
print("SMOKE: wave=", wave, " alive=", alive, " player_hp=", player.hp)
|
||||
get_tree().quit())
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not awaiting_web_start:
|
||||
## Il pulsante GIOCA della schermata iniziale delega qui: sblocca l'audio (policy dei
|
||||
## browser: serve un gesto dell'utente) e avvia la prima ondata. I test headless
|
||||
## chiamano la stessa funzione con save_profile=false per non toccare il profilo reale.
|
||||
func _on_start_pressed(player_name: String) -> void:
|
||||
start_game(player_name, true)
|
||||
|
||||
func start_game(player_name: String, save_profile := true) -> void:
|
||||
profile["name"] = player_name
|
||||
if save_profile:
|
||||
Profile.save_profile(profile)
|
||||
if not player.touch_mode:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
if touch != null:
|
||||
touch.enabled = true
|
||||
if start_screen != null:
|
||||
start_screen.queue_free()
|
||||
start_screen = null
|
||||
_start_music()
|
||||
_next_wave()
|
||||
|
||||
## Alla morte si invia UNA volta il punteggio finale con nome e id del giocatore.
|
||||
func _on_player_died() -> void:
|
||||
if score_submitted:
|
||||
return
|
||||
var started := false
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
started = true
|
||||
elif event is InputEventScreenTouch and event.pressed:
|
||||
started = true # primo tocco su mobile
|
||||
if started:
|
||||
awaiting_web_start = false
|
||||
start_label.queue_free()
|
||||
if not player.touch_mode:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
if touch != null:
|
||||
touch.enabled = true
|
||||
# Il browser concede l'audio solo dopo un gesto dell'utente: qui il click/tocco.
|
||||
_start_music()
|
||||
_next_wave()
|
||||
score_submitted = true
|
||||
if int(score) > int(profile.get("best", 0)):
|
||||
profile["best"] = score
|
||||
Profile.save_profile(profile)
|
||||
player.set_death_note("invio del punteggio in corso...")
|
||||
leaderboard.submit(profile["name"], score)
|
||||
|
||||
func _on_score_submitted(info: Dictionary) -> void:
|
||||
var total := int(info.get("total", 0))
|
||||
player.set_death_note("PUNTEGGIO %d — %d° su %d partite" % [score, int(info.get("rank", 0)), total])
|
||||
leaderboard.fetch()
|
||||
|
||||
func _on_submit_failed(reason: String) -> void:
|
||||
player.set_death_note("punteggio non inviato (%s)" % reason)
|
||||
|
||||
func _on_leaderboard_ready(rows: Array, total: int, me: Dictionary) -> void:
|
||||
if start_screen != null:
|
||||
start_screen.show_leaderboard(rows, total, me)
|
||||
|
||||
func _on_leaderboard_failed(reason: String) -> void:
|
||||
if start_screen != null:
|
||||
start_screen.show_leaderboard_error(reason)
|
||||
|
||||
## Nota: qui non c'e' piu' nessuna rete di sicurezza sull'input. La partita parte
|
||||
## solo dal pulsante GIOCA della schermata iniziale (segnale `started`): un click o un
|
||||
## tocco sullo sfondo del menu non deve avviare nulla.
|
||||
|
||||
## Controlli on-screen: attivi su browser mobile o touchscreen, forzabili con
|
||||
## --touch / --no-touch sulla riga di comando e con ?touch=1 / ?touch=0 sul Web.
|
||||
|
||||
Reference in New Issue
Block a user