Add in-game leaderboard screen
This commit is contained in:
1 parent
31ddba0d45
commit
5a3b7b6e0c
4 files changed
+94
-2
No files matched your search
@@ -22,6 +22,8 @@ Die Kennungen folgen diesem Muster: `mission_map0_difficulty0`.
|
|||||||
|
|
||||||
Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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`.
|
> **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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ var tower_info_rect := Rect2(1135, 105, 440, 104)
|
|||||||
var endless_button := Rect2(545, 515, 510, 70)
|
var endless_button := Rect2(545, 515, 510, 70)
|
||||||
var finish_mission_button := Rect2(625, 610, 350, 62)
|
var finish_mission_button := Rect2(625, 610, 350, 62)
|
||||||
var profile_button := Rect2(1280, 25, 270, 52)
|
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 profile_back_button := Rect2(55, 35, 185, 54)
|
||||||
var multiplayer_button := Rect2(1000, 705, 350, 60)
|
var multiplayer_button := Rect2(1000, 705, 350, 60)
|
||||||
var multiplayer_back_button := Rect2(55, 35, 185, 54)
|
var multiplayer_back_button := Rect2(55, 35, 185, 54)
|
||||||
@@ -440,10 +449,25 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
|||||||
return
|
return
|
||||||
if start_button.has_point(position):
|
if start_button.has_point(position):
|
||||||
_start_game()
|
_start_game()
|
||||||
|
elif leaderboard_button.has_point(position):
|
||||||
|
_open_leaderboard()
|
||||||
elif multiplayer_button.has_point(position):
|
elif multiplayer_button.has_point(position):
|
||||||
game_mode = "multiplayer_menu"
|
game_mode = "multiplayer_menu"
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
return
|
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 game_mode == "multiplayer_menu":
|
||||||
if multiplayer_back_button.has_point(position):
|
if multiplayer_back_button.has_point(position):
|
||||||
game_mode = "menu"
|
game_mode = "menu"
|
||||||
@@ -768,6 +792,30 @@ func _delete_profile_account() -> void:
|
|||||||
queue_redraw()
|
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:
|
func _start_game() -> void:
|
||||||
coop_online_mode = false
|
coop_online_mode = false
|
||||||
credits = int(_difficulty().start_credits)
|
credits = int(_difficulty().start_credits)
|
||||||
@@ -1274,11 +1322,13 @@ func _load_progress() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _draw() -> 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_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))
|
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06))
|
||||||
if game_mode == "menu":
|
if game_mode == "menu":
|
||||||
_draw_menu()
|
_draw_menu()
|
||||||
|
elif game_mode == "leaderboard":
|
||||||
|
_draw_leaderboard()
|
||||||
elif game_mode == "profile":
|
elif game_mode == "profile":
|
||||||
_draw_profile()
|
_draw_profile()
|
||||||
elif game_mode == "profile_name_entry":
|
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_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_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(start_button, "MISSION STARTEN", false, Color("18556f"))
|
||||||
|
_draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b"))
|
||||||
_draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75"))
|
_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, 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:
|
func _draw_multiplayer_menu() -> void:
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ func _run() -> void:
|
|||||||
game.call("_process", 0.1)
|
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.")
|
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("_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.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.")
|
assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
|
||||||
account_manager.storage_enabled = false
|
account_manager.storage_enabled = false
|
||||||
|
|||||||
Reference in new issue
Block a user