Editor plugin WIP
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,5 +3,4 @@
|
||||
.vscode/
|
||||
/android/
|
||||
|
||||
/addons/
|
||||
*.tmp
|
||||
|
||||
16
Development_Status.md
Normal file
16
Development_Status.md
Normal 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
120
GAME_DESIGN_DOCUMENT.md
Normal 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.
|
||||
153
addons/module_builder_plugin/builder_dock.gd
Normal file
153
addons/module_builder_plugin/builder_dock.gd
Normal 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()
|
||||
1
addons/module_builder_plugin/builder_dock.gd.uid
Normal file
1
addons/module_builder_plugin/builder_dock.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://yjbq3ihlmad8
|
||||
42
addons/module_builder_plugin/builder_dock.tscn
Normal file
42
addons/module_builder_plugin/builder_dock.tscn
Normal 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"
|
||||
13
addons/module_builder_plugin/construction_inspector.tscn
Normal file
13
addons/module_builder_plugin/construction_inspector.tscn
Normal 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
|
||||
14
addons/module_builder_plugin/construction_tree.tscn
Normal file
14
addons/module_builder_plugin/construction_tree.tscn
Normal 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
|
||||
34
addons/module_builder_plugin/module_builder_editor_plugin.gd
Normal file
34
addons/module_builder_plugin/module_builder_editor_plugin.gd
Normal 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")
|
||||
@ -0,0 +1 @@
|
||||
uid://wndpl5b4kfkx
|
||||
29
addons/module_builder_plugin/module_editor.tscn
Normal file
29
addons/module_builder_plugin/module_editor.tscn
Normal 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"]
|
||||
7
addons/module_builder_plugin/plugin.cfg
Normal file
7
addons/module_builder_plugin/plugin.cfg
Normal 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"
|
||||
@ -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]
|
||||
|
||||
|
||||
66
scenes/ship/builder/module.gd
Normal file
66
scenes/ship/builder/module.gd
Normal 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()
|
||||
@ -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")
|
||||
|
||||
@ -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="."]
|
||||
@ -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.")
|
||||
@ -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")]
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user