Prepare Void Bastion alpha 7 release

This commit is contained in:
Emporex committed 2026-10-06 23:59:12 +02:00
1 parent 56077e88a5
commit e63e08ddde
18 files changed
+799 -95

No files matched your search

+375 -52
View File
@@ -17,6 +17,9 @@ const ENEMY_TEXTURE := preload("res://assets/enemy_ships.png")
const ENEMY_INTERCEPTOR_TEXTURE := preload("res://assets/enemy_interceptor.png")
const ENEMY_SHIELD_CARRIER_TEXTURE := preload("res://assets/enemy_shield_carrier.png")
const ENEMY_SIEGE_DESTROYER_TEXTURE := preload("res://assets/enemy_siege_destroyer.png")
const ENEMY_GROUND_ROVER_TEXTURE := preload("res://assets/enemy_ground_rover.png")
const ENEMY_GROUND_TANK_TEXTURE := preload("res://assets/enemy_ground_tank.png")
const ENEMY_GROUND_WALKER_TEXTURE := preload("res://assets/enemy_ground_walker.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")
@@ -81,21 +84,24 @@ const DIFFICULTY_DESCRIPTIONS := [
]
const TOWER_DATA := [
{"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässiges Projektilgeschütz."},
{"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "color": Color("62fff1"), "unlock_cost": 0, "description": "Schneller Strahl, stark gegen Schilde."},
{"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Explosionsschaden gegen Gruppen."},
{"name": "PLASMA", "cost": 230, "damage": 48.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "color": Color("e27bff"), "unlock_cost": 18, "description": "Plasmaexplosion trifft Gegner im Umkreis."},
{"name": "EMP-PULS", "cost": 210, "damage": 34.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118", "color": Color("77e8ff"), "unlock_cost": 24, "description": "Breiter Impuls trifft ganze Gegnergruppen."},
{"name": "RAILGUN", "cost": 300, "damage": 110.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "SCHILD-DURCHSCHLAG", "color": Color("87efff"), "unlock_cost": 30, "description": "Weitreichender Schuss, ignoriert Schildschutz."}
{"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "target_class": "ground", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässig gegen Bodenziele."},
{"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "target_class": "both", "both_damage_scale": 0.82, "color": Color("62fff1"), "unlock_cost": 0, "description": "Mehrzwecklaser; durchdringt Schilde von Luft- und Bodenzielen."},
{"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "target_class": "ground", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Bodenraketen treffen Gegnergruppen."},
{"name": "PLASMA", "cost": 230, "damage": 34.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "target_class": "both", "both_damage_scale": 0.72, "color": Color("e27bff"), "unlock_cost": 18, "description": "Mehrzweckwaffe mit reduziertem Schaden."},
{"name": "EMP-PULS", "cost": 210, "damage": 20.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118 · ALLE", "target_class": "both", "both_damage_scale": 1.0, "color": Color("77e8ff"), "unlock_cost": 24, "description": "Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden."},
{"name": "RAILGUN", "cost": 300, "damage": 77.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "PIERCING-STRAHL", "target_class": "both", "both_damage_scale": 0.70, "color": Color("c86bff"), "unlock_cost": 30, "description": "Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie."}
]
const ENEMY_DATA := [
{"name": "JAEGER", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28},
{"name": "FREGATTE", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28},
{"name": "KREUZER", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28},
{"name": "ABFANGJAEGER", "hp": 52.0, "speed": 178.0, "reward": 22, "size": 48.0, "breach_damage": 2, "shield_resistance": 0.28},
{"name": "SCHILDKREUZER", "hp": 260.0, "speed": 58.0, "reward": 52, "size": 92.0, "breach_damage": 2, "shield_resistance": 0.18, "default_shield": true},
{"name": "BELAGERER", "hp": 740.0, "speed": 48.0, "reward": 108, "size": 128.0, "breach_damage": 4, "shield_resistance": 0.28}
{"name": "JAEGER", "category": "air", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28},
{"name": "FREGATTE", "category": "air", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28},
{"name": "KREUZER", "category": "air", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28},
{"name": "ABFANGJAEGER", "category": "air", "hp": 52.0, "speed": 178.0, "reward": 22, "size": 48.0, "breach_damage": 2, "shield_resistance": 0.28},
{"name": "SCHILDKREUZER", "category": "air", "hp": 260.0, "speed": 58.0, "reward": 52, "size": 92.0, "breach_damage": 2, "shield_resistance": 0.18, "default_shield": true},
{"name": "BELAGERER", "category": "air", "hp": 740.0, "speed": 48.0, "reward": 108, "size": 128.0, "breach_damage": 4, "shield_resistance": 0.28},
{"name": "SPAEHER-ROVER", "category": "ground", "hp": 88.0, "speed": 142.0, "reward": 22, "size": 54.0, "breach_damage": 1, "shield_resistance": 0.28},
{"name": "STURMPANZER", "category": "ground", "hp": 390.0, "speed": 61.0, "reward": 61, "size": 92.0, "breach_damage": 3, "shield_resistance": 0.28},
{"name": "BELAGERUNGSLAEUFER", "category": "ground", "hp": 980.0, "speed": 36.0, "reward": 132, "size": 124.0, "breach_damage": 5, "shield_resistance": 0.28}
]
var path_points := PackedVector2Array()
@@ -139,6 +145,11 @@ var next_enemy_id := 1
var wave_active := false
var enemies_to_spawn := 0
var spawn_timer := 0.0
var wave_type_schedule: Array[Dictionary] = []
var wave_spawn_index := 0
var online_wave_types_remaining: Array[int] = []
var enemy_strength_multiplier := 1.0
var developer_panel_open := false
var status_text := ""
var status_timer := 0.0
var elapsed := 0.0
@@ -163,6 +174,13 @@ var wave_button := Rect2(1320, 18, 255, 54)
var pause_button := Rect2(1242, 18, 62, 54)
var speed_button := Rect2(1170, 18, 62, 54)
var auto_wave_button := Rect2(1320, 73, 255, 22)
var wave_current_preview_rect := Rect2(520, 67, 258, 44)
var wave_next_preview_rect := Rect2(786, 67, 258, 44)
var developer_panel_rect := Rect2(1060, 104, 515, 78)
var developer_strength_down_button := Rect2(1074, 140, 42, 30)
var developer_strength_up_button := Rect2(1260, 140, 42, 30)
var developer_crystals_down_button := Rect2(1320, 140, 50, 30)
var developer_crystals_up_button := Rect2(1512, 140, 50, 30)
var back_button := Rect2(1070, 18, 90, 54)
var tower_buttons := [
Rect2(15, 768, 255, 122), Rect2(278, 768, 255, 122), Rect2(541, 768, 255, 122),
@@ -173,6 +191,7 @@ var sell_button := Rect2(1430, 187, 145, 44)
var tutorial_skip_button := Rect2(365, 205, 145, 30)
var reward_ad_button := Rect2(1070, 690, 300, 54)
var tower_info_rect := Rect2(1275, 103, 300, 76)
var target_priority_button := Rect2(1275, 236, 300, 34)
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)
@@ -314,10 +333,14 @@ func _update_wave(delta: float) -> void:
func _spawn_enemy() -> void:
var is_boss := wave > 0 and wave % BOSS_WAVE_INTERVAL == 0 and enemies_to_spawn <= (2 if coop_mode else 1)
var type := _enemy_type_for_wave(wave, randf(), is_boss)
var schedule_entry: Dictionary = wave_type_schedule[wave_spawn_index] if wave_spawn_index < wave_type_schedule.size() else {}
var is_boss := bool(schedule_entry.get("boss", wave > 0 and wave % BOSS_WAVE_INTERVAL == 0 and enemies_to_spawn <= (2 if coop_mode else 1)))
var type := int(schedule_entry.get("type", _enemy_type_for_wave(wave, randf(), is_boss)))
wave_spawn_index += 1
var data: Dictionary = ENEMY_DATA[type]
var health_scale := _enemy_health_scale(wave, is_boss)
var health_scale := _enemy_health_scale(wave, is_boss) * enemy_strength_multiplier
var has_shield := bool(data.get("default_shield", false)) or (wave >= 4 and randf() < minf(0.42, 0.10 + wave * 0.018 + selected_difficulty * 0.06))
var max_shield_hp: float = data.hp * health_scale * 0.55 if has_shield else 0.0
var route_id := 0
var route := path_points
if coop_mode:
@@ -332,13 +355,16 @@ func _spawn_enemy() -> void:
"route_id": route_id,
"hp": data.hp * health_scale,
"max_hp": data.hp * health_scale,
"base_max_hp": data.hp * _enemy_health_scale(wave, is_boss),
"speed": data.speed * (1.0 + wave * 0.012) * (0.82 if is_boss else 1.0) * float(_difficulty().speed),
"reward": int((data.reward + wave) * (4 if is_boss else 1) * float(_difficulty().income)),
"angle": 0.0,
"flash": 0.0,
"boss": is_boss,
"size_scale": 1.34 if is_boss else 1.0,
"shield": bool(data.get("default_shield", false)) or (wave >= 4 and randf() < minf(0.42, 0.10 + wave * 0.018 + selected_difficulty * 0.06)),
"shield": has_shield,
"shield_hp": max_shield_hp,
"max_shield_hp": max_shield_hp,
"shield_resistance": float(data.shield_resistance)
})
if is_boss:
@@ -348,8 +374,14 @@ func _spawn_enemy() -> void:
func _enemy_type_for_wave(for_wave: int, roll: float, is_boss: bool) -> int:
if is_boss:
return 5
return 8 if for_wave >= 10 else 5
var advanced_offset := 2 if selected_difficulty == 2 else (1 if selected_difficulty == 1 else 0)
if for_wave >= 10 and roll >= 0.96:
return 8
if for_wave >= 8 and roll >= 0.84 and roll < 0.94:
return 7
if for_wave >= 3 and roll >= 0.52 and roll < 0.64:
return 6
if for_wave >= 10 - advanced_offset and roll >= 0.94:
return 5
if for_wave >= 7 - advanced_offset and roll >= 0.80:
@@ -363,6 +395,78 @@ func _enemy_type_for_wave(for_wave: int, roll: float, is_boss: bool) -> int:
return 0
func _wave_population(for_wave: int) -> int:
if for_wave <= 0:
return 0
var population_scale := 1.0 + maxf(0.0, for_wave - 3.0) * 0.08
if selected_difficulty == 2:
population_scale += maxf(0.0, for_wave - 4.0) * 0.025
if for_wave > MAX_WAVES:
population_scale += (for_wave - MAX_WAVES) * 0.035
return ceili((7 + for_wave * 2) * float(_difficulty().count) * population_scale) * (2 if coop_mode else 1)
func _build_wave_schedule(for_wave: int, count: int) -> Array[Dictionary]:
var schedule: Array[Dictionary] = []
var boss_count := mini(2 if coop_mode else 1, count) if for_wave > 0 and for_wave % BOSS_WAVE_INTERVAL == 0 else 0
for index in count:
var is_boss := index >= count - boss_count
var roll := fposmod(float(index + 1) * 0.61803398875 + float(for_wave) * 0.38196601125, 1.0)
var type := _enemy_type_for_wave(for_wave, roll, is_boss)
schedule.append({"type": type, "boss": is_boss})
return schedule
func _enemy_category(enemy_type: int) -> String:
return str(ENEMY_DATA[clampi(enemy_type, 0, ENEMY_DATA.size() - 1)].category)
func _tower_target_class(tower_type: int) -> String:
return str(TOWER_DATA[clampi(tower_type, 0, TOWER_DATA.size() - 1)].get("target_class", "both"))
func _tower_display_order() -> Array[int]:
var ordered_types: Array[int] = []
for target_class in ["air", "ground", "both"]:
for tower_type in TOWER_DATA.size():
if _tower_target_class(tower_type) == target_class:
ordered_types.append(tower_type)
return ordered_types
func _tower_can_target(tower_type: int, enemy_type: int) -> bool:
var target_class := _tower_target_class(tower_type)
return target_class == "both" or target_class == _enemy_category(enemy_type)
func _tower_target_label(tower_type: int) -> String:
match _tower_target_class(tower_type):
"air": return "LUFT"
"ground": return "BODEN"
_: return "BEIDE"
func _set_enemy_strength_multiplier(value: float) -> void:
if coop_online_mode:
_show_status("TESTMULTIPLIKATOR IST IM ONLINE-KOOP DEAKTIVIERT")
return
enemy_strength_multiplier = clampf(roundf(value * 10.0) / 10.0, 0.5, 3.0)
for enemy in enemies:
var health_ratio := clampf(float(enemy.hp) / maxf(1.0, float(enemy.max_hp)), 0.0, 1.0)
var adjusted_max := float(enemy.get("base_max_hp", enemy.max_hp)) * enemy_strength_multiplier
enemy.max_hp = adjusted_max
enemy.hp = adjusted_max * health_ratio
_show_status("TEST: GEGNERSTÄRKE ×%.1f" % enemy_strength_multiplier)
queue_redraw()
func _adjust_core_crystals(amount: int) -> void:
core_crystals = clampi(core_crystals + amount, 0, 999999)
_save_progress()
_show_status("TEST: KERNKRISTALLE %s25 • STAND %d" % ["+" if amount >= 0 else "−", core_crystals])
queue_redraw()
func _enemy_health_scale(for_wave: int, is_boss: bool) -> float:
var early_ramp := maxf(0.0, for_wave - 1.0) * 0.20
var post_four_ramp := maxf(0.0, for_wave - 3.0) * 0.09
@@ -407,24 +511,77 @@ func _update_towers(delta: float) -> void:
func _find_target(tower: Dictionary) -> Dictionary:
var best: Dictionary = {}
var best_progress := -1.0
var priority := "shield" if int(tower.type) == 1 else str(tower.get("target_priority", "first"))
var best_progress := -INF if priority != "last" else INF
var best_hp := -1.0
var best_is_shielded := false
var effective_range := _tower_range(tower.type, tower.level)
for enemy in enemies:
if tower.pos.distance_to(enemy.pos) <= effective_range:
if _tower_can_target(tower.type, int(enemy.type)) and 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:
var enemy_hp := float(enemy.hp)
var is_shielded := bool(enemy.get("shield", false))
var is_better := best.is_empty()
match priority:
"last": is_better = is_better or progress < best_progress
"strongest": is_better = is_better or enemy_hp > best_hp
"shield":
is_better = is_better or (is_shielded and not best_is_shielded)
if is_shielded == best_is_shielded and not best.is_empty():
is_better = progress > best_progress
_:
is_better = is_better or progress > best_progress
if is_better:
best_progress = progress
best_hp = enemy_hp
best_is_shielded = is_shielded
best = enemy
return best
func _tower_priority_label(priority: String) -> String:
match priority:
"shield": return "SCHILD ZUERST"
"last": return "LETZTE"
"strongest": return "MEISTE HP"
_: return "ERSTE"
func _cycle_selected_tower_priority() -> void:
if selected_tower_index < 0 or selected_tower_index >= towers.size():
return
var tower: Dictionary = towers[selected_tower_index]
if int(tower.type) == 1:
_show_status("LASER VISIERT SCHILDGEGNER IMMER ZUERST AN")
return
var priorities := ["first", "last", "strongest", "shield"]
var current := priorities.find(str(tower.get("target_priority", "first")))
tower.target_priority = priorities[(current + 1) % priorities.size()]
_show_status("TURMZIEL: %s" % _tower_priority_label(str(tower.target_priority)))
queue_redraw()
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_target_class(tower.type) == "both":
damage *= float(data.get("both_damage_scale", 0.7))
if tower.type == 1:
_apply_damage(target, damage, int(tower.get("owner", 0)), tower.type)
beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.09, "color": data.color})
elif tower.type == 5:
var direction: Vector2 = tower.pos.direction_to(target.pos)
var beam_end: Vector2 = tower.pos + direction * _tower_range(tower.type, tower.level)
for enemy in enemies.duplicate():
if not _tower_can_target(tower.type, int(enemy.type)):
continue
var offset: Vector2 = enemy.pos - tower.pos
var along_beam: float = offset.dot(direction)
var across_beam: float = absf(offset.cross(direction))
if along_beam >= 0.0 and along_beam <= _tower_range(tower.type, tower.level) and across_beam <= 18.0:
_apply_damage(enemy, damage, int(tower.get("owner", 0)), tower.type)
beams.append({"from": tower.pos, "to": beam_end, "ttl": 0.14, "color": data.color, "piercing": true})
else:
projectiles.append({
"pos": tower.pos,
@@ -448,9 +605,9 @@ func _update_projectiles(delta: float) -> void:
if shot.pos.distance_to(target.pos) < 14.0:
if shot.type in [2, 3, 4]:
var blast_radius := 76.0 if shot.type == 2 else (92.0 if shot.type == 3 else 118.0)
var splash_factor := 0.45 if shot.type == 2 else (0.62 if shot.type == 3 else 0.38)
var splash_factor := 0.45 if shot.type == 2 else (0.62 if shot.type == 3 else 1.0)
for enemy in enemies.duplicate():
if enemy.pos.distance_to(target.pos) < blast_radius:
if _tower_can_target(int(shot.type), int(enemy.type)) and enemy.pos.distance_to(target.pos) < blast_radius:
_apply_damage(enemy, shot.damage if enemy.id == target.id else shot.damage * splash_factor, int(shot.get("owner", 0)), int(shot.type))
var blast_color: Color = Color("ff9b4a") if shot.type == 2 else (Color("db65ff") if shot.type == 3 else Color("71eaff"))
_spawn_burst(target.pos, blast_color, 14 if shot.type == 2 else 18)
@@ -464,8 +621,19 @@ func _apply_damage(enemy: Dictionary, amount: float, owner := 0, weapon_type :=
if not enemies.has(enemy):
return
var applied_damage := amount
if bool(enemy.get("shield", false)) and weapon_type not in [1, 5]:
applied_damage *= float(enemy.get("shield_resistance", 0.28))
if bool(enemy.get("shield", false)):
if weapon_type == 1:
var shield_hp := float(enemy.get("shield_hp", enemy.max_hp * 0.55))
var shield_damage := minf(shield_hp, amount)
shield_hp -= shield_damage
enemy.shield_hp = shield_hp
if shield_hp <= 0.0:
enemy.shield = false
applied_damage = amount - shield_damage
else:
applied_damage = 0.0
elif weapon_type != 5:
applied_damage *= float(enemy.get("shield_resistance", 0.28))
enemy.hp -= applied_damage
enemy.flash = 0.06
if enemy.hp <= 0.0:
@@ -508,6 +676,10 @@ func _update_effects(delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F9 and game_mode in ["playing", "paused"]:
developer_panel_open = not developer_panel_open
queue_redraw()
return
if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_ESCAPE, KEY_BACK, KEY_Q]:
if audio_settings_open:
audio_settings_open = false
@@ -573,6 +745,21 @@ func _unhandled_input(event: InputEvent) -> void:
func _handle_pointer(position: Vector2, is_touch := false) -> void:
_play_ui_sound()
if developer_panel_open and game_mode in ["playing", "paused"]:
if developer_strength_down_button.has_point(position):
_set_enemy_strength_multiplier(enemy_strength_multiplier - 0.1)
return
if developer_strength_up_button.has_point(position):
_set_enemy_strength_multiplier(enemy_strength_multiplier + 0.1)
return
if developer_crystals_down_button.has_point(position):
_adjust_core_crystals(-25)
return
if developer_crystals_up_button.has_point(position):
_adjust_core_crystals(25)
return
if developer_panel_rect.has_point(position):
return
if audio_settings_open:
if audio_close_button.has_point(position) or not audio_panel_rect.has_point(position):
audio_settings_open = false
@@ -812,9 +999,10 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
if wave_button.has_point(position):
_start_wave()
return
var tower_display_order := _tower_display_order()
for index in tower_buttons.size():
if tower_buttons[index].has_point(position):
var tower_type := index
var tower_type: int = tower_display_order[index]
if not unlocked_towers.has(tower_type):
store_return_mode = game_mode
game_mode = "store"
@@ -827,6 +1015,12 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
tutorial_step = 1
return
if selected_tower_index >= 0:
if target_priority_button.has_point(position):
if coop_online_mode:
_show_status("ZIELPRIORITÄT IST IM ONLINE-KOOP NOCH NICHT EINSTELLBAR")
else:
_cycle_selected_tower_priority()
return
if upgrade_button.has_point(position):
_upgrade_selected()
return
@@ -1164,12 +1358,9 @@ func _start_wave() -> void:
_show_status("WELLENSTART AN SERVER GESENDET")
return
wave += 1
var wave_population_scale := 1.0 + maxf(0.0, wave - 3.0) * 0.08
if selected_difficulty == 2:
wave_population_scale += maxf(0.0, wave - 4.0) * 0.025
if endless_mode:
wave_population_scale += maxf(0.0, wave - MAX_WAVES) * 0.035
enemies_to_spawn = ceili((7 + wave * 2) * float(_difficulty().count) * wave_population_scale) * (2 if coop_mode else 1)
enemies_to_spawn = _wave_population(wave)
wave_spawn_index = 0
wave_type_schedule = _build_wave_schedule(wave, enemies_to_spawn)
spawn_timer = 0.15
wave_active = true
if coop_ai_mode:
@@ -1339,7 +1530,7 @@ func _place_tower(position: Vector2) -> void:
return
_spend_credits(0, int(data.cost))
towers_built_this_run += 1
towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0})
towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0, "target_priority": "shield" if selected_tower_type == 1 else "first"})
selected_tower_index = towers.size() - 1
selected_tower_type = -1
pending_touch_build_position = Vector2(-1, -1)
@@ -1611,6 +1802,9 @@ func _apply_online_snapshot(snapshot: Dictionary) -> void:
wave = int(snapshot.get("wave", wave))
wave_active = bool(snapshot.get("wave_active", false))
enemies_to_spawn = int(snapshot.get("enemies_to_spawn", 0))
online_wave_types_remaining.clear()
for enemy_type in snapshot.get("wave_types_remaining", []):
online_wave_types_remaining.append(int(enemy_type))
credits = _player_credits(local_player_slot)
var previous_enemy_positions: Dictionary = {}
@@ -1896,8 +2090,13 @@ func _draw_world() -> void:
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)
if bool(beam.get("piercing", false)):
draw_line(beam.from, beam.to, Color("c86bff", 0.18), 18.0)
draw_line(beam.from, beam.to, Color("c86bff", 0.9), 8.0)
draw_line(beam.from, beam.to, Color("f3ddff", 1.0), 2.0)
else:
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)))
@@ -2045,22 +2244,17 @@ func _draw_tower(tower: Dictionary) -> void:
func _draw_enemy(enemy: Dictionary) -> void:
var enemy_type := int(enemy.type)
var enemy_texture: Texture2D = ENEMY_TEXTURE
var source: Rect2
var destination: Rect2
var size: float = ENEMY_DATA[enemy.type].size * enemy.size_scale
if enemy_type < 3:
var source_width: float = ENEMY_TEXTURE.get_width() / 3.0
source = Rect2(source_width * enemy_type, 0, source_width, ENEMY_TEXTURE.get_height())
destination = Rect2(-size * 0.5, -size * 0.5, size, size)
else:
enemy_texture = [ENEMY_INTERCEPTOR_TEXTURE, ENEMY_SHIELD_CARRIER_TEXTURE, ENEMY_SIEGE_DESTROYER_TEXTURE][enemy_type - 3]
source = Rect2(Vector2.ZERO, enemy_texture.get_size())
destination = Rect2(-size * 0.75, -size * 0.5, size * 1.5, size)
var enemy_texture := _enemy_texture(enemy_type)
var source := _enemy_source_rect(enemy_type)
destination = Rect2(-size * 0.75, -size * 0.5, size * 1.5, size)
var tint := Color.WHITE if enemy.flash <= 0.0 else Color("ffb6a3")
if bool(enemy.get("shield", false)):
draw_circle(enemy.pos, size * 0.63, Color(0.25, 0.80, 1.0, 0.12))
draw_arc(enemy.pos, size * 0.63, 0.0, TAU, 28, Color(0.38, 0.86, 1.0, 0.76), 2.2)
var shield_ratio := clampf(float(enemy.get("shield_hp", 1.0)) / maxf(1.0, float(enemy.get("max_shield_hp", 1.0))), 0.0, 1.0)
draw_circle(enemy.pos, size * 0.63, Color(0.25, 0.80, 1.0, 0.08))
draw_arc(enemy.pos, size * 0.63, 0.0, TAU, 40, Color(0.28, 0.66, 0.82, 0.28), 2.0)
draw_arc(enemy.pos, size * 0.63, -PI * 0.5, -PI * 0.5 - TAU * shield_ratio, 48, Color(0.38, 0.86, 1.0, 0.92), 3.2)
draw_set_transform(enemy.pos - camera_offset, enemy.angle, Vector2.ONE)
draw_texture_rect_region(enemy_texture, destination, source, tint)
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
@@ -2074,6 +2268,22 @@ func _draw_enemy(enemy: Dictionary) -> void:
draw_string(ThemeDB.fallback_font, enemy.pos + Vector2(-45, -size * 0.58), "KOMMANDOSCHIFF", HORIZONTAL_ALIGNMENT_CENTER, 90, 11, Color("ffab78"))
func _enemy_texture(enemy_type: int) -> Texture2D:
if enemy_type < 3:
return ENEMY_TEXTURE
if enemy_type < 6:
return [ENEMY_INTERCEPTOR_TEXTURE, ENEMY_SHIELD_CARRIER_TEXTURE, ENEMY_SIEGE_DESTROYER_TEXTURE][enemy_type - 3]
return [ENEMY_GROUND_ROVER_TEXTURE, ENEMY_GROUND_TANK_TEXTURE, ENEMY_GROUND_WALKER_TEXTURE][enemy_type - 6]
func _enemy_source_rect(enemy_type: int) -> Rect2:
var texture := _enemy_texture(enemy_type)
if enemy_type < 3:
var source_width := texture.get_width() / 3.0
return Rect2(source_width * enemy_type, 0.0, source_width, texture.get_height())
return Rect2(Vector2.ZERO, texture.get_size())
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))
@@ -2101,15 +2311,27 @@ func _draw_hud() -> void:
_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)
var tower_display_order := _tower_display_order()
for index in tower_buttons.size():
_draw_tower_card(index, index, selected_tower_type == index)
var tower_type: int = tower_display_order[index]
_draw_tower_card(index, tower_type, selected_tower_type == tower_type)
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 not coop_online_mode:
var priority_label := "ZIEL: %s" % _tower_priority_label(str(tower.get("target_priority", "first")))
if int(tower.type) == 1:
priority_label = "ZIEL: SCHILD ZUERST (LASER FIX)"
_draw_button(target_priority_button, priority_label, int(tower.type) == 1, Color("26394a"))
if selected_tower_type >= 0 or selected_tower_index >= 0:
_draw_tower_info_panel()
_draw_wave_preview_panel(wave_current_preview_rect, "JETZT", wave)
var next_wave := wave + 1
if wave >= MAX_WAVES and not endless_mode:
next_wave = 0
_draw_wave_preview_panel(wave_next_preview_rect, "DANN", next_wave)
draw_rect(auto_wave_button, Color("102d40"), true)
draw_rect(auto_wave_button, Color("37677b"), false, 1.0)
draw_rect(Rect2(auto_wave_button.position + Vector2(7, 4), Vector2(14, 14)), Color("07131f"), true)
@@ -2122,11 +2344,99 @@ func _draw_hud() -> void:
draw_rect(option_rect, Color("102d40"), true)
draw_rect(option_rect, Color("37677b"), false, 1.0)
draw_string(font, option_rect.position + Vector2(0, 18), "%.0f×" % SPEED_OPTIONS[speed_index], HORIZONTAL_ALIGNMENT_CENTER, option_rect.size.x, 13, Color("8ff4ff") if is_equal_approx(game_speed, SPEED_OPTIONS[speed_index]) else Color.WHITE)
if developer_panel_open:
draw_rect(developer_panel_rect, Color(0.008, 0.024, 0.045, 0.96), true)
draw_rect(developer_panel_rect, Color("ffad62"), false, 1.5)
draw_string(font, developer_panel_rect.position + Vector2(12, 22), "TESTMODUS • F9 ZUM SCHLIESSEN", HORIZONTAL_ALIGNMENT_LEFT, 490, 11, Color("ffc277"))
_draw_button(developer_strength_down_button, "−", false, Color("44333a"))
draw_string(font, developer_panel_rect.position + Vector2(58, 57), "GEGNER ×%.1f" % enemy_strength_multiplier, HORIZONTAL_ALIGNMENT_CENTER, 128, 13, Color.WHITE)
_draw_button(developer_strength_up_button, "+", false, Color("44333a"))
_draw_button(developer_crystals_down_button, "−25", false, Color("44333a"))
draw_string(font, developer_panel_rect.position + Vector2(310, 57), "KRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 136, 13, Color("8ff4ff"))
_draw_button(developer_crystals_up_button, "+25", false, Color("44333a"))
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_wave_preview_panel(rect: Rect2, heading: String, for_wave: int) -> void:
var font := ThemeDB.fallback_font
draw_rect(rect, Color(0.008, 0.028, 0.052, 0.90), true)
draw_rect(rect, Color(0.22, 0.54, 0.66, 0.68), false, 1.0)
var summary := _wave_preview_summary(for_wave)
var title := "%s --" % heading if for_wave <= 0 else "%s %02d" % [heading, for_wave]
draw_string(font, rect.position + Vector2(8, 12), title, HORIZONTAL_ALIGNMENT_LEFT, 82, 9, Color("96d8e8"))
draw_string(font, rect.position + Vector2(99, 12), "%d SCHIFFE" % int(summary.total), HORIZONTAL_ALIGNMENT_RIGHT, 150, 9, Color("dceaf0"))
var type_counts: Dictionary = summary.types
var type_order: Array = summary.get("type_order", [])
for category_info in [{"name": "air", "x": 7.0}, {"name": "ground", "x": 133.0}]:
var category: String = category_info.name
var category_x: float = float(category_info.x)
_draw_wave_category_icon(Vector2(rect.position.x + category_x + 7.0, rect.position.y + 32.0), category, Color("78f29a"))
var drawn := 0
for type in type_order:
if _enemy_category(type) != category:
continue
if drawn >= 3:
break
var icon_rect := Rect2(rect.position.x + category_x + 17.0 + drawn * 35.0, rect.position.y + 19.0, 15.0, 13.0)
_draw_enemy_miniature(type, icon_rect)
draw_string(font, icon_rect.position + Vector2(16, 12), "×%d" % int(type_counts[type]), HORIZONTAL_ALIGNMENT_LEFT, 19, 8, Color("dceaf0"))
drawn += 1
func _wave_preview_summary(for_wave: int) -> Dictionary:
var summary := {"total": 0, "air": 0, "ground": 0, "types": {}, "type_order": []}
for enemy_type in _wave_preview_types(for_wave):
var type := int(enemy_type)
var category := _enemy_category(type)
if not summary.types.has(type):
summary.types[type] = 0
summary.type_order.append(type)
summary.types[type] = int(summary.types[type]) + 1
summary[category] = int(summary[category]) + 1
summary.total = int(summary.total) + 1
return summary
func _wave_preview_types(for_wave: int) -> Array[int]:
var result: Array[int] = []
if for_wave <= 0:
return result
if coop_online_mode and for_wave == wave:
for enemy in enemies:
result.append(int(enemy.type))
result.append_array(online_wave_types_remaining)
return result
if for_wave == wave and not wave_type_schedule.is_empty():
for entry in wave_type_schedule:
result.append(int(entry.type))
return result
for entry in _build_wave_schedule(for_wave, _wave_population(for_wave)):
result.append(int(entry.type))
return result
func _draw_wave_category_icon(center: Vector2, category: String, color: Color) -> void:
var direction := 1.0 if category == "air" else -1.0
var arrow := PackedVector2Array([
center + Vector2(0, -7.0 * direction),
center + Vector2(-5, -1.0 * direction),
center + Vector2(-2, -1.0 * direction),
center + Vector2(-2, 5.0 * direction),
center + Vector2(2, 5.0 * direction),
center + Vector2(2, -1.0 * direction),
center + Vector2(5, -1.0 * direction)
])
draw_colored_polygon(arrow, color)
func _draw_enemy_miniature(enemy_type: int, rect: Rect2) -> void:
draw_rect(rect.grow(1.0), Color(0.008, 0.018, 0.03, 0.88), true)
draw_texture_rect_region(_enemy_texture(enemy_type), rect, _enemy_source_rect(enemy_type), Color.WHITE)
draw_rect(rect.grow(1.0), Color("4e8194", 0.75), false, 1.0)
func _speed_option_rect(index: int) -> Rect2:
return Rect2(speed_button.position.x, speed_button.end.y + 2.0 + index * 28.0, speed_button.size.x, 27.0)
@@ -2136,16 +2446,29 @@ func _draw_tower_card(slot: int, type: int, active: bool) -> void:
var data: Dictionary = TOWER_DATA[type]
var font := ThemeDB.fallback_font
var unlocked := unlocked_towers.has(type)
draw_rect(rect, Color("226c88") if active else (Color("173c55") if unlocked else Color("102b3b")), true)
draw_rect(rect, Color("65dfff") if active else (Color("37677b") if unlocked else Color("29485a")), false, 1.5)
var class_color := _tower_class_color(type)
var panel_color := class_color.lightened(0.28) if active else (class_color.darkened(0.42) if unlocked else class_color.darkened(0.62))
draw_rect(rect, panel_color, true)
draw_rect(rect, class_color.lightened(0.30) if active else class_color.darkened(0.18), false, 1.5)
draw_rect(Rect2(rect.position, Vector2(5, rect.size.y)), class_color.lightened(0.25) if unlocked else class_color.darkened(0.15), true)
draw_string(font, rect.position + Vector2(9, 22), data.name, HORIZONTAL_ALIGNMENT_LEFT, 150, 15, Color.WHITE if unlocked else Color("9ab0ba"))
draw_string(font, rect.position + Vector2(158, 22), "%d EN" % data.cost, HORIZONTAL_ALIGNMENT_RIGHT, 86, 12, Color("7ce7ff"))
draw_string(font, rect.position + Vector2(9, 47), "SCH %.0f • %.2f SCHUSS/S" % [data.damage, _tower_shots_per_second(type, 1)], HORIZONTAL_ALIGNMENT_LEFT, 237, 11, Color("dceaf0"))
draw_string(font, rect.position + Vector2(9, 72), "RW %.0f • %s" % [data.range, data.damage_type], HORIZONTAL_ALIGNMENT_LEFT, 237, 10, Color("9fdce9"))
draw_string(font, rect.position + Vector2(9, 72), "RW %.0f • %s" % [data.range, _tower_target_label(type)], HORIZONTAL_ALIGNMENT_LEFT, 237, 10, Color("9fdce9"))
var hint := "ANTIPPEN • BAUPLATZ WÄHLEN" if unlocked else "GESPERRT • IM SHOP FREISCHALTEN"
draw_string(font, rect.position + Vector2(9, 105), hint, HORIZONTAL_ALIGNMENT_LEFT, 237, 10, Color("719aaa") if unlocked else Color("ffcf7a"))
func _tower_class_color(tower_type: int) -> Color:
match _tower_target_class(tower_type):
"air":
return Color("bb5260") # Rot: Luft
"ground":
return Color("668c65") # Grün: Boden
_:
return Color("347b98") # Blau-Cyan: Luft + Boden
func _draw_store() -> void:
var font := ThemeDB.fallback_font
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.70), true)
@@ -2181,7 +2504,7 @@ func _draw_store() -> void:
draw_texture_rect(textures[slot], Rect2(card.position + Vector2(112, 18), Vector2(136, 136)), false)
draw_string(font, card.position + Vector2(20, 190), data.name, HORIZONTAL_ALIGNMENT_CENTER, 320, 23, Color(data.color))
draw_string(font, card.position + Vector2(22, 224), "SCH %.0f • %.2f SCHUSS/S" % [data.damage, _tower_shots_per_second(tower_type, 1)], HORIZONTAL_ALIGNMENT_CENTER, 316, 15, Color.WHITE)
draw_string(font, card.position + Vector2(22, 251), "RW %.0f • %s" % [data.range, data.damage_type], HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("9fdce9"))
draw_string(font, card.position + Vector2(22, 251), "RW %.0f • %s" % [data.range, _tower_target_label(tower_type)], HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("9fdce9"))
draw_string(font, card.position + Vector2(22, 295), data.description, HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("c4d7df"))
var is_unlocked := unlocked_towers.has(tower_type)
var button_text := "FREIGESCHALTET" if is_unlocked else "FREISCHALTEN • %d KERNKRISTALLE" % int(data.unlock_cost)
@@ -2212,7 +2535,7 @@ func _draw_tower_info_panel() -> void:
elif is_built:
draw_string(font, tower_info_rect.position + Vector2(10, 62), "MAXIMALE AUSBAUSTUFE", HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
else:
draw_string(font, tower_info_rect.position + Vector2(10, 62), str(data.damage_type), HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
draw_string(font, tower_info_rect.position + Vector2(10, 62), "%s • %s" % [_tower_target_label(type), str(data.damage_type)], HORIZONTAL_ALIGNMENT_LEFT, 280, 9, Color("84ffca"))
func _draw_button(rect: Rect2, text: String, active: bool, base := Color("173c55")) -> void: