extends Node2D const SCREEN := Vector2(1600.0, 900.0) const WORLD_SCALE := 1.0 const WORLD_SIZE := SCREEN const SAVE_PATH := "user://void_bastion_save.json" const MAX_WAVES := 15 const BOSS_WAVE_INTERVAL := 5 const PLAYFIELD_TOP := 92.0 const PLAYFIELD_BOTTOM := 758.0 const GRID_SIZE := 80.0 const TOWER_TEXTURE := preload("res://assets/defense_towers.png") const PLASMA_TEXTURE := preload("res://assets/tower_plasma.png") const EMP_TEXTURE := preload("res://assets/tower_emp.png") const RAILGUN_TEXTURE := preload("res://assets/tower_railgun.png") const ENEMY_TEXTURE := preload("res://assets/enemy_ships.png") const ENEMY_INTERCEPTOR_TEXTURE := preload("res://assets/enemy_interceptor.png") const ENEMY_SHIELD_CARRIER_TEXTURE := preload("res://assets/enemy_shield_carrier.png") const ENEMY_SIEGE_DESTROYER_TEXTURE := preload("res://assets/enemy_siege_destroyer.png") const ROAD_STRAIGHT_TEXTURE := preload("res://assets/road_straight.svg") const ROAD_CORNER_TEXTURE := preload("res://assets/road_corner.svg") const ROAD_SURFACE_TEXTURE := preload("res://assets/road_surface_material_v1.png") const ROAD_CORNER_SURFACE_TEXTURE := preload("res://assets/road_corner_surface_v1.png") const MAP_TEXTURES := [ preload("res://assets/orbital_platform_grid_base.png"), preload("res://assets/orbital_shipyard_grid_base.png"), preload("res://assets/asteroid_refinery_grid_base.png"), preload("res://assets/orbital_biodome_bg.png"), preload("res://assets/cryo_refinery_bg.png"), preload("res://assets/solar_forge_bg.png"), preload("res://assets/vestige_megastructure_bg.png") ] const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE", "NOVA-ARKOLOGIE", "CRYO-SCHLEUSE", "SOLARFORGE", "VESTIGE-9"] const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring", "Biodom über dem Nebel", "Eisige Raffinerie", "Schmiede am Gasriesen", "Verlassene Megastruktur"] const MAP_UNLOCK_COSTS := [0, 0, 0, 24, 32, 40, 48] 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(1480, 520), Vector2(1480, 440)], [Vector2(-40, 280), Vector2(1320, 280), Vector2(1320, 440), Vector2(280, 440), Vector2(280, 680), Vector2(1560, 680), Vector2(1560, 440), Vector2(1480, 440)], [Vector2(-40, 120), Vector2(360, 120), Vector2(360, 600), Vector2(1320, 600), Vector2(1320, 280), Vector2(1480, 280), Vector2(1480, 360)], [Vector2(-40, 120), Vector2(1240, 120), Vector2(1240, 280), Vector2(280, 280), Vector2(280, 520), Vector2(1480, 520), Vector2(1480, 440)], [Vector2(-40, 680), Vector2(1000, 680), Vector2(1000, 520), Vector2(200, 520), Vector2(200, 360), Vector2(1400, 360), Vector2(1480, 360), Vector2(1480, 440)], [Vector2(-40, 120), Vector2(1400, 120), Vector2(1400, 280), Vector2(200, 280), Vector2(200, 600), Vector2(1480, 600), Vector2(1480, 440)], [Vector2(-40, 360), Vector2(200, 360), Vector2(200, 120), Vector2(1400, 120), Vector2(1400, 280), Vector2(440, 280), Vector2(440, 600), Vector2(1240, 600), Vector2(1240, 520), Vector2(1480, 520), Vector2(1480, 440)] ] const MAP_BUILD_ZONES := [ [Rect2(155, 95, 1380, 660)], [Rect2(120, 95, 1380, 660)], [Rect2(180, 110, 1270, 645)], [Rect2(40, 96, 1520, 664)], [Rect2(40, 96, 1520, 664)], [Rect2(40, 96, 1520, 664)], [Rect2(40, 96, 1520, 664)] ] const MAP_REACTOR_ZONES := [ [{"center": Vector2(1480, 440), "radius": 190.0}], [{"center": Vector2(1480, 440), "radius": 185.0}], [ {"center": Vector2(1480, 360), "radius": 165.0}, {"center": Vector2(690, 385), "radius": 155.0} ], [{"center": Vector2(1480, 440), "radius": 185.0}], [{"center": Vector2(1480, 440), "radius": 185.0}], [{"center": Vector2(1480, 440), "radius": 185.0}], [{"center": Vector2(1480, 440), "radius": 185.0}] ] const COOP_MIRROR_X := 1480.0 const COOP_WORLD_WIDTH := COOP_MIRROR_X * 2.0 const COOP_LEFT_PATH := [Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1400, 680), Vector2(1400, 520), Vector2(1480, 520), Vector2(1480, 440)] const COOP_REACTOR_CENTER := Vector2(COOP_MIRROR_X, 440) const COOP_REACTOR_RADIUS := 112.0 const DIFFICULTY_DATA := [ {"name": "LEICHT", "hp": 1.0, "speed": 1.0, "count": 1.0, "income": 1.0, "spawn_delay": 1.0, "start_credits": 450, "lives": 20, "crystals": 5}, {"name": "MITTEL", "hp": 1.32, "speed": 1.07, "count": 1.24, "income": 0.94, "spawn_delay": 0.91, "start_credits": 430, "lives": 17, "crystals": 7}, {"name": "SCHWER", "hp": 1.72, "speed": 1.15, "count": 1.48, "income": 0.78, "spawn_delay": 0.82, "start_credits": 340, "lives": 12, "crystals": 12} ] const DIFFICULTY_DESCRIPTIONS := [ "Bisheriges Balancing • 20 Reaktor • Kartenbonus 5", "+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7", "+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12" ] const TOWER_DATA := [ {"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässiges Projektilgeschütz."}, {"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "color": Color("62fff1"), "unlock_cost": 0, "description": "Schneller Strahl, stark gegen Schilde."}, {"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Explosionsschaden gegen Gruppen."}, {"name": "PLASMA", "cost": 230, "damage": 48.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "color": Color("e27bff"), "unlock_cost": 18, "description": "Plasmaexplosion trifft Gegner im Umkreis."}, {"name": "EMP-PULS", "cost": 210, "damage": 34.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118", "color": Color("77e8ff"), "unlock_cost": 24, "description": "Breiter Impuls trifft ganze Gegnergruppen."}, {"name": "RAILGUN", "cost": 300, "damage": 110.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "SCHILD-DURCHSCHLAG", "color": Color("87efff"), "unlock_cost": 30, "description": "Weitreichender Schuss, ignoriert Schildschutz."} ] const ENEMY_DATA := [ {"name": "JAEGER", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28}, {"name": "FREGATTE", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28}, {"name": "KREUZER", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28}, {"name": "ABFANGJAEGER", "hp": 52.0, "speed": 178.0, "reward": 22, "size": 48.0, "breach_damage": 2, "shield_resistance": 0.28}, {"name": "SCHILDKREUZER", "hp": 260.0, "speed": 58.0, "reward": 52, "size": 92.0, "breach_damage": 2, "shield_resistance": 0.18, "default_shield": true}, {"name": "BELAGERER", "hp": 740.0, "speed": 48.0, "reward": 108, "size": 128.0, "breach_damage": 4, "shield_resistance": 0.28} ] var path_points := PackedVector2Array() var path_cells: Dictionary = {} var build_zones: Array[Rect2] = [] var selected_map := 0 var selected_difficulty := 1 var camera_offset := Vector2.ZERO var game_mode := "menu" var menu_step := "map" var store_return_mode := "menu" var credits := 450 var reactor_lives := 20 var wave := 0 var best_wave := 0 var best_endless_wave := 0 var core_crystals := 0 var unlocked_towers := [0, 1, 2] var unlocked_maps := [0, 1, 2] var store_tab := "towers" var tutorial_completed := false var tutorial_step := 0 var game_speed := 1.0 const SPEED_OPTIONS := [1.0, 2.0, 3.0, 4.0, 5.0] var speed_dropdown_open := false var auto_start_waves := false var auto_wave_countdown := 0.0 var towers_built_this_run := 0 var cursor_position := Vector2(-1000, -1000) var cursor_screen_position := Vector2(-1000, -1000) var selected_tower_type := -1 var selected_tower_index := -1 var pending_touch_build_position := Vector2(-1, -1) var towers: Array[Dictionary] = [] var enemies: Array[Dictionary] = [] var projectiles: Array[Dictionary] = [] var beams: Array[Dictionary] = [] var particles: Array[Dictionary] = [] var next_enemy_id := 1 var wave_active := false var enemies_to_spawn := 0 var spawn_timer := 0.0 var status_text := "" var status_timer := 0.0 var elapsed := 0.0 var endless_mode := false var mission_reward_granted := false var last_mission_reward := 0 var victory_reward_pending := false var coop_mode := false var coop_ai_mode := false var coop_online_mode := false var local_player_slot := 0 var coop_paths: Array[PackedVector2Array] = [] var next_spawn_route := 0 var start_button := Rect2(625, 674, 350, 58) var map_buttons := [ Rect2(30, 390, 340, 130), Rect2(430, 390, 340, 130), Rect2(830, 390, 340, 130), Rect2(1230, 390, 340, 130), Rect2(255, 540, 340, 130), Rect2(630, 540, 340, 130), Rect2(1005, 540, 340, 130) ] var difficulty_buttons := [Rect2(535, 520, 165, 50), Rect2(718, 520, 165, 50), Rect2(901, 520, 165, 50)] var wave_button := Rect2(1320, 18, 255, 54) var pause_button := Rect2(1242, 18, 62, 54) var speed_button := Rect2(1170, 18, 62, 54) var auto_wave_button := Rect2(1320, 73, 255, 22) var back_button := Rect2(1070, 18, 90, 54) var tower_buttons := [ Rect2(15, 768, 255, 122), Rect2(278, 768, 255, 122), Rect2(541, 768, 255, 122), Rect2(804, 768, 255, 122), Rect2(1067, 768, 255, 122), Rect2(1330, 768, 255, 122) ] var upgrade_button := Rect2(1275, 187, 145, 44) var sell_button := Rect2(1430, 187, 145, 44) var tutorial_skip_button := Rect2(365, 205, 145, 30) var reward_ad_button := Rect2(1070, 690, 300, 54) var tower_info_rect := Rect2(1275, 103, 300, 76) var endless_button := Rect2(545, 515, 510, 70) var finish_mission_button := Rect2(625, 610, 350, 62) var profile_button := Rect2(1280, 25, 270, 52) var leaderboard_button := Rect2(100, 790, 250, 54) var shop_button := Rect2(640, 790, 250, 54) var map_step_back_button := Rect2(55, 320, 170, 50) var shop_back_button := Rect2(55, 35, 185, 54) var shop_purchase_buttons := [Rect2(190, 545, 360, 62), Rect2(620, 545, 360, 62), Rect2(1050, 545, 360, 62)] var map_shop_purchase_buttons := [Rect2(70, 565, 350, 62), Rect2(460, 565, 350, 62), Rect2(850, 565, 350, 62), Rect2(1240, 565, 350, 62)] var store_tower_tab_button := Rect2(590, 174, 200, 42) var store_map_tab_button := Rect2(810, 174, 200, 42) var leaderboard_back_button := Rect2(55, 35, 185, 54) var leaderboard_mission_button := Rect2(510, 212, 270, 54) var leaderboard_endless_button := Rect2(820, 212, 270, 54) var leaderboard_refresh_button := Rect2(1265, 35, 280, 54) var leaderboard_map_previous_button := Rect2(235, 150, 58, 50) var leaderboard_map_button := Rect2(303, 150, 410, 50) var leaderboard_map_next_button := Rect2(723, 150, 58, 50) var leaderboard_difficulty_previous_button := Rect2(819, 150, 58, 50) var leaderboard_difficulty_button := Rect2(887, 150, 300, 50) var leaderboard_difficulty_next_button := Rect2(1197, 150, 58, 50) var leaderboard_rows: Array = [] var leaderboard_endless := false var leaderboard_map := 0 var leaderboard_difficulty := 1 var leaderboard_loading := false var leaderboard_message := "" var profile_back_button := Rect2(55, 35, 185, 54) var multiplayer_button := Rect2(370, 790, 250, 54) var multiplayer_back_button := Rect2(55, 35, 185, 54) var quick_match_button := Rect2(125, 665, 300, 64) var friend_match_button := Rect2(475, 665, 300, 64) var private_match_button := Rect2(825, 665, 300, 64) var ai_match_button := Rect2(1175, 665, 300, 64) var cancel_search_button := Rect2(625, 650, 350, 64) var lobby_ready_button := Rect2(625, 580, 350, 58) var room_code_input_rect := Rect2(510, 340, 580, 90) var join_room_button := Rect2(625, 525, 350, 64) var coop_side_button := Rect2(920, 18, 140, 54) var room_code_input := "" var profile_name_input := "" var profile_message := "" var profile_delete_armed := false var profile_rename_button := Rect2(220, 305, 250, 48) var profile_support_button := Rect2(570, 685, 250, 48) var profile_privacy_button := Rect2(840, 685, 250, 48) var profile_delete_button := Rect2(1110, 685, 250, 48) var profile_name_input_rect := Rect2(460, 330, 680, 82) var profile_name_save_button := Rect2(625, 470, 350, 62) var update_button := Rect2(30, 25, 390, 54) var update_dismiss_button := Rect2(430, 25, 54, 54) var available_update: Dictionary = {} var update_notice_visible := false var audio_settings_open := false var master_volume := 0.65 var audio_settings_button := Rect2(910, 790, 250, 54) var exit_button := Rect2(1180, 790, 250, 54) var audio_panel_rect := Rect2(450, 325, 700, 270) var audio_close_button := Rect2(1010, 345, 110, 44) var audio_slider_rect := Rect2(565, 460, 520, 32) @onready var ads_manager = $AdsManager @onready var account_manager = $AccountManager @onready var multiplayer_manager = $MultiplayerManager @onready var online_manager = $OnlineManager @onready var update_manager = $UpdateManager @onready var music_player: AudioStreamPlayer = $MusicPlayer @onready var ui_sound_player: AudioStreamPlayer = $UiSoundPlayer func _ready() -> void: _select_map(selected_map) _load_progress() _load_audio_settings() # The Android AudioTrack backend currently crashes on this generated PCM stream # on the supported emulator/device test target. Keep the game playable there # without starting native audio until we replace it with a packaged audio asset. if not OS.has_feature("android"): music_player.stream = _build_ambient_stream() music_player.play() ui_sound_player.stream = _build_ui_click_stream() ads_manager.energy_reward_granted.connect(_on_energy_reward_granted) ads_manager.ad_unavailable.connect(_show_status) ads_manager.victory_interstitial_finished.connect(_on_victory_interstitial_finished) multiplayer_manager.online_game_ready.connect(_start_coop_online_game) multiplayer_manager.online_snapshot_updated.connect(_apply_online_snapshot) multiplayer_manager.online_event_received.connect(_handle_online_event) update_manager.update_available.connect(_on_update_available) queue_redraw() func _process(delta: float) -> void: elapsed += delta if status_timer > 0.0: status_timer -= delta if game_mode == "playing": if auto_wave_countdown > 0.0: auto_wave_countdown -= delta if auto_wave_countdown <= 0.0 and auto_start_waves and not wave_active and (endless_mode or wave < MAX_WAVES): _start_wave() if coop_online_mode: _update_online_interpolation(delta) _update_effects(delta) else: var simulation_delta := delta * game_speed _update_wave(simulation_delta) _update_enemies(simulation_delta) _update_towers(simulation_delta) _update_projectiles(simulation_delta) _update_effects(simulation_delta) queue_redraw() func _update_wave(delta: float) -> void: if not wave_active: return if enemies_to_spawn > 0: spawn_timer -= delta if spawn_timer <= 0.0: _spawn_enemy() enemies_to_spawn -= 1 spawn_timer = maxf(0.24, (0.82 - wave * 0.025) * float(_difficulty().spawn_delay)) elif enemies.is_empty(): wave_active = false if wave >= MAX_WAVES and not endless_mode: _offer_endless_mode() return var bonus := int((55 + wave * 8) * float(_difficulty().income)) _award_credits(0, bonus) if coop_mode: _award_credits(1, bonus) _show_status("WELLE %d ABGEWEHRT +%d" % [wave, bonus]) if wave > best_wave: best_wave = wave _save_progress() if auto_start_waves and (wave < MAX_WAVES or endless_mode) and not coop_online_mode: auto_wave_countdown = 1.4 _show_status("WELLE ABGEWEHRT • NÄCHSTE WELLE IN 1,4 S") func _spawn_enemy() -> void: var is_boss := wave > 0 and wave % BOSS_WAVE_INTERVAL == 0 and enemies_to_spawn <= (2 if coop_mode else 1) var type := _enemy_type_for_wave(wave, randf(), is_boss) var data: Dictionary = ENEMY_DATA[type] var health_scale := _enemy_health_scale(wave, is_boss) var route_id := 0 var route := path_points if coop_mode: route_id = next_spawn_route next_spawn_route = (next_spawn_route + 1) % coop_paths.size() route = coop_paths[route_id] enemies.append({ "id": next_enemy_id, "type": type, "pos": route[0], "path_index": 1, "route_id": route_id, "hp": data.hp * health_scale, "max_hp": data.hp * health_scale, "speed": data.speed * (1.0 + wave * 0.012) * (0.82 if is_boss else 1.0) * float(_difficulty().speed), "reward": int((data.reward + wave) * (4 if is_boss else 1) * float(_difficulty().income)), "angle": 0.0, "flash": 0.0, "boss": is_boss, "size_scale": 1.34 if is_boss else 1.0, "shield": bool(data.get("default_shield", false)) or (wave >= 4 and randf() < minf(0.42, 0.10 + wave * 0.018 + selected_difficulty * 0.06)), "shield_resistance": float(data.shield_resistance) }) if is_boss: _show_status("WARNUNG: KOMMANDOSCHIFF IM ANFLUG") next_enemy_id += 1 func _enemy_type_for_wave(for_wave: int, roll: float, is_boss: bool) -> int: if is_boss: return 5 var advanced_offset := 2 if selected_difficulty == 2 else (1 if selected_difficulty == 1 else 0) if for_wave >= 10 - advanced_offset and roll >= 0.94: return 5 if for_wave >= 7 - advanced_offset and roll >= 0.80: return 4 if for_wave >= 4 - advanced_offset and roll >= 0.65: return 3 if for_wave >= 7 and roll >= 0.45: return 2 if for_wave >= 3 and roll >= 0.28: return 1 return 0 func _enemy_health_scale(for_wave: int, is_boss: bool) -> float: var early_ramp := maxf(0.0, for_wave - 1.0) * 0.20 var post_four_ramp := maxf(0.0, for_wave - 3.0) * 0.09 var hard_late_ramp := maxf(0.0, for_wave - 5.0) * 0.055 if selected_difficulty == 2 else 0.0 var endless_ramp := maxf(0.0, for_wave - MAX_WAVES) * 0.035 if endless_mode else 0.0 return (1.0 + early_ramp + post_four_ramp + hard_late_ramp + endless_ramp) * (3.25 if is_boss else 1.0) * float(_difficulty().hp) func _update_enemies(delta: float) -> void: for enemy in enemies.duplicate(): if enemy.flash > 0.0: enemy.flash -= delta var route: PackedVector2Array = _enemy_route(enemy) var target: Vector2 = route[enemy.path_index] var direction: Vector2 = enemy.pos.direction_to(target) enemy.angle = direction.angle() enemy.pos += direction * enemy.speed * delta if enemy.pos.distance_to(target) < 9.0: enemy.path_index += 1 if enemy.path_index >= route.size(): enemies.erase(enemy) var enemy_data: Dictionary = ENEMY_DATA[int(enemy.type)] reactor_lives -= int(enemy_data.breach_damage) + (4 if enemy.boss else 0) if coop_mode: multiplayer_manager.shared_reactor_lives = maxi(0, reactor_lives) _spawn_burst(route[-1], Color("54dfff"), 16) if reactor_lives <= 0: _game_over() func _update_towers(delta: float) -> void: for tower in towers: tower.cooldown -= delta var target := _find_target(tower) if target.is_empty(): continue tower.angle = _tower_aim_angle(tower.type, tower.pos, target.pos) if tower.cooldown <= 0.0: _fire_tower(tower, target) tower.cooldown = _tower_cooldown(tower.type, tower.level) func _find_target(tower: Dictionary) -> Dictionary: var best: Dictionary = {} var best_progress := -1.0 var effective_range := _tower_range(tower.type, tower.level) for enemy in enemies: if tower.pos.distance_to(enemy.pos) <= effective_range: var route: PackedVector2Array = _enemy_route(enemy) var progress: float = enemy.path_index * 1000.0 - enemy.pos.distance_to(route[enemy.path_index]) if progress > best_progress: best_progress = progress best = enemy return best func _fire_tower(tower: Dictionary, target: Dictionary) -> void: var data: Dictionary = TOWER_DATA[tower.type] var damage := _tower_damage(tower.type, tower.level) if tower.type == 1: _apply_damage(target, damage, int(tower.get("owner", 0)), tower.type) beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.09, "color": data.color}) else: projectiles.append({ "pos": tower.pos, "target_id": target.id, "damage": damage, "speed": data.shot_speed, "type": tower.type, "owner": int(tower.get("owner", 0)), "color": data.color }) func _update_projectiles(delta: float) -> void: for shot in projectiles.duplicate(): var target := _enemy_by_id(shot.target_id) if target.is_empty(): projectiles.erase(shot) continue var direction: Vector2 = shot.pos.direction_to(target.pos) shot.pos += direction * shot.speed * delta if shot.pos.distance_to(target.pos) < 14.0: if shot.type in [2, 3, 4]: var blast_radius := 76.0 if shot.type == 2 else (92.0 if shot.type == 3 else 118.0) var splash_factor := 0.45 if shot.type == 2 else (0.62 if shot.type == 3 else 0.38) for enemy in enemies.duplicate(): if enemy.pos.distance_to(target.pos) < blast_radius: _apply_damage(enemy, shot.damage if enemy.id == target.id else shot.damage * splash_factor, int(shot.get("owner", 0)), int(shot.type)) var blast_color: Color = Color("ff9b4a") if shot.type == 2 else (Color("db65ff") if shot.type == 3 else Color("71eaff")) _spawn_burst(target.pos, blast_color, 14 if shot.type == 2 else 18) else: _apply_damage(target, shot.damage, int(shot.get("owner", 0)), int(shot.type)) _spawn_burst(target.pos, shot.color, 4) projectiles.erase(shot) func _apply_damage(enemy: Dictionary, amount: float, owner := 0, weapon_type := -1) -> void: if not enemies.has(enemy): return var applied_damage := amount if bool(enemy.get("shield", false)) and weapon_type not in [1, 5]: applied_damage *= float(enemy.get("shield_resistance", 0.28)) enemy.hp -= applied_damage enemy.flash = 0.06 if enemy.hp <= 0.0: _award_credits(owner, int(enemy.reward)) _spawn_burst(enemy.pos, Color("ff6b3d"), 11 + enemy.type * 4) enemies.erase(enemy) func _enemy_route(enemy: Dictionary) -> PackedVector2Array: if coop_mode and not coop_paths.is_empty(): return coop_paths[clampi(int(enemy.get("route_id", 0)), 0, coop_paths.size() - 1)] return path_points func _enemy_by_id(id: int) -> Dictionary: for enemy in enemies: if enemy.id == id: return enemy return {} func _spawn_burst(position: Vector2, color: Color, count: int) -> void: for index in count: var angle := randf() * TAU var speed := randf_range(45.0, 180.0) particles.append({"pos": position, "vel": Vector2.from_angle(angle) * speed, "ttl": randf_range(0.22, 0.55), "color": color}) func _update_effects(delta: float) -> void: for beam in beams.duplicate(): beam.ttl -= delta if beam.ttl <= 0.0: beams.erase(beam) for particle in particles.duplicate(): particle.pos += particle.vel * delta particle.vel *= 0.94 particle.ttl -= delta if particle.ttl <= 0.0: particles.erase(particle) func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_ESCAPE, KEY_BACK, KEY_Q]: if audio_settings_open: audio_settings_open = false _save_audio_settings() queue_redraw() return if game_mode == "menu": get_tree().quit() return if audio_settings_open and event is InputEventMouseMotion and (event.button_mask & MOUSE_BUTTON_MASK_LEFT) != 0: _set_master_volume((event.position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x) return if audio_settings_open and event is InputEventScreenDrag: _set_master_volume((event.position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x) return if audio_settings_open and ((event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed) or (event is InputEventScreenTouch and not event.pressed)): _save_audio_settings() return if game_mode == "profile_name_entry" and event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_ESCAPE: _hide_virtual_keyboard() game_mode = "profile" elif event.keycode == KEY_BACKSPACE: profile_name_input = profile_name_input.left(maxi(0, profile_name_input.length() - 1)) elif event.keycode == KEY_ENTER: _save_profile_name() elif event.unicode > 0 and profile_name_input.length() < AccountManager.MAX_DISPLAY_NAME_LENGTH: var character := char(event.unicode) var allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-ÄÖÜäöüß" if allowed.contains(character): profile_name_input += character queue_redraw() return if game_mode == "room_code_entry" and event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_ESCAPE: _hide_virtual_keyboard() game_mode = "multiplayer_menu" elif event.keycode == KEY_BACKSPACE: room_code_input = room_code_input.left(maxi(0, room_code_input.length() - 1)) elif event.keycode == KEY_ENTER: _join_entered_room() elif event.unicode > 0 and room_code_input.length() < 6: var character := char(event.unicode).to_upper() if "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".contains(character): room_code_input += character queue_redraw() return if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_P, KEY_ESCAPE] and game_mode in ["playing", "paused"]: _toggle_pause() return if event is InputEventScreenTouch and event.pressed: cursor_screen_position = event.position cursor_position = event.position + camera_offset _handle_pointer(event.position, true) elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed: cursor_screen_position = event.position cursor_position = event.position + camera_offset _handle_pointer(event.position, false) elif event is InputEventMouseMotion: cursor_screen_position = event.position cursor_position = event.position + camera_offset func _handle_pointer(position: Vector2, is_touch := false) -> void: _play_ui_sound() if audio_settings_open: if audio_close_button.has_point(position) or not audio_panel_rect.has_point(position): audio_settings_open = false _save_audio_settings() elif audio_slider_rect.grow(12.0).has_point(position): _set_master_volume((position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x) _save_audio_settings() queue_redraw() return if game_mode == "menu": if update_notice_visible and update_button.has_point(position): OS.shell_open(str(available_update.get("url", "https://void-bastion.gergames.de/"))) return if update_notice_visible and update_dismiss_button.has_point(position): update_notice_visible = false queue_redraw() return if profile_button.has_point(position): game_mode = "profile" queue_redraw() return if audio_settings_button.has_point(position): audio_settings_open = true queue_redraw() return if exit_button.has_point(position): get_tree().quit() return if shop_button.has_point(position): store_return_mode = "menu" store_tab = "towers" game_mode = "store" queue_redraw() return if menu_step == "difficulty" and map_step_back_button.has_point(position): menu_step = "map" queue_redraw() return if menu_step == "map": for index in map_buttons.size(): if map_buttons[index].has_point(position): var map_index := index if not unlocked_maps.has(map_index): store_return_mode = "menu" store_tab = "maps" game_mode = "store" queue_redraw() return _select_map(map_index) menu_step = "difficulty" queue_redraw() return else: for index in difficulty_buttons.size(): if difficulty_buttons[index].has_point(position): selected_difficulty = index _save_progress() queue_redraw() return if menu_step == "difficulty" and start_button.has_point(position): _start_game() elif leaderboard_button.has_point(position): _open_leaderboard() elif multiplayer_button.has_point(position): game_mode = "multiplayer_menu" queue_redraw() return if game_mode == "store": if shop_back_button.has_point(position): game_mode = store_return_mode queue_redraw() return if store_tower_tab_button.has_point(position): store_tab = "towers" queue_redraw() return if store_map_tab_button.has_point(position): store_tab = "maps" queue_redraw() return if store_tab == "maps": for index in map_shop_purchase_buttons.size(): if map_shop_purchase_buttons[index].has_point(position): _buy_map_with_crystals(index + 3) queue_redraw() return else: for index in shop_purchase_buttons.size(): if shop_purchase_buttons[index].has_point(position): _buy_tower_with_crystals(index + 3) queue_redraw() return return if game_mode == "leaderboard": if leaderboard_back_button.has_point(position): game_mode = "menu" elif leaderboard_mission_button.has_point(position) and leaderboard_endless: leaderboard_endless = false _refresh_leaderboard() elif leaderboard_endless_button.has_point(position) and not leaderboard_endless: leaderboard_endless = true _refresh_leaderboard() elif leaderboard_refresh_button.has_point(position): _refresh_leaderboard() elif leaderboard_map_previous_button.has_point(position): leaderboard_map = wrapi(leaderboard_map - 1, 0, MAP_NAMES.size()) _refresh_leaderboard() elif leaderboard_map_button.has_point(position) or leaderboard_map_next_button.has_point(position): leaderboard_map = wrapi(leaderboard_map + 1, 0, MAP_NAMES.size()) _refresh_leaderboard() elif leaderboard_difficulty_previous_button.has_point(position): leaderboard_difficulty = wrapi(leaderboard_difficulty - 1, 0, DIFFICULTY_DATA.size()) _refresh_leaderboard() elif leaderboard_difficulty_button.has_point(position) or leaderboard_difficulty_next_button.has_point(position): leaderboard_difficulty = wrapi(leaderboard_difficulty + 1, 0, DIFFICULTY_DATA.size()) _refresh_leaderboard() queue_redraw() return if game_mode == "multiplayer_menu": if multiplayer_back_button.has_point(position): game_mode = "menu" elif quick_match_button.has_point(position): _open_matchmaking("quick") elif friend_match_button.has_point(position): _open_room_code_entry() elif private_match_button.has_point(position): _open_private_room() elif ai_match_button.has_point(position): _start_coop_ai_game() queue_redraw() return if game_mode == "room_code_entry": if multiplayer_back_button.has_point(position): _hide_virtual_keyboard() game_mode = "multiplayer_menu" elif room_code_input_rect.has_point(position): _show_room_code_keyboard() elif join_room_button.has_point(position): _join_entered_room() queue_redraw() return if game_mode == "matchmaking": if lobby_ready_button.has_point(position) and multiplayer_manager.matchmaking_state in ["hosting", "matched", "lobby", "reconnected"]: multiplayer_manager.toggle_lobby_ready() elif cancel_search_button.has_point(position) or multiplayer_back_button.has_point(position): multiplayer_manager.cancel_search() game_mode = "multiplayer_menu" queue_redraw() return if game_mode == "profile": if profile_back_button.has_point(position): profile_delete_armed = false game_mode = "menu" elif profile_rename_button.has_point(position): _open_profile_name_entry() elif profile_support_button.has_point(position): OS.shell_open(_support_email_url()) elif profile_privacy_button.has_point(position): OS.shell_open("https://void-bastion.gergames.de/privacy.html") elif profile_delete_button.has_point(position): if not online_manager.is_authenticated(): profile_message = "Kontolöschung benötigt eine Verbindung zum GerGames-Server." elif not profile_delete_armed: profile_delete_armed = true profile_message = "Noch einmal bestätigen: Online-Konto, Ranglisten und lokale Werte werden gelöscht." else: _delete_profile_account() queue_redraw() return if game_mode == "profile_name_entry": if profile_back_button.has_point(position): _hide_virtual_keyboard() game_mode = "profile" elif profile_name_input_rect.has_point(position): _show_profile_name_keyboard() elif profile_name_save_button.has_point(position): _save_profile_name() queue_redraw() return if back_button.has_point(position): _return_to_menu() return if coop_mode and coop_side_button.has_point(position): _toggle_coop_side() return if game_mode in ["game_over", "victory", "endless_result"]: if start_button.has_point(position): if game_mode == "victory": _return_to_menu() elif coop_ai_mode: _start_coop_ai_game() else: _start_game() return if game_mode == "endless_offer": if victory_reward_pending: return if endless_button.has_point(position): _start_endless_mode() elif finish_mission_button.has_point(position): _mission_complete() return if not tutorial_completed and tutorial_skip_button.has_point(position): _complete_tutorial() return if ads_manager.can_offer_rewarded() and reward_ad_button.has_point(position): ads_manager.request_rewarded_energy() return if pause_button.has_point(position): _toggle_pause() return if speed_dropdown_open: for speed_index in SPEED_OPTIONS.size(): if _speed_option_rect(speed_index).has_point(position): game_speed = SPEED_OPTIONS[speed_index] speed_dropdown_open = false _show_status("SIMULATION %.0f×" % game_speed) queue_redraw() return speed_dropdown_open = false if speed_button.has_point(position): if coop_online_mode: _show_status("ONLINE-SIMULATION LÄUFT AUF SERVERTEMPO") return speed_dropdown_open = true queue_redraw() return if auto_wave_button.has_point(position): auto_start_waves = not auto_start_waves if not auto_start_waves: auto_wave_countdown = 0.0 _show_status("AUTO-WELLENSTART %s" % ("EIN" if auto_start_waves else "AUS")) queue_redraw() return if game_mode == "paused": return if wave_button.has_point(position): _start_wave() return for index in tower_buttons.size(): if tower_buttons[index].has_point(position): var tower_type := index if not unlocked_towers.has(tower_type): store_return_mode = game_mode game_mode = "store" queue_redraw() return selected_tower_type = tower_type selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) if not tutorial_completed and tutorial_step == 0: tutorial_step = 1 return if selected_tower_index >= 0: if upgrade_button.has_point(position): _upgrade_selected() return if sell_button.has_point(position): _sell_selected() return var world_position := position + camera_offset var existing := _tower_at(world_position) if existing >= 0: if coop_mode and int(towers[existing].get("owner", 0)) != _local_credit_slot(): _show_status("PARTNER-TURM • NICHT VERFÜGBAR") return selected_tower_index = existing selected_tower_type = -1 pending_touch_build_position = Vector2(-1, -1) return if selected_tower_type >= 0 and position.y > PLAYFIELD_TOP and position.y < PLAYFIELD_BOTTOM: var snapped_position := _snap_to_grid(world_position) if is_touch and pending_touch_build_position != snapped_position: pending_touch_build_position = snapped_position _show_status("REICHWEITE PRÜFEN • ERNEUT TIPPEN ZUM BAUEN") return _place_tower(world_position) func _select_map(index: int) -> void: coop_mode = false coop_ai_mode = false coop_online_mode = false selected_map = clampi(index, 0, MAP_TEXTURES.size() - 1) path_points = PackedVector2Array() for point in MAP_PATHS[selected_map]: path_points.append(point * WORLD_SCALE) _validate_route_ends_at_reactor(path_points, selected_map) _rebuild_path_cells() build_zones.clear() for zone in MAP_BUILD_ZONES[selected_map]: build_zones.append(Rect2(zone.position * WORLD_SCALE, zone.size * WORLD_SCALE)) camera_offset = Vector2.ZERO queue_redraw() func _configure_coop_arena() -> void: coop_paths.clear() var left := PackedVector2Array(COOP_LEFT_PATH) var right: PackedVector2Array = multiplayer_manager.mirrored_path(left, COOP_WORLD_WIDTH) coop_paths.append(left) coop_paths.append(right) path_points = left path_cells.clear() for route in coop_paths: _add_path_cells(route) build_zones = [Rect2(120, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP), Rect2(1560, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP)] camera_offset = Vector2.ZERO if left[-1].distance_to(COOP_REACTOR_CENTER) > 1.0 or right[-1].distance_to(COOP_REACTOR_CENTER) > 1.0: push_error("Koop-Routen müssen sichtbar im gemeinsamen Reaktor enden.") func _validate_route_ends_at_reactor(route: PackedVector2Array, map_index: int) -> bool: if route.is_empty() or map_index < 0 or map_index >= MAP_REACTOR_ZONES.size(): push_error("Kartenroute oder Reaktordaten fehlen.") return false var reactor_center: Vector2 = MAP_REACTOR_ZONES[map_index][0].center var is_connected := route[-1].distance_to(reactor_center) <= 1.0 if not is_connected: push_error("Karte %s: Der Laufweg muss exakt im Reaktormittelpunkt enden." % MAP_NAMES[map_index]) return is_connected func _toggle_coop_side() -> void: selected_tower_type = -1 selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) camera_offset.x = COOP_WORLD_WIDTH - SCREEN.x if camera_offset.x < 1.0 else 0.0 cursor_position = cursor_screen_position + camera_offset var side_name := "PARTNER-SEITE" if coop_online_mode else "KI-SEITE" _show_status(side_name if camera_offset.x > 0.0 else "DEINE SEITE") func _start_coop_ai_game() -> void: coop_mode = true coop_ai_mode = true coop_online_mode = false local_player_slot = 0 _configure_coop_arena() multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30) credits = multiplayer_manager.player_credits[0] reactor_lives = multiplayer_manager.shared_reactor_lives wave = 0 towers_built_this_run = 0 auto_wave_countdown = 0.0 towers.clear() enemies.clear() projectiles.clear() beams.clear() particles.clear() selected_tower_type = -1 selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) wave_active = false endless_mode = false mission_reward_granted = false last_mission_reward = 0 victory_reward_pending = false next_spawn_route = 0 game_mode = "playing" game_speed = 1.0 tutorial_step = 3 ads_manager.begin_mission() _run_ai_builder(true) _show_status("KOOP-ARENA ONLINE • KI-PARTNER BEREIT") func _start_coop_online_game(player_slot: int) -> void: local_player_slot = clampi(player_slot, 0, 1) coop_mode = true coop_ai_mode = false coop_online_mode = true _configure_coop_arena() wave = 0 towers.clear() enemies.clear() projectiles.clear() beams.clear() particles.clear() selected_tower_type = -1 selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) wave_active = false endless_mode = false mission_reward_granted = false last_mission_reward = 0 victory_reward_pending = false game_mode = "playing" game_speed = 1.0 tutorial_step = 3 camera_offset.x = COOP_WORLD_WIDTH - SCREEN.x if local_player_slot == 1 else 0.0 ads_manager.begin_mission() _apply_online_snapshot(multiplayer_manager.latest_snapshot) _show_status("ONLINE-KOOP BEREIT • DU VERTEIDIGST %s" % ("RECHTS" if local_player_slot == 1 else "LINKS")) func _open_matchmaking(mode: String) -> void: multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30) multiplayer_manager.begin_search(mode, selected_difficulty) game_mode = "matchmaking" queue_redraw() func _open_private_room() -> void: multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30) multiplayer_manager.create_private_room(selected_difficulty) game_mode = "matchmaking" queue_redraw() func _open_room_code_entry() -> void: room_code_input = "" game_mode = "room_code_entry" _show_room_code_keyboard() queue_redraw() func _join_entered_room() -> void: if room_code_input.length() != 6: _show_status("DER RAUMCODE MUSS SECHS ZEICHEN HABEN") return _hide_virtual_keyboard() multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30) multiplayer_manager.join_private_room(room_code_input) game_mode = "matchmaking" queue_redraw() func _show_room_code_keyboard() -> void: if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD): DisplayServer.virtual_keyboard_show(room_code_input, room_code_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, 6, room_code_input.length(), room_code_input.length()) func _hide_virtual_keyboard() -> void: if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD): DisplayServer.virtual_keyboard_hide() func _app_version() -> String: return str(ProjectSettings.get_setting("application/config/version", "Entwicklung")) func _support_email_url() -> String: var subject := "Void Bastion Feedback %s" % _app_version() var body := "Version: %s\nPlattform: %s\nSpieler-ID: %s\n\nBeschreibung:\n" % [ _app_version(), OS.get_name(), account_manager.short_id(), ] return "mailto:support@gergames.de?subject=%s&body=%s" % [subject.uri_encode(), body.uri_encode()] func _open_profile_name_entry() -> void: profile_name_input = account_manager.display_name profile_message = "" profile_delete_armed = false game_mode = "profile_name_entry" _show_profile_name_keyboard() queue_redraw() func _show_profile_name_keyboard() -> void: if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD): DisplayServer.virtual_keyboard_show(profile_name_input, profile_name_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, AccountManager.MAX_DISPLAY_NAME_LENGTH, profile_name_input.length(), profile_name_input.length()) func _save_profile_name() -> void: var validation_error: String = account_manager.validate_display_name(profile_name_input) if not validation_error.is_empty(): profile_message = validation_error queue_redraw() return profile_message = "Spielername wird gespeichert …" queue_redraw() var result: Dictionary = await online_manager.update_display_name(profile_name_input) if not bool(result.get("accepted", false)): profile_message = str(result.get("error", "Der Spielername konnte nicht gespeichert werden.")) queue_redraw() return _hide_virtual_keyboard() profile_message = "Spielername gespeichert%s." % (" und online synchronisiert" if bool(result.get("online", false)) else "") game_mode = "profile" queue_redraw() func _delete_profile_account() -> void: profile_delete_armed = false profile_message = "Konto wird unwiderruflich gelöscht …" queue_redraw() var result: Dictionary = await online_manager.delete_online_account() if not bool(result.get("accepted", false)): profile_message = str(result.get("error", "Das Konto konnte nicht gelöscht werden.")) queue_redraw() return best_wave = 0 best_endless_wave = 0 core_crystals = 0 unlocked_towers = [0, 1, 2] unlocked_maps = [0, 1, 2] _save_progress() profile_message = "Online-Konto und lokale Profildaten gelöscht. Ein neues Gastprofil wurde erstellt." queue_redraw() func _open_leaderboard() -> void: game_mode = "leaderboard" leaderboard_endless = false leaderboard_map = selected_map leaderboard_difficulty = selected_difficulty _refresh_leaderboard() func _refresh_leaderboard() -> void: if leaderboard_loading: return leaderboard_loading = true leaderboard_message = "BESTENLISTE WIRD GELADEN …" leaderboard_rows.clear() queue_redraw() if not online_manager.is_authenticated(): leaderboard_loading = false leaderboard_message = "GERGAMES-SERVER WIRD VERBUNDEN – BITTE GLEICH ERNEUT LADEN" queue_redraw() return leaderboard_rows = await online_manager.load_leaderboard(leaderboard_map, leaderboard_difficulty, leaderboard_endless, 12) leaderboard_loading = false leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else "" queue_redraw() func _start_game() -> void: coop_online_mode = false credits = int(_difficulty().start_credits) reactor_lives = int(_difficulty().lives) wave = 0 towers_built_this_run = 0 auto_wave_countdown = 0.0 towers.clear() enemies.clear() projectiles.clear() beams.clear() particles.clear() selected_tower_type = -1 selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) wave_active = false endless_mode = false mission_reward_granted = false last_mission_reward = 0 victory_reward_pending = false game_mode = "playing" game_speed = 1.0 camera_offset = Vector2.ZERO tutorial_step = 0 if not tutorial_completed else 3 ads_manager.begin_mission() _show_status("VERTEIDIGUNG ONLINE") func _return_to_menu() -> void: if coop_online_mode: online_manager.leave_current_match() game_mode = "menu" menu_step = "map" wave_active = false game_speed = 1.0 enemies_to_spawn = 0 towers.clear() enemies.clear() projectiles.clear() beams.clear() particles.clear() selected_tower_type = -1 selected_tower_index = -1 pending_touch_build_position = Vector2(-1, -1) camera_offset = Vector2.ZERO coop_mode = false coop_ai_mode = false coop_online_mode = false _select_map(selected_map) queue_redraw() func _start_wave() -> void: if wave_active: _show_status("WELLE LÄUFT BEREITS") return if coop_online_mode: online_manager.request_wave_start() _show_status("WELLENSTART AN SERVER GESENDET") return wave += 1 var wave_population_scale := 1.0 + maxf(0.0, wave - 3.0) * 0.08 if selected_difficulty == 2: wave_population_scale += maxf(0.0, wave - 4.0) * 0.025 if endless_mode: wave_population_scale += maxf(0.0, wave - MAX_WAVES) * 0.035 enemies_to_spawn = ceili((7 + wave * 2) * float(_difficulty().count) * wave_population_scale) * (2 if coop_mode else 1) spawn_timer = 0.15 wave_active = true if coop_ai_mode: _run_ai_builder(false) if not tutorial_completed and tutorial_step >= 2: _complete_tutorial() _show_status("BOSSWELLE %d" % wave if wave % BOSS_WAVE_INTERVAL == 0 else "WELLE %d NÄHERT SICH" % wave) func _toggle_pause() -> void: if coop_online_mode: _show_status("ONLINE-PARTIEN KÖNNEN NICHT PAUSIERT WERDEN") return if game_mode == "playing": game_mode = "paused" elif game_mode == "paused": game_mode = "playing" func _game_over() -> void: if game_mode != "playing": return if endless_mode: _endless_run_complete() return game_mode = "game_over" wave_active = false if wave > best_wave: best_wave = wave _record_account_score(false) _save_progress() func _mission_complete() -> void: game_mode = "victory" wave_active = false func _offer_endless_mode() -> void: game_mode = "endless_offer" wave_active = false best_wave = maxi(best_wave, wave) _record_account_score(false) _save_progress() victory_reward_pending = true ads_manager.show_victory_interstitial() func _start_endless_mode() -> void: endless_mode = true game_mode = "playing" _show_status("ENDLOSMODUS AKTIV • NÄCHSTE WELLE: %d" % (wave + 1)) func _endless_run_complete() -> void: game_mode = "endless_result" wave_active = false best_wave = maxi(best_wave, wave) best_endless_wave = maxi(best_endless_wave, wave) _record_account_score(true) _save_progress() func _on_victory_interstitial_finished(_was_shown: bool) -> void: if not victory_reward_pending: return victory_reward_pending = false _grant_map_reward() _save_progress() queue_redraw() func _record_account_score(was_endless: bool) -> void: var score := _calculate_run_score() account_manager.record_run(selected_map, selected_difficulty, was_endless, wave, score) func _calculate_run_score() -> int: var base_score := wave * 1000 + maxi(reactor_lives, 0) * 150 + _player_credits(_local_credit_slot()) * 2 var tower_efficiency := clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50) return roundi(base_score * tower_efficiency) func _grant_map_reward() -> void: if mission_reward_granted: return mission_reward_granted = true last_mission_reward = int(_difficulty().crystals) core_crystals += last_mission_reward _save_progress() func _buy_tower_with_crystals(tower_type: int) -> void: if tower_type < 3 or tower_type >= TOWER_DATA.size(): return if unlocked_towers.has(tower_type): _show_status("%s IST BEREITS FREIGESCHALTET" % TOWER_DATA[tower_type].name) return var price := int(TOWER_DATA[tower_type].unlock_cost) if core_crystals < price: _show_status("ZU WENIG KERNKRISTALLE • BENÖTIGT: %d" % price) return core_crystals -= price unlocked_towers.append(tower_type) unlocked_towers.sort() _save_progress() _show_status("%s FREIGESCHALTET • IM ARSENAL VERFÜGBAR" % TOWER_DATA[tower_type].name) func _buy_map_with_crystals(map_index: int) -> void: if map_index < 3 or map_index >= MAP_NAMES.size(): return if unlocked_maps.has(map_index): _show_status("%s IST BEREITS FREIGESCHALTET" % MAP_NAMES[map_index]) return var price := int(MAP_UNLOCK_COSTS[map_index]) if core_crystals < price: _show_status("ZU WENIG KERNKRISTALLE • BENÖTIGT: %d" % price) return core_crystals -= price unlocked_maps.append(map_index) unlocked_maps.sort() _save_progress() _show_status("%s FREIGESCHALTET • NEUER EINSATZSEKTOR" % MAP_NAMES[map_index]) func _difficulty() -> Dictionary: return DIFFICULTY_DATA[clampi(selected_difficulty, 0, DIFFICULTY_DATA.size() - 1)] func _on_update_available(info: Dictionary) -> void: available_update = info.duplicate(true) update_notice_visible = true queue_redraw() func _on_energy_reward_granted(amount: int) -> void: _award_credits(0, amount) _show_status("NOTFALLENERGIE +%d" % amount) func _complete_tutorial() -> void: tutorial_completed = true tutorial_step = 3 _save_progress() _show_status("EINFÜHRUNG ABGESCHLOSSEN") func _place_tower(position: Vector2) -> void: position = _snap_to_grid(position) if selected_tower_type < 0 or not unlocked_towers.has(selected_tower_type): _show_status("TURM ZUERST IM ARSENAL FREISCHALTEN") return var data: Dictionary = TOWER_DATA[selected_tower_type] var credit_slot := _local_credit_slot() if _player_credits(credit_slot) < data.cost: _show_status("NICHT GENUG ENERGIE") return if not _is_valid_build_position(position, credit_slot): _show_status("BAUPLATZ BLOCKIERT") return if coop_online_mode: online_manager.request_tower_build(selected_tower_type, position) selected_tower_type = -1 pending_touch_build_position = Vector2(-1, -1) _show_status("BAUAUFTRAG AN SERVER GESENDET") return _spend_credits(0, int(data.cost)) towers_built_this_run += 1 towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0}) selected_tower_index = towers.size() - 1 selected_tower_type = -1 pending_touch_build_position = Vector2(-1, -1) if not tutorial_completed and tutorial_step == 1: tutorial_step = 2 _spawn_burst(position, Color("65eaff"), 12) func _upgrade_selected() -> void: if selected_tower_index < 0 or selected_tower_index >= towers.size(): return var tower: Dictionary = towers[selected_tower_index] if coop_online_mode and int(tower.get("owner", -1)) != local_player_slot: _show_status("NUR EIGENE TÜRME KÖNNEN VERBESSERT WERDEN") return if tower.level >= 3: _show_status("MAXIMALE STUFE") return var cost: int = _upgrade_cost(tower) if _player_credits(_local_credit_slot()) < cost: _show_status("NICHT GENUG ENERGIE") return if coop_online_mode: online_manager.request_tower_upgrade(int(tower.get("id", -1))) _show_status("UPGRADE AN SERVER GESENDET") return _spend_credits(0, cost) tower.level += 1 _spawn_burst(tower.pos, Color("65eaff"), 16) func _sell_selected() -> void: if selected_tower_index < 0 or selected_tower_index >= towers.size(): return var tower: Dictionary = towers[selected_tower_index] if coop_online_mode: if int(tower.get("owner", -1)) != local_player_slot: _show_status("NUR EIGENE TÜRME KÖNNEN VERKAUFT WERDEN") return online_manager.request_tower_sell(int(tower.get("id", -1))) selected_tower_index = -1 _show_status("VERKAUF AN SERVER GESENDET") return var base_cost: int = TOWER_DATA[tower.type].cost _award_credits(0, int(base_cost * (0.55 + tower.level * 0.12))) towers.remove_at(selected_tower_index) selected_tower_index = -1 func _upgrade_cost(tower: Dictionary) -> int: return int(TOWER_DATA[tower.type].cost * (0.72 + tower.level * 0.28)) func _tower_damage(type: int, level: int) -> float: return TOWER_DATA[type].damage * (1.0 + (level - 1) * 0.52) func _tower_range(type: int, level: int) -> float: return TOWER_DATA[type].range * (1.0 + (level - 1) * 0.08) func _tower_cooldown(type: int, level: int) -> float: return TOWER_DATA[type].rate / (1.0 + (level - 1) * 0.12) func _tower_shots_per_second(type: int, level: int) -> float: return 1.0 / _tower_cooldown(type, level) func _tower_aim_angle(type: int, origin: Vector2, target: Vector2) -> float: return origin.angle_to_point(target) + (PI if type in [0, 3, 5] else 0.0) func _tower_at(position: Vector2, radius := 52.0) -> int: for index in towers.size(): if towers[index].pos.distance_to(position) <= radius: return index return -1 func _grid_cell(position: Vector2) -> Vector2i: return Vector2i( roundi((position.x - GRID_SIZE * 0.5) / GRID_SIZE), roundi((position.y - GRID_SIZE * 0.5) / GRID_SIZE) ) func _cell_center(cell: Vector2i) -> Vector2: return Vector2(cell) * GRID_SIZE + Vector2.ONE * GRID_SIZE * 0.5 func _rebuild_path_cells() -> void: path_cells.clear() _add_path_cells(path_points) func _add_path_cells(route: PackedVector2Array) -> void: for index in route.size() - 1: var start := route[index] var finish := route[index + 1] var steps := maxi(1, ceili(start.distance_to(finish) / (GRID_SIZE * 0.20))) for step in steps + 1: var sample := start.lerp(finish, float(step) / float(steps)) path_cells[_grid_cell(sample)] = true func _is_path_cell(position: Vector2) -> bool: return path_cells.has(_grid_cell(position)) func _snap_to_grid(position: Vector2) -> Vector2: return Vector2( floor(position.x / GRID_SIZE) * GRID_SIZE + GRID_SIZE * 0.5, floor(position.y / GRID_SIZE) * GRID_SIZE + GRID_SIZE * 0.5 ) func _is_in_build_zone(position: Vector2) -> bool: for zone in build_zones: if zone.has_point(position): return true return false func _is_in_reactor_zone(position: Vector2) -> bool: if coop_mode: return position.distance_to(COOP_REACTOR_CENTER) <= COOP_REACTOR_RADIUS for reactor in MAP_REACTOR_ZONES[selected_map]: if position.distance_to(reactor.center) <= float(reactor.radius): return true return false func _player_credits(player_index: int) -> int: if coop_mode: return int(multiplayer_manager.player_credits[clampi(player_index, 0, 1)]) return credits func _spend_credits(player_index: int, amount: int) -> bool: if coop_mode: var success: bool = multiplayer_manager.spend_player_credits(player_index, amount) credits = multiplayer_manager.player_credits[0] return success if credits < amount: return false credits -= amount return true func _award_credits(player_index: int, amount: int) -> void: if coop_mode: multiplayer_manager.add_player_credits(player_index, amount) credits = multiplayer_manager.player_credits[0] else: credits += amount func _run_ai_builder(initial_setup: bool) -> void: if not coop_ai_mode: return var desired_towers := mini(9, 2 + int(wave / 2)) if initial_setup: desired_towers = 2 var ai_tower_count := 0 for tower in towers: if int(tower.get("owner", 0)) == 1: ai_tower_count += 1 while ai_tower_count < desired_towers: var tower_type := ai_tower_count % TOWER_DATA.size() var cost: int = int(TOWER_DATA[tower_type].cost) if _player_credits(1) < cost: break var build_position := _find_ai_build_position(ai_tower_count) if build_position == Vector2.ZERO: break _spend_credits(1, cost) towers.append({"pos": build_position, "type": tower_type, "level": 1, "cooldown": 0.15, "angle": PI, "owner": 1}) _spawn_burst(build_position, Color("ffbf69"), 12) ai_tower_count += 1 if not initial_setup and ai_tower_count >= desired_towers: _upgrade_ai_tower() func _find_ai_build_position(offset: int) -> Vector2: if coop_paths.size() < 2: return Vector2.ZERO var route: PackedVector2Array = coop_paths[1] var desired_towers := mini(9, 2 + int(wave / 2)) var target_progress := _route_length(route) * float(offset + 1) / float(desired_towers + 1) var tower_range: float = TOWER_DATA[offset % TOWER_DATA.size()].range var best_position := Vector2.ZERO var best_score := INF for x in range(1560, 2841, 80): for y in range(120, 721, 80): var candidate := Vector2(x, y) if _is_valid_build_position(candidate, 1): var distance := _distance_to_route(candidate, route) if distance < 82.0 or distance > tower_range - 12.0: continue var progress := _route_progress_at(candidate, route) var score := absf(progress - target_progress) + absf(distance - tower_range * 0.70) * 1.25 for tower in towers: if int(tower.get("owner", 0)) == 1: var separation: float = candidate.distance_to(tower.pos) if separation < 150.0: score += (150.0 - separation) * 4.0 if score < best_score: best_score = score best_position = candidate return best_position func _route_length(route: PackedVector2Array) -> float: var result := 0.0 for index in route.size() - 1: result += route[index].distance_to(route[index + 1]) return result func _route_progress_at(position: Vector2, route: PackedVector2Array) -> float: var travelled := 0.0 var best_distance := INF var best_progress := 0.0 for index in route.size() - 1: var start: Vector2 = route[index] var segment: Vector2 = route[index + 1] - start var segment_length := segment.length() var ratio := clampf((position - start).dot(segment) / maxf(0.001, segment.length_squared()), 0.0, 1.0) var point := start + segment * ratio var distance := position.distance_to(point) if distance < best_distance: best_distance = distance best_progress = travelled + segment_length * ratio travelled += segment_length return best_progress func _distance_to_route(position: Vector2, route: PackedVector2Array) -> float: var result := INF for index in route.size() - 1: var closest := Geometry2D.get_closest_point_to_segment(position, route[index], route[index + 1]) result = minf(result, position.distance_to(closest)) return result func _upgrade_ai_tower() -> void: for tower in towers: if int(tower.get("owner", 0)) != 1 or int(tower.level) >= 3: continue var cost := _upgrade_cost(tower) if _spend_credits(1, cost): tower.level += 1 _spawn_burst(tower.pos, Color("ffbf69"), 10) return func _local_credit_slot() -> int: return local_player_slot if coop_online_mode else 0 func _apply_online_snapshot(snapshot: Dictionary) -> void: if not coop_online_mode or snapshot.is_empty(): return selected_difficulty = clampi(int(snapshot.get("difficulty", selected_difficulty)), 0, DIFFICULTY_DATA.size() - 1) if bool(snapshot.get("connection_interrupted", false)): _show_status("MITPILOT GETRENNT • WIEDERBEITRITT %d S" % int(snapshot.get("reconnect_seconds", 0))) reactor_lives = int(snapshot.get("reactor_lives", reactor_lives)) wave = int(snapshot.get("wave", wave)) wave_active = bool(snapshot.get("wave_active", false)) enemies_to_spawn = int(snapshot.get("enemies_to_spawn", 0)) credits = _player_credits(local_player_slot) var previous_enemy_positions: Dictionary = {} for enemy in enemies: previous_enemy_positions[int(enemy.get("id", -1))] = enemy.get("pos", Vector2.ZERO) var synced_enemies: Array[Dictionary] = [] for entry in snapshot.get("enemies", []): if not entry is Dictionary: continue var enemy_id := int(entry.get("id", -1)) var target_position := Vector2(float(entry.get("x", 0.0)), float(entry.get("y", 0.0))) synced_enemies.append({ "id": enemy_id, "type": int(entry.get("type", 0)), "pos": previous_enemy_positions.get(enemy_id, target_position), "target_pos": target_position, "path_index": int(entry.get("path_index", 1)), "route_id": int(entry.get("route_id", 0)), "hp": float(entry.get("hp", 1.0)), "max_hp": float(entry.get("max_hp", 1.0)), "speed": float(entry.get("speed", 0.0)), "reward": int(entry.get("reward", 0)), "angle": float(entry.get("angle", 0.0)), "flash": 0.0, "boss": bool(entry.get("boss", false)), "size_scale": 1.34 if bool(entry.get("boss", false)) else 1.0 }) enemies = synced_enemies var synced_towers: Array[Dictionary] = [] for entry in snapshot.get("towers", []): if entry is Dictionary: synced_towers.append({ "id": int(entry.get("id", -1)), "pos": Vector2(float(entry.get("x", 0.0)), float(entry.get("y", 0.0))), "type": int(entry.get("type", 0)), "level": int(entry.get("level", 1)), "owner": int(entry.get("owner", 0)), "cooldown": float(entry.get("cooldown", 0.0)), "angle": float(entry.get("angle", 0.0)) }) towers = synced_towers if selected_tower_index >= towers.size(): selected_tower_index = -1 if bool(snapshot.get("game_over", false)) and game_mode == "playing": _game_over() func _update_online_interpolation(delta: float) -> void: var weight := clampf(delta * 12.0, 0.0, 1.0) for enemy in enemies: enemy.pos = enemy.pos.lerp(enemy.get("target_pos", enemy.pos), weight) func _handle_online_event(event: Dictionary) -> void: if not coop_online_mode: return match String(event.get("type", "")): "wave_complete": _show_status("WELLE %d ABGEWEHRT +%d" % [int(event.get("wave", wave)), int(event.get("bonus", 0))]) "partner_left": _show_status("VERBINDUNG ZUM PARTNER VERLOREN") "command_rejected": _show_status("AKTION VOM SERVER ABGELEHNT") "server_shutdown": _show_status("SPIELSERVER WIRD NEU GESTARTET") func _show_status(text: String) -> void: status_text = text status_timer = 2.1 func _load_audio_settings() -> void: var config := ConfigFile.new() if config.load("user://void_bastion_settings.cfg") == OK: master_volume = clampf(float(config.get_value("audio", "master_volume", master_volume)), 0.0, 1.0) _apply_master_volume() func _set_master_volume(value: float) -> void: master_volume = clampf(value, 0.0, 1.0) _apply_master_volume() queue_redraw() func _apply_master_volume() -> void: var master_bus := AudioServer.get_bus_index("Master") if master_bus >= 0: AudioServer.set_bus_volume_db(master_bus, linear_to_db(maxf(master_volume, 0.0001))) func _save_audio_settings() -> void: var config := ConfigFile.new() config.set_value("audio", "master_volume", master_volume) config.save("user://void_bastion_settings.cfg") func _build_ambient_stream() -> AudioStreamWAV: const sample_rate := 22050 const duration := 12 var frame_count := sample_rate * duration var samples := PackedByteArray() samples.resize(frame_count * 4) for frame in frame_count: var time := float(frame) / sample_rate var slow_breathe := 0.72 + 0.28 * sin(TAU * time / duration) var chord := sin(TAU * 55.0 * time) * 0.34 + sin(TAU * 82.5 * time) * 0.22 + sin(TAU * 110.0 * time) * 0.16 + sin(TAU * 165.0 * time) * 0.08 var shimmer := sin(TAU * 220.0 * time + sin(TAU * time / duration) * 0.45) * 0.035 var mono := clampf((chord + shimmer) * slow_breathe * 0.12, -1.0, 1.0) var left := int(mono * 32767.0) var right := int(clampf(mono * (0.97 + 0.03 * sin(TAU * time / duration)), -1.0, 1.0) * 32767.0) samples.encode_s16(frame * 4, left) samples.encode_s16(frame * 4 + 2, right) var stream := AudioStreamWAV.new() stream.format = AudioStreamWAV.FORMAT_16_BITS stream.mix_rate = sample_rate stream.stereo = true stream.loop_mode = AudioStreamWAV.LOOP_FORWARD stream.loop_end = frame_count stream.data = samples return stream func _build_ui_click_stream() -> AudioStreamWAV: const sample_rate := 22050 const duration := 0.075 var frame_count := int(sample_rate * duration) var samples := PackedByteArray() samples.resize(frame_count * 2) var phase := 0.0 for frame in frame_count: var time := float(frame) / sample_rate var frequency := lerpf(1350.0, 720.0, time / duration) phase += TAU * frequency / sample_rate var envelope := 1.0 - time / duration samples.encode_s16(frame * 2, int(sin(phase) * envelope * 0.14 * 32767.0)) var stream := AudioStreamWAV.new() stream.format = AudioStreamWAV.FORMAT_16_BITS stream.mix_rate = sample_rate stream.stereo = false stream.data = samples return stream func _play_ui_sound() -> void: if ui_sound_player.stream != null: ui_sound_player.play() func _save_progress() -> void: var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE) if file: file.store_string(JSON.stringify({ "best_wave": best_wave, "best_endless_wave": best_endless_wave, "core_crystals": core_crystals, "unlocked_towers": unlocked_towers, "unlocked_maps": unlocked_maps, "selected_difficulty": selected_difficulty, "tutorial_completed": tutorial_completed })) func _load_progress() -> void: if not FileAccess.file_exists(SAVE_PATH): return var file := FileAccess.open(SAVE_PATH, FileAccess.READ) if not file: return var data = JSON.parse_string(file.get_as_text()) if data is Dictionary: best_wave = int(data.get("best_wave", 0)) best_endless_wave = int(data.get("best_endless_wave", 0)) core_crystals = maxi(0, int(data.get("core_crystals", 0))) unlocked_towers = [0, 1, 2] var saved_unlocks = data.get("unlocked_towers", [0, 1, 2]) if saved_unlocks is Array: for tower_type in saved_unlocks: if int(tower_type) >= 0 and int(tower_type) < 3 and not unlocked_towers.has(int(tower_type)): unlocked_towers.append(int(tower_type)) elif int(tower_type) >= 3 and int(tower_type) < TOWER_DATA.size() and not unlocked_towers.has(int(tower_type)): unlocked_towers.append(int(tower_type)) unlocked_maps = [0, 1, 2] var saved_maps = data.get("unlocked_maps", [0, 1, 2]) if saved_maps is Array: for map_index in saved_maps: if int(map_index) >= 3 and int(map_index) < MAP_NAMES.size() and not unlocked_maps.has(int(map_index)): unlocked_maps.append(int(map_index)) selected_difficulty = clampi(int(data.get("selected_difficulty", 1)), 0, DIFFICULTY_DATA.size() - 1) tutorial_completed = bool(data.get("tutorial_completed", false)) func _draw() -> void: if game_mode in ["menu", "store", "leaderboard", "profile", "profile_name_entry", "multiplayer_menu", "room_code_entry", "matchmaking"]: draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, SCREEN), false, Color("f3f6fb")) draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06)) if game_mode == "menu": _draw_menu() if audio_settings_open: _draw_audio_settings() elif game_mode == "store": _draw_store() elif game_mode == "leaderboard": _draw_leaderboard() elif game_mode == "profile": _draw_profile() elif game_mode == "profile_name_entry": _draw_profile_name_entry() elif game_mode == "multiplayer_menu": _draw_multiplayer_menu() elif game_mode == "room_code_entry": _draw_room_code_entry() else: _draw_matchmaking() return draw_set_transform(-camera_offset, 0.0, Vector2.ONE) if coop_mode: _draw_coop_background() else: draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, WORLD_SIZE), false, Color.WHITE) draw_rect(Rect2(Vector2.ZERO, Vector2(_active_world_width(), WORLD_SIZE.y)), Color(0.01, 0.025, 0.055, 0.035)) _draw_world() draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) _draw_hud() if game_mode == "playing" and not tutorial_completed and not coop_mode: _draw_tutorial() if game_mode == "paused": _draw_overlay("PAUSE", "Zum Fortsetzen tippen oder P drücken") elif game_mode == "game_over": _draw_game_over() elif game_mode == "victory": _draw_victory() elif game_mode == "endless_offer": _draw_endless_offer() elif game_mode == "endless_result": _draw_endless_result() func _active_world_width() -> float: return COOP_WORLD_WIDTH if coop_mode else WORLD_SIZE.x func _draw_coop_background() -> void: var texture: Texture2D = MAP_TEXTURES[selected_map] var source_width := texture.get_width() * COOP_MIRROR_X / WORLD_SIZE.x var source := Rect2(0, 0, source_width, texture.get_height()) draw_texture_rect_region(texture, Rect2(0, 0, COOP_MIRROR_X, WORLD_SIZE.y), source, Color.WHITE) draw_set_transform(Vector2(COOP_WORLD_WIDTH, 0) - camera_offset, 0.0, Vector2(-1, 1)) draw_texture_rect_region(texture, Rect2(0, 0, COOP_MIRROR_X, WORLD_SIZE.y), source, Color.WHITE) draw_set_transform(-camera_offset, 0.0, Vector2.ONE) func _draw_world() -> void: _draw_route() if coop_mode: draw_circle(COOP_REACTOR_CENTER, COOP_REACTOR_RADIUS, Color(0.02, 0.20, 0.30, 0.72)) for radius in [94.0, 70.0, 44.0]: draw_arc(COOP_REACTOR_CENTER, radius, 0.0, TAU, 64, Color(0.28, 0.92, 1.0, 0.72), 3.0) draw_circle(COOP_REACTOR_CENTER, 25.0, Color("8ffaff")) _draw_reactor_approach() _draw_build_grid() if selected_tower_type >= 0 and cursor_screen_position.y > PLAYFIELD_TOP and cursor_screen_position.y < PLAYFIELD_BOTTOM: var snapped_position := _snap_to_grid(cursor_position) var valid_position := _is_valid_build_position(snapped_position, _local_credit_slot()) var placement_color := Color(0.25, 1.0, 0.7, 0.22) if valid_position else Color(1.0, 0.2, 0.2, 0.22) var preview_range := _tower_range(selected_tower_type, 1) draw_circle(snapped_position, preview_range, Color(placement_color, 0.025)) draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.46), 1.5) draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.10), true) draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.62), false, 1.5) _draw_tower({"pos": snapped_position, "type": selected_tower_type, "level": 1, "angle": 0.0}) for tower_index in towers.size(): var tower: Dictionary = towers[tower_index] if tower_index == selected_tower_index: var range_value := _tower_range(tower.type, tower.level) draw_circle(tower.pos, range_value, Color(0.2, 0.85, 1.0, 0.07)) draw_arc(tower.pos, range_value, 0, TAU, 80, Color(0.35, 0.9, 1.0, 0.45), 2.0) _draw_tower(tower) for enemy in enemies: _draw_enemy(enemy) for shot in projectiles: draw_circle(shot.pos, 6.0 if shot.type == 2 else 3.5, shot.color) draw_circle(shot.pos, 12.0 if shot.type == 2 else 7.0, Color(shot.color, 0.20)) for beam in beams: draw_line(beam.from, beam.to, Color(beam.color, 0.92), 3.0) draw_line(beam.from, beam.to, Color.WHITE, 1.0) for particle in particles: draw_circle(particle.pos, maxf(1.0, particle.ttl * 8.0), Color(particle.color, minf(1.0, particle.ttl * 3.0))) func _draw_route() -> void: for cell in path_cells: var center := _cell_center(cell) if center.x < -GRID_SIZE or center.x > _active_world_width() + GRID_SIZE or center.y < PLAYFIELD_TOP - GRID_SIZE or center.y > PLAYFIELD_BOTTOM + GRID_SIZE: continue var left := path_cells.has(cell + Vector2i(-1, 0)) var right := path_cells.has(cell + Vector2i(1, 0)) var up := path_cells.has(cell + Vector2i(0, -1)) var down := path_cells.has(cell + Vector2i(0, 1)) var texture := ROAD_STRAIGHT_TEXTURE var angle := 0.0 var is_corner := (left or right) and (up or down) if is_corner: texture = ROAD_CORNER_TEXTURE if left and down: angle = 0.0 elif left and up: angle = PI * 0.5 elif right and up: angle = PI else: angle = PI * 1.5 elif up or down: angle = PI * 0.5 draw_set_transform(center - camera_offset, angle, Vector2.ONE) var variant := absi(cell.x * 3 + cell.y * 5) % 4 var ray_x := -18.0 if variant % 2 == 0 else 18.0 var tile_opacity := _path_cell_opacity(cell) if not is_corner: draw_colored_polygon(PackedVector2Array([Vector2(ray_x - 5, -36), Vector2(ray_x + 5, -36), Vector2(ray_x + 14, -62), Vector2(ray_x - 14, -62)]), Color(1.0, 0.48, 0.12, 0.055 * tile_opacity)) draw_colored_polygon(PackedVector2Array([Vector2(ray_x - 5, 36), Vector2(ray_x + 5, 36), Vector2(ray_x + 14, 62), Vector2(ray_x - 14, 62)]), Color(1.0, 0.48, 0.12, 0.055 * tile_opacity)) var source_positions := [Vector2(80, 80), Vector2(650, 80), Vector2(80, 700), Vector2(650, 700)] var source_position: Vector2 = source_positions[variant] draw_texture_rect_region(ROAD_SURFACE_TEXTURE, Rect2(-40, -31, 80, 62), Rect2(source_position, Vector2(520, 403)), Color(1.0, 1.0, 1.0, tile_opacity)) draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity)) else: draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity)) draw_texture_rect(ROAD_CORNER_SURFACE_TEXTURE, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity)) draw_set_transform(-camera_offset, 0.0, Vector2.ONE) func _path_cell_opacity(cell: Vector2i) -> float: var routes: Array = [path_points] if coop_mode: routes = coop_paths var center := _cell_center(cell) var nearest_remaining := INF for route in routes: if route.size() < 2: continue var length_after_segment := 0.0 for segment_index in range(route.size() - 2, -1, -1): var segment_start: Vector2 = route[segment_index] var segment_end: Vector2 = route[segment_index + 1] var segment := segment_end - segment_start var segment_length := segment.length() if segment_length > 0.0: var direction := segment / segment_length var distance_along := clampf((center - segment_start).dot(direction), 0.0, segment_length) var projection := segment_start + direction * distance_along if center.distance_to(projection) <= GRID_SIZE * 0.55: var remaining := center.distance_to(projection) + segment_length - distance_along + length_after_segment nearest_remaining = minf(nearest_remaining, remaining) length_after_segment += segment_length if nearest_remaining >= 320.0: return 1.0 var fade_progress := clampf(nearest_remaining / 320.0, 0.0, 1.0) return fade_progress * fade_progress * (3.0 - 2.0 * fade_progress) func _draw_reactor_approach() -> void: var routes: Array = [path_points] if coop_mode: routes = coop_paths for route in routes: if route.size() < 2: continue var approach_start: Vector2 = route[route.size() - 2] var reactor_end: Vector2 = route[route.size() - 1] var direction := approach_start.direction_to(reactor_end) var approach_color := Color("65eaff") # Fade the road's cyan guide in short overlapping steps as it reaches the core. for segment_index in 12: var progress_start := float(segment_index) / 12.0 var progress_end := float(segment_index + 1) / 12.0 var fade := 1.0 - (progress_start + progress_end) * 0.5 var fade_start := approach_start.lerp(reactor_end, progress_start) var fade_end := approach_start.lerp(reactor_end, progress_end) draw_line(fade_start, fade_end, Color(approach_color, 0.10 * fade), 13.0, true) draw_line(fade_start, fade_end, Color(approach_color, 0.18 * fade), 2.0, true) # Multiple dim pulses travel inward, like energy being carried to the core. for pulse_index in 3: var pulse_progress := fposmod(elapsed * 0.34 + float(pulse_index) / 3.0, 1.0) var pulse_position := approach_start.lerp(reactor_end, pulse_progress) var pulse_alpha := (0.22 + 0.12 * sin(pulse_progress * PI)) * (1.0 - pulse_progress * 0.72) draw_line(pulse_position - direction * 12.0, pulse_position + direction * 12.0, Color(approach_color, pulse_alpha), 3.0, true) draw_circle(pulse_position, 7.0, Color(approach_color, pulse_alpha * 0.20)) draw_circle(pulse_position, 2.2, Color(0.78, 0.98, 1.0, pulse_alpha + 0.18)) var reactor_breathe := 0.5 + 0.5 * sin(elapsed * TAU * 0.42) draw_circle(reactor_end, 13.0 + reactor_breathe * 3.0, Color(approach_color, 0.035 + reactor_breathe * 0.025)) draw_arc(reactor_end, 22.0 + reactor_breathe * 2.0, 0.0, TAU, 40, Color(approach_color, 0.10 + reactor_breathe * 0.08), 1.5) func _draw_build_grid() -> void: var line_color := Color(0.24, 0.82, 1.0, 0.14 if selected_tower_type >= 0 else 0.10) var fill_color := Color(0.08, 0.55, 0.72, 0.035 if selected_tower_type >= 0 else 0.025) for x_index in int(_active_world_width() / GRID_SIZE): for y_index in int(WORLD_SIZE.y / GRID_SIZE): var center := Vector2(x_index * GRID_SIZE + GRID_SIZE * 0.5, y_index * GRID_SIZE + GRID_SIZE * 0.5) if not _is_in_build_zone(center) or _is_in_reactor_zone(center): continue if _is_path_cell(center): continue var cell_line_color := line_color var cell_fill_color := fill_color if coop_mode and center.x > COOP_MIRROR_X: cell_line_color = Color(1.0, 0.67, 0.32, 0.16) cell_fill_color = Color(0.75, 0.35, 0.08, 0.025) var cell := Rect2(center - Vector2(34, 34), Vector2(68, 68)) draw_rect(cell, cell_fill_color, true) draw_rect(cell, cell_line_color, false, 1.0) func _draw_tower(tower: Dictionary) -> void: var size: float = 74.0 + (tower.level - 1) * 4.0 if coop_mode and int(tower.get("owner", 0)) == 1: draw_circle(tower.pos, size * 0.54, Color(1.0, 0.56, 0.20, 0.18)) draw_arc(tower.pos, size * 0.54, 0.0, TAU, 28, Color(1.0, 0.70, 0.34, 0.72), 2.0) draw_set_transform(tower.pos - camera_offset, tower.angle, Vector2.ONE) var destination := Rect2(-size * 0.5, -size * 0.5, size, size) if tower.type < 3: var source_width: float = TOWER_TEXTURE.get_width() / 3.0 draw_texture_rect_region(TOWER_TEXTURE, destination, Rect2(source_width * tower.type, 0, source_width, TOWER_TEXTURE.get_height())) else: var tower_texture: Texture2D = PLASMA_TEXTURE if tower.type == 3 else (EMP_TEXTURE if tower.type == 4 else RAILGUN_TEXTURE) draw_texture_rect(tower_texture, destination, false) draw_set_transform(-camera_offset, 0.0, Vector2.ONE) for level_mark in tower.level: draw_circle(tower.pos + Vector2(-10 + level_mark * 10, 43), 3.0, Color("6cecff")) func _draw_enemy(enemy: Dictionary) -> void: var enemy_type := int(enemy.type) var enemy_texture: Texture2D = ENEMY_TEXTURE var source: Rect2 var destination: Rect2 var size: float = ENEMY_DATA[enemy.type].size * enemy.size_scale if enemy_type < 3: var source_width: float = ENEMY_TEXTURE.get_width() / 3.0 source = Rect2(source_width * enemy_type, 0, source_width, ENEMY_TEXTURE.get_height()) destination = Rect2(-size * 0.5, -size * 0.5, size, size) else: enemy_texture = [ENEMY_INTERCEPTOR_TEXTURE, ENEMY_SHIELD_CARRIER_TEXTURE, ENEMY_SIEGE_DESTROYER_TEXTURE][enemy_type - 3] source = Rect2(Vector2.ZERO, enemy_texture.get_size()) destination = Rect2(-size * 0.75, -size * 0.5, size * 1.5, size) var tint := Color.WHITE if enemy.flash <= 0.0 else Color("ffb6a3") if bool(enemy.get("shield", false)): draw_circle(enemy.pos, size * 0.63, Color(0.25, 0.80, 1.0, 0.12)) draw_arc(enemy.pos, size * 0.63, 0.0, TAU, 28, Color(0.38, 0.86, 1.0, 0.76), 2.2) draw_set_transform(enemy.pos - camera_offset, enemy.angle, Vector2.ONE) draw_texture_rect_region(enemy_texture, destination, source, tint) draw_set_transform(-camera_offset, 0.0, Vector2.ONE) var bar_width := size * 0.72 var ratio: float = clampf(enemy.hp / enemy.max_hp, 0.0, 1.0) draw_rect(Rect2(enemy.pos + Vector2(-bar_width * 0.5, -size * 0.48), Vector2(bar_width, 5)), Color("261923")) draw_rect(Rect2(enemy.pos + Vector2(-bar_width * 0.5, -size * 0.48), Vector2(bar_width * ratio, 5)), Color("ff6049")) if bool(enemy.get("shield", false)): draw_string(ThemeDB.fallback_font, enemy.pos + Vector2(-30, -size * 0.62), "SCHILD", HORIZONTAL_ALIGNMENT_CENTER, 60, 9, Color("75eaff")) if enemy.boss: draw_string(ThemeDB.fallback_font, enemy.pos + Vector2(-45, -size * 0.58), "KOMMANDOSCHIFF", HORIZONTAL_ALIGNMENT_CENTER, 90, 11, Color("ffab78")) func _draw_hud() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(0, 0, 1600, 90), Color(0.015, 0.035, 0.07, 0.92)) draw_line(Vector2(0, 90), Vector2(1600, 90), Color("2e7593"), 2.0) draw_string(font, Vector2(25, 35), "VOID BASTION", HORIZONTAL_ALIGNMENT_LEFT, -1, 25, Color("bcefff")) draw_string(font, Vector2(25, 65), "ENERGIE %04d" % _player_credits(_local_credit_slot()), HORIZONTAL_ALIGNMENT_LEFT, -1, 20, Color("65eaff")) draw_string(font, Vector2(310, 52), "REAKTOR %02d" % max(reactor_lives, 0), HORIZONTAL_ALIGNMENT_LEFT, -1, 22, Color("8feeff")) draw_string(font, Vector2(535, 52), "WELLE %02d/%02d" % [wave, MAX_WAVES], HORIZONTAL_ALIGNMENT_LEFT, -1, 22, Color.WHITE) var partner_slot := 1 - _local_credit_slot() var partner_name := "PARTNER" if coop_online_mode else "KI" var map_label := "KOOP-ARENA • %s %04d EN" % [partner_name, _player_credits(partner_slot)] if coop_mode else "KARTE %s" % MAP_NAMES[selected_map] draw_string(font, Vector2(715, 52), map_label, HORIZONTAL_ALIGNMENT_LEFT, -1, 15, Color("ffcf7a") if coop_mode else Color("91a8bc")) draw_string(font, Vector2(920, 34), _difficulty().name, HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color("ffcf7a")) draw_string(font, Vector2(920, 62), "KERNKRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color("89f4ff")) _draw_button(back_button, "ZURÜCK", false, Color("26394a")) if coop_mode: var viewing_right := camera_offset.x > 0.0 var viewing_own_side := (viewing_right and _local_credit_slot() == 1) or (not viewing_right and _local_credit_slot() == 0) var switch_label := ("< PARTNER" if viewing_right else "PARTNER >") if viewing_own_side else ("< ICH" if viewing_right else "ICH >") _draw_button(coop_side_button, switch_label, false, Color("604827")) _draw_button(speed_button, "%.0f×" % game_speed, game_speed > 1.0) _draw_button(pause_button, "II" if game_mode == "playing" else ">", false) _draw_button(wave_button, "WELLE %d STARTEN" % (wave + 1) if not wave_active else "%d IM ANFLUG" % (enemies.size() + enemies_to_spawn), wave_active) if tutorial_completed and ads_manager.can_offer_rewarded(): _draw_button(reward_ad_button, "VIDEO: +150 NOTFALLENERGIE", false, Color("285044")) draw_rect(Rect2(0, 760, 1600, 140), Color(0.012, 0.028, 0.055, 0.97)) draw_line(Vector2(0, 760), Vector2(1600, 760), Color("2e7593"), 2.0) for index in tower_buttons.size(): _draw_tower_card(index, index, selected_tower_type == index) if selected_tower_index >= 0 and selected_tower_index < towers.size(): var tower: Dictionary = towers[selected_tower_index] var upgrade_label := "MAX. STUFE" if tower.level >= 3 else "UPGRADE %d" % _upgrade_cost(tower) _draw_button(upgrade_button, upgrade_label, false) _draw_button(sell_button, "VERKAUFEN", false, Color("6b3542")) if selected_tower_type >= 0 or selected_tower_index >= 0: _draw_tower_info_panel() draw_rect(auto_wave_button, Color("102d40"), true) draw_rect(auto_wave_button, Color("37677b"), false, 1.0) draw_rect(Rect2(auto_wave_button.position + Vector2(7, 4), Vector2(14, 14)), Color("07131f"), true) if auto_start_waves: draw_rect(Rect2(auto_wave_button.position + Vector2(10, 7), Vector2(8, 8)), Color("65eaff"), true) draw_string(font, auto_wave_button.position + Vector2(29, 16), "NÄCHSTE WELLE AUTO STARTEN", HORIZONTAL_ALIGNMENT_LEFT, 220, 11, Color("dceaf0")) if speed_dropdown_open: for speed_index in SPEED_OPTIONS.size(): var option_rect := _speed_option_rect(speed_index) draw_rect(option_rect, Color("102d40"), true) draw_rect(option_rect, Color("37677b"), false, 1.0) draw_string(font, option_rect.position + Vector2(0, 18), "%.0f×" % SPEED_OPTIONS[speed_index], HORIZONTAL_ALIGNMENT_CENTER, option_rect.size.x, 13, Color("8ff4ff") if is_equal_approx(game_speed, SPEED_OPTIONS[speed_index]) else Color.WHITE) if status_timer > 0.0: draw_rect(Rect2(560, 108, 480, 44), Color(0.01, 0.035, 0.07, 0.88), true) draw_string(font, Vector2(570, 138), status_text, HORIZONTAL_ALIGNMENT_CENTER, 460, 18, Color("a9efff")) func _speed_option_rect(index: int) -> Rect2: return Rect2(speed_button.position.x, speed_button.end.y + 2.0 + index * 28.0, speed_button.size.x, 27.0) func _draw_tower_card(slot: int, type: int, active: bool) -> void: var rect: Rect2 = tower_buttons[slot] var data: Dictionary = TOWER_DATA[type] var font := ThemeDB.fallback_font var unlocked := unlocked_towers.has(type) draw_rect(rect, Color("226c88") if active else (Color("173c55") if unlocked else Color("102b3b")), true) draw_rect(rect, Color("65dfff") if active else (Color("37677b") if unlocked else Color("29485a")), false, 1.5) draw_string(font, rect.position + Vector2(9, 22), data.name, HORIZONTAL_ALIGNMENT_LEFT, 150, 15, Color.WHITE if unlocked else Color("9ab0ba")) draw_string(font, rect.position + Vector2(158, 22), "%d EN" % data.cost, HORIZONTAL_ALIGNMENT_RIGHT, 86, 12, Color("7ce7ff")) draw_string(font, rect.position + Vector2(9, 47), "SCH %.0f • %.2f SCHUSS/S" % [data.damage, _tower_shots_per_second(type, 1)], HORIZONTAL_ALIGNMENT_LEFT, 237, 11, Color("dceaf0")) draw_string(font, rect.position + Vector2(9, 72), "RW %.0f • %s" % [data.range, data.damage_type], HORIZONTAL_ALIGNMENT_LEFT, 237, 10, Color("9fdce9")) var hint := "ANTIPPEN • BAUPLATZ WÄHLEN" if unlocked else "GESPERRT • IM SHOP FREISCHALTEN" draw_string(font, rect.position + Vector2(9, 105), hint, HORIZONTAL_ALIGNMENT_LEFT, 237, 10, Color("719aaa") if unlocked else Color("ffcf7a")) func _draw_store() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.70), true) _draw_button(shop_back_button, "< ZURÜCK", false, Color("26394a")) draw_string(font, Vector2(0, 115), "GERGAMES-ARSENAL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff")) draw_string(font, Vector2(0, 160), "Türme und Einsatzsektoren mit Kernkristallen freischalten", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8feeff")) draw_string(font, Vector2(1270, 55), "KERNKRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_RIGHT, 270, 20, Color("8ff4ff")) _draw_button(store_tower_tab_button, "TÜRME", store_tab == "towers", Color("24475b")) _draw_button(store_map_tab_button, "KARTEN", store_tab == "maps", Color("24475b")) if store_tab == "maps": for slot in map_shop_purchase_buttons.size(): var map_index := slot + 3 var button: Rect2 = map_shop_purchase_buttons[slot] var card := Rect2(button.position.x, 225, 350, 405) var is_unlocked := unlocked_maps.has(map_index) draw_rect(card, Color(0.012, 0.04, 0.07, 0.94), true) draw_rect(card, Color("65dfff", 0.72), false, 2.0) draw_texture_rect(MAP_TEXTURES[map_index], Rect2(card.position + Vector2(12, 12), Vector2(326, 176)), false) draw_string(font, card.position + Vector2(10, 222), MAP_NAMES[map_index], HORIZONTAL_ALIGNMENT_CENTER, 330, 17, Color("9feeff")) draw_string(font, card.position + Vector2(10, 250), MAP_DESCRIPTIONS[map_index], HORIZONTAL_ALIGNMENT_CENTER, 330, 13, Color("dceaf0")) draw_string(font, card.position + Vector2(10, 282), "80-PX-RASTER • DIREKTZIEL REAKTOR", HORIZONTAL_ALIGNMENT_CENTER, 330, 10, Color("8fddef")) var button_text := "FREIGESCHALTET" if is_unlocked else "FREISCHALTEN • %d KERNKRISTALLE" % int(MAP_UNLOCK_COSTS[map_index]) _draw_button(button, button_text, is_unlocked, Color("24516a") if not is_unlocked else Color("245843")) draw_string(font, Vector2(0, 685), "Jeder Sektor hat einen eigenen, rastergenauen Laufweg bis in den Reaktor.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("ffcf7a")) return var textures: Array[Texture2D] = [PLASMA_TEXTURE, EMP_TEXTURE, RAILGUN_TEXTURE] for slot in shop_purchase_buttons.size(): var tower_type := slot + 3 var card := Rect2(shop_purchase_buttons[slot].position.x, 215, 360, 400) var data: Dictionary = TOWER_DATA[tower_type] draw_rect(card, Color(0.012, 0.04, 0.07, 0.94), true) draw_rect(card, Color(data.color, 0.72), false, 2.0) draw_texture_rect(textures[slot], Rect2(card.position + Vector2(112, 18), Vector2(136, 136)), false) draw_string(font, card.position + Vector2(20, 190), data.name, HORIZONTAL_ALIGNMENT_CENTER, 320, 23, Color(data.color)) draw_string(font, card.position + Vector2(22, 224), "SCH %.0f • %.2f SCHUSS/S" % [data.damage, _tower_shots_per_second(tower_type, 1)], HORIZONTAL_ALIGNMENT_CENTER, 316, 15, Color.WHITE) draw_string(font, card.position + Vector2(22, 251), "RW %.0f • %s" % [data.range, data.damage_type], HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("9fdce9")) draw_string(font, card.position + Vector2(22, 295), data.description, HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("c4d7df")) var is_unlocked := unlocked_towers.has(tower_type) var button_text := "FREIGESCHALTET" if is_unlocked else "FREISCHALTEN • %d KERNKRISTALLE" % int(data.unlock_cost) _draw_button(shop_purchase_buttons[slot], button_text, is_unlocked, Color("24516a") if not is_unlocked else Color("245843")) draw_string(font, Vector2(0, 685), "Weitere Verteidigungen lassen sich dauerhaft mit Kernkristallen freischalten.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("ffcf7a")) func _draw_tower_info_panel() -> void: var type := selected_tower_type var level := 1 var is_built := selected_tower_index >= 0 and selected_tower_index < towers.size() if is_built: var tower: Dictionary = towers[selected_tower_index] type = tower.type level = tower.level if type < 0: return var data: Dictionary = TOWER_DATA[type] var font := ThemeDB.fallback_font draw_rect(tower_info_rect, Color(0.008, 0.028, 0.055, 0.58), true) draw_rect(tower_info_rect, Color(0.29, 0.60, 0.70, 0.72), false, 1.5) var heading := "%s • STUFE %d" % [data.name, level] if is_built else "%s • BAUVORSCHAU" % data.name draw_string(font, tower_info_rect.position + Vector2(10, 20), heading, HORIZONTAL_ALIGNMENT_LEFT, 280, 13, Color("a9efff")) draw_string(font, tower_info_rect.position + Vector2(10, 41), "SCH %.0f • %.2f/S • RW %.0f" % [_tower_damage(type, level), _tower_shots_per_second(type, level), _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 280, 10, Color.WHITE) if is_built and level < 3: var next_level := level + 1 draw_string(font, tower_info_rect.position + Vector2(10, 62), "UPGRADE +%.0f SCH +%.2f/S +%.0f RW" % [_tower_damage(type, next_level) - _tower_damage(type, level), _tower_shots_per_second(type, next_level) - _tower_shots_per_second(type, level), _tower_range(type, next_level) - _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca")) elif is_built: draw_string(font, tower_info_rect.position + Vector2(10, 62), "MAXIMALE AUSBAUSTUFE", HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca")) else: 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: var font := ThemeDB.fallback_font var color: Color = Color("226c88") if active else base draw_rect(rect, color, true) draw_rect(rect, Color("65dfff") if active else Color("37677b"), false, 2.0) var lines := text.split("\n") if lines.size() == 1: draw_string(font, rect.position + Vector2(0, rect.size.y * 0.62), lines[0], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 17, Color.WHITE) else: draw_string(font, rect.position + Vector2(0, 28), lines[0], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 18, Color.WHITE) draw_string(font, rect.position + Vector2(0, 54), lines[1], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 14, Color("7ce7ff")) func _draw_menu() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.52)) draw_string(font, Vector2(0, 145), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 64, Color("c7f5ff")) draw_string(font, Vector2(0, 202), "ORBITAL DEFENSE PROTOCOL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color("56cce9")) _draw_button(profile_button, "%s • PROFIL" % account_manager.display_name, false, Color("233f57")) if menu_step == "map": draw_string(font, Vector2(0, 300), "EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 20, Color("8feeff")) for index in map_buttons.size(): var card: Rect2 = map_buttons[index] var map_index := index var is_unlocked := unlocked_maps.has(map_index) draw_texture_rect(MAP_TEXTURES[map_index], card, false, Color(0.82, 0.88, 0.94, 1.0) if is_unlocked else Color(0.38, 0.46, 0.54, 1.0)) draw_rect(card, Color(0.005, 0.015, 0.035, 0.22 if is_unlocked else 0.48), true) draw_rect(card, Color("70eaff") if selected_map == map_index and is_unlocked else Color("436574"), false, 4.0 if selected_map == map_index and is_unlocked else 2.0) var footer_y := card.size.y - 52.0 draw_rect(Rect2(card.position + Vector2(0, footer_y), Vector2(card.size.x, 52)), Color(0.01, 0.025, 0.05, 0.88), true) draw_string(font, card.position + Vector2(0, footer_y + 21), MAP_NAMES[map_index], HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 14, Color.WHITE) var description: String = MAP_DESCRIPTIONS[map_index] if is_unlocked else "GESPERRT • %d KERNKRISTALLE" % int(MAP_UNLOCK_COSTS[map_index]) draw_string(font, card.position + Vector2(0, footer_y + 42), description, HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 11, Color("8fddef") if is_unlocked else Color("ffcf7a")) draw_string(font, Vector2(0, 720), "Wähle einen Sektor, um Schwierigkeit und Missionsstart festzulegen.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("d7e2ea")) else: _draw_button(map_step_back_button, "< KARTE", false, Color("26394a")) var selected_card := Rect2(500, 315, 600, 145) draw_texture_rect(MAP_TEXTURES[selected_map], selected_card, false, Color(0.86, 0.90, 0.95, 1.0)) draw_rect(selected_card, Color(0.005, 0.015, 0.035, 0.34), true) draw_rect(selected_card, Color("70eaff"), false, 3.0) draw_rect(Rect2(selected_card.position + Vector2(0, 92), Vector2(selected_card.size.x, 53)), Color(0.01, 0.025, 0.05, 0.88), true) draw_string(font, selected_card.position + Vector2(0, 116), MAP_NAMES[selected_map], HORIZONTAL_ALIGNMENT_CENTER, selected_card.size.x, 18, Color.WHITE) draw_string(font, selected_card.position + Vector2(0, 138), MAP_DESCRIPTIONS[selected_map], HORIZONTAL_ALIGNMENT_CENTER, selected_card.size.x, 13, Color("8fddef")) draw_string(font, Vector2(0, 490), "SCHWIERIGKEIT WÄHLEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("9fdce9")) for index in difficulty_buttons.size(): _draw_button(difficulty_buttons[index], DIFFICULTY_DATA[index].name, selected_difficulty == index, Color("27394d")) draw_string(font, Vector2(0, 603), DIFFICULTY_DESCRIPTIONS[selected_difficulty], HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a")) _draw_button(start_button, "MISSION STARTEN", false, Color("18556f")) draw_string(font, Vector2(0, 755), "KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d" % [int(_difficulty().crystals), best_wave, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("86c8d8")) _draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b")) _draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75")) _draw_button(shop_button, "ARSENAL / SHOP", false, Color("24516a")) _draw_button(audio_settings_button, "EINSTELLUNGEN", false, Color("24516a")) _draw_button(exit_button, "SPIEL BEENDEN", false, Color("5c3342")) 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_audio_settings() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.002, 0.008, 0.02, 0.62), true) draw_rect(audio_panel_rect, Color(0.01, 0.035, 0.065, 0.98), true) draw_rect(audio_panel_rect, Color("4ab8d1"), false, 2.0) draw_string(font, audio_panel_rect.position + Vector2(30, 54), "AUDIOEINSTELLUNGEN", HORIZONTAL_ALIGNMENT_LEFT, 450, 25, Color("bcefff")) _draw_button(audio_close_button, "SCHLIESSEN", false, Color("26394a")) draw_string(font, Vector2(audio_slider_rect.position.x, audio_slider_rect.position.y - 18), "MASTER-LAUTSTÄRKE", HORIZONTAL_ALIGNMENT_LEFT, 260, 16, Color("dceaf0")) draw_string(font, Vector2(audio_slider_rect.end.x - 80, audio_slider_rect.position.y - 18), "%d%%" % roundi(master_volume * 100.0), HORIZONTAL_ALIGNMENT_RIGHT, 80, 16, Color("8ff4ff")) var track := Rect2(audio_slider_rect.position.x, audio_slider_rect.position.y + 13, audio_slider_rect.size.x, 6) draw_rect(track, Color("24475b"), true) draw_rect(Rect2(track.position, Vector2(track.size.x * master_volume, track.size.y)), Color("45d4ee"), true) var knob_position := Vector2(track.position.x + track.size.x * master_volume, track.position.y + track.size.y * 0.5) draw_circle(knob_position, 13, Color("0b1d2c")) draw_circle(knob_position, 9, Color("8ff4ff")) draw_string(font, Vector2(audio_panel_rect.position.x + 30, audio_panel_rect.end.y - 28), "Die Einstellung wird automatisch gespeichert.", HORIZONTAL_ALIGNMENT_LEFT, audio_panel_rect.size.x - 60, 13, Color("91a8bc")) func _draw_leaderboard() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.84)) _draw_button(leaderboard_back_button, "< ZURÜCK", false, Color("26394a")) _draw_button(leaderboard_refresh_button, "AKTUALISIEREN", leaderboard_loading, Color("24516a")) draw_string(font, Vector2(0, 108), "GLOBALE BESTENLISTE", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff")) _draw_button(leaderboard_map_previous_button, "<", false, Color("26394a")) _draw_button(leaderboard_map_button, "KARTE • %s" % MAP_NAMES[leaderboard_map], true, Color("24475b")) _draw_button(leaderboard_map_next_button, ">", false, Color("26394a")) _draw_button(leaderboard_difficulty_previous_button, "<", false, Color("26394a")) _draw_button(leaderboard_difficulty_button, "STUFE • %s" % DIFFICULTY_DATA[leaderboard_difficulty].name, true, Color("5b452b")) _draw_button(leaderboard_difficulty_next_button, ">", false, Color("26394a")) _draw_button(leaderboard_mission_button, "MISSION", not leaderboard_endless, Color("24475b")) _draw_button(leaderboard_endless_button, "ENDLOSMODUS", leaderboard_endless, Color("5b452b")) var panel := Rect2(205, 280, 1190, 505) draw_rect(panel, Color(0.012, 0.035, 0.065, 0.96), true) draw_rect(panel, Color("39788e"), false, 2.0) draw_string(font, Vector2(235, 319), "RANG", HORIZONTAL_ALIGNMENT_LEFT, 75, 13, Color("91a8bc")) draw_string(font, Vector2(325, 319), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 270, 13, Color("91a8bc")) draw_string(font, Vector2(610, 319), "KARTE", HORIZONTAL_ALIGNMENT_LEFT, 240, 13, Color("91a8bc")) draw_string(font, Vector2(850, 319), "STUFE", HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("91a8bc")) draw_string(font, Vector2(995, 319), "WELLE", HORIZONTAL_ALIGNMENT_CENTER, 110, 13, Color("91a8bc")) draw_string(font, Vector2(1160, 319), "PUNKTE", HORIZONTAL_ALIGNMENT_RIGHT, 190, 13, Color("91a8bc")) draw_line(Vector2(230, 334), Vector2(1370, 334), Color("315c70"), 1.0) var own_user_id: String = str(online_manager.session.user_id) if online_manager.session != null else "" for index in leaderboard_rows.size(): var row: Dictionary = leaderboard_rows[index] var y := 370.0 + index * 32.0 if index % 2 == 0: draw_rect(Rect2(225, y - 23, 1150, 29), Color(0.03, 0.09, 0.14, 0.64), true) var owner_color := Color("83ffc8") if not own_user_id.is_empty() and str(row.get("owner_id", "")) == own_user_id else Color.WHITE draw_string(font, Vector2(240, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 65, 14, Color("8ff4ff")) draw_string(font, Vector2(325, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 270, 14, owner_color) draw_string(font, Vector2(610, y), MAP_NAMES[int(row.get("map", leaderboard_map))], HORIZONTAL_ALIGNMENT_LEFT, 225, 13, Color("9fdce9")) draw_string(font, Vector2(850, y), DIFFICULTY_DATA[int(row.get("difficulty", leaderboard_difficulty))].name, HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("ffcf7a")) draw_string(font, Vector2(995, y), "%02d" % int(row.get("wave", 0)), HORIZONTAL_ALIGNMENT_CENTER, 110, 14, Color("ffcf7a")) draw_string(font, Vector2(1160, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, Color("8fffea")) if not leaderboard_message.is_empty(): draw_string(font, Vector2(300, 520), leaderboard_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 17, Color("ffcf7a")) draw_string(font, Vector2(0, 810), "Spielernamen werden aus dem aktuellen öffentlichen Profil geladen.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("91a8bc")) draw_string(font, Vector2(0, 838), "WERTUNG: WELLEN + REAKTORREST + RESTENERGIE • WENIGER GEBAUTE TÜRME ERHÖHEN DEN EFFIZIENZ-MULTIPLIKATOR", HORIZONTAL_ALIGNMENT_CENTER, 1600, 12, Color("75aaba")) func _draw_multiplayer_menu() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.78)) _draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a")) draw_string(font, Vector2(0, 105), "KOOPERATIVE VERTEIDIGUNG", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff")) draw_string(font, Vector2(0, 142), "ZWEI KOMMANDANTEN • EIN GEMEINSAMER REAKTOR", HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("66dff3")) var arena := Rect2(170, 180, 1260, 410) draw_rect(arena, Color(0.01, 0.04, 0.075, 0.96), true) draw_rect(arena, Color("39788e"), false, 2.0) draw_line(Vector2(800, 200), Vector2(800, 570), Color(0.35, 0.82, 1.0, 0.24), 2.0) _draw_coop_lane_preview(false) _draw_coop_lane_preview(true) for radius in [66.0, 48.0, 30.0]: draw_arc(Vector2(800, 385), radius, 0.0, TAU, 48, Color(0.25, 0.92, 1.0, 0.78), 3.0) draw_circle(Vector2(800, 385), 18.0, Color("8ffaff")) draw_string(font, Vector2(665, 490), "GEMEINSAMER REAKTOR", HORIZONTAL_ALIGNMENT_CENTER, 270, 15, Color("8ff4ff")) draw_string(font, Vector2(215, 225), "%s • ENERGIE %d" % [account_manager.display_name, multiplayer_manager.player_credits[0]], HORIZONTAL_ALIGNMENT_LEFT, 440, 17, Color("8ff4ff")) draw_string(font, Vector2(945, 225), "MITSPIELER • ENERGIE %d" % multiplayer_manager.player_credits[1], HORIZONTAL_ALIGNMENT_RIGHT, 440, 17, Color("ffcf7a")) draw_string(font, Vector2(570, 555), "REAKTORLEBEN %02d • GETRENNTE RESSOURCEN" % multiplayer_manager.shared_reactor_lives, HORIZONTAL_ALIGNMENT_CENTER, 460, 16, Color.WHITE) _draw_button(quick_match_button, "SCHNELLES SPIEL", false, Color("185f68")) _draw_button(friend_match_button, "RAUM BEITRETEN", false, Color("254f75")) _draw_button(private_match_button, "RAUM ERSTELLEN", false, Color("493f67")) _draw_button(ai_match_button, "MIT KI-PARTNER", false, Color("6b4b28")) var service_text := "GERGAMES-SERVER VERBUNDEN • ONLINE-KOOP IST SPIELBEREIT" if online_manager.is_online() else "VERBINDUNG ZUM GERGAMES-SERVER WIRD AUTOMATISCH AUFGEBAUT" var service_color := Color("83ffc8") if online_manager.is_online() else Color("ffcf7a") draw_string(font, Vector2(0, 770), service_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, service_color) func _draw_room_code_entry() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88)) _draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a")) draw_string(font, Vector2(0, 205), "PRIVATEN RAUM BEITRETEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff")) draw_string(font, Vector2(0, 260), "GIB DEN SECHSSTELLIGEN CODE DEINES MITPILOTEN EIN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("66dff3")) draw_rect(room_code_input_rect, Color(0.01, 0.035, 0.065, 0.98), true) draw_rect(room_code_input_rect, Color("65eaff") if room_code_input.length() == 6 else Color("39788e"), false, 3.0) var shown_code := room_code_input if shown_code.is_empty(): shown_code = "------" draw_string(font, Vector2(room_code_input_rect.position.x, room_code_input_rect.position.y + 61), " ".join(shown_code.split("")), HORIZONTAL_ALIGNMENT_CENTER, room_code_input_rect.size.x, 38, Color("8ffaff") if not room_code_input.is_empty() else Color("688293")) draw_string(font, Vector2(0, 470), "BUCHSTABEN UND ZAHLEN • GROSS-/KLEINSCHREIBUNG IST EGAL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("9fdce9")) _draw_button(join_room_button, "RAUM BEITRETEN", room_code_input.length() == 6, Color("185f68") if room_code_input.length() == 6 else Color("26394a")) func _draw_coop_lane_preview(mirrored: bool) -> void: var points := PackedVector2Array([Vector2(205, 300), Vector2(505, 300), Vector2(505, 385), Vector2(720, 385)]) if mirrored: points = multiplayer_manager.mirrored_path(points, 1600.0) for index in points.size() - 1: draw_line(points[index], points[index + 1], Color(0.18, 0.34, 0.42, 0.95), 54.0, true) draw_line(points[index], points[index + 1], Color(0.52, 0.82, 0.88, 0.42), 2.0, true) for index in 5: var x := 300.0 + index * 78.0 if mirrored: x = 1600.0 - x draw_rect(Rect2(Vector2(x - 27, 465), Vector2(54, 54)), Color(0.08, 0.42, 0.55, 0.12), true) draw_rect(Rect2(Vector2(x - 27, 465), Vector2(54, 54)), Color(0.28, 0.72, 0.82, 0.36), false, 1.0) func _draw_matchmaking() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88)) _draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a")) var private_mode: bool = multiplayer_manager.matchmaking_mode in ["private_host", "private_join"] draw_string(font, Vector2(0, 210), "PRIVATER KOOP-RAUM" if private_mode else "MITSPIELERSUCHE", HORIZONTAL_ALIGNMENT_CENTER, 1600, 46, Color("c7f5ff")) var in_lobby: bool = multiplayer_manager.player_slot >= 0 and multiplayer_manager.matchmaking_state in ["hosting", "matched", "lobby", "reconnected"] if in_lobby: _draw_online_lobby_players(font) else: var pulse := 56.0 + sin(elapsed * 3.0) * 8.0 draw_arc(Vector2(800, 390), pulse, -PI * 0.2, PI * 1.45, 52, Color("65eaff"), 5.0) draw_circle(Vector2(800, 390), 22.0, Color("8ffaff")) var mode_labels := {"quick": "SCHNELLES SPIEL", "private_host": "RAUM ERSTELLEN", "private_join": "RAUM BEITRETEN"} var mode_text: String = mode_labels.get(multiplayer_manager.matchmaking_mode, "KOOP-SPIEL") if not multiplayer_manager.private_room_code.is_empty(): mode_text = "RAUMCODE %s" % " ".join(multiplayer_manager.private_room_code.split("")) draw_string(font, Vector2(0, 500), mode_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE) if in_lobby: _draw_button(lobby_ready_button, "NICHT BEREIT" if multiplayer_manager.local_ready else "BEREIT", multiplayer_manager.local_ready, Color("8b6330") if multiplayer_manager.local_ready else Color("185f68")) _draw_button(cancel_search_button, "ZURÜCK" if multiplayer_manager.matchmaking_state == "room_not_found" else "SUCHE ABBRECHEN", false, Color("5c3342")) var online_text := "ONLINE • SUCHE NACH EINEM MITPILOTEN" var online_color := Color("83ffc8") if multiplayer_manager.matchmaking_state == "waiting_for_service": online_text = "VERBINDUNG ZUM GERGAMES-SERVER WIRD AUFGEBAUT …" online_color = Color("ffcf7a") elif multiplayer_manager.matchmaking_state == "offline": online_text = "SERVER NOCH NICHT ERREICHBAR • BITTE SPÄTER ERNEUT VERSUCHEN" online_color = Color("ff8e8e") elif multiplayer_manager.matchmaking_state == "matched": online_text = "MITPILOT GEFUNDEN • WÄHLE BEREIT" online_color = Color("8ffaff") elif multiplayer_manager.matchmaking_state in ["lobby", "reconnected"]: online_text = "BEIDE SPIELER MÜSSEN BEREIT SEIN" online_color = Color("8ffaff") elif multiplayer_manager.matchmaking_state == "reconnecting": online_text = "VERBINDUNG WIRD WIEDERHERGESTELLT ." online_color = Color("ffcf7a") elif multiplayer_manager.matchmaking_state == "connection_lost": online_text = "VERBINDUNG VERLOREN • BITTE ZURÜCKKEHREN" online_color = Color("ff8e8e") elif multiplayer_manager.matchmaking_state == "creating_room": online_text = "PRIVATER RAUM WIRD ERSTELLT ." online_color = Color("ffcf7a") elif multiplayer_manager.matchmaking_state == "hosting": online_text = "TEILE DEN RAUMCODE MIT DEINEM MITPILOTEN • WARTE AUF BEITRITT" online_color = Color("83ffc8") elif multiplayer_manager.matchmaking_state == "joining_room": online_text = "PRIVATER RAUM WIRD GESUCHT ." online_color = Color("ffcf7a") elif multiplayer_manager.matchmaking_state == "room_not_found": online_text = "RAUM NICHT GEFUNDEN ODER BEREITS VOLL" online_color = Color("ff8e8e") draw_rect(Rect2(180, 520, 1240, 44), Color(0.005, 0.015, 0.035, 0.98), true) draw_string(font, Vector2(0, 548), online_text, HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, online_color) func _draw_online_lobby_players(font: Font) -> void: for slot in 2: var rect := Rect2(270 + slot * 560, 285, 500, 130) var player = multiplayer_manager.lobby_players[slot] if slot < multiplayer_manager.lobby_players.size() else null var occupied: bool = player is Dictionary var connected: bool = occupied and bool(player.get("connected", false)) var ready: bool = occupied and bool(player.get("ready", false)) var border := Color("5de8a8") if ready else (Color("65eaff") if connected else Color("425567")) draw_rect(rect, Color(0.012, 0.035, 0.065, 0.96), true) draw_rect(rect, border, false, 3.0) var name := String(player.get("name", "PILOT")) if occupied else "WARTE AUF MITPILOTEN" var side := "LINKE VERTEIDIGUNG" if slot == 0 else "RECHTE VERTEIDIGUNG" var state := "BEREIT" if ready else ("NICHT BEREIT" if connected else "NICHT VERBUNDEN") draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 43), name.to_upper(), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 24, Color("e8fbff")) draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 78), side, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 14, Color("8fcddd")) draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 108), state, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 16, border) func _draw_profile() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.76)) _draw_button(profile_back_button, "< ZURÜCK", false, Color("26394a")) draw_string(font, Vector2(0, 112), "PILOTENPROFIL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 44, Color("c7f5ff")) draw_string(font, Vector2(1110, 112), "VERSION %s" % _app_version(), HORIZONTAL_ALIGNMENT_RIGHT, 290, 14, Color("91a8bc")) draw_rect(Rect2(170, 155, 1260, 600), Color(0.012, 0.035, 0.065, 0.94), true) draw_rect(Rect2(170, 155, 1260, 600), Color("4ab8d1"), false, 2.0) draw_string(font, Vector2(220, 220), account_manager.display_name, HORIZONTAL_ALIGNMENT_LEFT, 520, 30, Color("8ff4ff")) draw_string(font, Vector2(220, 258), account_manager.provider_label(), HORIZONTAL_ALIGNMENT_LEFT, 650, 16, Color("ffcf7a")) draw_string(font, Vector2(220, 292), "SPIELER-ID %s" % account_manager.short_id(), HORIZONTAL_ALIGNMENT_LEFT, 650, 14, Color("91a8bc")) _draw_button(profile_rename_button, "SPIELERNAME ÄNDERN", false, Color("24516a")) draw_string(font, Vector2(840, 215), "KONTO & CLOUD", HORIZONTAL_ALIGNMENT_LEFT, 470, 18, Color("8feeff")) var cloud_label := "GERGAMES ONLINE VERBUNDEN" if online_manager.is_authenticated() else "GERGAMES ONLINE OFFLINE" var cloud_color := Color("83ffc8") if online_manager.is_authenticated() else Color("ffcf7a") draw_string(font, Vector2(840, 252), cloud_label, HORIZONTAL_ALIGNMENT_LEFT, 470, 15, cloud_color) draw_string(font, Vector2(840, 282), "GAME CENTER / APPLE NÄCHSTE STUFE", HORIZONTAL_ALIGNMENT_LEFT, 470, 15, Color("d7e2ea")) draw_string(font, Vector2(840, 312), "GOOGLE PLAY GAMES NÄCHSTE STUFE", HORIZONTAL_ALIGNMENT_LEFT, 470, 15, Color("d7e2ea")) draw_line(Vector2(220, 345), Vector2(1380, 345), Color("315c70"), 1.0) draw_string(font, Vector2(220, 392), "LOKALE BESTWERTE", HORIZONTAL_ALIGNMENT_LEFT, 1160, 20, Color("8feeff")) draw_string(font, Vector2(220, 430), "EINSATZSEKTOR", HORIZONTAL_ALIGNMENT_LEFT, 420, 14, Color("91a8bc")) draw_string(font, Vector2(700, 430), "MISSION", HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc")) draw_string(font, Vector2(930, 430), "ENDLOS", HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc")) draw_string(font, Vector2(1160, 430), "BESTE PUNKTE", HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc")) for index in MAP_NAMES.size(): var y := 466.0 + index * 28.0 draw_rect(Rect2(210, y - 19, 1180, 25), Color(0.03, 0.09, 0.14, 0.70), true) draw_string(font, Vector2(235, y), MAP_NAMES[index], HORIZONTAL_ALIGNMENT_LEFT, 420, 12, Color.WHITE) draw_string(font, Vector2(700, y), "WELLE %02d" % account_manager.get_map_best_wave(index, false), HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("8ff4ff")) draw_string(font, Vector2(930, y), "WELLE %02d" % account_manager.get_map_best_wave(index, true), HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("ffcf7a")) var score := maxi(account_manager.get_map_best_score(index, false), account_manager.get_map_best_score(index, true)) draw_string(font, Vector2(1160, y), "%07d" % score, HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("8fffea")) if not profile_message.is_empty(): draw_string(font, Vector2(220, 658), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1160, 14, Color("ffcf7a")) _draw_button(profile_support_button, "SUPPORT / FEEDBACK", false, Color("24516a")) _draw_button(profile_privacy_button, "DATENSCHUTZ", false, Color("24475b")) _draw_button(profile_delete_button, "LÖSCHEN BESTÄTIGEN" if profile_delete_armed else "KONTO LÖSCHEN", profile_delete_armed, Color("6b2d3b")) func _draw_profile_name_entry() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.82)) _draw_button(profile_back_button, "< ZURÜCK", false, Color("26394a")) draw_string(font, Vector2(0, 190), "SPIELERNAME", HORIZONTAL_ALIGNMENT_CENTER, 1600, 44, Color("c7f5ff")) draw_string(font, Vector2(0, 250), "Dieser Name ist für Mitspieler und in Ranglisten sichtbar.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("a9d8e5")) draw_rect(profile_name_input_rect, Color(0.012, 0.035, 0.065, 0.96), true) draw_rect(profile_name_input_rect, Color("65dfff"), false, 3.0) draw_string(font, profile_name_input_rect.position + Vector2(24, 53), profile_name_input, HORIZONTAL_ALIGNMENT_CENTER, profile_name_input_rect.size.x - 48, 26, Color.WHITE) draw_string(font, Vector2(0, 448), "3–20 Zeichen · Buchstaben, Zahlen, Leerzeichen, _ und -", HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("91a8bc")) _draw_button(profile_name_save_button, "SPIELERNAME SPEICHERN", false, Color("185f68")) if not profile_message.is_empty(): draw_string(font, Vector2(300, 585), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 16, Color("ffcf7a")) func _draw_overlay(title: String, subtitle: String) -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.01, 0.03, 0.72)) draw_string(font, Vector2(0, 390), title, HORIZONTAL_ALIGNMENT_CENTER, 1600, 54, Color.WHITE) draw_string(font, Vector2(0, 445), subtitle, HORIZONTAL_ALIGNMENT_CENTER, 1600, 19, Color("8adff2")) func _draw_game_over() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.05, 0.005, 0.015, 0.76)) draw_string(font, Vector2(0, 330), "REAKTOR VERLOREN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 48, Color("ff8b78")) draw_string(font, Vector2(0, 395), "Erreichte Welle: %d" % wave, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE) draw_string(font, Vector2(0, 430), "Highscore: %d • Effizienzbonus: ×%.2f" % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0")) _draw_button(start_button, "NEUER VERSUCH", false, Color("6a2835")) func _draw_victory() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.035, 0.05, 0.82)) draw_string(font, Vector2(0, 310), "SEKTOR GESICHERT", HORIZONTAL_ALIGNMENT_CENTER, 1600, 50, Color("8fffea")) draw_string(font, Vector2(0, 375), "Alle %d Angriffswellen wurden abgewehrt." % MAX_WAVES, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE) draw_string(font, Vector2(0, 415), "Der Orbitalkern bleibt unter unserer Kontrolle.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8adff2")) draw_string(font, Vector2(0, 458), "+%d KERNKRISTALLE • GUTHABEN %d" % [last_mission_reward, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8fffea")) draw_string(font, Vector2(0, 492), "Highscore: %d • Effizienzbonus: ×%.2f" % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0")) _draw_button(start_button, "ZUM HAUPTMENÜ", false, Color("185f68")) func _draw_endless_offer() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.025, 0.055, 0.88)) draw_string(font, Vector2(0, 280), "SEKTOR GESICHERT", HORIZONTAL_ALIGNMENT_CENTER, 1600, 50, Color("8fffea")) draw_string(font, Vector2(0, 345), "Alle %d regulären Wellen wurden abgewehrt." % MAX_WAVES, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE) draw_string(font, Vector2(0, 405), "Möchtest du den Einsatz im immer schwereren Endlosmodus fortsetzen?", HORIZONTAL_ALIGNMENT_CENTER, 1600, 19, Color("8adff2")) if victory_reward_pending: draw_string(font, Vector2(0, 450), "WERBUNG WIRD GELADEN • KERNKRISTALLE DANACH: %d" % int(_difficulty().crystals), HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("ffcf7a")) else: draw_string(font, Vector2(0, 450), "+%d Kernkristalle gutgeschrieben. Ein späteres Ende gilt nicht als gescheitert." % last_mission_reward, HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("8fffea")) _draw_button(endless_button, "ENDLOSMODUS STARTEN", false, Color("215b68")) _draw_button(finish_mission_button, "MISSION BEENDEN", false, Color("24394c")) func _draw_endless_result() -> void: var font := ThemeDB.fallback_font draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.02, 0.045, 0.84)) draw_string(font, Vector2(0, 315), "ENDLOSAUFTRAG BEENDET", HORIZONTAL_ALIGNMENT_CENTER, 1600, 48, Color("8feeff")) draw_string(font, Vector2(0, 382), "Erreichte Welle: %d" % wave, HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE) draw_string(font, Vector2(0, 425), "Der reguläre Sektorauftrag bleibt erfolgreich abgeschlossen.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("8adff2")) draw_string(font, Vector2(0, 462), "KERNKRISTALLE %d" % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("8fffea")) draw_string(font, Vector2(0, 492), "Highscore: %d • Effizienzbonus: ×%.2f" % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0")) _draw_button(start_button, "NEUER EINSATZ", false, Color("18556f")) func _draw_tutorial() -> void: var font := ThemeDB.fallback_font var title := "1/3 VERTEIDIGUNG WÄHLEN" var body := "Wähle unten das KINETIK-Geschütz." if tutorial_step == 1: title = "2/3 TURM PLATZIEREN" body = "Setze den Turm auf eine freie Metallfläche neben der Flugroute." elif tutorial_step == 2: title = "3/3 ANGRIFF STARTEN" body = "Starte oben rechts die erste Welle." draw_rect(Rect2(30, 110, 500, 132), Color(0.01, 0.035, 0.07, 0.95)) draw_rect(Rect2(30, 110, 500, 132), Color("55d8ee"), false, 2.0) draw_string(font, Vector2(50, 147), title, HORIZONTAL_ALIGNMENT_LEFT, 450, 19, Color("8ff4ff")) draw_string(font, Vector2(50, 184), body, HORIZONTAL_ALIGNMENT_LEFT, 445, 15, Color.WHITE) _draw_button(tutorial_skip_button, "ÜBERSPRINGEN", false, Color("24394c")) func _is_valid_build_position(position: Vector2, owner := 0) -> bool: var correct_coop_side := true if coop_mode: correct_coop_side = position.x < COOP_MIRROR_X - GRID_SIZE * 0.5 if owner == 0 else position.x > COOP_MIRROR_X + GRID_SIZE * 0.5 return correct_coop_side and _is_in_build_zone(position) and not _is_in_reactor_zone(position) and not _is_path_cell(position) and position.x >= GRID_SIZE * 0.5 and position.x <= _active_world_width() - GRID_SIZE * 0.5 and position.y >= GRID_SIZE * 0.5 and position.y <= WORLD_SIZE.y - GRID_SIZE * 0.5 and _tower_at(position, GRID_SIZE * 0.72) < 0