WIP Placing 3d ShipParts
This commit is contained in:
23
src/data/structure/definitions/1m_square_plate.tres
Normal file
23
src/data/structure/definitions/1m_square_plate.tres
Normal file
@ -0,0 +1,23 @@
|
||||
[gd_resource type="Resource" script_class="StructureData" load_steps=2 format=3 uid="uid://squareplate001"]
|
||||
|
||||
[ext_resource type="Script" path="res://data/structure/structure_data.gd" id="1_script"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_script")
|
||||
piece_name = "Square Plate 1x1"
|
||||
type = 1
|
||||
base_mass = 10.0
|
||||
health_max = 100.0
|
||||
shape = "Square"
|
||||
vertices = [
|
||||
Vector3(-0.5, -0.5, 0.0),
|
||||
Vector3(-0.5, 0.5, 0.0),
|
||||
Vector3(0.5, 0.5, 0.0),
|
||||
Vector3(0.5, -0.5, 0.0)
|
||||
]
|
||||
mounts = [
|
||||
{ "position": Vector3(0, 0.5, 0), "normal": Vector3(0, 1, 0), "up": Vector3(0, 0, 1), "type": 0 },
|
||||
{ "position": Vector3(0, -0.5, 0), "normal": Vector3(0, -1, 0), "up": Vector3(0, 0, 1), "type": 0 },
|
||||
{ "position": Vector3(-0.5, 0, 0), "normal": Vector3(-1, 0, 0), "up": Vector3(0, 0, 1), "type": 0 },
|
||||
{ "position": Vector3(0.5, 0, 0), "normal": Vector3(1, 0, 0), "up": Vector3(0, 0, 1), "type": 0 }
|
||||
]
|
||||
21
src/data/structure/definitions/1m_triangle_plate.tres
Normal file
21
src/data/structure/definitions/1m_triangle_plate.tres
Normal file
@ -0,0 +1,21 @@
|
||||
[gd_resource type="Resource" script_class="StructureData" load_steps=2 format=3 uid="uid://triangleplate001"]
|
||||
|
||||
[ext_resource type="Script" path="res://data/structure/structure_data.gd" id="1_script"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_script")
|
||||
piece_name = "Triangle Plate 1m"
|
||||
type = 1 # PLATE
|
||||
base_mass = 5.0
|
||||
health_max = 100.0
|
||||
shape = "Triangle"
|
||||
vertices = [
|
||||
Vector3(-0.5, -0.288675, 0.0),
|
||||
Vector3(0.0, 0.57735, 0.0),
|
||||
Vector3(0.5, -0.288675, 0.0)
|
||||
]
|
||||
mounts = [
|
||||
{ "position": Vector3(0, -0.288, 0), "normal": Vector3(0, -1, 0), "up": Vector3(0, 0, 1), "type": 0 },
|
||||
{ "position": Vector3(0.25, 0.144, 0), "normal": Vector3(0.866, 0.5, 0), "up": Vector3(0, 0, 1), "type": 0 },
|
||||
{ "position": Vector3(-0.25, 0.144, 0), "normal": Vector3(-0.866, 0.5, 0), "up": Vector3(0, 0, 1), "type": 0 }
|
||||
]
|
||||
62
src/data/structure/structure_data.gd
Normal file
62
src/data/structure/structure_data.gd
Normal file
@ -0,0 +1,62 @@
|
||||
class_name StructureData extends Resource
|
||||
|
||||
enum PieceType {STRUT, PLATE, CONNECTOR}
|
||||
|
||||
|
||||
@export_group("Identity")
|
||||
@export var piece_name: String = "Structure"
|
||||
@export var type: PieceType = PieceType.STRUT
|
||||
@export var base_mass: float = 10.0
|
||||
@export var health_max: float = 100.0
|
||||
|
||||
@export_group("Visuals & Physics")
|
||||
## The mesh to display for static pieces. Leave null for procedural pieces.
|
||||
@export var mesh: Mesh
|
||||
## The collision shape for physics. Leave null for procedural pieces.
|
||||
@export var collision_shape: Shape3D
|
||||
|
||||
@export_group("Procedural Parameters")
|
||||
# For procedural pieces, we store parameters instead of a mesh
|
||||
@export var shape: String = "Cube"
|
||||
@export var vertices: Array[Vector3] = [
|
||||
Vector3(1.0, 1.0, 1.0),
|
||||
Vector3(-1.0, 1.0, 1.0),
|
||||
Vector3(-1.0, -1.0, 1.0),
|
||||
Vector3(1.0, -1.0, 1.0),
|
||||
Vector3(1.0, 1.0, -1.0),
|
||||
Vector3(-1.0, 1.0, -1.0),
|
||||
Vector3(-1.0, -1.0, -1.0),
|
||||
Vector3(1.0, -1.0, -1.0)
|
||||
]
|
||||
# @export var procedural_params: Dictionary = {}
|
||||
|
||||
|
||||
@export_group("Mounts")
|
||||
## Array of Dictionaries defining attachment points.
|
||||
## Format: { "position": Vector3, "normal": Vector3, "up": Vector3, "type": int }
|
||||
@export var mounts: Array[Dictionary] = []
|
||||
|
||||
# Helper to get mounts transformed into world space for snapping calculations
|
||||
func get_mounts_transformed(global_transform: Transform3D) -> Array:
|
||||
var world_mounts = []
|
||||
for mount in mounts:
|
||||
# Default to identity rotation if normal/up are missing
|
||||
var normal = mount.get("normal", Vector3.BACK) # Default -Z forward
|
||||
var up = mount.get("up", Vector3.UP)
|
||||
|
||||
world_mounts.append({
|
||||
"position": global_transform * mount.get("position", Vector3.ZERO),
|
||||
"normal": global_transform.basis * normal,
|
||||
"up": global_transform.basis * up,
|
||||
"type": mount.get("type", 0)
|
||||
})
|
||||
return world_mounts
|
||||
|
||||
# Helper to add a mount dynamically (for procedural pieces)
|
||||
func add_mount(pos: Vector3, normal: Vector3, up: Vector3 = Vector3.UP, type: int = 0):
|
||||
mounts.append({
|
||||
"position": pos,
|
||||
"normal": normal,
|
||||
"up": up,
|
||||
"type": type
|
||||
})
|
||||
1
src/data/structure/structure_data.gd.uid
Normal file
1
src/data/structure/structure_data.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bdllldtl4bia3
|
||||
@ -158,6 +158,11 @@ left_click={
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
toggle_build_mode={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
|
||||
@ -33,49 +33,50 @@ properties/4/path = NodePath(".:angular_velocity")
|
||||
properties/4/spawn = false
|
||||
properties/4/replication_mode = 0
|
||||
|
||||
[node name="CharacterPawn3D" type="RigidBody3D" unique_id=288275840]
|
||||
[node name="CharacterPawn3D" type="RigidBody3D"]
|
||||
physics_interpolation_mode = 1
|
||||
top_level = true
|
||||
script = ExtResource("1_4frsu")
|
||||
metadata/_custom_type_script = "uid://cdmmiixa75f3x"
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1967015232]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape3D_6vm80")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1703183586]
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh_6vm80")
|
||||
|
||||
[node name="CameraAnchor" type="Marker3D" parent="." unique_id=462168232]
|
||||
[node name="CameraAnchor" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7000000000000001, 0)
|
||||
|
||||
[node name="CameraPivot" type="Node3D" parent="CameraAnchor" unique_id=794640520]
|
||||
[node name="CameraPivot" type="Node3D" parent="CameraAnchor"]
|
||||
physics_interpolation_mode = 1
|
||||
|
||||
[node name="SpringArm" type="SpringArm3D" parent="CameraAnchor/CameraPivot" unique_id=1399441728]
|
||||
[node name="SpringArm" type="SpringArm3D" parent="CameraAnchor/CameraPivot"]
|
||||
shape = SubResource("CapsuleShape3D_673rh")
|
||||
spring_length = 2.0
|
||||
margin = 0.1
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraAnchor/CameraPivot/SpringArm" unique_id=1779046272]
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraAnchor/CameraPivot/SpringArm"]
|
||||
far = 200000.0
|
||||
|
||||
[node name="GripDetector" type="Area3D" parent="." unique_id=734413990]
|
||||
[node name="GripDetector" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
|
||||
collision_layer = 0
|
||||
collision_mask = 32768
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="GripDetector" unique_id=1939219836]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="GripDetector"]
|
||||
shape = SubResource("SphereShape3D_gnddn")
|
||||
|
||||
[node name="ZeroGMovementComponent" type="Node3D" parent="." unique_id=594953523]
|
||||
[node name="ZeroGMovementComponent" type="Node3D" parent="."]
|
||||
script = ExtResource("4_8jhjh")
|
||||
metadata/_custom_type_script = "uid://y3vo40i16ek3"
|
||||
|
||||
[node name="EVAMovementComponent" parent="." unique_id=1806288315 instance=ExtResource("3_gnddn")]
|
||||
[node name="EVAMovementComponent" parent="." instance=ExtResource("3_gnddn")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.13939085347041424, 0.5148942200402955)
|
||||
|
||||
[node name="PlayerController3d" parent="." unique_id=1450011826 instance=ExtResource("4_bcy3l")]
|
||||
[node name="PlayerController3d" parent="." instance=ExtResource("4_bcy3l")]
|
||||
mouse_sensitivity = null
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=732324183]
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("SceneReplicationConfig_gnddn")
|
||||
|
||||
@ -8,39 +8,78 @@ class_name PlayerController3D
|
||||
@export var mouse_sensitivity: float = 0.002 # Radians per pixel motion
|
||||
var _mouse_motion_input: Vector2 = Vector2.ZERO
|
||||
|
||||
# --- Builder State ---
|
||||
var active_preview_piece: ProceduralPiece = null
|
||||
var active_structure_data: StructureData = null
|
||||
var build_mode_enabled: bool = false
|
||||
|
||||
# Resources
|
||||
const SQUARE_RES = preload("res://data/structure/definitions/1m_square_plate.tres")
|
||||
const TRIANGLE_RES = preload("res://data/structure/definitions/1m_triangle_plate.tres")
|
||||
const PROCEDURAL_PIECE_SCENE = preload("res://scenes/ship/builder/pieces/procedural_piece.tscn")
|
||||
|
||||
class KeyInput:
|
||||
var pressed: bool = false
|
||||
var held: bool = false
|
||||
var released: bool = false
|
||||
|
||||
func _init(_p: bool, _h: bool, _r: bool):
|
||||
func _init(_p: bool = false, _h: bool = false, _r: bool = false):
|
||||
pressed = _p
|
||||
held = _h
|
||||
released = _r
|
||||
|
||||
func _to_dict():
|
||||
return {
|
||||
"pressed": pressed,
|
||||
"held": held,
|
||||
"released": released
|
||||
}
|
||||
|
||||
static func from_dict(dict: Dictionary) -> KeyInput:
|
||||
return KeyInput.new(dict.get("pressed", false), dict.get("held", false), dict.get("released", false))
|
||||
|
||||
# Helper to convert to Dictionary for RPC
|
||||
func to_dict() -> Dictionary:
|
||||
return {"p": pressed, "h": held, "r": released}
|
||||
|
||||
# Helper to create from Dictionary
|
||||
static func from_dict(d: Dictionary) -> KeyInput:
|
||||
return KeyInput.new(d.get("p", false), d.get("h", false), d.get("r", false))
|
||||
|
||||
func _ready():
|
||||
# If we are spawned dynamically, the owner_id might be set by GameManager.
|
||||
# Fallback: assume the pawn's name is the player ID (common pattern).
|
||||
# Fallback: assume the pawn's name is the player ID.
|
||||
if get_parent().name.is_valid_int():
|
||||
set_multiplayer_authority(int(get_parent().name))
|
||||
|
||||
func _unhandled_input(event: InputEvent):
|
||||
|
||||
# Check if THIS client is the owner of this controller
|
||||
# Check if THIS client is the owner of this controller
|
||||
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
|
||||
return
|
||||
|
||||
# Toggle Build Mode
|
||||
if event.is_action_pressed("toggle_build_mode"): # Map 'B' or similar in Project Settings
|
||||
build_mode_enabled = !build_mode_enabled
|
||||
if not build_mode_enabled:
|
||||
_clear_preview()
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) # Ensure mouse captured when leaving
|
||||
else:
|
||||
_select_piece(1) # Default to square
|
||||
print("Build Mode Enabled")
|
||||
|
||||
if not build_mode_enabled:
|
||||
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
||||
_mouse_motion_input += Vector2(event.relative.x, -event.relative.y)
|
||||
return
|
||||
|
||||
# --- Build Mode Inputs ---
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_1:
|
||||
print("Selected Square Piece")
|
||||
_select_piece(1)
|
||||
elif event.keycode == KEY_2:
|
||||
_select_piece(2)
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
_place_piece()
|
||||
|
||||
# Allow camera look while holding right click in build mode
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if event.pressed:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
else:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
# Handle mouse motion input directly here
|
||||
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
||||
_mouse_motion_input += Vector2(event.relative.x, -event.relative.y)
|
||||
@ -49,54 +88,174 @@ func _physics_process(_delta):
|
||||
# Check if THIS client is the owner
|
||||
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
|
||||
return
|
||||
|
||||
# STRENGTHENED CHECK: Ensure pawn is valid and inside the tree
|
||||
if not is_instance_valid(possessed_pawn) or not possessed_pawn.is_inside_tree():
|
||||
return
|
||||
|
||||
# 1. Handle Mouse Rotation
|
||||
if _mouse_motion_input != Vector2.ZERO:
|
||||
# Calculate yaw and pitch based on mouse movement
|
||||
var sensitivity_modified_mouse_input = Vector2(_mouse_motion_input.x, _mouse_motion_input.y) * mouse_sensitivity
|
||||
|
||||
# Send rotation input via RPC immediately
|
||||
# Send to Server (ID 1)
|
||||
server_process_rotation_input.rpc_id(1, sensitivity_modified_mouse_input)
|
||||
|
||||
# Reset the buffer
|
||||
_mouse_motion_input = Vector2.ZERO
|
||||
|
||||
|
||||
# 2. Gather Movement Inputs (Only process movement if NOT in build mode, or perhaps allow moving while building?)
|
||||
# Let's allow movement while building for now.
|
||||
var move_vec = Input.get_vector("move_left_3d", "move_right_3d", "move_backward_3d", "move_forward_3d")
|
||||
var roll_input = Input.get_action_strength("roll_right_3d") - Input.get_action_strength("roll_left_3d")
|
||||
var vertical_input = Input.get_action_strength("move_up_3d") - Input.get_action_strength("move_down_3d")
|
||||
var interact_input = KeyInput.new(Input.is_action_just_pressed("spacebar_3d"), Input.is_action_pressed("spacebar_3d"), Input.is_action_just_released("spacebar_3d"))
|
||||
|
||||
var interact_input = KeyInput.new(Input.is_action_just_pressed("spacebar_3d"), Input.is_action_pressed("spacebar_3d"), Input.is_action_just_released("spacebar_3d"))
|
||||
var l_input = KeyInput.new(Input.is_action_just_pressed("left_click"), Input.is_action_pressed("left_click"), Input.is_action_just_released("left_click"))
|
||||
var r_input = KeyInput.new(Input.is_action_just_pressed("right_click"), Input.is_action_pressed("right_click"), Input.is_action_just_released("right_click"))
|
||||
|
||||
# Send to Server (ID 1), converting KeyInput objects to Dictionaries
|
||||
server_process_movement_input.rpc_id(1, move_vec, roll_input, vertical_input)
|
||||
server_process_interaction_input.rpc_id(1, interact_input._to_dict())
|
||||
server_process_clicks.rpc_id(1, l_input._to_dict(), r_input._to_dict())
|
||||
server_process_interaction_input.rpc_id(1, interact_input.to_dict())
|
||||
server_process_clicks.rpc_id(1, l_input.to_dict(), r_input.to_dict())
|
||||
|
||||
# 3. Update Builder Preview
|
||||
if build_mode_enabled and active_preview_piece:
|
||||
_update_preview_transform()
|
||||
|
||||
# --- Builder Functions ---
|
||||
|
||||
func _select_piece(index: int):
|
||||
_clear_preview()
|
||||
|
||||
if index == 1:
|
||||
active_structure_data = SQUARE_RES
|
||||
elif index == 2:
|
||||
active_structure_data = TRIANGLE_RES
|
||||
|
||||
if active_structure_data:
|
||||
var piece = PROCEDURAL_PIECE_SCENE.instantiate()
|
||||
piece.structure_data = active_structure_data
|
||||
piece.is_preview = true
|
||||
get_tree().current_scene.add_child(piece)
|
||||
active_preview_piece = piece
|
||||
|
||||
func _update_preview_transform():
|
||||
if not is_instance_valid(possessed_pawn): return
|
||||
|
||||
# 1. Cast Ray from Camera
|
||||
var cam = possessed_pawn.camera
|
||||
var space_state = possessed_pawn.get_world_3d().direct_space_state
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
var from = cam.project_ray_origin(mouse_pos)
|
||||
var to = from + cam.project_ray_normal(mouse_pos) * 10.0 # 10m reach
|
||||
|
||||
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 1 << 14 # Layer 15 (Weld/Structure)
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
|
||||
if result:
|
||||
var collider = result.collider
|
||||
# Try to find the Module or Piece
|
||||
var target_module: Module = null
|
||||
if collider is StructuralPiece:
|
||||
target_module = collider.get_root_module()
|
||||
elif collider is Module:
|
||||
target_module = collider
|
||||
|
||||
if target_module:
|
||||
# Call Snapping Tool
|
||||
var snap_transform = SnappingTool.get_best_snap_transform(
|
||||
active_structure_data,
|
||||
target_module,
|
||||
result.position, # Ray hit position
|
||||
(to - from).normalized()
|
||||
)
|
||||
active_preview_piece.global_transform = snap_transform
|
||||
return
|
||||
|
||||
# Fallback: Float in front of player
|
||||
var float_pos = from + cam.project_ray_normal(mouse_pos) * 3.0
|
||||
active_preview_piece.global_position = float_pos
|
||||
# Orient to face camera roughly
|
||||
active_preview_piece.look_at(cam.global_position, Vector3.UP)
|
||||
|
||||
func _place_piece():
|
||||
if not active_preview_piece or not active_structure_data: return
|
||||
|
||||
# Tell Server to spawn the real piece at this transform
|
||||
server_request_place_piece.rpc_id(1, active_structure_data.resource_path, active_preview_piece.global_transform)
|
||||
|
||||
func _clear_preview():
|
||||
if is_instance_valid(active_preview_piece):
|
||||
active_preview_piece.queue_free()
|
||||
active_preview_piece = null
|
||||
|
||||
# --- RPCs: Allow "any_peer" so clients can call this on the Server ---
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_request_place_piece(resource_path: String, transform: Transform3D):
|
||||
# Server validation logic here (distance check, cost check)
|
||||
# Security: Check if sender is allowed to build
|
||||
|
||||
var res = load(resource_path) as StructureData
|
||||
if not res: return
|
||||
|
||||
# Find nearby module to attach to
|
||||
var query_pos = transform.origin
|
||||
var module = _find_module_near_server(query_pos)
|
||||
|
||||
if not module:
|
||||
# Logic to create a new module could go here
|
||||
print("No module nearby to attach piece.")
|
||||
var new_module = Module.new()
|
||||
new_module.name = "Module_%d" % get_process_delta_time() # Unique name
|
||||
possessed_pawn.get_parent().add_child(new_module)
|
||||
new_module.global_position = query_pos
|
||||
module = new_module
|
||||
print("Created new module %s for piece placement." % new_module.name)
|
||||
|
||||
if module:
|
||||
var piece = PROCEDURAL_PIECE_SCENE.instantiate()
|
||||
piece.structure_data = res
|
||||
module.add_child(piece)
|
||||
piece.global_transform = transform
|
||||
piece.owner = module # Ensure persistence
|
||||
|
||||
# Trigger weld logic on the new piece
|
||||
piece.try_weld()
|
||||
|
||||
# Helper to find modules on server side (uses global overlap check)
|
||||
func _find_module_near_server(pos: Vector3) -> Module:
|
||||
# Simple distance check against all modules in the system data
|
||||
# A better way uses a Physics Shape Query
|
||||
if GameManager.current_star_system and GameManager.current_star_system.system_data:
|
||||
for ship in GameManager.current_star_system.system_data.ships:
|
||||
if ship is Module and ship.global_position.distance_to(pos) < 10.0: # Arbitrary range
|
||||
return ship
|
||||
return null
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_process_movement_input(move: Vector2, roll: float, vertical: float):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
# Debug: Uncomment to verify flow
|
||||
# if move.length() > 0: print("Server Pawn %s Move: %s" % [owner_id, move])
|
||||
possessed_pawn.set_movement_input(move, roll, vertical)
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_process_interaction_input(interact_input: Dictionary):
|
||||
func server_process_interaction_input(interact_data: Dictionary):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_interaction_input(KeyInput.from_dict(interact_input))
|
||||
if interact_data.has_all(["p", "h", "r"]):
|
||||
possessed_pawn.set_interaction_input(KeyInput.from_dict(interact_data))
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_process_rotation_input(input: Vector2):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_rotation_input(input)
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_process_clicks(l_action: Dictionary, r_action: Dictionary):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_click_input(KeyInput.from_dict(l_action), KeyInput.from_dict(r_action))
|
||||
|
||||
func possess(pawn_to_control: CharacterPawn3D):
|
||||
possessed_pawn = pawn_to_control
|
||||
|
||||
#print("PlayerController3D %d possessed: %s" % [multiplayer.get_unique_id(), possessed_pawn.name])
|
||||
@rpc("any_peer", "call_local")
|
||||
func server_process_clicks(l_data: Dictionary, r_data: Dictionary):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
var l_action = KeyInput.from_dict(l_data) if l_data.has("p") else KeyInput.new()
|
||||
var r_action = KeyInput.from_dict(r_data) if r_data.has("p") else KeyInput.new()
|
||||
possessed_pawn.set_click_input(l_action, r_action)
|
||||
|
||||
# Optional: Release mouse when losing focus
|
||||
func _notification(what):
|
||||
@ -109,4 +268,3 @@ func _notification(what):
|
||||
print("PlayerController exited tree")
|
||||
NOTIFICATION_ENTER_TREE:
|
||||
print("PlayerController %s entered tree" % multiplayer.get_unique_id())
|
||||
|
||||
|
||||
140
src/scenes/ship/builder/pieces/procedural_piece.gd
Normal file
140
src/scenes/ship/builder/pieces/procedural_piece.gd
Normal file
@ -0,0 +1,140 @@
|
||||
class_name ProceduralPiece extends StructuralPiece
|
||||
|
||||
# For a strut: Start Point, End Point
|
||||
# For a plate: 3 or 4 vertices
|
||||
@export var vertices: Array[Vector3] = []:
|
||||
set(value):
|
||||
vertices = value
|
||||
_generate_geometry()
|
||||
|
||||
@export var thickness: float = 0.1
|
||||
|
||||
func on_structure_data_update():
|
||||
vertices = structure_data.vertices
|
||||
if not vertices.is_empty():
|
||||
_generate_geometry()
|
||||
|
||||
super._ready()
|
||||
|
||||
# Configure this piece as a simple Strut (Beam)
|
||||
func configure_strut(start: Vector3, end: Vector3):
|
||||
vertices = [start, end]
|
||||
|
||||
# Create ephemeral data for this specific instance
|
||||
structure_data = StructureData.new()
|
||||
structure_data.piece_name = "Strut"
|
||||
structure_data.type = StructureData.PieceType.STRUT
|
||||
|
||||
# Define Mounts at both ends
|
||||
var dir = (end - start).normalized()
|
||||
|
||||
# Mount 0: At Start, facing away from End (-dir)
|
||||
structure_data.add_mount(start, -dir)
|
||||
|
||||
# Mount 1: At End, facing away from Start (+dir)
|
||||
structure_data.add_mount(end, dir)
|
||||
|
||||
_generate_geometry()
|
||||
|
||||
# Configure this piece as a Plate (Triangle/Quad)
|
||||
func configure_plate(points: Array[Vector3]):
|
||||
vertices = points
|
||||
|
||||
structure_data = StructureData.new()
|
||||
structure_data.piece_name = "Plate"
|
||||
structure_data.type = StructureData.PieceType.PLATE
|
||||
|
||||
# Add Mounts along edges (simplified: midpoints of edges)
|
||||
var center = Vector3.ZERO
|
||||
for p in points: center += p
|
||||
center /= points.size()
|
||||
|
||||
for i in range(points.size()):
|
||||
var p1 = points[i]
|
||||
var p2 = points[(i + 1) % points.size()]
|
||||
var mid = (p1 + p2) / 2.0
|
||||
var edge_norm = (mid - center).normalized()
|
||||
|
||||
structure_data.add_mount(mid, edge_norm)
|
||||
|
||||
_generate_geometry()
|
||||
|
||||
func _generate_geometry():
|
||||
if vertices.is_empty(): return
|
||||
|
||||
print("Generating geometry for ProceduralPiece with vertices:")
|
||||
print(vertices)
|
||||
|
||||
# Clear existing meshes
|
||||
for c in get_children():
|
||||
if c is MeshInstance3D or c is CollisionShape3D:
|
||||
c.queue_free()
|
||||
|
||||
if vertices.size() == 2:
|
||||
_build_strut_mesh()
|
||||
elif vertices.size() >= 3:
|
||||
_build_plate_mesh()
|
||||
|
||||
func _build_strut_mesh():
|
||||
# Generate a Box or Cylinder between v[0] and v[1]
|
||||
var start = vertices[0]
|
||||
var end = vertices[1]
|
||||
var length = start.distance_to(end)
|
||||
var mid = (start + end) / 2.0
|
||||
|
||||
var mesh_inst = MeshInstance3D.new()
|
||||
var box = BoxMesh.new()
|
||||
box.size = Vector3(thickness, thickness, length)
|
||||
mesh_inst.mesh = box
|
||||
add_child(mesh_inst)
|
||||
|
||||
# Orient mesh
|
||||
mesh_inst.position = mid
|
||||
if length > 0.001:
|
||||
mesh_inst.look_at(end, Vector3.UP)
|
||||
|
||||
# Collision
|
||||
var col = CollisionShape3D.new()
|
||||
var shape = BoxShape3D.new()
|
||||
shape.size = box.size
|
||||
col.shape = shape
|
||||
col.position = mid
|
||||
col.rotation = mesh_inst.rotation
|
||||
add_child(col)
|
||||
|
||||
func _build_plate_mesh():
|
||||
# Use SurfaceTool to build a polygon
|
||||
var st = SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
|
||||
# Assume points are coplanar and ordered
|
||||
var normal = (vertices[1] - vertices[0]).cross(vertices[2] - vertices[0]).normalized()
|
||||
|
||||
# Simple Fan Triangulation (works for convex shapes)
|
||||
for i in range(1, vertices.size() - 1):
|
||||
st.set_normal(normal)
|
||||
st.add_vertex(vertices[0])
|
||||
st.set_normal(normal)
|
||||
st.add_vertex(vertices[i])
|
||||
st.set_normal(normal)
|
||||
st.add_vertex(vertices[i + 1])
|
||||
|
||||
# Backface
|
||||
st.set_normal(-normal)
|
||||
st.add_vertex(vertices[i + 1])
|
||||
st.set_normal(-normal)
|
||||
st.add_vertex(vertices[i])
|
||||
st.set_normal(-normal)
|
||||
st.add_vertex(vertices[0])
|
||||
|
||||
var mesh_inst = MeshInstance3D.new()
|
||||
mesh_inst.mesh = st.commit()
|
||||
add_child(mesh_inst)
|
||||
|
||||
# Collision (Convex)
|
||||
var col = CollisionShape3D.new()
|
||||
var shape = ConvexPolygonShape3D.new()
|
||||
shape.points = vertices
|
||||
# Extrude slightly for thickness? Or just use flat collision for plates.
|
||||
col.shape = shape
|
||||
add_child(col)
|
||||
1
src/scenes/ship/builder/pieces/procedural_piece.gd.uid
Normal file
1
src/scenes/ship/builder/pieces/procedural_piece.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://biwe3npxmykbi
|
||||
7
src/scenes/ship/builder/pieces/procedural_piece.tscn
Normal file
7
src/scenes/ship/builder/pieces/procedural_piece.tscn
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bt31qetyom88q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://biwe3npxmykbi" path="res://scenes/ship/builder/pieces/procedural_piece.gd" id="1_33s2w"]
|
||||
|
||||
[node name="ProceduralPiece" type="RigidBody3D"]
|
||||
script = ExtResource("1_33s2w")
|
||||
metadata/_custom_type_script = "uid://wlm40n8ywr"
|
||||
@ -1,9 +1,28 @@
|
||||
class_name StructuralPiece extends ShipPiece
|
||||
|
||||
# Track who we are welded to so we don't double-weld
|
||||
var connected_neighbors: Array[StructuralPiece] = []
|
||||
# The definition of this piece.
|
||||
@export var structure_data: StructureData = null:
|
||||
set(value):
|
||||
structure_data = value
|
||||
print("StructuralPiece assigned StructureData: %s" % structure_data.piece_name if structure_data else "null")
|
||||
print(structure_data)
|
||||
if structure_data:
|
||||
on_structure_data_update()
|
||||
|
||||
var attachment_areas: Array[Area3D] = []
|
||||
func on_structure_data_update():
|
||||
# Placeholder: In a real implementation, this would create the MeshInstance3D
|
||||
# and CollisionShape3D based on 'structure_data'.
|
||||
pass
|
||||
|
||||
# Track who we are welde d to so we don't double-weld
|
||||
var connected_neighbors: Array[StructuralPiece] = []
|
||||
var mount_areas: Array[Area3D] = []
|
||||
|
||||
# Flag to distinguish between a preview piece (no physics) and a placed piece
|
||||
var is_preview: bool = false:
|
||||
set(value):
|
||||
is_preview = value
|
||||
_update_preview_visuals()
|
||||
|
||||
func _init():
|
||||
base_mass = 50.0
|
||||
@ -12,57 +31,83 @@ func _init():
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
|
||||
if is_preview:
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
return
|
||||
|
||||
sleeping = false
|
||||
can_sleep = false
|
||||
|
||||
# 1. Find all our attachment point areas
|
||||
for child in get_children():
|
||||
if child is Area3D and child.name.begins_with("AttachmentPoint"):
|
||||
attachment_areas.append(child)
|
||||
# Ensure they are monitoring
|
||||
child.monitoring = true
|
||||
child.monitorable = true
|
||||
|
||||
# 2. Attempt to weld to anything we are already touching (for spawning in)
|
||||
# We wait one frame to let physics settle positions
|
||||
_initial_weld_scan()
|
||||
|
||||
func _initial_weld_scan():
|
||||
# Wait for the node to settle in the scene tree
|
||||
await get_tree().physics_frame
|
||||
# Wait one more tick for the Physics Server to calculate overlaps
|
||||
await get_tree().physics_frame
|
||||
# If we have data, generate the mount points
|
||||
if structure_data:
|
||||
_generate_mount_detectors()
|
||||
|
||||
_scan_and_weld_neighbors()
|
||||
# Attempt to weld to anything we are already touching
|
||||
# _initial_weld_scan()
|
||||
|
||||
# --- PUBLIC API ---
|
||||
func _generate_mount_detectors():
|
||||
for i in range(structure_data.mounts.size()):
|
||||
var mount_def = structure_data.mounts[i]
|
||||
|
||||
var area = Area3D.new()
|
||||
area.name = "Mount_%d" % i
|
||||
add_child(area)
|
||||
|
||||
# Position
|
||||
area.position = mount_def.get("position", Vector3.ZERO)
|
||||
|
||||
# Orientation: Align Area -Z with Mount Normal
|
||||
var normal = mount_def.get("normal", Vector3.BACK)
|
||||
var up = mount_def.get("up", Vector3.UP)
|
||||
|
||||
# Safety check for parallel vectors
|
||||
if normal.cross(up).is_zero_approx():
|
||||
up = Vector3.RIGHT if abs(normal.y) > 0.9 else Vector3.UP
|
||||
|
||||
if normal.length_squared() > 0.01:
|
||||
area.transform.basis = Basis.looking_at(normal, up)
|
||||
|
||||
# Shape
|
||||
var shape = CollisionShape3D.new()
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = 0.15 # Small tolerance radius
|
||||
shape.shape = sphere
|
||||
area.add_child(shape)
|
||||
|
||||
# Collision Layers (Use Layer 15 "Weld" = bit 14)
|
||||
area.collision_layer = 1 << 14
|
||||
area.collision_mask = 1 << 14
|
||||
area.monitoring = true
|
||||
area.monitorable = true
|
||||
|
||||
mount_areas.append(area)
|
||||
|
||||
# Call this after placing a piece during runtime construction
|
||||
func try_weld():
|
||||
_scan_and_weld_neighbors()
|
||||
|
||||
# --- INTERNAL LOGIC ---
|
||||
func _initial_weld_scan():
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
if not is_instance_valid(self): return
|
||||
_scan_and_weld_neighbors()
|
||||
|
||||
func _scan_and_weld_neighbors():
|
||||
for my_area in attachment_areas:
|
||||
# Check what other attachment points this area is touching
|
||||
for my_area in mount_areas:
|
||||
for other_area in my_area.get_overlapping_areas():
|
||||
var other_piece = other_area.get_parent()
|
||||
|
||||
# Validate the target
|
||||
if other_piece is StructuralPiece and other_piece != self:
|
||||
# Here we could check mount compatibility (types, alignment)
|
||||
if not other_piece in connected_neighbors:
|
||||
_create_weld_to(other_piece)
|
||||
|
||||
func _create_weld_to(neighbor: StructuralPiece):
|
||||
# print("Welding %s to %s" % [self.name, neighbor.name])
|
||||
|
||||
# 1. Create the Joint
|
||||
var joint = Generic6DOFJoint3D.new()
|
||||
|
||||
|
||||
# 2. Configure as a rigid lock
|
||||
_lock_joint_axis(joint)
|
||||
|
||||
# 3. Add to scene (Joints should generally be peers or children of the system root)
|
||||
# Adding it to 'self' is fine; if 'self' dies, the joint breaks, which is correct.
|
||||
add_child(joint)
|
||||
@ -74,7 +119,7 @@ func _create_weld_to(neighbor: StructuralPiece):
|
||||
# 5. Connect Bodies
|
||||
joint.node_a = self.get_path()
|
||||
joint.node_b = neighbor.get_path()
|
||||
|
||||
|
||||
# 6. Record connection on BOTH sides
|
||||
connected_neighbors.append(neighbor)
|
||||
neighbor.connected_neighbors.append(self)
|
||||
@ -90,10 +135,15 @@ func _lock_joint_axis(joint: Generic6DOFJoint3D):
|
||||
joint.set_param_y(axis, 0.0)
|
||||
joint.set_param_z(axis, 0.0)
|
||||
|
||||
# Handle destruction/detachment
|
||||
func _update_preview_visuals():
|
||||
var mesh_instance = find_child("MeshInstance3D")
|
||||
if mesh_instance and mesh_instance.mesh:
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(0.5, 0.8, 1.0, 0.5)
|
||||
mesh_instance.material_override = mat
|
||||
|
||||
func _exit_tree():
|
||||
# super._exit_tree()
|
||||
# Clean up references in neighbors so they don't hold onto dead objects
|
||||
for neighbor in connected_neighbors:
|
||||
if is_instance_valid(neighbor):
|
||||
neighbor.connected_neighbors.erase(self)
|
||||
|
||||
94
src/scenes/ship/builder/snapping_tool.gd
Normal file
94
src/scenes/ship/builder/snapping_tool.gd
Normal file
@ -0,0 +1,94 @@
|
||||
class_name SnappingTool extends RefCounted
|
||||
|
||||
const SNAP_DISTANCE = 2.0 # Meters
|
||||
const SNAP_ANGLE_THRESHOLD = deg_to_rad(45.0)
|
||||
|
||||
## Returns a Transform3D for the 'piece_to_place' that aligns one of its mounts
|
||||
## with a compatible mount on 'target_module'.
|
||||
static func get_best_snap_transform(
|
||||
piece_data: StructureData,
|
||||
target_module: Module,
|
||||
ray_origin: Vector3,
|
||||
ray_direction: Vector3
|
||||
) -> Transform3D:
|
||||
|
||||
var best_dist = INF
|
||||
var best_transform = Transform3D()
|
||||
var found_snap = false
|
||||
|
||||
# 1. Harvest all world-space mounts from the module's existing pieces.
|
||||
# Optimization: The Module could cache this list.
|
||||
var world_target_mounts = []
|
||||
for child in target_module.get_structural_pieces():
|
||||
if child is StructuralPiece and child.structure_data:
|
||||
world_target_mounts.append_array(child.structure_data.get_mounts_transformed(child.global_transform))
|
||||
|
||||
# 2. Determine where the player is pointing (Project ray out)
|
||||
var cursor_pos = ray_origin + ray_direction * 4.0 # Default 4m reach if no snap
|
||||
|
||||
# 3. Find the BEST pair of mounts (New Mount <-> Target Mount)
|
||||
for new_mount in piece_data.mounts:
|
||||
var local_mount_pos = new_mount.get("position", Vector3.ZERO)
|
||||
var local_mount_norm = new_mount.get("normal", Vector3.BACK)
|
||||
|
||||
for target_mount in world_target_mounts:
|
||||
# A. Check Distance to cursor
|
||||
# We check distance between target mount and cursor to see if user is "aiming" at it
|
||||
var dist_to_cursor = cursor_pos.distance_to(target_mount.position)
|
||||
|
||||
# Optimization: If user is too far from this target mount, skip detailed math
|
||||
if dist_to_cursor > SNAP_DISTANCE: continue
|
||||
|
||||
# B. Check if this is the closest snap so far
|
||||
if dist_to_cursor < best_dist:
|
||||
|
||||
# C. Calculate Orientation
|
||||
# We want the New Piece's Mount Normal to oppose the Target Mount Normal
|
||||
# Target Normal points OUT of the ship. New Normal points OUT of the piece.
|
||||
# When connected, they point at each other (180 degrees apart).
|
||||
|
||||
var desired_normal = -target_mount.normal # The direction we want our mount to face
|
||||
|
||||
# Calculate rotation to align 'local_mount_norm' to 'desired_normal'
|
||||
var rot_quat = _get_rotation_between_vectors(local_mount_norm, desired_normal)
|
||||
var final_basis = Basis(rot_quat)
|
||||
|
||||
# D. Calculate Position
|
||||
# We know where the target mount is (Target_Pos).
|
||||
# We need to place the Piece Origin such that:
|
||||
# Piece_Origin + (Rotated_Mount_Offset) = Target_Pos
|
||||
# Therefore: Piece_Origin = Target_Pos - (Rotated_Mount_Offset)
|
||||
var rotated_offset = final_basis * local_mount_pos
|
||||
var final_pos = target_mount.position - rotated_offset
|
||||
|
||||
best_transform = Transform3D(final_basis, final_pos)
|
||||
best_dist = dist_to_cursor
|
||||
found_snap = true
|
||||
|
||||
if found_snap:
|
||||
return best_transform
|
||||
else:
|
||||
# Fallback: Just place floating at cursor
|
||||
return Transform3D(Basis(), cursor_pos)
|
||||
|
||||
# Helper: Calculate quaternion to rotate vector A to vector B
|
||||
static func _get_rotation_between_vectors(v1: Vector3, v2: Vector3) -> Quaternion:
|
||||
v1 = v1.normalized()
|
||||
v2 = v2.normalized()
|
||||
|
||||
# If vectors are parallel (same direction)
|
||||
if v1.dot(v2) > 0.999:
|
||||
return Quaternion.IDENTITY
|
||||
|
||||
# If vectors are opposite (180 degrees)
|
||||
if v1.dot(v2) < -0.999:
|
||||
# We need any axis perpendicular to v1 to rotate 180 degrees around
|
||||
var axis = v1.cross(Vector3.UP)
|
||||
if axis.length_squared() < 0.01:
|
||||
axis = v1.cross(Vector3.RIGHT)
|
||||
return Quaternion(axis.normalized(), PI)
|
||||
|
||||
var cross = v1.cross(v2)
|
||||
var dot = v1.dot(v2)
|
||||
var w = sqrt(v1.length_squared() * v2.length_squared()) + dot
|
||||
return Quaternion(cross.x, cross.y, cross.z, w).normalized()
|
||||
1
src/scenes/ship/builder/snapping_tool.gd.uid
Normal file
1
src/scenes/ship/builder/snapping_tool.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bs73w22nrmlhj
|
||||
@ -1,7 +1,8 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://ddfsn0rtdnfda"]
|
||||
[gd_scene load_steps=8 format=3 uid="uid://ddfsn0rtdnfda"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://5noqmp8b267n" path="res://scenes/ship/components/grips/single_handhold.tscn" id="1_jlvj7"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="2_jlvj7"]
|
||||
[ext_resource type="PackedScene" uid="uid://7yc6a07xoccy" path="res://scenes/character/character_pawn_3d.tscn" id="3_7yxt7"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_kateb"]
|
||||
size = Vector3(50, 1, 50)
|
||||
@ -161,3 +162,6 @@ shape = SubResource("CylinderShape3D_nvgim")
|
||||
|
||||
[node name="Spawner" parent="." instance=ExtResource("2_jlvj7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.309784, 0, -12.84836)
|
||||
|
||||
[node name="CharacterPawn3D" parent="." instance=ExtResource("3_7yxt7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.12362689122544, -0.49999999999999645, -11.643245313710281)
|
||||
|
||||
Reference in New Issue
Block a user