2 Commits

Author SHA1 Message Date
09a4003839 Functional plugin 2025-10-03 06:04:33 +02:00
7c689f6023 WIP 2025-10-03 05:47:55 +02:00
19 changed files with 679 additions and 213 deletions

View File

@ -1,33 +1,82 @@
# Project "Stardust Drifter" Status Summary
Project "Stardust Drifter" Development Status (Phase 0.2: Custom Physics & Ship Interior)
Overview
## Implemented Systems
The project is currently focused on Phase 0.2, which involves migrating from Godot's built-in RigidBody2D physics to a custom, manually integrated OrbitalBody2D model. This is critical for supporting the multi-scale physics simulation (orbital vs. local) and preparing for networked multiplayer. The core physics model is now stable and functional.
I. Fully Implemented & Stable Systems
Custom Physics Core
- Core Physics Simulation: The OrbitalMechanics singleton successfully handles a robust N-body gravity simulation. All celestial bodies and the ship correctly inherit from OrbitalBody2D, ensuring they are part of the same physics simulation.
Physics Unification: All physically simulated objects (Spaceship, Module, Thruster, StructuralPiece) now inherit from the custom OrbitalBody2D class. This ensures a consistent hierarchy and simplified force routing.
- Procedural World Generation: The StarSystemGenerator creates a dynamic solar system with stars, planets, moons, and stations in stable, nested orbits.
Asymmetric Gravity Model (WIP Foundation): The OrbitalMechanics singleton is refactored to check if a body is a simulation root, applying forces manually (linear_velocity, angular_velocity). This is the foundation for implementing the SCALE_FACTOR for miniature ship gravity.
- Modular Ship Systems: A foundation for modular ships is in place. The Spaceship class is a central hub for subsystems like FuelSystem and LifeSupport. The ThrusterController is highly advanced, capable of self-calibrating to a ship's unique mass and inertia.
Modular Force Integration: Thrusters are now OrbitalBody2D children that correctly apply force to themselves. The force is then routed up the hierarchy to the root ship node, calculating torque based on its application point. This solved the uncontrollable spinning bug.
- Navigation & Map UI: A functional map UI exists, with zoom-to-cursor and click-and-drag panning. It includes dynamic culling and can be locked onto any celestial body. The NavigationComputer can calculate and execute Hohmann transfers.
Modular Mass Aggregation (Top-Down): The OrbitalBody2D class recursively calculates the total mass of the ship by summing the base_mass of all its component children, even those nested behind non-physics containers.
- Editor Plugin (WIP): We've started building an editor plugin to handle ship construction. The core functionality for creating a custom dock and listening for editor input has been implemented, but the full UI and piece-placement logic still needs to be completed.
Ship Interior & Crew Movement
## Designed but Unimplemented Systems
Character Zero-G Movement: The PilotBall character now has complex, state-based movement logic to simulate zero-G physics inside the ship:
- Free-Form Module Construction: The core building system is designed but not yet fully implemented. The Module and StructuralPiece scripts are in place, but the physics recalculation and room sealing logic is not yet finished.
No Control: Applies heavy drag if not overlapping a structural piece.
- Unified RigidBody2D Character Controller: The CrewMember.tscn scene and the logic for Spaceship.gd to simulate G-forces still need to be created and integrated.
Zero-G Interior: Allows sluggish control (simulating pushing off hullplates).
- Economy, Missions, and Factions: The high-level design for a multi-faction world, a mission system, and an economy exists, but no code or assets have been created for these features yet.
Ladder Grip: Provides snappy, direct control (simulating climbing/gripping).
- Multi-Depth Modules: Your idea for a 2.5D building system with multiple depth layers and a "flip" function is designed but has been tabled for later.
Ladder/Component Interaction: The character uses the interact input (Spacebar) to switch to the LADDER_GRIP state and uses the same input release to perform a velocity-based launch into the zero-G interior.
### Planned Systems: Asymmetric N-Body Physics Simulation
Ship Builder Foundation (Structural)
To handle the vast difference in scale between celestial bodies and player ships, the physics simulation will be refactored to use an asymmetric N-body approach. This means that while all celestial bodies will affect each other, smaller bodies like ships and stations will only be affected by the gravity of larger celestial bodies. This will allow the use of realistic masses for ship components while maintaining stable and predictable orbits for planets and moons.
Component Base Class (Component.gd): Created to standardize component attachment, defining properties like grid_size and AttachmentType (Interior/Exterior).
- Objective: Modify the OrbitalMechanics singleton to apply a scaling factor to gravitational calculations for player-controlled ships and components.
Module Attachment Grid: The Module.gd script now exposes a get_attachment_points() method that calculates valid grid locations based on the placement and type of underlying structural pieces (Hullplate, Bulkhead).
- Implementation: Introduce const SCALE_FACTOR = 100000.0 to the OrbitalMechanics.gd script. When calculating gravity for ships and other scaled bodies, multiply the masses of celestial bodies by this factor.
II. Work-In-Progress (WIP) and Planned Systems
- New Class: A new abstract class, ScaledOrbitalBody2D, will be created that inherits from OrbitalBody2D and handles the new scaled physics.
System
Status
Next Steps / Required Work
Ship Builder Plugin (UI)
WIP / Priority
Integrate the new attachment logic (Module.get_attachment_points()) into the CustomGrid.gd to visualize placement points. Implement the logic for the Component Tab in the dock.
Physics Cleanup (Celestial)
WIP / Required
Unify the inheritance of all celestial bodies (Star, Planet, etc.) to inherit from OrbitalBody2D, removing reliance on deprecated RigidBody2D base nodes.
Collision Handling
Planned
Implement the _recalculate_collision_shape() function in Module.gd to merge the structural piece collision shapes into a single, cohesive shape for the root ship.
Hull Sealing Logic
Designed
Implement the pressure and hull sealing logic using the HullVolume nodes.
Scaled Gravity Implementation
Designed
Implement the SCALE_FACTOR and ScaledOrbitalBody2D inheritance to test the multi-scale simulation (low-speed local movement vs. high-speed orbital movement).

View File

@ -28,14 +28,25 @@ 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_piece: StructuralPiece = null
var active_piece_scene: PackedScene = null
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)
@ -87,13 +98,11 @@ func _setup_docks():
add_control_to_dock(DOCK_SLOT_RIGHT_UL, construction_inspector_dock)
func switch_to_dock_tab(dock_control: Control, tab_name: String):
# Find the TabContainer within the dock's control node.
var tab_container = dock_control.find_child("TabContainer")
if not is_instance_valid(tab_container):
print("Error: TabContainer not found in dock control.")
return
# Iterate through the tabs to find the one with the correct name.
for i in range(tab_container.get_tab_count()):
if tab_container.get_tab_title(i) == tab_name:
tab_container.current_tab = i
@ -159,7 +168,7 @@ func _update_ui_labels():
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
@ -168,56 +177,97 @@ func _on_viewport_input(event: InputEvent) -> void:
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 InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
if is_instance_valid(preview_piece):
on_clear_preview_piece()
else:
_remove_piece_under_mouse()
if event is InputEventMouseMotion:
_update_preview_position()
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
_place_piece_from_preview()
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() == "R":
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_piece):
preview_piece.queue_free()
if is_instance_valid(preview_node):
preview_node.queue_free()
active_piece_scene = scene
preview_piece = scene.instantiate() as StructuralPiece
preview_piece.is_preview = true
builder_scene_root.add_child(preview_piece)
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_clear_preview_piece():
if is_instance_valid(preview_piece):
preview_piece.queue_free()
preview_piece = null
active_piece_scene = null
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_piece):
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()
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
preview_piece.global_position = snapped_pos
preview_piece.rotation = rotation_angle
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_piece):
if not is_instance_valid(preview_node) or not is_instance_valid(active_scene):
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
@ -233,38 +283,69 @@ func _place_piece_from_preview():
target_module.global_position = snapped_pos
target_module.owner = builder_scene_root
var piece_to_place = active_piece_scene.instantiate()
target_module.structural_container.add_child(piece_to_place)
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
# --- The Undo/Redo Logic ---
undo_redo.create_action("Place Structural Piece")
# DO method: adds the piece to the scene.
undo_redo.add_do_method(target_module.structural_container, "add_child", piece_to_place)
undo_redo.add_do_method(target_module, "add_child", piece_to_place)
undo_redo.add_do_method(piece_to_place, "set_owner", target_module)
# DO method: recalculates physics.
undo_redo.add_do_method(target_module, "_recalculate_collision_shape")
# UNDO method: removes the piece from the scene parent.
undo_redo.add_undo_method(target_module.structural_container, "remove_child", piece_to_place)
# UNDO method: recalculates physics.
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, "_update_mass_and_inertia")
undo_redo.add_undo_method(target_module, "_update_mass_and_inertia")
undo_redo.commit_action()
preview_node.global_position = closest_point.position
# --- Find Nearby Modules remains the same ---
func _find_nearby_modules(position: Vector2) -> Module:
# Define a margin for the overlap check.
const OVERLAP_MARGIN = 20.0
# Get the shape from the active piece scene.
var piece_shape = active_piece_scene.instantiate().find_child("CollisionShape2D").shape
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()
# Create a temporary, slightly larger shape for the overlap check.
var enlarged_shape
if piece_shape is RectangleShape2D:
enlarged_shape = RectangleShape2D.new()
@ -274,63 +355,78 @@ func _find_nearby_modules(position: Vector2) -> Module:
enlarged_shape.radius = piece_shape.radius + OVERLAP_MARGIN
enlarged_shape.height = piece_shape.height + OVERLAP_MARGIN
else:
# Fallback for other shapes
return null
# Use a PhysicsShapeQuery to find overlapping pieces.
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) # Limit to a single result
var result = space_state.intersect_shape(query, 1)
if not result.is_empty():
var collider = result[0].get("collider")
if collider is StructuralPiece:
if collider.get_parent() and collider.get_parent().get_parent() is Module:
return collider.get_parent().get_parent()
# --- 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 snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
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)
for node in builder_scene_root.get_children():
if node is Module:
for piece in node.structural_container.get_children():
if piece is StructuralPiece and piece.global_position == snapped_pos:
_remove_piece_with_undo_redo(piece)
return
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
var parent = piece.get_parent()
if not is_instance_valid(module) or not module is Module:
return
undo_redo.create_action("Remove Structural Piece")
print(module.structural_container.get_child_count(false))
if module.structural_container.get_child_count(false) >= 1:
# 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:
undo_redo.add_do_method(parent, "remove_child", piece)
# 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(parent, "add_child", piece)
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 ---
# --- Toolbar Button Functions (No changes needed) ---
func _on_rotate_button_pressed():
rotation_angle = wrapf(rotation_angle + PI / 2, 0, TAU)
if preview_piece:
preview_piece.rotation = rotation_angle
if is_instance_valid(preview_node):
preview_node.rotation = rotation_angle
_update_preview_position()
func _on_center_button_pressed():
@ -341,52 +437,33 @@ func _on_pressurise_button_pressed():
pass
func _on_save_button_pressed():
# Find a module to save. We'll prioritize the selected one.
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]
elif builder_scene_root.get_child_count() > 0:
for node in builder_scene_root.get_children():
if node is Module:
module_to_save = node
break
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
# Create and configure the save dialog
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"
# FIX: Add the dialog to the main editor screen, not the plugin's control node
EditorInterface.get_editor_main_screen().add_child(save_dialog)
save_dialog.popup_centered_ratio()
# Connect the signal to our new save function
save_dialog.file_selected.connect(Callable(self, "_perform_save").bind(module_to_save))
func _perform_save(file_path: String, module_to_save: Module):
# Make sure the directory exists before attempting to save.
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)
#
## FIX: Manually get the structural container reference from the duplicated module.
#var duplicate_structural_container = duplicate_module.find_child("StructuralContainer")
#
#if is_instance_valid(duplicate_structural_container):
## FIX: Correctly set the owner of all child nodes to the new root.
## This is the crucial step to ensure the children are packed correctly.
#for piece in duplicate_structural_container.get_children():
#print(piece)
#piece.owner = duplicate_module
# Pack the node into a PackedScene.
var packed_scene = PackedScene.new()
var error = packed_scene.pack(module_to_save)
@ -394,10 +471,6 @@ func _perform_save(file_path: String, module_to_save: Module):
push_error("Error packing scene: ", error_string(error))
return
# FIX: Reset the duplicated module's position so it's centered in its own scene.
#duplicate_module.global_position = Vector2.ZERO
# Save the PackedScene to a file.
var save_result = ResourceSaver.save(packed_scene, file_path)
if save_result == OK:
@ -409,7 +482,8 @@ func _perform_save(file_path: String, module_to_save: Module):
func _on_undo_redo_action_committed():
_refresh_tree_display()
# --- REFACTORED: Tree Display ---
func _refresh_tree_display():
if not is_instance_valid(tree_control):
return
@ -425,8 +499,26 @@ func _refresh_tree_display():
module_item.set_text(0, module.name)
module_item.set_meta("node", module)
for piece in module.structural_container.get_children():
if piece is StructuralPiece:
var piece_item = tree_control.create_item(module_item)
piece_item.set_text(0, piece.name)
piece_item.set_meta("node", piece)
# 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

31
modules/Module.tscn Normal file
View File

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

31
modules/New_module.tscn Normal file
View File

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

49
modules/Tube.tscn Normal file
View File

@ -0,0 +1,49 @@
[gd_scene load_steps=4 format=3 uid="uid://didt2nsdtbmra"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_nqe0s"]
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_foqop"]
[ext_resource type="PackedScene" uid="uid://d3hitk62fice4" path="res://scenes/ship/builder/pieces/bulkhead.tscn" id="4_dmrms"]
[node name="Module" type="Node2D"]
script = ExtResource("1_nqe0s")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
[node name="Hullplate" parent="." instance=ExtResource("2_foqop")]
[node name="@StaticBody2D@30634" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, 100)
[node name="@StaticBody2D@30635" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, -100)
[node name="Bulkhead" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 100)
[node name="@StaticBody2D@30636" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 0)
[node name="@StaticBody2D@30637" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, -100)
[node name="@StaticBody2D@30638" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, -100)
[node name="@StaticBody2D@30639" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, -150)
rotation = 1.5708
[node name="@StaticBody2D@30640" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, 150)
rotation = 4.71239
[node name="@StaticBody2D@30641" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 100)
[node name="@StaticBody2D@30642" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 0)

View File

@ -1,71 +1,156 @@
extends CharacterBody2D
class_name PilotBall
# Local movement speed when unattached (e.g., inside a ship or during EVA)
const LOCAL_SPEED = 200.0
# --- Movement Constants (Friction Simulation) ---
# When in open space (no module overlap), movement is zeroed out quickly.
const EXTERIOR_DRAG_FACTOR: float = 0.05
var attached_to_station: Node2D = null
var owning_ship: RigidBody2D = null # The ship the character is currently anchored to.
# When pushing off hullplates (low friction, slow acceleration)
const INTERIOR_SLUGGISH_SPEED: float = 100.0
const INTERIOR_SLUGGISH_ACCEL: float = 0.15 # Low acceleration, simulating mass and small push
# When gripping a ladder (high friction, direct control)
const LADDER_SPEED: float = 350.0
const LADDER_ACCEL: float = 0.9 # High acceleration, simulating direct grip
# --- State Variables ---
enum MovementState {
NO_CONTROL,
ZERO_G_INTERIOR,
LADDER_GRIP
}
var current_state: MovementState = MovementState.NO_CONTROL
var ladder_area: Area2D = null # Area of the ladder currently overlapped
var is_grabbing_ladder: bool = false # True if 'Space' is held while on ladder
# --- Overlap Detection (Assuming you use Area2D for detection) ---
var overlapping_modules: int = 0
# --- Ladder Constants ---
const LAUNCH_VELOCITY: float = 300.0
# --- New: Physics Initialization (Assuming CharacterBody2D is parented to the scene root or Ship) ---
# NOTE: CharacterBody2D cannot inherit OrbitalBody2D, so we manage its velocity manually.
func _ready():
# Assume the ship is a parent somewhere up the tree for now.
# This should be set upon spawning inside a ship.
owning_ship = get_parent().find_parent("Spaceship")
# Set up overlap signals if they aren't already connected in the scene file
# You must have an Area2D child on PilotBall to detect overlaps.
# Placeholder: Assuming the PilotBall has an Area2D named 'OverlapChecker'
var overlap_checker = find_child("OverlapChecker")
if overlap_checker:
overlap_checker.body_entered.connect(on_body_entered)
overlap_checker.body_exited.connect(on_body_exited)
# Ensure this action is set in project settings: "interact" mapped to Space.
if !InputMap.has_action("interact"):
push_error("Missing 'interact' input action for ladder logic.")
func on_body_entered(body: Node2D):
# Detect Modules (which all inherit OrbitalBody2D via StructuralPiece)
if body is StructuralPiece:
overlapping_modules += 1
# Detect Ladders
if body is Ladder:
ladder_area = body.find_child("ClimbArea") # Assuming the Ladder has a specific Area2D for climbing
func on_body_exited(body: Node2D):
if body is StructuralPiece:
overlapping_modules -= 1
if body is Ladder:
if body.find_child("ClimbArea") == ladder_area:
ladder_area = null
is_grabbing_ladder = false # Force detach if the ladder moves away
func _physics_process(delta):
# If attached, do not move locally. The station handles movement control (thrusters).
if is_attached():
velocity = Vector2.ZERO
return
# 1. Update State based on environment
_update_movement_state()
var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
match current_state:
MovementState.NO_CONTROL:
# Apply heavy drag to simulate floating in space without external push
_apply_drag(EXTERIOR_DRAG_FACTOR)
MovementState.ZERO_G_INTERIOR:
# Sluggish movement: player is pushing off nearby walls/hullplates
_sluggish_movement(input_dir, delta)
MovementState.LADDER_GRIP:
# Snappy movement: direct control and high acceleration
_ladder_movement(input_dir, delta)
# Local movement: Use A/D/W/S to move the ball around.
var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input_direction * LOCAL_SPEED
# 2. Handle Ladder Grab/Launch Input
_handle_ladder_input(input_dir)
move_and_slide()
func is_attached():
return attached_to_station != null
func attach(station: Node2D):
if is_attached(): return
attached_to_station = station
# 1. Store the global position before changing parent.
var old_global_pos = global_position
# 2. Change the parent to the station/ship (Anchoring)
# This makes the ball's movement purely relative to the ship.
reparent(station)
# 3. Reset local position. Its new global position is defined relative to the ship/station.
global_position = old_global_pos
# 4. Notify the station it is now in control
station.set_pilot(self)
# --- State Machine Update ---
func detach():
if not is_attached(): return
var station = attached_to_station
attached_to_station = null
# 1. Notify the station to release control
station.set_pilot(null)
# 2. Store the current global transform (which is relative to the ship)
var new_global_pos = global_position
# 3. Reparent back to the main world (or a dedicated 'space' node)
# This is the critical moment: the node re-enters the high-velocity global space.
if owning_ship and owning_ship.get_parent():
reparent(owning_ship.get_parent())
func _update_movement_state():
# Priority 1: Ladder Grip
if ladder_area and Input.is_action_pressed("interact"):
is_grabbing_ladder = true
current_state = MovementState.LADDER_GRIP
return
# 4. Restore global position and inherit the ship's massive orbital velocity
global_position = new_global_pos
velocity = owning_ship.linear_velocity
else:
# Fallback if ship structure is unknown
reparent(get_tree().root)
global_position = new_global_pos
velocity = Vector2.ZERO
# Priority 2: Interior Zero-G (must overlap a module/piece AND not be grabbing)
if overlapping_modules > 0:
if is_grabbing_ladder:
# If we were grabbing a ladder but released 'interact', we transition to zero-G interior
is_grabbing_ladder = false
current_state = MovementState.ZERO_G_INTERIOR
return
current_state = MovementState.ZERO_G_INTERIOR
return
# Priority 3: No Control (floating free)
is_grabbing_ladder = false
current_state = MovementState.NO_CONTROL
# --- Movement Implementations ---
func _apply_drag(factor: float):
# Gently slow down the velocity (simulating environmental drag)
velocity = velocity.lerp(Vector2.ZERO, factor)
func _sluggish_movement(input_dir: Vector2, delta: float):
# Simulates pushing off the wall: slow acceleration, but minimal drag
var target_velocity = input_dir * INTERIOR_SLUGGISH_SPEED
velocity = velocity.lerp(target_velocity, INTERIOR_SLUGGISH_ACCEL)
func _ladder_movement(input_dir: Vector2, delta: float):
# Simulates direct grip: fast acceleration, perfect control
var target_velocity = input_dir * LADDER_SPEED
velocity = velocity.lerp(target_velocity, LADDER_ACCEL)
# --- Ladder Input and Launch Logic ---
func _handle_ladder_input(input_dir: Vector2):
# If currently grabbing, SPACE press is handled in _update_movement_state
if current_state == MovementState.LADDER_GRIP:
if Input.is_action_just_released("interact"):
# Launch the player away from the ladder
# Determine launch direction: opposite of input, or default forward
var launch_direction = -input_dir.normalized()
if launch_direction == Vector2.ZERO:
# Default launch: use the character's forward direction (e.g., rotation 0)
launch_direction = Vector2.UP.rotated(rotation)
velocity = launch_direction * LAUNCH_VELOCITY
# Immediately switch to zero-G interior state
is_grabbing_ladder = false
current_state = MovementState.ZERO_G_INTERIOR

View File

@ -1,48 +1,94 @@
@tool
class_name Module
extends OrbitalBody2D
@onready var structural_container: Node2D = $StructuralContainer
@onready var hull_volume_container: Node2D = $HullVolumeContainer
# REMOVED: @onready vars for containers are no longer needed.
# The function name is updated to reflect its new, limited scope.
func _recalculate_collision_shape():
# This logic should typically be on the main Spaceship node,
# but for modularity, the Module can trigger it on its children.
const COMPONENT_GRID_SIZE = 64.0
# --- NEW: Helper functions to get children by type ---
func get_structural_pieces() -> Array[StructuralPiece]:
var pieces: Array[StructuralPiece]
for child in get_children():
if child is StructuralPiece:
pieces.append(child)
return pieces
func get_components() -> Array[Component]:
var components: Array[Component]
for child in get_children():
if child is Component:
components.append(child)
return components
# --- UPDATED: Logic now uses the helper function ---
func get_attachment_points() -> Array:
var points = []
# 1. Clear any existing combined collision shape on this module.
# (You would likely have a central CollisionShape2D node for the combined shape)
# var combined_shape_node = find_child("CombinedCollisionShape")
# if combined_shape_node:
# for child in combined_shape_node.get_children(): child.queue_free()
# Iterate through all StructuralPiece children directly
for piece in get_structural_pieces():
var piece_center = piece.global_position
# --- Hullplates (Interior Grid) ---
if piece is Hullplate:
for i in range(-1, 2, 2):
for j in range(-1, 2, 2):
var offset = Vector2(i, j) * (COMPONENT_GRID_SIZE / 2.0)
points.append({
"position": piece_center + offset,
"type": Component.AttachmentType.INTERIOR_WALL,
"piece": piece
})
# --- Bulkheads (Interior and Exterior Edge Attachments) ---
elif piece is Bulkhead:
var interior_point = piece_center + piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
points.append({
"position": interior_point,
"type": Component.AttachmentType.INTERIOR_WALL,
"piece": piece
})
var exterior_point = piece_center - piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
points.append({
"position": exterior_point,
"type": Component.AttachmentType.EXTERIOR_HULL,
"piece": piece
})
# 2. Iterate through all StructuralPiece children (which are now OrbitalBody2D)
# and gather their global collision transforms/shapes.
return points
# --- This function remains largely the same ---
func attach_component(component: Component, global_pos: Vector2, parent_piece: StructuralPiece):
component.position = global_pos - global_position
component.attached_piece = parent_piece
add_child(component)
component.owner = self
_update_mass_and_inertia()
# --- UPDATED: Logic now uses the helper function ---
func _recalculate_collision_shape():
# This logic is much simpler now. We just iterate over relevant children.
var combined_polygons = []
for child in get_children():
# StructuralPiece now inherits OrbitalBody2D, so this check is valid
if child is StructuralPiece:
# You would use logic here to transform the piece's local shape
# into the Module's local space and add it to the list.
# Example Placeholder (requires full implementation):
# var piece_collision_shape = child.find_child("CollisionShape2D")
# if piece_collision_shape:
# combined_polygons.append(piece_collision_shape.shape.points)
pass
for piece in get_structural_pieces():
# You would use logic here to transform the piece's local shape
# into the Module's local space and add it to the list.
# Example Placeholder (requires full implementation):
# var piece_collision_shape = piece.find_child("CollisionShape2D")
# if piece_collision_shape:
# combined_polygons.append(piece_collision_shape.shape.points)
pass
# 3. Create a new shape (e.g., ConcavePolygonShape2D) and assign it.
# This part is complex and usually requires an external library or custom code
# to merge multiple 2D shapes efficiently.
# After implementation, you may want to signal the change:
# SignalBus.module_structure_changed.emit(self)
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass and center of mass!
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass!
pass
# --- UPDATED: Clear module now iterates over all relevant children ---
func clear_module():
for piece in structural_container.get_children():
# We queue_free both structural pieces and components
for piece in get_structural_pieces():
piece.queue_free()
for component in get_components():
component.queue_free()
_recalculate_collision_shape()

View File

@ -0,0 +1,6 @@
@tool
class_name Bulkhead
extends StructuralPiece
# This piece represents a wall or edge.
# No additional logic is needed right now, we just need the class_name.

View File

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

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://d3hitk62fice4"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_1wp2n"]
[ext_resource type="Script" uid="uid://b4g288mje38nj" path="res://scenes/ship/builder/pieces/bulkhead.gd" id="1_1wp2n"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
size = Vector2(10, 100)

View File

@ -0,0 +1,6 @@
@tool
class_name Hullplate
extends StructuralPiece
# This piece represents an interior surface.
# No additional logic is needed right now, we just need the class_name.

View File

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

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://bho8x10x4oab7"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_ecow4"]
[ext_resource type="Script" uid="uid://crmwm623rh1ps" path="res://scenes/ship/builder/pieces/hullplate.gd" id="1_ecow4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
size = Vector2(100, 100)

View File

@ -1,2 +1,25 @@
class_name ShipComponent
extends OrbitalBody2D
class_name Component
# Defines the size of the component in terms of the grid (e.g., 1x1, 1x2, 2x2)
@export_range(1, 4, 1, "suffix:x") var grid_size_x: int = 1
@export_range(1, 4, 1, "suffix:x") var grid_size_y: int = 1
# Specifies the type of structural piece surface this component can attach to.
# Used by the editor to validate placement.
enum AttachmentType { INTERIOR_WALL, EXTERIOR_HULL, FLOOR_OR_CEILING }
@export var attachment_type: AttachmentType = AttachmentType.INTERIOR_WALL
# Reference to the StructuralPiece this component is physically attached to
var attached_piece: StructuralPiece = null
func _ready():
# OrbitalBody2D will handle mass initialization and physics setup.
pass
# Components can implement activation logic here (e.g., Thruster fires, Life Support starts)
func activate():
pass
func deactivate():
pass

View File

@ -0,0 +1,32 @@
extends Component
class_name Ladder
# --- Component Properties ---
# A standard ladder is typically one grid unit wide.
# We make the height variable to allow for multi-story climbing in one piece.
@export var ladder_grid_height: int = 4 # Height in grid units (e.g., 4x64px)
# --- Inherited OrbitalBody2D & Component Setup ---
func _ready():
# Set the base mass based on its material/size
base_mass = float(ladder_grid_height) * 25.0 # Example: 25kg per grid unit height
# Set inherited Component properties
grid_size_x = 1
grid_size_y = ladder_grid_height
attachment_type = AttachmentType.INTERIOR_WALL
# Call the parent's _ready to ensure mass calculation is triggered
super._ready()
# You would add logic here to dynamically resize the ladder's visual and collision
# shape to match 'ladder_grid_height' if necessary.
if Engine.is_editor_hint():
# Hide mass from the inspector if the base class doesn't need to see it
# You can also set a custom icon for the editor here.
pass
# The player character's script will be responsible for checking if it overlaps
# this component and entering a 'climbing' state, using the ladder's position and height.

View File

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

View File

@ -0,0 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://dxtxb2p7lpt51"]
[ext_resource type="Script" uid="uid://bh1t0cqdjm5ye" path="res://scenes/ship/components/ladder.gd" id="1_ygkvf"]
[node name="Ladder" type="Node2D"]
script = ExtResource("1_ygkvf")
metadata/_custom_type_script = "uid://bh1t0cqdjm5ye"
[node name="ClimbArea" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="ClimbArea"]
[node name="Sprite2D" type="Sprite2D" parent="."]

View File

@ -1,6 +1,6 @@
# Thruster.gd
class_name Thruster
extends ShipComponent
extends Component
@onready var pin_joint_a: PinJoint2D = $PinJointA
@onready var pin_joint_b: PinJoint2D = $PinJointB

View File

@ -31,7 +31,7 @@ func _ready():
# FIX: Enable _physics_process for ALL OrbitalBody2D nodes (including Thrusters).
# The 'if is_sim_root' inside _physics_process will prevent integration for children.
set_physics_process(true)
set_physics_process(Engine.is_editor_hint())
# --- PUBLIC FORCE APPLICATION METHODS ---
# This method is called by a component (like Thruster) at its global position.