WIP 3d refactor COMPILING

This commit is contained in:
2025-11-07 09:52:39 +01:00
parent 6b9efda0d2
commit 245be4a4f5
34 changed files with 347 additions and 380 deletions

View File

@ -1,30 +1,24 @@
[gd_scene load_steps=3 format=3 uid="uid://b1kpyek60vyof"]
[gd_scene load_steps=3 format=3 uid="uid://dhp1kc684qklv"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_1abiy"]
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_risxe"]
[node name="Module" type="RigidBody2D"]
position = Vector2(-50, 50)
mass = null
mass = 1.0
center_of_mass_mode = 1
center_of_mass = Vector2(-50, 0)
inertia = null
linear_velocity = null
angular_velocity = null
inertia = 0.0
linear_velocity = Vector2(0, 0)
angular_velocity = 0.0
script = ExtResource("1_1abiy")
base_mass = null
inertia = null
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="Hullplate" parent="StructuralContainer" instance=ExtResource("2_risxe")]
base_mass = null
inertia = null
[node name="@StaticBody2D@23989" parent="StructuralContainer" instance=ExtResource("2_risxe")]
position = Vector2(-100, 0)
base_mass = null
inertia = null
[node name="HullVolumeContainer" type="Node2D" parent="."]

View File

@ -1,30 +1,24 @@
[gd_scene load_steps=3 format=3 uid="uid://baeikwxkh26fh"]
[gd_scene load_steps=3 format=3 uid="uid://cvqs2vivnrepf"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_1rae4"]
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_fbnt1"]
[node name="Module" type="RigidBody2D"]
position = Vector2(-50, 50)
mass = null
mass = 1.0
center_of_mass_mode = 1
center_of_mass = Vector2(-50, 0)
inertia = null
linear_velocity = null
angular_velocity = null
inertia = 0.0
linear_velocity = Vector2(0, 0)
angular_velocity = 0.0
script = ExtResource("1_1rae4")
base_mass = null
inertia = null
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="Hullplate" parent="StructuralContainer" instance=ExtResource("2_fbnt1")]
base_mass = null
inertia = null
[node name="@StaticBody2D@23989" parent="StructuralContainer" instance=ExtResource("2_fbnt1")]
position = Vector2(-100, 0)
base_mass = null
inertia = null
[node name="HullVolumeContainer" type="Node2D" parent="."]

View File

@ -7,7 +7,7 @@ signal follow_requested(body: Node2D)
@onready var name_label: Label = $NameLabel
var body_reference: OrbitalBody2D
var body_reference: OrbitalBody3D
var dot_color: Color = Color.WHITE
var hover_tween: Tween
@ -27,7 +27,7 @@ func _ready() -> void:
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
func initialize(body: OrbitalBody2D):
func initialize(body: OrbitalBody3D):
body_reference = body
name_label.text = body.name

View File

@ -1,5 +1,5 @@
class_name Asteroid
extends OrbitalBody2D
extends OrbitalBody3D
# The orbital radius for this asteroid.
var orbital_radius: float

View File

@ -1,5 +1,5 @@
class_name Moon
extends OrbitalBody2D
extends OrbitalBody3D
# The orbital radius for this moon.
var orbital_radius: float

View File

@ -1,5 +1,5 @@
class_name Planet
extends OrbitalBody2D
extends OrbitalBody3D
# The orbital radius for this planet.
var orbital_radius: float

View File

@ -1,5 +1,5 @@
class_name Star
extends OrbitalBody2D
extends OrbitalBody3D
func get_class_name() -> String:
return "Star"

View File

@ -1,5 +1,5 @@
class_name Station
extends OrbitalBody2D
extends OrbitalBody3D
# The orbital radius for this station.
var orbital_radius: float

View File

@ -63,7 +63,7 @@ func _ready():
camera.make_current()
func on_body_entered(body: Node2D):
func on_body_entered(body: OrbitalBody3D):
# Detect Modules (which all inherit OrbitalBody2D via StructuralPiece)
if body is StructuralPiece:
overlapping_modules += 1
@ -72,7 +72,7 @@ func on_body_entered(body: Node2D):
if body is Ladder:
ladder_area = body.find_child("ClimbArea") # Assuming the Ladder has a specific Area2D for climbing
func on_body_exited(body: Node2D):
func on_body_exited(body: OrbitalBody3D):
if body is StructuralPiece:
overlapping_modules -= 1
@ -126,13 +126,13 @@ func process_interaction():
# Priority 1: Disengage from a station if we are in one.
if current_station:
current_station.disengage(self)
# current_station.disengage(self)
current_station = null
return
# Priority 2: Occupy a nearby station if we are not in one.
elif is_instance_valid(nearby_station):
current_station = nearby_station
current_station.occupy(self)
# current_station.occupy(self)
return
# Priority 3: Handle ladder launch logic.

View File

@ -60,11 +60,11 @@ func _unhandled_input(event: InputEvent):
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.zoom /= 1.2
func _on_marker_selected(body: Node2D):
func _on_marker_selected(body):
# Update the info panel with the selected body's data.
var text = "[b]%s[/b]\n" % body.name
if body is OrbitalBody2D:
if body is OrbitalBody3D:
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]

View File

@ -1,6 +1,6 @@
@tool
class_name Module
extends OrbitalBody2D
extends OrbitalBody3D
@export var ship_name: String = "Unnamed Ship" # Only relevant for the root module
@export var hull_integrity: float = 100.0 # This could also be a calculated property later
@ -43,14 +43,14 @@ func get_attachment_points() -> Array:
# --- Bulkheads (Interior and Exterior Edge Attachments) ---
elif piece is Bulkhead:
var interior_point = piece_center + piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
var interior_point = piece_center + piece.transform.origin.y * (COMPONENT_GRID_SIZE / 2.0)
points.append({
"position": interior_point,
"type": Component.AttachmentType.INTERIOR_WALL,
"piece": piece
})
var exterior_point = piece_center - piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
var exterior_point = piece_center - piece.transform.origin.y * (COMPONENT_GRID_SIZE / 2.0)
points.append({
"position": exterior_point,
"type": Component.AttachmentType.EXTERIOR_HULL,
@ -60,7 +60,7 @@ func get_attachment_points() -> Array:
return points
# --- This function remains largely the same ---
func attach_component(component: Component, global_pos: Vector2, parent_piece: StructuralPiece):
func attach_component(component: Component, global_pos: Vector3, parent_piece: StructuralPiece):
component.position = global_pos - global_position
component.attached_piece = parent_piece
add_child(component)
@ -82,7 +82,7 @@ func _recalculate_collision_shape():
# combined_polygons.append(piece_collision_shape.shape.points)
pass
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass!
# NOTE: The OrbitalBody3D's _update_mass_and_inertia() takes care of mass!
pass
# --- UPDATED: Clear module now iterates over all relevant children ---

View File

@ -2,12 +2,6 @@
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_b1h2b"]
[node name="Module" type="Node2D"]
[node name="Module" type="Node3D"]
script = ExtResource("1_b1h2b")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
mass = 1.0

View File

@ -1,6 +1,6 @@
@tool
class_name StructuralPiece
extends OrbitalBody2D
extends OrbitalBody3D
# Does this piece block atmosphere? (e.g., a hull plate would, a girder would not).
@export var is_pressurized: bool = true
@ -12,9 +12,9 @@ extends OrbitalBody2D
var is_preview: bool = false:
set(value):
is_preview = value
if is_preview:
# Make the piece translucent if it's a preview.
modulate = Color(1, 1, 1, 0.5)
else:
# Make it opaque if it's a permanent piece.
modulate = Color(1, 1, 1, 1)
# if is_preview:
# # Make the piece translucent if it's a preview.
# modulate = Color(1, 1, 1, 0.5)
# else:
# # Make it opaque if it's a permanent piece.
# modulate = Color(1, 1, 1, 1)

View File

@ -1,4 +1,4 @@
extends OrbitalBody2D
extends OrbitalBody3D
class_name Component
# Defines the size of the component in terms of the grid (e.g., 1x1, 1x2, 2x2)

View File

@ -16,7 +16,7 @@ var UiWindowScene = preload("res://scenes/UI/ui_window.tscn")
var wiring_schematic: WiringSchematic
# --- State ---
var occupants: Array[PilotBall] = []
var occupants: Array[CharacterPawn3D] = []
var active_shard_instances: Array[Databank] = []
var persistent_panel_instances: Array[BasePanel] = []
var occupant_panel_map: Dictionary = {}
@ -72,7 +72,7 @@ func _process(_delta):
func is_occupied() -> bool:
return not occupants.is_empty()
func occupy(character: PilotBall):
func occupy(character: CharacterPawn3D):
if character in occupants: return
occupants.append(character)
@ -84,7 +84,7 @@ func occupy(character: PilotBall):
occupancy_changed.emit(true)
func disengage(character: PilotBall):
func disengage(character: CharacterPawn3D):
if not character in occupants: return
# --- FIX: Close UI for THIS character only ---
@ -98,7 +98,7 @@ func disengage(character: PilotBall):
# --- UI MANAGEMENT ---
func close_interfaces_for_occupant(character: PilotBall):
func close_interfaces_for_occupant(character: CharacterPawn3D):
if occupant_panel_map.has(character):
occupant_panel_map[character].queue_free()
occupant_panel_map.erase(character)
@ -109,7 +109,7 @@ func close_interface(c: Control):
occupant_panel_map[occupant].queue_free()
occupant_panel_map.erase(occupant)
func launch_interfaces_for_occupant(character: PilotBall):
func launch_interfaces_for_occupant(character: CharacterPawn3D):
var ui_container = character.get_ui_container()
if not ui_container: return

View File

@ -74,41 +74,38 @@ func _physics_process(delta: float):
# If the thruster is active, apply a constant central force in its local "up" direction.
if is_firing:
apply_thrust_force()
# Also, ensure the visual effect is running
queue_redraw()
# Function called by the ThrusterController system to fire the thruster
func apply_thrust_force():
if is_firing:
# 1. Calculate the local force vector (magnitude and direction)
var local_force = Vector2.UP * max_thrust
var local_force = Vector3.UP * max_thrust
# 2. FIX: Convert the force to global space using ONLY the rotation (basis).
# This ensures the force vector's magnitude is not corrupted by the thruster's global position.
# NOTE: This replaces the problematic global_transform * vector or global_transform.xform(vector)
var force_vector = global_transform.basis_xform(local_force)
var force_vector = global_transform.basis * local_force
# 3. Apply the force to itself.
apply_force(force_vector, global_position)
func _draw():
# This function is only called if the thruster is firing (due to queue_redraw)
if not is_firing:
return
# func _draw():
# # This function is only called if the thruster is firing (due to queue_redraw)
# if not is_firing:
# return
# --- Draw a fiery, flickering cone ---
# The plume goes in the OPPOSITE direction of the thrust
var plume_direction = Vector2.DOWN
var plume_length = randf_range(20.0, 30.0) # Random length for a flickering effect
# # --- Draw a fiery, flickering cone ---
# # The plume goes in the OPPOSITE direction of the thrust
# var plume_direction = Vector2.DOWN
# var plume_length = randf_range(20.0, 30.0) # Random length for a flickering effect
# Define the 3 points of a triangle for the cone
var tip = plume_direction * plume_length
var base_offset = plume_direction.orthogonal() * 8.0
var base1 = base_offset
var base2 = -base_offset
# # Define the 3 points of a triangle for the cone
# var tip = plume_direction * plume_length
# var base_offset = plume_direction.orthogonal() * 8.0
# var base1 = base_offset
# var base2 = -base_offset
var points = PackedVector2Array([base1, tip, base2])
# var points = PackedVector2Array([base1, tip, base2])
# Draw the cone with a fiery color
draw_polygon(points, PackedColorArray([Color.ORANGE_RED, Color.GOLD, Color.ORANGE_RED]))
# # Draw the cone with a fiery color
# draw_polygon(points, PackedColorArray([Color.ORANGE_RED, Color.GOLD, Color.ORANGE_RED]))

View File

@ -2,7 +2,7 @@
class_name SensorPanel
extends BasePanel
signal body_selected_for_planning(body: OrbitalBody2D)
signal body_selected_for_planning(body: OrbitalBody3D)
@export var map_icon_scene: PackedScene
@ -13,11 +13,11 @@ const ICON_CULLING_PIXEL_THRESHOLD = 40.0
var map_scale: float = 0.001
var map_offset: Vector2 = Vector2.ZERO
var focal_body: OrbitalBody2D
var focal_body: OrbitalBody3D
var icon_map: Dictionary = {}
var followed_body: OrbitalBody2D = null
var followed_body: OrbitalBody3D = null
var map_tween: Tween
# The starting point for our lerp animation.
@ -37,7 +37,7 @@ func get_input_sockets():
return ["update_sensor_feed"]
# This is now the primary input for the map. It receives the "sensor feed".
func update_sensor_feed(all_bodies: Array[OrbitalBody2D]):
func update_sensor_feed(all_bodies: Array[OrbitalBody3D]):
# This function replaces the old _populate_map logic.
# We'll check which bodies are new and which have been removed.
var bodies_in_feed = all_bodies.duplicate()
@ -69,7 +69,7 @@ func _draw() -> void:
# TODO: The calculation of the projections should be moved into a databank
# as this panel should only ever display control nodes and possibly projection paths that are fed to it
var star_system = GameManager.current_star_system
var star_orbiters: Array[OrbitalBody2D] = []
var star_orbiters: Array[OrbitalBody3D] = []
star_orbiters.append(star_system.get_star())
star_orbiters.append_array(star_system.get_planetary_systems())
star_orbiters.append_array(star_system.get_orbital_bodies())
@ -86,7 +86,7 @@ func _draw() -> void:
draw_projected_orbits(planet_system.get_internal_attractors())
else: continue
func draw_projected_orbits(bodies_to_project: Array[OrbitalBody2D]):
func draw_projected_orbits(bodies_to_project: Array[OrbitalBody3D]):
var map_center = get_rect().size / 2.0
var focal_body = bodies_to_project[0]
@ -192,10 +192,10 @@ func _gui_input(event: InputEvent) -> void:
map_offset += event.relative
func _on_map_icon_selected(body: OrbitalBody2D):
func _on_map_icon_selected(body: OrbitalBody3D):
body_selected_for_planning.emit(body)
func _on_follow_requested(body: OrbitalBody2D):
func _on_follow_requested(body: OrbitalBody3D):
print("Map view locking on to: ", body.name)
follow_progress = 0.0
followed_body = body

View File

@ -23,21 +23,21 @@ class ImpulsiveBurnPlan:
var delta_v_magnitude: float
var wait_time: float = 0.0
var burn_duration: float
var desired_rotation_rad: float
var desired_rotation_rad: Basis
class PathProjection:
var body_ref: OrbitalBody2D
var body_ref: OrbitalBody3D
var points: Array[PathPoint]
func _init(b: OrbitalBody2D):
func _init(b: OrbitalBody3D):
body_ref = b
class PathPoint:
var time: float # Time in seconds from the start of the projection
var position: Vector2
var velocity: Vector2
var position: Vector3
var velocity: Vector3
func _init(t: float, p: Vector2, v: Vector2):
func _init(t: float, p: Vector3, v: Vector3):
time = t
position = p
velocity = v

View File

@ -54,7 +54,7 @@ func execute_plan():
print("AUTOPILOT: Executing plan. Waiting for first burn window.")
for step in current_plan:
status = "Performing Rotation: T- %f" % rad_to_deg(step.desired_rotation_rad)
# status = "Performing Rotation: T- %f" % rad_to_deg(step.desired_rotation_rad)
var time_elapsed: float = await _execute_autopilot_rotation(step)
current_timer = get_tree().create_timer(step.wait_time - time_elapsed)
@ -95,32 +95,59 @@ func _execute_next_burn(step: DataTypes.ImpulsiveBurnPlan):
# --- AUTOPILOT "BANG-COAST-BANG" LOGIC (REFACTORED) ---
func _execute_autopilot_rotation(step: DataTypes.ImpulsiveBurnPlan) -> float:
var time_window = minf(step.wait_time, max_rot_time)
var angle_to_turn = shortest_angle_between(root_module.rotation, step.desired_rotation_rad)
# --- 3D REFACTOR ---
# 1. We assume 'step.desired_rotation_rad' is now 'step.desired_basis'
# You MUST update your planners (Hohman, Brachistochrone) to
# calculate a target Basis (e.g., Basis.looking_at(prograde_vec, Vector3.UP))
# and store it in the ImpulsiveBurnPlan.
var step_target_basis: Basis = step.desired_rotation_rad # DANGER: This line assumes you updated DataTypes.
# For this to compile, you MUST change ImpulsiveBurnPlan in data_types.gd:
# var desired_rotation_rad: float -> var desired_basis: Basis
var error_quaternion: Quaternion = shortest_rotation_between(root_module.global_transform.basis, step_target_basis)
var angle_to_turn: float = error_quaternion.get_angle()
var axis_to_turn: Vector3 = error_quaternion.get_axis()
var init_time = Time.get_ticks_msec()
if abs(angle_to_turn) < 0.01:
request_rotation.emit(step.desired_rotation_rad)
if angle_to_turn < 0.01: # Already aligned
request_rotation.emit(step_target_basis) # Send the Basis to the helm
request_attitude_hold.emit(true)
return 0.0
return 0.0
# --- Get the specific torque values for each phase ---
var accel_torque = RCS_calibration.max_pos_torque if angle_to_turn > 0 else RCS_calibration.max_neg_torque
var decel_torque = RCS_calibration.max_neg_torque if angle_to_turn > 0 else RCS_calibration.max_pos_torque
# --- 3D Torque Calculation ---
# This logic is now much more complex. We need to find the torque
# vector to apply.
# For a simple "bang-bang" controller, we just apply max torque
# along the calculated axis.
if accel_torque == 0 or decel_torque == 0:
print("AUTOPILOT ERROR: Missing thrusters for a full rotation.")
return 0.0
print(" - Performing rotation.")
# TODO: This assumes your calibration data is now 3D
# (e.g., max_pos_torque is now max_torque_vector).
# This needs a calibration refactor, which is complex.
# --- SIMPLIFIED 3D LOGIC (for now) ---
# We'll re-use the PD controller logic from your Helm Shard
# to get a torque vector.
var error_torque = axis_to_turn * angle_to_turn * RCS_calibration.max_pos_torque # (This is a P-controller)
var damping_torque = -root_module.angular_velocity * (RCS_calibration.max_pos_torque * 0.5) # (This is a D-controller)
var desired_torque_vector = error_torque + damping_torque
# We are no longer calculating burn times, we are just applying
# torque until we reach the target.
# This is a full change from bang-bang to a PD controller.
# --- REFACTORING THE "BANG-BANG" LOGIC FOR 3D ---
# Let's stick closer to your original design.
# 1. Get calibrated torque values (this now needs to be per-axis)
# Let's assume a simplified calibration for now.
var accel_torque_magnitude = RCS_calibration.max_pos_torque # Needs refactor
var decel_torque_magnitude = RCS_calibration.max_neg_torque # Needs refactor
var accel_angular_accel = accel_torque_magnitude / root_module.inertia
var decel_angular_accel = decel_torque_magnitude / root_module.inertia
# --- Asymmetrical Burn Calculation ---
# This is a more complex kinematic problem. We solve for the peak velocity and individual times.
var accel_angular_accel = accel_torque / root_module.inertia
var decel_angular_accel = decel_torque / root_module.inertia
# Solve for peak angular velocity (ω_peak) and times (t1, t2)
var peak_angular_velocity = (2 * angle_to_turn * accel_angular_accel * decel_angular_accel) / (accel_angular_accel + decel_angular_accel)
peak_angular_velocity = sqrt(abs(peak_angular_velocity)) * sign(angle_to_turn)
@ -130,31 +157,27 @@ func _execute_autopilot_rotation(step: DataTypes.ImpulsiveBurnPlan) -> float:
var total_maneuver_time = accel_burn_time + decel_burn_time
if total_maneuver_time > time_window:
print("AUTOPILOT WARNING: Maneuver is impossible in the given time window. Performing max-power turn.")
# Fallback to a simple 50/50 burn if time is too short.
accel_burn_time = time_window / 2.0
decel_burn_time = time_window / 2.0
# No coast time in this simplified model, but it could be added back with more complex math.
print(" - Asymmetrical Rotation Plan: Accel Burn %.2fs, Decel Burn %.2fs" % [accel_burn_time, decel_burn_time])
# --- Execute Maneuver (3D) ---
# --- Execute Maneuver ---
# ACCELERATION BURN
request_rotation_thrust.emit(sign(angle_to_turn))
# ACCELERATION BURN: Apply torque along the axis
request_rotation_thrust.emit(axis_to_turn * accel_torque_magnitude)
await get_tree().create_timer(accel_burn_time).timeout
# DECELERATION BURN
print(" - Rotation acceleration complete, executing deceleration burn.")
request_rotation_thrust.emit(sign(-angle_to_turn))
# DECELERATION BURN: Apply torque against the axis
request_rotation_thrust.emit(-axis_to_turn * decel_torque_magnitude)
await get_tree().create_timer(decel_burn_time).timeout
print(" - Rotation de-acceleration complete, executing deceleration burn.")
request_rotation.emit(step.desired_rotation_rad)
# Stop all torque
request_rotation_thrust.emit(Vector3.ZERO)
# Set final hold
request_rotation.emit(step_target_basis)
request_attitude_hold.emit(true)
print("AUTOPILOT: Rotation maneuver complete.")
print("AUTOPILOT: 3D Rotation maneuver complete.")
return init_time - Time.get_ticks_msec()
@ -168,3 +191,29 @@ func shortest_angle_between(from_angle: float, to_angle: float) -> float:
return difference - TAU
else:
return difference
# A simple class to hold the result of a Basis comparison.
class BasisComparisonResult:
var axis: Vector3
var angle: float
func _init(axis: Vector3, angle: float):
self.axis = axis
self.angle = angle
# Finds the shortest rotation (as an axis and angle) between two Basis objects.
# Returns a Dictionary: {"axis": Vector3, "angle": float}
func shortest_rotation_between(from_basis: Basis, to_basis: Basis) -> Quaternion:
var current_quat = from_basis.get_rotation_quaternion().normalized()
var target_quat = to_basis.get_rotation_quaternion().normalized()
# Calculate the difference quaternion (rotation from 'current' to 'target')
var diff_quat = target_quat * current_quat.inverse()
# Ensure we're taking the shortest path.
# A quaternion and its negative represent the same orientation,
# but one is the "long way around".
if diff_quat.w < 0:
diff_quat = -diff_quat
return diff_quat

View File

@ -8,7 +8,7 @@ class_name HelmLogicShard
@export var HOLD_KP: float = 8000.0 # Proportional gain
@export var HOLD_KD: float = 1200.0 # Derivative gain
@onready var target_rotation_rad: float = 0.0
@onready var target_rotation_rad: Basis
var attitude_hold_enabled: bool = false
var thruster_calibration_data: DataTypes.ThrusterCalibration
@ -32,14 +32,14 @@ func initialize(ship_root: Module):
# You can add logic here to listen for parts being added/removed to re-scan.
# Default to holding the initial attitude.
target_rotation_rad = root_module.rotation
target_rotation_rad = root_module.basis
func _physics_process(_delta):
if not is_instance_valid(root_module): return
# If attitude hold is on, run the PD controller.
if attitude_hold_enabled:
_perform_manual_hold()
# # If attitude hold is on, run the PD controller.
# if attitude_hold_enabled:
# _perform_manual_hold()
# --- INPUT SOCKETS (Called by Panels or other Shards) ---
@ -55,7 +55,7 @@ func set_rotation_input(value: float):
# When input stops, re-engage hold at the current rotation.
if not attitude_hold_enabled:
attitude_hold_enabled = true
target_rotation_rad = root_module.rotation
target_rotation_rad = root_module.basis
## This is an "input socket" for translational control (main thrusters).
## It takes a value from 0.0 to 1.0.
@ -77,21 +77,21 @@ func set_throttle_input(value: float):
print(" - Main Engine Shut Off")
thruster.turn_off()
func set_desired_rotation(r: float):
target_rotation_rad = r
func set_desired_rotation(b: Basis):
target_rotation_rad = b
func set_attitude_hold(hold: bool):
attitude_hold_enabled = hold
# --- LOGIC (Migrated from ThrusterController.gd) ---
func _perform_manual_hold():
var error = shortest_angle_between(root_module.rotation, target_rotation_rad)
if abs(error) > 0.001:
var desired_torque = (error * HOLD_KP) - (root_module.angular_velocity * HOLD_KD)
# func _perform_manual_hold():
# var error = shortest_angle_between(root_module.rotation, target_rotation_rad)
# if abs(error) > 0.001:
# var desired_torque = (error * HOLD_KP) - (root_module.angular_velocity * HOLD_KD)
apply_rotational_thrust(desired_torque)
else: apply_rotational_thrust(0.0)
# apply_rotational_thrust(desired_torque)
# else: apply_rotational_thrust(0.0)
# --- REFACTORED: This is the other key change ---
func apply_rotational_thrust(desired_torque: float):

View File

@ -20,18 +20,18 @@ func get_output_sockets() -> Array[String]:
func _physics_process(delta):
if not is_instance_valid(root_module):
return
# 1. Gather all the data from the root module.
var rotation_deg = rad_to_deg(root_module.rotation)
var angular_vel_dps = rad_to_deg(root_module.angular_velocity)
var linear_vel_mps = root_module.linear_velocity.length()
# # 1. Gather all the data from the root module.
# var rotation_deg = rad_to_deg(root_module.rotation)
# var angular_vel_dps = rad_to_deg(root_module.angular_velocity)
# var linear_vel_mps = root_module.linear_velocity.length()
# 2. Build the string that will be displayed.
var status_text = """
[font_size=24]Ship Status[/font_size]
[font_size=18]Rotation: %.1f deg[/font_size]
[font_size=18]Ang. Vel.: %.2f deg/s[/font_size]
[font_size=18]Velocity: %.2f m/s[/font_size]
""" % [rotation_deg, angular_vel_dps, linear_vel_mps]
# # 2. Build the string that will be displayed.
# var status_text = """
# [font_size=24]Ship Status[/font_size]
# [font_size=18]Rotation: %.1f deg[/font_size]
# [font_size=18]Ang. Vel.: %.2f deg/s[/font_size]
# [font_size=18]Velocity: %.2f m/s[/font_size]
# """ % [rotation_deg, angular_vel_dps, linear_vel_mps]
# 3. Emit the signal with the formatted text.
status_updated.emit(status_text)
# # 3. Emit the signal with the formatted text.
# status_updated.emit(status_text)

View File

@ -6,7 +6,7 @@ class_name BrachistochronePlannerShard
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
# --- References ---
var target_body: OrbitalBody2D = null
var target_body: OrbitalBody3D = null
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
@ -17,7 +17,7 @@ func get_output_sockets() -> Array[String]:
return ["maneuver_calculated"]
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
func target_updated(new_target: OrbitalBody2D):
func target_updated(new_target: OrbitalBody3D):
print("BRACHISTOCHRONE PLANNER: Target received %s." % new_target.name)
target_body = new_target
@ -54,7 +54,7 @@ func calculate_brachistochrone_transfer():
accel_burn.wait_time = 0 # Start immediately
accel_burn.burn_duration = time_for_half_journey
# The desired rotation is the direction vector from ship to target
accel_burn.desired_rotation_rad = root_module.global_position.direction_to(target_body.global_position).angle() + (PI / 2.0)
accel_burn.desired_rotation_rad = root_module.global_position.direction_to(target_body.global_position) # + (PI / 2.0)
plan.append(accel_burn)
# --- Step 2: Deceleration Burn (The flip is handled by the autopilot between steps) ---

View File

@ -7,7 +7,7 @@ signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
# --- References ---
var selection_shard: NavSelectionShard
var target_body: OrbitalBody2D = null
var target_body: OrbitalBody3D = null
# --- Configurations ---
var boost_factor: float = 1.0
@ -21,7 +21,7 @@ func get_output_sockets() -> Array[String]:
return ["maneuver_calculated"]
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
func target_updated(new_target: OrbitalBody2D):
func target_updated(new_target: OrbitalBody3D):
print("MANEUVER PLANNER: Target recieved %s." % new_target)
target_body = new_target
@ -119,7 +119,7 @@ func calculate_hohmann_transfer():
maneuver_calculated.emit(plan)
# Simulates the ship's 2-body orbit around the star to predict its future state.
func _predict_state_after_coast(body_to_trace: OrbitalBody2D, primary: OrbitalBody2D, time: float) -> Dictionary:
func _predict_state_after_coast(body_to_trace: OrbitalBody3D, primary: OrbitalBody3D, time: float) -> Dictionary:
# --- Simulation Parameters ---
var time_step = 1.0 # Simulate in 1-second increments
var num_steps = int(ceil(time / time_step))

View File

@ -16,7 +16,7 @@ func get_output_sockets() -> Array[String]:
## Projects the future paths of an array of bodies interacting with each other.
## Returns a dictionary mapping each body to its calculated PackedVector2Array path.
func project_n_body_paths(
bodies_to_trace: Array[OrbitalBody2D],
bodies_to_trace: Array[OrbitalBody3D],
num_steps: int,
time_step: float
):

View File

@ -3,9 +3,9 @@ extends Databank
class_name NavSelectionShard
## Emitted whenever a new navigation target is selected from the map.
signal target_selected(body: OrbitalBody2D)
signal target_selected(body: OrbitalBody3D)
var selected_body: OrbitalBody2D = null
var selected_body: OrbitalBody3D = null
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
@ -16,7 +16,7 @@ func get_output_sockets() -> Array[String]:
return ["target_selected"]
# INPUT SOCKET: This function is connected to the SensorPanel's "body_selected" signal.
func body_selected(body: OrbitalBody2D):
func body_selected(body: OrbitalBody3D):
if is_instance_valid(body) and body != selected_body:
print("NAV SELECTION: New target acquired - ", body.name)
selected_body = body

View File

@ -1,7 +1,7 @@
extends Databank
class_name SensorSystemShard
signal sensor_feed_updated(bodies: Array[OrbitalBody2D])
signal sensor_feed_updated(bodies: Array[OrbitalBody3D])
@export_group("Projection Settings")
@export var projection_steps: int = 500
@ -23,7 +23,7 @@ func _process(_delta: float):
return
# Gather all bodies that need to be included in the simulation.
var tracked_bodies: Array[OrbitalBody2D] = []
var tracked_bodies: Array[OrbitalBody3D] = []
tracked_bodies.append(star_system.get_star())
tracked_bodies.append_array(star_system.get_planetary_systems())
tracked_bodies.append_array(star_system.get_orbital_bodies())

View File

@ -1,6 +1,6 @@
# scripts/barycenter.gd
class_name Barycenter
extends OrbitalBody2D
extends OrbitalBody3D
func _ready():
physics_mode = PhysicsMode.INDEPENDENT
@ -10,10 +10,10 @@ func _ready():
# 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] = []
func get_internal_attractors() -> Array[OrbitalBody3D]:
var internal_attractors: Array[OrbitalBody3D] = []
for child in get_children():
if child is OrbitalBody2D:
if child is OrbitalBody3D:
internal_attractors.append(child)
return internal_attractors

View File

@ -1,5 +1,8 @@
extends Node2D
class_name OrbitalBody2D
# orbital_body_3d.gd
# REFACTOR: Extends Node3D instead of Node2D
@tool
class_name OrbitalBody3D
extends Node3D
# Defines the physical behavior of this body.
enum PhysicsMode {
@ -10,74 +13,68 @@ enum PhysicsMode {
@export var physics_mode: PhysicsMode = PhysicsMode.INDEPENDENT
var current_grid_authority: OrbitalBody2D = null
var current_grid_authority: OrbitalBody3D = null
# Mass of this individual component
@export var base_mass: float = 1.0
@export var mass: float = 0.0 # Aggregated mass of this body and all its OrbitalBody2D children
@export var linear_velocity: Vector2 = Vector2.ZERO
@export var angular_velocity: float = 0.0
@export var mass: float = 0.0 # Aggregated mass of this body and all its OrbitalBody3D children
# REFACTOR: All physics properties are now Vector3
@export var linear_velocity: Vector3 = Vector3.ZERO
@export var angular_velocity: Vector3 = Vector3.ZERO # Represents angular velocity around X, Y, and Z axes
# Variables to accumulate forces applied during the current physics frame
var accumulated_force: Vector2 = Vector2.ZERO
var accumulated_torque: float = 0.0
var accumulated_force: Vector3 = Vector3.ZERO
var accumulated_torque: Vector3 = Vector3.ZERO
# Placeholder for Moment of Inertia.
# REFACTOR: This is a simplification. For true 3D physics, this would be an
# inertia tensor (a Basis). But for game physics, a single float
# (like your CharacterPawn3D) is much simpler to work with.
@export var inertia: float = 1.0
func _ready():
# Ensure mass update runs immediately before the first _physics_process.
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, pos: Vector2 = self.global_position):
# REFACTOR: All arguments are now Vector3
func apply_force(force: Vector3, pos: Vector3 = self.global_position):
# This is the force routing logic.
match physics_mode:
PhysicsMode.INDEPENDENT:
_add_forces(force, pos)
PhysicsMode.COMPOSITE:
_add_forces(force, pos)
## 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.
# If we are not the root, we must route the force to the next OrbitalBody3D parent.
var p = get_parent()
while p:
if p is OrbitalBody2D:
if p is OrbitalBody3D:
# Recursively call the parent's apply_force method.
# This sends the force (and its original global position) up the chain.
p.apply_force(force, pos)
return # Stop at the first OrbitalBody2D parent
return # Stop at the first OrbitalBody3D parent
p = p.get_parent()
push_error("Anchored OrbitalBody2D has become dislodged and is now Composite.")
push_error("Anchored OrbitalBody3D has become dislodged and is now Composite.")
physics_mode = PhysicsMode.COMPOSITE
apply_force(force, position)
func _add_forces(force: Vector2, pos: Vector2 = Vector2.ZERO):
func _add_forces(force: Vector3, pos: Vector3 = Vector3.ZERO):
# 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 = pos - global_position
var torque = r.x * force.y - r.y * force.x
# REFACTOR: Use 3D cross product (r x F) for torque instead of 2D (r.x*F.y - r.y*F.x)
var torque = r.cross(force)
accumulated_torque += torque
func _update_mass_and_inertia():
mass = base_mass
for child in get_children():
if child is OrbitalBody2D:
if child is OrbitalBody3D:
child._update_mass_and_inertia() # Recurse into children
mass += child.mass
@ -85,8 +82,6 @@ func _update_mass_and_inertia():
func _physics_process(delta):
if not Engine.is_editor_hint():
# Note: We're not integrating forces for anchored bodies
# anchored bodies add forces to their parents and
match physics_mode:
PhysicsMode.INDEPENDENT:
_integrate_forces(delta)
@ -97,44 +92,41 @@ func _integrate_forces(delta):
# Safety Check for Division by Zero
var sim_mass = mass
if sim_mass <= 0.0:
# If mass is zero, stop all physics to prevent NaN explosion.
accumulated_force = Vector2.ZERO
accumulated_torque = 0.0
accumulated_force = Vector3.ZERO
accumulated_torque = Vector3.ZERO
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 +
# 3. Apply Linear Physics (F = ma)
var linear_acceleration = accumulated_force / sim_mass # Division is now safe
linear_velocity += linear_acceleration * delta
global_position += linear_velocity * delta
# 4. Apply Rotational Physics (T = I * angular_acceleration)
var angular_acceleration = accumulated_torque / inertia
angular_velocity += angular_acceleration * delta
rotation += angular_velocity * delta
# 5. Reset accumulated forces for the next frame
accumulated_force = Vector2.ZERO
accumulated_torque = 0.0
# REFACTOR: Use the simplified 3D torque equation from your CharacterPawn3D
if inertia > 0:
var angular_acceleration = accumulated_torque / inertia
angular_velocity += angular_acceleration * delta
# REFACTOR: Apply 3D rotation using the integrated angular velocity
# (This is the same method your CharacterPawn3D uses)
if angular_velocity.length_squared() > 0.0001:
rotate(angular_velocity.normalized(), angular_velocity.length() * delta)
# Optional: Add damping
# angular_velocity *= (1.0 - 0.1 * delta)
# 5. Reset accumulated forces for the next frame
accumulated_force = Vector3.ZERO
accumulated_torque = Vector3.ZERO
# This is the new, corrected function.
func recalculate_physical_properties():
# For non-composite bodies, the calculation is simple.
if physics_mode != PhysicsMode.COMPOSITE:
mass = base_mass
# --- THE FIX ---
# An independent body doesn't calculate inertia from parts.
# We ensure it has a non-zero default value to prevent division by zero.
if inertia <= 0.0:
inertia = 1.0
return
var all_parts: Array[OrbitalBody2D] = []
var all_parts: Array[OrbitalBody3D] = []
_collect_anchored_parts(all_parts)
if all_parts.is_empty():
@ -144,39 +136,34 @@ func recalculate_physical_properties():
# --- Step 1: Calculate Total Mass and LOCAL Center of Mass ---
var total_mass = 0.0
var weighted_local_pos_sum = Vector2.ZERO
# REFACTOR: Use Vector3
var weighted_local_pos_sum = Vector3.ZERO
for part in all_parts:
total_mass += part.base_mass
# We get the part's position *relative to the root module*
var local_pos = part.global_position - self.global_position
weighted_local_pos_sum += local_pos * part.base_mass
var local_center_of_mass = Vector2.ZERO
var local_center_of_mass = Vector3.ZERO
if total_mass > 0:
local_center_of_mass = weighted_local_pos_sum / total_mass
# --- Step 2: Calculate Total Moment of Inertia around the LOCAL CoM ---
var total_inertia = 0.0
for part in all_parts:
# Get the part's position relative to the root module again
var local_pos = part.global_position - self.global_position
# The radius is the distance from the part's local position to the ship's local center of mass
# REFACTOR: This logic (Parallel Axis Theorem) is still correct for Vector3
var r_squared = (local_pos - local_center_of_mass).length_squared()
total_inertia += part.base_mass * r_squared
# --- Step 3: Assign the final values ---
self.mass = total_mass
# We apply a scaling factor here because our "units" are pixels.
# This brings the final value into a range that feels good for gameplay.
# You can tune this factor to make ships feel heavier or lighter.
self.inertia = total_inertia * 0.01
#print("Physics Recalculated: Mass=%.2f kg, Inertia=%.2f" % [mass, inertia])
if self.inertia <= 0.0: # Safety check
self.inertia = 1.0
# A recursive helper function to get an array of all OrbitalBody2D children
# A recursive helper function to get an array of all OrbitalBody3D children
func _collect_anchored_parts(parts_array: Array):
parts_array.append(self)
for child in get_children():
# TODO: this assumes that all OrbitalBody2D that are attached are done in a clean chain without breaks, which may not be the case
if child is OrbitalBody2D and child.physics_mode == PhysicsMode.ANCHORED:
child._collect_anchored_parts(parts_array)
if child is OrbitalBody3D and child.physics_mode == PhysicsMode.ANCHORED:
child._collect_anchored_parts(parts_array)

View File

@ -1 +1 @@
uid://0isnsk356que
uid://wlm40n8ywr

View File

@ -98,7 +98,7 @@ func register_star_system(system_node):
current_star_system = system_node
print("GameManager: Star system registered.")
func register_ship(ship: OrbitalBody2D):
func register_ship(ship: OrbitalBody3D):
if not is_instance_valid(current_star_system):
return
@ -119,8 +119,8 @@ func get_system_data() -> SystemData:
return current_star_system.get_system_data()
return null
func get_all_trackable_bodies() -> Array[OrbitalBody2D]:
var all_bodies: Array[OrbitalBody2D] = []
func get_all_trackable_bodies() -> Array[OrbitalBody3D]:
var all_bodies: Array[OrbitalBody3D] = []
if current_star_system:
# First, get all the celestial bodies (planets, moons, etc.)
var system_data = current_star_system.get_system_data()

View File

@ -5,19 +5,10 @@ extends Node
# The scaled gravitational constant for the entire 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:
@ -26,93 +17,72 @@ func _physics_process(_delta: float) -> void:
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]
var top_level_bodies: Array[OrbitalBody3D] = [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:
func calculate_gravitational_force(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> Vector3:
if not is_instance_valid(orbiter) or not is_instance_valid(primary):
return Vector2.ZERO
# REFACTOR: Return Vector3.ZERO
return Vector3.ZERO
var distance_sq = orbiter.global_position.distance_squared_to(primary.global_position)
if distance_sq < 1.0:
return Vector2.ZERO
return Vector3.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
return Vector3.ZERO
var force_magnitude = (G * primary.mass * orbiter.mass) / distance_sq
# REFACTOR: direction_to is now 3D, this logic is fine
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
func apply_n_body_forces(attractors: Array[OrbitalBody3D]):
for i in range(attractors.size()):
var body_a: OrbitalBody2D = attractors[i]
var body_a: OrbitalBody3D = attractors[i]
if not is_instance_valid(body_a): continue
for j in range(i + 1, attractors.size()):
var body_b: OrbitalBody2D = attractors[j]
var body_b: OrbitalBody3D = attractors[j]
if not is_instance_valid(body_b): continue
# Calculate the force vector ONCE
# REFACTOR: force_vector is now Vector3
var force_vector = calculate_gravitational_force(body_a, body_b)
# Apply the force symmetrically
if force_vector != Vector2.ZERO:
if force_vector != Vector3.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
func calculate_n_body_force(body: OrbitalBody3D, attractors: Array[OrbitalBody3D]) -> Vector3:
var total_pull: Vector3 = Vector3.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
func calculate_n_body_gravity_forces(body_to_affect: Node3D) -> Vector3:
var total_force = Vector3.ZERO
if not is_instance_valid(body_to_affect):
return total_force
# Get the list of all major gravitational bodies from the GameManager.
var system_data = GameManager.get_system_data()
if not system_data:
return total_force
# We only consider planets and the star as major attractors for performance.
var attractors = system_data.all_bodies()
for attractor in attractors:
@ -121,39 +91,43 @@ func calculate_n_body_gravity_forces(body_to_affect: Node2D) -> Vector2:
return total_force
# Calculates the perfect initial velocity for a stable circular orbit.
func calculate_circular_orbit_velocity(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
func calculate_circular_orbit_velocity(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> Vector3:
if not is_instance_valid(primary):
return Vector2.ZERO
return Vector3.ZERO
var distance = orbiter.global_position.distance_to(primary.global_position)
if distance == 0:
return Vector2.ZERO
return Vector3.ZERO
# v = sqrt(G * M / r)
var speed_magnitude = sqrt(G * primary.mass / distance)
var direction_to_orbiter = primary.global_position.direction_to(orbiter.global_position)
var perpendicular_direction = Vector2(direction_to_orbiter.y, -direction_to_orbiter.x)
# REFACTOR: This is the biggest 2D -> 3D logic change.
# We can't just get a simple perpendicular vector. We need to define
# an orbital plane. We'll assume a "flat" system on the XZ plane,
# so the "up" vector is Vector3.UP.
# We find the perpendicular by crossing "up" with the direction to the orbiter.
var perpendicular_direction = Vector3.UP.cross(direction_to_orbiter).normalized()
return perpendicular_direction * speed_magnitude
func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
# REFACTOR: Returns PackedVector3Array
func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVector3Array:
var num_steps = 10
var time_step = 60
var ghost_position = body_to_trace.global_position
var ghost_velocity = body_to_trace.linear_velocity
var path_points = PackedVector2Array()
var path_points = PackedVector3Array()
for i in range(num_steps):
# Create a temporary "ghost" body to calculate forces on.
var ghost_body = OrbitalBody2D.new()
var ghost_body = OrbitalBody3D.new()
ghost_body.global_position = ghost_position
ghost_body.mass = body_to_trace.mass
# Use our library to get the total gravitational force at the ghost's position.
var total_force = calculate_n_body_gravity_forces(ghost_body)
var acceleration = total_force / ghost_body.mass
@ -161,14 +135,14 @@ func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVecto
ghost_position += ghost_velocity * time_step
path_points.append(ghost_position)
ghost_body.free() # Clean up the temporary node
ghost_body.free()
return path_points
# Calculates an array of points for the orbit RELATIVE to the primary body.
func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
# REFACTOR: Returns PackedVector3Array
func _calculate_relative_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVector3Array:
if not is_instance_valid(body_to_trace) or not body_to_trace.has_method("get_primary") or not is_instance_valid(body_to_trace.get_primary()):
return PackedVector2Array()
return PackedVector3Array()
var primary = body_to_trace.get_primary()
var primary_mass = primary.mass
@ -179,11 +153,10 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVec
var r_magnitude = ghost_relative_pos.length()
if r_magnitude == 0:
return PackedVector2Array()
return PackedVector3Array()
var v_sq = ghost_relative_vel.length_squared()
var mu = G * primary_mass
var specific_energy = v_sq / 2.0 - mu / r_magnitude
var num_steps = 200
@ -195,13 +168,12 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVec
var orbital_period = 2.0 * PI * sqrt(pow(semi_major_axis, 3) / mu)
time_step = orbital_period / float(num_steps)
var path_points = PackedVector2Array()
var path_points = PackedVector3Array()
for i in range(num_steps):
var distance_sq = ghost_relative_pos.length_squared()
if distance_sq < 1.0:
break
var direction = -ghost_relative_pos.normalized()
var force_magnitude = (G * primary_mass * body_mass) / distance_sq
var force_vector = direction * force_magnitude
@ -213,29 +185,22 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVec
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:
# --- These functions are scalar and need no changes ---
func calculate_hill_sphere(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> 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:
func calculate_simplified_roche_limit(primary: OrbitalBody3D) -> 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 100.0
return sqrt(primary.mass) * ROCHE_LIMIT_MASS_MULTIPLIER
func get_orbital_time_in_seconds(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> float:
func get_orbital_time_in_seconds(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> float:
var mu = OrbitalMechanics.G * primary.mass
var r = orbiter.global_position.distance_to(primary.global_position)
return TAU * sqrt(pow(r, 3) / mu)

View File

@ -20,7 +20,7 @@ func _ready():
GameManager.start_game()
# --- Public API for accessing system data ---
func get_star() -> OrbitalBody2D:
func get_star() -> OrbitalBody3D:
if is_instance_valid(system_data):
return system_data.star
return null
@ -33,12 +33,12 @@ func get_planetary_systems() -> Array[Barycenter]:
return bodies
func get_orbital_bodies() -> Array[OrbitalBody2D]:
var bodies: Array[OrbitalBody2D] = []
func get_orbital_bodies() -> Array[OrbitalBody3D]:
var bodies: Array[OrbitalBody3D] = []
for child in get_children():
if child is Star or child is Barycenter:
continue
if child is OrbitalBody2D:
if child is OrbitalBody3D:
bodies.append(child)
return bodies
@ -50,4 +50,4 @@ class AsteroidBelt:
var width : float
var mass : float
var centered_radius : float = 0.0
var asteroids : Array[OrbitalBody2D]
var asteroids : Array[OrbitalBody3D]

View File

@ -2,72 +2,65 @@
class_name StarSystemGenerator
extends RefCounted
# --- Stable Mass Ratios & Generation Rules ---
# --- (Constants are fine) ---
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 PLANET_MASS = STAR_MASS / 10000.0
const MOON_MASS = PLANET_MASS / 1000.0
const MIN_PLANETS = 3
const MAX_PLANETS = 8
const MAX_MOONS_PER_PLANET = 5
const ORBIT_SAFETY_FACTOR = 5 # Increase space between orbits
const ORBIT_SAFETY_FACTOR = 5
# --- The main public method ---
func generate(star_system: StarSystem) -> SystemData:
# 1. Create the root Barycenter for the entire system.
var system_data = SystemData.new()
# 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)
# 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
var current_orbit_radius = 15000.0
for i in range(num_planets):
# A. Create the Barycenter for the new planetary system.
var planet_barycenter = Barycenter.new()
planet_barycenter.name = "PlanetSystem_%d" % (i + 1)
star_system.add_child(planet_barycenter)
# 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
# REFACTOR: Set 3D position
planet.position = Vector3.ZERO
planet_barycenter.recalculate_total_mass()
# C. Create moons for this planet.
_generate_moons(planet, planet_barycenter, system_data)
# D. Place the entire planetary system in a stable orbit.
planet_barycenter.global_position = Vector2(current_orbit_radius, 0).rotated(randf_range(0, TAU))
# REFACTOR: Place the planet in 3D space (on the XZ plane)
# We rotate around the Y-axis (Vector3.UP)
planet_barycenter.global_position = Vector3(current_orbit_radius, 0, 0).rotated(Vector3.UP, randf_range(0, TAU))
# REFACTOR: This now receives a Vector3 velocity
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
# --- Spawn the ship at the last planet's L4 point ---
if i == num_planets - 1:
_spawn_player_ship(star_system, star, planet_barycenter)
return system_data
func _generate_moons(planet: OrbitalBody2D, planet_barycenter: Barycenter, system_data: SystemData):
var num_moons = randi_range(0, int(planet.mass / MOON_MASS / 2.0)) # Heavier planets get more moons
# REFACTOR: Takes OrbitalBody3D
func _generate_moons(planet: OrbitalBody3D, planet_barycenter: Barycenter, system_data: SystemData):
var num_moons = randi_range(0, int(planet.mass / MOON_MASS / 2.0))
num_moons = min(num_moons, MAX_MOONS_PER_PLANET)
var current_orbit_radius = 200.0 # OrbitalMechanics.calculate_simplified_roche_limit(planet) # Start with the first orbit
var current_orbit_radius = 200.0
for i in range(num_moons):
var moon = Moon.new()
@ -78,38 +71,32 @@ func _generate_moons(planet: OrbitalBody2D, planet_barycenter: Barycenter, syste
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)
# REFACTOR: Position in 3D, rotated around Y-axis
moon.position = Vector3(current_orbit_radius, 0, 0).rotated(Vector3.UP, randf_range(0, TAU))
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
# --- 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.
# REFACTOR: Takes OrbitalBody3D
func _spawn_player_ship(star_system: StarSystem, star: OrbitalBody3D, planet_system: Barycenter):
# REFACTOR: Calculate L4/L5 in 3D
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)
# Rotate around the Y-axis
var l4_position = star.global_position + star_to_planet_vec.rotated(Vector3.UP, PI / 3.0)
var l4_velocity = planet_system.linear_velocity.rotated(Vector3.UP, 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()
GameManager.register_ship(ship_instance)
ship_instance.name = "PlayerShip"
star_system.add_child(ship_instance) # Add ship to the root StarSystem node
star_system.add_child(ship_instance)
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)
# REFACTOR: Use look_at to orient the ship in 3D
# We point it "prograde" (in the direction of its velocity)
# We use Vector3.UP as the "up" reference
ship_instance.look_at(ship_instance.global_position + l4_velocity.normalized(), Vector3.UP)
print("Player ship spawned at L4 point of %s" % planet_system.name)

View File

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