From e075ff580dcf292d8a5141910dee12c1f4a2a9de Mon Sep 17 00:00:00 2001 From: Olof Pettersson Date: Sat, 25 Oct 2025 16:47:32 +0200 Subject: [PATCH] ZeroGMovementComponent WIP --- eva_suit_controller.tscn | 2 +- project.godot | 5 + scenes/tests/3d/character_pawn_3d.gd | 130 ++++------- scenes/tests/3d/character_pawn_3d.tscn | 16 +- ...ontroller.gd => eva_movement_component.gd} | 9 +- ...r.gd.uid => eva_movement_component.gd.uid} | 0 scenes/tests/3d/grips/grip_area_3d.gd | 3 + scenes/tests/3d/grips/single_handhold.tscn | 1 + scenes/tests/3d/player_controller_3d.gd | 16 +- scenes/tests/3d/zero_g_movement_component.gd | 218 ++++++++++++++++++ .../tests/3d/zero_g_movement_component.gd.uid | 1 + 11 files changed, 303 insertions(+), 98 deletions(-) rename scenes/tests/3d/{eva_controller.gd => eva_movement_component.gd} (95%) rename scenes/tests/3d/{eva_controller.gd.uid => eva_movement_component.gd.uid} (100%) create mode 100644 scenes/tests/3d/zero_g_movement_component.gd create mode 100644 scenes/tests/3d/zero_g_movement_component.gd.uid diff --git a/eva_suit_controller.tscn b/eva_suit_controller.tscn index 2020422..325ce30 100644 --- a/eva_suit_controller.tscn +++ b/eva_suit_controller.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://bm1rbv4tuppbc"] -[ext_resource type="Script" uid="uid://d4jka2etva22s" path="res://scenes/tests/3d/eva_controller.gd" id="1_mb22m"] +[ext_resource type="Script" uid="uid://d4jka2etva22s" path="res://scenes/tests/3d/eva_movement_component.gd" id="1_mb22m"] [node name="EVASuitController" type="Node3D"] script = ExtResource("1_mb22m") diff --git a/project.godot b/project.godot index aca9a03..42dd5ab 100644 --- a/project.godot +++ b/project.godot @@ -142,6 +142,11 @@ move_down_3d={ "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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +left_click={ +"deadzone": 0.2, +"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) +] +} [layer_names] diff --git a/scenes/tests/3d/character_pawn_3d.gd b/scenes/tests/3d/character_pawn_3d.gd index 252b7a3..d0fe5b3 100644 --- a/scenes/tests/3d/character_pawn_3d.gd +++ b/scenes/tests/3d/character_pawn_3d.gd @@ -7,7 +7,15 @@ class_name CharacterPawn3D @export var base_inertia: float = 1.0 # Pawn's inertia without suit ## State Machine -enum State {FLOATING, GRABBING_SURFACE, CHARGING_LAUNCH, ON_LADDER, GRIPPING_LADDER, WALKING, REACHING_MOVE} +enum State { + FLOATING, + GRABBING_GRIP, + CHARGING_LAUNCH, + ON_LADDER, + GRIPPING_LADDER, + WALKING, + REACHING_MOVE +} var current_state: State = State.FLOATING: set(new_state): if new_state == current_state: return @@ -21,6 +29,9 @@ var _roll_input: float = 0.0 var _vertical_input: float = 0.0 var _interact_pressed: bool = false var _interact_released: bool = false +var _l_pressed: bool = false +var _l_held: bool = false +var _l_released: bool = false var _r_pressed: bool = false var _r_held: bool = false var _r_released: bool = false @@ -33,9 +44,9 @@ var _pitch_yaw_input: Vector2 = Vector2.ZERO @export_range(0, PI / 2.0 - 0.01) var max_pitch_rad: float = deg_to_rad(60.0) @export var head_turn_lerp_speed: float = 15.0 -## Equipment Slots -var eva_suit_controller: EVASuitController = null -# var reach_move_controller = null # Placeholder +## Movement Components +@onready var eva_suit_component: EVAMovementComponent = $EVAMovementComponent +@onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent ## Physics State (Managed by Pawn) var angular_velocity: Vector3 = Vector3.ZERO @@ -44,39 +55,21 @@ var angular_velocity: Vector3 = Vector3.ZERO ## Other State Variables var current_gravity: Vector3 = Vector3.ZERO # TODO: Implement gravity detection var overlapping_ladder_area: Area3D = null -@onready var ladder_detector: Area3D = $LadderDetector # Ensure this exists in scene -var grab_surface_normal: Vector3 = Vector3.ZERO -var launch_direction: Vector3 = Vector3.ZERO -var launch_charge: float = 0.0 -@export var launch_charge_rate: float = 20.0 -@export var max_launch_speed: float = 15.0 -@export var orientation_speed: float = 2.0 # Used for orienting body to camera -@onready var grab_ray: RayCast3D = $GrabRay # Ensure this exists -@export var grab_check_distance: float = 5 @onready var grip_detector: Area3D = $GripDetector -var nearby_grips: Array[GripArea3D] = [] # List of available grips # Constants for State Checks const WALKABLE_GRAVITY_THRESHOLD: float = 1.0 func _ready(): - # Find equipped controllers - eva_suit_controller = find_child("EVASuitController", false, false) # Non-recursive, ignore owner - if eva_suit_controller: print("Found EVA Suit Controller") - # TODO: Find ReachMoveController - - # Connect ladder detector - if ladder_detector: - ladder_detector.area_entered.connect(_on_ladder_area_entered) - ladder_detector.area_exited.connect(_on_ladder_area_exited) - else: - printerr("LadderDetector Area3D node not found on CharacterPawn!") + # find movement components + if eva_suit_component: print("Found EVA Suit Controller") + if zero_g_movemement_component: print("Found Zero-G Movement Controller") # Connect grip detector signals - if grip_detector: + if grip_detector and zero_g_movemement_component: print("GripDetector Area3D node found") - grip_detector.area_entered.connect(_on_grip_area_entered) - grip_detector.area_exited.connect(_on_grip_area_exited) + grip_detector.area_entered.connect(zero_g_movemement_component.on_grip_area_entered) + grip_detector.area_exited.connect(zero_g_movemement_component.on_grip_area_exited) else: printerr("GripDetector Area3D node not found on CharacterPawn!") @@ -85,7 +78,7 @@ func _ready(): func _physics_process(delta: float): # 1. Apply Mouse Rotation (Universal head look) - _apply_mouse_rotation(delta) + _apply_mouse_rotation() # 2. Determine Potential State & Handle Transitions _update_state_transitions() @@ -93,11 +86,14 @@ func _physics_process(delta: float): # 3. Execute State Logic (Delegate to Controllers / Handle Pawn-native states) match current_state: State.FLOATING: - if eva_suit_controller: - eva_suit_controller.process_movement(delta, _move_input, _vertical_input, _roll_input, _r_held) - # Stabilization is handled within eva_suit_controller.process_movement - else: - pass # Pure freefall, velocity/angular velocity persist + if eva_suit_component: + eva_suit_component.process_movement(delta, _move_input, _vertical_input, _roll_input, _r_held) + # Stabilization is handled within eva_suit_component.process_movement + if zero_g_movemement_component: # Fallback to ZeroG controller (for initiating reach) + print("Pawn: Reaching: ", _l_held) + zero_g_movemement_component.process_movement(delta, _move_input, _l_pressed, _l_held, _l_released) + + pass # Pure freefall, velocity/angular velocity persist State.WALKING: _apply_walking_movement(delta) @@ -105,10 +101,6 @@ func _physics_process(delta: float): _apply_ladder_floating_drag(delta) State.GRIPPING_LADDER: _apply_ladder_movement(delta) - State.GRABBING_SURFACE: - _handle_grabbed_surface_state(delta) # Sets vel/angular to zero - State.CHARGING_LAUNCH: - _handle_launch_charge(delta) # Sets vel/angular to zero State.REACHING_MOVE: # TODO: Delegate to ReachMoveController.process_movement(...) pass @@ -130,8 +122,8 @@ func _physics_process(delta: float): if collision_count > 0: var collision = get_slide_collision(collision_count - 1) # Get last collision # Delegate or handle basic bounce - if current_state == State.FLOATING and eva_suit_controller: - eva_suit_controller.handle_collision(collision, collision_energy_loss) + if current_state == State.FLOATING and eva_suit_component: + eva_suit_component.handle_collision(collision, collision_energy_loss) else: _handle_basic_collision(collision) else: @@ -148,17 +140,21 @@ func _on_enter_state(state: State): print("Entering State: ", State.keys()[state]) match state: State.FLOATING: - if eva_suit_controller: eva_suit_controller.on_enter_state() - State.GRABBING_SURFACE, State.GRIPPING_LADDER, State.CHARGING_LAUNCH: + if eva_suit_component: eva_suit_component.on_enter_state() + State.GRABBING_GRIP, State.GRIPPING_LADDER, State.CHARGING_LAUNCH: velocity = Vector3.ZERO - angular_velocity = Vector3.ZERO # Stop all motion immediately + angular_velocity = Vector3.ZERO + if zero_g_movemement_component and state == State.GRABBING_GRIP: + zero_g_movemement_component.on_enter_state(state) # Notify controller # Add other state entry logic as needed func _on_exit_state(state: State): # print("Exiting State: ", State.keys()[state]) # Optional debug match state: State.FLOATING: - if eva_suit_controller: eva_suit_controller.on_exit_state() + if eva_suit_component: eva_suit_component.on_exit_state() + State.GRABBING_GRIP: + if zero_g_movemement_component: zero_g_movemement_component.on_exit_state(state) # Add other state exit logic as needed # --- State Transition Logic --- @@ -172,10 +168,8 @@ func _update_state_transitions(): State.FLOATING, State.WALKING, State.ON_LADDER: if _interact_pressed: if potential_state == State.ON_LADDER: self.current_state = State.GRIPPING_LADDER - elif _check_for_grab_surface(): self.current_state = State.GRABBING_SURFACE elif current_state != potential_state and \ current_state != State.GRIPPING_LADDER and \ - current_state != State.GRABBING_SURFACE and \ current_state != State.CHARGING_LAUNCH and \ current_state != State.REACHING_MOVE: self.current_state = potential_state @@ -184,10 +178,6 @@ func _update_state_transitions(): # if not _interact_held or not is_instance_valid(overlapping_ladder_area): self.current_state = State.ON_LADDER # elif _move_input != Vector2.ZERO: _start_charge(true) - # State.GRABBING_SURFACE: - # if _interact_released: self.current_state = potential_state - # elif _move_input != Vector2.ZERO: _start_charge(false) - # State.CHARGING_LAUNCH: # if _interact_released: _execute_launch(); self.current_state = potential_state # elif _move_input == Vector2.ZERO: _cancel_charge() @@ -196,7 +186,7 @@ func _update_state_transitions(): pass # TODO # --- Universal Rotation --- -func _apply_mouse_rotation(delta: float): +func _apply_mouse_rotation(): if _pitch_yaw_input != Vector2.ZERO: camera_pivot.rotate_y(-_pitch_yaw_input.x) @@ -227,53 +217,35 @@ func _handle_basic_collision(collision: KinematicCollision3D): # Applies torque affecting angular velocity func add_torque(torque_global: Vector3, delta: float): # Calculate effective inertia (base + suit multiplier if applicable) - var effective_inertia = base_inertia * (eva_suit_controller.inertia_multiplier if eva_suit_controller else 1.0) + var effective_inertia = base_inertia * (eva_suit_component.inertia_multiplier if eva_suit_component else 1.0) if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero # Apply change directly to angular velocity using the global torque angular_velocity += (torque_global / effective_inertia) * delta # --- Movement Implementations (Keep non-EVA ones here) --- -func _apply_walking_movement(delta: float): pass # TODO +func _apply_walking_movement(_delta: float): pass # TODO func _apply_ladder_floating_drag(delta: float): velocity = velocity.lerp(Vector3.ZERO, delta * 2.0); angular_velocity = angular_velocity.lerp(Vector3.ZERO, delta * 2.0) -func _apply_ladder_movement(delta: float): pass # TODO -func _handle_grabbed_surface_state(delta: float): velocity = Vector3.ZERO; angular_velocity = Vector3.ZERO -func _handle_launch_charge(delta: float): launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed); velocity = Vector3.ZERO; angular_velocity = Vector3.ZERO -func _execute_launch(): velocity = launch_direction * launch_charge; launch_charge = 0.0 +func _apply_ladder_movement(_delta: float): pass # TODO # --- Input Setters/Resets (Add vertical to set_movement_input) --- func set_movement_input(move: Vector2, roll: float, vertical: float): _move_input = move; _roll_input = roll; _vertical_input = vertical func set_interaction_input(pressed: bool, released: bool): _interact_pressed = pressed; _interact_released = released func set_rotation_input(pitch_yaw_input: Vector2): _pitch_yaw_input += pitch_yaw_input -func set_click_input(r_pressed: bool, r_held: bool, r_released: bool): _r_pressed = r_pressed; _r_held = r_held; _r_released = r_released +func set_click_input(l_pressed: bool, l_held: bool, l_released: bool, r_pressed: bool, r_held: bool, r_released: bool): + _l_pressed = l_pressed + _l_held = l_held + _l_released = l_released + _r_pressed = r_pressed + _r_held = r_held + _r_released = r_released func _reset_inputs(): _move_input = Vector2.ZERO; _roll_input = 0.0; _vertical_input = 0.0; _interact_pressed = false; _interact_released = false; _r_pressed = false; _r_released = false; _pitch_yaw_input = Vector2.ZERO # Keep _r_held # --- Helper Functions --- -func _check_for_grab_surface() -> bool: - grab_ray.target_position = camera_pivot.transform.basis.z * -grab_check_distance # Ray points forward from camera pivot - grab_ray.force_raycast_update() - if grab_ray.is_colliding(): grab_surface_normal = grab_ray.get_collision_normal(); return true - return false - func _on_ladder_area_entered(area: Area3D): if area.is_in_group("Ladders"): overlapping_ladder_area = area func _on_ladder_area_exited(area: Area3D): if area == overlapping_ladder_area: overlapping_ladder_area = null func _reset_head_yaw(delta: float): # Smoothly apply the reset target to the actual pivot rotation camera_pivot.rotation.y = lerpf(camera_pivot.rotation.y, 0.0, delta * head_turn_lerp_speed) - -func _on_grip_area_entered(area: Area3D): - print("Area detected") - if area is GripArea3D: # Check if the entered area is actually a GripArea3D node - var grip = area as GripArea3D - if not grip in nearby_grips: - nearby_grips.append(grip) - print("Detected nearby grip: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN") # Print parent name for context - -func _on_grip_area_exited(area: Area3D): - if area is GripArea3D: - var grip = area as GripArea3D - if grip in nearby_grips: - nearby_grips.erase(grip) - print("Grip out of range: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN") diff --git a/scenes/tests/3d/character_pawn_3d.tscn b/scenes/tests/3d/character_pawn_3d.tscn index 89bdb78..47fbacb 100644 --- a/scenes/tests/3d/character_pawn_3d.tscn +++ b/scenes/tests/3d/character_pawn_3d.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=7 format=3 uid="uid://7yc6a07xoccy"] +[gd_scene load_steps=8 format=3 uid="uid://7yc6a07xoccy"] [ext_resource type="Script" uid="uid://cdmmiixa75f3x" path="res://scenes/tests/3d/character_pawn_3d.gd" id="1_4frsu"] [ext_resource type="Script" uid="uid://vjfk3xnapfti" path="res://scenes/tests/3d/player_controller_3d.gd" id="2_r62el"] [ext_resource type="PackedScene" uid="uid://bm1rbv4tuppbc" path="res://eva_suit_controller.tscn" id="3_gnddn"] +[ext_resource type="Script" uid="uid://y3vo40i16ek3" path="res://scenes/tests/3d/zero_g_movement_component.gd" id="4_8jhjh"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_6vm80"] @@ -21,11 +22,6 @@ shape = SubResource("CapsuleShape3D_6vm80") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] mesh = SubResource("CapsuleMesh_6vm80") -[node name="GrabRay" type="RayCast3D" parent="."] -target_position = Vector3(0, 0, -1) -collide_with_areas = true -debug_shape_custom_color = Color(0.443137, 0, 0, 0.615686) - [node name="PlayerController3D" type="Node" parent="."] script = ExtResource("2_r62el") metadata/_custom_type_script = "uid://vjfk3xnapfti" @@ -39,8 +35,6 @@ spring_length = 3.0 [node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"] current = true -[node name="EVASuitController" parent="." instance=ExtResource("3_gnddn")] - [node name="GripDetector" type="Area3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.985542) collision_layer = 0 @@ -49,3 +43,9 @@ monitorable = false [node name="CollisionShape3D" type="CollisionShape3D" parent="GripDetector"] shape = SubResource("SphereShape3D_gnddn") + +[node name="ZeroGMovementComponent" type="Node3D" parent="."] +script = ExtResource("4_8jhjh") +metadata/_custom_type_script = "uid://y3vo40i16ek3" + +[node name="EVAMovementComponent" parent="." instance=ExtResource("3_gnddn")] diff --git a/scenes/tests/3d/eva_controller.gd b/scenes/tests/3d/eva_movement_component.gd similarity index 95% rename from scenes/tests/3d/eva_controller.gd rename to scenes/tests/3d/eva_movement_component.gd index ec756f4..093f019 100644 --- a/scenes/tests/3d/eva_controller.gd +++ b/scenes/tests/3d/eva_movement_component.gd @@ -1,6 +1,6 @@ # eva_suit_controller.gd extends Node # Or Node3D if thrusters need specific positions later -class_name EVASuitController +class_name EVAMovementComponent ## References (Set automatically in _ready) var pawn: CharacterBody3D @@ -8,6 +8,7 @@ var camera_pivot: Node3D var camera: Camera3D ## EVA Parameters (Moved from ZeroGPawn) +@export var orientation_speed: float = 2.0 # Used for orienting body to camera @export var move_speed: float = 2.0 @export var roll_torque: float = 2.5 @export var angular_damping: float = 0.95 # Base damping applied by pawn, suit might add more? @@ -22,7 +23,7 @@ var stabilization_enabled: bool = false func _ready(): pawn = get_parent() as CharacterBody3D if not pawn: - printerr("EVASuitController must be a child of a CharacterBody3D pawn.") + printerr("EVAMovementComponent must be a child of a CharacterBody3D pawn.") return # Make sure the paths match your CharacterPawn scene structure camera_pivot = pawn.get_node_or_null("CameraPivot") @@ -30,7 +31,7 @@ func _ready(): camera = camera_pivot.get_node_or_null("SpringArm/Camera3D") # Adjusted path for SpringArm if not camera_pivot or not camera: - printerr("EVASuitController could not find CameraPivot/SpringArm/Camera3D on pawn.") + printerr("EVAMovementComponent could not find CameraPivot/SpringArm/Camera3D on pawn.") # --- Standardized Movement API --- @@ -124,7 +125,7 @@ func _orient_pawn(delta: float): # 2. Smoothly Interpolate Towards Target Basis var current_basis = pawn.global_transform.basis - var new_basis = current_basis.slerp(target_basis, delta * pawn.orientation_speed) + var new_basis = current_basis.slerp(target_basis, delta * orientation_speed) # Store the body's yaw *before* applying the new basis var old_body_yaw = current_basis.get_euler().y diff --git a/scenes/tests/3d/eva_controller.gd.uid b/scenes/tests/3d/eva_movement_component.gd.uid similarity index 100% rename from scenes/tests/3d/eva_controller.gd.uid rename to scenes/tests/3d/eva_movement_component.gd.uid diff --git a/scenes/tests/3d/grips/grip_area_3d.gd b/scenes/tests/3d/grips/grip_area_3d.gd index 2c1c3c0..d84c804 100644 --- a/scenes/tests/3d/grips/grip_area_3d.gd +++ b/scenes/tests/3d/grips/grip_area_3d.gd @@ -65,3 +65,6 @@ func release(pawn: CharacterPawn3D): # Re-enable collision? return true return false + +func is_occupied() -> bool: + return occupant != null diff --git a/scenes/tests/3d/grips/single_handhold.tscn b/scenes/tests/3d/grips/single_handhold.tscn index dc7d109..a920e39 100644 --- a/scenes/tests/3d/grips/single_handhold.tscn +++ b/scenes/tests/3d/grips/single_handhold.tscn @@ -24,6 +24,7 @@ radius = 0.01 [node name="Grip" type="StaticBody3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.1) +collision_layer = 32769 [node name="MeshInstance3D" type="MeshInstance3D" parent="Grip"] mesh = SubResource("CylinderMesh_c81dj") diff --git a/scenes/tests/3d/player_controller_3d.gd b/scenes/tests/3d/player_controller_3d.gd index e5f3384..bb67bfc 100644 --- a/scenes/tests/3d/player_controller_3d.gd +++ b/scenes/tests/3d/player_controller_3d.gd @@ -30,13 +30,17 @@ func _physics_process(_delta): var vertical_input = Input.get_action_strength("move_up_3d") - Input.get_action_strength("move_down_3d") var interact_pressed = Input.is_action_just_pressed("spacebar_3d") var interact_released = Input.is_action_just_released("spacebar_3d") # Send release too - var right_click_pressed = Input.is_action_just_pressed("right_click") - var right_click_held = Input.is_action_pressed("right_click") - var right_click_released = Input.is_action_just_released("right_click") server_process_movement_input.rpc_id(1, move_vec, roll_input, vertical_input) server_process_interaction_input.rpc_id(1, interact_pressed, interact_released) - server_process_clicks.rpc_id(1, right_click_pressed, right_click_held, right_click_released) + server_process_clicks.rpc_id(1, + Input.is_action_just_pressed("left_click"), + Input.is_action_pressed("left_click"), + Input.is_action_just_released("left_click"), + Input.is_action_just_pressed("right_click"), + Input.is_action_pressed("right_click"), + Input.is_action_just_released("right_click") + ) @rpc("any_peer", "call_local") func server_process_movement_input(move: Vector2, roll: float, vertical: float): @@ -54,9 +58,9 @@ func server_process_rotation_input(input: Vector2): possessed_pawn.set_rotation_input(input) @rpc("any_peer", "call_local") -func server_process_clicks(r_pressed, r_held, r_released): +func server_process_clicks(l_pressed, l_held, l_released, r_pressed, r_held, r_released): if is_instance_valid(possessed_pawn): - possessed_pawn.set_click_input(r_pressed, r_held, r_released) + possessed_pawn.set_click_input(l_pressed, l_held, l_released, r_pressed, r_held, r_released) func possess(pawn_to_control: CharacterPawn3D): possessed_pawn = pawn_to_control diff --git a/scenes/tests/3d/zero_g_movement_component.gd b/scenes/tests/3d/zero_g_movement_component.gd new file mode 100644 index 0000000..d9a2663 --- /dev/null +++ b/scenes/tests/3d/zero_g_movement_component.gd @@ -0,0 +1,218 @@ +# zero_g_move_controller.gd +extends Node +class_name ZeroGMovementComponent + +## References +var pawn: CharacterPawn3D +var camera_pivot: Node3D + +## State & Parameters +var current_grip: GripArea3D = null # Use GripArea3D type hint +var nearby_grips: Array[GripArea3D] = [] +@export var reach_speed: float = 10.0 # Speed pawn moves towards grip +@export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip +@export var launch_charge_rate: float = 20.0 +@export var max_launch_speed: float = 15.0 +var launch_direction: Vector3 = Vector3.ZERO +var launch_charge: float = 0.0 + +# Enum for internal state (distinct from pawn's main state) +enum ReachState { IDLE, REACHING, GRIPPING, CHARGING_LAUNCH } +var reach_state: ReachState = ReachState.IDLE + +func _ready(): + pawn = get_parent() as CharacterPawn3D + if not pawn: printerr("ZeroGMovementComponent must be child of CharacterPawn3D") + camera_pivot = pawn.get_node_or_null("CameraPivot") + if not camera_pivot: printerr("ZeroGMovementComponent couldn't find CameraPivot") + +# --- Standardized Movement API --- + +## Called by Pawn when relevant state is active (e.g., GRABBING_GRIP, REACHING_MOVE) +func process_movement(delta: float, move_input: Vector2, reaching_pressed: bool, reaching_held: bool, reaching_released: bool): + if not is_instance_valid(pawn): return + + _update_reach_state(delta, move_input, reaching_pressed, reaching_held, reaching_released) + + match reach_state: + ReachState.IDLE: + # If pawn is FLOATING and interact pressed, try initiating a reach + print("ZerGoMovementComponent: Reaching: ", reaching_held) + ReachState.REACHING: + if pawn.current_state == CharacterPawn3D.State.FLOATING and reaching_held: + print("bar") + _try_initiate_reach() + _process_reaching(delta) + ReachState.GRIPPING: + _handle_gripping_state(delta, move_input) + ReachState.CHARGING_LAUNCH: + _handle_launch_charge(delta) + + +## Called by Pawn for collision (optional, might not be needed if grabbing stops movement) +func handle_collision(collision: KinematicCollision3D, collision_energy_loss: float): + # Basic bounce if somehow colliding while using this controller + var surface_normal = collision.get_normal() + pawn.velocity = pawn.velocity.bounce(surface_normal) + pawn.velocity *= (1.0 - collision_energy_loss * 0.5) + +## Called by Pawn when entering a state managed by this controller +func on_enter_state(pawn_state: CharacterPawn3D.State): + print("ZeroGMovementComponent activated for state: ", CharacterPawn3D.State.keys()[pawn_state]) + if pawn_state == CharacterPawn3D.State.GRABBING_GRIP: + reach_state = ReachState.GRIPPING + pawn.velocity = Vector3.ZERO + pawn.angular_velocity = Vector3.ZERO + # else: # e.g., REACHING_MOVE? + # reach_state = ReachState.IDLE # Or SEARCHING? + +## Called by Pawn when exiting a state managed by this controller +func on_exit_state(pawn_state: CharacterPawn3D.State): + print("ZeroGMovementComponent deactivated for state: ", CharacterPawn3D.State.keys()[pawn_state]) + # Ensure grip is released if pawn state changes unexpectedly + _release_current_grip() + reach_state = ReachState.IDLE + + +# --- Internal Logic --- + +func _update_reach_state(_delta: float, _move_input: Vector2, reaching_pressed: bool, reaching_held: bool, _reaching_released: bool): + match reach_state: + ReachState.IDLE: + # Already handled initiating reach in process_movement + if reaching_pressed or reaching_held: + reach_state = ReachState.REACHING + ReachState.REACHING: + # TODO: If reach animation completes/hand near target -> GRIPPING + # If interact released during reach -> CANCEL -> IDLE + + if not reaching_held: + _cancel_reach() + ReachState.GRIPPING: + if not is_instance_valid(current_grip): + _release_current_grip() + # Pawn's main state machine will handle transition out + # elif move_input != Vector2.ZERO: + # _start_charge(move_input) # Start charging launch + # ReachState.CHARGING_LAUNCH: + # if reaching_released: + # _execute_launch() + # # Pawn's main state machine handles transition out + # elif move_input == Vector2.ZERO: # Cancel charge while holding interact + # reach_state = ReachState.GRIPPING + # print("ZeroGMovementComponent: Cancelled Launch Charge") + + +func _try_initiate_reach(): + print("foo") + print("ZeroGMovementComponent: Trying to Initiate Grab on", nearby_grips) + + var closest_grip: GripArea3D = _find_closest_available_grip() + print(closest_grip) + if is_instance_valid(closest_grip): + # if closest_grip.grab(pawn): + current_grip = closest_grip + # Instead of directly setting pawn state, maybe pawn checks if grip was successful? + # For now, assume direct control: + pawn.current_state = CharacterPawn3D.State.GRABBING_GRIP # Transition pawn state + reach_state = ReachState.GRIPPING # Set internal state + print("ZeroGMovementComponent: Initiated grab on ", current_grip.get_parent().name) + # else: + # print("ZeroGMovementComponent: Grab failed (grip occupied?)") + else: + print("ZeroGMovementComponent: No available grips in range to initiate reach.") + # TODO: Initiate generic surface grab? + +func _find_closest_available_grip() -> GripArea3D: + var closest_grip: GripArea3D = null + var closest_distance: float = INF + for grip: GripArea3D in nearby_grips: + if grip.is_occupied(): + continue + var distance = pawn.global_transform.origin.distance_to(grip.global_transform.origin) + if distance < closest_distance: + closest_distance = distance + closest_grip = grip + return closest_grip + +func _process_reaching(_delta: float): + # TODO: Drive IK target towards current_grip.get_grip_transform().origin + # TODO: Monitor distance / animation state + # When close enough: reach_state = ReachState.GRIPPING + pass + + +func _handle_gripping_state(delta: float, _move_input: Vector2): + pawn.velocity = Vector3.ZERO + pawn.angular_velocity = Vector3.ZERO + if not is_instance_valid(current_grip): _cancel_reach(); return # Safety check + + # --- Simple Visualization: Lerp towards Grip Transform --- + var target_transform = current_grip.get_grip_transform(pawn.global_position) + pawn.global_transform.origin = pawn.global_transform.origin.lerp(target_transform.origin, delta * reach_speed) + pawn.global_transform.basis = pawn.global_transform.basis.slerp(target_transform.basis, delta * reach_orient_speed) + + +func _start_charge(move_input: Vector2): + if not is_instance_valid(current_grip): return + reach_state = ReachState.CHARGING_LAUNCH + launch_charge = 0.0 + + # Calculate launch direction based on input and push-off normal + var push_dir_local = (Vector3.FORWARD * -move_input.y + Vector3.RIGHT * move_input.x).normalized() + var push_normal = current_grip.get_push_off_normal() + # Basis oriented away from surface, using pawn's current up as reference + var _surface_basis = Basis.looking_at(push_normal, pawn.global_transform.basis.y).orthonormalized() + + var input_influence = 0.5 # Blend between pushing straight off and sliding along input dir + var input_dir_world = pawn.camera_pivot.global_transform.basis * push_dir_local # Convert input dir relative to camera/head + var push_dir_along_surface = input_dir_world.slide(push_normal).normalized() + + launch_direction = (push_normal * (1.0 - input_influence) + push_dir_along_surface * input_influence).normalized() + print("ZeroGMovementComponent: Charging Launch") + + +func _handle_launch_charge(delta: float): + launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed) + pawn.velocity = Vector3.ZERO + pawn.angular_velocity = Vector3.ZERO + + +func _execute_launch(): + if not is_instance_valid(current_grip): return # Safety check + _release_current_grip() # Release AFTER calculating direction + pawn.velocity = launch_direction * launch_charge # Apply launch velocity to pawn + launch_charge = 0.0 + reach_state = ReachState.IDLE + print("ZeroGMovementComponent: Launched with speed ", pawn.velocity.length()) + + +func _release_current_grip(): + if is_instance_valid(current_grip): + current_grip.release(pawn) + current_grip = null + reach_state = ReachState.IDLE + + +func _cancel_reach(): + # TODO: Logic to stop IK/animation if reach is cancelled mid-way + _release_current_grip() # Ensure grip reference is cleared + reach_state = ReachState.IDLE + print("ZeroGMovementComponent: Reach cancelled.") + + +# --- Signal Handlers --- +func on_grip_area_entered(area: Area3D): + print("Area detected") + if area is GripArea3D: # Check if the entered area is actually a GripArea3D node + var grip = area as GripArea3D + if not grip in nearby_grips: + nearby_grips.append(grip) + print("Detected nearby grip: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN") # Print parent name for context + +func on_grip_area_exited(area: Area3D): + if area is GripArea3D: + var grip = area as GripArea3D + if grip in nearby_grips: + nearby_grips.erase(grip) + print("Grip out of range: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN") diff --git a/scenes/tests/3d/zero_g_movement_component.gd.uid b/scenes/tests/3d/zero_g_movement_component.gd.uid new file mode 100644 index 0000000..1d9b4a9 --- /dev/null +++ b/scenes/tests/3d/zero_g_movement_component.gd.uid @@ -0,0 +1 @@ +uid://y3vo40i16ek3