ZeroGMovementComponent WIP

This commit is contained in:
2025-10-25 16:47:32 +02:00
parent 90e756ad28
commit e075ff580d
11 changed files with 303 additions and 98 deletions

View File

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

View File

@ -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]

View File

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

View File

@ -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")]

View File

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

View File

@ -65,3 +65,6 @@ func release(pawn: CharacterPawn3D):
# Re-enable collision?
return true
return false
func is_occupied() -> bool:
return occupant != null

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
uid://y3vo40i16ek3