Files
alpacaman 98eaa77175
/ build (push) Failing after 2m12s
add ci build
2026-04-21 15:32:59 +02:00

1276 lines
45 KiB
GDScript

extends Node2D
var W: float = 960.0
var H: float = 600.0
const STAR_COUNT := 80
const STAR_MAX := 160 # hard cap including white hole injections
const PLANET_COUNT := 4
const PLANET_MAX := 20 # hard cap including supernova spawns
const NEBULA_COUNT := 3
const GALAXY_COUNT := 4
const ENEMY_COUNT := 2
const BH_START_COUNT := 1
const BH_MAX_COUNT := 8
const WHITE_HOLE_MAX := 2 # cap white holes so they don't pile up
const COMET_SPAWN_MIN := 3.0
const COMET_SPAWN_MAX := 10.0
const ANTIMATTER_SPAWN_MIN := 300.0
const ANTIMATTER_SPAWN_MAX := 600.0
const DIFFICULTY_RAMP_SECS := 300.0 # full difficulty at 5 minutes
const OBJECT_WIPE_THRESHOLD := 500
const PHYS_DT := 1.0 / 60.0
const MAX_ENEMEYS := 10000
var frame: int = 0
var is_playing: bool = false
var is_multiplayer: bool = false
var palette_p1: Dictionary
var palette_p2: Dictionary
var stars: Array = []
var planets: Array = []
var nebulae: Array = []
var comets: Array = []
var galaxies: Array = []
var black_holes: Array = []
var quasars: Array = []
var white_holes: Array = []
var neutron_stars: Array = []
var antimatter: Array = []
var bullets: Array = []
var particles: Array = []
var antimatter_stars: Array = []
var players: Array = []
var enemies: Array = []
var comet_timer: float = 0.0
var comet_next: float = 5.0
var antimatter_timer: float = 0.0
var antimatter_next: float = 50.0
var antimatter_warn_pending: bool = false
var antimatter_warn_pos: Vector2 = Vector2.ZERO
var antimatter_warn_timer: float = 0.0
const ANTIMATTER_WARN_DURATION := 1.5
var credit_tick_timer: float = 0.0
var big_wipe: BigWipe = null
var wipe_count_p1: int = 0
var wipe_count_p2: int = 0
var main_node: Node = null
var menu_mode: bool = false
var game_time: float = 0.0
var difficulty: float = 0.0
var wave_duration: float = 60.0 # dynamisch: 20s Welle 1, +8s pro Welle, max 120s
var wave_timer: float = 0.0
var wave_complete_flag: bool = false
var _boss: BossShip = null
var _wave_number_local: int = 1
var _phys_accum: float = 0.0
var _planet_spawn_t: float = 0.0
func _ready() -> void:
_update_viewport_size()
get_viewport().size_changed.connect(_update_viewport_size)
set_process(false)
func _update_viewport_size() -> void:
var vs: Vector2 = get_viewport_rect().size
W = vs.x
H = vs.y
func init_menu_mode() -> void:
menu_mode = true
is_playing = false
frame = 0
_phys_accum = 0.0
_clear_all()
_spawn_universe()
# Spawn enemy ships so the simulation looks alive
for i in ENEMY_COUNT:
var e := EnemyShip.new()
var ex := W * 0.85 if i == 0 else W * 0.15
var ey := H * 0.15 if i == 0 else H * 0.85
e.init(ex, ey, i % 2, i)
enemies.append(e)
big_wipe = BigWipe.new()
big_wipe.wipe_complete.connect(_on_wipe_complete)
set_process(true)
func init_world(pal1: Dictionary, pal2, multi: bool, stats1: ShipStats = null, stats2: ShipStats = null, wave: int = 1) -> void:
menu_mode = false
palette_p1 = pal1
palette_p2 = pal2 if pal2 != null else {}
is_multiplayer = multi
is_playing = false
frame = 0
_phys_accum = 0.0
game_time = 0.0
difficulty = 0.0
credit_tick_timer = 0.0
wave_timer = 0.0
wave_complete_flag = false
_boss = null
_wave_number_local = wave
wave_duration = clamp(20.0 + float(wave - 1) * 8.0, 20.0, 120.0)
_clear_all()
_spawn_universe()
var p1 := Spaceship.new()
p1.init(W * 0.3, H * 0.5, pal1, 0)
p1.invuln_timer = Spaceship.INVULN_FRAMES * 3 # 4.5s grace at game start
if stats1 != null: p1.apply_stats(stats1)
players.append(p1)
if multi and pal2 != null:
var p2 := Spaceship.new()
p2.init(W * 0.7, H * 0.5, pal2, 1)
p2.invuln_timer = Spaceship.INVULN_FRAMES * 3
if stats2 != null: p2.apply_stats(stats2)
players.append(p2)
# 2 enemies on wave 1, +1 per wave (faster scale than before), capped by MAX_ENEMEYS
var enemy_count: int = min(2 + (wave - 1), MAX_ENEMEYS)
for i in enemy_count:
var e := EnemyShip.new()
var ex: float
var ey: float
if i == 0:
ex = W * 0.85; ey = H * 0.15
elif i == 1:
ex = W * 0.15; ey = H * 0.85
else:
# Extra enemies from random edges
match randi() % 4:
0: ex = randf_range(0.0, W); ey = 0.0
1: ex = randf_range(0.0, W); ey = H
2: ex = 0.0; ey = randf_range(0.0, H)
_: ex = W; ey = randf_range(0.0, H)
e.init(ex, ey, i % 2, i)
if wave >= 10:
var item: Dictionary = ItemDB.roll_shop(1, [])[0]
var es := ShipStats.new()
es.apply_item(item["effects"])
e.apply_stats(es)
enemies.append(e)
# Boss-Spawn auf Welle 5 und 8
if wave == 5:
_boss = BossShip.new()
_boss.init_miniboss(W * 0.5, H * 0.5)
elif wave == 8:
_boss = BossShip.new()
_boss.init_boss(W * 0.5, H * 0.5)
big_wipe = BigWipe.new()
big_wipe.wipe_complete.connect(_on_wipe_complete)
set_process(true)
func _clear_all() -> void:
stars.clear(); planets.clear(); nebulae.clear(); comets.clear()
galaxies.clear(); black_holes.clear(); quasars.clear()
white_holes.clear(); neutron_stars.clear(); antimatter.clear()
bullets.clear(); particles.clear(); players.clear(); enemies.clear()
antimatter_stars.clear()
func _spawn_universe() -> void:
var star_count: int = [40, STAR_COUNT, 160][Settings.star_density]
for _i in star_count:
var s := CosmicObjects.Star.new()
s.init(randf()*W, randf()*H, W, H)
stars.append(s)
for _i in PLANET_COUNT:
var p := CosmicObjects.Planet.new()
p.init(randf()*W, randf()*H, W, H)
planets.append(p)
for _i in NEBULA_COUNT:
var n := CosmicObjects.Nebula.new()
n.init(W, H)
nebulae.append(n)
for _i in GALAXY_COUNT:
var g := CosmicObjects.Galaxy.new()
g.init(randf()*W, randf()*H)
galaxies.append(g)
# Spawn mobile black holes — keep them away from player spawn areas
var bh_attempts := 0
var spawned_bhs := 0
while spawned_bhs < BH_START_COUNT and bh_attempts < 30:
bh_attempts += 1
var bx := randf_range(W * 0.1, W * 0.9)
var by := randf_range(H * 0.1, H * 0.9)
# Reject if too close to either player start
var d1 := sqrt((bx - W*0.3)*(bx - W*0.3) + (by - H*0.5)*(by - H*0.5))
var d2 := sqrt((bx - W*0.7)*(bx - W*0.7) + (by - H*0.5)*(by - H*0.5))
if d1 < 220.0 or d2 < 220.0:
continue
var bh := BlackHole.new()
bh.init(bx, by, true)
black_holes.append(bh)
spawned_bhs += 1
var q := CosmicObjects.Quasar.new()
q.init(randf()*W, randf()*H)
quasars.append(q)
func start_playing() -> void:
is_playing = true
wave_timer = 0.0
wave_complete_flag = false
# Boss-Musik starten wenn Welle einen Boss hat
if _boss != null and main_node and main_node.has_method("start_boss_music"):
# LEVIATHAN (Welle 8) bekommt eigenen Track; WRAITH (Welle 5) den Standard-Boss-Track
main_node.start_boss_music(not _boss.is_miniboss)
func on_player_died() -> void:
is_playing = false
func get_score_data() -> Dictionary:
var data: Dictionary = {"p1_time": 0.0, "p1_kills": 0, "p1_wipe": 0,
"p2_time": 0.0, "p2_kills": 0, "p2_wipe": 0, "multiplayer": is_multiplayer}
if players.size() > 0:
var p0: Spaceship = players[0]
data["p1_time"] = p0.survival_time
data["p1_kills"] = p0.kills
data["p1_wipe"] = wipe_count_p1 * BigWipe.WIPE_BONUS
if players.size() > 1:
var p1: Spaceship = players[1]
data["p2_time"] = p1.survival_time
data["p2_kills"] = p1.kills
data["p2_wipe"] = wipe_count_p2 * BigWipe.WIPE_BONUS
return data
func _total_object_count() -> int:
return stars.size() + planets.size() + comets.size() + galaxies.size() \
+ black_holes.size() + quasars.size() + white_holes.size() \
+ neutron_stars.size() + antimatter.size() + antimatter_stars.size()
func _process(delta: float) -> void:
if not is_playing and not menu_mode:
queue_redraw()
return
_phys_accum = min(_phys_accum + delta, PHYS_DT * 5.0)
while _phys_accum >= PHYS_DT:
_phys_accum -= PHYS_DT
frame += 1
_tick(PHYS_DT)
queue_redraw()
func _tick(dt: float) -> void:
_handle_input(dt)
_update_objects(dt)
_update_bullets()
# Boss-Bewegung und Schüsse (vor Kollisionscheck, damit Kugeln im Array sind)
if _boss != null and not _boss.dead and is_playing and not menu_mode:
var boss_bullets: Array = _boss.update(dt, W * 0.5, H * 0.5)
for bb in boss_bullets:
bullets.append(bb)
_update_particles(dt)
_check_collisions()
_check_big_wipe()
if big_wipe and big_wipe.is_active():
big_wipe.update(dt, is_multiplayer)
# Advance difficulty only during real gameplay
if not menu_mode and is_playing:
game_time += dt
difficulty = clamp(game_time / DIFFICULTY_RAMP_SECS, 0.0, 1.0)
# Passive credit trickle: 5 credits per 10 seconds
credit_tick_timer += dt
if credit_tick_timer >= 10.0:
credit_tick_timer = 0.0
if main_node and players.size() > 0:
main_node.add_credits(0, 5)
# Wave timer — skaliert von 20s (Welle 1) auf 120s
wave_timer += dt
if wave_timer >= wave_duration and not wave_complete_flag:
wave_complete_flag = true
if main_node:
# Boss überlebt → Musik trotzdem zurückschalten
if _boss != null and main_node.has_method("stop_boss_music"):
main_node.stop_boss_music()
main_node.on_wave_complete()
# Boss gestorben → Welle sofort abschließen + Bonus-Credits
if _boss != null and _boss.dead and not wave_complete_flag:
wave_complete_flag = true
var boss_bonus: int = 150 if _boss.is_miniboss else 300
if main_node:
main_node.add_credits(0, boss_bonus)
if players.size() > 1:
main_node.add_credits(1, boss_bonus)
# Zurück zur normalen Musik
if main_node.has_method("stop_boss_music"):
main_node.stop_boss_music()
main_node.on_wave_complete()
# Boss Phase-2-Übergang: schwarzes Loch spawnen + Musik verdichtet
if _boss != null and not _boss.is_miniboss and _boss._just_entered_phase2:
_boss._just_entered_phase2 = false
if black_holes.size() < BH_MAX_COUNT:
var phase_bh := BlackHole.new()
phase_bh.init(_boss.x, _boss.y, true)
black_holes.append(phase_bh)
if main_node and main_node.has_method("boss_phase_changed"):
main_node.boss_phase_changed(2)
comet_timer += dt
var comet_max_interval: float = lerp(float(COMET_SPAWN_MAX), float(COMET_SPAWN_MIN), difficulty)
if comet_timer >= comet_next:
comet_timer = 0.0
comet_next = randf_range(COMET_SPAWN_MIN, comet_max_interval)
var c := CosmicObjects.Comet.new()
c.init(W, H)
comets.append(c)
antimatter_timer += dt
if not antimatter_warn_pending:
var am_max_interval: float = lerp(float(ANTIMATTER_SPAWN_MAX), 120.0, difficulty)
if antimatter_timer >= antimatter_next:
antimatter_timer = 0.0
antimatter_next = randf_range(ANTIMATTER_SPAWN_MIN, am_max_interval)
antimatter_warn_pos = Vector2(W * 0.5 + randf_range(-100, 100), H * 0.5 + randf_range(-100, 100))
antimatter_warn_pending = true
antimatter_warn_timer = 0.0
SoundManager.play_sfx("antimatter_warn")
if antimatter_warn_pending:
antimatter_warn_timer += dt
if antimatter_warn_timer >= ANTIMATTER_WARN_DURATION:
antimatter_warn_pending = false
_spawn_antimatter_swarm(antimatter_warn_pos.x, antimatter_warn_pos.y)
SoundManager.play_sfx("antimatter_swarm")
var all_dead: bool = true
for player in players:
var sp: Spaceship = player
if not sp.dead:
all_dead = false
if all_dead and is_playing:
is_playing = false
if main_node:
main_node.on_game_over()
func _handle_input(dt: float) -> void:
if players.size() > 0:
var p0: Spaceship = players[0]
if not p0.dead:
var thrust: bool = Input.is_action_pressed("p1_thrust")
var turn: int = 0
if Input.is_action_pressed("p1_left"): turn = -1
elif Input.is_action_pressed("p1_right"): turn = 1
p0.update(thrust, turn, W, H, dt)
if p0.has_charge_weapon():
if Input.is_action_just_pressed("p1_shoot"):
p0.charge_timer = 0.0
elif Input.is_action_pressed("p1_shoot"):
p0.charge_timer = min(p0.charge_timer + dt, p0.CHARGE_MAX)
elif Input.is_action_just_released("p1_shoot"):
var b: Bullet = p0.shoot_charged(p0.charge_timer / p0.CHARGE_MAX)
if b != null:
bullets.append(b)
SoundManager.play_sfx("player_shoot")
elif Input.is_action_pressed("p1_shoot"):
for b: Bullet in p0.shoot_burst():
bullets.append(b)
SoundManager.play_sfx("player_shoot")
if Input.is_action_pressed("p1_wipe") and big_wipe and big_wipe.is_active():
big_wipe.player_press_survive(0)
if Input.is_action_just_pressed("p1_boost"):
if p0.try_boost():
SoundManager.play_sfx("player_shoot")
if players.size() > 1:
var p1: Spaceship = players[1]
if not p1.dead:
var thrust: bool = Input.is_action_pressed("p2_thrust")
var turn: int = 0
if Input.is_action_pressed("p2_left"): turn = -1
elif Input.is_action_pressed("p2_right"): turn = 1
p1.update(thrust, turn, W, H, dt)
if p1.has_charge_weapon():
if Input.is_action_just_pressed("p2_shoot"):
p1.charge_timer = 0.0
elif Input.is_action_pressed("p2_shoot"):
p1.charge_timer = min(p1.charge_timer + dt, p1.CHARGE_MAX)
elif Input.is_action_just_released("p2_shoot"):
var b: Bullet = p1.shoot_charged(p1.charge_timer / p1.CHARGE_MAX)
if b != null:
bullets.append(b)
SoundManager.play_sfx("player_shoot")
elif Input.is_action_pressed("p2_shoot"):
for b: Bullet in p1.shoot_burst():
bullets.append(b)
SoundManager.play_sfx("player_shoot")
if Input.is_action_pressed("p2_wipe") and big_wipe and big_wipe.is_active():
big_wipe.player_press_survive(1)
if Input.is_action_just_pressed("p2_boost"):
if p1.try_boost():
SoundManager.play_sfx("player_shoot")
func _update_objects(delta: float) -> void:
for star in stars:
var s: CosmicObjects.Star = star
var spawn_swarm: bool = s.update(delta, W, H)
if spawn_swarm:
_spawn_antimatter_swarm(s.x, s.y)
s.dead = true
stars = stars.filter(func(s): return not s.dead)
while stars.size() < STAR_COUNT:
var ns := CosmicObjects.Star.new()
ns.init(randf()*W, H + 2.0, W, H)
stars.append(ns)
for planet in planets:
var p: CosmicObjects.Planet = planet
p.update(delta)
planets = planets.filter(func(p): return not p.dead)
# Slow refill: one planet every 20 s while below baseline, up to hard cap
if planets.size() < PLANET_COUNT:
_planet_spawn_t += delta
if _planet_spawn_t >= 20.0:
_planet_spawn_t = 0.0
var rp := CosmicObjects.Planet.new()
rp.init(randf_range(W * 0.05, W * 0.95), randf_range(H * 0.05, H * 0.95), W, H)
planets.append(rp)
for neb in nebulae:
var n: CosmicObjects.Nebula = neb
n.update(W, H)
for comet in comets:
var c: CosmicObjects.Comet = comet
c.update(W, H)
for comet in comets:
var c: CosmicObjects.Comet = comet
if c.dead:
SoundManager.play_sfx("comet_whistle")
_fire_music_event("comet_pass")
comets = comets.filter(func(c): return not c.dead)
for gal in galaxies:
var g: CosmicObjects.Galaxy = gal
g.update(delta, W, H)
# When galaxy finishes being consumed, trigger SMBH
if g.dead and not g.respawning and g.consuming_bh != null:
var cbh: BlackHole = g.consuming_bh
_spawn_spiral_particles(g.x, g.y, cbh.x, cbh.y, 16, Color(1.0, 0.8, 0.3), 3.0)
cbh.trigger_flash(Color(1.0, 0.8, 0.3), 1.0)
g.consuming_bh = null
g.respawning = true
g.respawn_timer = randf_range(5.0, 10.0)
_fire_music_event("galaxy_consumed")
if cbh.consumed >= 12 and not cbh.is_smbh:
cbh.become_smbh()
SoundManager.play_sfx("smbh_spawn")
_fire_music_event("bh_pulse")
for qsr in quasars:
var q: CosmicObjects.Quasar = qsr
q.update(delta)
# Radiation push — always active, no thrust required
for player in players:
var p: Spaceship = player
if p.dead: continue
var nv: Vector2 = q.push_object(p.x, p.y, p.vx, p.vy)
p.vx = nv.x; p.vy = nv.y
for en in enemies:
var e: EnemyShip = en
if not e.dead:
var nv: Vector2 = q.push_object(e.x, e.y, e.vx, e.vy)
e.vx = nv.x; e.vy = nv.y
for comet in comets:
var c: CosmicObjects.Comet = comet
if not c.dead:
var nv: Vector2 = q.push_object(c.x, c.y, c.vx, c.vy)
c.vx = nv.x; c.vy = nv.y
# Jet boost — only while thrusting
for player in players:
var p: Spaceship = player
if p.dead or not p.is_thrusting: continue
var nv: Vector2 = q.boost_if_in_jet(p.x, p.y, p.vx, p.vy)
if nv.x != p.vx or nv.y != p.vy:
p.vx = nv.x; p.vy = nv.y
p.boost_active_t = max(p.boost_active_t, 0.25)
if q.boost_sound_cd <= 0.0:
q.boost_sound_cd = 1.2
SoundManager.play_sfx("quasar_boost")
_fire_music_event("quasar_jet")
quasars = quasars.filter(func(q): return not q.dead)
_merge_quasars()
var new_stars_from_wh: Array = []
var new_planets_from_wh: Array = []
for whole in white_holes:
var wh: CosmicObjects.WhiteHole = whole
var result: Dictionary = wh.update(delta)
new_stars_from_wh.append_array(result["stars"])
new_planets_from_wh.append_array(result["planets"])
if result["stars"].size() > 0 or result["planets"].size() > 0:
_fire_music_event("white_hole_eject")
for player in players:
var p: Spaceship = player
if not p.dead:
var nv: Vector2 = wh.push_object(p.x, p.y, p.vx, p.vy)
p.vx = nv.x; p.vy = nv.y
for en in enemies:
var e: EnemyShip = en
if not e.dead:
var nv: Vector2 = wh.push_object(e.x, e.y, e.vx, e.vy)
e.vx = nv.x; e.vy = nv.y
for comet in comets:
var c: CosmicObjects.Comet = comet
if not c.dead:
var nv: Vector2 = wh.push_object(c.x, c.y, c.vx, c.vy)
c.vx = nv.x; c.vy = nv.y
for star in stars:
var s: CosmicObjects.Star = star
if s.dead or s.is_spiraling: continue
var nv: Vector2 = wh.push_object(s.x, s.y, s.grav_vx, s.grav_vy)
s.grav_vx = nv.x; s.grav_vy = nv.y
# Only inject up to the hard caps
for ws in new_stars_from_wh:
if stars.size() < STAR_MAX: stars.append(ws)
for wp in new_planets_from_wh:
if planets.size() < PLANET_MAX: planets.append(wp)
white_holes = white_holes.filter(func(wh): return not wh.dead)
for nstar in neutron_stars:
var ns: CosmicObjects.NeutronStar = nstar
ns.update(delta)
for player in players:
var p: Spaceship = player
if not p.dead:
var prev_vx := p.vx; var prev_vy := p.vy
var nv: Vector2 = ns.push_if_in_beam(p.x, p.y, p.vx, p.vy)
p.vx = nv.x; p.vy = nv.y
if nv.x != prev_vx or nv.y != prev_vy:
_fire_music_event("neutron_beam")
neutron_stars = neutron_stars.filter(func(ns): return not ns.dead)
for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj
am.update(W, H, delta, antimatter)
antimatter = antimatter.filter(func(am): return not am.dead)
_check_antimatter_clustering()
for as_obj in antimatter_stars:
var astar: CosmicObjects.AntimatterStar = as_obj
astar.update(delta)
for player in players:
var p: Spaceship = player
if not p.dead:
var nv: Vector2 = astar.apply_repulsion(p.x, p.y, p.vx, p.vy)
p.vx = nv.x; p.vy = nv.y
antimatter_stars = antimatter_stars.filter(func(a): return not a.dead)
_apply_gravity_all(delta)
for en in enemies:
var e: EnemyShip = en
if e.dead:
e.respawn_timer -= delta
if e.respawn_timer <= 0:
# Spawn from a random screen edge, away from players
var edge := randi() % 4
var ex: float; var ey: float
match edge:
0: ex = randf_range(0.0, W); ey = 0.0
1: ex = randf_range(0.0, W); ey = H
2: ex = 0.0; ey = randf_range(0.0, H)
_: ex = W; ey = randf_range(0.0, H)
e.init(ex, ey, e.enemy_index, e.role_id)
continue
var b: Bullet = e.update(players, black_holes, enemies, W, H, delta)
if b != null:
bullets.append(b)
SoundManager.play_sfx("enemy_shoot")
_update_bh_proximity_audio()
func _apply_gravity_all(_delta: float) -> void:
for bhole in black_holes:
var bh: BlackHole = bhole
if bh.dead: continue # skip BHs already killed this tick
bh.update(_delta, players, W, H)
# ── Stars ──────────────────────────────────────────────────────────
for star in stars:
if bh.dead: break # BH just supernovaed — stop immediately
var s: CosmicObjects.Star = star
var nv: Vector2 = bh.apply_gravity(s.x, s.y, s.grav_vx, s.grav_vy)
s.grav_vx = nv.x * 0.99
s.grav_vy = nv.y * 0.99
var gsp := sqrt(s.grav_vx*s.grav_vx + s.grav_vy*s.grav_vy)
if gsp > BlackHole.OBJ_MAX_VEL:
s.grav_vx = s.grav_vx/gsp * BlackHole.OBJ_MAX_VEL
s.grav_vy = s.grav_vy/gsp * BlackHole.OBJ_MAX_VEL
if not s.is_spiraling and bh.check_swallow(s.x, s.y):
s.start_spiral(bh.x, bh.y)
bh.trigger_flash(Color(0.6, 0.7, 1.0), 0.6)
var trigger: bool = bh.on_swallow()
SoundManager.play_sfx("bh_swallow")
if trigger:
_trigger_supernova(bh)
break # BH is dead — do not consume more objects this tick
if bh.dead: continue
# ── Planets ────────────────────────────────────────────────────────
for planet in planets:
if bh.dead: break
var p: CosmicObjects.Planet = planet
if not p.captured:
var dx: float = p.x - bh.x; var dy: float = p.y - bh.y
if sqrt(dx*dx + dy*dy) < bh.pull_radius * 0.6:
p.start_capture(bh.x, bh.y)
if p.captured and bh.check_swallow(p.x, p.y):
_spawn_spiral_particles(p.x, p.y, bh.x, bh.y, int(p.radius * 3.0), p.color, 5.0)
bh.trigger_flash(p.color, 0.8)
p.dead = true
SoundManager.play_sfx("planet_impact")
_fire_music_event("planet_captured")
var trigger: bool = bh.on_swallow()
if trigger:
_trigger_supernova(bh)
break
if bh.dead: continue
# ── Bullets, Players, Enemies ──────────────────────────────────────
for bul in bullets:
var b: Bullet = bul
if bh.check_swallow(b.x, b.y):
b.dead = true
for player in players:
var p: Spaceship = player
if p.dead or p.is_invulnerable(): continue
var prev_vx := p.vx; var prev_vy := p.vy
var nv: Vector2 = bh.apply_gravity(p.x, p.y, p.vx, p.vy)
if p.stats and p.stats.bh_resist > 0.0:
var resist := 1.0 - p.stats.bh_resist
p.vx = lerp(prev_vx, nv.x, resist)
p.vy = lerp(prev_vy, nv.y, resist)
else:
p.vx = nv.x; p.vy = nv.y
if bh.check_swallow(p.x, p.y):
_kill_player(p)
SoundManager.play_sfx("player_die")
for en in enemies:
var e: EnemyShip = en
if e.dead: continue
var nv: Vector2 = bh.apply_gravity(e.x, e.y, e.vx, e.vy)
e.vx = nv.x; e.vy = nv.y
# ── Galaxies ───────────────────────────────────────────────────────
for gal in galaxies:
var g: CosmicObjects.Galaxy = gal
if g.dead or g.respawning or g.being_consumed: continue
var gdx: float = bh.x - g.x; var gdy: float = bh.y - g.y
if sqrt(gdx*gdx + gdy*gdy) < g.radius * 0.55:
g.being_consumed = true
g.consuming_bh = bh
g.consume_bh_x = bh.x
g.consume_bh_y = bh.y
g.consume_initial_radius = g.radius
g.consume_initial_alpha = g.alpha
g.original_color = g.color
for gal in galaxies:
var g: CosmicObjects.Galaxy = gal
if g.being_consumed and g.consuming_bh != null and g.consuming_bh == bh:
g.consume_bh_x = bh.x
g.consume_bh_y = bh.y
# ── Antimatter ─────────────────────────────────────────────────────
for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj
if am.dead: continue
if bh.check_swallow(am.x, am.y):
am.dead = true
_spawn_spiral_particles(am.x, am.y, bh.x, bh.y, 10, Color(1.0, 0.2, 0.9), 4.0)
bh.trigger_flash(Color(1.0, 0.2, 0.9), 0.5)
# ── Antimatter Stars ───────────────────────────────────────────────
for as_obj in antimatter_stars:
if bh.dead: break
var astar: CosmicObjects.AntimatterStar = as_obj
if astar.dead: continue
if bh.check_swallow(astar.x, astar.y):
astar.dead = true
_spawn_spiral_particles(astar.x, astar.y, bh.x, bh.y, 20, Color(0.9, 0.1, 1.0), 5.0)
bh.trigger_flash(Color(0.9, 0.1, 1.0), 0.8)
var trigger: bool = bh.on_swallow()
if trigger:
_trigger_supernova(bh)
break
# ── White holes (one per tick, break after any consumption) ────────
for whole in white_holes:
if bh.dead: break
var wh: CosmicObjects.WhiteHole = whole
if wh.dead: continue
var wdx: float = wh.x - bh.x; var wdy: float = wh.y - bh.y
if sqrt(wdx*wdx + wdy*wdy) < bh.radius + wh.radius:
wh.dead = true
_spawn_spiral_particles(wh.x, wh.y, bh.x, bh.y, 20, Color(0.6, 0.9, 1.0), 5.0)
bh.trigger_flash(Color(0.6, 0.9, 1.0), 1.0)
var trigger: bool = bh.on_swallow()
SoundManager.play_sfx("bh_swallow")
if trigger:
_trigger_supernova(bh)
break # only one white hole consumed per tick
# ── Quasars (only eaten if BH is larger) ──────────────────────────
for qsr in quasars:
if bh.dead: break
var q: CosmicObjects.Quasar = qsr
if q.dead or bh.radius <= q.radius: continue
var qdx := q.x - bh.x; var qdy := q.y - bh.y
if sqrt(qdx*qdx + qdy*qdy) < bh.radius + q.radius:
q.dead = true
_spawn_spiral_particles(q.x, q.y, bh.x, bh.y, 25, Color(1.0, 0.9, 0.3), 5.0)
bh.trigger_flash(Color(1.0, 0.9, 0.3), 1.0)
var trigger: bool = bh.on_swallow()
SoundManager.play_sfx("bh_swallow")
if trigger:
_trigger_supernova(bh)
break
# BH-BH merging — larger eats smaller when they overlap
_merge_black_holes()
func _merge_quasars() -> void:
var n := quasars.size()
for i in n:
var qa: CosmicObjects.Quasar = quasars[i]
if qa.dead: continue
for j in range(i + 1, n):
var qb: CosmicObjects.Quasar = quasars[j]
if qb.dead: continue
var dx := qa.x - qb.x; var dy := qa.y - qb.y
if sqrt(dx*dx + dy*dy) < qa.radius + qb.radius + 60.0:
var bigger := qa if qa.radius >= qb.radius else qb
var smaller := qb if qa.radius >= qb.radius else qa
bigger.radius = minf(55.0, bigger.radius + smaller.radius * 0.6)
bigger.life = maxf(0.0, bigger.life - 8.0)
_spawn_spiral_particles(smaller.x, smaller.y, bigger.x, bigger.y,
18, Color(1.0, 0.9, 0.3), 4.0)
smaller.dead = true
func _merge_black_holes() -> void:
var pending_supernovas: Array = []
var n := black_holes.size()
for i in n:
var bh1: BlackHole = black_holes[i]
if bh1.dead: continue
for j in range(i + 1, n):
var bh2: BlackHole = black_holes[j]
if bh2.dead: continue
var dx: float = bh1.x - bh2.x
var dy: float = bh1.y - bh2.y
if sqrt(dx*dx + dy*dy) < bh1.radius + bh2.radius:
var bigger: BlackHole = bh1 if bh1.radius >= bh2.radius else bh2
var smaller: BlackHole = bh2 if bh1.radius >= bh2.radius else bh1
_spawn_spiral_particles(smaller.x, smaller.y, bigger.x, bigger.y, 20, Color(0.5, 0.1, 1.0), 6.0)
bigger.trigger_flash(Color(0.6, 0.2, 1.0), 1.0)
smaller.dead = true
var trigger: bool = bigger.on_swallow()
if trigger:
pending_supernovas.append(bigger)
else:
SoundManager.play_sfx("bh_swallow")
# Collect dying SMBHs before filtering
var pending_smbh_collapses: Array = []
for bhole in black_holes:
var bh: BlackHole = bhole
if bh.dead and bh.is_smbh and bh.smbh_dying:
pending_smbh_collapses.append(bh)
# Filter first, then trigger events to avoid mid-loop array modification
black_holes = black_holes.filter(func(b): return not b.dead)
for sn in pending_supernovas:
_trigger_supernova(sn)
for bh in pending_smbh_collapses:
_trigger_smbh_collapse(bh)
func _trigger_supernova(bh: BlackHole) -> void:
SoundManager.play_sfx("wipe_flash")
_spawn_explosion_particles(bh.x, bh.y, 40, Color(1.0, 0.8, 0.3))
bh.dead = true
black_holes = black_holes.filter(func(b): return not b.dead)
# Spawn Quasar from the explosion
var q := CosmicObjects.Quasar.new()
q.init(bh.x, bh.y)
quasars.append(q)
# Spawn 1-2 new mobile black holes — ejected outward so they don't re-eat fresh objects
var bh_cap := int(lerp(1.0, float(BH_MAX_COUNT), difficulty))
var spawn_count := randi_range(1, 2)
for _si in spawn_count:
if black_holes.size() < bh_cap:
var nbh := BlackHole.new()
var bea := randf() * TAU
nbh.init(bh.x + cos(bea) * randf_range(120.0, 220.0),
bh.y + sin(bea) * randf_range(120.0, 220.0), true)
black_holes.append(nbh)
# Also spawn a white hole (capped) or neutron star
if randf() > 0.5 and white_holes.size() < WHITE_HOLE_MAX:
var wh := CosmicObjects.WhiteHole.new()
wh.init(bh.x + randf_range(-60,60), bh.y + randf_range(-60,60))
white_holes.append(wh)
else:
var ns := CosmicObjects.NeutronStar.new()
ns.init(bh.x, bh.y)
neutron_stars.append(ns)
# Stars ejected radially — they fly outward from the explosion
for _si in randi_range(8, 14):
if stars.size() >= STAR_MAX: break
var ns2 := CosmicObjects.Star.new()
var ea := randf() * TAU
var ed := randf_range(25.0, 150.0)
ns2.x = bh.x + cos(ea) * ed
ns2.y = bh.y + sin(ea) * ed
ns2.speed = randf_range(0.15, 0.7)
ns2.size = randf_range(1.5, 4.5)
ns2.alpha = 0.7 + randf() * 0.3
ns2.glow = ns2.size >= 3.0
ns2.color = Color(randf_range(0.85, 1.0), randf_range(0.55, 0.95), randf_range(0.2, 0.6))
ns2.grav_vx = cos(ea) * randf_range(0.5, 2.0)
ns2.grav_vy = sin(ea) * randf_range(0.5, 2.0)
ns2.twinkle_phase = randf() * TAU
ns2.twinkle_speed = randf_range(0.5, 2.0)
stars.append(ns2)
for _pi in randi_range(3, 5):
if planets.size() >= PLANET_MAX: break
var np := CosmicObjects.Planet.new()
var ea := randf() * TAU
np.init(bh.x + cos(ea) * randf_range(60.0, 200.0),
bh.y + sin(ea) * randf_range(60.0, 200.0), W, H)
planets.append(np)
# Antimatter burst only during actual gameplay (not menu background)
if not menu_mode:
_spawn_antimatter_swarm(bh.x, bh.y)
func _trigger_smbh_collapse(bh: BlackHole) -> void:
SoundManager.play_sfx("wipe_flash")
_spawn_explosion_particles(bh.x, bh.y, 60, Color(1.0, 0.65, 0.0))
var bh_cap := int(lerp(1.0, float(BH_MAX_COUNT), difficulty))
# Large birth burst — stars ejected radially in all directions
for _i in randi_range(12, 18):
if stars.size() >= STAR_MAX: break
var s := CosmicObjects.Star.new()
var ea := randf() * TAU
var ed := randf_range(40.0, 260.0)
s.x = bh.x + cos(ea) * ed
s.y = bh.y + sin(ea) * ed
s.speed = randf_range(0.2, 0.8)
s.size = randf_range(2.0, 5.5)
s.alpha = 0.8 + randf() * 0.2
s.glow = true
s.color = Color(randf_range(0.9, 1.0), randf_range(0.6, 0.9), randf_range(0.2, 0.5))
s.twinkle_phase = randf() * TAU
s.twinkle_speed = randf_range(0.5, 2.0)
s.grav_vx = cos(ea) * randf_range(1.0, 3.5)
s.grav_vy = sin(ea) * randf_range(1.0, 3.5)
stars.append(s)
# Planets
for _i in randi_range(5, 8):
if planets.size() >= PLANET_MAX: break
var p := CosmicObjects.Planet.new()
var ea := randf() * TAU
p.init(bh.x + cos(ea) * randf_range(80.0, 300.0),
bh.y + sin(ea) * randf_range(80.0, 300.0), W, H)
planets.append(p)
# New smaller BHs — scattered wide so they don't cluster on fresh objects
for _i in 3:
if black_holes.size() >= bh_cap: break
var nb := BlackHole.new()
var ea := randf() * TAU
nb.init(bh.x + cos(ea) * randf_range(160.0, 300.0),
bh.y + sin(ea) * randf_range(160.0, 300.0), true)
black_holes.append(nb)
# White hole at the collapse site
if white_holes.size() < WHITE_HOLE_MAX:
var wh := CosmicObjects.WhiteHole.new()
wh.init(bh.x, bh.y)
white_holes.append(wh)
func _check_antimatter_clustering() -> void:
const CLUSTER_RADIUS := 30.0
const CLUSTER_MIN := 5
var used := {}
var to_remove: Array = []
for i in antimatter.size():
if i in used: continue
var a: CosmicObjects.Antimatter = antimatter[i]
var group: Array = [a]
for j in range(i + 1, antimatter.size()):
if j in used: continue
var b: CosmicObjects.Antimatter = antimatter[j]
var dx := b.x - a.x; var dy := b.y - a.y
if sqrt(dx*dx + dy*dy) < CLUSTER_RADIUS:
group.append(b)
used[j] = true
if group.size() >= CLUSTER_MIN:
used[i] = true
var cx := 0.0; var cy := 0.0
for m in group:
cx += float(m.x); cy += float(m.y)
cx /= group.size(); cy /= group.size()
var astar := CosmicObjects.AntimatterStar.new()
astar.init(cx, cy)
antimatter_stars.append(astar)
for m in group:
to_remove.append(m)
for m in to_remove:
antimatter.erase(m)
func _update_bullets() -> void:
for bul in bullets:
var b: Bullet = bul
b.update(W, H)
bullets = bullets.filter(func(b): return not b.dead)
func _update_particles(delta: float) -> void:
for pt in particles:
pt["prev_x"] = pt["x"]
pt["prev_y"] = pt["y"]
if pt.has("target_x"):
var pdx: float = float(pt["target_x"]) - float(pt["x"])
var pdy: float = float(pt["target_y"]) - float(pt["y"])
var pdist: float = sqrt(pdx * pdx + pdy * pdy)
if pdist > 1.0:
var grav: float = float(pt.get("gravity", 3.0))
var force: float = grav / maxf(pdist * 0.3, 1.0)
pt["vx"] = float(pt["vx"]) + (pdx / pdist) * force * delta * 60.0
pt["vy"] = float(pt["vy"]) + (pdy / pdist) * force * delta * 60.0
pt["x"] = float(pt["x"]) + float(pt["vx"])
pt["y"] = float(pt["y"]) + float(pt["vy"])
pt["life"] = float(pt["life"]) - delta
particles = particles.filter(func(p): return float(p["life"]) > 0.0)
func _check_collisions() -> void:
for bul in bullets:
var b: Bullet = bul
if b.dead: continue
if b.owner_type == "p1" or b.owner_type == "p2":
for en in enemies:
var e: EnemyShip = en
if e.dead or e.invuln_timer > 0: continue
var dx: float = b.x - e.x
var dy: float = b.y - e.y
var hit_r := b.effective_hit_radius
if dx*dx + dy*dy < hit_r * hit_r:
# Pierce: bullet survives if it hasn't hit too many targets
if b.pierce:
b.pierce_hits += 1
if b.pierce_hits >= 2: b.dead = true
else:
b.dead = true
if e.current_shields > 0:
e.current_shields -= 1
e.invuln_timer = 60
_spawn_explosion_particles(e.x, e.y, 8, Color(0.3, 0.6, 1.0))
continue
e.dead = true
e.respawn_timer = randf_range(EnemyShip.RESPAWN_MIN, EnemyShip.RESPAWN_MAX)
_spawn_explosion_particles(e.x, e.y, 16, Color(1,0.4,0.2))
SoundManager.play_sfx("enemy_die")
for player in players:
var p: Spaceship = player
if b.owner_type == ("p1" if p.player_index == 0 else "p2"):
p.kills += 1
# Award kill credits
if main_node and not menu_mode:
var bonus: float = p.stats.credit_bonus if p.stats else 1.0
main_node.add_credits(p.player_index, int(15.0 * bonus))
for bul in bullets:
var b: Bullet = bul
if b.dead: continue
if b.owner_type == "enemy" or b.owner_type == "boss":
for player in players:
var p: Spaceship = player
if p.dead or p.is_invulnerable(): continue
var dx: float = b.x - p.x
var dy: float = b.y - p.y
if dx*dx + dy*dy < Bullet.HIT_RADIUS * Bullet.HIT_RADIUS:
b.dead = true
_kill_player(p)
SoundManager.play_sfx("player_die")
for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj
if am.dead: continue
for player in players:
var p: Spaceship = player
if p.dead or p.is_invulnerable(): continue
var dx: float = am.x - p.x
var dy: float = am.y - p.y
if dx*dx + dy*dy < 100.0:
am.dead = true
_kill_player(p)
SoundManager.play_sfx("antimatter_hit")
for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj
if am.dead: continue
for en in enemies:
var e: EnemyShip = en
if e.dead or e.invuln_timer > 0: continue
var dx: float = am.x - e.x
var dy: float = am.y - e.y
if dx*dx + dy*dy < 100.0:
am.dead = true
if e.current_shields > 0:
e.current_shields -= 1
e.invuln_timer = 60
_spawn_explosion_particles(e.x, e.y, 8, Color(0.3, 0.6, 1.0))
else:
e.dead = true
e.respawn_timer = randf_range(EnemyShip.RESPAWN_MIN, EnemyShip.RESPAWN_MAX)
_spawn_explosion_particles(e.x, e.y, 12, Color(1,0.2,0.8))
SoundManager.play_sfx("antimatter_hit")
# ── INFERNO ram damage ──────────────────────────────────────────────────────
const INFERNO_RAM_SPEED := 4.5
const INFERNO_RAM_RADIUS := 10.0
for player in players:
var p: Spaceship = player
if p.dead or p.is_invulnerable(): continue
if p.stats == null or p.stats.ship_id != "inferno": continue
var spd := sqrt(p.vx*p.vx + p.vy*p.vy)
if spd < INFERNO_RAM_SPEED: continue
for en in enemies:
var e: EnemyShip = en
if e.dead or e.invuln_timer > 0: continue
var dx := p.x - e.x; var dy := p.y - e.y
if dx*dx + dy*dy < INFERNO_RAM_RADIUS * INFERNO_RAM_RADIUS:
p.invuln_timer = Spaceship.INVULN_FRAMES
_spawn_explosion_particles(e.x, e.y, 16, Color(1.0, 0.35, 0.1))
SoundManager.play_sfx("enemy_die")
if e.current_shields > 0:
e.current_shields -= 1
e.invuln_timer = 60
_spawn_explosion_particles(e.x, e.y, 8, Color(0.3, 0.6, 1.0))
else:
e.dead = true
e.respawn_timer = randf_range(EnemyShip.RESPAWN_MIN, EnemyShip.RESPAWN_MAX)
p.kills += 1
if main_node and not menu_mode:
main_node.add_credits(p.player_index, int(15.0 * (p.stats.credit_bonus if p.stats else 1.0)))
break
# Spieler-Bullets treffen Boss
if _boss != null and not _boss.dead:
for bul in bullets:
var b: Bullet = bul
if b.dead: continue
if b.owner_type != "p1" and b.owner_type != "p2": continue
var dx: float = b.x - _boss.x
var dy: float = b.y - _boss.y
var hr: float = b.effective_hit_radius * 2.5 # Boss ist ein großes Ziel
if dx * dx + dy * dy < hr * hr:
b.dead = true
_boss.take_hit()
_spawn_explosion_particles(_boss.x, _boss.y, 6, Color(1.0, 0.45, 0.1))
func _kill_player(p: Spaceship) -> void:
if p.dead: return
# Shield absorbs the hit
if p.current_shields > 0:
p.current_shields -= 1
var invuln_frames := int(float(Spaceship.INVULN_FRAMES) * (p.stats.invuln_mult if p.stats else 1.0))
p.invuln_timer = invuln_frames
_spawn_explosion_particles(p.x, p.y, 12, Color(0.3, 0.6, 1.0))
return
p.dead = true
_spawn_explosion_particles(p.x, p.y, 30, Color(1, 0.6, 0.2))
func _check_big_wipe() -> void:
if not big_wipe: return # no wipe in menu mode
if big_wipe.is_active(): return
if _total_object_count() > OBJECT_WIPE_THRESHOLD:
big_wipe.start()
SoundManager.play_sfx("wipe_start")
func _on_wipe_complete(p1_ok: bool, p2_ok: bool) -> void:
if p1_ok:
wipe_count_p1 += 1
if players.size() > 0:
var p: Spaceship = players[0]
p.invuln_timer = Spaceship.INVULN_FRAMES * 2 # 3s post-wipe grace
if main_node and not menu_mode:
var bonus: float = p.stats.credit_bonus if p.stats else 1.0
main_node.add_credits(0, int(25.0 * bonus))
else:
if players.size() > 0:
_kill_player(players[0])
if is_multiplayer:
if p2_ok:
wipe_count_p2 += 1
if players.size() > 1:
var p: Spaceship = players[1]
p.invuln_timer = Spaceship.INVULN_FRAMES * 2
else:
if players.size() > 1:
_kill_player(players[1])
SoundManager.play_sfx("wipe_survived" if (p1_ok or p2_ok) else "wipe_flash")
_clear_universe_for_rebirth()
func _clear_universe_for_rebirth() -> void:
stars.clear(); comets.clear(); antimatter.clear()
quasars.clear(); white_holes.clear(); neutron_stars.clear()
antimatter_stars.clear()
black_holes = black_holes.slice(0, 1)
_spawn_universe_partial()
func _spawn_universe_partial() -> void:
while stars.size() < STAR_COUNT:
var s := CosmicObjects.Star.new()
s.init(randf()*W, randf()*H, W, H)
stars.append(s)
while planets.size() < PLANET_COUNT:
var p := CosmicObjects.Planet.new()
p.init(randf()*W, randf()*H, W, H)
planets.append(p)
func _spawn_antimatter_swarm(cx: float, cy: float) -> void:
var count: int = randi_range(3, 5)
for _i in count:
var a: float = randf() * TAU
var spd: float = randf_range(1.0, 3.0)
var am := CosmicObjects.Antimatter.new()
am.init(cx + randf_range(-10,10), cy + randf_range(-10,10), a, spd)
antimatter.append(am)
func _fire_music_event(event_name: String) -> void:
if main_node and "music_player" in main_node and main_node.music_player:
main_node.music_player.play_music_event(event_name)
func _update_bh_proximity_audio() -> void:
var mp: Node = main_node.music_player if main_node and "music_player" in main_node else null
if mp == null or not is_playing or players.size() == 0:
if mp: mp.set_bh_proximity(0.0, 1.0, false)
return
var player: Spaceship = players[0]
if player.dead:
mp.set_bh_proximity(0.0, 1.0, false)
return
var max_prox := 0.0
var max_size := 1.0
var any_smbh := false
for bhole in black_holes:
var bh: BlackHole = bhole
if bh.dead or bh.pull_radius <= 0.0: continue
var dx := player.x - bh.x
var dy := player.y - bh.y
var prox: float = clamp(1.0 - sqrt(dx*dx + dy*dy) / bh.pull_radius, 0.0, 1.0)
if prox > max_prox:
max_prox = prox
max_size = bh.radius / 14.0
any_smbh = bh.is_smbh
mp.set_bh_proximity(max_prox, max_size, any_smbh)
func _spawn_explosion_particles(cx: float, cy: float, count: int, col: Color) -> void:
for _i in count:
var a: float = randf() * TAU
var spd: float = randf_range(1.0, 6.0)
particles.append({"x": cx, "y": cy,
"vx": cos(a)*spd, "vy": sin(a)*spd,
"life": randf_range(0.3, 0.9), "max_life": 0.9, "color": col})
func _spawn_spiral_particles(cx: float, cy: float, bh_x: float, bh_y: float,
count: int, col: Color, grav: float = 4.0) -> void:
var dx: float = bh_x - cx; var dy: float = bh_y - cy
var dist: float = sqrt(dx * dx + dy * dy)
var nx: float = dx / maxf(dist, 1.0)
var ny: float = dy / maxf(dist, 1.0)
for _i in count:
var tangent_dir: float = 1.0 if randf() > 0.5 else -1.0
var tx: float = -ny * tangent_dir
var ty: float = nx * tangent_dir
var spd: float = randf_range(1.5, 4.0)
var radial_spd: float = randf_range(0.3, 1.5)
particles.append({
"x": cx + randf_range(-3.0, 3.0), "y": cy + randf_range(-3.0, 3.0),
"vx": tx * spd + nx * radial_spd, "vy": ty * spd + ny * radial_spd,
"life": randf_range(0.4, 1.2), "max_life": 1.2,
"color": col, "target_x": bh_x, "target_y": bh_y,
"gravity": grav, "stretch": true, "size": randf_range(1.0, 3.0)})
func _draw() -> void:
draw_rect(get_viewport_rect(), Color("#0a0a14"))
if antimatter_warn_pending:
var warn_t: float = antimatter_warn_timer / ANTIMATTER_WARN_DURATION
var pulse_a: float = abs(sin(antimatter_warn_timer * TAU * 3.5))
var ring_r: float = 25.0 + 8.0 * warn_t
var warn_alpha: float = 0.55 * pulse_a * (1.0 - warn_t * 0.25)
draw_arc(antimatter_warn_pos, ring_r, 0.0, TAU, 20, Color(1.0, 0.2, 0.8, warn_alpha), 2.0)
draw_arc(antimatter_warn_pos, ring_r - 5.0, 0.0, TAU, 14, Color(0.8, 0.0, 1.0, warn_alpha * 0.5), 1.0)
if Settings.nebula_enabled:
for neb in nebulae:
var n: CosmicObjects.Nebula = neb
n.draw(self)
for star in stars:
var s: CosmicObjects.Star = star
s.draw(self)
for gal in galaxies:
var g: CosmicObjects.Galaxy = gal
g.draw(self)
for planet in planets:
var p: CosmicObjects.Planet = planet
p.draw(self)
for comet in comets:
var c: CosmicObjects.Comet = comet
c.draw(self)
for qsr in quasars:
var q: CosmicObjects.Quasar = qsr
q.draw(self)
for whole in white_holes:
var wh: CosmicObjects.WhiteHole = whole
wh.draw(self)
for nstar in neutron_stars:
var ns: CosmicObjects.NeutronStar = nstar
ns.draw(self)
for bhole in black_holes:
var bh: BlackHole = bhole
bh.draw(self)
for am_obj in antimatter:
var am: CosmicObjects.Antimatter = am_obj
am.draw(self)
for as_obj in antimatter_stars:
var astar: CosmicObjects.AntimatterStar = as_obj
astar.draw(self)
for bul in bullets:
var b: Bullet = bul
b.draw(self)
for en in enemies:
var e: EnemyShip = en
e.draw(self)
# Boss nach Enemies, vor Spielern zeichnen (nur im aktiven Spiel)
if _boss != null and not _boss.dead and is_playing:
_boss.draw(self)
_draw_boss_hp_bar()
for player in players:
var p: Spaceship = player
p.draw(self, frame)
for pt in particles:
var a: float = float(pt["life"]) / float(pt["max_life"])
var c: Color = pt["color"]
var sz: float = float(pt.get("size", 4.0))
if pt.get("stretch", false) and pt.has("prev_x"):
var col := Color(c.r, c.g, c.b, a)
draw_line(Vector2(float(pt["prev_x"]), float(pt["prev_y"])),
Vector2(float(pt["x"]), float(pt["y"])),
col, maxf(1.0, sz * 0.5 * a))
else:
var hs: float = sz * 0.5
draw_rect(Rect2(float(pt["x"]) - hs, float(pt["y"]) - hs, sz, sz),
Color(c.r, c.g, c.b, a))
if big_wipe and big_wipe.is_active():
var t: float = float(frame) / 60.0
big_wipe.draw_overlay(self, W, H, t)
func _draw_boss_hp_bar() -> void:
if _boss == null or _boss.dead:
return
var bw := 300.0; var bh := 14.0
var bx := (W - bw) * 0.5; var by := H - 52.0
var name_str: String = "▸ WRAITH" if _boss.is_miniboss else "▸ LEVIATHAN"
var phase_str: String = "" if _boss.is_miniboss else (" [ PHASE %d ]" % _boss.phase)
# Schatten-Box
draw_rect(Rect2(bx - 3.0, by - 14.0, bw + 6.0, bh + 20.0), Color(0.0, 0.0, 0.0, 0.72))
# Leere Bar
draw_rect(Rect2(bx, by, bw, bh), Color(0.08, 0.0, 0.04, 0.95))
# HP-Füllung
var fill: float = (float(_boss.hp) / float(_boss.max_hp)) * bw
var hp_col: Color
if _boss.is_miniboss:
hp_col = Color(0.92, 0.08, 0.88, 1.0) # Magenta
elif _boss.phase == 2:
hp_col = Color(1.0, 0.12, 0.0, 1.0) # Feuerrot (Phase 2)
else:
hp_col = Color(1.0, 0.52, 0.0, 1.0) # Orange (Phase 1)
draw_rect(Rect2(bx, by, fill, bh), hp_col)
# Rahmen
draw_rect(Rect2(bx, by, bw, bh), Color(hp_col.r, hp_col.g, hp_col.b, 0.55), false, 1.5)
# Name + HP-Zahl
var font := ThemeDB.fallback_font
var label: String = name_str + phase_str + " %d / %d" % [_boss.hp, _boss.max_hp]
var tw: float = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x
draw_string(font, Vector2((W - tw) * 0.5, by - 2.0), label,
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(1.0, 0.92, 0.92, 0.92))