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))