1933 lines
83 KiB
GDScript
1933 lines
83 KiB
GDScript
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
|