Add in-game leaderboard screen

This commit is contained in:
Emporex committed 2026-10-04 09:08:04 +02:00
1 parent 31ddba0d45
commit 5a3b7b6e0c
4 files changed
+94 -2

No files matched your search

+2
View File
@@ -22,6 +22,8 @@ Die Kennungen folgen diesem Muster: `mission_map0_difficulty0`.
Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`.
Im Hauptmenü ist eine Ranglistenansicht für die aktuell gewählte Karte und Schwierigkeit eingebaut. Sie zeigt Mission und Endlosmodus getrennt, löst aktuelle öffentliche Profilnamen auf und hebt den eigenen Eintrag hervor.
## Multiplayer
Die Godot-Anbindung öffnet nach der Anmeldung eine WSS/WS-Echtzeitverbindung. Die schnelle Spielersuche vermittelt exakt zwei passende Spieler. Alternativ erstellt ein Spieler einen privaten Raum mit einem sechsstelligen Code, dem der zweite Spieler gezielt beitritt.
+2
View File
@@ -109,6 +109,8 @@ Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar.
> **Online-Stand 0.23:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, servergeprüfte öffentliche Spielernamen, Kontolöschung im Spiel, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets, serverautoritativer Zwei-Spieler-Koop, private Räume per Raumcode, eine Bereit-Lobby und Wiederverbindung sind eingebaut. Ranglisten lösen den aktuellen öffentlichen Profilnamen über die unveränderliche Spieler-ID auf. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let's-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`.
Die Schaltfläche **Bestenliste** im Hauptmenü zeigt die zwölf besten Piloten für die aktuell gewählte Karte und Schwierigkeit. Zwischen Mission und Endlosmodus kann direkt in der Ansicht umgeschaltet werden; der eigene Eintrag wird farblich hervorgehoben.
- Turm unten auswählen und auf eine freie Plattformfläche klicken oder tippen.
- Am PC folgt der Reichweitenkreis vor dem Bau dem Mauszeiger. Auf Mobilgeräten markiert der erste Tipp den Bauplatz samt Reichweite; der zweite Tipp auf dasselbe Feld bestätigt den Bau.
- Nur die markierten Rasterfelder sind bebaubar; Türme rasten automatisch in der Feldmitte ein. Die sichtbare Straße wird exakt auf denselben Rasterzellen und derselben Mittellinie gezeichnet, denen auch die Gegner folgen.
+86 -2
View File
@@ -129,6 +129,15 @@ var tower_info_rect := Rect2(1135, 105, 440, 104)
var endless_button := Rect2(545, 515, 510, 70)
var finish_mission_button := Rect2(625, 610, 350, 62)
var profile_button := Rect2(1280, 25, 270, 52)
var leaderboard_button := Rect2(250, 705, 350, 60)
var leaderboard_back_button := Rect2(55, 35, 185, 54)
var leaderboard_mission_button := Rect2(510, 155, 270, 54)
var leaderboard_endless_button := Rect2(820, 155, 270, 54)
var leaderboard_refresh_button := Rect2(1265, 35, 280, 54)
var leaderboard_rows: Array = []
var leaderboard_endless := false
var leaderboard_loading := false
var leaderboard_message := ""
var profile_back_button := Rect2(55, 35, 185, 54)
var multiplayer_button := Rect2(1000, 705, 350, 60)
var multiplayer_back_button := Rect2(55, 35, 185, 54)
@@ -440,10 +449,25 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
return
if start_button.has_point(position):
_start_game()
elif leaderboard_button.has_point(position):
_open_leaderboard()
elif multiplayer_button.has_point(position):
game_mode = "multiplayer_menu"
queue_redraw()
return
if game_mode == "leaderboard":
if leaderboard_back_button.has_point(position):
game_mode = "menu"
elif leaderboard_mission_button.has_point(position) and leaderboard_endless:
leaderboard_endless = false
_refresh_leaderboard()
elif leaderboard_endless_button.has_point(position) and not leaderboard_endless:
leaderboard_endless = true
_refresh_leaderboard()
elif leaderboard_refresh_button.has_point(position):
_refresh_leaderboard()
queue_redraw()
return
if game_mode == "multiplayer_menu":
if multiplayer_back_button.has_point(position):
game_mode = "menu"
@@ -768,6 +792,30 @@ func _delete_profile_account() -> void:
queue_redraw()
func _open_leaderboard() -> void:
game_mode = "leaderboard"
leaderboard_endless = false
_refresh_leaderboard()
func _refresh_leaderboard() -> void:
if leaderboard_loading:
return
leaderboard_loading = true
leaderboard_message = "BESTENLISTE WIRD GELADEN …"
leaderboard_rows.clear()
queue_redraw()
if not online_manager.is_authenticated():
leaderboard_loading = false
leaderboard_message = "GERGAMES-SERVER WIRD VERBUNDEN – BITTE GLEICH ERNEUT LADEN"
queue_redraw()
return
leaderboard_rows = await online_manager.load_leaderboard(selected_map, selected_difficulty, leaderboard_endless, 12)
leaderboard_loading = false
leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else ""
queue_redraw()
func _start_game() -> void:
coop_online_mode = false
credits = int(_difficulty().start_credits)
@@ -1274,11 +1322,13 @@ func _load_progress() -> void:
func _draw() -> void:
if game_mode in ["menu", "profile", "profile_name_entry", "multiplayer_menu", "room_code_entry", "matchmaking"]:
if game_mode in ["menu", "leaderboard", "profile", "profile_name_entry", "multiplayer_menu", "room_code_entry", "matchmaking"]:
draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, SCREEN), false, Color("f3f6fb"))
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06))
if game_mode == "menu":
_draw_menu()
elif game_mode == "leaderboard":
_draw_leaderboard()
elif game_mode == "profile":
_draw_profile()
elif game_mode == "profile_name_entry":
@@ -1568,9 +1618,43 @@ func _draw_menu() -> void:
_draw_button(difficulty_buttons[index], DIFFICULTY_DATA[index].name, selected_difficulty == index, Color("27394d"))
draw_string(font, Vector2(0, 686), DIFFICULTY_DESCRIPTIONS[selected_difficulty], HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a"))
_draw_button(start_button, "MISSION STARTEN", false, Color("18556f"))
_draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b"))
_draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75"))
draw_string(font, Vector2(0, 792), "BESTE WELLE %02d • KERNKRISTALLE %04d • KARTENBONUS %d" % [best_wave, core_crystals, int(_difficulty().crystals)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("86c8d8"))
draw_string(font, Vector2(0, 860), "VERSION 0.21 • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
draw_string(font, Vector2(0, 860), "VERSION 0.23 • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
func _draw_leaderboard() -> void:
var font := ThemeDB.fallback_font
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.84))
_draw_button(leaderboard_back_button, "< ZURÜCK", false, Color("26394a"))
_draw_button(leaderboard_refresh_button, "AKTUALISIEREN", leaderboard_loading, Color("24516a"))
draw_string(font, Vector2(0, 108), "GLOBALE BESTENLISTE", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
draw_string(font, Vector2(0, 142), "%s • %s" % [MAP_NAMES[selected_map], DIFFICULTY_DATA[selected_difficulty].name], HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("66dff3"))
_draw_button(leaderboard_mission_button, "MISSION", not leaderboard_endless, Color("24475b"))
_draw_button(leaderboard_endless_button, "ENDLOSMODUS", leaderboard_endless, Color("5b452b"))
var panel := Rect2(235, 235, 1130, 525)
draw_rect(panel, Color(0.012, 0.035, 0.065, 0.96), true)
draw_rect(panel, Color("39788e"), false, 2.0)
draw_string(font, Vector2(275, 277), "RANG", HORIZONTAL_ALIGNMENT_LEFT, 100, 14, Color("91a8bc"))
draw_string(font, Vector2(410, 277), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 480, 14, Color("91a8bc"))
draw_string(font, Vector2(950, 277), "WELLE", HORIZONTAL_ALIGNMENT_CENTER, 130, 14, Color("91a8bc"))
draw_string(font, Vector2(1125, 277), "PUNKTE", HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, Color("91a8bc"))
draw_line(Vector2(270, 293), Vector2(1330, 293), Color("315c70"), 1.0)
var own_user_id: String = str(online_manager.session.user_id) if online_manager.session != null else ""
for index in leaderboard_rows.size():
var row: Dictionary = leaderboard_rows[index]
var y := 331.0 + index * 34.0
if index % 2 == 0:
draw_rect(Rect2(260, y - 24, 1080, 31), Color(0.03, 0.09, 0.14, 0.64), true)
var owner_color := Color("83ffc8") if not own_user_id.is_empty() and str(row.get("owner_id", "")) == own_user_id else Color.WHITE
draw_string(font, Vector2(280, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 90, 16, Color("8ff4ff"))
draw_string(font, Vector2(410, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 480, 16, owner_color)
draw_string(font, Vector2(950, y), "%02d" % int(row.get("wave", 0)), HORIZONTAL_ALIGNMENT_CENTER, 130, 16, Color("ffcf7a"))
draw_string(font, Vector2(1125, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 16, Color("8fffea"))
if not leaderboard_message.is_empty():
draw_string(font, Vector2(300, 520), leaderboard_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 17, Color("ffcf7a"))
draw_string(font, Vector2(0, 810), "Spielernamen werden aus dem aktuellen öffentlichen Profil geladen.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("91a8bc"))
func _draw_multiplayer_menu() -> void:
+4
View File
@@ -76,6 +76,10 @@ func _run() -> void:
game.call("_process", 0.1)
assert(game.get("next_enemy_id") > 3, "Beide Koop-Routen müssen auch während der laufenden Simulation Gegner erzeugen.")
game.call("_return_to_menu")
game.call("_handle_pointer", Vector2(400, 735), false)
assert(game.get("game_mode") == "leaderboard", "Die globale Bestenliste muss direkt aus dem Hauptmenü erreichbar sein.")
game.call("_handle_pointer", Vector2(140, 60), false)
assert(game.get("game_mode") == "menu", "Die Bestenliste benötigt einen funktionierenden Zurück-Button.")
assert(not account_manager.player_id.is_empty(), "Beim ersten Start muss automatisch ein stabiles Gastprofil angelegt werden.")
assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
account_manager.storage_enabled = false