Add update alerts and leaderboard filters

This commit is contained in:
Emporex committed 2026-10-04 22:12:04 +02:00
1 parent 178a6bbc85
commit 6124331067
6 files changed
+239 -44

No files matched your search

+9 -1
View File
@@ -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. 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 ## 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`. > **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. - 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.
+105 -38
View File
@@ -23,9 +23,9 @@ const MAP_TEXTURES := [
const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE"] const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE"]
const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring"] const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring"]
const MAP_PATHS := [ 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, 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(1480, 680), Vector2(1480, 440), Vector2(1640, 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(1640, 280)] [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 := [ const MAP_BUILD_ZONES := [
[Rect2(155, 95, 1380, 660)], [Rect2(155, 95, 1380, 660)],
@@ -33,16 +33,16 @@ const MAP_BUILD_ZONES := [
[Rect2(180, 110, 1270, 645)] [Rect2(180, 110, 1270, 645)]
] ]
const MAP_REACTOR_ZONES := [ const MAP_REACTOR_ZONES := [
[{"center": Vector2(1470, 430), "radius": 190.0}], [{"center": Vector2(1480, 440), "radius": 190.0}],
[{"center": Vector2(1480, 430), "radius": 185.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} {"center": Vector2(690, 385), "radius": 155.0}
] ]
] ]
const COOP_MIRROR_X := 1480.0 const COOP_MIRROR_X := 1480.0
const COOP_WORLD_WIDTH := COOP_MIRROR_X * 2.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_CENTER := Vector2(COOP_MIRROR_X, 440)
const COOP_REACTOR_RADIUS := 112.0 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 sell_button := Rect2(1290, 814, 285, 64)
var tutorial_skip_button := Rect2(365, 205, 145, 30) var tutorial_skip_button := Rect2(365, 205, 145, 30)
var reward_ad_button := Rect2(1070, 690, 300, 54) 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 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_button := Rect2(250, 705, 350, 60)
var leaderboard_back_button := Rect2(55, 35, 185, 54) var leaderboard_back_button := Rect2(55, 35, 185, 54)
var leaderboard_mission_button := Rect2(510, 155, 270, 54) var leaderboard_mission_button := Rect2(510, 212, 270, 54)
var leaderboard_endless_button := Rect2(820, 155, 270, 54) var leaderboard_endless_button := Rect2(820, 212, 270, 54)
var leaderboard_refresh_button := Rect2(1265, 35, 280, 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_rows: Array = []
var leaderboard_endless := false var leaderboard_endless := false
var leaderboard_map := 0
var leaderboard_difficulty := 1
var leaderboard_loading := false var leaderboard_loading := false
var leaderboard_message := "" var leaderboard_message := ""
var profile_back_button := Rect2(55, 35, 185, 54) 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_delete_button := Rect2(1110, 685, 250, 48)
var profile_name_input_rect := Rect2(460, 330, 680, 82) var profile_name_input_rect := Rect2(460, 330, 680, 82)
var profile_name_save_button := Rect2(625, 470, 350, 62) 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 ads_manager = $AdsManager
@onready var account_manager = $AccountManager @onready var account_manager = $AccountManager
@onready var multiplayer_manager = $MultiplayerManager @onready var multiplayer_manager = $MultiplayerManager
@onready var online_manager = $OnlineManager @onready var online_manager = $OnlineManager
@onready var update_manager = $UpdateManager
func _ready() -> void: func _ready() -> void:
@@ -175,6 +188,7 @@ func _ready() -> void:
multiplayer_manager.online_game_ready.connect(_start_coop_online_game) multiplayer_manager.online_game_ready.connect(_start_coop_online_game)
multiplayer_manager.online_snapshot_updated.connect(_apply_online_snapshot) multiplayer_manager.online_snapshot_updated.connect(_apply_online_snapshot)
multiplayer_manager.online_event_received.connect(_handle_online_event) multiplayer_manager.online_event_received.connect(_handle_online_event)
update_manager.update_available.connect(_on_update_available)
queue_redraw() queue_redraw()
@@ -435,6 +449,13 @@ func _unhandled_input(event: InputEvent) -> void:
func _handle_pointer(position: Vector2, is_touch := false) -> void: func _handle_pointer(position: Vector2, is_touch := false) -> void:
if game_mode == "menu": 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): if profile_button.has_point(position):
game_mode = "profile" game_mode = "profile"
queue_redraw() queue_redraw()
@@ -467,6 +488,18 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
_refresh_leaderboard() _refresh_leaderboard()
elif leaderboard_refresh_button.has_point(position): elif leaderboard_refresh_button.has_point(position):
_refresh_leaderboard() _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() queue_redraw()
return return
if game_mode == "multiplayer_menu": if game_mode == "multiplayer_menu":
@@ -612,6 +645,7 @@ func _select_map(index: int) -> void:
path_points = PackedVector2Array() path_points = PackedVector2Array()
for point in MAP_PATHS[selected_map]: for point in MAP_PATHS[selected_map]:
path_points.append(point * WORLD_SCALE) path_points.append(point * WORLD_SCALE)
_validate_route_ends_at_reactor(path_points, selected_map)
_rebuild_path_cells() _rebuild_path_cells()
build_zones.clear() build_zones.clear()
for zone in MAP_BUILD_ZONES[selected_map]: for zone in MAP_BUILD_ZONES[selected_map]:
@@ -632,6 +666,19 @@ func _configure_coop_arena() -> void:
_add_path_cells(route) _add_path_cells(route)
build_zones = [Rect2(120, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP), Rect2(1560, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP)] build_zones = [Rect2(120, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP), Rect2(1560, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP)]
camera_offset = Vector2.ZERO 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: func _toggle_coop_side() -> void:
@@ -812,6 +859,8 @@ func _delete_profile_account() -> void:
func _open_leaderboard() -> void: func _open_leaderboard() -> void:
game_mode = "leaderboard" game_mode = "leaderboard"
leaderboard_endless = false leaderboard_endless = false
leaderboard_map = selected_map
leaderboard_difficulty = selected_difficulty
_refresh_leaderboard() _refresh_leaderboard()
@@ -827,7 +876,7 @@ func _refresh_leaderboard() -> void:
leaderboard_message = "GERGAMES-SERVER WIRD VERBUNDEN – BITTE GLEICH ERNEUT LADEN" leaderboard_message = "GERGAMES-SERVER WIRD VERBUNDEN – BITTE GLEICH ERNEUT LADEN"
queue_redraw() queue_redraw()
return 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_loading = false
leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else "" leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else ""
queue_redraw() queue_redraw()
@@ -977,6 +1026,12 @@ func _difficulty() -> Dictionary:
return DIFFICULTY_DATA[clampi(selected_difficulty, 0, DIFFICULTY_DATA.size() - 1)] 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: func _on_energy_reward_granted(amount: int) -> void:
_award_credits(0, amount) _award_credits(0, amount)
_show_status("NOTFALLENERGIE +%d" % 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 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 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) var preview_range := _tower_range(selected_tower_type, 1)
draw_circle(snapped_position, preview_range, Color(placement_color, 0.09)) draw_circle(snapped_position, preview_range, Color(placement_color, 0.025))
draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.82), 3.0) 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)), placement_color, true) 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.95), false, 3.0) 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}) _draw_tower({"pos": snapped_position, "type": selected_tower_type, "level": 1, "angle": 0.0})
for tower_index in towers.size(): for tower_index in towers.size():
var tower: Dictionary = towers[tower_index] var tower: Dictionary = towers[tower_index]
@@ -1472,8 +1527,8 @@ func _draw_route() -> void:
func _draw_build_grid() -> 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 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.08 if selected_tower_type >= 0 else 0.025) 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 x_index in int(_active_world_width() / GRID_SIZE):
for y_index in int(WORLD_SIZE.y / 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) 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 return
var data: Dictionary = TOWER_DATA[type] var data: Dictionary = TOWER_DATA[type]
var font := ThemeDB.fallback_font 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(0.008, 0.028, 0.055, 0.58), true)
draw_rect(tower_info_rect, Color("4a9ab2"), false, 2.0) 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 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(10, 20), heading, HORIZONTAL_ALIGNMENT_LEFT, 280, 13, 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, 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: if is_built and level < 3:
var next_level := level + 1 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: 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: 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: 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(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.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: 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_back_button, "< ZURÜCK", false, Color("26394a"))
_draw_button(leaderboard_refresh_button, "AKTUALISIEREN", leaderboard_loading, Color("24516a")) _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, 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_mission_button, "MISSION", not leaderboard_endless, Color("24475b"))
_draw_button(leaderboard_endless_button, "ENDLOSMODUS", leaderboard_endless, Color("5b452b")) _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(0.012, 0.035, 0.065, 0.96), true)
draw_rect(panel, Color("39788e"), false, 2.0) 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(235, 319), "RANG", HORIZONTAL_ALIGNMENT_LEFT, 75, 13, Color("91a8bc"))
draw_string(font, Vector2(410, 277), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 480, 14, Color("91a8bc")) draw_string(font, Vector2(325, 319), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 270, 13, Color("91a8bc"))
draw_string(font, Vector2(950, 277), "WELLE", HORIZONTAL_ALIGNMENT_CENTER, 130, 14, Color("91a8bc")) draw_string(font, Vector2(610, 319), "KARTE", HORIZONTAL_ALIGNMENT_LEFT, 240, 13, Color("91a8bc"))
draw_string(font, Vector2(1125, 277), "PUNKTE", HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, Color("91a8bc")) draw_string(font, Vector2(850, 319), "STUFE", HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("91a8bc"))
draw_line(Vector2(270, 293), Vector2(1330, 293), Color("315c70"), 1.0) 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 "" var own_user_id: String = str(online_manager.session.user_id) if online_manager.session != null else ""
for index in leaderboard_rows.size(): for index in leaderboard_rows.size():
var row: Dictionary = leaderboard_rows[index] var row: Dictionary = leaderboard_rows[index]
var y := 331.0 + index * 34.0 var y := 370.0 + index * 32.0
if index % 2 == 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 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(240, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 65, 14, 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(325, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 270, 14, 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(610, y), MAP_NAMES[int(row.get("map", leaderboard_map))], HORIZONTAL_ALIGNMENT_LEFT, 225, 13, Color("9fdce9"))
draw_string(font, Vector2(1125, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 16, Color("8fffea")) 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(): 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(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")) draw_string(font, Vector2(0, 810), "Spielernamen werden aus dem aktuellen öffentlichen Profil geladen.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("91a8bc"))
+5 -1
View File
@@ -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://main.gd" type="Script" id="1"]
[ext_resource path="res://ads_manager.gd" type="Script" id="2"] [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://account_manager.gd" type="Script" id="3"]
[ext_resource path="res://multiplayer_manager.gd" type="Script" id="4"] [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://online_manager.gd" type="Script" id="5"]
[ext_resource path="res://update_manager.gd" type="Script" id="6"]
[node name="VoidBastion" type="Node2D"] [node name="VoidBastion" type="Node2D"]
script = ExtResource("1") script = ExtResource("1")
@@ -20,3 +21,6 @@ script = ExtResource("4")
[node name="OnlineManager" type="Node" parent="."] [node name="OnlineManager" type="Node" parent="."]
script = ExtResource("5") script = ExtResource("5")
[node name="UpdateManager" type="Node" parent="."]
script = ExtResource("6")
+4 -1
View File
@@ -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)), "username": str(current_names.get(record.owner_id, record.username)),
"score": int(record.score), "score": int(record.score),
"wave": int(record.subscore), "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) leaderboard_loaded.emit(leaderboard_id, rows)
return rows return rows
+22 -3
View File
@@ -79,6 +79,12 @@ func _run() -> void:
game.call("_return_to_menu") game.call("_return_to_menu")
game.call("_handle_pointer", Vector2(400, 735), false) game.call("_handle_pointer", Vector2(400, 735), false)
assert(game.get("game_mode") == "leaderboard", "Die globale Bestenliste muss direkt aus dem Hauptmenü erreichbar sein.") 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) game.call("_handle_pointer", Vector2(140, 60), false)
assert(game.get("game_mode") == "menu", "Die Bestenliste benötigt einen funktionierenden Zurück-Button.") 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.")
@@ -107,8 +113,19 @@ func _run() -> void:
ads_manager.last_interstitial_time = 800 ads_manager.last_interstitial_time = 800
assert(not ads_manager.is_interstitial_due(1000), "Die 15-Minuten-Sperre zwischen Vollbildanzeigen muss greifen.") assert(not ads_manager.is_interstitial_due(1000), "Die 15-Minuten-Sperre zwischen Vollbildanzeigen muss greifen.")
ads_manager.last_interstitial_time = 0 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_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(_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("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.") 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.") 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) game.call("_select_map", 1)
assert(game.get("selected_map") == 1, "Die zweite Karte sollte auswählbar sein.") 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_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(_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(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.") 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(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.") assert(not game.call("_is_valid_build_position", Vector2(40, 680)), "Im freien Weltraum darf kein Bauplatz liegen.")
game.call("_select_map", 2) 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_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(_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(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.") assert(game.call("_is_in_reactor_zone", Vector2(680, 360)), "Auch die offene Pyrax-Reaktorkammer muss gesperrt sein.")
+94
View File
@@ -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