WIP Placing 3d ShipParts
This commit is contained in:
@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user