WIP Placing 3d ShipParts

This commit is contained in:
2025-11-20 22:20:41 +01:00
parent 8fd540ddfc
commit 24ce1edb38
14 changed files with 654 additions and 86 deletions

View File

@ -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())