diff --git a/ONLINE_SETUP.md b/ONLINE_SETUP.md index a71f2fb..728c76c 100644 --- a/ONLINE_SETUP.md +++ b/ONLINE_SETUP.md @@ -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. diff --git a/README.md b/README.md index 4ed5522..5566cd8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main.gd b/main.gd index 44fe8c0..3ef1a19 100644 --- a/main.gd +++ b/main.gd @@ -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: diff --git a/tests/smoke_test.gd b/tests/smoke_test.gd index cfb2e09..032f663 100644 --- a/tests/smoke_test.gd +++ b/tests/smoke_test.gd @@ -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