Add update alerts and leaderboard filters
This commit is contained in:
1 parent
178a6bbc85
commit
6124331067
6 files changed
+239
-44
No files matched your search
@@ -110,11 +110,19 @@ Der Stack ist auf dem vorgesehenen Unraid-Server eingerichtet. Nakamas Spiel-API
|
||||
|
||||
Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar. DNS, HTTPS/WSS über den vorhandenen Nginx Proxy Manager und die Godot-Nakama-Clientanbindung sind eingerichtet. Website und Spielserver verwenden damit getrennte Adressen.
|
||||
|
||||
### Spielupdates
|
||||
|
||||
Der Client prüft beim Start die öffentlichen Releases des Projekts. Ist eine neuere Version vorhanden, erscheint im Hauptmenü ein kompakter Update-Hinweis. Unter Windows öffnet er direkt das aktuelle Setup, unter Android künftig das aktuelle APK; fehlt ein passendes Paket, wird die öffentliche Release-Seite geöffnet. Ein Update wird niemals ungefragt installiert.
|
||||
|
||||
### Kartenregel: Laufweg und Reaktor
|
||||
|
||||
Jede Einzelspielerroute muss mit ihrem letzten Wegpunkt exakt im Mittelpunkt der primären Reaktorzone enden. `_validate_route_ends_at_reactor()` prüft diese Vorgabe beim Laden jeder Karte; der Smoke-Test prüft sie zusätzlich für alle vorhandenen Karten. Dadurch bleiben sichtbare Straße, Gegnerbewegung, Bausperre und Reaktoreinfahrt deckungsgleich.
|
||||
|
||||
## Steuerung
|
||||
|
||||
> **Online-Stand Alpha 0.1.0:** 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.
|
||||
Die Schaltfläche **Bestenliste** im Hauptmenü zeigt die zwölf besten Piloten. Karte, Schwierigkeitsgrad sowie Mission oder Endlosmodus können direkt in der Ranglistenansicht gefiltert werden. Jede Zeile nennt Karte und Schwierigkeit sichtbar; 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.
|
||||
|
||||
@@ -23,9 +23,9 @@ const MAP_TEXTURES := [
|
||||
const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE"]
|
||||
const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring"]
|
||||
const MAP_PATHS := [
|
||||
[Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1560, 680), Vector2(1560, 520), Vector2(1640, 520)],
|
||||
[Vector2(-40, 280), Vector2(1320, 280), Vector2(1320, 440), Vector2(280, 440), Vector2(280, 680), Vector2(1480, 680), Vector2(1480, 440), Vector2(1640, 440)],
|
||||
[Vector2(-40, 120), Vector2(360, 120), Vector2(360, 600), Vector2(1320, 600), Vector2(1320, 280), Vector2(1640, 280)]
|
||||
[Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1560, 680), Vector2(1560, 520), Vector2(1480, 520), Vector2(1480, 440)],
|
||||
[Vector2(-40, 280), Vector2(1320, 280), Vector2(1320, 440), Vector2(280, 440), Vector2(280, 680), Vector2(1560, 680), Vector2(1560, 440), Vector2(1480, 440)],
|
||||
[Vector2(-40, 120), Vector2(360, 120), Vector2(360, 600), Vector2(1320, 600), Vector2(1320, 280), Vector2(1560, 280), Vector2(1560, 360), Vector2(1480, 360)]
|
||||
]
|
||||
const MAP_BUILD_ZONES := [
|
||||
[Rect2(155, 95, 1380, 660)],
|
||||
@@ -33,16 +33,16 @@ const MAP_BUILD_ZONES := [
|
||||
[Rect2(180, 110, 1270, 645)]
|
||||
]
|
||||
const MAP_REACTOR_ZONES := [
|
||||
[{"center": Vector2(1470, 430), "radius": 190.0}],
|
||||
[{"center": Vector2(1480, 430), "radius": 185.0}],
|
||||
[{"center": Vector2(1480, 440), "radius": 190.0}],
|
||||
[{"center": Vector2(1480, 440), "radius": 185.0}],
|
||||
[
|
||||
{"center": Vector2(1450, 355), "radius": 165.0},
|
||||
{"center": Vector2(1480, 360), "radius": 165.0},
|
||||
{"center": Vector2(690, 385), "radius": 155.0}
|
||||
]
|
||||
]
|
||||
const COOP_MIRROR_X := 1480.0
|
||||
const COOP_WORLD_WIDTH := COOP_MIRROR_X * 2.0
|
||||
const COOP_LEFT_PATH := [Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1400, 680), Vector2(1400, 520), Vector2(1480, 520)]
|
||||
const COOP_LEFT_PATH := [Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1400, 680), Vector2(1400, 520), Vector2(1480, 520), Vector2(1480, 440)]
|
||||
const COOP_REACTOR_CENTER := Vector2(COOP_MIRROR_X, 440)
|
||||
const COOP_REACTOR_RADIUS := 112.0
|
||||
|
||||
@@ -125,17 +125,25 @@ var upgrade_button := Rect2(1000, 814, 270, 64)
|
||||
var sell_button := Rect2(1290, 814, 285, 64)
|
||||
var tutorial_skip_button := Rect2(365, 205, 145, 30)
|
||||
var reward_ad_button := Rect2(1070, 690, 300, 54)
|
||||
var tower_info_rect := Rect2(1135, 105, 440, 104)
|
||||
var tower_info_rect := Rect2(1275, 103, 300, 76)
|
||||
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_mission_button := Rect2(510, 212, 270, 54)
|
||||
var leaderboard_endless_button := Rect2(820, 212, 270, 54)
|
||||
var leaderboard_refresh_button := Rect2(1265, 35, 280, 54)
|
||||
var leaderboard_map_previous_button := Rect2(235, 150, 58, 50)
|
||||
var leaderboard_map_button := Rect2(303, 150, 410, 50)
|
||||
var leaderboard_map_next_button := Rect2(723, 150, 58, 50)
|
||||
var leaderboard_difficulty_previous_button := Rect2(819, 150, 58, 50)
|
||||
var leaderboard_difficulty_button := Rect2(887, 150, 300, 50)
|
||||
var leaderboard_difficulty_next_button := Rect2(1197, 150, 58, 50)
|
||||
var leaderboard_rows: Array = []
|
||||
var leaderboard_endless := false
|
||||
var leaderboard_map := 0
|
||||
var leaderboard_difficulty := 1
|
||||
var leaderboard_loading := false
|
||||
var leaderboard_message := ""
|
||||
var profile_back_button := Rect2(55, 35, 185, 54)
|
||||
@@ -160,11 +168,16 @@ var profile_privacy_button := Rect2(840, 685, 250, 48)
|
||||
var profile_delete_button := Rect2(1110, 685, 250, 48)
|
||||
var profile_name_input_rect := Rect2(460, 330, 680, 82)
|
||||
var profile_name_save_button := Rect2(625, 470, 350, 62)
|
||||
var update_button := Rect2(30, 25, 390, 54)
|
||||
var update_dismiss_button := Rect2(430, 25, 54, 54)
|
||||
var available_update: Dictionary = {}
|
||||
var update_notice_visible := false
|
||||
|
||||
@onready var ads_manager = $AdsManager
|
||||
@onready var account_manager = $AccountManager
|
||||
@onready var multiplayer_manager = $MultiplayerManager
|
||||
@onready var online_manager = $OnlineManager
|
||||
@onready var update_manager = $UpdateManager
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -175,6 +188,7 @@ func _ready() -> void:
|
||||
multiplayer_manager.online_game_ready.connect(_start_coop_online_game)
|
||||
multiplayer_manager.online_snapshot_updated.connect(_apply_online_snapshot)
|
||||
multiplayer_manager.online_event_received.connect(_handle_online_event)
|
||||
update_manager.update_available.connect(_on_update_available)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -435,6 +449,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
|
||||
func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
||||
if game_mode == "menu":
|
||||
if update_notice_visible and update_button.has_point(position):
|
||||
OS.shell_open(str(available_update.get("url", "https://void-bastion.gergames.de/")))
|
||||
return
|
||||
if update_notice_visible and update_dismiss_button.has_point(position):
|
||||
update_notice_visible = false
|
||||
queue_redraw()
|
||||
return
|
||||
if profile_button.has_point(position):
|
||||
game_mode = "profile"
|
||||
queue_redraw()
|
||||
@@ -467,6 +488,18 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
||||
_refresh_leaderboard()
|
||||
elif leaderboard_refresh_button.has_point(position):
|
||||
_refresh_leaderboard()
|
||||
elif leaderboard_map_previous_button.has_point(position):
|
||||
leaderboard_map = wrapi(leaderboard_map - 1, 0, MAP_NAMES.size())
|
||||
_refresh_leaderboard()
|
||||
elif leaderboard_map_button.has_point(position) or leaderboard_map_next_button.has_point(position):
|
||||
leaderboard_map = wrapi(leaderboard_map + 1, 0, MAP_NAMES.size())
|
||||
_refresh_leaderboard()
|
||||
elif leaderboard_difficulty_previous_button.has_point(position):
|
||||
leaderboard_difficulty = wrapi(leaderboard_difficulty - 1, 0, DIFFICULTY_DATA.size())
|
||||
_refresh_leaderboard()
|
||||
elif leaderboard_difficulty_button.has_point(position) or leaderboard_difficulty_next_button.has_point(position):
|
||||
leaderboard_difficulty = wrapi(leaderboard_difficulty + 1, 0, DIFFICULTY_DATA.size())
|
||||
_refresh_leaderboard()
|
||||
queue_redraw()
|
||||
return
|
||||
if game_mode == "multiplayer_menu":
|
||||
@@ -612,6 +645,7 @@ func _select_map(index: int) -> void:
|
||||
path_points = PackedVector2Array()
|
||||
for point in MAP_PATHS[selected_map]:
|
||||
path_points.append(point * WORLD_SCALE)
|
||||
_validate_route_ends_at_reactor(path_points, selected_map)
|
||||
_rebuild_path_cells()
|
||||
build_zones.clear()
|
||||
for zone in MAP_BUILD_ZONES[selected_map]:
|
||||
@@ -632,6 +666,19 @@ func _configure_coop_arena() -> void:
|
||||
_add_path_cells(route)
|
||||
build_zones = [Rect2(120, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP), Rect2(1560, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP)]
|
||||
camera_offset = Vector2.ZERO
|
||||
if left[-1].distance_to(COOP_REACTOR_CENTER) > 1.0 or right[-1].distance_to(COOP_REACTOR_CENTER) > 1.0:
|
||||
push_error("Koop-Routen müssen sichtbar im gemeinsamen Reaktor enden.")
|
||||
|
||||
|
||||
func _validate_route_ends_at_reactor(route: PackedVector2Array, map_index: int) -> bool:
|
||||
if route.is_empty() or map_index < 0 or map_index >= MAP_REACTOR_ZONES.size():
|
||||
push_error("Kartenroute oder Reaktordaten fehlen.")
|
||||
return false
|
||||
var reactor_center: Vector2 = MAP_REACTOR_ZONES[map_index][0].center
|
||||
var is_connected := route[-1].distance_to(reactor_center) <= 1.0
|
||||
if not is_connected:
|
||||
push_error("Karte %s: Der Laufweg muss exakt im Reaktormittelpunkt enden." % MAP_NAMES[map_index])
|
||||
return is_connected
|
||||
|
||||
|
||||
func _toggle_coop_side() -> void:
|
||||
@@ -812,6 +859,8 @@ func _delete_profile_account() -> void:
|
||||
func _open_leaderboard() -> void:
|
||||
game_mode = "leaderboard"
|
||||
leaderboard_endless = false
|
||||
leaderboard_map = selected_map
|
||||
leaderboard_difficulty = selected_difficulty
|
||||
_refresh_leaderboard()
|
||||
|
||||
|
||||
@@ -827,7 +876,7 @@ func _refresh_leaderboard() -> void:
|
||||
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_rows = await online_manager.load_leaderboard(leaderboard_map, leaderboard_difficulty, leaderboard_endless, 12)
|
||||
leaderboard_loading = false
|
||||
leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else ""
|
||||
queue_redraw()
|
||||
@@ -977,6 +1026,12 @@ func _difficulty() -> Dictionary:
|
||||
return DIFFICULTY_DATA[clampi(selected_difficulty, 0, DIFFICULTY_DATA.size() - 1)]
|
||||
|
||||
|
||||
func _on_update_available(info: Dictionary) -> void:
|
||||
available_update = info.duplicate(true)
|
||||
update_notice_visible = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_energy_reward_granted(amount: int) -> void:
|
||||
_award_credits(0, amount)
|
||||
_show_status("NOTFALLENERGIE +%d" % amount)
|
||||
@@ -1407,10 +1462,10 @@ func _draw_world() -> void:
|
||||
var valid_position := _is_valid_build_position(snapped_position, _local_credit_slot())
|
||||
var placement_color := Color(0.25, 1.0, 0.7, 0.22) if valid_position else Color(1.0, 0.2, 0.2, 0.22)
|
||||
var preview_range := _tower_range(selected_tower_type, 1)
|
||||
draw_circle(snapped_position, preview_range, Color(placement_color, 0.09))
|
||||
draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.82), 3.0)
|
||||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), placement_color, true)
|
||||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.95), false, 3.0)
|
||||
draw_circle(snapped_position, preview_range, Color(placement_color, 0.025))
|
||||
draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.46), 1.5)
|
||||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.10), true)
|
||||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.62), false, 1.5)
|
||||
_draw_tower({"pos": snapped_position, "type": selected_tower_type, "level": 1, "angle": 0.0})
|
||||
for tower_index in towers.size():
|
||||
var tower: Dictionary = towers[tower_index]
|
||||
@@ -1472,8 +1527,8 @@ func _draw_route() -> void:
|
||||
|
||||
|
||||
func _draw_build_grid() -> void:
|
||||
var line_color := Color(0.24, 0.82, 1.0, 0.22 if selected_tower_type >= 0 else 0.10)
|
||||
var fill_color := Color(0.08, 0.55, 0.72, 0.08 if selected_tower_type >= 0 else 0.025)
|
||||
var line_color := Color(0.24, 0.82, 1.0, 0.14 if selected_tower_type >= 0 else 0.10)
|
||||
var fill_color := Color(0.08, 0.55, 0.72, 0.035 if selected_tower_type >= 0 else 0.025)
|
||||
for x_index in int(_active_world_width() / GRID_SIZE):
|
||||
for y_index in int(WORLD_SIZE.y / GRID_SIZE):
|
||||
var center := Vector2(x_index * GRID_SIZE + GRID_SIZE * 0.5, y_index * GRID_SIZE + GRID_SIZE * 0.5)
|
||||
@@ -1587,18 +1642,18 @@ func _draw_tower_info_panel() -> void:
|
||||
return
|
||||
var data: Dictionary = TOWER_DATA[type]
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_rect(tower_info_rect, Color(0.008, 0.028, 0.055, 0.94), true)
|
||||
draw_rect(tower_info_rect, Color("4a9ab2"), false, 2.0)
|
||||
draw_rect(tower_info_rect, Color(0.008, 0.028, 0.055, 0.58), true)
|
||||
draw_rect(tower_info_rect, Color(0.29, 0.60, 0.70, 0.72), false, 1.5)
|
||||
var heading := "%s • STUFE %d" % [data.name, level] if is_built else "%s • BAUVORSCHAU" % data.name
|
||||
draw_string(font, tower_info_rect.position + Vector2(14, 26), heading, HORIZONTAL_ALIGNMENT_LEFT, 412, 17, Color("a9efff"))
|
||||
draw_string(font, tower_info_rect.position + Vector2(14, 52), "SCH %.0f | %.2f/S | RW %.0f | %s" % [_tower_damage(type, level), _tower_shots_per_second(type, level), _tower_range(type, level), data.damage_type], HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color.WHITE)
|
||||
draw_string(font, tower_info_rect.position + Vector2(10, 20), heading, HORIZONTAL_ALIGNMENT_LEFT, 280, 13, Color("a9efff"))
|
||||
draw_string(font, tower_info_rect.position + Vector2(10, 41), "SCH %.0f • %.2f/S • RW %.0f" % [_tower_damage(type, level), _tower_shots_per_second(type, level), _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 280, 10, Color.WHITE)
|
||||
if is_built and level < 3:
|
||||
var next_level := level + 1
|
||||
draw_string(font, tower_info_rect.position + Vector2(14, 80), "UPGRADE: +%.0f SCH +%.2f/S +%.0f RW" % [_tower_damage(type, next_level) - _tower_damage(type, level), _tower_shots_per_second(type, next_level) - _tower_shots_per_second(type, level), _tower_range(type, next_level) - _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
|
||||
draw_string(font, tower_info_rect.position + Vector2(10, 62), "UPGRADE +%.0f SCH +%.2f/S +%.0f RW" % [_tower_damage(type, next_level) - _tower_damage(type, level), _tower_shots_per_second(type, next_level) - _tower_shots_per_second(type, level), _tower_range(type, next_level) - _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
|
||||
elif is_built:
|
||||
draw_string(font, tower_info_rect.position + Vector2(14, 80), "MAXIMALE AUSBAUSTUFE ERREICHT", HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
|
||||
draw_string(font, tower_info_rect.position + Vector2(10, 62), "MAXIMALE AUSBAUSTUFE", HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
|
||||
else:
|
||||
draw_string(font, tower_info_rect.position + Vector2(14, 80), "Reichweite wird vor dem Bau angezeigt.", HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
|
||||
draw_string(font, tower_info_rect.position + Vector2(10, 62), str(data.damage_type), HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
|
||||
|
||||
|
||||
func _draw_button(rect: Rect2, text: String, active: bool, base := Color("173c55")) -> void:
|
||||
@@ -1638,7 +1693,10 @@ func _draw_menu() -> void:
|
||||
_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.23 • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
|
||||
draw_string(font, Vector2(0, 860), "VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN" % _app_version(), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
|
||||
if update_notice_visible:
|
||||
_draw_button(update_button, "UPDATE %s VERFÜGBAR" % str(available_update.get("version", "")), true, Color("1f5d4d"))
|
||||
_draw_button(update_dismiss_button, "×", false, Color("3d4650"))
|
||||
|
||||
|
||||
func _draw_leaderboard() -> void:
|
||||
@@ -1647,28 +1705,37 @@ func _draw_leaderboard() -> void:
|
||||
_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_map_previous_button, "<", false, Color("26394a"))
|
||||
_draw_button(leaderboard_map_button, "KARTE • %s" % MAP_NAMES[leaderboard_map], true, Color("24475b"))
|
||||
_draw_button(leaderboard_map_next_button, ">", false, Color("26394a"))
|
||||
_draw_button(leaderboard_difficulty_previous_button, "<", false, Color("26394a"))
|
||||
_draw_button(leaderboard_difficulty_button, "STUFE • %s" % DIFFICULTY_DATA[leaderboard_difficulty].name, true, Color("5b452b"))
|
||||
_draw_button(leaderboard_difficulty_next_button, ">", false, Color("26394a"))
|
||||
_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)
|
||||
var panel := Rect2(205, 280, 1190, 505)
|
||||
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)
|
||||
draw_string(font, Vector2(235, 319), "RANG", HORIZONTAL_ALIGNMENT_LEFT, 75, 13, Color("91a8bc"))
|
||||
draw_string(font, Vector2(325, 319), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 270, 13, Color("91a8bc"))
|
||||
draw_string(font, Vector2(610, 319), "KARTE", HORIZONTAL_ALIGNMENT_LEFT, 240, 13, Color("91a8bc"))
|
||||
draw_string(font, Vector2(850, 319), "STUFE", HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("91a8bc"))
|
||||
draw_string(font, Vector2(995, 319), "WELLE", HORIZONTAL_ALIGNMENT_CENTER, 110, 13, Color("91a8bc"))
|
||||
draw_string(font, Vector2(1160, 319), "PUNKTE", HORIZONTAL_ALIGNMENT_RIGHT, 190, 13, Color("91a8bc"))
|
||||
draw_line(Vector2(230, 334), Vector2(1370, 334), 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
|
||||
var y := 370.0 + index * 32.0
|
||||
if index % 2 == 0:
|
||||
draw_rect(Rect2(260, y - 24, 1080, 31), Color(0.03, 0.09, 0.14, 0.64), true)
|
||||
draw_rect(Rect2(225, y - 23, 1150, 29), 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"))
|
||||
draw_string(font, Vector2(240, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 65, 14, Color("8ff4ff"))
|
||||
draw_string(font, Vector2(325, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 270, 14, owner_color)
|
||||
draw_string(font, Vector2(610, y), MAP_NAMES[int(row.get("map", leaderboard_map))], HORIZONTAL_ALIGNMENT_LEFT, 225, 13, Color("9fdce9"))
|
||||
draw_string(font, Vector2(850, y), DIFFICULTY_DATA[int(row.get("difficulty", leaderboard_difficulty))].name, HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("ffcf7a"))
|
||||
draw_string(font, Vector2(995, y), "%02d" % int(row.get("wave", 0)), HORIZONTAL_ALIGNMENT_CENTER, 110, 14, Color("ffcf7a"))
|
||||
draw_string(font, Vector2(1160, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, 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"))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[gd_scene load_steps=6 format=3]
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource path="res://main.gd" type="Script" id="1"]
|
||||
[ext_resource path="res://ads_manager.gd" type="Script" id="2"]
|
||||
[ext_resource path="res://account_manager.gd" type="Script" id="3"]
|
||||
[ext_resource path="res://multiplayer_manager.gd" type="Script" id="4"]
|
||||
[ext_resource path="res://online_manager.gd" type="Script" id="5"]
|
||||
[ext_resource path="res://update_manager.gd" type="Script" id="6"]
|
||||
|
||||
[node name="VoidBastion" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
@@ -20,3 +21,6 @@ script = ExtResource("4")
|
||||
|
||||
[node name="OnlineManager" type="Node" parent="."]
|
||||
script = ExtResource("5")
|
||||
|
||||
[node name="UpdateManager" type="Node" parent="."]
|
||||
script = ExtResource("6")
|
||||
+4
-1
@@ -215,7 +215,10 @@ func load_leaderboard(map_index: int, difficulty_index: int, endless: bool, limi
|
||||
"username": str(current_names.get(record.owner_id, record.username)),
|
||||
"score": int(record.score),
|
||||
"wave": int(record.subscore),
|
||||
"owner_id": record.owner_id
|
||||
"owner_id": record.owner_id,
|
||||
"map": map_index,
|
||||
"difficulty": difficulty_index,
|
||||
"mode": "endless" if endless else "mission"
|
||||
})
|
||||
leaderboard_loaded.emit(leaderboard_id, rows)
|
||||
return rows
|
||||
|
||||
+22
-3
@@ -79,6 +79,12 @@ func _run() -> void:
|
||||
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.")
|
||||
var leaderboard_map_before: int = game.get("leaderboard_map")
|
||||
game.call("_handle_pointer", Vector2(750, 175), false)
|
||||
assert(game.get("leaderboard_map") == wrapi(leaderboard_map_before + 1, 0, 3), "Die Rangliste muss direkt nach Karte filterbar sein.")
|
||||
var leaderboard_difficulty_before: int = game.get("leaderboard_difficulty")
|
||||
game.call("_handle_pointer", Vector2(1225, 175), false)
|
||||
assert(game.get("leaderboard_difficulty") == wrapi(leaderboard_difficulty_before + 1, 0, 3), "Die Rangliste muss direkt nach Schwierigkeitsgrad filterbar 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.")
|
||||
@@ -107,8 +113,19 @@ func _run() -> void:
|
||||
ads_manager.last_interstitial_time = 800
|
||||
assert(not ads_manager.is_interstitial_due(1000), "Die 15-Minuten-Sperre zwischen Vollbildanzeigen muss greifen.")
|
||||
ads_manager.last_interstitial_time = 0
|
||||
assert(game.get("path_points").size() == 8, "Bastion Prime sollte aus sieben klaren Rasterstrecken bestehen.")
|
||||
var update_manager = game.get_node("UpdateManager")
|
||||
assert(update_manager.call("_is_newer_version", "0.1.0-alpha.3", "0.1.0-alpha.2"), "Eine neuere Alpha-Version muss als Update erkannt werden.")
|
||||
assert(update_manager.call("_is_newer_version", "0.1.0", "0.1.0-alpha.2"), "Eine stabile Version muss neuer als ihre Alpha-Version sein.")
|
||||
assert(not update_manager.call("_is_newer_version", "0.1.0-alpha.2", "0.1.0-alpha.2"), "Die installierte Version darf nicht als Update angeboten werden.")
|
||||
var announced_update := {}
|
||||
update_manager.update_available.connect(func(info: Dictionary): announced_update.merge(info, true))
|
||||
var fake_release_body := JSON.stringify([{"tag_name": "v0.1.0-alpha.3", "name": "Alpha.3", "draft": false, "assets": [{"name": "Void-Bastion-Setup-x64.exe", "browser_download_url": "https://git.dominicspringer.de/public-test.exe"}]}]).to_utf8_buffer()
|
||||
update_manager.call("_on_request_completed", HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), fake_release_body)
|
||||
assert(announced_update.get("version") == "0.1.0-alpha.3", "Die Update-Prüfung muss eine neuere öffentliche Version ankündigen.")
|
||||
assert(str(announced_update.get("url", "")).begins_with("https://git.dominicspringer.de/"), "Der Update-Hinweis darf nur einen öffentlichen Download-Link öffnen.")
|
||||
assert(game.get("path_points").size() == 9, "Bastion Prime sollte mit einer eigenen Einfahrt im Reaktor enden.")
|
||||
_assert_grid_aligned_path(game.get("path_points"), "Bastion Prime")
|
||||
assert(game.call("_validate_route_ends_at_reactor", game.get("path_points"), 0), "Bastion Prime muss exakt im sichtbaren Reaktor enden.")
|
||||
assert(_path_length(game.get("path_points")) >= 4000.0, "Die Bastion-Route sollte den sichtbaren Bauraum ausnutzen.")
|
||||
assert(game.get("camera_offset") == Vector2.ZERO, "Die ganze Karte muss ohne Verschieben sichtbar bleiben.")
|
||||
assert(game.get("path_cells").size() > 45, "Straße, Gegnerlinie und Bausperre müssen dieselbe lange Rasterzellenkette verwenden.")
|
||||
@@ -121,8 +138,9 @@ func _run() -> void:
|
||||
assert(game.call("_is_valid_build_position", Vector2(200, 680)), "Außerhalb der Wegzellen soll jede geeignete Plattformzelle bebaubar sein.")
|
||||
game.call("_select_map", 1)
|
||||
assert(game.get("selected_map") == 1, "Die zweite Karte sollte auswählbar sein.")
|
||||
assert(game.get("path_points").size() == 8, "Die Helios-Werft sollte aus sieben klaren Rasterstrecken bestehen.")
|
||||
assert(game.get("path_points").size() == 8, "Die Helios-Werft sollte direkt im sichtbaren Reaktor enden.")
|
||||
_assert_grid_aligned_path(game.get("path_points"), "Helios-Werft")
|
||||
assert(game.call("_validate_route_ends_at_reactor", game.get("path_points"), 1), "Die Helios-Route muss exakt im Reaktormittelpunkt enden.")
|
||||
assert(_path_length(game.get("path_points")) >= 4400.0, "Die Werft-Route sollte mehrere Feuerlinien bilden.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(600, 280)), "Auf der oberen Helios-Fahrbahn darf nicht gebaut werden.")
|
||||
assert(game.call("_is_valid_build_position", Vector2(600, 360)), "Auf Helios muss zwischen der oberen und mittleren Fahrbahn gebaut werden können.")
|
||||
@@ -136,8 +154,9 @@ func _run() -> void:
|
||||
assert(not game.call("_is_valid_build_position", Vector2(1480, 440)), "Auf dem Bastion-Reaktor darf nicht gebaut werden.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(40, 680)), "Im freien Weltraum darf kein Bauplatz liegen.")
|
||||
game.call("_select_map", 2)
|
||||
assert(game.get("path_points").size() == 6, "Pyrax sollte aus fünf klaren Rasterstrecken bestehen.")
|
||||
assert(game.get("path_points").size() == 8, "Pyrax sollte eine klar sichtbare Reaktoreinfahrt besitzen.")
|
||||
_assert_grid_aligned_path(game.get("path_points"), "Pyrax-Raffinerie")
|
||||
assert(game.call("_validate_route_ends_at_reactor", game.get("path_points"), 2), "Die Pyrax-Route muss exakt im Reaktormittelpunkt enden.")
|
||||
assert(_path_length(game.get("path_points")) >= 2480.0, "Die Pyrax-Route sollte den Bauraum weiträumig erschließen.")
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(1440, 360)), "Der Pyrax-Hauptreaktor muss als Sperrzone erkannt werden.")
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(680, 360)), "Auch die offene Pyrax-Reaktorkammer muss gesperrt sein.")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
class_name UpdateManager
|
||||
extends Node
|
||||
|
||||
signal update_available(info: Dictionary)
|
||||
|
||||
const RELEASES_API := "https://git.dominicspringer.de/api/v1/repos/Emporex/Void-Bastion-Releases/releases"
|
||||
const PUBLIC_RELEASE_BASE := "https://git.dominicspringer.de/Emporex/Void-Bastion-Releases/releases/tag/"
|
||||
|
||||
var _request: HTTPRequest
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if DisplayServer.get_name() == "headless" or Engine.is_editor_hint():
|
||||
return
|
||||
_request = HTTPRequest.new()
|
||||
add_child(_request)
|
||||
_request.request_completed.connect(_on_request_completed)
|
||||
call_deferred("check_for_update")
|
||||
|
||||
|
||||
func check_for_update() -> void:
|
||||
if _request == null or _request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
_request.request(RELEASES_API, ["Accept: application/json", "User-Agent: Void-Bastion/%s" % _current_version()])
|
||||
|
||||
|
||||
func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
return
|
||||
var releases = JSON.parse_string(body.get_string_from_utf8())
|
||||
if not releases is Array:
|
||||
return
|
||||
for release in releases:
|
||||
if not release is Dictionary or bool(release.get("draft", false)):
|
||||
continue
|
||||
var version := str(release.get("tag_name", "")).trim_prefix("v")
|
||||
if version.is_empty() or not _is_newer_version(version, _current_version()):
|
||||
return
|
||||
var download_url := PUBLIC_RELEASE_BASE + "v" + version
|
||||
var wanted_extension := ".apk" if OS.get_name() == "Android" else ".exe"
|
||||
for asset in release.get("assets", []):
|
||||
if asset is Dictionary and str(asset.get("name", "")).to_lower().ends_with(wanted_extension):
|
||||
download_url = str(asset.get("browser_download_url", download_url))
|
||||
break
|
||||
update_available.emit({
|
||||
"version": version,
|
||||
"name": str(release.get("name", version)),
|
||||
"url": download_url
|
||||
})
|
||||
return
|
||||
|
||||
|
||||
func _current_version() -> String:
|
||||
return str(ProjectSettings.get_setting("application/config/version", "0.0.0"))
|
||||
|
||||
|
||||
func _is_newer_version(remote: String, local: String) -> bool:
|
||||
var remote_parts := _version_parts(remote)
|
||||
var local_parts := _version_parts(local)
|
||||
for index in 3:
|
||||
if remote_parts[index] != local_parts[index]:
|
||||
return remote_parts[index] > local_parts[index]
|
||||
var remote_suffix: String = remote_parts[3]
|
||||
var local_suffix: String = local_parts[3]
|
||||
if remote_suffix.is_empty():
|
||||
return not local_suffix.is_empty()
|
||||
if local_suffix.is_empty():
|
||||
return false
|
||||
return _prerelease_weight(remote_suffix) > _prerelease_weight(local_suffix)
|
||||
|
||||
|
||||
func _version_parts(value: String) -> Array:
|
||||
var normalized := value.trim_prefix("v").strip_edges().to_lower()
|
||||
var sections := normalized.split("-", true, 1)
|
||||
var numbers := sections[0].split(".")
|
||||
return [
|
||||
int(numbers[0]) if numbers.size() > 0 else 0,
|
||||
int(numbers[1]) if numbers.size() > 1 else 0,
|
||||
int(numbers[2]) if numbers.size() > 2 else 0,
|
||||
sections[1] if sections.size() > 1 else ""
|
||||
]
|
||||
|
||||
|
||||
func _prerelease_weight(value: String) -> int:
|
||||
var stage := 0
|
||||
if value.begins_with("beta"):
|
||||
stage = 100000
|
||||
elif value.begins_with("rc"):
|
||||
stage = 200000
|
||||
var sequence := 0
|
||||
for part in value.split("."):
|
||||
if part.is_valid_int():
|
||||
sequence = int(part)
|
||||
return stage + sequence
|
||||
Reference in new issue
Block a user