Finished move

This commit is contained in:
2025-11-18 11:00:14 +01:00
parent e2da700bcd
commit 86762d0d50
62 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,39 @@
Project State Summary
Core Plugin Logic (module_builder_editor_plugin.gd)
The main plugin script has been refactored into a single controller that manages the entire builder workflow. Key functionalities include:
Custom Workspace: The plugin provides a dedicated editor tab with its own isolated SubViewport and a Node2D root, separating the building environment from the main scene.
Camera Controls: It now has a functional Camera2D with working pan (right-click drag) and zoom (mouse wheel) controls, as well as a "Center" button to reset the camera to the origin.
Visual Grid: A CustomGrid.gd script, attached to a Node2D in the viewport, draws a dynamic grid that scales with the camera's zoom level, prevents visual clutter by culling lines, and accurately aligns with the placement snapping.
Piece Placement: The plugin can correctly preview and place structural pieces from the bottom dock into the viewport. The pieces snap to a grid and are added as children to a Module node.
Module Management: It can find existing Module nodes based on the placement position of a new piece. If no module is found, a new one is instantiated.
Undo/Redo System: The plugin now supports a robust undo/redo system for both placing and removing structural pieces. This uses the editor's native EditorUndoRedoManager to ensure actions are reversible with Ctrl+Z and Ctrl+Shift+Z.
Docks: The plugin instantiates and adds custom docks for the ConstructionTree and ConstructionInspector to the left-hand side of the editor.
Other File Updates
builder_dock.gd: The script is now a simple UI controller that emits a signal when a piece button is pressed, centralizing the logic in the main plugin script.
module.gd: The physics recalculation logic is now a public function that correctly combines all child StructuralPiece collision shapes into a single ConcavePolygonShape2D for the parent RigidBody2D.
structural_piece.gd: The is_preview property is used to make preview pieces translucent and non-interactive, allowing mouse events to pass through to the viewport.
Outstanding Tasks
R key binding: The 'R' key binding to rotate pieces is not working correctly and needs to be addressed.
Save Functionality: The _on_save_button_pressed function needs to be implemented to save the built module as a .tscn file.
Dock Functionality: The ConstructionTree dock needs to be populated with the module hierarchy, and the ConstructionInspector needs to display the properties of the selected pieces.
Pressurize Button: The _on_pressurise_button_pressed function needs to be implemented. This will likely involve using the HullVolume.gd script.
This summary provides all the necessary context to continue our work on the plugin.

View File

@ -0,0 +1,88 @@
@tool
extends Control
const PIECE_DIRECTORY = "res://scenes/ship/builder/pieces/"
@onready var pieces_container: HFlowContainer = %PiecesContainer
@onready var clear_button: Button = %ClearButton
var preview_piece: StructuralPiece = null
var active_piece_scene: PackedScene = null
var rotation_angle: float = 0.0
var grid_size: float = 50.0
signal active_piece_set(piece: StructuralPiece)
func _ready() -> void:
if not Engine.is_editor_hint():
return
_setup_piece_buttons()
clear_button.pressed.connect(_on_clear_button_pressed)
func _exit_tree() -> void:
if preview_piece:
preview_piece.queue_free()
func _setup_piece_buttons() -> void:
for child in pieces_container.get_children():
child.queue_free()
var dir = DirAccess.open(PIECE_DIRECTORY)
if not dir:
push_error("Could not open directory: ", PIECE_DIRECTORY)
return
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
if file_name.ends_with(".tscn"):
var file_path = PIECE_DIRECTORY.path_join(file_name)
_create_piece_button(file_path)
file_name = dir.get_next()
dir.list_dir_end()
func _create_piece_button(path: String) -> void:
var piece_scene = load(path) as PackedScene
if piece_scene and piece_scene.get_state().get_node_count() > 0:
var root = piece_scene.instantiate() as StructuralPiece
if root:
var button = Button.new()
button.text = root.name
button.pressed.connect(Callable(self, "_on_piece_button_pressed").bind(piece_scene))
button.size = Vector2(100, 100)
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
var color_rect = root.find_child("ColorRect") as ColorRect
if color_rect:
var icon_texture = ImageTexture.create_from_image(Image.create(color_rect.size.x, color_rect.size.y, false, Image.FORMAT_RGBA8))
icon_texture.get_image().fill(color_rect.color)
button.icon = icon_texture
pieces_container.add_child(button)
root.queue_free()
func _on_piece_button_pressed(scene: PackedScene):
if is_instance_valid(preview_piece):
preview_piece.queue_free()
active_piece_set.emit(scene)
func _on_clear_button_pressed():
if is_instance_valid(preview_piece):
preview_piece.queue_free()
preview_piece = null
var selected_nodes = EditorInterface.get_selection().get_selected_nodes()
if not selected_nodes.is_empty() and selected_nodes[0] is Module:
selected_nodes[0].clear_module()

View File

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

View File

@ -0,0 +1,39 @@
[gd_scene load_steps=2 format=3 uid="uid://cx6g4wgy3v8l"]
[ext_resource type="Script" uid="uid://yjbq3ihlmad8" path="res://addons/module_builder_plugin/builder_dock.gd" id="1_8casj"]
[node name="BuilderDock" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_8casj")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Select Piece"
[node name="ClearButton" type="Button" parent="VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 8
text = "Clear Module"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="PiecesContainer" type="HFlowContainer" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3

View File

@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://bsubuh3qxqs8e"]
[node name="Construction Inspector" type="VBoxContainer"]
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.25
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Inspector"
[node name="Inspector" type="Control" parent="."]
layout_mode = 2
size_flags_vertical = 3

View File

@ -0,0 +1,14 @@
[gd_scene format=3 uid="uid://2abscstf0tdd"]
[node name="Construction Tree" type="VBoxContainer"]
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.25
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Scene Hierarchy"
[node name="Tree" type="Tree" parent="."]
layout_mode = 2
size_flags_vertical = 3
hide_root = true

View File

@ -0,0 +1,68 @@
@tool
extends Node2D
@export var grid_size: float = 50.0
func _ready():
set_process(true)
func _process(delta):
queue_redraw()
func _draw():
var camera_transform = get_viewport().get_canvas_transform()
var camera_scale = camera_transform.get_scale().x
var line_width = 1.0 / camera_scale
# Get the visible rectangle in world coordinates.
var viewport_rect_in_world = camera_transform.affine_inverse() * get_viewport().get_visible_rect()
var visible_rect_min = viewport_rect_in_world.position
var visible_rect_max = viewport_rect_in_world.end
# --- Draw the Axes (always 2px thick in screen space) ---
draw_line(Vector2(visible_rect_min.x, 0), Vector2(visible_rect_max.x, 0), Color.RED, 2 / camera_scale)
draw_line(Vector2(0, visible_rect_min.y), Vector2(0, visible_rect_max.y), Color.GREEN, 2 / camera_scale)
# --- Draw the Major Grid Lines with Culling ---
var major_step = grid_size
const MIN_SCREEN_SPACING = 30.0 # Minimum pixel spacing between grid lines.
# Skip lines if they would be too close on screen.
while major_step * camera_scale < MIN_SCREEN_SPACING:
major_step *= 2
var first_x_major = floor(visible_rect_min.x / major_step) * major_step
var last_x_major = ceil(visible_rect_max.x / major_step) * major_step
var first_y_major = floor(visible_rect_min.y / major_step) * major_step
var last_y_major = ceil(visible_rect_max.y / major_step) * major_step
# Draw major vertical lines
for x in range(int(first_x_major / major_step), int(last_x_major / major_step) + 1):
var draw_x = x * major_step
draw_line(Vector2(draw_x, visible_rect_min.y), Vector2(draw_x, visible_rect_max.y), Color(Color.GRAY, 0.5), line_width)
# Draw major horizontal lines
for y in range(int(first_y_major / major_step), int(last_y_major / major_step) + 1):
var draw_y = y * major_step
draw_line(Vector2(visible_rect_min.x, draw_y), Vector2(visible_rect_max.x, draw_y), Color(Color.GRAY, 0.5), line_width)
# --- Draw Minor Grid Lines (only when zoomed in) ---
if major_step == grid_size:
var minor_step = grid_size / 5.0
var first_x_minor = floor(visible_rect_min.x / minor_step) * minor_step
var last_x_minor = ceil(visible_rect_max.x / minor_step) * minor_step
var first_y_minor = floor(visible_rect_min.y / minor_step) * minor_step
var last_y_minor = ceil(visible_rect_max.y / minor_step) * minor_step
# Draw minor vertical lines
for x in range(int(first_x_minor / minor_step), int(last_x_minor / minor_step) + 1):
var draw_x = x * minor_step
# Skip major lines to avoid drawing over them.
if fmod(draw_x, major_step) != 0:
draw_line(Vector2(draw_x, visible_rect_min.y), Vector2(draw_x, visible_rect_max.y), Color(Color.GRAY, 0.2), line_width)
# Draw minor horizontal lines
for y in range(int(first_y_minor / minor_step), int(last_y_minor / minor_step) + 1):
var draw_y = y * minor_step
# Skip major lines to avoid drawing over them.
if fmod(draw_y, major_step) != 0:
draw_line(Vector2(visible_rect_min.x, draw_y), Vector2(visible_rect_max.x, draw_y), Color(Color.GRAY, 0.2), line_width)

View File

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

View File

@ -0,0 +1,524 @@
@tool
class_name BuilderEditor
extends EditorPlugin
# --- Constants and Scene References ---
const MAIN_EDITOR_SCENE = preload("res://addons/module_builder_plugin/module_editor.tscn")
const BUILDER_DOCK_SCENE = preload("res://addons/module_builder_plugin/builder_dock.tscn")
const CONSTRUCTION_TREE_SCENE = preload("res://addons/module_builder_plugin/construction_tree.tscn")
const CONSTRUCTION_INSPECTOR_SCENE = preload("res://addons/module_builder_plugin/construction_inspector.tscn")
const MODULE_SCENE = preload("res://scenes/ship/builder/module.tscn")
# --- Dock references ---
var builder_dock: Control
var construction_tree_dock: Control
var construction_inspector_dock: Control
var tree_control: Tree
# --- Node References from the main screen scene ---
var builder_world: World2D
var main_screen: Control
var main_viewport: SubViewport
var zoom_label: Label
var rotate_button: Button
var center_button: Button
var pressurize_button: Button
var save_button: Button
var builder_scene_root: Node2D
var builder_camera: Camera2D
# --- State Management Enum ---
enum BuilderState {
IDLE,
PLACING_STRUCTURAL,
PLACING_COMPONENT,
WIRING # For future use
}
var current_state: BuilderState = BuilderState.IDLE
# --- State Variables ---
var preview_node = null # Can be either StructuralPiece or Component
var active_scene: PackedScene = null # Can be either StructuralPiece or Component
var rotation_angle: float = 0.0
var grid_size: float = 50.0
var undo_redo: EditorUndoRedoManager
# --- Most of the setup functions remain the same ---
func _enter_tree():
main_screen = MAIN_EDITOR_SCENE.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_screen)
main_viewport = main_screen.find_child("SubViewport")
builder_camera = main_screen.find_child("Camera2D")
# Get button and label references
zoom_label = main_screen.find_child("ZoomLabel")
rotate_button = main_screen.find_child("RotateButton")
center_button = main_screen.find_child("CenterButton")
pressurize_button = main_screen.find_child("PressuriseButton")
save_button = main_screen.find_child("SaveButton")
_setup_builder_world()
_setup_button_connections()
_update_ui_labels()
main_screen.hide()
undo_redo = EditorInterface.get_editor_undo_redo()
undo_redo.action_is_committing.connect(_on_undo_redo_action_committed)
func _setup_builder_world():
builder_world = World2D.new()
if is_instance_valid(main_viewport):
main_viewport.world_2d = builder_world
builder_scene_root = Node2D.new()
builder_scene_root.name = "BuilderRoot"
main_viewport.add_child(builder_scene_root)
func _setup_docks():
if BUILDER_DOCK_SCENE:
builder_dock = BUILDER_DOCK_SCENE.instantiate()
builder_dock.active_piece_set.connect(on_active_piece_set)
add_control_to_bottom_panel(builder_dock, "Ship Builder")
if CONSTRUCTION_TREE_SCENE:
construction_tree_dock = CONSTRUCTION_TREE_SCENE.instantiate()
tree_control = construction_tree_dock.find_child("Tree")
add_control_to_dock(DOCK_SLOT_LEFT_UR, construction_tree_dock)
_refresh_tree_display()
builder_world.changed.connect(_refresh_tree_display)
if CONSTRUCTION_INSPECTOR_SCENE:
construction_inspector_dock = CONSTRUCTION_INSPECTOR_SCENE.instantiate()
add_control_to_dock(DOCK_SLOT_RIGHT_UL, construction_inspector_dock)
func switch_to_dock_tab(dock_control: Control, tab_name: String):
var tab_container = dock_control.find_child("TabContainer")
if not is_instance_valid(tab_container):
print("Error: TabContainer not found in dock control.")
return
for i in range(tab_container.get_tab_count()):
if tab_container.get_tab_title(i) == tab_name:
tab_container.current_tab = i
return
print("Warning: Tab '%s' not found." % tab_name)
func _teardown_docks():
if builder_dock:
remove_control_from_bottom_panel(builder_dock)
builder_dock.queue_free()
if construction_tree_dock:
remove_control_from_docks(construction_tree_dock)
construction_tree_dock.queue_free()
builder_world.changed.disconnect(_refresh_tree_display)
if construction_inspector_dock:
remove_control_from_docks(construction_inspector_dock)
construction_inspector_dock.queue_free()
func _exit_tree():
if main_screen:
main_screen.queue_free()
func _has_main_screen() -> bool:
return true
func _make_visible(visible):
if main_screen:
main_screen.visible = visible
_setup_gui_input_listener(visible)
if visible:
_setup_docks()
else:
_teardown_docks()
func _get_plugin_name():
return "Ship Builder"
func _get_plugin_icon():
return EditorInterface.get_editor_theme().get_icon("Node", "EditorIcons")
func _setup_gui_input_listener(connect: bool):
if main_screen:
if connect:
main_screen.gui_input.connect(_on_viewport_input)
else:
main_screen.gui_input.disconnect(_on_viewport_input)
func _setup_button_connections():
if rotate_button: rotate_button.pressed.connect(_on_rotate_button_pressed)
if center_button: center_button.pressed.connect(_on_center_button_pressed)
if pressurize_button: pressurize_button.pressed.connect(_on_pressurise_button_pressed)
if save_button: save_button.pressed.connect(_on_save_button_pressed)
func _update_ui_labels():
if is_instance_valid(zoom_label):
var zoom_percent = int(builder_camera.zoom.x * 100)
zoom_label.text = "Zoom: %d%%" % zoom_percent
func _process(_delta):
_update_ui_labels()
_refresh_tree_display()
func _on_viewport_input(event: InputEvent) -> void:
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_RIGHT:
builder_camera.position -= event.relative / builder_camera.zoom
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_WHEEL_UP:
builder_camera.zoom *= 1.1
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
builder_camera.zoom /= 1.1
if event is InputEventMouseMotion:
_update_preview_position()
match current_state:
BuilderState.IDLE:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
_remove_piece_under_mouse()
BuilderState.PLACING_STRUCTURAL:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
_place_piece_from_preview()
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
on_clear_preview()
BuilderState.PLACING_COMPONENT:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
_place_component_from_preview()
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
on_clear_preview()
BuilderState.WIRING:
pass
func _unhandled_key_input(event: InputEvent):
if not event.is_pressed(): return
if event is InputEventKey and event.as_text().to_lower() == "r":
_on_rotate_button_pressed()
get_tree().set_input_as_handled()
func on_active_piece_set(scene: PackedScene):
if is_instance_valid(preview_node):
preview_node.queue_free()
current_state = BuilderState.PLACING_STRUCTURAL
active_scene = scene
preview_node = scene.instantiate() as StructuralPiece
preview_node.is_preview = true
builder_scene_root.add_child(preview_node)
_update_preview_position()
func _on_component_selected(component_scene: PackedScene):
if is_instance_valid(preview_node):
preview_node.queue_free()
current_state = BuilderState.PLACING_COMPONENT
active_scene = component_scene
preview_node = component_scene.instantiate() as Component
builder_scene_root.add_child(preview_node)
print("Now placing component: ", component_scene.resource_path)
func on_clear_preview():
if is_instance_valid(preview_node):
preview_node.queue_free()
preview_node = null
active_scene = null
current_state = BuilderState.IDLE
func _update_preview_position():
if not is_instance_valid(preview_node):
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
match current_state:
BuilderState.PLACING_STRUCTURAL:
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
preview_node.global_position = snapped_pos
preview_node.rotation = rotation_angle
BuilderState.PLACING_COMPONENT:
var target_module = _find_first_module()
if target_module:
var closest_point = _find_closest_attachment_point(target_module, world_mouse_pos)
if closest_point:
preview_node.global_position = closest_point.position
else:
preview_node.global_position = world_mouse_pos
else:
preview_node.global_position = world_mouse_pos
# --- REFACTORED: Piece Placement ---
func _place_piece_from_preview():
if not is_instance_valid(preview_node) or not is_instance_valid(active_scene):
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
var target_module = _find_nearby_modules(snapped_pos)
if not target_module:
target_module = MODULE_SCENE.instantiate() as Module
builder_scene_root.add_child(target_module)
target_module.global_position = snapped_pos
target_module.owner = builder_scene_root
var piece_to_place = active_scene.instantiate()
# --- The main change: Add as a direct child of the module ---
target_module.add_child(piece_to_place)
piece_to_place.owner = target_module
piece_to_place.rotation = rotation_angle
piece_to_place.global_position = snapped_pos
undo_redo.create_action("Place Structural Piece")
undo_redo.add_do_method(target_module, "add_child", piece_to_place)
undo_redo.add_do_method(piece_to_place, "set_owner", target_module)
undo_redo.add_do_method(target_module, "_recalculate_collision_shape")
undo_redo.add_undo_method(target_module, "remove_child", piece_to_place)
undo_redo.add_undo_method(target_module, "_recalculate_collision_shape")
undo_redo.commit_action()
# --- Component Placement remains the same ---
func _place_component_from_preview():
if not is_instance_valid(preview_node) or not is_instance_valid(active_scene):
push_error("Cannot place component: Invalid preview or scene.")
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var target_module = _find_first_module()
if not target_module:
push_error("No module found to attach component to.")
return
var closest_point = _find_closest_attachment_point(target_module, world_mouse_pos)
if not closest_point:
print("No valid attachment point nearby.")
return
var component_to_place = active_scene.instantiate() as Component
target_module.attach_component(component_to_place, closest_point.position, closest_point.piece)
undo_redo.create_action("Place Component")
undo_redo.add_do_method(target_module, "attach_component", component_to_place, closest_point.position, closest_point.piece)
undo_redo.add_undo_method(target_module, "remove_child", component_to_place)
undo_redo.add_do_method(target_module, "recalculate_physical_properties")
undo_redo.add_undo_method(target_module, "recalculate_physical_properties")
undo_redo.commit_action()
preview_node.global_position = closest_point.position
# --- Find Nearby Modules remains the same ---
func _find_nearby_modules(position: Vector2) -> Module:
const OVERLAP_MARGIN = 20.0
if not active_scene or not active_scene.can_instantiate(): return null
var piece_instance = active_scene.instantiate()
var shape_node = piece_instance.find_child("CollisionShape2D")
if not shape_node:
piece_instance.queue_free()
return null
var piece_shape = shape_node.shape
piece_instance.queue_free()
var enlarged_shape
if piece_shape is RectangleShape2D:
enlarged_shape = RectangleShape2D.new()
enlarged_shape.size = piece_shape.size + Vector2(OVERLAP_MARGIN, OVERLAP_MARGIN) * 2
elif piece_shape is CapsuleShape2D:
enlarged_shape = CapsuleShape2D.new()
enlarged_shape.radius = piece_shape.radius + OVERLAP_MARGIN
enlarged_shape.height = piece_shape.height + OVERLAP_MARGIN
else:
return null
var space_state = builder_world.direct_space_state
var query = PhysicsShapeQueryParameters2D.new()
query.set_shape(enlarged_shape)
query.transform = Transform2D(0, position)
var result = space_state.intersect_shape(query, 1)
if not result.is_empty():
var collider = result[0].get("collider")
if collider is StructuralPiece:
# --- REFACTORED: The module is now the direct parent/owner ---
if is_instance_valid(collider.owner) and collider.owner is Module:
return collider.owner
return null
func _find_first_module() -> Module:
for node in builder_scene_root.get_children():
if node is Module:
return node
return null
func _remove_piece_under_mouse():
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var space_state = builder_world.direct_space_state
var query = PhysicsPointQueryParameters2D.new()
query.position = world_mouse_pos
var result = space_state.intersect_point(query, 1)
if not result.is_empty():
var collider = result[0].get("collider")
if collider is StructuralPiece:
_remove_piece_with_undo_redo(collider)
elif collider is Component:
pass
# --- REFACTORED: Piece Removal ---
func _remove_piece_with_undo_redo(piece: StructuralPiece):
var module = piece.owner as Module
if not is_instance_valid(module) or not module is Module:
return
undo_redo.create_action("Remove Structural Piece")
# If this is the last structural piece of the module...
if module.get_structural_pieces().size() == 1:
# ...remove the entire module.
undo_redo.add_do_method(builder_scene_root, "remove_child", module)
undo_redo.add_undo_method(builder_scene_root, "add_child", module)
undo_redo.add_undo_method(module, "set_owner", builder_scene_root)
else:
# Otherwise, just remove the single piece from its parent (the module).
undo_redo.add_do_method(module, "remove_child", piece)
undo_redo.add_do_method(module, "_recalculate_collision_shape")
undo_redo.add_undo_method(module, "add_child", piece)
undo_redo.add_undo_method(piece, "set_owner", module) # Re-assign owner on undo
undo_redo.add_undo_method(module, "_recalculate_collision_shape")
undo_redo.commit_action()
# --- Toolbar Button Functions (No changes needed) ---
func _on_rotate_button_pressed():
rotation_angle = wrapf(rotation_angle + PI / 2, 0, TAU)
if is_instance_valid(preview_node):
preview_node.rotation = rotation_angle
_update_preview_position()
func _on_center_button_pressed():
builder_camera.position = Vector2.ZERO
builder_camera.zoom = Vector2(1.0, 1.0)
func _on_pressurise_button_pressed():
pass
func _on_save_button_pressed():
var module_to_save: Module
var selected_nodes = EditorInterface.get_selection().get_selected_nodes()
if not selected_nodes.is_empty() and selected_nodes[0] is Module:
module_to_save = selected_nodes[0]
else:
module_to_save = _find_first_module()
if not is_instance_valid(module_to_save):
push_error("Error: No Module node found or selected to save.")
return
var save_dialog = EditorFileDialog.new()
save_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
save_dialog.add_filter("*.tscn; Godot Scene")
save_dialog.current_path = "res://modules/" + module_to_save.name + ".tscn"
EditorInterface.get_editor_main_screen().add_child(save_dialog)
save_dialog.popup_centered_ratio()
save_dialog.file_selected.connect(Callable(self, "_perform_save").bind(module_to_save))
func _perform_save(file_path: String, module_to_save: Module):
var save_dir = file_path.get_base_dir()
var dir = DirAccess.open("res://")
if not dir.dir_exists(save_dir):
dir.make_dir_recursive(save_dir)
var packed_scene = PackedScene.new()
var error = packed_scene.pack(module_to_save)
if error != OK:
push_error("Error packing scene: ", error_string(error))
return
var save_result = ResourceSaver.save(packed_scene, file_path)
if save_result == OK:
print("Module saved successfully to ", file_path)
else:
push_error("Error saving scene: ", error_string(save_result))
EditorInterface.get_resource_filesystem().scan()
func _on_undo_redo_action_committed():
_refresh_tree_display()
# --- REFACTORED: Tree Display ---
func _refresh_tree_display():
if not is_instance_valid(tree_control):
return
tree_control.clear()
var root_item = tree_control.create_item()
root_item.set_text(0, builder_scene_root.name)
# Iterate through all modules and populate the tree.
for module in builder_scene_root.get_children():
if module is Module:
var module_item = tree_control.create_item(root_item)
module_item.set_text(0, module.name)
module_item.set_meta("node", module)
# Use the module's helper functions to find children
for piece in module.get_structural_pieces():
var piece_item = tree_control.create_item(module_item)
piece_item.set_text(0, piece.name)
piece_item.set_meta("node", piece)
for component in module.get_components():
var component_item = tree_control.create_item(module_item)
component_item.set_text(0, component.name)
component_item.set_meta("node", component)
func _find_closest_attachment_point(module: Module, world_pos: Vector2):
var min_distance_sq = module.COMPONENT_GRID_SIZE * module.COMPONENT_GRID_SIZE * 0.5
var closest_point = null
for point in module.get_attachment_points():
var dist_sq = point.position.distance_squared_to(world_pos)
if dist_sq < min_distance_sq:
min_distance_sq = dist_sq
closest_point = point
return closest_point

View File

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

View File

@ -0,0 +1,83 @@
[gd_scene load_steps=2 format=3 uid="uid://b018j62t6j24l"]
[ext_resource type="Script" uid="uid://cr6b1a5pvka3j" path="res://addons/module_builder_plugin/custom_grid.gd" id="1_5stev"]
[node name="ModuleEditor" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 1)
[node name="MainViewport" type="SubViewportContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
stretch = true
[node name="SubViewport" type="SubViewport" parent="MainViewport"]
transparent_bg = true
handle_input_locally = false
size = Vector2i(1152, 648)
render_target_update_mode = 4
[node name="Camera2D" type="Camera2D" parent="MainViewport/SubViewport"]
zoom = Vector2(0.5, 0.5)
[node name="Grid" type="Node2D" parent="MainViewport/SubViewport"]
script = ExtResource("1_5stev")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 0
offset_right = 318.0
offset_bottom = 300.0
[node name="Toolbar" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="RotateButton" type="Button" parent="VBoxContainer/Toolbar"]
layout_mode = 2
text = "Rotate"
[node name="CenterButton" type="Button" parent="VBoxContainer/Toolbar"]
layout_mode = 2
text = "Center"
[node name="PressuriseButton" type="Button" parent="VBoxContainer/Toolbar"]
layout_mode = 2
text = "Pressurize"
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ZoomLabel" type="Label" parent="VBoxContainer/Toolbar"]
layout_mode = 2
text = "Zoom: 100%"
[node name="SaveName" type="TextEdit" parent="VBoxContainer/Toolbar"]
visible = false
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
placeholder_text = "./EngineeringModule"
[node name="SaveButton" type="Button" parent="VBoxContainer/Toolbar"]
layout_mode = 2
text = "Save Scene"

View File

@ -0,0 +1,7 @@
[plugin]
name="Module Builder Plugin"
description="An editor tool for building modular ships."
author="Olof Pettersson"
version="0.1"
script="module_builder_editor_plugin.gd"

1
src/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 994 B

37
src/icon.svg.import Normal file
View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b7sxyli8cn36w"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

6
src/main.tscn Normal file
View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dogqi2c58qdc0"]
[ext_resource type="Script" uid="uid://bkcouefvi7iup" path="res://scripts/star_system.gd" id="1_ig7tw"]
[node name="StarSystem" type="Node3D"]
script = ExtResource("1_ig7tw")

View File

@ -0,0 +1,27 @@
[gd_scene load_steps=9 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"]
[ext_resource type="PackedScene" uid="uid://clt4qlsjcfgln" path="res://scenes/planet.tscn" id="3_272bh"]
[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"]
[node name="Node2D" type="Node2D"]
script = ExtResource("1_h2yge")
min_asteroid_belts = 0
star_scene = ExtResource("2_7mycd")
planet_scene = ExtResource("3_272bh")
moon_scene = ExtResource("4_5vw27")
station_scene = ExtResource("5_kek77")
asteroid_scene = ExtResource("6_4c57u")
sim_scale = 1e+09
[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

@ -0,0 +1,566 @@
[gd_scene load_steps=5 format=3 uid="uid://bkwogkfqk2uxo"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_ktv2t"]
[ext_resource type="PackedScene" uid="uid://bsyufiv0m1018" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_shb7f"]
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="3_ism2t"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_ism2t"]
properties/0/path = NodePath(".:position")
properties/0/spawn = true
properties/0/replication_mode = 1
[node name="3dTestShip" type="RigidBody3D" unique_id=246037729]
script = ExtResource("1_ktv2t")
physics_mode = 1
base_mass = 10000.0
metadata/_custom_type_script = "uid://6co67nfy8ngb"
[node name="Hullplate7" parent="." unique_id=1182121679 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, 0)
physics_mode = 2
[node name="Hullplate8" parent="." unique_id=294855274 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, -1)
physics_mode = 2
[node name="Hullplate9" parent="." unique_id=130054924 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, 1)
physics_mode = 2
[node name="Hullplate4" parent="." unique_id=2133064539 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, 0)
physics_mode = 2
[node name="Hullplate5" parent="." unique_id=1436331513 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, -1)
physics_mode = 2
[node name="Hullplate6" parent="." unique_id=1249365999 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, 1)
physics_mode = 2
[node name="Hullplate11" parent="." unique_id=1656979163 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 0, -4)
physics_mode = 2
[node name="Hullplate13" parent="." unique_id=1426276711 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, -3)
physics_mode = 2
[node name="Hullplate14" parent="." unique_id=1212526811 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, -4)
physics_mode = 2
[node name="Hullplate15" parent="." unique_id=403515873 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, -2)
physics_mode = 2
[node name="Hullplate16" parent="." unique_id=145935239 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, -3)
physics_mode = 2
[node name="Hullplate17" parent="." unique_id=1662804653 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, -4)
physics_mode = 2
[node name="Hullplate18" parent="." unique_id=741829932 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, -2)
physics_mode = 2
[node name="Hullplate21" parent="." unique_id=31417961 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 0, 4)
physics_mode = 2
[node name="Hullplate22" parent="." unique_id=1845702661 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, 3)
physics_mode = 2
[node name="Hullplate23" parent="." unique_id=1747432968 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, 2)
physics_mode = 2
[node name="Hullplate24" parent="." unique_id=1486518216 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, -1, 4)
physics_mode = 2
[node name="Hullplate25" parent="." unique_id=1880158566 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, 3)
physics_mode = 2
[node name="Hullplate26" parent="." unique_id=1506445603 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, 2)
physics_mode = 2
[node name="Hullplate27" parent="." unique_id=1749302489 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 2, 1, 4)
physics_mode = 2
[node name="Hullplate31" parent="." unique_id=1965678834 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, -4)
physics_mode = 2
[node name="Hullplate32" parent="." unique_id=515940324 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, -3)
physics_mode = 2
[node name="Hullplate33" parent="." unique_id=313389603 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, -2)
physics_mode = 2
[node name="Hullplate34" parent="." unique_id=363616195 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, 0)
physics_mode = 2
[node name="Hullplate35" parent="." unique_id=568985619 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, -1)
physics_mode = 2
[node name="Hullplate36" parent="." unique_id=193191417 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, 1)
physics_mode = 2
[node name="Hullplate38" parent="." unique_id=1152815429 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 0, -4)
physics_mode = 2
[node name="Hullplate40" parent="." unique_id=1303768723 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, -1)
physics_mode = 2
[node name="Hullplate41" parent="." unique_id=1489680526 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, 0)
physics_mode = 2
[node name="Hullplate42" parent="." unique_id=1454642421 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, 1)
physics_mode = 2
[node name="Hullplate43" parent="." unique_id=1322280114 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, -3)
physics_mode = 2
[node name="Hullplate44" parent="." unique_id=1380061102 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, -4)
physics_mode = 2
[node name="Hullplate45" parent="." unique_id=1740305308 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, -2)
physics_mode = 2
[node name="Hullplate48" parent="." unique_id=587023569 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 0, 4)
physics_mode = 2
[node name="Hullplate49" parent="." unique_id=1103858035 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, 3)
physics_mode = 2
[node name="Hullplate50" parent="." unique_id=916625356 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, 2)
physics_mode = 2
[node name="Hullplate51" parent="." unique_id=2115734988 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, -1, 4)
physics_mode = 2
[node name="Hullplate52" parent="." unique_id=1715698306 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, 3)
physics_mode = 2
[node name="Hullplate53" parent="." unique_id=369018899 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, 2)
physics_mode = 2
[node name="Hullplate54" parent="." unique_id=1618415296 instance=ExtResource("2_shb7f")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -2, 1, 4)
physics_mode = 2
[node name="Hullplate57" parent="." unique_id=1148292814 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5, 0, 4.5)
physics_mode = 2
[node name="Hullplate58" parent="." unique_id=1183219370 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.50098634, -1, 4.4908433)
physics_mode = 2
[node name="Hullplate59" parent="." unique_id=95522376 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.49901366, -1, 4.4908433)
physics_mode = 2
[node name="Hullplate60" parent="." unique_id=960534764 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5, -1, 4.5)
physics_mode = 2
[node name="Hullplate61" parent="." unique_id=1862079328 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.50098634, 1.000001, 4.4908433)
physics_mode = 2
[node name="Hullplate62" parent="." unique_id=876185578 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.49901366, 1.000001, 4.4908433)
physics_mode = 2
[node name="Hullplate64" parent="." unique_id=622302151 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, 9.536743e-07, 4.4908433)
physics_mode = 2
[node name="Hullplate65" parent="." unique_id=2027647666 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, -1, 4.4908433)
physics_mode = 2
[node name="Hullplate66" parent="." unique_id=335333911 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, 1.000001, 4.4908433)
physics_mode = 2
[node name="Hullplate63" parent="." unique_id=779321466 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5, 1, 4.5)
physics_mode = 2
[node name="Hullplate69" parent="." unique_id=391423682 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5009866, 9.536743e-07, -4.5091567)
physics_mode = 2
[node name="Hullplate70" parent="." unique_id=1436426809 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.50098634, -1, -4.5091567)
physics_mode = 2
[node name="Hullplate71" parent="." unique_id=1045660804 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.49901366, -1, -4.5091567)
physics_mode = 2
[node name="Hullplate72" parent="." unique_id=1696784058 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5009866, -1, -4.5091567)
physics_mode = 2
[node name="Hullplate73" parent="." unique_id=1709873058 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.50098634, 1.000001, -4.5091567)
physics_mode = 2
[node name="Hullplate74" parent="." unique_id=1071906843 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.49901366, 1.000001, -4.5091567)
physics_mode = 2
[node name="Hullplate75" parent="." unique_id=413542580 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, 9.536743e-07, -4.5091567)
physics_mode = 2
[node name="Hullplate76" parent="." unique_id=448578032 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, -1, -4.5091567)
physics_mode = 2
[node name="Hullplate77" parent="." unique_id=1162322851 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4990137, 1.000001, -4.5091567)
physics_mode = 2
[node name="Hullplate78" parent="." unique_id=790206161 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, 0, 1, 0, -0.014328662, 0, 0.99989736, -1.5009866, 1.000001, -4.5091567)
physics_mode = 2
[node name="Hullplate79" parent="." unique_id=1019136641 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, -3.009157)
physics_mode = 2
[node name="Hullplate80" parent="." unique_id=152922175 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, -3.009157)
physics_mode = 2
[node name="Hullplate81" parent="." unique_id=771888008 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, -3.009157)
physics_mode = 2
[node name="Hullplate82" parent="." unique_id=816092557 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, -2.009157)
physics_mode = 2
[node name="Hullplate83" parent="." unique_id=1871920861 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, -2.009157)
physics_mode = 2
[node name="Hullplate84" parent="." unique_id=103727539 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, -2.009157)
physics_mode = 2
[node name="Hullplate85" parent="." unique_id=1457444620 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, -4.009157)
physics_mode = 2
[node name="Hullplate86" parent="." unique_id=1402217859 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, -4.009157)
physics_mode = 2
[node name="Hullplate87" parent="." unique_id=293240152 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, -3.009157)
physics_mode = 2
[node name="Hullplate88" parent="." unique_id=158231735 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, -2.009157)
physics_mode = 2
[node name="Hullplate89" parent="." unique_id=2017317978 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, -4.009157)
physics_mode = 2
[node name="Hullplate90" parent="." unique_id=1810711362 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, -4.009157)
physics_mode = 2
[node name="Hullplate91" parent="." unique_id=648502427 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, -0.009156942)
physics_mode = 2
[node name="Hullplate92" parent="." unique_id=1280848561 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, -0.009156942)
physics_mode = 2
[node name="Hullplate93" parent="." unique_id=1000182357 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, -0.009156942)
physics_mode = 2
[node name="Hullplate94" parent="." unique_id=663755561 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, 0.99084306)
physics_mode = 2
[node name="Hullplate95" parent="." unique_id=977211031 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, 0.99084306)
physics_mode = 2
[node name="Hullplate96" parent="." unique_id=1017704164 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, 0.99084306)
physics_mode = 2
[node name="Hullplate97" parent="." unique_id=2095269489 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, -1.0091572)
physics_mode = 2
[node name="Hullplate98" parent="." unique_id=615154295 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, -1.0091572)
physics_mode = 2
[node name="Hullplate99" parent="." unique_id=1435686924 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, -0.009156942)
physics_mode = 2
[node name="Hullplate100" parent="." unique_id=361501534 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, 0.99084306)
physics_mode = 2
[node name="Hullplate101" parent="." unique_id=776176100 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, -1.0091572)
physics_mode = 2
[node name="Hullplate102" parent="." unique_id=1146417492 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, -1.0091572)
physics_mode = 2
[node name="Hullplate103" parent="." unique_id=1413321748 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, 2.990843)
physics_mode = 2
[node name="Hullplate104" parent="." unique_id=1044980803 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, 2.990843)
physics_mode = 2
[node name="Hullplate105" parent="." unique_id=1804409489 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, 2.990843)
physics_mode = 2
[node name="Hullplate106" parent="." unique_id=1076107521 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.5, 1.5, 4)
physics_mode = 2
[node name="Hullplate107" parent="." unique_id=1190510681 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.5, 1.5, 4)
physics_mode = 2
[node name="Hullplate108" parent="." unique_id=855909591 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, 3.990843)
physics_mode = 2
[node name="Hullplate109" parent="." unique_id=946006990 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, 1.5000012, 1.9908428)
physics_mode = 2
[node name="Hullplate110" parent="." unique_id=1957722835 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, 1.5000012, 1.9908428)
physics_mode = 2
[node name="Hullplate111" parent="." unique_id=1708941560 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, 2.990843)
physics_mode = 2
[node name="Hullplate112" parent="." unique_id=598393913 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, 3.990843)
physics_mode = 2
[node name="Hullplate113" parent="." unique_id=629535431 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, 1.5000012, 1.9908428)
physics_mode = 2
[node name="Hullplate114" parent="." unique_id=1483594858 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, 1.5000012, 1.9908428)
physics_mode = 2
[node name="Hullplate115" parent="." unique_id=1186769437 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, -3.009157)
physics_mode = 2
[node name="Hullplate116" parent="." unique_id=752889015 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, -3.009157)
physics_mode = 2
[node name="Hullplate117" parent="." unique_id=175698677 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, -3.009157)
physics_mode = 2
[node name="Hullplate118" parent="." unique_id=670641245 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, -2.009157)
physics_mode = 2
[node name="Hullplate119" parent="." unique_id=988678524 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, -2.009157)
physics_mode = 2
[node name="Hullplate120" parent="." unique_id=896262764 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, -2.009157)
physics_mode = 2
[node name="Hullplate121" parent="." unique_id=1336630931 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, -4.009157)
physics_mode = 2
[node name="Hullplate122" parent="." unique_id=101919359 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, -4.009157)
physics_mode = 2
[node name="Hullplate123" parent="." unique_id=1356736016 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, -3.009157)
physics_mode = 2
[node name="Hullplate124" parent="." unique_id=742815341 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, -2.009157)
physics_mode = 2
[node name="Hullplate125" parent="." unique_id=1651537246 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, -4.009157)
physics_mode = 2
[node name="Hullplate126" parent="." unique_id=1253078352 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, -4.009157)
physics_mode = 2
[node name="Hullplate127" parent="." unique_id=519787812 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, -0.009156942)
physics_mode = 2
[node name="Hullplate128" parent="." unique_id=629828036 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, -0.009156942)
physics_mode = 2
[node name="Hullplate129" parent="." unique_id=2010663580 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, -0.009156942)
physics_mode = 2
[node name="Hullplate130" parent="." unique_id=1705163002 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, 0.99084306)
physics_mode = 2
[node name="Hullplate131" parent="." unique_id=1635599014 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, 0.99084306)
physics_mode = 2
[node name="Hullplate132" parent="." unique_id=789401102 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, 0.99084306)
physics_mode = 2
[node name="Hullplate133" parent="." unique_id=1671040057 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, -1.0091572)
physics_mode = 2
[node name="Hullplate134" parent="." unique_id=2118015321 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, -1.0091572)
physics_mode = 2
[node name="Hullplate135" parent="." unique_id=1970124357 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, -0.009156942)
physics_mode = 2
[node name="Hullplate136" parent="." unique_id=2129372302 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, 0.99084306)
physics_mode = 2
[node name="Hullplate137" parent="." unique_id=543355427 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, -1.0091572)
physics_mode = 2
[node name="Hullplate138" parent="." unique_id=1885736043 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, -1.0091572)
physics_mode = 2
[node name="Hullplate139" parent="." unique_id=654209436 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, 2.990843)
physics_mode = 2
[node name="Hullplate140" parent="." unique_id=1938132143 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, 2.990843)
physics_mode = 2
[node name="Hullplate141" parent="." unique_id=486424951 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, 2.990843)
physics_mode = 2
[node name="Hullplate142" parent="." unique_id=910140496 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, 3.990843)
physics_mode = 2
[node name="Hullplate143" parent="." unique_id=515293159 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, 3.990843)
physics_mode = 2
[node name="Hullplate144" parent="." unique_id=890871001 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, 3.990843)
physics_mode = 2
[node name="Hullplate145" parent="." unique_id=1626468827 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.50098634, -1.4999988, 1.9908428)
physics_mode = 2
[node name="Hullplate146" parent="." unique_id=578516444 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.49901366, -1.4999988, 1.9908428)
physics_mode = 2
[node name="Hullplate147" parent="." unique_id=402255852 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, 2.990843)
physics_mode = 2
[node name="Hullplate148" parent="." unique_id=1631434711 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, 3.990843)
physics_mode = 2
[node name="Hullplate149" parent="." unique_id=726702930 instance=ExtResource("2_shb7f")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 1.4990137, -1.4999988, 1.9908428)
physics_mode = 2
[node name="Hullplate150" parent="." unique_id=1001521061 instance=ExtResource("2_shb7f")]
transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08, 0.99989736, 6.263257e-10, -1, -4.37069e-08, -1.5009866, -1.4999988, 1.9908428)
physics_mode = 2
[node name="Spawner" parent="." unique_id=6714366 instance=ExtResource("3_ism2t")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
[node name="OmniLight3D" type="OmniLight3D" parent="." unique_id=1071155008]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 1, -3)
[node name="OmniLight3D2" type="OmniLight3D" parent="." unique_id=151820223]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.6, 1, -3)
[node name="OmniLight3D3" type="OmniLight3D" parent="." unique_id=390575041]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.6, 1, 4)
[node name="OmniLight3D4" type="OmniLight3D" parent="." unique_id=1659652061]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 1, 4)
[node name="Camera3D" type="Camera3D" parent="." unique_id=1905582997]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
current = true
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=2096937457]
replication_config = SubResource("SceneReplicationConfig_ism2t")

View File

@ -0,0 +1,26 @@
[gd_scene load_steps=4 format=3 uid="uid://xcgmicfdqqb1"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_ogx5r"]
[ext_resource type="PackedScene" uid="uid://bsyufiv0m1018" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_nyqc6"]
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="3_3bya3"]
[node name="PhysicsTestingShip" type="RigidBody3D"]
script = ExtResource("1_ogx5r")
base_mass = 200.0
metadata/_custom_type_script = "uid://6co67nfy8ngb"
[node name="Hullplate" parent="." instance=ExtResource("2_nyqc6")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, -1, 0)
[node name="Spawner" parent="." instance=ExtResource("3_3bya3")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.021089494, 0)
[node name="OmniLight3D" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, -2)
[node name="OmniLight3D2" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, -2)
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
current = true

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://cwblg6q5qse6c"]
[ext_resource type="PackedScene" uid="uid://d3hitk62fice4" path="res://scenes/ship/builder/pieces/bulkhead.tscn" id="1_obkto"]
[node name="TestShip" type="Node3D"]
[node name="Bulkhead" parent="." instance=ExtResource("1_obkto")]

View File

@ -0,0 +1,14 @@
@tool
class_name ControlPanel
extends Resource
## The UI scene for this panel (e.g., a lever, a screen).
@export var ui_scene: PackedScene
## Describes the signals this panel emits (e.g., "lever_pulled").
func get_output_signals() -> Dictionary:
return {}
## Describes the functions this panel has to display data (e.g., "update_text").
func get_input_sockets() -> Dictionary:
return {}

View File

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

View File

@ -0,0 +1,43 @@
class_name DataTypes
extends Node
# TODO: Add comments and export tooltips for these classes so players can understand what they hold
class ThrusterCalibration:
var thruster_data: Dictionary[Thruster, ThrusterData]
var max_pos_torque: float
var max_neg_torque: float
class ThrusterData:
enum ThrusterType {
LINEAR,
ROTATIONAL,
UNCALIBRATED
}
var thruster_node: Thruster
var thruster_type: ThrusterType = ThrusterType.UNCALIBRATED
var measured_torque_vector: Vector3 # The rotational force it provides
var measured_thrust: float # The linear force it provides
class ImpulsiveBurnPlan:
var delta_v_magnitude: float
var wait_time: float = 0.0
var burn_duration: float
var desired_basis: Basis
class PathProjection:
var body_ref: OrbitalBody3D
var points: Array[PathPoint]
func _init(b: OrbitalBody3D):
body_ref = b
class PathPoint:
var time: float # Time in seconds from the start of the projection
var position: Vector3
var velocity: Vector3
func _init(t: float, p: Vector3, v: Vector3):
time = t
position = p
velocity = v

View File

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

View File

@ -0,0 +1,16 @@
class_name Databank
extends Node
var root_module: Module
# --- Initialization ---
func initialize(ship_root: Module):
self.root_module = ship_root
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return []
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return []

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="StationPanel" load_steps=3 format=3 uid="uid://c4wyouanvf86c"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_ts7kh"]
[ext_resource type="PackedScene" uid="uid://dt1t2n7dewucw" path="res://scenes/ship/computer/UI/button_panel.tscn" id="2_72ur5"]
[resource]
script = ExtResource("1_ts7kh")
ui_scene = ExtResource("2_72ur5")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,274 @@
@tool
extends Container
class_name PanelFrame
# A column is defined as Constants.UI_GRID_SIZE pixels in width
@export var columns = 12
# A row is defined as Constants.UI_GRID_SIZE pixels in height
@export var rows = 6
enum WiringState { IDLE, DRAGGING_WIRE }
var current_state: WiringState = WiringState.IDLE
var current_schematic: WiringSchematic
var active_wire_points: Array[Vector2] = []
var start_socket: Socket
var end_socket: Socket
var wiring_mode: bool = false
var databanks: Array[Databank]
var databanks_container: GridContainer
# --- NO CHANGE HERE ---
# This getter is a nice way to access only the BasePanel children.
var installed_panels: Array[BasePanel]:
get:
installed_panels = []
for child in get_children():
if child is BasePanel:
installed_panels.append(child as BasePanel)
return installed_panels
func _init() -> void:
size = Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
# --- NEW FUNCTION ---
# This is a crucial function for all Control nodes, especially containers.
# It tells the layout system the smallest size this container can be.
func _get_minimum_size() -> Vector2:
# The minimum size is simply the grid dimensions multiplied by the pixel size.
return Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
func _notification(what: int) -> void:
if what == NOTIFICATION_SORT_CHILDREN:
_sort_children()
func build(panel_scenes: Array[PackedScene], station: SystemStation):
# Instead of manually calling our placement function, we tell Godot
# that the layout needs to be updated. Godot will then call
# _notification(NOTIFICATION_SORT_CHILDREN) for us at the correct time.
var col = 0
var row = 0
#print("STATION: Building panels using autolayout")
for panel_scene in panel_scenes:
if not panel_scene: continue
var panel_instance = panel_scene.instantiate()
if not panel_instance is BasePanel:
panel_instance.queue_free()
continue
var panel: BasePanel = panel_instance as BasePanel
panel.initialize(station)
# Store the grid coordinates on the panel itself. The container will use
# this information when it arranges its children.
if panel.grid_height <= self.rows - row:
panel.placed_in_col = col
panel.placed_in_row = row
add_child(panel)
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
row += panel.grid_height
else:
var last_panel = get_children()[-1]
col += last_panel.grid_width
row = 0
panel.placed_in_col = col
panel.placed_in_row = row
add_child(panel)
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
row += panel.grid_height
queue_sort()
# This is the core logic. It positions and sizes every child.
func _sort_children():
#print("PanelFrame Sorting children")
for child in get_children():
if child == databanks_container:
print("Databanks container found %s" % child)
fit_child_in_rect(child, Rect2(Vector2(0, rows * Constants.UI_GRID_SIZE), child.size))
continue
# Skip any nodes that aren't a BasePanel.
if not child is BasePanel:
continue
var panel := child as BasePanel
# Calculate the desired position based on the panel's stored grid coordinates.
var start_pos = Vector2(panel.placed_in_col * Constants.UI_GRID_SIZE, panel.placed_in_row * Constants.UI_GRID_SIZE)
# Calculate the desired size based on the panel's width and height in grid units.
var panel_size = Vector2(panel.grid_width * Constants.UI_GRID_SIZE, panel.grid_height * Constants.UI_GRID_SIZE)
#print(" - %s, Pos %s Size %s" % [panel, start_pos, panel_size])
# This single function tells the container to position AND size the child
# within the given rectangle. The Rect2's origin is the position.
fit_child_in_rect(panel, Rect2(start_pos, panel_size))
# TODO: Expose grid to install panels
func toggle_wiring_mode():
wiring_mode = !wiring_mode
for panel in installed_panels:
panel.set_wiring_mode(wiring_mode)
if wiring_mode:
_build_databanks(databanks)
pass
if is_instance_valid(databanks_container):
if wiring_mode: databanks_container.show()
else: databanks_container.hide()
class InstalledDatabank:
extends Control
var databank_ref: Databank
var all_sockets: Array[Socket] = []
var SocketScene: PackedScene = preload("res://scenes/ship/computer/wiring/socket.tscn")
var inputs_container: VBoxContainer
var outputs_container: VBoxContainer
func _populate_sockets():
all_sockets.clear()
if not is_instance_valid(inputs_container):
inputs_container = VBoxContainer.new()
add_child(inputs_container)
if not is_instance_valid(outputs_container):
outputs_container = VBoxContainer.new()
add_child(outputs_container)
# Populate Input Sockets
for socket_name in databank_ref.get_input_sockets():
var socket = SocketScene.instantiate()
inputs_container.add_child(socket)
socket.initialize(socket_name, Socket.SocketType.INPUT)
all_sockets.append(socket)
# Populate Output Sockets
for socket_name in databank_ref.get_output_sockets():
var socket = SocketScene.instantiate()
outputs_container.add_child(socket)
socket.initialize(socket_name, Socket.SocketType.OUTPUT)
all_sockets.append(socket)
func _build_databanks(dbs_to_install: Array[Databank]):
if not is_instance_valid(databanks_container):
databanks_container = GridContainer.new()
databanks_container.columns = columns
databanks_container.add_theme_constant_override("h_separation", Constants.UI_GRID_SIZE * 3)
databanks_container.add_theme_constant_override("v_separation", Constants.UI_GRID_SIZE + 16)
add_child(databanks_container)
var installed_databanks = databanks_container.get_children()
for to_install in dbs_to_install:
if installed_databanks.any(func(existing_db): return existing_db.databank_ref == to_install):
continue
var installed_databank = InstalledDatabank.new()
installed_databank.databank_ref = to_install
databanks_container.add_child(installed_databank)
installed_databank._populate_sockets()
func _gui_input(event: InputEvent):
if event is InputEventMouseButton:
# --- Start or End a Wire ---
if event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
var socket = _get_socket_at_pos(event.position)
if socket:
current_state = WiringState.DRAGGING_WIRE
if not start_socket:
# start new wire
start_socket = socket
# Add start point to wire points
active_wire_points.append(start_socket.icon.get_global_rect().get_center() - get_global_position())
elif start_socket and socket.socket_type != start_socket.socket_type:
end_socket = socket
_save_new_connection()
_reset_wiring_state()
elif current_state == WiringState.DRAGGING_WIRE:
# Add intermediate point
active_wire_points.append(get_local_mouse_position())
elif event.button_index == MOUSE_BUTTON_RIGHT:
# Pop Last Point
active_wire_points.remove_at(active_wire_points.size() - 1)
# Check if wire points are empty, then we remove the whole wire
if active_wire_points.size() <= 0:
_reset_wiring_state()
if event is InputEventMouseMotion and current_state == WiringState.DRAGGING_WIRE:
queue_redraw()
func _draw():
# 1. Draw all saved wires from the schematic.
if current_schematic:
for connection in current_schematic.connections:
if connection.path_points.size() > 1:
_draw_wire_path(connection.path_points, Color.GREEN)
# 2. Draw the active wire being dragged by the user.
if current_state == WiringState.DRAGGING_WIRE:
var live_path: Array[Vector2] = active_wire_points.duplicate()
live_path.append(get_local_mouse_position())
if live_path.size() > 1:
_draw_wire_path(live_path, Color.YELLOW)
# --- NEW: Helper function to draw a multi-point path ---
func _draw_wire_path(points: Array[Vector2], color: Color):
for i in range(points.size() - 1):
var p1 = points[i]
var p2 = points[i+1]
# var control_offset = Vector2(abs(p2.x - p1.x) * 0.5, 0)
draw_line(p1, p2, color, 3.0)
func _save_new_connection():
var new_connection = WireConnection.new()
if start_socket.socket_type == end_socket.socket_type:
push_error("Start socket and end socket of same type!")
return
if start_socket.socket_type == Socket.SocketType.INPUT:
new_connection.input_socket_name = start_socket.socket_name
elif start_socket.socket_type == Socket.SocketType.OUTPUT:
new_connection.output_socket_name = start_socket.socket_name
if end_socket.socket_type == Socket.SocketType.INPUT:
new_connection.input_socket_name = end_socket.socket_name
elif end_socket.socket_type == Socket.SocketType.OUTPUT:
new_connection.output_socket_name = end_socket.socket_name
var end_pos = end_socket.icon.get_global_rect().get_center() - get_global_position()
active_wire_points.append(end_pos)
new_connection.path_points = active_wire_points
if not current_schematic:
current_schematic = WiringSchematic.new()
current_schematic.connections.append(new_connection)
print("Connection saved!")
func _reset_wiring_state():
current_state = WiringState.IDLE
start_socket = null
end_socket = null
active_wire_points.clear()
queue_redraw()
func _get_socket_at_pos(global_pos: Vector2) -> Socket:
for panel in installed_panels:
for socket in panel.all_sockets:
if is_instance_valid(socket) and socket.icon.get_global_rect().has_point(global_pos):
return socket
return null

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ControlPanel" load_steps=3 format=3 uid="uid://57y6igb07e10"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_540xq"]
[ext_resource type="PackedScene" uid="uid://cdbqjkgsj02or" path="res://scenes/ship/computer/UI/readout_screen_panel.tscn" id="2_iy0t0"]
[resource]
script = ExtResource("1_540xq")
ui_scene = ExtResource("2_iy0t0")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ControlPanel" load_steps=3 format=3 uid="uid://dl7g67mtqkfx2"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_f0h3m"]
[ext_resource type="PackedScene" uid="uid://rd1c22nsru8y" path="res://scenes/ship/computer/UI/sensor_panel.tscn" id="2_kyhrs"]
[resource]
script = ExtResource("1_f0h3m")
ui_scene = ExtResource("2_kyhrs")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="StationPanel" load_steps=3 format=3 uid="uid://dcyr6utrk376h"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_8h7ox"]
[ext_resource type="PackedScene" uid="uid://pq55j75t3fda" path="res://scenes/ship/computer/UI/throttle_lever_panel.tscn" id="2_8h7ox"]
[resource]
script = ExtResource("1_8h7ox")
ui_scene = ExtResource("2_8h7ox")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,219 @@
# scenes/ship/computer/shards/autopilot_databank.gd
extends Databank
class_name AutopilotShard
signal execution_state_changed(is_executing: bool, status: String)
signal fmt_out(text: String)
signal request_attitude_hold(b: bool)
signal request_rotation(r: float)
signal request_rotation_thrust(r: float)
signal request_main_engine_thrust(t: float)
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["maneuver_received", "execute_plan", "set_thruster_calibration"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["execution_state_changed", "fmt_out", "request_attitude_hold", "request_rotation", "request_rotation_thrust", "request_main_engine_thrust"]
# --- State ---
enum State { IDLE, WAITING_FOR_WINDOW, EXECUTING_BURN }
var current_state: State = State.IDLE
var current_plan: Array[DataTypes.ImpulsiveBurnPlan] = []
var max_rot_time: float = 30.0
var RCS_calibration: DataTypes.ThrusterCalibration
var is_executing: bool = false
var status: String = ""
var current_timer: SceneTreeTimer
func _process(delta):
var fmt = ""
var state_name = State.keys()[current_state]
if current_timer and current_timer.time_left:
var time_str = "%d:%02d" % [int(current_timer.time_left) / 60, int(current_timer.time_left) % 60]
var interpolated_status = status % time_str
fmt = "Autopilot: %s\n%s" % [state_name, interpolated_status]
else:
fmt = "Autopilot: %s\n%s" % [state_name, status]
fmt_out.emit(fmt)
# INPUT SOCKET: Connected to the ManeuverPlanner's "maneuver_calculated" signal.
func maneuver_received(plan: Array[DataTypes.ImpulsiveBurnPlan]):
current_plan = plan
print("AUTOPILOT: Maneuver plan received.")
status = "Plan Received.\nPress Execute."
# In a UI, this would enable the "Execute" button.
# UI ACTION: An "Execute" button on a panel would call this.
func execute_plan():
if not current_plan.is_empty():
current_state = State.WAITING_FOR_WINDOW
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)
var time_elapsed: float = await _execute_autopilot_rotation(step)
current_timer = get_tree().create_timer(step.wait_time - time_elapsed)
status = "Waiting for burn window: T- %s"
await current_timer.timeout
await _execute_next_burn(step)
func set_thruster_calibration(data: DataTypes.ThrusterCalibration):
RCS_calibration = data
# --- PROCESSS FUNCTIONS: Functions being run to execute the steps of a planned transfer ---
func _execute_next_burn(step: DataTypes.ImpulsiveBurnPlan):
current_state = State.EXECUTING_BURN
status = "Executing Main Engine Burn: %s"
print("AUTOPILOT: Commanding main engine burn for %.2f seconds." % step.burn_duration)
request_main_engine_thrust.emit(1.0)
current_timer = get_tree().create_timer(step.burn_duration)
await current_timer.timeout
request_main_engine_thrust.emit(0.0)
# Transition to the next state
if not current_plan.is_empty():
current_state = State.WAITING_FOR_WINDOW
else:
current_state = State.IDLE
execution_state_changed.emit(false, "Maneuver complete.")
# --- AUTOPILOT "BANG-COAST-BANG" LOGIC (REFACTORED) ---
func _execute_autopilot_rotation(step: DataTypes.ImpulsiveBurnPlan) -> float:
var time_window = minf(step.wait_time, max_rot_time)
# --- 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_basis # 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 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
# --- 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.
# 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
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)
var accel_burn_time = abs(peak_angular_velocity / accel_angular_accel)
var decel_burn_time = abs(peak_angular_velocity / decel_angular_accel)
var total_maneuver_time = accel_burn_time + decel_burn_time
if total_maneuver_time > time_window:
accel_burn_time = time_window / 2.0
decel_burn_time = time_window / 2.0
# --- Execute Maneuver (3D) ---
# 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: Apply torque against the axis
request_rotation_thrust.emit(-axis_to_turn * decel_torque_magnitude)
await get_tree().create_timer(decel_burn_time).timeout
# 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: 3D Rotation maneuver complete.")
return init_time - Time.get_ticks_msec()
# --- HELPERS ---
# 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
# 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

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://dwhpjwuobcqdu"]
[ext_resource type="Script" path="res://scenes/ship/computer/shards/helm_autopilot_databank.gd" id="1_0abvf"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_tpm1x"]
[resource]
script = ExtResource("1_tpm1x")
logic_script = ExtResource("1_0abvf")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,253 @@
extends Databank
class_name HelmLogicShard
# --- References ---
@onready var thrusters: Array[Thruster] = []
# --- PD Controller Constants ---
@export var HOLD_KP: float = 8000.0 # Proportional gain
@export var HOLD_KD: float = 1200.0 # Derivative gain
@onready var target_rotation_rad: Basis
var attitude_hold_enabled: bool = false
var thruster_calibration_data: DataTypes.ThrusterCalibration
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["shutdown_rcs", "calibrate_rcs_performance", "set_throttle_input", "set_rotation_input", "set_desired_rotation", "set_attitude_hold"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["thruster_calibrated"]
# The Station calls this after instantiating the shard.
func initialize(ship_root: Module):
self.root_module = ship_root
if not is_instance_valid(root_module):
push_error("Helm Shard initialized without a valid ship root!")
return
thrusters = _find_all_thrusters(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.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()
# --- INPUT SOCKETS (Called by Panels or other Shards) ---
## This is an "input socket" for rotational control.
## It takes a value from -1.0 to 1.0.
func set_rotation_input(value: float):
if abs(value) > 0.1:
# Manual input overrides attitude hold.
attitude_hold_enabled = false
var desired_torque = (calibration_data.max_pos_torque if value > 0 else calibration_data.max_neg_torque) * value
apply_rotational_thrust(desired_torque)
else:
# When input stops, re-engage hold at the current rotation.
if not attitude_hold_enabled:
attitude_hold_enabled = true
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.
# --- REFACTORED: This is the key change ---
func set_throttle_input(value: float):
print("THRUSTER CONTROLLER: Throttle input recieved: %f.1" % value)
# This function now works with the simple on/off thrusters.
if not calibration_data:
print("THRUSTER CONTROLLER: No Calibration Data Found")
return
for thruster in calibration_data.thruster_data:
var thruster_data: DataTypes.ThrusterData = calibration_data.thruster_data[thruster]
if thruster_data.thruster_type == DataTypes.ThrusterData.ThrusterType.LINEAR:
print(" - Main thruster identified with thrust capacity: %f" % thruster_data.measured_thrust)
if value > 0.1:
print(" - Main Engine Activated")
thruster.turn_on()
else:
print(" - Main Engine Shut Off")
thruster.turn_off()
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)
# apply_rotational_thrust(desired_torque)
# else: apply_rotational_thrust(0.0)
# --- REFACTORED: This is the other key change ---
func apply_rotational_thrust(desired_torque: Vector3):
if not is_instance_valid(root_module):
return
# Iterate through all available RCS thrusters that have been calibrated
for thruster in calibration_data.thruster_data:
var thruster_data: DataTypes.ThrusterData = calibration_data.thruster_data[thruster]
if thruster_data.thruster_type == DataTypes.ThrusterData.ThrusterType.ROTATIONAL:
# If this thruster can help apply the desired torque, turn it on.
# Otherwise, explicitly turn it off to ensure it's not firing incorrectly.
var contribution = thruster_data.measured_torque_vector.dot(desired_torque)
if contribution > 0 and desired_torque != Vector3.ZERO:
thruster.turn_on()
else:
thruster.turn_off()
func shutdown_rcs():
for thruster in thrusters:
if not thruster.main_thruster:
thruster.turn_off()
func _find_all_thrusters(node: Node) -> Array[Thruster]:
var thrusters: Array[Thruster] = []
for child in node.get_children():
if child is Thruster:
thrusters.append(child)
if child.get_child_count() > 0:
thrusters.append_array(_find_all_thrusters(child))
return thrusters
# Angle difference in rad
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
signal thruster_calibrated(data: DataTypes.ThrusterCalibration)
var calibration_data: DataTypes.ThrusterCalibration
# --- CALIBRATION LOGIC (Migrated from ThrusterController.gd) ---
## Manages the calibration sequence for all non-main thrusters.
func calibrate_rcs_performance():
print("Helm Shard: Beginning RCS calibration protocol...")
if not is_instance_valid(root_module): return
# --- Disable attitude hold during calibration ---
var original_attitude_hold_state = attitude_hold_enabled
attitude_hold_enabled = false
shutdown_rcs() # Ensure all thrusters are off before we start
await get_tree().physics_frame
print("Helm Shard: Attitude hold protocol: %s" % ("enabled" if attitude_hold_enabled else "disabled"))
calibration_data = DataTypes.ThrusterCalibration.new()
for thruster in thrusters:
var data: DataTypes.ThrusterData = await _calibrate_single_thruster(thruster)
calibration_data.thruster_data[thruster] = data
print(calibration_data)
# Now that we have the data, calculate the ship's max torque values
calibration_data.max_pos_torque = 0.0
calibration_data.max_neg_torque = 0.0
for data in calibration_data.thruster_data.values():
if data.measured_torque_vector > 0:
calibration_data.max_pos_torque += data.measured_torque_vector
else:
calibration_data.max_neg_torque += abs(data.measured_torque_vector)
print("RCS Calibration Complete: Max Pos Torque: %.2f, Max Neg Torque: %.2f" % [calibration_data.max_pos_torque, calibration_data.max_neg_torque])
# Auto-tune the PD controller with the new values
if calibration_data.max_pos_torque > 0 and calibration_data.max_neg_torque > 0:
var average_max_torque = (calibration_data.max_pos_torque + calibration_data.max_neg_torque) / 2.0
HOLD_KP = average_max_torque * 0.1
HOLD_KD = HOLD_KP * 1 # You can tune this multiplier
print("PD Controller Auto-Tuned: Kp set to %.2f, Kd set to %.2f" % [HOLD_KP, HOLD_KD])
attitude_hold_enabled = original_attitude_hold_state
print("Helm Shard: Calibration complete. Attitude hold is now %s." % ("enabled" if attitude_hold_enabled else "disabled"))
thruster_calibration_data = calibration_data
thruster_calibrated.emit(calibration_data)
## Performs a test fire of a single thruster and measures the resulting change in angular velocity.
func _calibrate_single_thruster(thruster: Thruster) -> DataTypes.ThrusterData:
var data = DataTypes.ThrusterData.new()
data.thruster_node = thruster
# Prepare for test: save initial state
var initial_angular_velocity = root_module.angular_velocity
var initial_linear_velocity = root_module.linear_velocity
var test_burn_duration = 0.5 # A very short burst
# --- Perform Test Fire ---
thruster.turn_on()
await get_tree().create_timer(test_burn_duration).timeout
thruster.turn_off()
# Let the physics engine settle for one frame to ensure the velocity update is complete
await get_tree().physics_frame
# --- Measure Results ---
var delta_angular_velocity = root_module.angular_velocity - initial_angular_velocity
var delta_linear_velocity = root_module.linear_velocity - initial_linear_velocity
data.measured_torque_vector = Vector3.ZERO
data.measured_thrust = 0.0
# --- Calculate Performance ---
# Torque = inertia * angular_acceleration (alpha = dw/dt)
if root_module.inertia.length_squared() > 0:
data.measured_torque_vector = root_module.inertia * (delta_angular_velocity / test_burn_duration)
else:
data.measured_torque_vector = Vector3.ZERO
push_warning("Root module inertia is 0. Cannot calibrate torque.")
if root_module.mass > 0:
data.measured_thrust = root_module.mass * (delta_linear_velocity.length() / test_burn_duration)
else:
data.measured_thrust = 0.0
push_warning("Root module mass is 0. Cannot calibrate torque.")
if data.measured_thrust > abs(data.measured_torque_vector):
print(" - Calibrated %s: Linear(%.3f)" % [thruster.name, data.measured_thrust])
data.thruster_type = DataTypes.ThrusterData.ThrusterType.LINEAR
elif data.measured_thrust < abs(data.measured_torque_vector):
print(" - Calibrated %s: Torque(%.3f)" % [thruster.name, data.measured_torque_vector])
data.thruster_type = DataTypes.ThrusterData.ThrusterType.ROTATIONAL
# --- Cleanup: Counter the spin from the test fire ---
if data.measured_torque_vector.length_squared() > 0.001:
var counter_torque = -data.measured_torque_vector
var counter_burn_duration = (root_module.inertia * root_module.angular_velocity) / counter_torque
# Find a thruster that can apply the counter-torque
for other_thruster in thrusters:
var other_data = calibration_data.thruster_data.get(other_thruster)
if other_data and sign(other_data.measured_torque_vector) == sign(counter_torque):
other_thruster.turn_on()
await get_tree().create_timer(abs(counter_burn_duration)).timeout
other_thruster.turn_off()
break # Use the first one we find
await get_tree().physics_frame
return data

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://dghg3pbws42yu"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_kih5s"]
[ext_resource type="Script" uid="uid://cfbyqvnvf3hna" path="res://scenes/ship/computer/shards/helm_logic_databank.gd" id="1_vvsub"]
[resource]
script = ExtResource("1_kih5s")
logic_script = ExtResource("1_vvsub")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,37 @@
extends Databank
class_name ShipStatusShard
## This shard emits a signal with the formatted ship status text.
signal status_updated(text: String)
# Called by the Station when it's created.
func initialize(ship_root: Module):
self.root_module = ship_root
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return []
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["status_updated"]
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()
# # 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)

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://bx7wgunvy5hfa"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_2fbxe"]
[ext_resource type="Script" uid="uid://ctgl5kxyagw0f" path="res://scenes/ship/computer/shards/helm_ship_status.gd" id="1_880kd"]
[resource]
script = ExtResource("1_2fbxe")
logic_script = ExtResource("1_880kd")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,71 @@
# space_simulation/scenes/ship/computer/shards/nav_brachistochrone_planner.gd
extends Databank
class_name BrachistochronePlannerShard
## Emitted when a maneuver plan has been successfully calculated.
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
# --- References ---
var target_body: OrbitalBody3D = null
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["target_updated", "calculate_hohmann_transfer"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["maneuver_calculated"]
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
func target_updated(new_target: OrbitalBody3D):
print("BRACHISTOCHRONE PLANNER: Target received %s." % new_target.name)
target_body = new_target
# TODO: All positions and velocities for calculating should be gathered from a sensor databank
# UI ACTION: A panel button would call this function.
func calculate_brachistochrone_transfer():
if not is_instance_valid(root_module) or not is_instance_valid(target_body):
print("BRACHISTOCHRONE PLANNER: Cannot calculate without ship and target.")
return
# 1. Get total main engine thrust from all thruster components
# TODO: This should be gathered from a calibration shard
var main_engine_thrust = 0.0
for component in root_module.get_components():
if component is Thruster and component.main_thruster:
main_engine_thrust += component.max_thrust
if main_engine_thrust == 0.0 or root_module.mass == 0.0:
print("BRACHISTOCHRONE PLANNER: Ship has no main engine thrust or mass.")
return
var acceleration = main_engine_thrust / root_module.mass
var distance = root_module.global_position.distance_to(target_body.global_position)
# Using the kinematic equation: d = (1/2)at^2, solved for t: t = sqrt(2d/a)
# Since we accelerate for half the distance and decelerate for the other half:
var time_for_half_journey = sqrt(distance / acceleration)
# --- Assemble the plan as two ImpulsiveBurnPlan steps ---
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
# --- Step 1: Acceleration Burn ---
var accel_burn = DataTypes.ImpulsiveBurnPlan.new()
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
var direction_to_target = root_module.global_position.direction_to(target_body.global_position).normalized()
var up_vec = Vector3.UP
accel_burn.desired_basis = Basis.looking_at(direction_to_target, up_vec)
plan.append(accel_burn)
# --- Step 2: Deceleration Burn (The flip is handled by the autopilot between steps) ---
var decel_burn = DataTypes.ImpulsiveBurnPlan.new()
decel_burn.wait_time = 0 # No coasting period
decel_burn.burn_duration = time_for_half_journey
# The desired rotation is opposite the first burn
decel_burn.desired_basis = Basis.looking_at(-direction_to_target, up_vec)
plan.append(decel_burn)
print("BRACHISTOCHRONE PLANNER: Plan calculated. Total time: %.2f s" % (time_for_half_journey * 2.0))
maneuver_calculated.emit(plan)

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://bnyce8i208qby"]
[ext_resource type="Script" uid="uid://ghluwjd5c5ul" path="res://scenes/ship/computer/shards/nav_brachistochrone_planner.gd" id="1_asajk"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_xdqj8"]
[resource]
script = ExtResource("1_xdqj8")
logic_script = ExtResource("1_asajk")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,162 @@
# scenes/ship/computer/shards/maneuver_planner_databank.gd
extends Databank
class_name HohmanPlannerShard
## Emitted when a maneuver plan has been successfully calculated.
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
# --- References ---
var selection_shard: NavSelectionShard
var target_body: OrbitalBody3D = null
# --- Configurations ---
var boost_factor: float = 1.0
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["target_updated", "calculate_hohmann_transfer"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["maneuver_calculated"]
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
func target_updated(new_target: OrbitalBody3D):
print("MANEUVER PLANNER: Target recieved %s." % new_target)
target_body = new_target
# In a UI, this would enable the "Calculate" button.
func set_boost_factor(value: float):
boost_factor = value
# UI ACTION: A panel button would call this function.
func calculate_hohmann_transfer():
if not is_instance_valid(root_module) or not is_instance_valid(target_body):
print("MANEUVER PLANNER: Cannot calculate without ship and target.")
return
var star = GameManager.current_star_system.get_star()
if not is_instance_valid(star): return
var mu = OrbitalMechanics.G * star.mass
var r1 = root_module.global_position.distance_to(star.global_position)
var r2 = target_body.global_position.distance_to(star.global_position)
var a_transfer = (r1 + r2) / 2.0 * boost_factor
var v_source_orbit = sqrt(mu / r1)
var v_target_orbit = sqrt(mu / r2)
var v_transfer_periapsis = sqrt(mu * ((2.0 / r1) - (1.0 / a_transfer)))
var v_transfer_apoapsis = sqrt(mu * ((2.0 / r2) - (1.0 / a_transfer)))
var delta_v1 = v_transfer_periapsis - v_source_orbit
var delta_v2 = v_target_orbit - v_transfer_apoapsis
var time_of_flight = PI * sqrt(pow(a_transfer, 3) / mu)
var ang_vel_target = sqrt(mu / pow(r2, 3))
var travel_angle = ang_vel_target * time_of_flight
var required_phase_angle = PI - travel_angle
var vec_to_ship = (root_module.global_position - star.global_position).normalized()
var vec_to_target = (target_body.global_position - star.global_position).normalized()
var current_phase_angle = vec_to_ship.angle_to(vec_to_target)
var ang_vel_ship = sqrt(mu / pow(r1, 3))
var relative_ang_vel = ang_vel_ship - ang_vel_target
var angle_to_wait = current_phase_angle - required_phase_angle
if relative_ang_vel == 0: return # Avoid division by zero
var wait_time = abs(angle_to_wait / relative_ang_vel)
# TODO: Need a way to get this from a shared calibration databank shard
var main_engine_thrust = 0.0
for thruster in root_module.get_components():
if thruster is Thruster and thruster.main_thruster:
main_engine_thrust += thruster.max_thrust
if main_engine_thrust == 0: return
var acceleration = main_engine_thrust / root_module.mass
# --- Use the absolute value of delta_v for burn duration ---
var burn_duration1 = abs(delta_v1) / acceleration
var burn_duration2 = abs(delta_v2) / acceleration
# --- NEW: Predict the ship's state at the time of the burn ---
var predicted_state = _predict_state_after_coast(root_module, star, wait_time)
var predicted_velocity_vec = predicted_state["velocity"]
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
var burn1 = DataTypes.ImpulsiveBurnPlan.new()
burn1.delta_v_magnitude = abs(delta_v1)
burn1.wait_time = wait_time
burn1.burn_duration = burn_duration1
# --- Determine rotation based on the sign of delta_v ---
# Prograde (speeding up) or retrograde (slowing down)
var prograde_vec = predicted_velocity_vec.normalized()
var up_vec = Vector3.UP
var target_basis = Basis.looking_at(prograde_vec, up_vec)
# For retrograde (slowing down), we look "backward"
if delta_v1 < 0:
target_basis = Basis.looking_at(-prograde_vec, up_vec)
burn1.desired_basis = target_basis # Renamed in data_types.gd
plan.append(burn1)
var burn2 = DataTypes.ImpulsiveBurnPlan.new()
burn2.delta_v_magnitude = delta_v2
burn2.wait_time = time_of_flight - burn_duration1
burn2.burn_duration = burn_duration2
# --- Determine rotation for the second burn ---
target_basis = Basis.looking_at(-prograde_vec, up_vec)
# For retrograde (slowing down), we look "backward"
if delta_v2 < 0:
target_basis = Basis.looking_at(prograde_vec, up_vec)
var target_prograde_direction = (target_body.global_position - star.global_position).orthogonal().angle()
burn2.desired_basis = target_prograde_direction if delta_v2 >= 0 else target_prograde_direction + PI
plan.append(burn2)
print("Hohmann Plan:")
print(" - Wait: %d s" % wait_time)
print(" - Burn 1: %.1f m/s (%.1f s)" % [delta_v1, burn_duration1])
print(" - Flight time: %d s" % time_of_flight)
print(" - Burn 2: %.1f m/s (%.1f s)" % [delta_v2, burn_duration2])
print("MANEUVER PLANNER: Hohmann transfer calculated. Emitting plan.")
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: 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))
# --- Initial State (relative to the primary) ---
var ghost_relative_pos = body_to_trace.global_position - primary.global_position
var ghost_relative_vel = body_to_trace.linear_velocity - primary.linear_velocity
var mu = OrbitalMechanics.G * primary.mass
for i in range(num_steps):
# --- Physics Calculation ---
var distance_sq = ghost_relative_pos.length_squared()
if distance_sq < 1.0: break
var direction = -ghost_relative_pos.normalized()
var force_magnitude = mu / distance_sq # Simplified F = mu*m/r^2 and a=F/m
var acceleration = direction * force_magnitude
# --- Integration (Euler method) ---
ghost_relative_vel += acceleration * time_step
ghost_relative_pos += ghost_relative_vel * time_step
# --- Return the final state, converted back to global space ---
return {
"position": ghost_relative_pos + primary.global_position,
"velocity": ghost_relative_vel + primary.linear_velocity
}

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://6jj1jd14cdlt"]
[ext_resource type="Script" path="res://scenes/ship/computer/shards/nav_hohman_planner.gd" id="1_attn3"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_nleqa"]
[resource]
script = ExtResource("1_nleqa")
logic_script = ExtResource("1_attn3")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,79 @@
# space_simulation/scenes/ship/computer/shards/nav_intercept_solver.gd
extends Databank
class_name InterceptSolverShard
signal solution_found(plan: Array[DataTypes.ImpulsiveBurnPlan])
signal solution_impossible
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["project_n_body_paths"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["solution_found", "solution_impossible"]
# INPUT SOCKET: Planners will call this with a projected path.
func solve_rendezvous_plan(
target_path: Array[DataTypes.PathPoint],
maneuver_type: String # e.g., "brachistochrone" or "hohmann"
):
if not is_instance_valid(root_module) or target_path.is_empty():
emit_signal("solution_impossible")
return
var rendezvous_point = find_earliest_rendezvous(target_path)
if not rendezvous_point:
emit_signal("solution_impossible")
return
# Once we have the target point (time, pos, vel), we can generate
# the specific burn plan based on the requested type.
var plan: Array[DataTypes.ImpulsiveBurnPlan]
match maneuver_type:
"brachistochrone":
plan = _generate_brachistochrone_plan(rendezvous_point)
# "hohmann" would be more complex, as it has constraints
_:
print("Unknown maneuver type for solver.")
emit_signal("solution_impossible")
return
emit_signal("solution_found", plan)
# This is the core solver logic.
func find_earliest_rendezvous(target_path: Array[DataTypes.PathPoint]) -> DataTypes.PathPoint:
# For each point in the target's future path...
for point in target_path:
# 1. Calculate the required change in position (displacement).
var delta_p = point.position - root_module.global_position
# 2. Calculate the required change in velocity.
var delta_v = point.velocity - root_module.linear_velocity
# 3. Using kinematics (d = v_initial*t + 0.5at^2), find the constant
# acceleration 'a' required to satisfy both delta_p and delta_v over
# the time 'point.time'.
# a = 2 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
var required_acceleration_vector = 2.0 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
# 4. Check if the magnitude of this required acceleration is something our ship can actually do.
var max_accel = root_module.main_engine_thrust / root_module.mass # Assumes we need a get_main_engine_thrust() helper
if required_acceleration_vector.length() <= max_accel:
# This is the first point in time we can reach. This is our solution.
return point
# If we get through the whole path and can't reach any of them, it's impossible.
return null
func _generate_brachistochrone_plan(rendezvous_point: DataTypes.PathPoint) -> Array[DataTypes.ImpulsiveBurnPlan]:
# This function would now use the data from the solved rendezvous_point
# to create the two-burn Brachistochrone plan, similar to before.
# The key difference is that all calculations are now based on a confirmed possible intercept.
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
# ... logic to build the plan ...
return plan

View File

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

View File

@ -0,0 +1,87 @@
# space_simulation/scenes/ship/computer/shards/nav_path_projection.gd
extends Databank
class_name PathProjectionShard
## Emitted after a requested path has been calculated.
signal projected_system_bus(paths: Array[DataTypes.PathPoint])
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["project_n_body_paths"]
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["projected_system_bus"]
## 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[OrbitalBody3D],
num_steps: int,
time_step: float
):
# --- Step 1: Create a "ghost state" for each body ---
# A ghost state is just a simple dictionary holding the physics properties.
var ghost_states = []
for body in bodies_to_trace:
ghost_states.append({
"body_ref": body,
"mass": body.mass,
"position": body.global_position,
"velocity": body.linear_velocity # Velocity is always in the same space
})
# --- Step 2: Prepare the results dictionary ---
var paths: Dictionary = {}
for state in ghost_states:
paths[state.body_ref] = []
# --- Step 3: Run the ghost simulation ---
for i in range(num_steps):
# Create a list to hold the forces for this time step
var forces_for_step = {}
for state in ghost_states:
forces_for_step[state.body_ref] = Vector2.ZERO
# a) Calculate all gravitational forces between the ghosts
for j in range(ghost_states.size()):
var state_a = ghost_states[j]
for k in range(j + 1, ghost_states.size()):
var state_b = ghost_states[k]
# Calculate force between the two ghost states2:
var distance_sq = state_a.position.distance_squared_to(state_b.position)
if distance_sq < 1.0: return Vector2.ZERO
var force_magnitude = (OrbitalMechanics.G * state_a.mass * state_b.mass) / distance_sq
var direction = state_a.position.direction_to(state_b.position)
var force_vector = direction * force_magnitude
# Store the forces to be applied
forces_for_step[state_a.body_ref] += force_vector
forces_for_step[state_b.body_ref] -= force_vector
# b) Integrate forces for each ghost to find its next position
for state in ghost_states:
if state.mass > 0:
var acceleration = forces_for_step[state.body_ref] / state.mass
state.velocity += acceleration * time_step
state.position += state.velocity * time_step
# c) Record the new position in the path
paths[state.body_ref].append(DataTypes.PathPoint.new(i * time_step, state.position, state.velocity))
# --- Step 4: Prepare the results dictionary ---
var projections: Array[DataTypes.PathProjection] = []
for state in ghost_states:
var projection: DataTypes.PathProjection = DataTypes.PathProjection.new(state.body_ref)
projection.points = paths[state.body_ref]
projections.append(projection)
projected_system_bus.emit(paths)

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://d4e5f6g7h8jaj"]
[ext_resource type="Script" path="res://scenes/ship/computer/shards/nav_path_projection.gd" id="1_proj"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="2_data"]
[resource]
script = ExtResource("2_data")
logic_script = ExtResource("1_proj")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,23 @@
# scenes/ship/computer/shards/nav_selection_databank.gd
extends Databank
class_name NavSelectionShard
## Emitted whenever a new navigation target is selected from the map.
signal target_selected(body: OrbitalBody3D)
var selected_body: OrbitalBody3D = null
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return ["body_selected"]
## Describes the signals this shard can output.
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: OrbitalBody3D):
if is_instance_valid(body) and body != selected_body:
print("NAV SELECTION: New target acquired - ", body.name)
selected_body = body
emit_signal("target_selected", body)

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://g4ho63f30vjm"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_d0eru"]
[ext_resource type="Script" uid="uid://t12etsdx2h38" path="res://scenes/ship/computer/shards/nav_selection_databank.gd" id="1_mt7ap"]
[resource]
script = ExtResource("1_d0eru")
logic_script = ExtResource("1_mt7ap")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,34 @@
extends Databank
class_name SensorSystemShard
signal sensor_feed_updated(bodies: Array[OrbitalBody3D])
@export_group("Projection Settings")
@export var projection_steps: int = 500
@export var time_per_step: float = 60.0 # Project at 1-minute intervals
## Describes the functions this shard needs as input.
func get_input_sockets() -> Array[String]:
return []
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return ["sensor_feed_updated"]
# We use _process instead of _physics_process to avoid slowing down the physics thread.
# This calculation can happen on a separate frame if needed.
func _process(_delta: float):
var star_system = GameManager.current_star_system
if not is_instance_valid(star_system):
return
# Gather all bodies that need to be included in the simulation.
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())
if tracked_bodies.is_empty():
return
sensor_feed_updated.emit(tracked_bodies)

View File

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

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://b0suy3sxjwhtv"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_nbqt3"]
[ext_resource type="Script" uid="uid://diu2tgusi3vmt" path="res://scenes/ship/computer/shards/sensor_databank.gd" id="1_uxkgc"]
[resource]
script = ExtResource("1_nbqt3")
logic_script = ExtResource("1_uxkgc")
metadata/_custom_type_script = "uid://osk1l75vlikn"

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,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bm1rbv4tuppbc"]
[ext_resource type="Script" uid="uid://d4jka2etva22s" path="res://scenes/tests/3d/eva_movement_component.gd" id="1_mb22m"]
[node name="EVASuitController" type="Node3D"]
script = ExtResource("1_mb22m")