Files
Void-Bastion/main.gd
T

1933 lines
83 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends 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 ENEMY_TEXTURE := preload("res://assets/enemy_ships.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")
]
const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE"]
const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring"]
const MAP_PATHS := [
[Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1560, 680), Vector2(1560, 520), Vector2(1640, 520)],
[Vector2(-40, 280), Vector2(1320, 280), Vector2(1320, 440), Vector2(280, 440), Vector2(280, 680), Vector2(1480, 680), Vector2(1480, 440), Vector2(1640, 440)],
[Vector2(-40, 120), Vector2(360, 120), Vector2(360, 600), Vector2(1320, 600), Vector2(1320, 280), Vector2(1640, 280)]
]
const MAP_BUILD_ZONES := [
[Rect2(155, 95, 1380, 660)],
[Rect2(120, 95, 1380, 660)],
[Rect2(180, 110, 1270, 645)]
]
const MAP_REACTOR_ZONES := [
[{"center": Vector2(1470, 430), "radius": 190.0}],
[{"center": Vector2(1480, 430), "radius": 185.0}],
[
{"center": Vector2(1450, 355), "radius": 165.0},
{"center": Vector2(690, 385), "radius": 155.0}
]
]
const COOP_MIRROR_X := 1480.0
const COOP_WORLD_WIDTH := COOP_MIRROR_X * 2.0
const COOP_LEFT_PATH := [Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1400, 680), Vector2(1400, 520), Vector2(1480, 520)]
const COOP_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.84, "spawn_delay": 0.82, "start_credits": 400, "lives": 14, "crystals": 10}
]
const DIFFICULTY_DESCRIPTIONS := [
"Bisheriges Balancing • 20 Reaktor • Kartenbonus 5",
"+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7",
"+72 % Gegnerhülle • +48 % Gegner • Kartenbonus 10"
]
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")},
{"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "color": Color("62fff1")},
{"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")}
]
const ENEMY_DATA := [
{"name": "JAEGER", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0},
{"name": "FREGATTE", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0},
{"name": "KREUZER", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0}
]
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 credits := 450
var reactor_lives := 20
var wave := 0
var best_wave := 0
var best_endless_wave := 0
var core_crystals := 0
var tutorial_completed := false
var tutorial_step := 0
var game_speed := 1.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 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, 705, 350, 60)
var map_buttons := [Rect2(185, 430, 390, 145), Rect2(605, 430, 390, 145), Rect2(1025, 430, 390, 145)]
var difficulty_buttons := [Rect2(535, 610, 165, 50), Rect2(718, 610, 165, 50), Rect2(901, 610, 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 back_button := Rect2(1070, 18, 90, 54)
var tower_buttons := [Rect2(22, 770, 305, 120), Rect2(347, 770, 305, 120), Rect2(672, 770, 305, 120)]
var upgrade_button := Rect2(1000, 814, 270, 64)
var sell_button := Rect2(1290, 814, 285, 64)
var tutorial_skip_button := Rect2(365, 205, 145, 30)
var reward_ad_button := Rect2(1070, 690, 300, 54)
var tower_info_rect := Rect2(1135, 105, 440, 104)
var endless_button := Rect2(545, 515, 510, 70)
var finish_mission_button := Rect2(625, 610, 350, 62)
var profile_button := Rect2(1280, 25, 270, 52)
var leaderboard_button := Rect2(250, 705, 350, 60)
var leaderboard_back_button := Rect2(55, 35, 185, 54)
var leaderboard_mission_button := Rect2(510, 155, 270, 54)
var leaderboard_endless_button := Rect2(820, 155, 270, 54)
var leaderboard_refresh_button := Rect2(1265, 35, 280, 54)
var leaderboard_rows: Array = []
var leaderboard_endless := false
var leaderboard_loading := false
var leaderboard_message := ""
var profile_back_button := Rect2(55, 35, 185, 54)
var multiplayer_button := Rect2(1000, 705, 350, 60)
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)
@onready var ads_manager = $AdsManager
@onready var account_manager = $AccountManager
@onready var multiplayer_manager = $MultiplayerManager
@onready var online_manager = $OnlineManager
func _ready() -> void:
_select_map(selected_map)
_load_progress()
ads_manager.energy_reward_granted.connect(_on_energy_reward_granted)
ads_manager.ad_unavailable.connect(_show_status)
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)
queue_redraw()
func _process(delta: float) -> void:
elapsed += delta
if status_timer > 0.0:
status_timer -= delta
if game_mode == "playing":
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()
func _spawn_enemy() -> void:
var type := 0
var is_boss := wave > 0 and wave % BOSS_WAVE_INTERVAL == 0 and enemies_to_spawn <= (2 if coop_mode else 1)
var roll := randf()
if is_boss:
type = 2
elif wave >= 7 and roll > 0.82:
type = 2
elif wave >= 3 and roll > 0.53:
type = 1
var data: Dictionary = ENEMY_DATA[type]
var health_scale := (1.0 + maxf(0.0, wave - 1.0) * 0.115) * (3.25 if is_boss else 1.0) * float(_difficulty().hp)
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
})
if is_boss:
_show_status("WARNUNG: KOMMANDOSCHIFF IM ANFLUG")
next_enemy_id += 1
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)
reactor_lives -= 1 + enemy.type + (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)))
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 == 2:
for enemy in enemies.duplicate():
if enemy.pos.distance_to(target.pos) < 76.0:
_apply_damage(enemy, shot.damage if enemy.id == target.id else shot.damage * 0.45, int(shot.get("owner", 0)))
_spawn_burst(target.pos, Color("ff9b4a"), 14)
else:
_apply_damage(target, shot.damage, int(shot.get("owner", 0)))
_spawn_burst(target.pos, shot.color, 4)
projectiles.erase(shot)
func _apply_damage(enemy: Dictionary, amount: float, owner := 0) -> void:
if not enemies.has(enemy):
return
enemy.hp -= amount
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 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:
if game_mode == "menu":
if profile_button.has_point(position):
game_mode = "profile"
queue_redraw()
return
for index in map_buttons.size():
if map_buttons[index].has_point(position):
_select_map(index)
return
for index in difficulty_buttons.size():
if difficulty_buttons[index].has_point(position):
selected_difficulty = index
_save_progress()
return
if 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 == "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()
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 coop_ai_mode:
_start_coop_ai_game()
else:
_start_game()
return
if game_mode == "endless_offer":
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_button.has_point(position):
if coop_online_mode:
_show_status("ONLINE-SIMULATION LÄUFT AUF SERVERTEMPO")
return
game_speed = 2.0 if game_speed < 1.5 else 1.0
_show_status("SIMULATION %.0f×" % game_speed)
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):
selected_tower_type = index
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)
_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
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.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
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
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
_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
_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(selected_map, selected_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.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
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"
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
enemies_to_spawn = ceili((7 + wave * 2) * float(_difficulty().count)) * (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()
ads_manager.record_mission_finished()
func _mission_complete() -> void:
game_mode = "victory"
wave_active = false
best_wave = maxi(best_wave, wave)
_record_account_score(false)
_grant_map_reward()
_save_progress()
ads_manager.record_mission_finished()
func _offer_endless_mode() -> void:
game_mode = "endless_offer"
wave_active = false
best_wave = maxi(best_wave, wave)
_grant_map_reward()
_save_progress()
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)
_grant_map_reward()
_save_progress()
ads_manager.record_mission_finished()
func _record_account_score(was_endless: bool) -> void:
var score := wave * 1000 + maxi(reactor_lives, 0) * 100 + _player_credits(_local_credit_slot())
account_manager.record_run(selected_map, selected_difficulty, was_endless, wave, score)
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
func _difficulty() -> Dictionary:
return DIFFICULTY_DATA[clampi(selected_difficulty, 0, DIFFICULTY_DATA.size() - 1)]
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)
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.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 == 0 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:
var candidates: Array[Vector2] = []
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, coop_paths[1])
if distance >= 90.0 and distance <= 205.0:
candidates.append(candidate)
if candidates.is_empty():
return Vector2.ZERO
return candidates[offset % candidates.size()]
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 _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,
"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)))
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", "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()
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_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.09))
draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.82), 3.0)
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), placement_color, true)
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.95), false, 3.0)
_draw_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
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))
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))
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)))
draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false)
else:
draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false)
draw_texture_rect(ROAD_CORNER_SURFACE_TEXTURE, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false)
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
func _draw_build_grid() -> void:
var line_color := Color(0.24, 0.82, 1.0, 0.22 if selected_tower_type >= 0 else 0.10)
var fill_color := Color(0.08, 0.55, 0.72, 0.08 if selected_tower_type >= 0 else 0.025)
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 source_width: float = TOWER_TEXTURE.get_width() / 3.0
var source := Rect2(source_width * tower.type, 0, source_width, TOWER_TEXTURE.get_height())
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)
draw_texture_rect_region(TOWER_TEXTURE, Rect2(-size * 0.5, -size * 0.5, size, size), source)
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 source_width: float = ENEMY_TEXTURE.get_width() / 3.0
var source := Rect2(source_width * enemy.type, 0, source_width, ENEMY_TEXTURE.get_height())
var size: float = ENEMY_DATA[enemy.type].size * enemy.size_scale
var tint := Color.WHITE if enemy.flash <= 0.0 else Color("ffb6a3")
draw_set_transform(enemy.pos - camera_offset, enemy.angle, Vector2.ONE)
draw_texture_rect_region(ENEMY_TEXTURE, Rect2(-size * 0.5, -size * 0.5, size, size), 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 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), "KERN %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, 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()
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 _draw_tower_card(type: int, active: bool) -> void:
var rect: Rect2 = tower_buttons[type]
var data: Dictionary = TOWER_DATA[type]
var font := ThemeDB.fallback_font
draw_rect(rect, Color("226c88") if active else Color("173c55"), true)
draw_rect(rect, Color("65dfff") if active else Color("37677b"), false, 2.0)
draw_string(font, rect.position + Vector2(14, 27), data.name, HORIZONTAL_ALIGNMENT_LEFT, 170, 19, Color.WHITE)
draw_string(font, rect.position + Vector2(185, 27), "%d EN" % data.cost, HORIZONTAL_ALIGNMENT_RIGHT, 105, 16, Color("7ce7ff"))
draw_string(font, rect.position + Vector2(14, 59), "SCHADEN %.0f • %.2f SCHUSS/S" % [data.damage, _tower_shots_per_second(type, 1)], HORIZONTAL_ALIGNMENT_LEFT, 278, 14, Color("dceaf0"))
draw_string(font, rect.position + Vector2(14, 86), "REICHWEITE %.0f • %s" % [data.range, data.damage_type], HORIZONTAL_ALIGNMENT_LEFT, 278, 14, Color("9fdce9"))
draw_string(font, rect.position + Vector2(14, 109), "Antippen, dann Bauplatz wählen", HORIZONTAL_ALIGNMENT_LEFT, 278, 12, Color("719aaa"))
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.94), true)
draw_rect(tower_info_rect, Color("4a9ab2"), false, 2.0)
var heading := "%s • STUFE %d" % [data.name, level] if is_built else "%s • BAUVORSCHAU" % data.name
draw_string(font, tower_info_rect.position + Vector2(14, 26), heading, HORIZONTAL_ALIGNMENT_LEFT, 412, 17, Color("a9efff"))
draw_string(font, tower_info_rect.position + Vector2(14, 52), "SCH %.0f | %.2f/S | RW %.0f | %s" % [_tower_damage(type, level), _tower_shots_per_second(type, level), _tower_range(type, level), data.damage_type], HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color.WHITE)
if is_built and level < 3:
var next_level := level + 1
draw_string(font, tower_info_rect.position + Vector2(14, 80), "UPGRADE: +%.0f SCH +%.2f/S +%.0f RW" % [_tower_damage(type, next_level) - _tower_damage(type, level), _tower_shots_per_second(type, next_level) - _tower_shots_per_second(type, level), _tower_range(type, next_level) - _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
elif is_built:
draw_string(font, tower_info_rect.position + Vector2(14, 80), "MAXIMALE AUSBAUSTUFE ERREICHT", HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
else:
draw_string(font, tower_info_rect.position + Vector2(14, 80), "Reichweite wird vor dem Bau angezeigt.", HORIZONTAL_ALIGNMENT_LEFT, 412, 13, Color("84ffca"))
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_string(font, Vector2(250, 270), "Wähle einen Einsatzsektor und verteidige den Reaktorkern.", HORIZONTAL_ALIGNMENT_CENTER, 1100, 21, Color("d7e2ea"))
_draw_button(profile_button, "%s • PROFIL" % account_manager.display_name, false, Color("233f57"))
draw_string(font, Vector2(0, 375), "EINSATZKARTE WÄHLEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 20, Color("8feeff"))
for index in map_buttons.size():
var card: Rect2 = map_buttons[index]
draw_texture_rect(MAP_TEXTURES[index], card, false, Color(0.76, 0.82, 0.88, 1.0))
draw_rect(card, Color(0.005, 0.015, 0.035, 0.30), true)
draw_rect(card, Color("70eaff") if selected_map == index else Color("436574"), false, 4.0 if selected_map == index else 2.0)
draw_rect(Rect2(card.position + Vector2(0, 92), Vector2(card.size.x, 53)), Color(0.01, 0.025, 0.05, 0.90), true)
draw_string(font, card.position + Vector2(0, 116), MAP_NAMES[index], HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 18, Color.WHITE)
draw_string(font, card.position + Vector2(0, 138), MAP_DESCRIPTIONS[index], HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 13, Color("8fddef"))
draw_string(font, Vector2(0, 600), "SCHWIERIGKEIT • LEICHT ENTSPRICHT DEM BISHERIGEN BALANCING", HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, 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, 686), DIFFICULTY_DESCRIPTIONS[selected_difficulty], HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a"))
_draw_button(start_button, "MISSION STARTEN", false, Color("18556f"))
_draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b"))
_draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75"))
draw_string(font, Vector2(0, 792), "BESTE WELLE %02d • KERNKRISTALLE %04d • KARTENBONUS %d" % [best_wave, core_crystals, int(_difficulty().crystals)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("86c8d8"))
draw_string(font, Vector2(0, 860), "VERSION 0.23 • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN", HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
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_string(font, Vector2(0, 142), "%s • %s" % [MAP_NAMES[selected_map], DIFFICULTY_DATA[selected_difficulty].name], HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("66dff3"))
_draw_button(leaderboard_mission_button, "MISSION", not leaderboard_endless, Color("24475b"))
_draw_button(leaderboard_endless_button, "ENDLOSMODUS", leaderboard_endless, Color("5b452b"))
var panel := Rect2(235, 235, 1130, 525)
draw_rect(panel, Color(0.012, 0.035, 0.065, 0.96), true)
draw_rect(panel, Color("39788e"), false, 2.0)
draw_string(font, Vector2(275, 277), "RANG", HORIZONTAL_ALIGNMENT_LEFT, 100, 14, Color("91a8bc"))
draw_string(font, Vector2(410, 277), "PILOT", HORIZONTAL_ALIGNMENT_LEFT, 480, 14, Color("91a8bc"))
draw_string(font, Vector2(950, 277), "WELLE", HORIZONTAL_ALIGNMENT_CENTER, 130, 14, Color("91a8bc"))
draw_string(font, Vector2(1125, 277), "PUNKTE", HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, Color("91a8bc"))
draw_line(Vector2(270, 293), Vector2(1330, 293), Color("315c70"), 1.0)
var own_user_id: String = str(online_manager.session.user_id) if online_manager.session != null else ""
for index in leaderboard_rows.size():
var row: Dictionary = leaderboard_rows[index]
var y := 331.0 + index * 34.0
if index % 2 == 0:
draw_rect(Rect2(260, y - 24, 1080, 31), Color(0.03, 0.09, 0.14, 0.64), true)
var owner_color := Color("83ffc8") if not own_user_id.is_empty() and str(row.get("owner_id", "")) == own_user_id else Color.WHITE
draw_string(font, Vector2(280, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 90, 16, Color("8ff4ff"))
draw_string(font, Vector2(410, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 480, 16, owner_color)
draw_string(font, Vector2(950, y), "%02d" % int(row.get("wave", 0)), HORIZONTAL_ALIGNMENT_CENTER, 130, 16, Color("ffcf7a"))
draw_string(font, Vector2(1125, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 16, Color("8fffea"))
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"))
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 := 482.0 + index * 62.0
draw_rect(Rect2(210, y - 30, 1180, 48), Color(0.03, 0.09, 0.14, 0.70), true)
draw_string(font, Vector2(235, y), MAP_NAMES[index], HORIZONTAL_ALIGNMENT_LEFT, 420, 17, Color.WHITE)
draw_string(font, Vector2(700, y), "WELLE %02d" % account_manager.get_map_best_wave(index, false), HORIZONTAL_ALIGNMENT_CENTER, 180, 16, Color("8ff4ff"))
draw_string(font, Vector2(930, y), "WELLE %02d" % account_manager.get_map_best_wave(index, true), HORIZONTAL_ALIGNMENT_CENTER, 180, 16, 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, 16, 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_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_button(start_button, "MISSION WIEDERHOLEN", 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"))
draw_string(font, Vector2(0, 450), "+%d Kernkristalle erhalten. 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_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