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") 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.5", "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.5") 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.5", "0.1.0-alpha.4"), "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.5", "0.1.0-alpha.5"), "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.6", "name": "Alpha.6", "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.6", "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() == 6, "Drei zusätzliche Gegnerrollen müssen im Spiel verfügbar sein.") 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.") 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} 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, 100.0, 0, 1) assert(is_equal_approx(shielded_enemy.hp, 872.0), "Laser müssen Schildgegner effektiv beschädigen.") 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.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()