extends RefCounted # Accessed as CosmicObjects.Planet via preload in cosmic_objects.gd. # No class_name to avoid outer-scope shadowing inside CosmicObjects' inner classes. enum PType { TERRESTRIAL, DESERT, GAS_GIANT, ICE, LAVA, TOXIC } # Orbit / position var orbit_cx: float; var orbit_cy: float var orbit_radius: float; var orbit_angle: float; var orbit_speed: float var x: float; var y: float; var radius: float # Legacy public fields (kept for compatibility with game_world.gd / debris) var color: Color var ring: bool = false var moons: Array = [] var alpha: float = 1.0 var dead: bool = false # Capture / tidal state (unchanged) var captured: bool = false var capture_bh_x: float = 0.0; var capture_bh_y: float = 0.0 var capture_angle: float = 0.0; var capture_dist: float = 0.0 var capture_initial_dist: float = 0.0; var capture_timer: float = 0.0 var initial_radius: float = 0.0 var debris_trail: Array = [] const CAPTURE_DURATION := 1.2 const DEBRIS_MAX := 20 # New: type & palette var ptype: int = PType.TERRESTRIAL var palette: Array = [] # [base, accent, highlight] # New: surface noise & animation var surface_noise: FastNoiseLite var cloud_noise: FastNoiseLite var has_clouds: bool = false var spin_angle: float = 0.0 var spin_speed: float = 0.25 var cloud_drift: float = 0.0 var cloud_speed: float = 0.5 # Gas giant spot var has_spot: bool = false var spot_longitude: float = 0.0 # 0..TAU var spot_latitude: float = 0.0 # in -radius..+radius var spot_radius: float = 2.5 var spot_band_offset: float = 0.0 # for band index match # Rings (extended) var ring_count: int = 0 # 0 = none var ring_inner: float = 0.0 var ring_outer: float = 0.0 var ring_tilt: float = 0.25 var ring_colors: Array = [] var ring_gaps: Array = [] # indices of skipped "bands" func init(cx: float, cy: float, _world_w: float, _world_h: float) -> void: orbit_cx = cx; orbit_cy = cy orbit_radius = randf_range(19.0, 72.0) orbit_angle = randf() * TAU orbit_speed = randf_range(0.001, 0.004) * (1.0 if randf() > 0.5 else -1.0) var is_gas_giant := randf() < 0.35 radius = randf_range(12.0, 19.0) if is_gas_giant else randf_range(5.0, 10.0) initial_radius = radius # Type selection if is_gas_giant: ptype = PType.GAS_GIANT else: var r := randf() if r < 0.35: ptype = PType.TERRESTRIAL elif r < 0.65: ptype = PType.DESERT elif r < 0.80: ptype = PType.ICE elif r < 0.90: ptype = PType.LAVA else: ptype = PType.TOXIC _setup_palette() _setup_noise() _setup_animation() _setup_rings(is_gas_giant) _setup_moons(is_gas_giant) color = palette[0] # debris / legacy compatibility func _setup_palette() -> void: match ptype: PType.TERRESTRIAL: palette = [Color("#2a5aa0"), Color("#3a8a4a"), Color("#e8f0ff")] PType.DESERT: palette = [Color("#c87848"), Color("#7a3e25"), Color("#f0d8b0")] PType.GAS_GIANT: # Two palette variants — warm (Jupiter-like) or cool (Neptune-like) if randf() < 0.6: palette = [Color("#d8b078"), Color("#a06040"), Color("#f0d8a0")] else: palette = [Color("#88a8d8"), Color("#506090"), Color("#c8d8f0")] PType.ICE: palette = [Color("#b8d8f0"), Color("#5080a0"), Color("#ffffff")] PType.LAVA: palette = [Color("#2a0a00"), Color("#ff4408"), Color("#ffc040")] PType.TOXIC: palette = [Color("#4a8030"), Color("#d0d040"), Color("#204018")] func _setup_noise() -> void: surface_noise = FastNoiseLite.new() surface_noise.seed = randi() surface_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX match ptype: PType.TERRESTRIAL: surface_noise.frequency = 0.25 PType.DESERT: surface_noise.frequency = 0.22 PType.GAS_GIANT: surface_noise.frequency = 0.15 PType.ICE: surface_noise.frequency = 0.50 PType.LAVA: surface_noise.frequency = 0.35 PType.TOXIC: surface_noise.frequency = 0.18 func _setup_animation() -> void: spin_angle = randf() * TAU spin_speed = randf_range(0.15, 0.55) * (1.0 if randf() > 0.5 else -1.0) if ptype == PType.TERRESTRIAL and randf() < 0.7: has_clouds = true cloud_noise = FastNoiseLite.new() cloud_noise.seed = randi() cloud_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX cloud_noise.frequency = 0.30 cloud_drift = randf() * 100.0 cloud_speed = randf_range(0.4, 0.9) if ptype == PType.GAS_GIANT: has_spot = randf() < 0.55 if has_spot: spot_longitude = randf() * TAU spot_latitude = randf_range(-radius * 0.4, radius * 0.4) spot_radius = randf_range(2.0, 3.0) func _setup_rings(is_gas_giant: bool) -> void: var has_ring := (is_gas_giant and randf() < 0.7) or (not is_gas_giant and randf() < 0.15) if not has_ring: ring = false return ring = true ring_count = (randi() % 2) + 1 # 1..2 visible bands ring_inner = radius + randf_range(3.0, 5.0) ring_outer = ring_inner + randf_range(4.0, 8.0) ring_tilt = randf_range(0.15, 0.35) # Slight variation in ring color per band ring_colors.clear() for i in ring_count: var base: Color = palette[0] if randf() < 0.5 else palette[2] var shade: float = randf_range(0.65, 1.0) ring_colors.append(Color(base.r * shade, base.g * shade, base.b * shade)) # Random gap bands (visual holes); we use radii, so gaps are "dark" arcs ring_gaps.clear() if randf() < 0.4: ring_gaps.append(randf_range(ring_inner + 1.0, ring_outer - 1.0)) func _setup_moons(is_gas_giant: bool) -> void: var max_moons := 4 if is_gas_giant else 3 var moon_count := randi() % (max_moons + 1) for _i in moon_count: var mtype := randi() % 3 var mcolor: Color match mtype: 0: mcolor = Color(0.75, 0.75, 0.78) # rocky grey 1: mcolor = Color(0.85, 0.92, 1.0) # icy 2: mcolor = Color(0.85, 0.35, 0.25) # volcanic _: mcolor = Color(0.8, 0.8, 0.8) var msize: float = randf_range(2.0, 6.0) if is_gas_giant else randf_range(2.0, 4.0) moons.append({ "angle": randf() * TAU, "dist": radius + randf_range(8.0, 22.0), "speed": randf_range(0.03, 0.08), "size": msize, "color": mcolor, "mtype": mtype, }) func start_capture(bh_x: float, bh_y: float) -> void: captured = true capture_bh_x = bh_x; capture_bh_y = bh_y var dx := x - bh_x; var dy := y - bh_y capture_dist = sqrt(dx * dx + dy * dy) capture_initial_dist = maxf(capture_dist, 1.0) capture_angle = atan2(dy, dx) capture_timer = 0.0 func update(delta: float) -> void: # Animation spin_angle += spin_speed * delta cloud_drift += cloud_speed * delta if captured: capture_timer += delta var t: float = clampf(capture_timer / CAPTURE_DURATION, 0.0, 1.0) capture_dist = capture_initial_dist * (1.0 - t * t) var angular_speed: float = 4.0 + 10.0 * t * t capture_angle += angular_speed * delta x = capture_bh_x + cos(capture_angle) * capture_dist y = capture_bh_y + sin(capture_angle) * capture_dist radius = maxf(1.0, initial_radius * (1.0 - t * 0.8)) if int(capture_timer * 12.5) > int((capture_timer - delta) * 12.5): var da: float = randf() * TAU debris_trail.append({"x": x + cos(da) * radius, "y": y + sin(da) * radius, "life": 0.5}) if debris_trail.size() > DEBRIS_MAX: debris_trail.pop_front() for d: Dictionary in debris_trail: d["life"] = float(d["life"]) - delta debris_trail = debris_trail.filter(func(d): return float(d["life"]) > 0.0) if t > 0.6: alpha = maxf(0.0, 1.0 - (t - 0.6) / 0.4) if capture_timer >= CAPTURE_DURATION: dead = true return orbit_angle += orbit_speed x = orbit_cx + cos(orbit_angle) * orbit_radius y = orbit_cy + sin(orbit_angle) * orbit_radius * 0.4 for m: Dictionary in moons: m["angle"] = float(m["angle"]) + float(m["speed"]) # ─── Drawing ────────────────────────────────────────────────────────────────── func draw(canvas: CanvasItem) -> void: # Back half of rings (behind planet) if ring: _draw_ring_half(canvas, true) # Lava outer halo (drawn before body, under-glow) if ptype == PType.LAVA: var halo_r := int(ceil(radius + 3.0)) var pulse: float = 0.55 + sin(spin_angle * 3.0) * 0.15 var hc := Color(palette[1].r, palette[1].g, palette[1].b, alpha * 0.18 * pulse) canvas.draw_rect(Rect2(x - halo_r, y - halo_r, halo_r * 2 + 1, halo_r * 2 + 1), hc) # Planet body _draw_body(canvas) # Front half of rings (in front of planet) if ring: _draw_ring_half(canvas, false) # Moons for m: Dictionary in moons: var mx: float = x + cos(float(m["angle"])) * float(m["dist"]) var my: float = y + sin(float(m["angle"])) * float(m["dist"]) * 0.5 _draw_moon(canvas, mx, my, m) # Debris trail (tidal disruption) for d: Dictionary in debris_trail: var da: float = float(d["life"]) / 0.5 canvas.draw_rect(Rect2(float(d["x"]) - 1, float(d["y"]) - 1, 2, 2), Color(palette[0].r, palette[0].g, palette[0].b, alpha * da * 0.7)) func _draw_body(canvas: CanvasItem) -> void: var ir := int(ceil(radius)) var r2 := radius * radius for py in range(-ir, ir + 1): for px in range(-ir, ir + 1): if px * px + py * py > r2: continue var idx := _sample_pattern(px, py) var c: Color = palette[idx] # Spherical shading — light from upper-left var nx := float(px) / radius var ny := float(py) / radius var light: float = clampf(0.65 - nx * 0.35 + ny * 0.35, 0.35, 1.0) var out := Color(c.r * light, c.g * light, c.b * light, alpha) canvas.draw_rect(Rect2(x + px, y + py, 1, 1), out) # Cloud layer (terrestrial only) if has_clouds and ptype == PType.TERRESTRIAL: var cv: float = cloud_noise.get_noise_2d( float(px) + cloud_drift + spin_angle * radius * 0.6, float(py) * 1.2) if cv > 0.35: var cloud_a: float = alpha * 0.55 * clampf((cv - 0.35) / 0.25, 0.0, 1.0) * light canvas.draw_rect(Rect2(x + px, y + py, 1, 1), Color(1.0, 1.0, 1.0, cloud_a)) # Lava glow — small emissive bloom near glow pixels if ptype == PType.LAVA and idx == 2: var gc := Color(palette[2].r, palette[2].g, palette[2].b, alpha * 0.35) canvas.draw_rect(Rect2(x + px - 1, y + py, 1, 1), gc) canvas.draw_rect(Rect2(x + px + 1, y + py, 1, 1), gc) canvas.draw_rect(Rect2(x + px, y + py - 1, 1, 1), gc) canvas.draw_rect(Rect2(x + px, y + py + 1, 1, 1), gc) func _sample_pattern(px: int, py: int) -> int: var fpx: float = float(px) var fpy: float = float(py) var shifted_x: float = fpx + spin_angle * radius * 0.6 match ptype: PType.TERRESTRIAL: # Polar caps if absf(fpy) / radius > 0.78: return 2 var v: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1) return 1 if v > 0.05 else 0 PType.DESERT: if fpy / radius < -0.82: return 2 var v2: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1) return 1 if v2 > 0.15 else 0 PType.GAS_GIANT: # Distorted banding; spot overrides band if has_spot: # Spot position rotates with spin var spot_x: float = cos(spot_longitude + spin_angle) * radius * 0.6 # Only visible on front half (cos > 0) var front: float = cos(spot_longitude + spin_angle) if front > 0.0: var dx: float = fpx - spot_x var dy: float = fpy - spot_latitude if dx * dx + dy * dy < spot_radius * spot_radius: return 2 var warp: float = surface_noise.get_noise_2d(shifted_x * 0.5, fpy * 4.0) * 2.0 var band: int = int(floor((fpy + warp + radius) / 2.5)) var m: int = band % 3 if m < 0: m += 3 return m PType.ICE: var v3: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1) if v3 > 0.55: return 2 if absf(v3) < 0.08: return 1 return 0 PType.LAVA: var shift: float = sin(spin_angle * 3.0) * 0.03 var v4: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1) if v4 > 0.45 + shift: return 2 if v4 > 0.15 + shift: return 1 return 0 PType.TOXIC: var v5: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1) var v6: float = surface_noise.get_noise_2d(shifted_x + v5 * 3.0, fpy * 1.1 + v5 * 3.0) if v6 > 0.3: return 2 if v6 > 0.0: return 1 return 0 return 0 func _draw_ring_half(canvas: CanvasItem, back: bool) -> void: # Build band radii (each integer radius = one pixel band). Split by ring_count groups. var band_width: float = (ring_outer - ring_inner) / float(maxi(ring_count, 1) * 2 + 1) var a_start: float = PI var a_end: float = TAU if not back: a_start = 0.0 a_end = PI # For each band for i in ring_count: var inner: float = ring_inner + float(i * 2) * band_width var outer: float = inner + band_width var col: Color = ring_colors[i] var rc := Color(col.r, col.g, col.b, alpha * 0.55) # Sample arc points var steps: int = int(clampf(outer * 1.6, 20.0, 64.0)) var prev: Vector2 = Vector2.ZERO var have_prev: bool = false for s in range(steps + 1): var a: float = a_start + (a_end - a_start) * float(s) / float(steps) # For each radius in [inner, outer] var rad: float = (inner + outer) * 0.5 if _ring_gap_hit(rad): have_prev = false continue var px: float = x + cos(a) * rad var py: float = y + sin(a) * rad * ring_tilt if have_prev: canvas.draw_line(prev, Vector2(px, py), rc, maxf(1.0, band_width)) prev = Vector2(px, py) have_prev = true func _ring_gap_hit(rad: float) -> bool: for g in ring_gaps: if absf(rad - float(g)) < 0.6: return true return false func _draw_moon(canvas: CanvasItem, mx: float, my: float, m: Dictionary) -> void: var mc: Color = m["color"] var ms: float = float(m["size"]) var a_out := Color(mc.r, mc.g, mc.b, alpha) # Solid body canvas.draw_rect(Rect2(mx - ms * 0.5, my - ms * 0.5, ms, ms), a_out) # Type-specific detail var mtype: int = int(m["mtype"]) match mtype: 0: # Rocky — darker crater pixel var dark := Color(mc.r * 0.55, mc.g * 0.55, mc.b * 0.55, alpha) canvas.draw_rect(Rect2(mx - ms * 0.5 + 1, my - ms * 0.5, 1, 1), dark) if ms >= 4.0: canvas.draw_rect(Rect2(mx + ms * 0.5 - 2, my + ms * 0.5 - 2, 1, 1), dark) 1: # Icy — bright highlight var hi := Color(1.0, 1.0, 1.0, alpha) canvas.draw_rect(Rect2(mx - ms * 0.5, my - ms * 0.5, 1, 1), hi) 2: # Volcanic — red/black speckle var hot := Color(1.0, 0.6, 0.1, alpha) var black := Color(0.1, 0.05, 0.05, alpha) canvas.draw_rect(Rect2(mx - ms * 0.5 + 1, my - ms * 0.5 + 1, 1, 1), hot) canvas.draw_rect(Rect2(mx + ms * 0.5 - 1, my - ms * 0.5, 1, 1), black)