WIP Almost working reworked N-body simulation

This commit is contained in:
2025-10-16 11:56:35 +02:00
parent c61fa2b917
commit c14b07d24f
23 changed files with 554 additions and 503 deletions

View File

@ -1,24 +1,6 @@
[gd_scene load_steps=8 format=3 uid="uid://dogqi2c58qdc0"]
[gd_scene load_steps=2 format=3 uid="uid://dogqi2c58qdc0"]
[ext_resource type="Script" uid="uid://j3j483itissq" path="res://scripts/star_system_generator.gd" id="1_h2yge"]
[ext_resource type="PackedScene" uid="uid://5uqp4amjj7ww" path="res://scenes/celestial_bodies/star.tscn" id="2_7mycd"]
[ext_resource type="PackedScene" uid="uid://clt4qlsjcfgln" path="res://scenes/celestial_bodies/planet.tscn" id="3_272bh"]
[ext_resource type="PackedScene" uid="uid://74ppvxcw8an4" path="res://scenes/celestial_bodies/moon.tscn" id="4_5vw27"]
[ext_resource type="PackedScene" uid="uid://dm3s33o4xhqfv" path="res://scenes/celestial_bodies/station.tscn" id="5_kek77"]
[ext_resource type="PackedScene" uid="uid://bawsujtlpmh5r" path="res://scenes/celestial_bodies/asteroid.tscn" id="6_4c57u"]
[ext_resource type="PackedScene" uid="uid://didt2nsdtbmra" path="res://modules/Tube.tscn" id="7_272bh"]
[ext_resource type="Script" uid="uid://bkcouefvi7iup" path="res://scripts/star_system.gd" id="1_ig7tw"]
[node name="StarSystem" type="Node2D"]
script = ExtResource("1_h2yge")
min_planets = 1
max_planets = 4
max_moons = 10
min_asteroid_belts = 0
max_asteroid_belts = 0
max_star_stations = 0
star_scene = ExtResource("2_7mycd")
planet_scene = ExtResource("3_272bh")
moon_scene = ExtResource("4_5vw27")
station_scene = ExtResource("5_kek77")
asteroid_scene = ExtResource("6_4c57u")
spaceship_scene = ExtResource("7_272bh")
script = ExtResource("1_ig7tw")

View File

@ -20,108 +20,75 @@
[node name="Module" type="Node2D"]
script = ExtResource("1_nqe0s")
physics_mode = 1
mass = 13.0
inertia = 1200.92
mass = 2.0
inertia = 0.5
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="Hullplate" parent="." instance=ExtResource("2_foqop")]
is_pressurized = false
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30634" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, 100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30635" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, -100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="Bulkhead" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30636" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 0)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30637" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, -100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30638" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, -100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30639" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, -150)
rotation = 1.5708
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30640" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, 150)
rotation = 4.71239
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30641" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 100)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="@StaticBody2D@30642" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 0)
is_pressurized = false
health = 0.0
physics_mode = null
base_mass = 0.0
linear_velocity = 1.0
inertia = 0.0
[node name="Station" parent="." instance=ExtResource("5_nqe0s")]
position = Vector2(0, -10)

View File

@ -0,0 +1,26 @@
# res://scenes/orrey_view/debug_marker.gd
class_name DebugMarker
extends Control
signal marker_selected(body: Node2D)
@onready var icon: ColorRect = $Icon
@onready var label: Label = $Label
var body_reference: Node2D
func initialize(body: Node2D):
body_reference = body
label.text = body.name
# Color-code the marker for easy identification
if body is Barycenter:
icon.color = Color.YELLOW
elif body.get_parent() is Barycenter and body.get_parent().name == "StarBarycenter":
icon.color = Color.ORANGE
else:
icon.color = Color.CYAN
func _gui_input(event: InputEvent):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
emit_signal("marker_selected", body_reference)

View File

@ -0,0 +1 @@
uid://b5tcvtjh2050f

View File

@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://b8fmmp4axba1j"]
[ext_resource type="Script" uid="uid://b5tcvtjh2050f" path="res://scenes/orrey_view/debug_marker.gd" id="1_2uxs6"]
[node name="DebugMarker" type="Control"]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_2uxs6")
[node name="Icon" type="ColorRect" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="Label" type="Label" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 23.0

View File

@ -0,0 +1,74 @@
# res://scenes/debug/orrery_view.gd
extends Node2D
@export_group("Generation Assets")
@export var star_scene: PackedScene
@export var planet_scene: PackedScene
@export var moon_scene: PackedScene
@export var barycenter_scene: PackedScene
@export var spaceship_scene: PackedScene # Add this even if unused for now
@export_group("Orrery Settings")
@export var debug_marker_scene: PackedScene # Link your new DebugMarker.tscn here
@onready var camera: Camera2D = $Camera2D
@onready var system_root: Node2D = $GeneratedSystem
@onready var info_label: RichTextLabel = $UI/InfoPanel/MarginContainer/InfoLabel
var markers: Dictionary = {} # Maps a body to its marker
func _ready():
# 1. Create and configure the generator tool.
var generator = StarSystemGenerator.new()
# Create a temporary StarSystem-like object to pass to the generator
var temp_system_node = Node2D.new()
temp_system_node.star_scene = star_scene
temp_system_node.planet_scene = planet_scene
temp_system_node.moon_scene = moon_scene
temp_system_node.barycenter_scene = barycenter_scene
temp_system_node.spaceship_scene = spaceship_scene
# 2. Generate the system under our dedicated root node.
var generation_result = generator.generate(temp_system_node)
system_root.add_child(temp_system_node) # Keep the generated hierarchy
# 3. Create a debug marker for every generated body.
var all_bodies = generation_result.all_bodies + generation_result.all_barycenters
for body in all_bodies:
if is_instance_valid(body):
var marker = debug_marker_scene.instantiate()
add_child(marker) # Add markers to the OrreryView, not the generated system
marker.initialize(body)
marker.marker_selected.connect(_on_marker_selected)
markers[body] = marker
func _process(_delta):
# Keep the markers synced with their body's global position.
for body in markers:
var marker = markers[body]
marker.global_position = body.global_position
func _unhandled_input(event: InputEvent):
# Simple camera controls
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
camera.offset -= event.relative / camera.zoom.x
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
camera.zoom *= 1.2
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.zoom /= 1.2
func _on_marker_selected(body: Node2D):
# Update the info panel with the selected body's data.
var text = "[b]%s[/b]\n" % body.name
if body is OrbitalBody2D:
text += "Mass: %.2f\n" % body.mass
text += "Velocity: (%.2f, %.2f)\n" % [body.linear_velocity.x, body.linear_velocity.y]
text += "Position: (%.0f, %.0f)\n" % [body.global_position.x, body.global_position.y]
if body is Barycenter and is_instance_valid(body.get_parent()):
text += "Parent: %s" % body.get_parent().name
info_label.text = text

View File

@ -0,0 +1 @@
uid://cwurbqorbwtil

View File

@ -0,0 +1,22 @@
[gd_scene load_steps=2 format=3 uid="uid://bysrpulqqlic3"]
[ext_resource type="Script" uid="uid://cwurbqorbwtil" path="res://scenes/orrey_view/orrey_view.gd" id="1_4smux"]
[node name="OrreyView" type="Node2D"]
script = ExtResource("1_4smux")
[node name="GeneratedSystem" type="Node2D" parent="."]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="UI" type="CanvasLayer" parent="."]
[node name="InfoPanel" type="PanelContainer" parent="UI"]
offset_right = 40.0
offset_bottom = 40.0
[node name="MarginContainer" type="MarginContainer" parent="UI/InfoPanel"]
layout_mode = 2
[node name="InfoLabel" type="RichTextLabel" parent="UI/InfoPanel/MarginContainer"]
layout_mode = 2

View File

@ -6,9 +6,7 @@
[node name="Station" type="Node2D"]
script = ExtResource("1_8usqu")
physics_mode = null
mass = 1.0
inertia = 0.0
[node name="InteractionArea" type="Area2D" parent="."]

View File

@ -58,31 +58,32 @@ func _draw() -> void:
var map_center = get_rect().size / 2.0
var system_data = GameManager.get_system_data()
if system_data and system_data.belts:
for belt in system_data.belts:
var radius = belt.centered_radius * map_scale
draw_circle(map_center + map_offset, radius, Color(Color.WHITE, 0.1), false)
#if system_data and system_data.belts:
#for belt in system_data.belts:
#var radius = belt.centered_radius * map_scale
#draw_circle(map_center + map_offset, radius, Color(Color.WHITE, 0.1), false)
for body in icon_map:
if body is Asteroid: continue
var icon = icon_map[body]
if not icon.visible: continue
var path_points = []
if body is CelestialBody: path_points = OrbitalMechanics._calculate_relative_orbital_path(body)
elif body is OrbitalBody2D: path_points = OrbitalMechanics._calculate_n_body_orbital_path(body)
else: continue
var scaled_path_points = PackedVector2Array()
for point in path_points:
# Ensure path is drawn relative to the main focal body (the star)
var path_world_pos = point + focal_body.global_position
var relative_pos = path_world_pos - focal_body.global_position
var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
scaled_path_points.append(scaled_pos)
if scaled_path_points.size() > 1:
draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
# TODO: Turn this into drawing of trajectories for bodies that are selected
#for body in icon_map:
#if body is Asteroid: continue
#var icon = icon_map[body]
#if not icon.visible: continue
#
#var path_points = []
#if body is CelestialBody: path_points = OrbitalMechanics._calculate_relative_orbital_path(body)
#elif body is OrbitalBody2D: path_points = OrbitalMechanics._calculate_n_body_orbital_path(body)
#else: continue
#var scaled_path_points = PackedVector2Array()
#
#for point in path_points:
## Ensure path is drawn relative to the main focal body (the star)
#var path_world_pos = point + focal_body.global_position
#var relative_pos = path_world_pos - focal_body.global_position
#var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
#scaled_path_points.append(scaled_pos)
#
#if scaled_path_points.size() > 1:
#draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
func _populate_map():
for child in get_children():

View File

@ -6,15 +6,12 @@
[node name="SensorPanel" type="Control"]
clip_contents = true
layout_mode = 3
anchors_preset = 8
anchors_preset = 13
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -350.0
offset_top = -180.0
offset_right = 350.0
offset_bottom = 180.0
anchor_bottom = 1.0
offset_left = -300.0
offset_right = 300.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_5yxry")

46
scripts/barycenter.gd Normal file
View File

@ -0,0 +1,46 @@
# scripts/barycenter.gd
class_name Barycenter
extends OrbitalBody2D
var primary_body: OrbitalBody2D
var child_systems: Array[Barycenter] = []
func _ready():
physics_mode = PhysicsMode.INDEPENDENT
await get_tree().process_frame
# Find children and calculate total mass (this logic remains the same)
for child in get_children():
if child is Barycenter:
child_systems.append(child)
elif primary_body == null and child is OrbitalBody2D:
primary_body = child
recalculate_total_mass()
# We no longer run a local simulation here.
# We only need physics_process to integrate our own movement.
set_physics_process(true)
func get_internal_attractors() -> Array[OrbitalBody2D]:
var internal_attractors: Array[OrbitalBody2D] = []
for child in get_children():
if child is OrbitalBody2D:
internal_attractors.append(child)
return internal_attractors
func recalculate_total_mass():
base_mass = 0.0
if is_instance_valid(primary_body):
base_mass += primary_body.base_mass
for system in child_systems:
system.recalculate_total_mass()
base_mass += system.base_mass
mass = base_mass
# --- API for accessing contents ---
func get_primary_body() -> OrbitalBody2D:
return primary_body
func get_child_systems() -> Array[Barycenter]:
return child_systems

View File

@ -0,0 +1 @@
uid://b2hb3bwrlh40c

View File

@ -30,23 +30,25 @@ func _ready():
recalculate_physical_properties()
set_physics_process(not Engine.is_editor_hint())
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
# --- PUBLIC FORCE APPLICATION METHODS ---
# This method is called by a component (like Thruster) at its global position.
func apply_force(force: Vector2, position: Vector2 = Vector2.ZERO):
func apply_force(force: Vector2, position: Vector2 = self.global_position):
# This is the force routing logic.
match physics_mode:
PhysicsMode.INDEPENDENT:
_add_forces(force, position)
PhysicsMode.COMPOSITE:
# If we are the root, accumulate the force and calculate torque on the total body.
accumulated_force += force
# Calculate torque (2D cross product: T = r x F = r.x * F.y - r.y * F.x)
# 'r' is the vector from the center of mass (global_position) to the point of force application (position).
var r = position - global_position
var torque = r.x * force.y - r.y * force.x
accumulated_torque += torque
_add_forces(force, position)
## If we are the root, accumulate the force and calculate torque on the total body.
#accumulated_force += force
#
## Calculate torque (2D cross product: T = r x F = r.x * F.y - r.y * F.x)
## 'r' is the vector from the center of mass (global_position) to the point of force application (position).
#var r = position - global_position
#var torque = r.x * force.y - r.y * force.x
#accumulated_torque += torque
PhysicsMode.ANCHORED:
# If we are not the root, we must route the force to the next OrbitalBody2D parent.
var p = get_parent()
@ -100,19 +102,17 @@ func _integrate_forces(delta):
accumulated_torque = 0.0
return
# 1. Calculate and accumulate gravitational force (F_g)
var total_gravity_force = OrbitalMechanics.calculate_n_body_gravity_forces(self)
# 2. Total all forces: F_total = F_g + F_accumulated_from_thrusters
var total_force = total_gravity_force + accumulated_force
## 1. Calculate and accumulate gravitational force (F_g)
#var total_gravity_force = OrbitalMechanics.calculate_n_body_gravity_forces(self)
#
## 2. Total all forces: F_total = F_g + F_accumulated_from_thrusters
#var total_force = total_gravity_force +
# 3. Apply Linear Physics (F = ma)
var linear_acceleration = total_force / sim_mass # Division is now safe
var linear_acceleration = accumulated_force / sim_mass # Division is now safe
linear_velocity += linear_acceleration * delta
global_position += linear_velocity * delta
#if self is Planet:
#print(global_position)
# 4. Apply Rotational Physics (T = I * angular_acceleration)
var angular_acceleration = accumulated_torque / inertia
angular_velocity += angular_acceleration * delta

View File

@ -1,6 +1,7 @@
[gd_resource type="Resource" script_class="GameConfig" load_steps=4 format=3 uid="uid://cv15sck8rl2b7"]
[gd_resource type="Resource" script_class="GameConfig" load_steps=5 format=3 uid="uid://cv15sck8rl2b7"]
[ext_resource type="PackedScene" uid="uid://chgycmkkaf7jv" path="res://scenes/characters/pilot_ball.tscn" id="1_s0mxw"]
[ext_resource type="PackedScene" uid="uid://didt2nsdtbmra" path="res://modules/Tube.tscn" id="2_75b4c"]
[ext_resource type="PackedScene" uid="uid://dnre6svquwdtb" path="res://scenes/characters/player_controller.tscn" id="2_sk8k5"]
[ext_resource type="Script" uid="uid://bfc6u1f8sigxj" path="res://scripts/singletons/game_config.gd" id="3_75b4c"]
@ -8,4 +9,5 @@
script = ExtResource("3_75b4c")
player_controller_scene = ExtResource("2_sk8k5")
default_pawn_scene = ExtResource("1_s0mxw")
default_ship_scene = ExtResource("2_75b4c")
metadata/_custom_type_script = "uid://bfc6u1f8sigxj"

View File

@ -4,4 +4,5 @@ extends Resource
@export var player_controller_scene: PackedScene
@export var default_pawn_scene: PackedScene
@export var default_ship_scene: PackedScene
# Add any other global settings you need here in the future

View File

@ -8,7 +8,7 @@ var player_controllers: Dictionary = {} # Key: player_id, Value: PlayerControlle
var player_pawns: Dictionary = {} # Key: player_id, Value: PilotBall node
# This variable will hold the reference to the currently active star system.
var current_star_system : StarSystemGenerator = null
var current_star_system : StarSystem = null
var ships_in_system : Array[OrbitalBody2D]
var registered_spawners: Array[Spawner] = []
@ -123,16 +123,11 @@ func register_spawner(spawner_node: Spawner):
_try_spawn_waiting_player()
# A helper function for easily accessing the system's data.
func get_system_data():
func get_system_data() -> SystemData:
if current_star_system:
return current_star_system.get_system_data()
return null
func get_system_bodies() -> Array[CelestialBody]:
if current_star_system:
return current_star_system.get_all_bodies()
return []
func get_all_trackable_bodies() -> Array:
var all_bodies: Array = []

View File

@ -4,7 +4,103 @@ extends Node
# This singleton holds all universal physics constants and functions.
# The scaled gravitational constant for the entire simulation.
const G = 10.0 # Adjust this to control the "speed" of your simulation
const G = 1.0 # Adjust this to control the "speed" of your simulation
const MIN_INFLUENCE_THRESHOLD = 0.00001
const ROCHE_LIMIT_MASS_MULTIPLIER = 0.5
class BodyTuple:
var body_a: OrbitalBody2D
var body_b: OrbitalBody2D
var cached_forces: Dictionary[BodyTuple, Vector2] = {
}
# --- Centralized Physics Process ---
func _physics_process(_delta: float) -> void:
var star_system: StarSystem = GameManager.current_star_system
if not star_system:
return
var star = star_system.get_star()
var planetary_systems = star_system.get_planetary_systems()
# TODO: Would this be true in case we are working with a system that is just a rouge planet or a brown dwarf?
if not star:
return
# 1: Calculate star system pull
# a: Get the star and top level Barycenters
var top_level_bodies: Array[OrbitalBody2D] = [star]
top_level_bodies.append_array(planetary_systems)
# b: calculate and apply pull between these
apply_n_body_forces(top_level_bodies)
# 2: Calculate Barycenters local pull
for system in planetary_systems:
# a: Get each Planetary Barycenters OrbitalBody2Ds (including Ships, Satelites, and Stations fully within the Barycenter)
var system_attractors = system.get_internal_attractors()
# b: Calculate and apply pull within each Barycenter
apply_n_body_forces(system_attractors)
## 3: Calculate top level Ships, Satelites, and Stations pull
## a: Get top level OrbitalBody2Ds of non-celestial classes
#for star_orbiter in star_system.get_orbital_bodies():
## b: Split into Star Orbiting and On-Approach using mass/distance ratios to Barycenters
## TODO: Check for distance to Barycenter
## c: For Star Orbiting objects -> Calculate and apply pull to star and Barycenter
#star_orbiter.apply_force(calculate_n_body_force(star_orbiter, top_level_bodies))
## d: For On Approach -> Calculate and apply pull to star and distant Barycenters
## as well as individual bodies within approaching Barycenter
func calculate_gravitational_force(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
if not is_instance_valid(orbiter) or not is_instance_valid(primary):
return Vector2.ZERO
var distance_sq = orbiter.global_position.distance_squared_to(primary.global_position)
if distance_sq < 1.0:
return Vector2.ZERO
# --- Influence Pruning (Culling) ---
# We check both directions of influence
var influence_a = primary.mass / distance_sq
var influence_b = orbiter.mass / distance_sq
if influence_a < MIN_INFLUENCE_THRESHOLD and influence_b < MIN_INFLUENCE_THRESHOLD:
return Vector2.ZERO
var force_magnitude = (G * primary.mass * orbiter.mass) / distance_sq
var direction = orbiter.global_position.direction_to(primary.global_position)
return direction * force_magnitude
# Calculates the pull between a set number of bodies
# Use carefully to simulate each level of the simulation
# Iterate through every unique pair of bodies (i, j) where j > i
func apply_n_body_forces(attractors: Array[OrbitalBody2D]):
# Iterate through every unique pair of bodies (i, j) where j > i
for i in range(attractors.size()):
var body_a: OrbitalBody2D = attractors[i]
if not is_instance_valid(body_a): continue
for j in range(i + 1, attractors.size()):
var body_b: OrbitalBody2D = attractors[j]
if not is_instance_valid(body_b): continue
# Calculate the force vector ONCE
var force_vector = calculate_gravitational_force(body_a, body_b)
# Apply the force symmetrically
if force_vector != Vector2.ZERO:
body_a.apply_force(force_vector)
body_b.apply_force(-force_vector)
func calculate_n_body_force(body: OrbitalBody2D, attractors: Array[OrbitalBody2D]) -> Vector2:
var total_pull: Vector2 = Vector2.ZERO
for attractor in attractors:
total_pull += calculate_gravitational_force(body, attractor)
return total_pull
func calculate_n_body_gravity_forces(body_to_affect: Node2D) -> Vector2:
var total_force = Vector2.ZERO
@ -25,32 +121,6 @@ func calculate_n_body_gravity_forces(body_to_affect: Node2D) -> Vector2:
return total_force
# Calculates the force of gravity exerted by a primary on an orbiter.
func calculate_gravitational_force(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
if not is_instance_valid(orbiter) or not is_instance_valid(primary):
return Vector2.ZERO
var direction = orbiter.global_position.direction_to(primary.global_position)
var distance_sq = orbiter.global_position.distance_squared_to(primary.global_position)
if distance_sq < 1.0: # Avoid division by zero
return Vector2.ZERO
var force_magnitude = (G * primary.mass * orbiter.mass) / distance_sq
return direction * force_magnitude
# Simplified n-body gravitational pull on orbiters.
# Station orbiting a moon will for instance calculate forces to the moon, the planet, and the star.
#func simple_n_body_grav(primary: OrbitalBody2D, orbiter : OrbitalBody2D) -> Vector2:
#var pull = calculate_gravitational_force(primary, orbiter)
#var inner_primary : CelestialBody = primary
#
#while (inner_primary.primary is CelestialBody):
#pull = pull + calculate_gravitational_force(inner_primary.primary, orbiter)
#inner_primary = inner_primary.primary
#
#return pull
# Calculates the perfect initial velocity for a stable circular orbit.
func calculate_circular_orbit_velocity(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
if not is_instance_valid(primary):
@ -65,9 +135,8 @@ func calculate_circular_orbit_velocity(orbiter: OrbitalBody2D, primary: OrbitalB
var direction_to_orbiter = primary.global_position.direction_to(orbiter.global_position)
var perpendicular_direction = Vector2(direction_to_orbiter.y, -direction_to_orbiter.x)
# The final velocity must include the primary's own velocity.
return (perpendicular_direction * speed_magnitude) + primary.linear_velocity
return perpendicular_direction * speed_magnitude
func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
var num_steps = 10
@ -164,3 +233,25 @@ func _calculate_relative_orbital_path(body_to_trace: CelestialBody) -> PackedVec
path_points.append(ghost_relative_pos)
return path_points
# Calculates the Hill Sphere radius for a satellite.
# This is the region where the satellite's gravity is dominant over its primary's.
func calculate_hill_sphere(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> float:
if not is_instance_valid(orbiter) or not is_instance_valid(primary) or primary.mass <= 0:
return 0.0
var distance = orbiter.global_position.distance_to(primary.global_position)
# The formula is: a * (m / 3M)^(1/3)
var mass_ratio = orbiter.mass / (3.0 * primary.mass)
if mass_ratio < 0: return 0.0
return distance * pow(mass_ratio, 1.0/3.0)
# Calculates a simplified Roche Limit, or minimum safe orbital distance.
func calculate_simplified_roche_limit(primary: OrbitalBody2D) -> float:
if not is_instance_valid(primary) or primary.mass <= 0:
return 100.0 # Return a small default if primary is invalid
# We approximate a "radius" from the square root of the mass, then apply a multiplier.
# This ensures more massive stars and planets have larger "keep-out" zones.
return sqrt(primary.mass) * ROCHE_LIMIT_MASS_MULTIPLIER

51
scripts/star_system.gd Normal file
View File

@ -0,0 +1,51 @@
# scripts/star_system.gd
class_name StarSystem
extends Node2D
@export_group("System Metadata")
@export var system_name: String = "Kepler-186"
@export var galactic_coordinates: Vector2i = Vector2i.ZERO
var system_data: SystemData
func _ready():
# 1. Create the generator tool.
var generator = StarSystemGenerator.new()
# 2. Tell the generator to build the system within this StarSystem node.
system_data = generator.generate(self)
# 3. Register the completed system with the GameManager.
GameManager.register_star_system(self)
GameManager.start_game()
# --- Public API for accessing system data ---
func get_star() -> OrbitalBody2D:
if is_instance_valid(system_data):
return system_data.star
return null
func get_planetary_systems() -> Array[Barycenter]:
var bodies: Array[Barycenter] = []
for child in get_children():
if child is Barycenter:
bodies.append(child)
return bodies
func get_orbital_bodies() -> Array[OrbitalBody2D]:
var bodies: Array[OrbitalBody2D] = []
for child in get_children():
if child is OrbitalBody2D:
bodies.append(child)
return bodies
func get_system_data() -> SystemData:
return system_data
class AsteroidBelt:
var width : float
var mass : float
var centered_radius : float = 0.0
var asteroids : Array[OrbitalBody2D]

View File

@ -0,0 +1 @@
uid://bkcouefvi7iup

View File

@ -1,377 +1,130 @@
# scripts/star_system_generator.gd
class_name StarSystemGenerator
extends Node2D
extends RefCounted
@export_group("System Generation")
# Minimum and maximum number of planets to generate.
@export var min_planets: int = 2
@export var max_planets: int = 7
# --- Stable Mass Ratios & Generation Rules ---
const STAR_MASS = 50000000.0
const PLANET_MASS = STAR_MASS / 10000.0 # Planet is 10,000x less massive than the star.
const MOON_MASS = PLANET_MASS / 1000.0 # Moon is 1,000x less massive than its planet.
const MIN_PLANETS = 3
const MAX_PLANETS = 8
const MAX_MOONS_PER_PLANET = 5
const ORBIT_SAFETY_FACTOR = 2.5 # Increase space between orbits
# Minimum and maximum number of moons to generate per planet.
@export var min_moons: int = 0
@export var max_moons: int = 4
# --- The main public method ---
func generate(star_system: StarSystem) -> SystemData:
# 1. Create the root Barycenter for the entire system.
var system_data = SystemData.new()
# Minimum and maximum number of asteroid belts to generate.
@export var min_asteroid_belts: int = 1
@export var max_asteroid_belts: int = 3
# 2. Create the star itself inside the root Barycenter.
var star = Star.new()
system_data.star = star
star.name = "Star"
star.base_mass = STAR_MASS
star_system.add_child(star)
# Minimum and maximum number of asteroids per belt.
@export var min_asteroids_per_belt: int = 20
@export var max_asteroids_per_belt: int = 100
# Minimum and maximum number of star-orbiting stations.
@export var min_star_stations: int = 0
@export var max_star_stations: int = 2
@export var min_planetary_stations : int = 0
@export var max_planetary_stations: int = 2
@export var min_moon_stations : int = 0
@export var max_moon_stations: int = 1
# References to the PackedScene resources for each celestial body.
@export var star_scene: PackedScene
@export var planet_scene: PackedScene
@export var moon_scene: PackedScene
@export var station_scene: PackedScene
@export var asteroid_scene: PackedScene
@export var spaceship_scene: PackedScene
# A factor to scale the initial orbital distances to make the system visually compelling.
@export var min_ring_distance: float = 10000
@export var min_planetary_orbit_radius : float = 200
# A factor to determine the buffer between orbits based on mass.
@export var orbit_buffer_factor: float = 1
const orbit_buffer_constant : float = 1000
var orbit_buffer : float = orbit_buffer_constant * orbit_buffer_factor
# A flag to check if the system has been generated.
var has_generated: bool = false
# Constants for real-world physics calculations.
#const G = 6.67430e-11 # Gravitational constant (N·m²/kg²)
#const SUN_MASS = 1.989e30 # Mass of the sun (kg)
const SUN_MASS: float = 10000000.0
const PLANETARY_MASS: float = SUN_MASS / 300000.0
const MOON_MASS: float = PLANETARY_MASS / 100.0
const ASTEROID_MASS: float = MOON_MASS / 5.0
const STATION_MASS: float = MOON_MASS / 1000.0
var _bodies_to_initialize: Array[Dictionary] = []
var system_data : SystemData
func _ready() -> void:
if not has_generated:
system_data = SystemData.new()
generate_star_system() # This now only creates the bodies
# --- MODIFIED: Defer the velocity calculations ---
# Wait until the next idle frame, ensuring all _ready functions have run
# and all global_positions have been calculated.
_initialize_orbits() # Now, calculate all velocities
has_generated = true
GameManager.start_game()
func generate_star_system() -> void:
# This function is now simpler.
var star_instance = _create_star()
add_child(star_instance)
_generate_and_place_bodies(star_instance)
# This new function will perform all the velocity calculations at once.
func _initialize_orbits():
for body_data in _bodies_to_initialize:
var orbiter: OrbitalBody2D = body_data.orbiter
var primary: OrbitalBody2D = body_data.primary
if is_instance_valid(orbiter) and is_instance_valid(primary):
orbiter.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(orbiter, primary)
print("Initialized orbit for: " + orbiter.name)
else:
push_warning("Could not initialize orbit due to invalid node.")
# 3. Procedurally generate and place the planetary systems.
var num_planets = randi_range(MIN_PLANETS, MAX_PLANETS)
var current_orbit_radius = 15000.0 # OrbitalMechanics.calculate_simplified_roche_limit(star) # Start with the first orbit
print("Star system orbit initialization complete.")
# Creates a single star instance.
func _create_star() -> OrbitalBody2D:
var star_instance = star_scene.instantiate() as Star
system_data.star = star_instance
star_instance.name = "Star"
# Set the star's properties.
#star_instance.orbit_radius_real = 0.0
star_instance.base_mass = SUN_MASS
star_instance.modulate = Color("ffe066") # Yellow
return star_instance
# Generates and places all bodies in randomized order.
func _generate_and_place_bodies(primary: OrbitalBody2D) -> void:
# 1. Randomize the number of each type of body.
var num_planets = randi_range(min_planets, max_planets)
var num_belts = randi_range(min_asteroid_belts, max_asteroid_belts)
var num_star_stations = randi_range(min_star_stations, max_star_stations)
# 2. Create an "inventory" of bodies to be placed.
var bodies_to_place = []
bodies_to_place.append({"type": "wormhole_exit", "num": 0})
for i in range(num_planets):
bodies_to_place.append({"type": "planet"})
for i in range(num_belts):
bodies_to_place.append({"type": "asteroid_belt"})
for i in range(num_star_stations):
bodies_to_place.append({"type": "station"})
# A. Create the Barycenter for the new planetary system.
var planet_barycenter = Barycenter.new()
# 3. Randomize the order of placement.
bodies_to_place.shuffle()
var current_orbit_radius = min_ring_distance
# 4. Place bodies sequentially based on the randomized order.
for body_data in bodies_to_place:
var body_type = body_data["type"]
planet_barycenter.name = "PlanetSystem_%d" % (i + 1)
star_system.add_child(planet_barycenter)
match body_type:
"planet":
var planet_instance = planet_scene.instantiate() as OrbitalBody2D
system_data.planets.append(planet_instance)
planet_instance.name = "Planet " + str(current_orbit_radius)
planet_instance.base_mass = PLANETARY_MASS # randf_range(1e24, 1e25)
# Calculate orbit based on the last placed body.
var orbit_radius_real = current_orbit_radius + (planet_instance.mass * orbit_buffer)
_create_body_in_ring(primary, planet_instance, orbit_radius_real)
_create_moons_and_stations(planet_instance)
current_orbit_radius = orbit_radius_real + (planet_instance.mass * orbit_buffer)
"asteroid_belt":
var belt = _create_asteroid_belt(primary, current_orbit_radius)
system_data.belts.append(belt)
current_orbit_radius = current_orbit_radius + (belt.mass * orbit_buffer) * 2
"station":
var station_instance = station_scene.instantiate() as OrbitalBody2D
system_data.stations.append(station_instance)
station_instance.name = "Star Station " + str(current_orbit_radius)
station_instance.base_mass = STATION_MASS # A very small mass
var orbit_radius_real = current_orbit_radius + (station_instance.mass * orbit_buffer)
_create_body_in_ring(primary, station_instance, orbit_radius_real)
current_orbit_radius = orbit_radius_real + (station_instance.mass * orbit_buffer)
"wormhole_exit":
if not spaceship_scene:
print("Spaceship scene not set in StarSystemGenerator!")
continue
print("Spawning spaceship...")
var spaceship_instance = spaceship_scene.instantiate() as Module
# 3. Position it in a stable orbit
var orbit_radius = current_orbit_radius + (spaceship_instance.mass * orbit_buffer)
var initial_position_vector = Vector2(orbit_radius, 0).rotated(randf() * TAU)
spaceship_instance.global_position = primary.global_position + initial_position_vector
# 4. Use the OrbitalMechanics library to calculate its initial velocity
spaceship_instance.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(spaceship_instance, primary)
add_child(spaceship_instance)
current_orbit_radius = orbit_radius + (spaceship_instance.mass * orbit_buffer)
print("Star system generation complete.")
GameManager.register_star_system(self)
# Creates moons and stations around a primary body.
func _create_moons_and_stations(primary: OrbitalBody2D) -> void:
var num_moons = randi_range(min_moons, max_moons)
var num_planetary_stations = randi_range(min_planetary_stations, max_planetary_stations)
# Start with a base orbital radius around the parent planet.
var next_local_orbit_radius = min_planetary_orbit_radius
# Create an "inventory" of bodies to be placed.
var bodies_to_place = []
for i in range(num_planetary_stations):
bodies_to_place.append({"type": "station", "num": i})
for i in range(num_moons):
bodies_to_place.append({"type": "moon", "num": i})
# Randomize the order of placement.
bodies_to_place.shuffle()
for body in bodies_to_place:
match body.type:
"station":
var station_instance = station_scene.instantiate() as OrbitalBody2D
system_data.stations.append(station_instance)
station_instance.name = "Station " + str(body.num + 1)
station_instance.base_mass= STATION_MASS
# Use the local orbit radius for this station.
_create_body_in_ring(primary, station_instance, next_local_orbit_radius)
# Increment the *local* orbit radius for the next body.
next_local_orbit_radius += randf_range(40, 100)
"moon":
var moon_instance = moon_scene.instantiate() as OrbitalBody2D
system_data.moons.append(moon_instance)
moon_instance.name = "Moon " + str(body.num + 1)
moon_instance.base_mass = MOON_MASS
# Use the local orbit radius for this moon.
_create_body_in_ring(primary, moon_instance, next_local_orbit_radius)
# Increment the *local* orbit radius for the next body.
next_local_orbit_radius += randf_range(40, 100)
# --- FIX for stations orbiting moons ---
var num_moon_stations = randi_range(min_moon_stations, max_moon_stations)
var next_moon_station_orbit = randf_range(20, 50) # Start closer for moon stations
for i in range(num_moon_stations):
var station_instance = station_scene.instantiate() as OrbitalBody2D
system_data.stations.append(station_instance)
station_instance.name = "Station " + str(i + 1)
station_instance.base_mass = STATION_MASS
# Set the orbit relative to the moon.
var orbit_radius_real = next_moon_station_orbit
_create_body_in_ring(moon_instance, station_instance, orbit_radius_real)
# Increment for the next station around this *same* moon.
next_moon_station_orbit += randf_range(20, 50)
#var num_moon_stations = randi_range(min_moon_stations, max_moon_stations)
#
#var moon_inner_orbit = randf_range(2, 5)
#
#for i in range(num_moon_stations):
## Generate a space station.
#var station_instance = station_scene.instantiate() as OrbitalBody2D
#system_data.stations.append(station_instance)
#station_instance.name = "Station " + str(i + 1)
#station_instance.primary = moon_instance
#station_instance.mass = STATION_MASS
#station_instance.orbit_radius_real = moon_inner_orbit
#_create_body_in_ring(moon_instance, station_instance)
#
#moon_inner_orbit = moon_inner_orbit + randf_range(2, 5)
# Creates a single asteroid belt.
func _create_asteroid_belt(primary: OrbitalBody2D, initial_offset: float) -> AsteroidBelt:
var num_asteroids = randi_range(min_asteroids_per_belt, max_asteroids_per_belt)
var belt = AsteroidBelt.new()
for i in range(num_asteroids):
var asteroid_instance = asteroid_scene.instantiate() as OrbitalBody2D
asteroid_instance.name = "Asteroid " + str(i + 1)
asteroid_instance.base_mass = ASTEROID_MASS # randf_range(1e10, 1e23)
# B. Create the planet itself inside its Barycenter.
var planet = Planet.new()
system_data.planets.append(planet)
planet.name = "Planet_%d" % (i + 1)
planet.base_mass = randf_range(PLANET_MASS * 0.2, PLANET_MASS * 5.0)
planet_barycenter.add_child(planet)
planet.owner = planet_barycenter
planet.position = Vector2.ZERO
belt.asteroids.append(asteroid_instance)
belt.mass = belt.asteroids.reduce(func(accum, asteroid : OrbitalBody2D): return accum + asteroid.mass, 0.0)
var offset = belt.mass * orbit_buffer
belt.centered_radius = initial_offset + offset
for asteroid in belt.asteroids:
# Asteroids within a belt have a random orbit radius within a certain range.
var orbit_radius_real = belt.centered_radius + randf_range(-offset / 2, offset / 2)
_create_body_in_ring(primary, asteroid, orbit_radius_real)
# C. Create moons for this planet.
_generate_moons(planet, planet_barycenter, star_system, system_data)
return belt
func _create_body_in_ring(primary: OrbitalBody2D, body_instance: OrbitalBody2D, orbit_radius) -> void:
# 1. Parent the new body to its primary.
primary.add_child(body_instance)
# 2. Set its LOCAL position.
var initial_position_vector = Vector2(orbit_radius, 0).rotated(randf() * TAU)
body_instance.position = initial_position_vector
# 3. --- THE FIX ---
# Instead of calculating velocity now, we store the nodes for later.
_bodies_to_initialize.append({
"orbiter": body_instance,
"primary": primary
})
# D. Place the entire planetary system in a stable orbit.
planet_barycenter.global_position = Vector2(current_orbit_radius, 0).rotated(randf_range(0, TAU))
planet_barycenter.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(planet_barycenter, star)
# Update the new edge of the star's influence
# 1. Calculate the Hill Sphere (gravitational influence) for the planet we just placed.
var hill_sphere = OrbitalMechanics.calculate_hill_sphere(planet_barycenter, star)
# 2. Add this zone of influence (plus a safety margin) to the current radius
# to determine the starting point for the NEXT planet. This ensures orbits never overlap.
current_orbit_radius += hill_sphere * ORBIT_SAFETY_FACTOR
# --- NEW: Spawn the ship at the last planet's L4 point ---
if i == num_planets - 1:
_spawn_player_ship(star_system, star, planet_barycenter)
# The print statements about velocity are no longer relevant here.
print("Created " + body_instance.name + " with radius " + str(orbit_radius) + " and mass " + str(body_instance.mass))
# Helper function to instantiate and place a body in a ring.
#func _create_body_in_ring(primary: OrbitalBody2D, body_instance: OrbitalBody2D, orbit_radius) -> void:
## 1. Parent the new body to its primary FIRST.
## This establishes the correct local coordinate space.
#primary.add_child(body_instance)
#
## 2. Calculate the position vector relative to the parent.
#var initial_position_vector = Vector2(orbit_radius, 0).rotated(randf() * TAU)
#
## 3. Set the LOCAL position of the body.
## Godot will now correctly calculate its global_position based on the parent's position.
#body_instance.position = initial_position_vector
#print("Initial global position is: " + str(body_instance.global_position))
#
## We wait for one physics frame before calculating velocity.
## This ensures the body_instance.global_position is updated and correct.
##await get_tree().physics_frame
#
## 4. Now that the global position is correct, calculate the velocity for a stable orbit.
## This part remains the same, as physics calculations need the final global positions.
#body_instance.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(body_instance, primary)
#
#print("Created " + body_instance.name + " with radius " + str(orbit_radius) + " and mass " + str(body_instance.mass))
##print("Initial global position is: " + str(body_instance.global_position))
#print("Initial orbital radius is: " + str(orbit_radius))
#print("Initial orbital velocity is: " + str(body_instance.linear_velocity))
# Recursively finds all celestial bodies in the scene.
func get_all_bodies() -> Array:
var bodies = []
for child in get_children():
if child is OrbitalBody2D:
bodies.append(child)
# Recursively add children if they are also celestial bodies.
for sub_child in child.get_children():
if sub_child is OrbitalBody2D:
bodies.append(sub_child)
return bodies
func get_system_data() -> SystemData:
return system_data
class AsteroidBelt:
var width : float
var mass : float
var centered_radius : float = 0.0
var asteroids : Array[OrbitalBody2D]
func _generate_moons(planet: OrbitalBody2D, planet_barycenter: Barycenter, star_system: StarSystem, system_data: SystemData):
var num_moons = randi_range(0, int(planet.mass / MOON_MASS / 2.0)) # Heavier planets get more moons
num_moons = min(num_moons, MAX_MOONS_PER_PLANET)
class SystemData:
var star : OrbitalBody2D
var planets : Array[OrbitalBody2D]
var moons: Array[OrbitalBody2D]
var stations : Array[OrbitalBody2D]
var belts : Array[AsteroidBelt]
var current_orbit_radius = 200.0 # OrbitalMechanics.calculate_simplified_roche_limit(planet) # Start with the first orbit
func all_bodies() -> Array[OrbitalBody2D]:
var bodies : Array[OrbitalBody2D] = [star]
bodies.append_array(planets)
bodies.append_array(stations)
for i in range(num_moons):
var moon = Moon.new()
system_data.moons.append(moon)
planet_barycenter.add_child(moon)
moon.owner = planet_barycenter
moon.name = "Moon_%d" % (i + 1)
moon.base_mass = randf_range(MOON_MASS * 0.1, MOON_MASS * 2.0)
moon.position = Vector2(current_orbit_radius, 0).rotated(randf_range(0, TAU))
# Velocity is calculated relative to the parent (the planet)
moon.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(moon, planet_barycenter)
# Update the new edge of the planets's influence
# 1. Calculate the Hill Sphere (gravitational influence) for the moon we just placed.
var hill_sphere = OrbitalMechanics.calculate_hill_sphere(moon, planet_barycenter)
# 2. Add this zone of influence (plus a safety margin) to the current radius
# to determine the starting point for the NEXT planet. This ensures orbits never overlap.
current_orbit_radius += hill_sphere * ORBIT_SAFETY_FACTOR
bodies.append_array(moons)
var all_asteroids : Array[OrbitalBody2D] = []
for belt in belts:
all_asteroids.append_array(belt.asteroids)
bodies.append_array(all_asteroids)
return bodies
# --- NEW FUNCTION: Spawns the player ship at a Lagrange point ---
func _spawn_player_ship(star_system: StarSystem, star: OrbitalBody2D, planet_system: Barycenter):
# L4 and L5 Lagrange points form an equilateral triangle with the star and planet.
# We'll calculate L4 by rotating the star-planet vector by +60 degrees.
var star_to_planet_vec = planet_system.global_position - star.global_position
var l4_position = star.global_position + star_to_planet_vec.rotated(PI / 3.0)
# The ship's velocity at L4 must match the orbital characteristics of that point.
# This is an approximation where we rotate the planet's velocity vector by 60 degrees.
var l4_velocity = planet_system.linear_velocity.rotated(PI / 3.0)
# Instantiate, position, and configure the ship.
var ship_instance = GameManager.config.default_ship_scene.instantiate()
ship_instance.name = "PlayerShip"
star_system.add_child(ship_instance) # Add ship to the root StarSystem node
ship_instance.global_position = l4_position
ship_instance.linear_velocity = l4_velocity
ship_instance.rotation = l4_velocity.angle() + (PI / 2.0) # Point prograde
# Make sure the new ship is included in the physics simulation
#_system_data_dict.all_bodies.append(ship_instance)
print("Player ship spawned at L4 point of %s" % planet_system.name)
# Simplified, local-space velocity calculation.
#func _get_orbital_velocity(orbiter: Node2D, primary: Node2D) -> Vector2:
#var distance = orbiter.position.length()
#if distance == 0: return Vector2.ZERO
#
## We use the masses of the Barycenters for the calculation
#var primary_mass = primary.get_parent().mass if primary.get_parent() is Barycenter else primary.mass
#
## v = sqrt(G * M / r)
#var speed_magnitude = sqrt(OrbitalMechanics.G * primary_mass / distance)
#
#var perpendicular_dir = orbiter.position.orthogonal().normalized()
#return perpendicular_dir * speed_magnitude

22
scripts/system_data.gd Normal file
View File

@ -0,0 +1,22 @@
class_name SystemData
extends Resource
var star : OrbitalBody2D
var planets : Array[OrbitalBody2D]
var moons: Array[OrbitalBody2D]
#var stations : Array[OrbitalBody2D]
#var belts : Array[AsteroidBelt]
func all_bodies() -> Array[OrbitalBody2D]:
var bodies : Array[OrbitalBody2D] = [star]
bodies.append_array(planets)
#bodies.append_array(stations)
bodies.append_array(moons)
#var all_asteroids : Array[OrbitalBody2D] = []
#for belt in belts:
#all_asteroids.append_array(belt.asteroids)
#bodies.append_array(all_asteroids)
return bodies

View File

@ -0,0 +1 @@
uid://deskhqcqv20y8