Ship building basics

This commit is contained in:
olof.pettersson
2025-09-22 22:46:39 +02:00
parent d08ec0d263
commit 5efeef7d8c
11 changed files with 242 additions and 78 deletions

View File

@ -11,7 +11,7 @@ config_version=5
[application]
config/name="space_simulation"
run/main_scene="uid://dogqi2c58qdc0"
run/main_scene="uid://bvogqgqig1hps"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"

View File

@ -0,0 +1,12 @@
[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"]
[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,109 +1,184 @@
# module_builder_controller.gd
# The root script for a player-constructed ship module. This is a RigidBody2D that
# acts as a container for all the player-placed structural pieces.
@tool
class_name ModuleBuilderController
extends RigidBody2D
# These nodes should be children of this RigidBody2D in the scene tree.
# --- 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 # For "gas leak" effects
@onready var atmosphere_visualizer: Node2D = $AtmosphereVisualizer
enum ModuleState { IN_CONSTRUCTION, SEALED, PRESSURIZED, BREACHED }
var current_state: ModuleState = ModuleState.IN_CONSTRUCTION
# --- Public API for In-Game Construction ---
func _get_property_list() -> Array:
var properties = []
var piece_names = available_pieces.keys()
piece_names.sort()
piece_names.insert(0, "Refresh")
# Called by the player's construction tool to add a wall, plate, etc.
func add_structural_piece(piece_scene: PackedScene, pos: Vector2, rot: float):
var piece = piece_scene.instantiate() as Node2D
piece.position = pos
piece.rotation = rot
structural_container.add_child(piece)
# Every time the structure changes, we must update the module's physics.
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()
# Called by the player to define the area that should contain an atmosphere.
func add_hull_volume(volume_scene: PackedScene, pos: Vector2):
var volume = volume_scene.instantiate() as Node2D
volume.position = pos
hull_volume_container.add_child(volume)
# This function checks if every defined room (Hull Volume) is properly sealed.
func check_for_seal() -> bool:
if hull_volume_container.get_child_count() == 0:
print("Module integrity check failed: No hull volumes defined.")
return false
var all_sealed = true
for volume in hull_volume_container.get_children():
if volume is HullVolume:
if not volume.check_seal():
all_sealed = false
# The volume can be responsible for showing its own leak effect.
if atmosphere_visualizer.has_method("show_leak_at"):
atmosphere_visualizer.show_leak_at(volume.get_leak_position())
if all_sealed:
print("Module is sealed! Ready for pressurization and component installation.")
current_state = ModuleState.SEALED
return true
else:
print("One or more hull volumes reported a leak.")
current_state = ModuleState.IN_CONSTRUCTION
return false
# --- Internal Physics Management ---
# Recalculates the total mass, center of mass, and combines all collision shapes.
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...")
# 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
self.set_center_of_mass(Vector2.ZERO)
# --- 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)
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)

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://d3hitk62fice4"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_1wp2n"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
size = Vector2(10, 100)
[node name="Bulkhead" type="StaticBody2D"]
collision_layer = 5
script = ExtResource("1_1wp2n")
metadata/_custom_type_script = "uid://b7f8x2qimvn37"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_1wp2n")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -5.0
offset_top = -50.0
offset_right = 5.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.6, 0.6, 0.6, 1)

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://bho8x10x4oab7"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_ecow4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
size = Vector2(100, 100)
[node name="Hullplate" type="StaticBody2D"]
collision_mask = 0
script = ExtResource("1_ecow4")
metadata/_custom_type_script = "uid://b7f8x2qimvn37"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_1wp2n")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.4, 0.4, 0.4, 1)

View File

@ -1,5 +1,4 @@
# structural_piece.gd
# A script for the individual, player-placeable pieces like walls, floors, and girders.
@tool
class_name StructuralPiece
extends StaticBody2D
@ -12,5 +11,16 @@ extends StaticBody2D
# The health of this specific piece.
@export var health: float = 100.0
# This setter is triggered by the editor.
var is_preview: bool = false:
set(value):
is_preview = value
if is_preview:
# Make the piece translucent if it's a preview.
modulate = Color(1, 1, 1, 0.5)
else:
# Make it opaque if it's a permanent piece.
modulate = Color(1, 1, 1, 1)
func get_mass() -> float:
return piece_mass

View File

@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3 uid="uid://ds4eilbvihjy7"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_6jsoj"]
[sub_resource type="CircleShape2D" id="CircleShape2D_jsbwo"]
[node name="StructuralPiece" type="StaticBody2D"]
script = ExtResource("1_6jsoj")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_jsbwo")
[node name="ColorRect" type="ColorRect" parent="."]
offset_right = 40.0
offset_bottom = 40.0

View File

@ -1,6 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://ds4eilbvihjy7"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/structural_piece.gd" id="1_6jsoj"]
[node name="StructuralPiece" type="StaticBody2D"]
script = ExtResource("1_6jsoj")

View File

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

View File

@ -1,8 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://bvogqgqig1hps"]
[ext_resource type="PackedScene" uid="uid://d53d4m797g36a" path="res://scenes/ship/builder/module_builder.tscn" id="1_74fwe"]
[ext_resource type="PackedScene" uid="uid://ds3qq4yg8y86y" path="res://scenes/ship/builder/module_builder.tscn" id="1_74fwe"]
[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"