From ea1aa6ea5d55dbdc524e5eded2b9ac237bec613d Mon Sep 17 00:00:00 2001 From: "olof.pettersson" Date: Wed, 24 Sep 2025 10:44:38 +0200 Subject: [PATCH] Editor plugin WIP --- .gitignore | 1 - Development_Status.md | 16 ++ GAME_DESIGN_DOCUMENT.md | 120 +++++++++++ addons/module_builder_plugin/builder_dock.gd | 153 ++++++++++++++ .../module_builder_plugin/builder_dock.gd.uid | 1 + .../module_builder_plugin/builder_dock.tscn | 42 ++++ .../construction_inspector.tscn | 13 ++ .../construction_tree.tscn | 14 ++ .../module_builder_editor_plugin.gd | 34 ++++ .../module_builder_editor_plugin.gd.uid | 1 + .../module_builder_plugin/module_editor.tscn | 29 +++ addons/module_builder_plugin/plugin.cfg | 7 + project.godot | 4 +- scenes/ship/builder/module.gd | 66 ++++++ ...uilder_controller.gd.uid => module.gd.uid} | 0 scenes/ship/builder/module.tscn | 2 +- scenes/ship/builder/module_builder.tscn | 12 -- .../ship/builder/module_builder_controller.gd | 188 ------------------ scenes/tests/integrity_test.tscn | 6 +- scenes/tests/ship_building_test.tscn | 9 +- 20 files changed, 501 insertions(+), 217 deletions(-) create mode 100644 Development_Status.md create mode 100644 GAME_DESIGN_DOCUMENT.md create mode 100644 addons/module_builder_plugin/builder_dock.gd create mode 100644 addons/module_builder_plugin/builder_dock.gd.uid create mode 100644 addons/module_builder_plugin/builder_dock.tscn create mode 100644 addons/module_builder_plugin/construction_inspector.tscn create mode 100644 addons/module_builder_plugin/construction_tree.tscn create mode 100644 addons/module_builder_plugin/module_builder_editor_plugin.gd create mode 100644 addons/module_builder_plugin/module_builder_editor_plugin.gd.uid create mode 100644 addons/module_builder_plugin/module_editor.tscn create mode 100644 addons/module_builder_plugin/plugin.cfg create mode 100644 scenes/ship/builder/module.gd rename scenes/ship/builder/{module_builder_controller.gd.uid => module.gd.uid} (100%) delete mode 100644 scenes/ship/builder/module_builder.tscn delete mode 100644 scenes/ship/builder/module_builder_controller.gd diff --git a/.gitignore b/.gitignore index 7ea6b12..d478d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ .vscode/ /android/ -/addons/ *.tmp diff --git a/Development_Status.md b/Development_Status.md new file mode 100644 index 0000000..73a6ae8 --- /dev/null +++ b/Development_Status.md @@ -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. \ No newline at end of file diff --git a/GAME_DESIGN_DOCUMENT.md b/GAME_DESIGN_DOCUMENT.md new file mode 100644 index 0000000..c78cbc7 --- /dev/null +++ b/GAME_DESIGN_DOCUMENT.md @@ -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. diff --git a/addons/module_builder_plugin/builder_dock.gd b/addons/module_builder_plugin/builder_dock.gd new file mode 100644 index 0000000..132a74d --- /dev/null +++ b/addons/module_builder_plugin/builder_dock.gd @@ -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() diff --git a/addons/module_builder_plugin/builder_dock.gd.uid b/addons/module_builder_plugin/builder_dock.gd.uid new file mode 100644 index 0000000..4d31b23 --- /dev/null +++ b/addons/module_builder_plugin/builder_dock.gd.uid @@ -0,0 +1 @@ +uid://yjbq3ihlmad8 diff --git a/addons/module_builder_plugin/builder_dock.tscn b/addons/module_builder_plugin/builder_dock.tscn new file mode 100644 index 0000000..03c78a3 --- /dev/null +++ b/addons/module_builder_plugin/builder_dock.tscn @@ -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" diff --git a/addons/module_builder_plugin/construction_inspector.tscn b/addons/module_builder_plugin/construction_inspector.tscn new file mode 100644 index 0000000..5ab1652 --- /dev/null +++ b/addons/module_builder_plugin/construction_inspector.tscn @@ -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 diff --git a/addons/module_builder_plugin/construction_tree.tscn b/addons/module_builder_plugin/construction_tree.tscn new file mode 100644 index 0000000..d4040ea --- /dev/null +++ b/addons/module_builder_plugin/construction_tree.tscn @@ -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 diff --git a/addons/module_builder_plugin/module_builder_editor_plugin.gd b/addons/module_builder_plugin/module_builder_editor_plugin.gd new file mode 100644 index 0000000..09cfe45 --- /dev/null +++ b/addons/module_builder_plugin/module_builder_editor_plugin.gd @@ -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") diff --git a/addons/module_builder_plugin/module_builder_editor_plugin.gd.uid b/addons/module_builder_plugin/module_builder_editor_plugin.gd.uid new file mode 100644 index 0000000..4594971 --- /dev/null +++ b/addons/module_builder_plugin/module_builder_editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://wndpl5b4kfkx diff --git a/addons/module_builder_plugin/module_editor.tscn b/addons/module_builder_plugin/module_editor.tscn new file mode 100644 index 0000000..ec6939f --- /dev/null +++ b/addons/module_builder_plugin/module_editor.tscn @@ -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"] diff --git a/addons/module_builder_plugin/plugin.cfg b/addons/module_builder_plugin/plugin.cfg new file mode 100644 index 0000000..8099f83 --- /dev/null +++ b/addons/module_builder_plugin/plugin.cfg @@ -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" diff --git a/project.godot b/project.godot index c184227..11e85d1 100644 --- a/project.godot +++ b/project.godot @@ -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] diff --git a/scenes/ship/builder/module.gd b/scenes/ship/builder/module.gd new file mode 100644 index 0000000..1ee6f59 --- /dev/null +++ b/scenes/ship/builder/module.gd @@ -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() diff --git a/scenes/ship/builder/module_builder_controller.gd.uid b/scenes/ship/builder/module.gd.uid similarity index 100% rename from scenes/ship/builder/module_builder_controller.gd.uid rename to scenes/ship/builder/module.gd.uid diff --git a/scenes/ship/builder/module.tscn b/scenes/ship/builder/module.tscn index 688fda3..a17a0f8 100644 --- a/scenes/ship/builder/module.tscn +++ b/scenes/ship/builder/module.tscn @@ -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") diff --git a/scenes/ship/builder/module_builder.tscn b/scenes/ship/builder/module_builder.tscn deleted file mode 100644 index a71c94a..0000000 --- a/scenes/ship/builder/module_builder.tscn +++ /dev/null @@ -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="."] diff --git a/scenes/ship/builder/module_builder_controller.gd b/scenes/ship/builder/module_builder_controller.gd deleted file mode 100644 index 9ae763a..0000000 --- a/scenes/ship/builder/module_builder_controller.gd +++ /dev/null @@ -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.") diff --git a/scenes/tests/integrity_test.tscn b/scenes/tests/integrity_test.tscn index aff0533..794b5b2 100644 --- a/scenes/tests/integrity_test.tscn +++ b/scenes/tests/integrity_test.tscn @@ -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")] diff --git a/scenes/tests/ship_building_test.tscn b/scenes/tests/ship_building_test.tscn index d63f15e..33f4231 100644 --- a/scenes/tests/ship_building_test.tscn +++ b/scenes/tests/ship_building_test.tscn @@ -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"