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:
@@ -0,0 +1,161 @@
|
||||
extends SceneTree
|
||||
## Verifica le regole della schermata iniziale:
|
||||
## - la partita parte SOLO dal pulsante GIOCA: click o tocco sullo sfondo, oppure
|
||||
## Invio/"Vai" nel campo del nome, non devono avviare nulla;
|
||||
## - il nome scelto una volta non viene piu' richiesto.
|
||||
## Avviare con: --headless --path . --script res://tools/test_start_screen.gd
|
||||
## (con display, per gli eventi di puntamento: --rendering-method gl_compatibility)
|
||||
const Profile = preload("res://scripts/player_profile.gd")
|
||||
const StartScreen = preload("res://scripts/start_screen.gd")
|
||||
|
||||
var world: Node3D
|
||||
var screen: CanvasLayer
|
||||
var failures := 0
|
||||
var checks := 0
|
||||
var emitted := ""
|
||||
var start_events := 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:
|
||||
await test_world_menu()
|
||||
await test_locked_name()
|
||||
await test_unlocked_name()
|
||||
print("%d controlli superati, %d falliti" % [checks - failures, failures])
|
||||
quit(1 if failures > 0 else 0)
|
||||
|
||||
## --- il menu del gioco vero ---------------------------------------------------
|
||||
|
||||
func test_world_menu() -> void:
|
||||
world = load("res://scenes/main.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
screen = world.start_screen
|
||||
screen.started.connect(_on_started)
|
||||
|
||||
var gioca: Button = null
|
||||
for button in screen.find_children("*", "Button", true, false):
|
||||
if button.text == "GIOCA":
|
||||
gioca = button
|
||||
var backdrop: ColorRect = screen.find_children("*", "ColorRect", true, false)[0]
|
||||
var outside := Vector2(40.0, root.size.y - 80.0)
|
||||
var button_center: Vector2 = gioca.get_global_rect().get_center()
|
||||
check("il pulsante GIOCA c'e' e lo sfondo usato nel test e' fuori dai controlli",
|
||||
gioca != null and not gioca.get_global_rect().has_point(outside),
|
||||
"pulsante=%s" % str(gioca))
|
||||
|
||||
_click(outside)
|
||||
await process_frame
|
||||
await process_frame
|
||||
check("il click sullo sfondo non avvia la partita", start_events == 0 and world.start_screen != null,
|
||||
"avvii=%d" % start_events)
|
||||
|
||||
_touch(outside)
|
||||
await process_frame
|
||||
await process_frame
|
||||
check("il tocco sullo sfondo non avvia la partita", start_events == 0 and world.start_screen != null,
|
||||
"avvii=%d" % start_events)
|
||||
|
||||
if screen.name_field != null:
|
||||
screen.name_field.text = "Prova"
|
||||
screen.name_field.text_submitted.emit("Prova")
|
||||
await process_frame
|
||||
await process_frame
|
||||
check("l'Invio nel campo del nome non avvia la partita",
|
||||
start_events == 0 and world.start_screen != null, "avvii=%d" % start_events)
|
||||
else:
|
||||
check("l'Invio nel campo del nome non avvia la partita", true, "(nome gia' bloccato)")
|
||||
|
||||
_click(button_center)
|
||||
await process_frame
|
||||
await process_frame
|
||||
check("il pulsante GIOCA avvia la partita", start_events == 1 and world.start_screen == null,
|
||||
"avvii=%d schermata=%s" % [start_events, "presente" if world.start_screen != null else "via"])
|
||||
# Regressione: senza --api= questa e' una build desktop di prova e non deve poter
|
||||
# inviare punteggi alla classifica pubblica (ci sono finite righe "Test").
|
||||
check("senza --api= la build di prova non invia punteggi alla classifica pubblica",
|
||||
world.leaderboard.disabled == true and world.leaderboard.base_url == "",
|
||||
"base=%s disattivata=%s" % [str(world.leaderboard.base_url), str(world.leaderboard.disabled)])
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
|
||||
func _on_started(player_name: String) -> void:
|
||||
emitted = player_name
|
||||
start_events += 1
|
||||
|
||||
## --- nome bloccato e non ------------------------------------------------------
|
||||
|
||||
func menu_with(profile: Dictionary) -> CanvasLayer:
|
||||
var layer := CanvasLayer.new()
|
||||
layer.set_script(StartScreen)
|
||||
layer.set("profile", profile)
|
||||
root.add_child(layer)
|
||||
layer.started.connect(_on_started)
|
||||
return layer
|
||||
|
||||
func test_locked_name() -> void:
|
||||
emitted = ""
|
||||
start_events = 0
|
||||
var menu := menu_with({"name": "Mat", "player_id": "abcdef0123456789", "best": 300, "locked": true})
|
||||
await process_frame
|
||||
var label_text := ""
|
||||
for label in menu.find_children("*", "Label", true, false):
|
||||
if str(label.text).begins_with("giocatore:"):
|
||||
label_text = str(label.text)
|
||||
check("con nome scelto il campo non viene richiesto",
|
||||
menu.get("name_field") == null and menu.get("name_locked") == true,
|
||||
"campo=%s bloccato=%s" % [str(menu.get("name_field")), str(menu.get("name_locked"))])
|
||||
check("il nome scelto e' mostrato al giocatore", label_text == "giocatore: Mat", label_text)
|
||||
var gioca: Button = null
|
||||
for button in menu.find_children("*", "Button", true, false):
|
||||
if button.text == "GIOCA":
|
||||
gioca = button
|
||||
if gioca != null:
|
||||
gioca.pressed.emit()
|
||||
check("la partita usa il nome memorizzato", emitted == "Mat", "nome=%s" % emitted)
|
||||
menu.queue_free()
|
||||
await process_frame
|
||||
|
||||
func test_unlocked_name() -> void:
|
||||
emitted = ""
|
||||
var menu := menu_with({"name": Profile.DEFAULT_NAME, "player_id": "abcdef0123456789", "best": 0})
|
||||
await process_frame
|
||||
var field: LineEdit = menu.get("name_field")
|
||||
check("senza nome scelto il campo e' presente e usabile",
|
||||
field != null and field.editable and field.max_length == Profile.NAME_MAX,
|
||||
"campo=%s" % str(field))
|
||||
menu.queue_free()
|
||||
await process_frame
|
||||
|
||||
## --- eventi sintetici --------------------------------------------------------
|
||||
|
||||
func _click(point: Vector2) -> void:
|
||||
var motion := InputEventMouseMotion.new()
|
||||
motion.position = point
|
||||
motion.global_position = point
|
||||
Input.parse_input_event(motion)
|
||||
for pressed in [true, false]:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = MOUSE_BUTTON_LEFT
|
||||
event.pressed = pressed
|
||||
event.position = point
|
||||
event.global_position = point
|
||||
Input.parse_input_event(event)
|
||||
|
||||
func _touch(point: Vector2) -> void:
|
||||
for pressed in [true, false]:
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.index = 0
|
||||
event.pressed = pressed
|
||||
event.position = point
|
||||
Input.parse_input_event(event)
|
||||
Reference in New Issue
Block a user