extends RefCounted class_name CosmicObjects # Planet lives in its own file now; keep CosmicObjects.Planet working. const Planet = preload("res://scripts/planet.gd") # ─── Star ───────────────────────────────────────────────────────────────────── class Star extends RefCounted: var x: float; var y: float; var speed: float; var size: float var color: Color; var alpha: float = 1.0; var glow: bool = false var twinkle_phase: float; var twinkle_speed: float; var dead: bool = false var rotation_angle: float = 0.0 var is_antimatter: bool = false var life: float = 0.0; var max_life: float = 0.0 # Gravity-induced drift (vx/vy from BH attraction, like original) var grav_vx: float = 0.0; var grav_vy: float = 0.0 # Spiral-into-BH state var is_spiraling: bool = false var spiral_timer: float = 0.0 var spiral_bh_x: float = 0.0; var spiral_bh_y: float = 0.0 var spiral_angle: float = 0.0 var spiral_dist: float = 0.0; var spiral_initial_dist: float = 0.0 var trail_points: Array = [] var original_color: Color = Color.WHITE const SPIRAL_DURATION := 0.8 const TRAIL_MAX := 16 func init(px: float, py: float, _w: float, _h: float) -> void: x = px; y = py # Three tiers (matching original): supergiant / bright / faint var tier := randf() if tier < 0.05: # supergiant size = randf_range(5.0, 7.0) * 2.4 / 4.0 # scale size alpha = 0.85 + randf() * 0.15 glow = true elif tier < 0.20: # bright main-sequence size = randf_range(3.0, 4.0) * 2.4 / 4.0 alpha = 0.65 + randf() * 0.3 glow = true else: # faint background size = 1.0 if randf() > 0.4 else 2.0 alpha = 0.2 + randf() * 0.55 glow = false speed = randf_range(0.05, 0.3) * 2.4 # original 0.05..0.3 * scale twinkle_phase = randf() * TAU twinkle_speed = randf_range(0.01, 0.03) * 60.0 # per-frame → per-second var star_colors := ["#ffffff","#ffe8a0","#a0c4ff","#ffc0c0","#c0ffc0"] color = Color(star_colors[randi() % star_colors.size()]) is_antimatter = randf() < 0.10 if is_antimatter: color = Color("#ff88ee"); max_life = randf_range(8.0, 18.0) func start_spiral(bh_x: float, bh_y: float) -> void: is_spiraling = true spiral_timer = 0.0 spiral_bh_x = bh_x; spiral_bh_y = bh_y var dx := x - bh_x; var dy := y - bh_y spiral_dist = sqrt(dx * dx + dy * dy) spiral_initial_dist = maxf(spiral_dist, 1.0) spiral_angle = atan2(dy, dx) original_color = color trail_points.clear() func update(delta: float, world_w: float, world_h: float) -> bool: if is_spiraling: spiral_timer += delta var t: float = clampf(spiral_timer / SPIRAL_DURATION, 0.0, 1.0) # Radius shrinks quadratically — tightening orbit spiral_dist = spiral_initial_dist * (1.0 - t * t) # Angular speed increases (Kepler-like feel) var angular_speed: float = 6.0 + 14.0 * t * t spiral_angle += angular_speed * delta # Position on orbit x = spiral_bh_x + cos(spiral_angle) * spiral_dist y = spiral_bh_y + sin(spiral_angle) * spiral_dist # Light trail trail_points.push_front(Vector2(x, y)) if trail_points.size() > TRAIL_MAX: trail_points.pop_back() # Blueshift — color lerps toward blue-white color = original_color.lerp(Color(0.6, 0.7, 1.0), clampf(t * 1.5, 0.0, 1.0)) # Alpha fades only in final 30% if t > 0.7: alpha = maxf(0.0, 1.0 - (t - 0.7) / 0.3) # Spaghettification — star shrinks size = maxf(0.5, size * (1.0 - delta * 2.0)) if spiral_timer >= SPIRAL_DURATION: dead = true return false rotation_angle += twinkle_speed * delta * 0.07 twinkle_phase += twinkle_speed * delta var flicker := sin(twinkle_phase) * 0.25 alpha = max(0.05, min(1.0, alpha + flicker * delta * 3.0)) # Upward drift + wavy horizontal (matching original: x += sin(y*0.02)*0.15) y -= speed x += grav_vx + sin(y * 0.0085) * 0.36 # 0.02/2.4 freq, 0.15*2.4 amplitude y += grav_vy if y < -10.0: y += world_h + 10.0 if x < -10.0: x += world_w + 10.0 elif x > world_w + 10.0: x -= world_w + 10.0 if is_antimatter: life += delta if life >= max_life: return true return false func draw(canvas: CanvasItem) -> void: # Light trail when spiraling into BH if is_spiraling and trail_points.size() > 1: for i in range(trail_points.size() - 1): var fade: float = 1.0 - float(i) / float(TRAIL_MAX) var tc := Color(color.r, color.g, color.b, alpha * fade * 0.6) canvas.draw_line(trail_points[i], trail_points[i + 1], tc, maxf(1.0, size * 0.5 * fade)) var c := color if glow: # Soft halo around bright stars var halo_r := size + 2.0 canvas.draw_rect(Rect2(x - halo_r, y - halo_r, halo_r*2.0+1.0, halo_r*2.0+1.0), Color(c.r, c.g, c.b, alpha * 0.18)) # Rotating cross flare for large stars if size >= 4.0: var fl := size + 4.0 for arm_i in 4: var arm_a := rotation_angle + float(arm_i) * PI * 0.5 for dist in range(1, int(fl) + 1): var ax := x + cos(arm_a) * dist var ay := y + sin(arm_a) * dist var fade := 1.0 - float(dist) / fl canvas.draw_rect(Rect2(ax, ay, 1, 1), Color(c.r, c.g, c.b, alpha * 0.5 * fade)) var sz: float = maxf(1.0, size) canvas.draw_rect(Rect2(x, y, sz, sz), Color(c.r, c.g, c.b, alpha)) # ─── Comet ──────────────────────────────────────────────────────────────────── class Comet extends RefCounted: var x: float; var y: float; var vx: float; var vy: float var alpha: float = 1.0; var dead: bool = false var trail_pts: Array = [] const TRAIL_MAX := 24 func init(world_w: float, world_h: float) -> void: var side := randi() % 4 match side: 0: x = randf()*world_w; y = 0; vx = randf_range(-3.0,3.0); vy = randf_range(1.0,4.0) 1: x = randf()*world_w; y = world_h; vx = randf_range(-3.0,3.0); vy = randf_range(-4.0,-1.0) 2: x = 0; y = randf()*world_h; vx = randf_range(1.0,4.0); vy = randf_range(-3.0,3.0) 3: x = world_w; y = randf()*world_h; vx = randf_range(-4.0,-1.0); vy = randf_range(-3.0,3.0) alpha = 1.0 func update(world_w: float, world_h: float) -> void: trail_pts.push_front(Vector2(x, y)) if trail_pts.size() > TRAIL_MAX: trail_pts.pop_back() x += vx; y += vy if x < -40 or x > world_w+40 or y < -40 or y > world_h+40: dead = true func draw(canvas: CanvasItem) -> void: for i in trail_pts.size(): var t := 1.0 - float(i) / float(TRAIL_MAX) var c := Color(1.0, 1.0, 1.0, t * 0.85) canvas.draw_rect(Rect2(trail_pts[i].x, trail_pts[i].y, 2, 2), c) canvas.draw_rect(Rect2(x-2, y-2, 5, 5), Color(1,1,1,alpha)) # ─── Nebula ─────────────────────────────────────────────────────────────────── class Nebula extends RefCounted: var x: float; var y: float; var radius: float; var color: Color var vx: float; var vy: float func init(world_w: float, world_h: float) -> void: x = randf() * world_w; y = randf() * world_h radius = randf_range(80.0, 160.0) var neb_colors := ["#6633cc80","#cc336680","#33669980","#66993380"] color = Color(neb_colors[randi() % neb_colors.size()]) vx = randf_range(-0.2, 0.2); vy = randf_range(-0.2, 0.2) func update(world_w: float, world_h: float) -> void: x += vx; y += vy if x < -radius: x += world_w + radius*2 elif x > world_w+radius: x -= world_w + radius*2 if y < -radius: y += world_h + radius*2 elif y > world_h+radius: y -= world_h + radius*2 func draw(canvas: CanvasItem) -> void: var ir := int(radius) for py in range(-ir, ir+1, 8): for px in range(-ir, ir+1, 8): var dist := sqrt(float(px*px + py*py)) if dist <= radius: var fade := 1.0 - dist / radius fade = floor(fade * 4.0) / 4.0 if fade <= 0.0: continue var c := Color(color.r, color.g, color.b, color.a * fade * 0.45) canvas.draw_rect(Rect2(x+px, y+py, 8, 8), c) # ─── Quasar ─────────────────────────────────────────────────────────────────── class Quasar extends RefCounted: var x: float; var y: float; var pulse: float = 0.0 var jet_angle: float; var dead: bool = false var life: float = 0.0; const MAX_LIFE := 30.0 var boost_sound_cd: float = 0.0 var radius: float = 22.0 const JET_LENGTH := 220.0 const JET_HALF_ANG := 0.25 # ~14° half-width — wide enough to feel generous const JET_FORCE := 0.55 # added velocity per frame while inside beam func init(px: float, py: float) -> void: x = px; y = py jet_angle = randf() * TAU func update(delta: float) -> void: pulse += delta * 3.0 jet_angle += delta * 0.4 life += delta if boost_sound_cd > 0.0: boost_sound_cd = max(0.0, boost_sound_cd - delta) if life >= MAX_LIFE: dead = true func push_object(ox: float, oy: float, ovx: float, ovy: float) -> Vector2: const PUSH_RADIUS := 150.0 const PUSH_STRENGTH := 0.45 var dx := ox - x; var dy := oy - y var dist := sqrt(dx*dx + dy*dy) if dist < PUSH_RADIUS and dist > 0.5: var force := PUSH_STRENGTH / (dist * 0.05 + 1.0) return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force) return Vector2(ovx, ovy) # Returns modified velocity if the position is inside one of the two jets. func boost_if_in_jet(ox: float, oy: float, ovx: float, ovy: float) -> Vector2: var dx := ox - x; var dy := oy - y var dist := sqrt(dx*dx + dy*dy) if dist > JET_LENGTH or dist < 4.0: return Vector2(ovx, ovy) var angle := atan2(dy, dx) for jd: float in [0.0, PI]: var ja := jet_angle + jd var diff := fmod(abs(angle - ja) + PI, TAU) - PI if abs(diff) < JET_HALF_ANG: var fade := 1.0 - dist / JET_LENGTH var force := JET_FORCE * (0.3 + 0.7 * fade) return Vector2(ovx + cos(ja) * force, ovy + sin(ja) * force) return Vector2(ovx, ovy) func draw(canvas: CanvasItem) -> void: var p := 0.7 + 0.3 * sin(pulse) var cv := Vector2(x, y) var R := radius # Luminosity halos — quasars outshine entire galaxies canvas.draw_circle(cv, R * 5.5, Color(1.0, 0.95, 0.4, 0.025)) canvas.draw_circle(cv, R * 3.2, Color(1.0, 0.9, 0.3, 0.06 * p)) canvas.draw_circle(cv, R * 2.0, Color(1.0, 0.95, 0.6, 0.11 * p)) # Relativistic jets — the defining quasar feature; cyan beams in opposite directions for jd: float in [0.0, PI]: var ja := jet_angle + jd for jl in range(6, 220, 4): var fade := 1.0 - float(jl) / 220.0 var jx := x + cos(ja) * float(jl) var jy := y + sin(ja) * float(jl) canvas.draw_circle(Vector2(jx, jy), maxf(0.5, fade * 3.2), Color(0.3, 0.85, 1.0, fade * p * 0.85)) # Accretion disk — much brighter and denser than SMBH (yellow/white palette) var ring_r := R + 3.0 + 3.0 * sin(pulse) for seg in 8: var sa := pulse * 0.55 + float(seg) / 8.0 * TAU canvas.draw_arc(cv, ring_r, sa, sa + TAU / 8.0 * 0.8, 6, Color(1.0, 0.92, 0.3, minf(p * 0.95, 1.0)), 3.5) for seg in 5: var sa := -pulse * 0.4 + float(seg) / 5.0 * TAU canvas.draw_arc(cv, ring_r + 10.0, sa, sa + TAU / 5.0 * 0.7, 5, Color(1.0, 1.0, 0.75, p * 0.6), 2.0) # Innermost corona ring canvas.draw_arc(cv, ring_r - 5.0, 0.0, TAU, 24, Color(1.0, 1.0, 0.9, p * 0.45), 1.5) # Black core (event horizon — same as BH/SMBH, links the visual family) canvas.draw_circle(cv, R * 0.55, Color(0.0, 0.0, 0.0, 1.0)) # Inner corona glow canvas.draw_circle(cv, R * 0.85, Color(1.0, 1.0, 0.8, 0.18 * p)) # ─── WhiteHole ──────────────────────────────────────────────────────────────── class WhiteHole extends RefCounted: var x: float; var y: float; var radius: float = 16.0 var push_strength: float = 0.9; var push_radius: float = 340.0 var alpha: float = 1.0; var dead: bool = false var pulse: float = 0.0; var life: float = 0.0 const MAX_LIFE := 60.0; const EJECT_INTERVAL := 3.0 var eject_timer: float = 0.0 func init(px: float, py: float) -> void: x = px; y = py func update(delta: float) -> Dictionary: pulse += delta * 2.5 life += delta eject_timer += delta var new_stars: Array = [] var new_planets: Array = [] if eject_timer >= EJECT_INTERVAL: eject_timer = 0.0 # Eject stars outward in all directions for _i in randi_range(4, 8): var s := Star.new() var eject_angle := randf() * TAU var eject_dist := randf_range(8.0, 20.0) s.x = x + cos(eject_angle) * eject_dist s.y = y + sin(eject_angle) * eject_dist s.speed = randf_range(0.4, 1.5) s.size = randf_range(1.0, 3.5) s.alpha = 0.7 + randf() * 0.3 s.glow = s.size >= 2.5 s.color = Color("#aaddff") s.twinkle_phase = randf() * TAU s.twinkle_speed = randf_range(0.5, 2.0) s.grav_vx = cos(eject_angle) * randf_range(0.3, 1.2) s.grav_vy = sin(eject_angle) * randf_range(0.3, 1.2) new_stars.append(s) # Occasionally eject a planet if randf() < 0.4: var p := Planet.new() p.init(x + randf_range(-25.0, 25.0), y + randf_range(-25.0, 25.0), 960.0, 600.0) new_planets.append(p) # Fade out and die after MAX_LIFE (unless a BH consumes it first) if life > MAX_LIFE: alpha = max(0.0, alpha - delta * 0.4) if alpha <= 0.05: dead = true return {"stars": new_stars, "planets": new_planets} func push_object(ox: float, oy: float, ovx: float, ovy: float) -> Vector2: var dx := ox - x; var dy := oy - y var dist := sqrt(dx*dx + dy*dy) if dist < push_radius and dist > 0.01: var force := push_strength / (dist * 0.05 + 1.0) return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force) return Vector2(ovx, ovy) func draw(canvas: CanvasItem) -> void: var p := 0.7 + 0.3 * sin(pulse) var cv := Vector2(x, y) # Outer glow canvas.draw_circle(cv, radius + 8.0, Color(0.5, 0.85, 1.0, alpha * 0.12)) # White core — single draw_circle replaces O(r²) pixel loop canvas.draw_circle(cv, radius, Color(1.0, 1.0, 1.0, alpha * p)) # Pulsing ring — draw_arc replaces 48 individual draw_rects var ring_r := radius + 6.0 + 4.0 * sin(pulse) canvas.draw_arc(cv, ring_r, 0.0, TAU, 32, Color(0.6, 0.9, 1.0, alpha * 0.8), 2.0) # ─── NeutronStar ────────────────────────────────────────────────────────────── class NeutronStar extends RefCounted: var x: float; var y: float; var beam_angle: float = 0.0 var dead: bool = false; var life: float = 0.0; const MAX_LIFE := 40.0 const BEAM_HALF_WIDTH := 0.18; const BEAM_LENGTH := 440.0 const PULL_RADIUS := 120.0; const GRAVITY := 0.6 func init(px: float, py: float) -> void: x = px; y = py; beam_angle = randf() * TAU func update(delta: float) -> void: beam_angle += delta * 1.8 life += delta if life > MAX_LIFE: dead = true func in_beam(ox: float, oy: float) -> bool: var dx := ox - x; var dy := oy - y var angle := atan2(dy, dx) var diff := fmod(abs(angle - beam_angle) + PI, TAU) - PI var diff2 := fmod(abs(angle - beam_angle - PI) + PI, TAU) - PI return abs(diff) < BEAM_HALF_WIDTH or abs(diff2) < BEAM_HALF_WIDTH func push_if_in_beam(ox: float, oy: float, ovx: float, ovy: float) -> Vector2: var dx := ox - x; var dy := oy - y var dist := sqrt(dx*dx + dy*dy) if dist < BEAM_LENGTH and in_beam(ox, oy): var force := 1.5 / (dist * 0.05 + 1.0) return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force) return Vector2(ovx, ovy) func draw(canvas: CanvasItem) -> void: for bd in [0.0, PI]: var ba: float = beam_angle + float(bd) for bl in range(0, int(BEAM_LENGTH), 4): var bx := x + cos(ba) * bl var by := y + sin(ba) * bl var ba2 := 1.0 - float(bl) / BEAM_LENGTH canvas.draw_rect(Rect2(bx, by, 2, 2), Color(0.8, 1.0, 0.8, ba2 * 0.7)) canvas.draw_rect(Rect2(x-3, y-3, 7, 7), Color(0.9, 1.0, 0.9, 1.0)) # ─── Antimatter ─────────────────────────────────────────────────────────────── class Antimatter extends RefCounted: var x: float; var y: float; var vx: float; var vy: float var dead: bool = false; var pulse: float = 0.0 func init(px: float, py: float, angle: float, speed: float) -> void: x = px; y = py vx = cos(angle) * speed; vy = sin(angle) * speed pulse = randf() * TAU func update(world_w: float, world_h: float, delta: float, nearby: Array = []) -> void: pulse += delta * 5.0 # Magnetic attraction to nearby antimatter particles const ATTRACT_RADIUS := 60.0 const ATTRACT_FORCE := 0.008 for other in nearby: if other == self: continue var dx: float = float(other.x) - x var dy: float = float(other.y) - y var d: float = sqrt(dx*dx + dy*dy) if d < ATTRACT_RADIUS and d > 1.0: vx += (dx / d) * ATTRACT_FORCE vy += (dy / d) * ATTRACT_FORCE # Clamp speed so clustering doesn't cause runaway velocity var spd := sqrt(vx*vx + vy*vy) if spd > 1.8: vx = vx / spd * 1.8 vy = vy / spd * 1.8 x += vx; y += vy if x < 0: x += world_w elif x > world_w: x -= world_w if y < 0: y += world_h elif y > world_h: y -= world_h func draw(canvas: CanvasItem) -> void: var p := 0.7 + 0.3 * sin(pulse) canvas.draw_rect(Rect2(x-3, y-3, 7, 7), Color(1.0, 0.2, 0.8, p * 0.3)) canvas.draw_rect(Rect2(x-1, y-1, 3, 3), Color(1.0, 0.2, 0.8, p)) canvas.draw_rect(Rect2(x, y, 1, 1), Color(1.0, 0.8, 1.0, p * 0.9)) # ─── Galaxy ─────────────────────────────────────────────────────────────────── class Galaxy extends RefCounted: var x: float; var y: float; var angle: float = 0.0 var rotation_speed: float = 0.3 # rad/s, increases during consumption var radius: float; var vx: float = 0.0; var vy: float = 0.0 var color: Color; var num_arms: int = 3 var star_points: Array = [] var dead: bool = false; var alpha: float = 1.0 var respawn_timer: float = 0.0; var respawning: bool = false # Gradual consumption (drifting into a BH) var being_consumed: bool = false var consume_timer: float = 0.0 const CONSUME_DURATION := 2.5 var consume_bh_x: float = 0.0 var consume_bh_y: float = 0.0 var consuming_bh = null # BlackHole ref — set by game_world var consume_initial_radius: float = 0.0 var consume_initial_alpha: float = 0.0 var original_color: Color = Color.WHITE func init(px: float, py: float) -> void: x = px; y = py vx = randf_range(-0.08, 0.08) * 2.4 # original ±0.08 * scale vy = randf_range(-0.05, 0.05) * 2.4 radius = randf_range(35.0, 65.0) * 2.4 / 2.0 # scale to 2.4x but not full angle = randf() * TAU rotation_speed = randf_range(0.001, 0.003) * 60.0 # rad/s num_arms = 2 if randf() < 0.4 else 3 var gal_colors := ["#ffeeaa","#aaddff","#ffaacc","#aaffcc","#ddaaff"] color = Color(gal_colors[randi() % gal_colors.size()]) alpha = randf_range(0.25, 0.55) _generate_stars() func _generate_stars() -> void: star_points.clear() for arm in num_arms: var arm_offset := (TAU / num_arms) * arm for i in 26: var t := float(i) / 25.0 var a := angle + arm_offset + t * 3.5 var r := 4.0 + t * (radius - 4.0) var scatter := randf_range(-6.0, 6.0) star_points.append(Vector2(cos(a)*r + scatter, sin(a)*r + scatter)) func update(delta: float, world_w: float, world_h: float) -> void: if being_consumed: consume_timer += delta var t: float = clampf(consume_timer / CONSUME_DURATION, 0.0, 1.0) # Drift toward BH with accelerating speed var target_dx: float = consume_bh_x - x var target_dy: float = consume_bh_y - y var target_dist: float = sqrt(target_dx * target_dx + target_dy * target_dy) if target_dist > 2.0: var drift_speed: float = 30.0 * t * t x += (target_dx / target_dist) * drift_speed * delta y += (target_dy / target_dist) * drift_speed * delta # Spin accelerates as it's consumed angle += rotation_speed * delta * (1.0 + t * 8.0) # Compress radius — tidal compression radius = maxf(3.0, consume_initial_radius * (1.0 - t * 0.85)) # Regenerate star points with compressed radius periodically if int(consume_timer * 5.0) > int((consume_timer - delta) * 5.0): _generate_stars() # Color warms toward orange/white (accretion heating) color = original_color.lerp(Color(1.0, 0.7, 0.3), t * 0.6) # Alpha fades in final 40% if t > 0.6: alpha = maxf(0.0, consume_initial_alpha * (1.0 - (t - 0.6) / 0.4)) if consume_timer >= CONSUME_DURATION or alpha <= 0.0: dead = true return angle += rotation_speed * delta x += vx; y += vy # Screen wrap if x < -radius: x += world_w + radius*2.0 elif x > world_w + radius: x -= world_w + radius*2.0 if y < -radius: y += world_h + radius*2.0 elif y > world_h + radius: y -= world_h + radius*2.0 if respawning: respawn_timer -= delta if respawn_timer <= 0.0: respawning = false; dead = false; alpha = randf_range(0.25, 0.55) being_consumed = false; consume_timer = 0.0; consuming_bh = null radius = randf_range(35.0, 65.0) * 2.4 / 2.0 rotation_speed = randf_range(0.001, 0.003) * 60.0 x = randf() * world_w; y = randf() * world_h angle = randf() * TAU num_arms = 2 if randf() < 0.4 else 3 _generate_stars() func draw(canvas: CanvasItem) -> void: if dead or respawning: return var cos_a := cos(angle); var sin_a := sin(angle) # Core halo canvas.draw_rect(Rect2(x - 5.0, y - 5.0, 11.0, 11.0), Color(color.r, color.g, color.b, alpha * 0.3)) # Spiral arms for pt in star_points: var rx: float = x + float(pt.x) * cos_a - float(pt.y) * sin_a var ry: float = y + float(pt.x) * sin_a + float(pt.y) * cos_a var dot_size := 2.0 if abs(float(pt.x)) < radius * 0.3 else 1.0 canvas.draw_rect(Rect2(rx, ry, dot_size, 1.0), Color(color.r, color.g, color.b, alpha * 0.75)) # Bright core canvas.draw_rect(Rect2(x - 2.0, y - 2.0, 5.0, 5.0), Color(color.r, color.g, color.b, alpha * 0.6)) canvas.draw_rect(Rect2(x - 1.0, y - 1.0, 3.0, 3.0), Color(1.0, 1.0, 1.0, alpha * 0.9)) # ─── AntimatterStar ─────────────────────────────────────────────────────────── class AntimatterStar extends RefCounted: const MAX_LIFE := 50.0 const REPEL_RADIUS := 90.0 const REPEL_FORCE := 0.055 var x: float; var y: float var radius: float = 18.0 var life: float = MAX_LIFE var pulse: float = 0.0 var dead: bool = false func init(px: float, py: float) -> void: x = px; y = py pulse = randf() * TAU func update(delta: float) -> void: life -= delta pulse += delta * 3.0 if life <= 0.0: dead = true func apply_repulsion(ox: float, oy: float, ovx: float, ovy: float) -> Vector2: var dx := ox - x; var dy := oy - y var d := sqrt(dx*dx + dy*dy) if d < REPEL_RADIUS and d > 0.01: var force := REPEL_FORCE * (1.0 - d / REPEL_RADIUS) return Vector2(ovx + (dx / d) * force, ovy + (dy / d) * force) return Vector2(ovx, ovy) func draw(canvas: CanvasItem) -> void: var p: float = 0.6 + 0.4 * sin(pulse) var alpha: float = clamp(life / MAX_LIFE, 0.0, 1.0) var cv := Vector2(x, y) # Outer magenta glow canvas.draw_circle(cv, radius + 8.0, Color(0.75, 0.0, 0.9, p * 0.05 * alpha)) canvas.draw_circle(cv, radius + 3.0, Color(0.6, 0.0, 0.85, p * 0.09 * alpha)) # Dark antimatter core canvas.draw_circle(cv, radius, Color(0.15, 0.0, 0.22, alpha)) # Rotating ring var ring_r := radius + 5.0 + 2.0 * sin(pulse) canvas.draw_arc(cv, ring_r, pulse * 0.7, pulse * 0.7 + TAU * 0.8, 14, Color(1.0, 0.2, 1.0, p * 0.85 * alpha), 2.0) canvas.draw_arc(cv, ring_r - 3.0, -pulse * 0.5, -pulse * 0.5 + TAU * 0.6, 10, Color(0.8, 0.0, 1.0, p * 0.5 * alpha), 1.5) # Bright centre dot canvas.draw_circle(cv, 3.0, Color(1.0, 0.5, 1.0, alpha * p))