WIP Thruster stuff

This commit is contained in:
olof.pettersson
2025-09-18 18:27:17 +02:00
parent d543236f19
commit aef02af6c9
30 changed files with 1182 additions and 125 deletions

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=9 format=3 uid="uid://dogqi2c58qdc0"]
[gd_scene load_steps=8 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/star.tscn" id="2_7mycd"]
@ -6,10 +6,9 @@
[ext_resource type="PackedScene" uid="uid://74ppvxcw8an4" path="res://scenes/moon.tscn" id="4_5vw27"]
[ext_resource type="PackedScene" uid="uid://dm3s33o4xhqfv" path="res://scenes/station.tscn" id="5_kek77"]
[ext_resource type="PackedScene" uid="uid://bawsujtlpmh5r" path="res://scenes/asteroid.tscn" id="6_4c57u"]
[ext_resource type="PackedScene" uid="uid://cm5qsuunboxm3" path="res://scenes/developer_pawn.tscn" id="7_272bh"]
[ext_resource type="PackedScene" uid="uid://ctlw5diis8h1x" path="res://scenes/map_canvas.tscn" id="8_5vw27"]
[ext_resource type="PackedScene" uid="uid://dlck1lyrn1xvp" path="res://scenes/spaceship/spaceship.tscn" id="7_5vw27"]
[node name="Node2D" type="Node2D"]
[node name="StarSystem" type="Node2D"]
script = ExtResource("1_h2yge")
min_planets = 1
max_planets = 4
@ -21,11 +20,5 @@ planet_scene = ExtResource("3_272bh")
moon_scene = ExtResource("4_5vw27")
station_scene = ExtResource("5_kek77")
asteroid_scene = ExtResource("6_4c57u")
spaceship_scene = ExtResource("7_5vw27")
sim_scale = 0.21
[node name="DeveloperPawn" parent="." node_paths=PackedStringArray("map_canvas") instance=ExtResource("7_272bh")]
input_pickable = true
map_canvas = NodePath("../MapCanvas")
[node name="MapCanvas" parent="." node_paths=PackedStringArray("star_system_generator") instance=ExtResource("8_5vw27")]
star_system_generator = NodePath("..")

View File

@ -15,6 +15,12 @@ run/main_scene="uid://dogqi2c58qdc0"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
OrbitalMechanics="*res://scripts/orbital_mechanics.gd"
GameManager="*res://scripts/game_manager.gd"
SignalBus="*res://scripts/signal_bus.gd"
[editor_plugins]
enabled=PackedStringArray()

View File

@ -0,0 +1,59 @@
[gd_scene load_steps=3 format=3 uid="uid://cxpjm8tp3l1j7"]
[ext_resource type="Script" uid="uid://cq2sgw12uj4jl" path="res://scripts/ship/navigation_computer.gd" id="1_ys00n"]
[ext_resource type="Script" uid="uid://uh0k4c3qj4x0" path="res://scripts/map_drawer.gd" id="2_378us"]
[node name="NavigationComputer" type="Area2D"]
script = ExtResource("1_ys00n")
[node name="Sprite2D" type="Sprite2D" parent="."]
[node name="NavigationUI" type="CanvasLayer" parent="."]
unique_name_in_owner = true
[node name="PanelContainer" type="PanelContainer" parent="NavigationUI"]
offset_right = 40.0
offset_bottom = 40.0
[node name="HSplitContainer" type="HSplitContainer" parent="NavigationUI/PanelContainer"]
layout_mode = 2
[node name="MapDrawer" type="Node2D" parent="NavigationUI/PanelContainer/HSplitContainer"]
unique_name_in_owner = true
script = ExtResource("2_378us")
[node name="VBoxContainer" type="VBoxContainer" parent="NavigationUI/PanelContainer/HSplitContainer"]
layout_mode = 2
[node name="InfoLabel" type="Label" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="ShipStatusLabel" type="Label" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="PlanHohmannButton" type="Button" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Hohmann Transfer"
[node name="PlanFastButton" type="Button" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Fast Impulse"
[node name="PlanTorchshipButton" type="Button" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Torchship Burn"
[node name="PlanGravityAssistButton" type="Button" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Grav Assist"
[node name="ExecutePlanButton" type="Button" parent="NavigationUI/PanelContainer/HSplitContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Launch"

View File

@ -0,0 +1,65 @@
[gd_scene load_steps=8 format=3 uid="uid://dlck1lyrn1xvp"]
[ext_resource type="Script" uid="uid://dyqbk4lcx3mhq" path="res://scripts/ship/spaceship.gd" id="1_ae4p7"]
[ext_resource type="Script" uid="uid://c0bx113ifxyh8" path="res://scripts/ship/thruster_controller.gd" id="2_xs8u7"]
[ext_resource type="Script" uid="uid://dx3uerblskj5r" path="res://scripts/ship/fuel_system.gd" id="3_xs8u7"]
[ext_resource type="Script" uid="uid://buyp6t5cppitw" path="res://scripts/ship/life_support.gd" id="4_v0rat"]
[ext_resource type="PackedScene" uid="uid://cxpjm8tp3l1j7" path="res://scenes/spaceship/navigation_computer.tscn" id="5_6nyhl"]
[ext_resource type="Script" uid="uid://cpc34i7o1puq2" path="res://scripts/ship/thruster.gd" id="6_fu1d6"]
[ext_resource type="Script" uid="uid://w1546qtaupd2" path="res://scripts/ship/ship_signal_bus.gd" id="7_yl4tl"]
[node name="Spaceship" type="RigidBody2D"]
can_sleep = false
script = ExtResource("1_ae4p7")
[node name="ThrusterController" type="Node" parent="."]
script = ExtResource("2_xs8u7")
[node name="FuelSystem" type="Node" parent="."]
script = ExtResource("3_xs8u7")
[node name="LifeSupport" type="Node" parent="."]
script = ExtResource("4_v0rat")
[node name="NavigationComputer" parent="." instance=ExtResource("5_6nyhl")]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="MainEngine" type="Node2D" parent="."]
position = Vector2(0, 30)
script = ExtResource("6_fu1d6")
[node name="RCS1" type="Node2D" parent="."]
position = Vector2(-10, -10)
script = ExtResource("6_fu1d6")
max_thrust = 0.001
specific_impulse_isp = 10.0
thrust_direction = Vector2(1, 0)
main_thruster = false
[node name="RCS2" type="Node2D" parent="."]
position = Vector2(-10, 10)
script = ExtResource("6_fu1d6")
max_thrust = 0.001
specific_impulse_isp = 10.0
thrust_direction = Vector2(1, 0)
main_thruster = false
[node name="RCS3" type="Node2D" parent="."]
position = Vector2(10, -10)
script = ExtResource("6_fu1d6")
max_thrust = 0.001
specific_impulse_isp = 10.0
thrust_direction = Vector2(-1, 0)
main_thruster = false
[node name="RCS4" type="Node2D" parent="."]
position = Vector2(10, 10)
script = ExtResource("6_fu1d6")
max_thrust = 0.001
specific_impulse_isp = 10.0
thrust_direction = Vector2(-1, 0)
main_thruster = false
[node name="SignalBus" type="Node" parent="."]
script = ExtResource("7_yl4tl")

View File

@ -0,0 +1,61 @@
[gd_scene load_steps=7 format=3 uid="uid://dlck1lyrn1xvp"]
[ext_resource type="Script" uid="uid://dyqbk4lcx3mhq" path="res://scripts/ship/spaceship.gd" id="1_ae4p7"]
[ext_resource type="Script" uid="uid://c0bx113ifxyh8" path="res://scripts/ship/thruster_controller.gd" id="2_xs8u7"]
[ext_resource type="Script" uid="uid://dx3uerblskj5r" path="res://scripts/ship/fuel_system.gd" id="3_xs8u7"]
[ext_resource type="Script" uid="uid://buyp6t5cppitw" path="res://scripts/ship/life_support.gd" id="4_v0rat"]
[ext_resource type="PackedScene" uid="uid://cxpjm8tp3l1j7" path="res://scenes/spaceship/navigation_computer.tscn" id="5_6nyhl"]
[ext_resource type="Script" uid="uid://cpc34i7o1puq2" path="res://scripts/ship/thruster.gd" id="6_fu1d6"]
[node name="Spaceship" type="RigidBody2D"]
script = ExtResource("1_ae4p7")
[node name="ThrusterController" type="Node" parent="."]
script = ExtResource("2_xs8u7")
[node name="FuelSystem" type="Node" parent="."]
script = ExtResource("3_xs8u7")
[node name="LifeSupport" type="Node" parent="."]
script = ExtResource("4_v0rat")
[node name="NavigationComputer" parent="." instance=ExtResource("5_6nyhl")]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="MainEngine" type="Node2D" parent="."]
position = Vector2(0, 30)
script = ExtResource("6_fu1d6")
thrust_direction = Vector2(0, 1)
[node name="RCS1" type="Node2D" parent="."]
position = Vector2(-10, -10)
script = ExtResource("6_fu1d6")
max_thrust = 10.0
specific_impulse_isp = 10.0
thrust_direction = Vector2(1, 0)
main_thruster = false
[node name="RCS2" type="Node2D" parent="."]
position = Vector2(-10, 10)
script = ExtResource("6_fu1d6")
max_thrust = 10.0
specific_impulse_isp = 10.0
thrust_direction = Vector2(1, 0)
main_thruster = false
[node name="RCS3" type="Node2D" parent="."]
position = Vector2(10, -10)
script = ExtResource("6_fu1d6")
max_thrust = 10.0
specific_impulse_isp = 10.0
thrust_direction = Vector2(-1, 0)
main_thruster = false
[node name="RCS4" type="Node2D" parent="."]
position = Vector2(10, 10)
script = ExtResource("6_fu1d6")
max_thrust = 10.0
specific_impulse_isp = 10.0
thrust_direction = Vector2(-1, 0)
main_thruster = false

View File

@ -4,37 +4,23 @@ extends RigidBody2D
# The celestial body that this body orbits.
@export var primary: CelestialBody
# Real-world gravitational constant.
const G = 10
# This is a placeholder for your pixel art texture.
@export var texture: Texture2D
# The radius of the body, used for drawing and future collision detection.
@export var radius: float = 10.0
# The scaling factor for the simulation. A value of 1.0 means no scaling.
var sim_scale: float = 1.0
# Default color based on body type for visualization.
var body_color: Color = Color.ORANGE_RED
var orbit_radius_real : float = 0.0
var linear_velocity_real : Vector2 = Vector2.ZERO
var global_position_real : Vector2 = Vector2.ZERO
var current_central_force_real : Vector2 = Vector2.ZERO
var direction_to_primary : Vector2 = Vector2.ZERO
var distance_to_primary : float = 0.0
func get_class_name() -> String:
return "CelestialBody"
func _ready() -> void:
# Set the scaled mass based on the real-world mass and the simulation scale.
# The scale is applied to the mass, so a smaller scale means a larger apparent mass.
# We will handle gravity manually, so we set the built-in gravity scale to 0.
gravity_scale = 0.0
@ -59,45 +45,15 @@ func _ready() -> void:
_:
body_color = Color.ORANGE_RED
var is_first_integration : bool = true
# This callback is the correct place to apply custom forces to a RigidBody2D.
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
if primary and is_instance_valid(primary):
current_central_force_real = simple_n_body_grav(primary, self.global_position)
state.apply_central_force(current_central_force_real)
var force = OrbitalMechanics.simple_n_body_grav(self, primary)
state.apply_central_force(force)
# We force a redraw here to update the body's visual representation.
queue_redraw()
func simple_n_body_grav(primary: CelestialBody, global_pos : Vector2) -> Vector2:
var pull = calc_grav_acc_to_object(primary, global_pos)
var inner_primary : CelestialBody = primary
while (inner_primary.primary is CelestialBody):
pull = pull + calc_grav_acc_to_object(inner_primary.primary, global_pos)
inner_primary = inner_primary.primary
return pull
func calc_grav_acc_to_object(primary: CelestialBody, global_pos : Vector2) -> Vector2:
# Get the vector pointing from this body to its primary.
var direction_to_primary = global_pos.direction_to(primary.global_position)
var distance_squared = global_pos.distance_squared_to(primary.global_position)
# Prevent division by zero or a large force if bodies are on top of each other.
if distance_squared > 1.0:
# Calculate the magnitude of the gravitational force using Newton's law.
# We now use the scaled masses, which is consistent with Godot's physics engine.
# F = G * (m1 * m2) / r^2
var force_magnitude = (G * self.mass * primary.mass) / (distance_squared)
# Apply the force in the direction of the primary.
return direction_to_primary * force_magnitude
return Vector2.ZERO
# Override the default drawing function to draw the body.
# This is useful for debugging and visualization.
func _draw() -> void:
@ -109,22 +65,3 @@ func _draw() -> void:
else:
# Otherwise, draw a simple placeholder circle.
draw_circle(Vector2.ZERO, radius, body_color)
# This function should replace your existing one.
func calculate_initial_orbit_real(primary_body: CelestialBody) -> Vector2:
# 1. Use the same pixel distance the physics engine will use.
var distance_pixels = self.global_position.distance_to(primary_body.global_position)
if distance_pixels == 0:
return Vector2.ZERO
# 2. Use the exact same G and primary mass as in _integrate_forces.
# v = sqrt(G * M / r)
var speed_magnitude = sqrt(G * primary_body.mass / distance_pixels)
# 3. Calculate the direction of the orbit (perpendicular to the primary).
var direction_to_self = primary_body.global_position.direction_to(self.global_position)
var perpendicular_direction = Vector2(direction_to_self.y, -direction_to_self.x)
# 4. Combine speed and direction, and add the primary's velocity.
var orbital_velocity = perpendicular_direction * speed_magnitude
return orbital_velocity + primary_body.linear_velocity

View File

@ -30,11 +30,15 @@ func _ready() -> void:
print("ERROR: MapCanvas reference not set on DeveloperPawn.")
set_physics_process(false)
return
if not map_drawer:
map_drawer = map_canvas.map_drawer
if map_drawer is not MapDrawer:
print("ERROR: MapDrawer reference not set on DeveloperPawn.")
set_physics_process(false)
return
# --- NEW: Connect to the map's signal ---
map_drawer.body_selected_for_follow.connect(on_body_selected_for_follow)

27
scripts/game_manager.gd Normal file
View File

@ -0,0 +1,27 @@
# GameManager.gd
extends Node
# This variable will hold the reference to the currently active star system.
var current_star_system : StarSystemGenerator = null
var ships_in_system : Array[Spaceship]
# Any scene that generates a star system will call this function to register itself.
func register_star_system(system_node):
current_star_system = system_node
print("GameManager: Star system registered.")
func register_ship(ship: Spaceship):
if not ships_in_system.has(ship):
ships_in_system.append(ship)
# A helper function for easily accessing the system's data.
func get_system_data():
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 []

View File

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

View File

@ -2,10 +2,10 @@ class_name MapCanvas
extends CanvasLayer
# A reference to the StarSystemGenerator node.
@export var star_system_generator: Node
@export var star_system_generator: StarSystemGenerator
# A reference to the new MapDrawer node.
@export var map_drawer: Node2D
@export var map_drawer: MapDrawer
func _ready() -> void:
# Ensure the generator and drawer references are set.

View File

@ -4,6 +4,9 @@ extends Node2D
# 1. Define the new signal at the top of the script.
signal body_selected_for_follow(body: CelestialBody)
# Add a new signal for the navigation computer
signal body_selected_for_planning(body: CelestialBody)
# The body at the center of the map view.
var focal_body: CelestialBody
# The list of bodies to be rendered in the current view.
@ -11,16 +14,18 @@ var bodies_to_draw: Array[CelestialBody] = []
# The scale used for drawing, stored to be accessible by the input handler.
var draw_scale: float = 1.0
# A reference to the generator, with a setter to initialize the view.
var star_system_generator: StarSystemGenerator:
set(value):
print("Setting star system generator")
star_system_generator = value
# When the reference is set, initialize the view on the star.
#if star_system_generator and star_system_generator.has_generated:
#set_view(star_system_generator.get_system_data().star)
get:
# If we don't have the reference yet, try to get it from the GameManager.
if not star_system_generator:
star_system_generator = GameManager.current_star_system
return star_system_generator
func _process(_delta: float) -> void:
# Ensure we have a valid reference before trying to draw.
if not is_instance_valid(star_system_generator):
return # Wait until the star system is registered.
if not focal_body:
set_view(star_system_generator.get_system_data().star)
@ -46,7 +51,10 @@ func set_view(new_focal_body: CelestialBody) -> void:
if child is CelestialBody:
bodies_to_draw.append(child)
print("Bodies to draw: " + str(bodies_to_draw))
for ship in GameManager.ships_in_system:
if ship is Spaceship and is_instance_valid(ship):
bodies_to_draw.append(ship)
# If the star is the focus, also find and draw the asteroid belts.
if focal_body is Star:
var system_data = star_system_generator.get_system_data()
@ -54,30 +62,22 @@ func set_view(new_focal_body: CelestialBody) -> void:
# We'll handle drawing belts separately in the _draw function.
pass
# --- Input Handling ---
# In the _unhandled_input function, change the click logic
func _unhandled_input(event: InputEvent) -> void:
if not focal_body:
return
# Instead of checking for Shift-click to follow,
# just use a normal click to select a planning target.
if event is InputEventMouseButton and event.is_pressed():
# Handle zoom out with Right Click
if event.button_index == MOUSE_BUTTON_RIGHT:
if is_instance_valid(focal_body.primary):
set_view(focal_body.primary)
get_viewport().set_input_as_handled()
# Handle zoom in and follow with Left Click
if event.button_index == MOUSE_BUTTON_LEFT:
var clicked_body = _find_body_at_pos(event.position)
if clicked_body:
if event.is_shift_pressed():
# --- NEW: On Shift-Click, emit the signal for the pawn ---
emit_signal("body_selected_for_follow", clicked_body)
else:
# On a normal click, just zoom in as before.
set_view(clicked_body)
if clicked_body and event.is_shift_pressed():
set_view(clicked_body)
elif clicked_body:
emit_signal("body_selected_for_planning", clicked_body)
get_viewport().set_input_as_handled()
# This function finds which body was clicked. It's refactored from your old input code.
@ -109,7 +109,7 @@ func _draw() -> void:
# Add a default zoom level if focused on a body with no children.
if max_distance == 0.0:
max_distance = 5.0e8 # An arbitrary distance for a nice solo view.
max_distance = 100000 # An arbitrary distance for a nice solo view.
var system_diameter = max_distance * 2.0
draw_scale = min(get_viewport_size().x, get_viewport_size().y) / system_diameter
@ -120,7 +120,7 @@ func _draw() -> void:
for belt in star_system_generator.get_system_data().belts:
var relative_radius = belt.centered_radius - focal_body.global_position.length()
var scaled_radius = relative_radius * draw_scale
draw_circle(get_viewport_center(), scaled_radius, Color.WHITE, false)
draw_circle(get_viewport_center(), scaled_radius, Color(Color.WHITE, 0.3), false)
# 3. Draw each celestial body relative to the focal body.
for body in bodies_to_draw:
@ -149,7 +149,7 @@ func _draw() -> void:
# Draw orbit line for all children
if body != focal_body and body is not Asteroid:
#draw_circle(get_viewport_center(), scaled_pos.distance_to(get_viewport_center()), Color(Color.WHITE, 0.2), false)
draw_circle(get_viewport_center(), scaled_pos.distance_to(get_viewport_center()), Color(Color.WHITE, 0.2), false)
draw_string(ThemeDB.fallback_font, scaled_pos + Vector2(radius + 5, 5), body.name, HORIZONTAL_ALIGNMENT_LEFT, -1, ThemeDB.fallback_font_size, color)
# Draw the body itself.
@ -162,7 +162,6 @@ func _draw() -> void:
draw_arrow(scaled_pos, scaled_pos + velocity, Color.GREEN)
draw_arrow(scaled_pos, scaled_pos + force, Color.RED)
# Calculates an array of points representing a body's future orbital path.
func _calculate_orbital_path(body_to_trace: CelestialBody) -> PackedVector2Array:
if not is_instance_valid(body_to_trace) or not is_instance_valid(body_to_trace.primary):
@ -201,7 +200,6 @@ func _calculate_relative_orbital_path(body_to_trace: CelestialBody) -> PackedVec
# --- Initial State ---
var primary = body_to_trace.primary
var G = body_to_trace.G
var primary_mass = primary.mass
var body_mass = body_to_trace.mass
@ -216,7 +214,7 @@ func _calculate_relative_orbital_path(body_to_trace: CelestialBody) -> PackedVec
return PackedVector2Array()
var v_sq = ghost_relative_vel.length_squared()
var mu = G * primary_mass # Standard Gravitational Parameter
var mu = OrbitalMechanics.G * primary_mass # Standard Gravitational Parameter
# 1. Calculate the specific orbital energy. Negative energy means it's a stable orbit.
var specific_energy = v_sq / 2.0 - mu / r_magnitude
@ -251,7 +249,7 @@ func _calculate_relative_orbital_path(body_to_trace: CelestialBody) -> PackedVec
# Direction is simply towards the origin.
var direction = -ghost_relative_pos.normalized()
var force_magnitude = (G * primary_mass * body_mass) / distance_sq
var force_magnitude = (OrbitalMechanics.G * primary_mass * body_mass) / distance_sq
var force_vector = direction * force_magnitude
var acceleration = force_vector / body_mass
@ -277,16 +275,17 @@ func _get_body_draw_radius(body: CelestialBody) -> float:
elif body is Moon: return 6.0
elif body is Station: return 7.0
elif body is Asteroid: return 4.0
elif body is Spaceship: return 8.0
return 5.0
func _get_body_draw_color(body: CelestialBody) -> Color:
match body.get_class_name():
"Star": return Color.GOLD
"Planet": return Color.BLUE
"Moon": return Color.PURPLE
"Station": return Color.WHITE
"Asteroid": return Color.BROWN
return Color.ORANGE_RED
if body is Star: return Color.GOLD
elif body is Planet: return Color.BLUE
elif body is Moon: return Color.PURPLE
elif body is Station: return Color.WHITE
elif body is Asteroid: return Color.BROWN
elif body is Spaceship: return Color.OLIVE_DRAB
return Color.ORANGE_RED
func draw_arrow(start: Vector2, end: Vector2, color: Color):
draw_line(start, end, color, 2.0)

View File

@ -0,0 +1,51 @@
# OrbitalMechanics.gd
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
# Calculates the force of gravity exerted by a primary on an orbiter.
func calculate_gravitational_force(orbiter: RigidBody2D, primary: RigidBody2D) -> 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: RigidBody2D, orbiter : RigidBody2D) -> 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: RigidBody2D, primary: RigidBody2D) -> Vector2:
if not is_instance_valid(primary):
return Vector2.ZERO
var distance = orbiter.global_position.distance_to(primary.global_position)
if distance == 0:
return Vector2.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)
# The final velocity must include the primary's own velocity.
return (perpendicular_direction * speed_magnitude) + primary.linear_velocity

View File

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

View File

@ -0,0 +1,27 @@
# FuelSystem.gd
class_name FuelSystem
extends Node
# Signal to notify the parent ship that its total mass has changed
signal fuel_mass_changed
# A dictionary to hold different types of fuel and their amounts
var fuel_tanks: Dictionary = {
"ChemicalFuel": 1000.0, # in kg
"XenonGas": 50.0 # in kg
}
func request_fuel(resource_name: String, amount: float) -> bool:
if fuel_tanks.has(resource_name) and fuel_tanks[resource_name] >= amount:
fuel_tanks[resource_name] -= amount
fuel_mass_changed.emit()
return true
else:
print("Out of ", resource_name, "!")
return false
func get_total_fuel_mass() -> float:
var total_mass: float = 0.0
for amount in fuel_tanks.values():
total_mass += amount
return total_mass

View File

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

View File

@ -0,0 +1,29 @@
# LifeSupport.gd
class_name LifeSupport
extends Node
# Signal to notify the parent ship of a breach and the resulting thrust vector
signal hull_breach_detected(breach_position: Vector2, force_vector: Vector2)
var internal_pressure: float = 101.0 # in kPa
var is_breached: bool = false
func check_for_breach(damage_position: Vector2):
# Simple logic: any damage has a chance to cause a breach
if randf() > 0.7 and not is_breached:
is_breached = true
print("Warning! Hull breach detected!")
# The force vector is opposite the direction from the ship's center to the breach
var ship_center = get_parent().global_position
var force_direction = (ship_center - damage_position).normalized()
hull_breach_detected.emit(damage_position, force_direction)
func _process(delta: float):
if is_breached and internal_pressure > 0:
# Atmosphere vents to space over time
internal_pressure -= 5.0 * delta
if internal_pressure <= 0:
internal_pressure = 0
print("Atmosphere depleted.")

View File

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

View File

@ -0,0 +1,334 @@
# NavigationComputer.gd
extends Node
@onready var ship: Spaceship = %Spaceship
var ship_signal_bus: ShipSignalBus
# --- Node References ---
@onready var navigation_ui: CanvasLayer = %NavigationUI
@onready var map_drawer: MapDrawer = %MapDrawer
@onready var info_label: Label = %InfoLabel
@onready var ship_status_label: Label = %ShipStatusLabel
# Buttons for different maneuvers
@onready var plan_hohmann_button: Button = %PlanHohmannButton
@onready var plan_fast_button: Button = %PlanFastButton
@onready var plan_torchship_button: Button = %PlanTorchshipButton
@onready var plan_gravity_assist_button: Button = %PlanGravityAssistButton
@onready var execute_plan_button: Button = %ExecutePlanButton
# How many seconds before a burn we should start orienting the ship.
const PRE_BURN_ORIENTATION_SECONDS = 30.0 # Give a larger window for the new logic
const ROTATION_SAFETY_BUFFER = 10.0 # Seconds to ensure rotation finishes before burn
# A flag to make sure we only send the signal once per maneuver
var rotation_plan_sent = false
# --- State Management ---
enum State { IDLE, PLANNING, WAITING, EXECUTING }
var current_state: State = State.IDLE
# --- Navigation Data ---
var source_body: CelestialBody
var target_body: CelestialBody
var current_plan # Can be an array of ImpulsiveBurn or a ContinuousBurnPlan
# --- Inner classes to hold maneuver data ---
class ImpulsiveBurn:
var delta_v_magnitude: float
var wait_time: float = 0.0
var burn_duration: float
var desired_rotation_rad: float # The world rotation the ship needs to be in
class ContinuousBurnPlan:
var total_travel_time: float
var acceleration_time: float
var initial_burn_direction: Vector2 # The world-space direction vector for the burn
func _ready() -> void:
# Connect to the global signal from the SignalBus
SignalBus.map_mode_toggled.connect(on_map_mode_toggled)
if is_instance_valid(ship):
ship_signal_bus = ship.signal_bus
# Ensure the UI starts hidden
if navigation_ui:
navigation_ui.hide()
# Connect UI signals
map_drawer.body_selected_for_planning.connect(_on_target_selected)
plan_hohmann_button.pressed.connect(_on_plan_hohmann_pressed)
plan_fast_button.pressed.connect(_on_plan_fast_pressed)
plan_torchship_button.pressed.connect(_on_plan_torchship_pressed)
plan_gravity_assist_button.pressed.connect(_on_plan_gravity_assist_pressed)
execute_plan_button.pressed.connect(_on_execute_plan_pressed)
ship_status_label.text = ""
update_ui()
# This function is called whenever any node in the game emits the "map_mode_toggled" signal.
func on_map_mode_toggled():
if navigation_ui:
# Toggle the visibility of the UI screen
navigation_ui.visible = not navigation_ui.visible
func _process(delta: float) -> void:
_update_ship_status_label()
if current_state == State.PLANNING and current_plan:
if current_plan is Array and not current_plan.is_empty():
var first_burn = current_plan[0]
# The plan is not locked in, but we can see the window approaching.
first_burn.wait_time -= delta
var time_str = _format_seconds_to_mmss(first_burn.wait_time)
info_label.text = "Optimal window in: %s.\nPress Execute to lock in plan." % time_str
if first_burn.wait_time < 0:
# If the window is missed during planning, mark the plan as stale.
info_label.text = "Transfer window missed. Please plan a new maneuver."
current_plan = null
update_ui()
if current_state == State.WAITING and current_plan:
if current_plan is Array and not current_plan.is_empty():
var next_burn: ImpulsiveBurn = current_plan[0]
next_burn.wait_time -= delta
var time_str = _format_seconds_to_mmss(next_burn.wait_time)
info_label.text = "Time to burn: %s" % time_str
# --- NEW: Emit the rotation plan ---
# When we are inside the orientation window, and haven't sent the plan yet...
if not rotation_plan_sent:
# The time allowed for rotation is the time we have left, minus a safety buffer.
var time_for_rotation = next_burn.wait_time - ROTATION_SAFETY_BUFFER
# Tell the thruster controller to handle it.
ship.signal_bus.emit_signal("timed_rotation_commanded", next_burn.desired_rotation_rad, time_for_rotation)
rotation_plan_sent = true # Mark the plan as sent
if next_burn.wait_time <= 0:
_execute_maneuver()
# The EXECUTING state would be handled by a dedicated autopilot script/node
func update_ui():
execute_plan_button.disabled = (current_plan == null or current_state != State.PLANNING)
if current_state == State.PLANNING:
# Show available plans based on engine type
# TODO change UI to show recommendations based on thruster type
#if installed_engine is ChemicalThruster:
plan_hohmann_button.show()
plan_fast_button.show()
plan_gravity_assist_button.show()
#elif installed_engine is IonDrive:
plan_torchship_button.show()
# --- Signal Handlers ---
func _on_target_selected(body: CelestialBody):
if current_state == State.IDLE or current_state == State.PLANNING:
target_body = body
# Assume ship is orbiting the target's primary (e.g., the star)
source_body = ship.primary
current_plan = null
current_state = State.PLANNING
info_label.text = "Target: %s. Select a maneuver." % target_body.name
update_ui()
func _on_execute_plan_pressed():
if current_plan:
current_state = State.WAITING
update_ui()
# --- Planning Function Calls ---
func _on_plan_hohmann_pressed():
current_plan = _calculate_hohmann_transfer(source_body, target_body)
# TODO: map_drawer.draw_planned_trajectory(...)
update_ui()
func _on_plan_fast_pressed():
# For simplicity, a "fast" transfer is a Hohmann with a 25% larger transfer orbit
current_plan = _calculate_hohmann_transfer(source_body, target_body, 1.25)
# TODO: map_drawer.draw_planned_trajectory(...)
update_ui()
func _on_plan_torchship_pressed():
current_plan = _calculate_brachistochrone_transfer()
# TODO: map_drawer.draw_planned_trajectory(...)
update_ui()
func _on_plan_gravity_assist_pressed():
info_label.text = "Gravity Assist calculation is highly complex and not yet implemented."
print("Placeholder for Gravity Assist logic.")
# --- Calculation and Execution ---
func _calculate_hohmann_transfer(source_planet: CelestialBody, target_planet: CelestialBody, transfer_boost_factor: float = 1.0) -> Array:
# Get the central star safely from the GameManager.
var ship_current_primary = ship.primary
var star = GameManager.get_system_data().star
if not is_instance_valid(star):
print("Hohmann Error: Could not find star in GameManager.")
return []
# This maneuver requires the ship and target to orbit the same star.
if ship_current_primary != star or target_planet.primary != star:
info_label.text = "Invalid Transfer: Ship and target must orbit the same star."
return []
# mu (μ): The Standard Gravitational Parameter of the star. It's G * M, a constant that simplifies calculations.
var mu = OrbitalMechanics.G * star.mass
# r1: The ship's current orbital radius (distance from the star).
var r1 = ship.global_position.distance_to(star.global_position)
# r2: The target planet's orbital radius.
var r2 = target_planet.global_position.distance_to(star.global_position)
# --- Hohmann Transfer Calculations ---
# v_source_orbit: The ship's current circular orbital speed.
var v_source_orbit = sqrt(mu / r1)
# v_target_orbit: The target planet's circular orbital speed.
var v_target_orbit = sqrt(mu / r2)
# a_transfer: The semi-major axis (average radius) of the elliptical transfer orbit.
var a_transfer = (r1 + r2) / 2.0 * transfer_boost_factor
# v_transfer_periapsis: The required speed at the start of the transfer (periapsis) to get onto the ellipse.
var v_transfer_periapsis = sqrt(mu * ((2.0 / r1) - (1.0 / a_transfer)))
# v_transfer_apoapsis: The speed the ship will have when it arrives at the end of the transfer (apoapsis).
var v_transfer_apoapsis = sqrt(mu * ((2.0 / r2) - (1.0 / a_transfer)))
# delta_v1: The first burn. The change in speed needed to go from the source orbit to the transfer orbit.
var delta_v1 = v_transfer_periapsis - v_source_orbit
# delta_v2: The second burn. The change in speed needed to go from the transfer orbit to the target orbit.
var delta_v2 = v_target_orbit - v_transfer_apoapsis
# time_of_flight: Half the period of the elliptical transfer orbit (Kepler's 3rd Law).
var time_of_flight = PI * sqrt(pow(a_transfer, 3) / mu)
# --- Launch Window (Phase Angle) Calculations ---
# ang_vel_target: The angular speed of the target planet (in radians per second).
var ang_vel_target = sqrt(mu / pow(r2, 3))
# travel_angle: The angle the target planet will travel through during the ship's flight time.
var travel_angle = ang_vel_target * time_of_flight
# required_phase_angle: The starting angle needed between the ship and target for a successful intercept.
var required_phase_angle = PI - travel_angle
# vec_to_ship/target: Direction vectors from the star to the ship and target.
var vec_to_ship = (ship.global_position - star.global_position).normalized()
var vec_to_target = (target_planet.global_position - star.global_position).normalized()
# current_phase_angle: The angle between the ship and target right now.
var current_phase_angle = vec_to_ship.angle_to(vec_to_target)
# ang_vel_ship: The ship's current angular speed.
var ang_vel_ship = sqrt(mu / pow(r1, 3))
# relative_ang_vel: How quickly the ship is catching up to (or falling behind) the target.
var relative_ang_vel = ang_vel_ship - ang_vel_target
# angle_to_wait: The angular distance the ship needs to "wait" for alignment.
var angle_to_wait = current_phase_angle - required_phase_angle
# wait_time: The final calculated time in seconds to wait for the optimal launch window.
var wait_time = abs(angle_to_wait / relative_ang_vel)
# --- Final Plan Assembly ---
# Calculate burn durations
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
var burn_duration1 = delta_v1 / acceleration
var burn_duration2 = delta_v2 / acceleration
var plan = []
var burn1 = ImpulsiveBurn.new()
burn1.delta_v_magnitude = delta_v1
burn1.wait_time = wait_time
burn1.burn_duration = burn_duration1
# The desired rotation is the angle of the ship's prograde (tangential) velocity vector.
burn1.desired_rotation_rad = ship.linear_velocity.angle()
plan.append(burn1)
var burn2 = ImpulsiveBurn.new()
burn2.delta_v_magnitude = delta_v2
burn2.wait_time = time_of_flight - burn_duration1
burn2.burn_duration = burn_duration2
# The desired rotation for burn 2 is the tangential direction at the target orbit.
burn2.desired_rotation_rad = (target_planet.global_position - star.global_position).orthogonal().angle()
plan.append(burn2)
info_label.text = "Hohmann Plan:\nWait: %d s\nBurn 1: %.1f m/s (%.1f s)" % [wait_time, delta_v1, burn_duration1]
return plan
func _calculate_brachistochrone_transfer() -> ContinuousBurnPlan:
var distance = ship.global_position.distance_to(target_body.global_position)
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
if acceleration == 0: return null
# d = 1/2 * a * t^2 => t = sqrt(2d/a). We do this twice (accel/decel).
var time_for_half_journey = sqrt(distance / acceleration)
var plan = ContinuousBurnPlan.new()
plan.total_travel_time = 2 * time_for_half_journey
plan.acceleration_time = time_for_half_journey
plan.required_acceleration = acceleration
info_label.text = "Torchship Trajectory Calculated.\nTravel Time: %d s" % plan.total_travel_time
return plan
func _execute_maneuver():
if current_state != State.WAITING or not current_plan: return
current_state = State.EXECUTING
var burn: ImpulsiveBurn = current_plan.pop_front()
# Tell the controller to start burning. Orientation is already handled.
ship.thruster_controller.autopilot_start_burn(burn.burn_duration)
# Set up for the next leg of the journey or finish
if not current_plan.is_empty():
# The next "wait_time" is the coasting period between burns.
current_state = State.WAITING
# Reset the flag here, preparing the system for the *next* burn's rotation command.
rotation_plan_sent = false
else:
current_state = State.IDLE
current_plan = null
update_ui()
func _update_ship_status_label():
if not is_instance_valid(ship):
ship_status_label.text = "NO SHIP DATA"
return
# Build an array of strings for each line of the display
var status_lines = []
# Get rotation data from the ship
var rotation_deg = rad_to_deg(ship.rotation)
var angular_vel_dps = rad_to_deg(ship.angular_velocity)
status_lines.append("Rotation: %.1f deg" % rotation_deg)
status_lines.append("Ang. Vel.: %.5f deg/s" % angular_vel_dps)
# Get burn data from the thruster controller
var burn_time = ship.thruster_controller.current_burn_time_remaining
var thrust_force = ship.thruster_controller.current_thrust_force
status_lines.append("Burn Time: %.1f s" % burn_time)
status_lines.append("Thrust: %.5f N" % thrust_force)
# Join the lines with a newline character and update the label
ship_status_label.text = "\n".join(status_lines)
# Helper function to format a float of seconds into a M:SS string
func _format_seconds_to_mmss(seconds_float: float) -> String:
if seconds_float < 0:
seconds_float = 0
var total_seconds = int(seconds_float)
var minutes = total_seconds / 60
var seconds = total_seconds % 60
# "%02d" formats the seconds with a leading zero if needed (e.g., 05)
return "%d:%02d" % [minutes, seconds]

View File

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

View File

@ -0,0 +1,14 @@
class_name ShipSignalBus
extends Node
# --- Navigation & Maneuvering Events ---
# Emitted when a maneuver plan is calculated.
signal maneuver_planned(plan)
# Emitted to command the start of a timed rotation.
signal timed_rotation_commanded(target_rotation_rad, time_window)
# --- Thruster & Ship Status Events ---
# Emitted when the main engine starts or stops firing.
signal main_engine_state_changed(is_firing: bool, total_thrust: float)
# Emitted when RCS thrusters are fired.
signal rcs_state_changed(is_firing: bool, torque: float)

View File

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

135
scripts/ship/spaceship.gd Normal file
View File

@ -0,0 +1,135 @@
# Spaceship.gd
class_name Spaceship
extends CelestialBody
@export var ship_name: String = "Stardust Drifter"
@export var dry_mass: float = 1000.0 # Mass of the ship without fuel/cargo (in kg)
@export var hull_integrity: float = 100.0
@onready var camera: Camera2D = $Camera2D
@export_category("Camera")
@export var zoom_speed: float = 1.0
var current_time_scale: float = Engine.time_scale
# --- Node References to Modular Systems ---
@onready var signal_bus: ShipSignalBus = $SignalBus
@onready var thruster_controller: ThrusterController = $ThrusterController
@onready var fuel_system = $FuelSystem
@onready var life_support = $LifeSupport
@onready var navigation_computer = $NavigationComputer
# @onready var weapon_system = $WeaponSystem
# @onready var power_grid = $PowerGrid
func _ready() -> void:
GameManager.register_ship(self)
linear_damp = 0
angular_damp_mode = RigidBody2D.DAMP_MODE_REPLACE
angular_damp = 0
# Connect to signals from our subsystems
fuel_system.fuel_mass_changed.connect(_on_fuel_mass_changed)
life_support.hull_breach_detected.connect(_on_hull_breach)
# Set the initial mass of the RigidBody
update_total_mass()
# Give the navigation computer a reference to this ship
if navigation_computer:
navigation_computer.ship = self
camera.make_current()
func _process(delta: float):
# The queue_redraw() call tells Godot that this object needs to be redrawn
# on the next frame, which then calls the _draw() function.
queue_redraw()
func _draw() -> void:
draw_circle(Vector2.ZERO, 5.0, Color.CORAL)
# This physics callback is for forces acting ON the ship, like damage
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
# The thruster_controller will apply its own forces directly to the ship's state.
# We can use this function for other things, like forces from hull breaches.
if is_instance_valid(primary):
# The spaceship is also affected by gravity, calculated by our library
var force = OrbitalMechanics.calculate_gravitational_force(self, primary)
state.apply_central_force(force)
# This function will now handle all non-UI input for the player-controlled ship.
func _unhandled_input(event: InputEvent) -> void:
# --- Map Toggling ---
if event.is_action_pressed("ui_map_mode"):
# Instead of showing/hiding a node directly, we broadcast our intent.
# The NavigationComputer will be listening for this global signal.
SignalBus.emit_signal("map_mode_toggled")
# --- Camera Zoom (from DeveloperPawn) ---
if event is InputEventMouseButton:
if camera:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
camera.zoom -= Vector2(zoom_speed, zoom_speed)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.zoom += Vector2(zoom_speed, zoom_speed)
camera.zoom.x = clamp(camera.zoom.x, 0.01, 250.0)
camera.zoom.y = clamp(camera.zoom.y, 0.01, 250.0)
# --- Time Scale Controls (from DeveloperPawn) ---
if event.is_action_pressed("time_increase"):
var new_value = min(current_time_scale * 1.2, 1000)
current_time_scale = clamp(new_value, 0.5, 1000)
Engine.time_scale = current_time_scale
elif event.is_action_pressed("time_decrease"):
var new_value = max(current_time_scale * 0.833, 0.1)
current_time_scale = clamp(new_value, 0.5, 1000)
Engine.time_scale = current_time_scale
elif event.is_action_pressed("time_reset"):
Engine.time_scale = 1.0
# --- Public API for Ship Management ---
func get_total_mass() -> float:
return self.mass
# Call this to take damage. Damage can have a position for breach effects.
func take_damage(amount: float, damage_position: Vector2):
hull_integrity -= amount
print("%s hull integrity at %.1f%%" % [ship_name, hull_integrity])
if hull_integrity <= 0:
destroy_ship()
else:
# Check if the hit caused a hull breach
life_support.check_for_breach(damage_position)
func destroy_ship():
print("%s has been destroyed!" % ship_name)
queue_free()
# --- Signal Handlers ---
func _on_fuel_mass_changed():
# Update the ship's total mass when fuel is consumed or added
update_total_mass()
func _on_hull_breach(breach_position: Vector2, force_vector: Vector2):
# A hull breach applies a continuous force at a specific point
# For simplicity, we can apply it as a central force and torque here
var force = force_vector * 100 # Scale the force
apply_central_force(force)
# Calculate torque: Torque = r x F (cross product of position vector and force)
var position_relative_to_center = breach_position - self.global_position
var torque = position_relative_to_center.cross(force)
apply_torque(torque)
func update_total_mass():
var total_mass = dry_mass + fuel_system.get_total_fuel_mass()
self.inertia = total_mass / 1000
self.mass = total_mass / 1000

View File

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

79
scripts/ship/thruster.gd Normal file
View File

@ -0,0 +1,79 @@
# Thruster.gd
class_name Thruster
extends Node2D
# Max force the thruster can produce (in scaled Newtons).
@export var max_thrust: float = 0.1
# Engine efficiency. Higher is better (more "bang for your buck").
# Measures how much momentum change you get per unit of fuel.
@export var specific_impulse_isp: float = 300.0
# The type of fuel resource this thruster consumes.
@export var fuel_resource_name: String = "ChemicalFuel"
@export var thrust_direction: Vector2 = Vector2(0, -1)
@export var main_thruster: bool = true
# A state variable to track if the thruster is active
var is_firing: bool = false
# Get a reference to the parent ship.
@onready var ship: Spaceship = get_parent()
func _ready() -> void:
# This thruster announces its existence to the whole scene tree.
add_to_group("ship_thrusters")
# This function calculates how much fuel is needed for a given thrust level and duration.
func calculate_fuel_consumption(thrust_force: float, delta_time: float) -> float:
if thrust_force <= 0: return 0.0
# Standard rocket equation using Isp. g0 is standard gravity (9.81).
var mass_flow_rate = thrust_force / (specific_impulse_isp * 9.81)
return mass_flow_rate * delta_time
# --- Public Methods ---
# The controller calls this ONCE to activate the thruster.
func turn_on():
is_firing = true
# The controller calls this ONCE to deactivate the thruster.
func turn_off():
is_firing = false
# --- Godot Physics Callback ---
func _physics_process(delta: float):
# If the thruster is in the "firing" state, it applies its own force.
if is_firing:
# Calculate the force vector in the ship's local space.
var force_local_vec = thrust_direction * max_thrust
# Convert the local force to world space for the physics engine.
var force_world_vec = force_local_vec.rotated(ship.rotation)
# Apply the force at this thruster's specific position.
ship.apply_force(force_world_vec, self.position)
# Also, ensure the visual effect is running
queue_redraw()
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 = -thrust_direction
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
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]))

View File

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

View File

@ -0,0 +1,194 @@
# ThrusterController.gd
class_name ThrusterController
extends Node
# The main ship body this controller will act upon
@onready var ship: Spaceship = get_parent()
var ship_signal_bus: ShipSignalBus
# References to the installed thruster scenes
@onready var main_engines: Array[Thruster] = []
@onready var rcs_thrusters: Array[Thruster] = []
var current_burn_time_remaining: float = 0.0
var current_thrust_force: float = 0.0
# --- Autopilot Constants ---
#const ROTATION_POWER = 50000.0 # Torque applied by RCS
# --- Autopilot State ---
var is_orienting: bool = false
var is_burning: bool = false
var target_rotation_rad: float = 0.0
func _ready() -> void:
# Wait one frame to ensure all thrusters have run their _ready() function.
await get_tree().process_frame
if is_instance_valid(ship):
ship_signal_bus = ship.signal_bus
# Connect to the local bus signals
ship_signal_bus.timed_rotation_commanded.connect(_on_rotation_maneuver_planned)
# Get all nodes in the "ship_thrusters" group that are children of our ship.
var thruster_nodes = get_tree().get_nodes_in_group("ship_thrusters")
for thruster : Thruster in thruster_nodes:
# Ensure we only register thrusters that belong to this ship.
if thruster.main_thruster:
main_engines.append(thruster)
else:
rcs_thrusters.append(thruster)
print("ThrusterController registered via group: ", thruster.name)
func _physics_process(delta: float) -> void:
# The controller now actively manages its state every physics frame.
#apply_rotational_thrust(1.0)
if is_burning:
_perform_main_burn()
# --- Private Worker Functions ---
func _on_rotation_maneuver_planned(target_rotation_rad: float, time_window: float):
time_window = min(time_window, 30.0)
print("AUTOPILOT: Received fuel-optimal rotation plan. Target: %.2f rad in %.2f s." % [target_rotation_rad, time_window])
var angle_to_turn = shortest_angle_between(ship.rotation, target_rotation_rad)
# 1. Determine the maximum possible torque we can generate.
var max_torque = _get_total_possible_torque(sign(angle_to_turn))
if max_torque == 0:
print("AUTOPILOT ERROR: No thrusters available for this rotation.")
return
# 2. Calculate the required burn time and angular velocity.
# The physics formulas are: Δθ = ω * t_coast and τ = I * Δω / t_burn
# We can simplify this to find the required burn time.
var burn_time = time_window / 2.0 - sqrt((time_window * time_window) / 4.0 - (abs(angle_to_turn) * ship.inertia) / max_torque)
print("AUTOPILOT: Max torque for rotational manuever %f", max_torque)
if burn_time < 0 or is_nan(burn_time):
print("AUTOPILOT WARNING: Cannot complete rotation in the given time window. Turning at max speed.")
# Fallback: Just burn for half the time and decelerate for the other half.
burn_time = time_window / 2.0
var coast_time = time_window - (2 * burn_time)
# 3. Execute the three-phase maneuver.
print("AUTOPILOT: Burn-Coast-Burn plan: Burn %.2fs, Coast %.2fs, Burn %.2fs" % [burn_time, coast_time, burn_time])
# --- ACCELERATION BURN ---
print("AUTOPILOT: Burn 1 started")
apply_rotational_thrust(sign(angle_to_turn) * max_torque)
await get_tree().create_timer(burn_time).timeout
for thruster in rcs_thrusters:
thruster.turn_off()
print("AUTOPILOT: Burn 1 complete")
# --- COASTING PHASE ---
print("AUTOPILOT: Rotation coasting")
await get_tree().create_timer(coast_time).timeout
# --- DECCELERATION BURN ---
print("AUTOPILOT: Burn 2 started")
apply_rotational_thrust(sign(angle_to_turn) * -max_torque)
await get_tree().create_timer(burn_time).timeout
print("AUTOPILOT: Burn 2 complete")
for thruster in rcs_thrusters:
thruster.turn_off()
print("AUTOPILOT: Fuel-optimal rotation maneuver complete.")
func _perform_main_burn():
current_thrust_force = main_engine_max_thrust() # Update public variable
for engine in main_engines:
var force_vector = Vector2.UP.rotated(ship.rotation) * -engine.max_thrust
ship.apply_central_force(force_vector)
# Applies forces from the correct thrusters to achieve a desired torque.
func apply_rotational_thrust(desired_torque: float):
if not is_instance_valid(ship):
return
# Iterate through all available RCS thrusters
for thruster in rcs_thrusters:
# 1. Get the thruster's position relative to the ship's center. This is its local position.
var thruster_local_pos = thruster.position
# 2. Calculate the force this thruster produces, also in LOCAL space.
var force_local_vec = thruster.thrust_direction * thruster.max_thrust
# 3. Calculate the torque in LOCAL space. This is now a valid calculation.
var produced_torque = thruster_local_pos.cross(force_local_vec)
# 4. Decide whether to fire the thruster. This check will now work correctly.
if sign(produced_torque) == sign(desired_torque):
thruster.turn_on()
else:
thruster.turn_off()
func main_engine_max_thrust():
return main_engines.reduce(func(thrust, engine : Thruster): return thrust + engine.max_thrust, 0.0)
# --- NEW: High-Level Autopilot Commands ---
func autopilot_start_orientation(rotation_rad: float):
print("AUTOPILOT: Receiving command to orient to %.2f rad." % rotation_rad)
target_rotation_rad = rotation_rad
is_orienting = true
func autopilot_start_burn(duration: float):
print("AUTOPILOT: Receiving command to burn for %.2f s." % duration)
is_burning = true
# We can use a timer to stop the burn automatically.
get_tree().create_timer(duration).timeout.connect(autopilot_stop_burn)
func autopilot_stop_burn():
is_burning = false
print("AUTOPILOT: Main engine burn complete.")
func autopilot_stop_all_maneuvers():
is_orienting = false
is_burning = false
# --- Private Autopilot Helper Functions ---
# Fires all main engines for a specified duration.
func _fire_main_engine(duration: float):
print("AUTOPILOT: Firing main engine for %.2f seconds." % duration)
var timer = duration
is_burning = true # Make sure this is set if not already
while timer > 0:
# Apply force from all main engines
for engine in main_engines:
var force_vector = Vector2.UP.rotated(ship.rotation) * -engine.max_thrust
ship.apply_central_force(force_vector)
timer -= get_physics_process_delta_time()
current_burn_time_remaining = timer # Update public variable
await get_tree().physics_frame
print("AUTOPILOT: Main engine burn complete.")
# Calculates the shortest angle between two angles (in radians).
# The result will be between -PI and +PI. The sign indicates the direction.
func shortest_angle_between(from_angle: float, to_angle: float) -> float:
var difference = fposmod(to_angle - from_angle, TAU)
if difference > PI:
return difference - TAU
else:
return difference
# Calculates the total torque available from all thrusters for a given direction.
func _get_total_possible_torque(direction: int) -> float:
var total_torque: float = 0.0
for thruster in rcs_thrusters:
var r = thruster.position
var force_local_vec = thruster.thrust_direction * thruster.max_thrust
var produced_torque = r.cross(force_local_vec)
if sign(produced_torque) == direction:
total_torque += abs(produced_torque)
return total_torque

View File

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

11
scripts/signal_bus.gd Normal file
View File

@ -0,0 +1,11 @@
extends Node
# Signal emitted when the player requests to toggle the map view.
signal map_mode_toggled
# Signal emitted from the map when a body is selected to be followed.
# It passes the selected CelestialBody as an argument.
signal follow_target_selected(body: CelestialBody)
# Emitted by the NavComputer to command a timed rotation.
signal rotation_maneuver_planned(target_rotation_rad: float, time_window_seconds: float)

View File

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

View File

@ -34,6 +34,7 @@ extends Node2D
@export var moon_scene: PackedScene
@export var station_scene: PackedScene
@export var asteroid_scene: PackedScene
@export var spaceship_scene: PackedScene
# A scaling parameter to convert real-world units to game units.
# 1 meter = sim_scale game units.
@ -88,7 +89,6 @@ func _create_star() -> RigidBody2D:
# Set the star's properties.
star_instance.orbit_radius_real = 0.0
star_instance.mass = SUN_MASS
star_instance.linear_velocity_real = Vector2.ZERO
star_instance.modulate = Color("ffe066") # Yellow
return star_instance
@ -101,7 +101,8 @@ func _generate_and_place_bodies(primary: RigidBody2D) -> void:
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 = []
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):
@ -152,8 +153,35 @@ func _generate_and_place_bodies(primary: RigidBody2D) -> void:
_create_body_in_ring(primary, station_instance)
current_orbit_radius = station_instance.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 Spaceship
# 1. Set its primary to the star
spaceship_instance.primary = primary
# 2. Set its mass (must be scaled like your celestial bodies)
spaceship_instance.mass = 1 # Example scaled mass
# 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: RigidBody2D) -> void:
@ -237,20 +265,14 @@ func _create_asteroid_belt(primary: RigidBody2D, initial_offset: float) -> Aster
# Helper function to instantiate and place a body in a ring.
func _create_body_in_ring(primary: CelestialBody, body_instance: CelestialBody) -> void:
#var stable_velocity = calculate_stable_orbit_velocity(body_instance.orbit_radius_real, primary.mass_real)
var initial_position_vector = Vector2(body_instance.orbit_radius_real, 0).rotated(randf() * TAU)
body_instance.global_position = primary.global_position_real + initial_position_vector
body_instance.global_position = primary.global_position + initial_position_vector
primary.add_child(body_instance)
body_instance.linear_velocity = body_instance.calculate_initial_orbit_real(primary)
body_instance.linear_velocity = OrbitalMechanics.calculate_circular_orbit_velocity(body_instance, primary)
print("Created " + body_instance.name + " with radius " + str(body_instance.orbit_radius_real) + " and mass " + str(body_instance.mass))
print("Initial orbital velocity is: " + str(body_instance.linear_velocity))
# Calculates the velocity required for a stable circular orbit.
#func calculate_stable_orbit_velocity(orbit_radius: float, primary_mass: float) -> float:
#return sqrt(G * primary_mass / orbit_radius)
# Recursively finds all celestial bodies in the scene.
func get_all_bodies() -> Array:
var bodies = []