Files
Void-Bastion/tests/smoke_test.gd
T

398 lines
33 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends SceneTree
func _initialize() -> void:
call_deferred("_run")
func _path_length(points: PackedVector2Array) -> float:
var total := 0.0
for index in points.size() - 1:
total += points[index].distance_to(points[index + 1])
return total
func _assert_grid_aligned_path(points: PackedVector2Array, map_name: String) -> void:
for point in points:
assert(is_zero_approx(fposmod(point.x - 40.0, 80.0)), "%s: Jeder Wegpunkt muss horizontal im Zentrum einer Rasterzelle liegen." % map_name)
assert(is_zero_approx(fposmod(point.y - 40.0, 80.0)), "%s: Jeder Wegpunkt muss vertikal im Zentrum einer Rasterzelle liegen." % map_name)
for index in points.size() - 1:
var delta := points[index + 1] - points[index]
assert(is_zero_approx(delta.x) or is_zero_approx(delta.y), "%s: Jede Straßenstrecke muss waagerecht oder senkrecht durchs Raster verlaufen." % map_name)
func _run() -> void:
var packed_scene: PackedScene = load("res://main.tscn")
var game := packed_scene.instantiate()
root.add_child(game)
await process_frame
var ads_manager = game.get_node("AdsManager")
var account_manager = game.get_node("AccountManager")
var multiplayer_manager = game.get_node("MultiplayerManager")
game.set("core_crystals", 0)
game.call("_adjust_core_crystals", 25)
assert(game.get("core_crystals") == 25, "Der Entwicklermodus muss Testkristalle hinzufügen können.")
game.call("_adjust_core_crystals", -25)
game.call("_adjust_core_crystals", -25)
assert(game.get("core_crystals") == 0, "Der Entwicklermodus muss Kristalle abziehen können, ohne unter null zu fallen.")
assert(game.get("MAP_NAMES").size() == 7, "Es müssen sieben Karten vorhanden sein.")
assert(game.get("MAP_TEXTURES").size() == 7, "Jede Karte benötigt einen eigenen Hintergrund.")
assert(game.get("map_buttons").size() == 7, "Alle sieben Kartenvorschauen müssen gleichzeitig auswählbar sein.")
game.set("unlocked_maps", [0, 1, 2])
assert(game.get("TOWER_DATA").size() == 6, "Das Arsenal muss sechs unterschiedliche Turmtypen enthalten.")
assert(game.get("tower_buttons").size() == 6, "Alle sechs Türme müssen gleichzeitig in der unteren Baumenüleiste sichtbar sein.")
game.call("_handle_pointer", Vector2(760, 815), false)
assert(game.get("game_mode") == "store", "Der interne Shop muss direkt aus dem Hauptmenü erreichbar sein.")
await process_frame
game.set("core_crystals", 24)
game.call("_handle_pointer", game.get("store_map_tab_button").get_center(), false)
assert(game.get("store_tab") == "maps", "Der Shop muss eine eigene Kartenseite haben.")
game.call("_handle_pointer", game.get("map_shop_purchase_buttons")[0].get_center(), false)
assert(game.get("unlocked_maps").has(3) and game.get("core_crystals") == 0, "Neue Karten müssen dauerhaft mit Kernkristallen freischaltbar sein.")
game.set("unlocked_maps", [0, 1, 2])
game.set("core_crystals", 0)
game.call("_handle_pointer", Vector2(140, 60), false)
assert(game.get("game_mode") == "menu", "Der Shop benötigt einen funktionierenden Zurück-Button.")
game.call("_handle_pointer", game.get("map_buttons")[3].get_center(), false)
assert(game.get("game_mode") == "store" and game.get("store_tab") == "maps", "Eine gesperrte Karte muss direkt den Karten-Shop öffnen.")
game.set("core_crystals", 24)
game.call("_handle_pointer", game.get("map_shop_purchase_buttons")[0].get_center(), false)
assert(game.get("unlocked_maps").has(3), "Die erste zusätzliche Karte muss kaufbar sein.")
game.call("_handle_pointer", game.get("shop_back_button").get_center(), false)
assert(game.get("game_mode") == "menu", "Nach dem Kartenkauf muss die Kartenauswahl erreichbar bleiben.")
game.call("_handle_pointer", game.get("map_buttons")[3].get_center(), false)
assert(game.get("menu_step") == "difficulty" and game.get("selected_map") == 3, "Eine freigeschaltete Karte muss danach spielbar sein.")
game.call("_handle_pointer", game.get("map_step_back_button").get_center(), false)
game.set("unlocked_maps", [0, 1, 2])
game.set("core_crystals", 0)
game.call("_select_map", 0)
game.call("_handle_pointer", game.get("start_button").get_center(), false)
assert(game.get("game_mode") == "menu", "Die Mission darf erst nach Auswahl einer Karte gestartet werden.")
var menu_map_index: int = game.get("selected_map")
game.call("_handle_pointer", game.get("map_buttons")[menu_map_index].get_center(), false)
assert(game.get("menu_step") == "difficulty", "Nach Auswahl einer Karte muss die Schwierigkeitsauswahl erscheinen.")
game.call("_handle_pointer", game.get("map_step_back_button").get_center(), false)
assert(game.get("menu_step") == "map", "Die Missionsvorbereitung benötigt eine Rückkehr zur Kartenauswahl.")
game.set("unlocked_towers", [0, 1, 2])
game.set("game_mode", "playing")
game.call("_handle_pointer", game.get("tower_buttons")[3].get_center(), false)
assert(game.get("game_mode") == "store", "Ein gesperrter Turm muss den Shop auch aus einer laufenden Mission öffnen.")
game.call("_handle_pointer", game.get("shop_back_button").get_center(), false)
assert(game.get("game_mode") == "playing", "Der Shop-Zurück-Button muss in die laufende Mission zurückführen.")
game.set("game_mode", "menu")
multiplayer_manager.reset_coop_session(500, 30)
assert(multiplayer_manager.player_credits == [500, 500], "Beide Koop-Spieler benötigen getrennte, gleich hohe Startressourcen.")
assert(multiplayer_manager.spend_player_credits(0, 120), "Spieler 1 muss aus seinem eigenen Guthaben bauen können.")
assert(multiplayer_manager.player_credits[0] == 380 and multiplayer_manager.player_credits[1] == 500, "Ausgaben eines Spielers dürfen das Guthaben des Mitspielers nicht verändern.")
multiplayer_manager.damage_shared_reactor(4)
assert(multiplayer_manager.shared_reactor_lives == 26, "Beide Seiten müssen dasselbe Reaktorleben verwenden.")
var left_path := PackedVector2Array([Vector2(200, 300), Vector2(700, 400)])
var right_path: PackedVector2Array = multiplayer_manager.mirrored_path(left_path)
assert(right_path[0] == Vector2(1400, 300) and right_path[1] == Vector2(900, 400), "Die zweite Koop-Kartenhälfte muss exakt gespiegelt werden.")
multiplayer_manager.begin_search("quick")
assert(multiplayer_manager.matchmaking_state == "waiting_for_service", "Die Spielersuche benötigt einen sichtbaren vorbereiteten Zustand.")
multiplayer_manager.cancel_search()
assert(multiplayer_manager.matchmaking_state == "idle", "Eine laufende Spielersuche muss abgebrochen werden können.")
game.call("_handle_pointer", Vector2(500, 815), false)
assert(game.get("game_mode") == "multiplayer_menu", "Der Multiplayerbereich muss direkt aus dem Hauptmenü erreichbar sein.")
game.call("_handle_pointer", Vector2(400, 695), false)
assert(game.get("game_mode") == "matchmaking", "Schnelles Spiel muss in den vorbereiteten Suchzustand wechseln.")
game.call("_handle_pointer", Vector2(800, 680), false)
assert(game.get("game_mode") == "multiplayer_menu", "Die Spielersuche muss über die sichtbare Schaltfläche abgebrochen werden können.")
game.call("_handle_pointer", Vector2(140, 60), false)
assert(game.get("game_mode") == "menu", "Der Multiplayerbereich benötigt einen funktionierenden Zurück-Button.")
game.set("selected_difficulty", 0)
game.call("_handle_pointer", Vector2(500, 815), false)
game.call("_handle_pointer", Vector2(1300, 695), false)
assert(game.get("game_mode") == "playing" and game.get("coop_mode") and game.get("coop_ai_mode"), "Der KI-Koop-Modus muss direkt spielbar gestartet werden können.")
assert(game.get("coop_paths").size() == 2, "Die Koop-Arena benötigt zwei gespiegelte Gegnerwege.")
assert(game.get("coop_paths")[0][0].x == -40.0 and game.get("coop_paths")[1][0].x == 3000.0, "Die Gegner müssen von beiden äußeren Rändern des verdoppelten Spielfelds angreifen.")
assert(game.get("coop_paths")[0][-1] == game.get("coop_paths")[1][-1], "Beide gespiegelten Wege müssen am gemeinsamen Reaktor zusammentreffen.")
assert(game.get("towers").size() >= 2, "Der Computergegner muss selbstständig eine Startverteidigung bauen.")
for ai_tower in game.get("towers"):
assert(int(ai_tower.get("owner", -1)) == 1 and ai_tower.pos.x > 1480.0, "KI-Türme dürfen nur auf der vollständigen gespiegelten KI-Seite stehen.")
assert(game.call("_distance_to_route", ai_tower.pos, game.get("coop_paths")[1]) <= 200.0, "KI-Türme müssen innerhalb ihrer effektiven Reichweite am Gegnerweg stehen.")
assert(multiplayer_manager.player_credits[0] > multiplayer_manager.player_credits[1], "Die KI muss ihre Baukosten ausschließlich aus dem eigenen Guthaben bezahlen.")
assert(game.call("_is_valid_build_position", Vector2(440, 200), 0), "Der Spieler benötigt gültige Bauplätze auf der linken Kartenhälfte.")
assert(game.call("_is_valid_build_position", Vector2(1240, 200), 0), "Die eigene Koop-Seite muss nahezu die volle Breite einer Einzelspielerkarte bieten.")
assert(not game.call("_is_valid_build_position", Vector2(2040, 200), 0), "Der Spieler darf die gespiegelte Kartenhälfte der KI nicht bebauen.")
game.call("_toggle_coop_side")
assert(game.get("camera_offset").x == 1360.0, "Die Ansicht muss ohne Verkleinerung auf die vollständige KI-Seite umschalten können.")
game.call("_toggle_coop_side")
assert(game.get("camera_offset") == Vector2.ZERO, "Die Ansicht muss wieder zur eigenen vollständigen Kartenhälfte zurückkehren.")
game.call("_start_wave")
assert(game.get("enemies_to_spawn") == 18, "Im Koop-Modus muss die erste Welle beide Gegnerseiten versorgen.")
game.call("_spawn_enemy")
game.call("_spawn_enemy")
assert(game.get("enemies")[-2].route_id != game.get("enemies")[-1].route_id, "Gegner müssen abwechselnd auf beiden Koop-Routen erscheinen.")
for coop_tick in 100:
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(225, 815), 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, game.get("MAP_NAMES").size()), "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.")
assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
assert(game.call("_app_version") == "0.1.0-alpha.7", "Profil, Installer und Online-Übertragung müssen dieselbe Alpha-Version verwenden.")
var support_url: String = game.call("_support_email_url")
assert(support_url.begins_with("mailto:support@gergames.de?"), "Die Feedback-Schaltfläche muss die öffentliche Supportadresse verwenden.")
assert(support_url.contains("0.1.0-alpha.7") and support_url.contains(account_manager.short_id()), "Die Support-Mail muss Version und gekürzte Spieler-ID zur Fehlerzuordnung enthalten.")
account_manager.storage_enabled = false
var original_display_name: String = account_manager.display_name
assert(account_manager.validate_display_name("Kommandant Ägir").is_empty(), "Ein normaler internationaler Spielername muss gültig sein.")
assert(not account_manager.validate_display_name("ADMIN").is_empty(), "Reservierte Spielernamen müssen lokal abgewiesen werden.")
assert(not account_manager.validate_display_name("A!").is_empty(), "Zu kurze Namen mit Sonderzeichen müssen lokal abgewiesen werden.")
assert(account_manager.set_display_name(" Kommandant Ägir "), "Mehrfache Leerzeichen müssen beim Speichern normalisiert werden.")
assert(account_manager.display_name == "Kommandant Ägir", "Der sichtbare Spielername muss normalisiert gespeichert werden.")
account_manager.set_display_name(original_display_name)
var previous_test_score: int = account_manager.get_map_best_score(0, false)
assert(account_manager.record_run(0, 0, false, 3, previous_test_score + 1000), "Ein neuer lokaler Highscore muss gespeichert werden.")
assert(account_manager.get_map_best_wave(0, false) >= 3, "Die Profilansicht muss den gespeicherten Missionsbestwert lesen können.")
assert(not ads_manager.mobile_ads_supported, "Auf Windows müssen Anzeigen vollständig deaktiviert bleiben.")
var update_manager = game.get_node("UpdateManager")
assert(update_manager.call("_is_newer_version", "0.1.0-alpha.6", "0.1.0-alpha.5"), "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.6", "0.1.0-alpha.6"), "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.8", "name": "Alpha.8", "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.8", "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.")
assert(game.call("_is_path_cell", Vector2(600, 120)), "Eine Zelle auf der Route muss als Wegzelle erkannt werden.")
assert(not game.call("_is_path_cell", Vector2(600, 200)), "Die benachbarte Rasterreihe darf nicht zur ein Feld hohen Route gehören.")
assert(not game.call("_is_valid_build_position", Vector2(600, 120)), "Auf einer Wegzelle darf kein Turm gebaut werden.")
assert(game.call("_is_valid_build_position", Vector2(600, 200)), "Direkt neben der ein Feld hohen Route muss gebaut werden können.")
assert(game.call("_is_valid_build_position", Vector2(600, 280)), "Zwischen den beiden oberen Wegschleifen von Bastion Prime muss gebaut werden können.")
assert(game.call("_is_valid_build_position", Vector2(200, 280)), "Der linke Plattformbereich von Bastion Prime soll als Baufläche nutzbar 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)
assert(game.get("selected_map") == 1, "Die zweite Karte sollte auswählbar sein.")
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.")
assert(not game.call("_is_valid_build_position", Vector2(600, 440)), "Auf der mittleren Helios-Fahrbahn darf nicht gebaut werden.")
assert(game.call("_is_valid_build_position", Vector2(600, 520)), "Auf Helios muss auch die innere Plattform zwischen den unteren Fahrbahnen nutzbar sein.")
assert(not game.call("_is_valid_build_position", Vector2(600, 680)), "Auf der unteren Helios-Fahrbahn darf nicht gebaut werden.")
assert(game.call("_is_in_reactor_zone", Vector2(1480, 440)), "Der Helios-Reaktor muss als Sperrzone erkannt werden.")
assert(not game.call("_is_valid_build_position", Vector2(1480, 440)), "Auf dem Helios-Reaktor darf nicht gebaut werden.")
for map_index in game.get("MAP_NAMES").size():
game.call("_select_map", map_index)
var current_path: PackedVector2Array = game.get("path_points")
_assert_grid_aligned_path(current_path, str(game.get("MAP_NAMES")[map_index]))
assert(game.call("_validate_route_ends_at_reactor", current_path, map_index), "Jede neue Karte muss exakt am sichtbaren Reaktor enden.")
assert(current_path.size() >= 7, "Jede Karte braucht einen ausreichend langen, gestalteten Laufweg.")
assert(game.call("_is_in_reactor_zone", current_path[-1]), "Der Zielpunkt muss in der Reaktorsperrzone liegen.")
for cell in game.get("path_cells"):
var cell_position: Vector2i = cell
var neighbors := 0
for direction in [Vector2i.LEFT, Vector2i.RIGHT, Vector2i.UP, Vector2i.DOWN]:
if game.get("path_cells").has(cell_position + direction):
neighbors += 1
assert(neighbors <= 2, "%s hat an Rasterzelle %s eine Wegkreuzung." % [game.get("MAP_NAMES")[map_index], cell_position])
game.call("_select_map", 0)
assert(game.call("_is_in_reactor_zone", Vector2(1480, 440)), "Der Bastion-Reaktor muss als Sperrzone erkannt 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.")
game.call("_select_map", 2)
assert(game.get("path_points").size() == 7, "Pyrax sollte ohne die beiden unnötigen letzten Bögen in den Reaktor führen.")
_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")) >= 2300.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.")
assert(not game.call("_is_valid_build_position", Vector2(1440, 360)), "Auf dem Pyrax-Reaktor darf nicht gebaut werden.")
game.call("_select_map", 0)
assert(is_equal_approx(game.call("_tower_range", 0, 1), 190.0), "Die Bauvorschau muss die echte Turmreichweite verwenden.")
assert(game.call("_tower_damage", 0, 2) > game.call("_tower_damage", 0, 1), "Ein Upgrade muss den Schaden sichtbar erhöhen.")
assert(game.call("_tower_shots_per_second", 0, 2) > game.call("_tower_shots_per_second", 0, 1), "Ein Upgrade muss die Feuerrate sichtbar erhöhen.")
assert(is_equal_approx(game.call("_tower_aim_angle", 0, Vector2.ZERO, Vector2.RIGHT), PI), "Der Kinetik-Sprite muss für sein links liegendes Geschütz um 180 Grad korrigiert werden.")
assert(is_equal_approx(game.call("_tower_aim_angle", 1, Vector2.ZERO, Vector2.RIGHT), 0.0), "Andere Türme dürfen durch die Kinetik-Korrektur nicht gedreht werden.")
game.set("selected_difficulty", 0)
game.call("_start_game")
assert(game.get("credits") == 450 and game.get("reactor_lives") == 20, "Leicht muss dem bisherigen Balancing entsprechen.")
game.call("_handle_pointer", Vector2(1180, 45), false)
assert(game.get("speed_dropdown_open"), "Die Spielgeschwindigkeit muss über eine Auswahl bedienbar sein.")
game.call("_handle_pointer", Vector2(1190, 198), false)
assert(is_equal_approx(game.get("game_speed"), 5.0), "Die Geschwindigkeitsauswahl muss bis 5× reichen.")
game.set("game_speed", 1.0)
game.call("_handle_pointer", Vector2(1330, 82), false)
assert(game.get("auto_start_waves"), "Der automatische Wellenstart muss per Checkbox aktivierbar sein.")
game.call("_handle_pointer", Vector2(1330, 82), false)
assert(not game.get("auto_start_waves"), "Der automatische Wellenstart muss per Checkbox abschaltbar sein.")
game.call("_handle_pointer", Vector2(1110, 45), false)
assert(game.get("game_mode") == "menu", "Der Zurück-Button muss eine laufende Runde abbrechen und zum Kartenmenü führen.")
assert(game.get("towers").is_empty() and game.get("enemies").is_empty(), "Beim Abbruch müssen alle Einheiten der laufenden Runde entfernt werden.")
game.call("_start_game")
game.set("selected_tower_type", 0)
var build_position := Vector2.ZERO
for x in range(120, 1480, 80):
for y in range(120, 740, 80):
var candidate: Vector2 = game.call("_snap_to_grid", Vector2(x, y))
if game.call("_is_valid_build_position", candidate):
build_position = candidate
break
if build_position != Vector2.ZERO:
break
assert(build_position != Vector2.ZERO, "Auf der vollständig sichtbaren Karte muss es sinnvolle Bauplätze geben.")
game.call("_handle_pointer", build_position, true)
assert(game.get("towers").is_empty(), "Der erste Fingertipp soll nur die Reichweite zur Kontrolle anzeigen.")
game.call("_handle_pointer", build_position, true)
assert(game.get("towers").size() == 1, "Ein Turm sollte platziert worden sein.")
assert(game.get("towers")[0].pos == build_position, "Der Turm sollte im Bauraster einrasten.")
assert(game.get("credits") == 330, "Die Turmkosten müssen abgezogen werden.")
game.call("_start_wave")
assert(game.get("enemies_to_spawn") == 9, "Leicht muss die bisherige Gegnerzahl der ersten Welle beibehalten.")
for index in 100:
game.call("_process", 0.1)
assert(game.get("wave") == 1, "Die erste Welle sollte gestartet sein.")
assert(game.get("next_enemy_id") > 1, "Die Welle sollte Gegner erzeugen.")
game.set("selected_difficulty", 1)
game.call("_start_game")
game.call("_start_wave")
assert(game.get("enemies_to_spawn") > 9, "Mittel muss mehr Gegner als Leicht erzeugen.")
game.set("selected_difficulty", 2)
game.call("_start_game")
assert(game.get("credits") == 340 and game.get("reactor_lives") == 12, "Schwer muss mit knapperen Startressourcen und weniger Reaktorleben beginnen.")
assert(int(game.call("_difficulty").crystals) == 12, "Ein Sieg auf Schwer muss den höheren Kristallbonus gewähren.")
assert(game.get("ENEMY_DATA").size() == 9, "Drei zusätzliche Gegnerrollen müssen im Spiel verfügbar sein.")
assert(game.call("_enemy_category", 0) == "air" and game.call("_enemy_category", 6) == "ground", "Luft- und Bodeneinheiten müssen eindeutig klassifiziert sein.")
assert(not game.call("_tower_can_target", 0, 0) and game.call("_tower_can_target", 1, 0) and game.call("_tower_can_target", 1, 6) and game.call("_tower_can_target", 5, 6), "Kinetik muss auf Boden beschränkt sein; Laser und Railgun dürfen beide Kategorien angreifen.")
assert(game.call("_enemy_texture", 6).get_width() > 0 and game.call("_enemy_texture", 8).get_height() > 0, "Bodeneinheiten benötigen eigene lesbare Grafiken.")
assert(game.get("TOWER_DATA")[0].target_class == "ground" and game.get("TOWER_DATA")[1].target_class == "both", "Der Laser muss neben Luft- auch Bodenziele angreifen können.")
assert(game.get("TOWER_DATA")[1].both_damage_scale < 1.0 and game.get("TOWER_DATA")[3].both_damage_scale < 1.0 and game.get("TOWER_DATA")[5].both_damage_scale < 1.0, "Laser, Plasma und Railgun müssen für ihre Mehrzweckfähigkeit einen Schadensmultiplikator erhalten.")
assert(game.get("TOWER_DATA")[4].damage == 20.0 and game.get("TOWER_DATA")[4].target_class == "both", "EMP muss Luft- und Bodenziele mit rund 20 Schaden erfassen.")
assert(game.call("_tower_display_order") == [0, 2, 1, 3, 4, 5], "Die Turmauswahl muss zuerst Luft-, dann Boden- und anschließend Mehrzwecktürme gruppieren.")
assert(game.call("_tower_class_color", 0).g > game.call("_tower_class_color", 0).r and game.call("_tower_class_color", 1).b > game.call("_tower_class_color", 1).r, "Bodenfelder müssen grün und Mehrzweckfelder blau-cyan eingefärbt sein.")
game.get("enemies").clear()
var rail_front := {"id": 96001, "type": 0, "pos": Vector2(400, 300), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
var rail_behind := {"id": 96002, "type": 6, "pos": Vector2(500, 300), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
var rail_offline := {"id": 96003, "type": 0, "pos": Vector2(450, 326), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
game.get("enemies").append_array([rail_front, rail_behind, rail_offline])
game.call("_fire_tower", {"type": 5, "level": 1, "pos": Vector2(300, 300), "owner": 0}, rail_front)
assert(rail_front.hp < 1000.0 and rail_behind.hp < 1000.0 and is_equal_approx(rail_offline.hp, 1000.0), "Der Railgun-Strahl muss Gegner hinter dem Ziel auf der Linie durchschlagen, aber keine Ziele neben der Linie treffen.")
assert(game.get("beams")[-1].piercing and game.get("beams")[-1].color == Color("c86bff"), "Der durchschlagende Railgun-Strahl muss sichtbar lila sein.")
game.get("enemies").clear()
game.get("projectiles").clear()
var emp_air := {"id": 96101, "type": 0, "pos": Vector2(500, 500), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
var emp_ground := {"id": 96102, "type": 6, "pos": Vector2(550, 500), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
var emp_outside := {"id": 96103, "type": 6, "pos": Vector2(700, 500), "hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "shield": false, "reward": 1}
game.get("enemies").append_array([emp_air, emp_ground, emp_outside])
game.call("_fire_tower", {"type": 4, "level": 1, "pos": Vector2(400, 500), "owner": 0}, emp_air)
game.get("projectiles")[-1].speed = 0.0
game.get("projectiles")[-1].pos = emp_air.pos
game.call("_update_projectiles", 0.01)
assert(is_equal_approx(emp_air.hp, 980.0) and is_equal_approx(emp_ground.hp, 980.0) and is_equal_approx(emp_outside.hp, 1000.0), "Der EMP muss Luft und Boden im 118er-Radius jeweils um 20 treffen und Gegner außerhalb verschonen.")
game.get("enemies").clear()
game.set("path_points", PackedVector2Array([Vector2(0, 0), Vector2(1000, 0), Vector2(1600, 0)]))
game.get("enemies").clear()
var shield_target := {"id": 201, "type": 6, "pos": Vector2(850, 0), "path_index": 1, "hp": 100.0, "shield": true}
var unshielded_target := {"id": 202, "type": 6, "pos": Vector2(950, 0), "path_index": 1, "hp": 500.0, "shield": false}
game.get("enemies").append(shield_target)
game.get("enemies").append(unshielded_target)
assert(game.call("_find_target", {"type": 1, "level": 1, "pos": Vector2(800, 0)}).id == 201, "Lasertürme müssen unabhängig vom Fortschritt immer zuerst ein Schildziel in Reichweite wählen.")
assert(game.call("_find_target", {"type": 0, "level": 1, "pos": Vector2(800, 0), "target_priority": "strongest"}).id == 202, "Die Priorität MEISTE HP muss das Ziel mit den meisten Lebenspunkten wählen.")
assert(game.call("_find_target", {"type": 0, "level": 1, "pos": Vector2(800, 0), "target_priority": "last"}).id == 201, "Die Priorität LETZTE muss die Einheit mit dem geringsten Wegfortschritt wählen.")
game.get("towers").clear()
game.get("towers").append({"type": 0, "target_priority": "first"})
game.set("selected_tower_index", 0)
game.call("_cycle_selected_tower_priority")
assert(game.get("towers")[0].target_priority == "last", "Die Zielpriorität eines ausgewählten Turms muss umstellbar sein.")
game.set("selected_tower_index", -1)
game.get("towers").clear()
game.get("enemies").clear()
game.call("_select_map", 0)
assert(game.call("_enemy_type_for_wave", 3, 0.9, false) == 3, "Auf Schwer muss der Abfangjäger ab Welle 3 auftreten.")
assert(game.call("_enemy_type_for_wave", 5, 0.85, false) == 4, "Auf Schwer muss der Schildkreuzer ab Welle 5 auftreten.")
assert(game.call("_enemy_type_for_wave", 8, 0.95, false) == 5, "Auf Schwer muss der Belagerer ab Welle 8 auftreten.")
game.set("selected_difficulty", 1)
var medium_late_health: float = game.call("_enemy_health_scale", 12, false)
game.set("selected_difficulty", 2)
assert(game.call("_enemy_health_scale", 12, false) > medium_late_health, "Schwer muss ab der Missionsmitte stärker skalieren als Mittel.")
game.set("wave", 1)
game.set("enemies_to_spawn", 2)
game.call("_spawn_enemy")
assert(game.get("enemies")[-1].max_hp > 100.0, "Schwer muss die Gegnerhülle deutlich verstärken.")
assert(game.call("_enemy_health_scale", 4, false) > game.call("_enemy_health_scale", 3, false), "Die Gegnerhülle muss ab Welle 4 stärker skalieren.")
game.set("wave", 3)
game.call("_start_wave")
assert(game.get("enemies_to_spawn") > 15, "Ab Welle 4 muss die Gegnerzahl merklich steigen.")
var preview_types: Array[int] = game.call("_wave_preview_types", 3)
assert(preview_types.has(6) and preview_types.has(0), "Die Vorschau muss Luft- und Bodentypen der gleichen Welle zeigen.")
var preview_summary: Dictionary = game.call("_wave_preview_summary", 3)
assert(preview_summary.total == preview_types.size() and preview_summary.air + preview_summary.ground == preview_summary.total, "Die Vorschau muss die Gesamtzahl der Schiffe sowie die korrekten Luft-/Bodenanteile zählen.")
assert(game.get("wave_type_schedule").size() == game.get("enemies_to_spawn"), "Die Vorschau und der tatsächliche Spawnplan müssen übereinstimmen.")
var test_enemy := {"hp": 100.0, "max_hp": 100.0, "base_max_hp": 100.0, "type": 0, "flash": 0.0, "pos": Vector2.ZERO}
game.get("enemies").append(test_enemy)
game.call("_set_enemy_strength_multiplier", 1.6)
assert(is_equal_approx(test_enemy.max_hp, 160.0) and is_equal_approx(test_enemy.hp, 160.0), "Der Testmultiplikator muss die Hülle laufender Gegner anpassen.")
game.call("_set_enemy_strength_multiplier", 1.0)
game.get("enemies").clear()
var shielded_enemy := {"hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "id": 98001, "type": 0, "pos": Vector2.ZERO, "speed": 1.0, "reward": 1, "boss": false, "size_scale": 1.0, "shield": true, "shield_hp": 50.0, "max_shield_hp": 50.0, "shield_resistance": 0.28}
game.get("enemies").append(shielded_enemy)
game.call("_apply_damage", shielded_enemy, 100.0, 0, 0)
assert(is_equal_approx(shielded_enemy.hp, 972.0), "Schilde müssen kinetischen Schaden deutlich reduzieren, aber nicht vollständig blockieren.")
game.call("_apply_damage", shielded_enemy, 30.0, 0, 1)
assert(is_equal_approx(shielded_enemy.hp, 972.0) and is_equal_approx(shielded_enemy.shield_hp, 20.0), "Laser müssen das Schildleben statt der Hülle abbauen.")
game.call("_apply_damage", shielded_enemy, 30.0, 0, 1)
assert(not shielded_enemy.shield and is_equal_approx(shielded_enemy.hp, 962.0), "Der Laser muss beim Schildbruch überschüssigen Schaden auf die Hülle übertragen.")
game.get("enemies").clear()
game.set("wave", 5)
game.set("reactor_lives", 12)
game.set("credits", 300)
game.set("towers_built_this_run", 4)
var lean_score: int = game.call("_calculate_run_score")
game.set("towers_built_this_run", 14)
assert(game.call("_calculate_run_score") < lean_score, "Zusätzliche Bauvorgänge müssen den Effizienz-Multiplikator senken, auch wenn Türme später verkauft werden.")
game.set("towers_built_this_run", 4)
game.set("credits", 500)
assert(game.call("_calculate_run_score") > lean_score, "Verbleibende Energie muss die Highscore-Punkte erhöhen.")
game.set("selected_difficulty", 0)
game.call("_start_game")
game.set("wave", 4)
game.call("_start_wave")
game.set("enemies_to_spawn", 1)
game.set("wave_spawn_index", 0)
game.set("wave_type_schedule", game.call("_build_wave_schedule", 5, 1))
game.call("_spawn_enemy")
var boss: Dictionary = game.get("enemies")[-1]
assert(boss.boss, "Jede fünfte Welle sollte ein Kommandoschiff erzeugen.")
assert(boss.type == 5, "Das Kommandoschiff sollte die schwere Belagerer-Klasse verwenden.")
game.call("_start_game")
var crystals_before: int = game.get("core_crystals")
game.set("wave", 15)
game.set("wave_active", true)
game.set("enemies_to_spawn", 0)
game.get("enemies").clear()
game.call("_update_wave", 0.1)
assert(game.get("game_mode") == "endless_offer", "Nach Welle 15 muss die Wahl zum Endlosmodus erscheinen.")
assert(game.get("core_crystals") == crystals_before + 5, "Eine abgeschlossene Karte muss Kernkristalle vergeben.")
assert(not game.get("victory_reward_pending"), "Unter Windows ohne Anzeigen muss die Missionsbelohnung nicht blockiert werden.")
game.call("_mission_complete")
game.call("_handle_pointer", game.get("start_button").get_center(), false)
assert(game.get("game_mode") == "menu", "Der Abschluss einer 15-Wellen-Mission muss aus dem Siegbildschirm zurück ins Hauptmenü führen, nicht neu starten.")
game.call("_start_endless_mode")
assert(game.get("endless_mode") and game.get("game_mode") == "playing", "Der Endlosmodus muss freiwillig startbar sein.")
game.call("_game_over")
assert(game.get("game_mode") == "endless_result", "Das Ende des Endlosmodus darf nicht als gescheiterte Mission erscheinen.")
assert(game.get("core_crystals") == crystals_before + 5, "Der Kartenbonus darf pro Mission nur einmal vergeben werden.")
print("VOID_BASTION_SMOKE_TEST_OK")
quit()