Editor plugin WIP

This commit is contained in:
olof.pettersson
2025-09-24 10:44:38 +02:00
parent 5efeef7d8c
commit ea1aa6ea5d
20 changed files with 501 additions and 217 deletions

1
.gitignore vendored
View File

@ -3,5 +3,4 @@
.vscode/
/android/
/addons/
*.tmp

16
Development_Status.md Normal file
View File

@ -0,0 +1,16 @@
### Project "Stardust Drifter" Status Summary
**Implemented Systems**
- **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.
- **Procedural World Generation:** The `StarSystemGenerator` creates a dynamic solar system with stars, planets, moons, and stations in stable, nested orbits.
- **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.
- **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.
- **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.
**Designed but Unimplemented Systems**
- **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.
- **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.
- **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.
- **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.

120
GAME_DESIGN_DOCUMENT.md Normal file
View File

@ -0,0 +1,120 @@
# Game Design Document: Project Stardust Drifter (Working Title)
## 1. Game Vision & Concept
Project Stardust Drifter is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
The game's aesthetic is inspired by the technical, gritty, and high-contrast 2D style of games like Barotrauma, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill.
## 2. Core Gameplay Loop
The gameplay is centered around a Plan -> Execute -> Manage loop:
1. Plan: The crew uses the Navigation Computer to analyze their orbit and plan complex maneuvers, such as a Hohmann transfer to another planet. They must account for launch windows, fuel costs, and travel time.
2. Execute: The crew engages the autopilot or manually pilots the ship. The Thruster Controller executes the planned burns, performing precise, fuel-optimal rotations and main engine thrusts to alter the ship's trajectory.
3. Manage: While underway, the crew manages the ship's modular systems, monitors resources like fuel and power, and responds to emergent events like hull breaches or system failures.
## 3. Key Features
### 1. Procedural Star System
The game world is a procedurally generated star system created by the StarSystemGenerator. Each system features a central star, a variable number of planets, moons, and asteroid belts, creating a unique environment for each playthrough.
### 2. N-Body Physics Simulation
Major bodies in orbit (CelestialBody class) are goveerened by a simplified n-body gravity simulation. Physical objects with player interaction (ships, crew characters, detached components, and eventually stations) are governed by a realistic N-body gravitational simulation, managed by the OrbitalMechanics library.
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics.
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
### 3. Modular Spaceship
The player's ship is not a monolithic entity but a collection of distinct, physically simulated components attached by joints. Key modules include:
- Spaceship: The main RigidBody2D hull that tracks overall mass, inertia, and health.
- Thruster: Self-contained RigidBody2D components that apply their own force. Their role (main engine, RCS, etc.) is an emergent property of their placement on the hull.
- ThrusterController: The "brains" of the ship's movement, featuring a sophisticated autopilot that can execute fuel-optimal "bang-coast-bang" rotational maneuvers and a PD controller for stable attitude hold.
- FuelSystem & LifeSupport: Centralized managers for resources and internal ship environment. Hull breaches can create thrust vectors from escaping atmosphere.
- On-board Sensors: Diegetic components like Accelerometers allow the crew to calibrate ship performance by test-firing thrusters and measuring the true physical output.
### 4. Advanced Navigation Computer
This is the primary crew interface for long-range travel.
- Maneuver Planning: The computer can calculate various orbital transfers, each with strategic trade-offs:
- Hohmann Transfer: The most fuel-efficient route.
- Fast Transfer: A quicker but more fuel-intensive option.
- Brachistochrone (Torchship) Trajectory: For ships with high-efficiency engines like Ion Drives, enabling constant-thrust travel.
- Gravity Assist: Planned for future implementation.
- Tactical Map: A fully interactive UI map that replaces custom drawing with instanced, clickable icons for all bodies. It features:
- Zoom-to-cursor and click-and-drag panning.
- Predictive orbital path drawing for all objects.
- Icon culling at a distance to reduce clutter.
- Custom hover effects and detailed tooltips with "sensor data."
- A "picture-in-picture" SubViewport showing the ship's main camera view.
### 5. Multi-Species Crew (Player Classes)
Character progression is based on distinct species with physical advantages and disadvantages, rather than a point-based system.
- Humanoid: A generalist base class.
- Multi-armed Heavy: Can perform more tasks simultaneously but has higher mass, requiring more fuel for EVAs and special considerations during high-G burns.
- Hard Vacuum Monster: An EVA specialist that can cling to the hull but is vulnerable in pressurized environments.
- Ship AI: A non-physical class that interacts directly with ship systems at the cost of high power and heat generation.
## 4. Technical Overview
- Architecture: The project uses a decoupled, modular architecture heavily reliant on a global SignalBus for inter-scene communication and a GameManager for global state. Ships feature their own local ShipSignalBus for internal component communication.
- Key Scripts:
- OrbitalBody2D.gd: The base class for all physical objects.
- Spaceship.gd: The central hub for a player ship.
- Thruster.gd: A self-contained, physically simulated thruster component.
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
- MapDrawer.gd: A Control node that manages the interactive map UI.
- MapIcon.gd: The reusable UI component for map objects.
- Art Style: Aims for a Barotraumainspired aesthetic using 2D ragdolls (Skeleton2D, PinJoint2D), detailed sprites with normal maps, and high-contrast dynamic lighting (PointLight2D, LightOccluder2D).
## 5. Game Progression & Economy
This is the biggest area for potential expansion. A new section could detail how the player engages with the world and improves their situation over time.
## 1. The Core Objective:
What is the player's primary motivation? Is it pure sandbox exploration, or is there an overarching goal (e.g., paying off a debt, reaching a distant star, uncovering a mystery)? Defining this will help shape the rest of the progression systems.
### 2. Mission & Contract System: How do players get tasks?
#### 1. Types:
Detail potential mission types (e.g., Cargo Hauling, Passenger Transport, Asteroid Surveying, Salvage Operations, Scientific Data Collection).
#### 2. Sources:
Where do players find missions? (e.g., Station bulletin boards, faction representatives, distress signals).
#### 3. Economy & Resources: How does the in-game economy function?
- Currency: What is the primary currency?
- Resource Management: Beyond fuel, what other resources are critical? (e.g., Spare Parts for repairs, Life Support supplies, Power Cells).
- Markets: Will there be dynamic markets at stations where players can buy low and sell high?
## 6. Emergent Events & Hazards
You mention "emergent events" in the gameplay loop. It would be beneficial to detail what these could be. This helps in planning the systems needed to trigger and manage them.
### 1. System Failures:
- Component Wear & Tear: Do ship modules degrade over time or from stress (e.g., high-G burns)? This would create a need for maintenance and repair gameplay.
- Power Grid Management: Could overloading the ship's reactor cause brownouts or damage to modules?
### 2. Environmental Hazards:
- Micrometeoroid Showers: Random events that can cause minor hull damage.
- Radiation Belts: Areas around planets that could interfere with sensors or harm the crew if the ship isn't properly shielded.
- Derelict Ships/Stations: Opportunities for exploration and salvage, but with potential dangers.
## 7. Crew Interaction & Ship Interior
Since co-op and crew management are central, detailing this aspect is crucial.
### 1. Ship Interior Management:
- Diegetic Interfaces: You mention this in the vision. It's worth specifying how the crew will interact with systems. Will they need to be at a specific console (like the Navigation Computer) to use it? Do repairs require a character to physically be at the damaged module?
- Atmospherics & Life Support: How is the ship's interior environment simulated? Will fires or toxic gas leaks be a possibility? This ties directly into your LifeSupport system.
### 2. Character States:
- Health & Injury: How are characters affected by hazards? Can they be injured in high-G maneuvers or from system failures?
- EVA (Extra-Vehicular Activity): Detail the mechanics for EVAs. What equipment is needed? How is movement handled in zero-G? This would be a perfect role for the "Hard Vacuum Monster" species.

View File

@ -0,0 +1,153 @@
@tool
extends Control
# The directory where all structural piece scenes are stored.
const PIECE_DIRECTORY = "res://scenes/ship/builder/"
@onready var pieces_container: HBoxContainer = $VBoxContainer/PiecesContainer
@onready var clear_button: Button = $VBoxContainer/ClearButton
var preview_piece: StructuralPiece = null
var active_piece_scene: PackedScene = null
var rotation_angle: float = 0.0
var grid_size: float = 32.0
func _ready() -> void:
if not Engine.is_editor_hint():
return
# Load pieces dynamically at startup.
_setup_piece_buttons()
clear_button.pressed.connect(_on_clear_button_pressed)
# This is the correct way to get input from the 2D viewport.
var viewport = EditorInterface.get_editor_main_screen().get_viewport_control()
if viewport:
viewport.gui_input.connect(_on_viewport_input)
func _has_main_screen():
return true
func _setup_piece_buttons() -> void:
# Clear old buttons.
for child in pieces_container.get_children():
child.queue_free()
# Scan for all scenes in the designated folder.
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()
print("Foo")
if root and root is StructuralPiece:
print("bar")
var button = Button.new()
button.text = root.name
button.pressed.connect(Callable(self, "_on_piece_button_pressed").bind(piece_scene))
# Add a visual representation to the button.
var icon = TextureRect.new()
icon.texture = root.get_node("ColorRect").get_texture() # Assuming a ColorRect is used for visuals
icon.expand_mode = TextureRect.EXPAND_KEEP_SIZE
button.icon = icon.texture
pieces_container.add_child(button)
root.queue_free() # Clean up the instantiated node.
func _on_viewport_input(event: InputEvent) -> void:
if not active_piece_scene:
return
# Handle preview movement.
if event is InputEventMouseMotion:
_update_preview_position(event.position)
# Handle rotation.
if event is InputEventKey and event.is_pressed() and event.as_text() == "R":
rotation_angle = wrapf(rotation_angle + PI / 2, 0, TAU)
if preview_piece:
preview_piece.rotation = rotation_angle
# Handle placement on mouse click.
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
_place_piece_from_preview(event.position)
func _on_piece_button_pressed(scene: PackedScene):
if is_instance_valid(preview_piece):
preview_piece.queue_free()
active_piece_scene = scene
preview_piece = active_piece_scene.instantiate() as StructuralPiece
preview_piece.is_preview = true
get_tree().get_edited_scene_root().add_child(preview_piece)
_update_preview_position(EditorInterface.get_editor_main_screen().get_global_mouse_position())
func _update_preview_position(mouse_pos: Vector2):
if preview_piece:
var viewport_control = EditorInterface.get_editor_main_screen().get_viewport_control()
var grid_pos = viewport_control.get_global_mouse_position().snapped(Vector2(grid_size, grid_size))
preview_piece.global_position = grid_pos
preview_piece.rotation = rotation_angle
func _place_piece_from_preview(mouse_pos: Vector2):
if not is_instance_valid(preview_piece):
return
var viewport_control = EditorInterface.get_editor_main_screen().get_viewport_control()
var snapped_pos = viewport_control.get_global_mouse_position().snapped(Vector2(grid_size, grid_size))
var nearby_modules = _find_nearby_modules(snapped_pos)
if not nearby_modules.is_empty():
var target_module = nearby_modules[0]
preview_piece.is_preview = false
preview_piece.reparent(target_module.structural_container)
preview_piece.position = target_module.structural_container.to_local(snapped_pos)
target_module._recalculate_physics_properties()
else:
var new_module_scene = preload("res://scenes/ship/builder/module.tscn")
var new_module = new_module_scene.instantiate() as Module
get_tree().get_edited_scene_root().add_child(new_module)
preview_piece.is_preview = false
preview_piece.reparent(new_module.structural_container)
preview_piece.position = new_module.structural_container.to_local(snapped_pos)
new_module._recalculate_physics_properties()
_on_piece_button_pressed(active_piece_scene)
func _find_nearby_modules(position: Vector2) -> Array[Module]:
var modules = []
for node in get_tree().get_edited_scene_root().get_children():
if node is Module:
if node.global_position.distance_to(position) < grid_size * 2:
modules.append(node)
return modules
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,42 @@
[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 = 2
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="VBoxContainer"]
layout_mode = 2
text = "Select Piece"
[node name="PiecesContainer" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="BulkheadButton" type="Button" parent="VBoxContainer/PiecesContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Bulkhead"
[node name="HullplateButton" type="Button" parent="VBoxContainer/PiecesContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Backplate"
[node name="ClearButton" type="Button" parent="VBoxContainer/PiecesContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Clear Module"

View File

@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://bsubuh3qxqs8e"]
[node name="RightPanel" 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="VBoxContainer" 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,34 @@
@tool
extends EditorPlugin
var main_screen: Control = null
var builder_dock_scene = preload("res://addons/module_builder_plugin/module_editor.tscn")
func _enter_tree():
# This function is called when the plugin is enabled.
# It instantiates the custom editor scene and adds it to the editor's main screen.
main_screen = builder_dock_scene.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_screen)
main_screen.hide()
func _exit_tree():
# This function is called when the plugin is disabled.
# It cleans up the custom editor scene and frees its memory.
if main_screen:
main_screen.queue_free()
func _has_main_screen():
# By returning true, we tell Godot that this plugin has its own main screen,
# which will appear as a new tab in the editor.
return true
func _make_visible(visible):
# This function is called by the editor when the plugin's tab is selected.
if main_screen:
main_screen.visible = visible
func _get_plugin_name():
return "Ship Builder"
func _get_plugin_icon():
return EditorInterface.get_editor_theme().get_icon("Node", "EditorIcons")

View File

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

View File

@ -0,0 +1,29 @@
[gd_scene format=3 uid="uid://b018j62t6j24l"]
[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
[node name="MainViewport" type="SubViewportContainer" parent="."]
layout_mode = 2
offset_left = -121.0
offset_top = -170.0
offset_right = 1273.0
offset_bottom = 818.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="SubViewport" type="SubViewport" parent="MainViewport"]
transparent_bg = true
handle_input_locally = false
size = Vector2i(1394, 988)
render_target_update_mode = 4
[node name="Camera2D" type="Camera2D" parent="MainViewport/SubViewport"]
zoom = Vector2(0.5, 0.5)
[node name="CustomGrid" type="Node2D" parent="MainViewport/SubViewport"]

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"

View File

@ -11,7 +11,7 @@ config_version=5
[application]
config/name="space_simulation"
run/main_scene="uid://bvogqgqig1hps"
run/main_scene="uid://dogqi2c58qdc0"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"
@ -23,7 +23,7 @@ GameManager="*res://scripts/singletons/game_manager.gd"
[editor_plugins]
enabled=PackedStringArray()
enabled=PackedStringArray("res://addons/module_builder_plugin/plugin.cfg")
[input]

View File

@ -0,0 +1,66 @@
@tool
class_name Module
extends RigidBody2D
@onready var structural_container: Node2D = $StructuralContainer
@onready var hull_volume_container: Node2D = $HullVolumeContainer
func _recalculate_physics_properties():
print("Recalculating physics properties...")
# 1. Sum masses and find a weighted center of mass
var total_mass: float = 0.0
var total_mass_pos = Vector2.ZERO
var all_structural_pieces = structural_container.get_children()
for piece in all_structural_pieces:
if piece is StructuralPiece:
total_mass += piece.piece_mass
# The position is relative to the structural_container.
total_mass_pos += piece.position * piece.piece_mass
if total_mass > 0:
var new_center_of_mass = total_mass_pos / total_mass
# We set the center of mass in the RigidBody2D.
self.set_center_of_mass(new_center_of_mass)
self.mass = total_mass
# --- Combine all collision shapes into one for the RigidBody ---
# This is a key part of your design, allowing for efficient physics calculations.
# Note: This is an expensive operation and should only be done on demand.
# Remove any existing collision shapes from this RigidBody2D
for child in get_children():
if child is CollisionShape2D or child is CollisionPolygon2D:
child.queue_free()
var shape = ConcavePolygonShape2D.new()
var all_points: PackedVector2Array = []
# Iterate through all structural pieces to get their collision polygons
for piece in all_structural_pieces:
if piece is StructuralPiece:
for child in piece.get_children():
if child is CollisionPolygon2D:
# Get the points in the structural piece's local space
var piece_points = child.polygon
var piece_xform = piece.transform
# Transform the points to the module's local space
for p in piece_points:
all_points.append(piece_xform.xform(p))
shape.set_segments(all_points)
var new_collision_shape = CollisionShape2D.new()
new_collision_shape.shape = shape
add_child(new_collision_shape)
print("Physics Recalculated: Total Mass: %.2f kg" % total_mass)
else:
print("No structural pieces found. Physics properties not updated.")
func clear_module():
for piece in structural_container.get_children():
piece.queue_free()
_recalculate_physics_properties()

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cm0rohkr6khd1"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module_builder_controller.gd" id="1_b1h2b"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_b1h2b"]
[node name="Module" type="RigidBody2D"]
script = ExtResource("1_b1h2b")

View File

@ -1,12 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://ds3qq4yg8y86y"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module_builder_controller.gd" id="1_b1h2b"]
[node name="Module" type="RigidBody2D"]
script = ExtResource("1_b1h2b")
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]

View File

@ -1,188 +0,0 @@
@tool
class_name ModuleBuilderController
extends RigidBody2D
# --- Editor Tool Properties ---
var available_pieces: Dictionary = {}
@export var piece_to_place: String = "Refresh":
set(value):
piece_to_place = value
if is_instance_valid(current_piece_preview):
current_piece_preview.queue_free()
_create_piece_preview()
@export var grid_size: int = 50.0
# --- Tool State ---
var current_piece_preview: StructuralPiece
var piece_rotation: int = 0 # 0, 90, 180, 270 degrees
@onready var structural_container: Node2D = $StructuralContainer
@onready var hull_volume_container: Node2D = $HullVolumeContainer
@onready var atmosphere_visualizer: Node2D = $AtmosphereVisualizer
enum ModuleState { IN_CONSTRUCTION, SEALED, PRESSURIZED, BREACHED }
var current_state: ModuleState = ModuleState.IN_CONSTRUCTION
func _get_property_list() -> Array:
var properties = []
var piece_names = available_pieces.keys()
piece_names.sort()
piece_names.insert(0, "Refresh")
properties.append({
"name": "piece_to_place",
"type": TYPE_STRING,
"hint": PROPERTY_HINT_ENUM,
"hint_string": ",".join(piece_names),
})
properties.append({
"name": "grid_size",
"type": TYPE_INT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "10,200,10"
})
return properties
# The fix: This function tells the editor we want to handle all input events.
func _handles_input_event(event: InputEvent) -> bool:
return Engine.is_editor_hint()
func _ready() -> void:
if Engine.is_editor_hint():
_scan_for_pieces()
set_physics_process(false)
func _enter_tree():
if Engine.is_editor_hint():
if not is_instance_valid(current_piece_preview) and piece_to_place != "Refresh":
_create_piece_preview()
func _exit_tree():
if Engine.is_editor_hint() and is_instance_valid(current_piece_preview):
current_piece_preview.queue_free()
func _input(event: InputEvent) -> void:
print("foo")
if not Engine.is_editor_hint():
return
if not is_instance_valid(current_piece_preview):
return
if event is InputEventMouseMotion:
var mouse_pos_in_world = get_viewport().get_mouse_position()
var snapped_pos = _snap_to_grid(mouse_pos_in_world)
current_piece_preview.position = to_local(snapped_pos)
get_viewport().set_input_as_handled()
if event.is_action_pressed("ui_rotate"):
piece_rotation = (piece_rotation + 90) % 360
current_piece_preview.rotation_degrees = piece_rotation
get_viewport().set_input_as_handled()
if event is InputEventMouseButton and event.is_pressed():
if event.button_index == MOUSE_BUTTON_LEFT:
_place_piece_from_preview()
_create_piece_preview()
get_viewport().set_input_as_handled()
elif event.button_index == MOUSE_BUTTON_RIGHT:
if is_instance_valid(current_piece_preview):
current_piece_preview.queue_free()
_create_piece_preview()
get_viewport().set_input_as_handled()
func _scan_for_pieces():
available_pieces.clear()
var directory = DirAccess.open("res://scenes/ship/builder/")
if directory:
directory.list_dir_begin()
var file_name = directory.get_next()
while file_name != "":
if file_name.ends_with(".tscn"):
var path = "res://scenes/ship/builder/" + file_name
var scene = load(path)
if scene and scene is PackedScene:
var instance = scene.instantiate()
if instance is StructuralPiece:
available_pieces[file_name.trim_suffix(".tscn")] = scene
instance.queue_free()
file_name = directory.get_next()
directory.list_dir_end()
notify_property_list_changed()
else:
print("Could not open directory: res://scenes/ship/builder/")
func _create_piece_preview():
if is_instance_valid(current_piece_preview):
current_piece_preview.queue_free()
if available_pieces.has(piece_to_place):
var piece_scene: PackedScene = available_pieces[piece_to_place]
current_piece_preview = piece_scene.instantiate() as StructuralPiece
add_child(current_piece_preview)
current_piece_preview.is_preview = true
current_piece_preview.set_physics_process(false)
func _place_piece_from_preview():
if not is_instance_valid(current_piece_preview):
return
var placed_piece = available_pieces[piece_to_place].instantiate() as StructuralPiece
placed_piece.position = current_piece_preview.position
placed_piece.rotation_degrees = current_piece_preview.rotation_degrees
placed_piece.is_preview = false
structural_container.add_child(placed_piece)
_recalculate_physics_properties()
func _snap_to_grid(pos: Vector2) -> Vector2:
return Vector2(
round(pos.x / grid_size) * grid_size,
round(pos.y / grid_size) * grid_size
)
func _recalculate_physics_properties():
print("Recalculating physics properties...")
var total_mass: float = 0.0
var all_structural_pieces = structural_container.get_children()
for piece in all_structural_pieces:
if piece is StructuralPiece:
total_mass += piece.piece_mass
if total_mass > 0:
self.mass = total_mass
self.set_center_of_mass(Vector2.ZERO)
var shape = ConcavePolygonShape2D.new()
var all_points: PackedVector2Array = []
for piece in all_structural_pieces:
if piece is StructuralPiece:
for child in piece.get_children():
if child is CollisionPolygon2D:
var piece_points = child.polygon
var piece_xform = piece.transform
for p in piece_points:
all_points.append(piece_xform.xform(p))
shape.set_segments(all_points)
for child in get_children():
if child is CollisionShape2D or child is CollisionPolygon2D:
child.queue_free()
var new_collision_shape = CollisionShape2D.new()
new_collision_shape.shape = shape
add_child(new_collision_shape)
print("Physics Recalculated: Total Mass: %.2f kg" % total_mass)
else:
print("No structural pieces found. Physics properties not updated.")

View File

@ -1,7 +1,3 @@
[gd_scene load_steps=2 format=3 uid="uid://bscabpucsv88k"]
[ext_resource type="PackedScene" uid="uid://ds3qq4yg8y86y" path="res://scenes/ship/builder/module_builder.tscn" id="1_h26xi"]
[gd_scene format=3 uid="uid://bscabpucsv88k"]
[node name="IntegrityTest" type="Node2D"]
[node name="Module" parent="." instance=ExtResource("1_h26xi")]

View File

@ -1,10 +1,3 @@
[gd_scene load_steps=2 format=3 uid="uid://bvogqgqig1hps"]
[ext_resource type="PackedScene" uid="uid://ds3qq4yg8y86y" path="res://scenes/ship/builder/module_builder.tscn" id="1_74fwe"]
[gd_scene format=3 uid="uid://bvogqgqig1hps"]
[node name="ShipBuildingTest" type="Node2D"]
[node name="ModuleBuilder" parent="." instance=ExtResource("1_74fwe")]
position = Vector2(500, 300)
piece_to_place = "bulkhead"
piece_to_place = "bulkhead"