edc40f9008
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay) - ItemDB: static preload list instead of DirAccess scan (export fix) - All 18 items with EN localisation (name_en, desc_en, category_en) - Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank - Quasar: SMBH visual, jet boost, merge, push, BH-eating - Atlas & UI text updated EN+DE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
591 lines
24 KiB
GDScript
591 lines
24 KiB
GDScript
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.10 * alpha))
|
|
canvas.draw_circle(cv, radius + 3.0, Color(0.6, 0.0, 0.85, p * 0.18 * 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))
|