feat: overhaul movement for flow — momentum-preserving states, smooth crouch, landing feel
- Quake-style air-strafe accelerate (real speed gain from strafing; external speed from dash/rockets never clamped) - Wall run: keeps entry momentum (incl. upward carry), accelerates along the wall instead of hard-setting velocity, gravity fades in over the run - Slide: slope physics (gravity projected along floor accelerates downhill), flat-ground decel instead of multiplicative friction, entry boost, direct slide->wallrun and slide->dash transitions - Dash: cooldown 10s -> 2s, per-player (was a static shared across instances), momentum fully kept on exit, FOV punch event - Grapple: taut pendulum with active rope control (W reels in, S pays out), release keeps swing energy - Wall climb: carries upward momentum in, jump-off kick, shared vault helper - Crouch capsule: single owner in the machine, smoothly lerped, ceiling check; camera eye height follows via crouch factor (no more head snapping) - Landing: machine emits 'land' events scaled by impact; camera dip + pitched down thud; footsteps get pitch variation - Camera rig: event-driven (land dip, dash FOV kick, vault pitch impulse), slide roll tilt - Model: Jump vs Fall split by vertical velocity, Land one-shot on heavy landings, wall-run body lean (synced to remotes via synced_wall_side) Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c4a538a469
commit
0ba14a9469
@@ -1,7 +1,12 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
class_name FPSCameraRig
|
class_name FPSCameraRig
|
||||||
|
|
||||||
## FPS camera rig — handles mouse look, head bob, FOV kick, wall-run tilt.
|
## FPS camera rig — mouse look, head bob, FOV kick, wall-run tilt, plus
|
||||||
|
## movement-feel feedback driven by MovementStateMachine events:
|
||||||
|
## - landing dip scaled by impact speed
|
||||||
|
## - pitch impulse (vault kick) with spring decay
|
||||||
|
## - smooth crouch/slide eye height (follows the capsule's smoothed height)
|
||||||
|
## - slide roll tilt and dash FOV punch
|
||||||
## Attach as child of the CharacterBody3D player. Camera3D is a child of this node.
|
## Attach as child of the CharacterBody3D player. Camera3D is a child of this node.
|
||||||
|
|
||||||
@export var sensitivity: float = 0.002
|
@export var sensitivity: float = 0.002
|
||||||
@@ -16,16 +21,50 @@ var _target_fov: float = 90.0
|
|||||||
var _weapon_fov_override: float = -1.0
|
var _weapon_fov_override: float = -1.0
|
||||||
var _step_offset_y: float = 0.0
|
var _step_offset_y: float = 0.0
|
||||||
|
|
||||||
|
# Movement-feel feedback state
|
||||||
|
var _base_eye_y: float = 0.7
|
||||||
|
var _land_dip: float = 0.0 # current downward dip (springs back to 0)
|
||||||
|
var _pitch_impulse: float = 0.0 # extra camera pitch in radians, decays
|
||||||
|
var _dash_fov_kick: float = 0.0 # extra FOV from dashing, decays
|
||||||
|
var _machine: MovementStateMachine = null
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
camera = get_node_or_null("Camera3D")
|
camera = get_node_or_null("Camera3D")
|
||||||
|
_base_eye_y = position.y
|
||||||
if camera and params:
|
if camera and params:
|
||||||
camera.fov = SettingsManager.world_fov
|
camera.fov = SettingsManager.world_fov
|
||||||
_target_fov = SettingsManager.world_fov
|
_target_fov = SettingsManager.world_fov
|
||||||
# Explicitly enable callbacks — set_script() at runtime doesn't auto-register them
|
# Explicitly enable callbacks — set_script() at runtime doesn't auto-register them
|
||||||
set_process_input(true)
|
set_process_input(true)
|
||||||
set_process(true)
|
set_process(true)
|
||||||
|
# The state machine may be added after us; hook events once the tree settles.
|
||||||
|
call_deferred("_connect_machine")
|
||||||
|
|
||||||
|
|
||||||
|
func _connect_machine() -> void:
|
||||||
|
var player := get_parent()
|
||||||
|
if not player:
|
||||||
|
return
|
||||||
|
var sm = player.get_node_or_null("MovementStateMachine")
|
||||||
|
if sm and sm is MovementStateMachine:
|
||||||
|
_machine = sm
|
||||||
|
if not sm.movement_event.is_connected(_on_movement_event):
|
||||||
|
sm.movement_event.connect(_on_movement_event)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||||
|
match ev:
|
||||||
|
"land":
|
||||||
|
var fall_speed: float = data.get("fall_speed", 0.0)
|
||||||
|
var scale_p: float = params.land_dip_scale if params else 0.012
|
||||||
|
var max_p: float = params.land_dip_max if params else 0.25
|
||||||
|
_land_dip = clampf(fall_speed * scale_p, 0.0, max_p)
|
||||||
|
"dash":
|
||||||
|
_dash_fov_kick = 15.0
|
||||||
|
"vault":
|
||||||
|
pass # pitch impulse is applied via add_pitch_impulse by the machine
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
@@ -53,6 +92,14 @@ func _process(delta: float) -> void:
|
|||||||
if not player:
|
if not player:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not _machine:
|
||||||
|
_connect_machine()
|
||||||
|
|
||||||
|
var state := ""
|
||||||
|
if _machine:
|
||||||
|
state = _machine.current_state
|
||||||
|
var is_sliding := state == "slide"
|
||||||
|
|
||||||
# ── FOV kick ──────────────────────────────────────────────────────────
|
# ── FOV kick ──────────────────────────────────────────────────────────
|
||||||
_target_fov = SettingsManager.world_fov
|
_target_fov = SettingsManager.world_fov
|
||||||
var hspeed := Vector2(player.velocity.x, player.velocity.z).length()
|
var hspeed := Vector2(player.velocity.x, player.velocity.z).length()
|
||||||
@@ -61,15 +108,24 @@ func _process(delta: float) -> void:
|
|||||||
var speed_factor = clampf((hspeed - params.walk_speed) / (params.walk_speed * 0.5), 0.0, 1.0)
|
var speed_factor = clampf((hspeed - params.walk_speed) / (params.walk_speed * 0.5), 0.0, 1.0)
|
||||||
_target_fov = lerpf(SettingsManager.world_fov, SettingsManager.world_fov + 20.0, speed_factor)
|
_target_fov = lerpf(SettingsManager.world_fov, SettingsManager.world_fov + 20.0, speed_factor)
|
||||||
|
|
||||||
|
# Dash punch decays on top
|
||||||
|
_dash_fov_kick = lerpf(_dash_fov_kick, 0.0, 1.0 - exp(-6.0 * delta))
|
||||||
|
_target_fov += _dash_fov_kick
|
||||||
|
|
||||||
if _weapon_fov_override > 0.0:
|
if _weapon_fov_override > 0.0:
|
||||||
_target_fov = _weapon_fov_override
|
_target_fov = _weapon_fov_override
|
||||||
|
|
||||||
camera.fov = lerpf(camera.fov, _target_fov, 1.0 - exp(-params.fov_lerp_speed * delta))
|
camera.fov = lerpf(camera.fov, _target_fov, 1.0 - exp(-params.fov_lerp_speed * delta))
|
||||||
|
|
||||||
# ── Head bob ──────────────────────────────────────────────────────────
|
# ── Crouch / slide eye height (follows the smoothed capsule) ──────────
|
||||||
var sm = player.get_node_or_null("MovementStateMachine")
|
var crouch_factor := 0.0
|
||||||
var is_sliding = sm and sm.current_state == "slide"
|
if _machine:
|
||||||
|
crouch_factor = _machine.get_crouch_factor()
|
||||||
|
var eye_drop: float = _machine.original_capsule_height * 0.5 if _machine else 0.9
|
||||||
|
var target_eye_y: float = _base_eye_y - crouch_factor * eye_drop
|
||||||
|
position.y = lerpf(position.y, target_eye_y, 1.0 - exp(-12.0 * delta))
|
||||||
|
|
||||||
|
# ── Head bob ──────────────────────────────────────────────────────────
|
||||||
if player.is_on_floor() and hspeed > 1.0 and not is_sliding:
|
if player.is_on_floor() and hspeed > 1.0 and not is_sliding:
|
||||||
_bob_timer += delta * params.head_bob_frequency * (hspeed / params.walk_speed)
|
_bob_timer += delta * params.head_bob_frequency * (hspeed / params.walk_speed)
|
||||||
var bob_y := sin(_bob_timer) * params.head_bob_amplitude
|
var bob_y := sin(_bob_timer) * params.head_bob_amplitude
|
||||||
@@ -81,12 +137,25 @@ func _process(delta: float) -> void:
|
|||||||
camera.position.y = lerp(camera.position.y, 0.0, 0.15)
|
camera.position.y = lerp(camera.position.y, 0.0, 0.15)
|
||||||
camera.position.x = lerp(camera.position.x, 0.0, 0.15)
|
camera.position.x = lerp(camera.position.x, 0.0, 0.15)
|
||||||
|
|
||||||
|
# ── Landing dip (springs back up) ─────────────────────────────────────
|
||||||
|
_land_dip = lerpf(_land_dip, 0.0, 1.0 - exp(-8.0 * delta))
|
||||||
|
camera.position.y -= _land_dip
|
||||||
|
|
||||||
# ── Step Smoothing ─────────────────────────────────────────────────────
|
# ── Step Smoothing ─────────────────────────────────────────────────────
|
||||||
_step_offset_y = lerpf(_step_offset_y, 0.0, 15.0 * delta)
|
_step_offset_y = lerpf(_step_offset_y, 0.0, 15.0 * delta)
|
||||||
camera.position.y += _step_offset_y
|
camera.position.y += _step_offset_y
|
||||||
|
|
||||||
# ── Wall-run tilt ─────────────────────────────────────────────────────
|
# ── Pitch impulse (vault kick etc.), spring-decayed ───────────────────
|
||||||
_current_tilt = lerpf(_current_tilt, _target_tilt, 1.0 - exp(-params.wall_run_tilt_speed * delta))
|
_pitch_impulse = lerpf(_pitch_impulse, 0.0, 1.0 - exp(-7.0 * delta))
|
||||||
|
camera.rotation.x = _pitch_impulse
|
||||||
|
|
||||||
|
# ── Tilt: wall-run lean + slide roll ──────────────────────────────────
|
||||||
|
var tilt_target := _target_tilt
|
||||||
|
if is_sliding:
|
||||||
|
# Subtle roll in the slide's steering direction
|
||||||
|
var slide_roll: float = params.slide_tilt_angle
|
||||||
|
tilt_target += -_machine.input_dir.x * slide_roll if _machine else 0.0
|
||||||
|
_current_tilt = lerpf(_current_tilt, tilt_target, 1.0 - exp(-params.wall_run_tilt_speed * delta))
|
||||||
camera.rotation.z = deg_to_rad(_current_tilt)
|
camera.rotation.z = deg_to_rad(_current_tilt)
|
||||||
|
|
||||||
## Called by the movement system to set wall-run tilt direction.
|
## Called by the movement system to set wall-run tilt direction.
|
||||||
@@ -105,8 +174,11 @@ func set_weapon_fov(fov: float) -> void:
|
|||||||
func clear_wall_tilt() -> void:
|
func clear_wall_tilt() -> void:
|
||||||
_target_tilt = 0.0
|
_target_tilt = 0.0
|
||||||
|
|
||||||
|
## Kick the camera pitch (degrees); decays back smoothly. Used for vaults.
|
||||||
|
func add_pitch_impulse(degrees: float) -> void:
|
||||||
|
_pitch_impulse += deg_to_rad(degrees)
|
||||||
|
|
||||||
## Adds an offset to the camera so that when the player physics body snaps up a step,
|
## Adds an offset to the camera so that when the player physics body snaps up a step,
|
||||||
## the camera interpolates smoothly instead of snapping.
|
## the camera interpolates smoothly instead of snapping.
|
||||||
func add_step_offset(offset_y: float) -> void:
|
func add_step_offset(offset_y: float) -> void:
|
||||||
_step_offset_y += offset_y
|
_step_offset_y += offset_y
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const CLIP_FALLBACKS := {
|
|||||||
"Sprint": ["Sprint", "Run", "Walk", "Idle"],
|
"Sprint": ["Sprint", "Run", "Walk", "Idle"],
|
||||||
"Jump": ["Jump", "Fall", "Idle"],
|
"Jump": ["Jump", "Fall", "Idle"],
|
||||||
"Fall": ["Fall", "Jump", "Idle"],
|
"Fall": ["Fall", "Jump", "Idle"],
|
||||||
|
"Land": ["Land", "Idle"],
|
||||||
"Crouch": ["CrouchIdle", "Crouch", "Idle"],
|
"Crouch": ["CrouchIdle", "Crouch", "Idle"],
|
||||||
"CrouchWalk": ["CrouchWalk", "Crouch", "CrouchIdle", "Walk"],
|
"CrouchWalk": ["CrouchWalk", "Crouch", "CrouchIdle", "Walk"],
|
||||||
"Slide": ["Slide", "CrouchIdle", "Crouch", "Idle"],
|
"Slide": ["Slide", "CrouchIdle", "Crouch", "Idle"],
|
||||||
@@ -65,10 +66,12 @@ var _pose_mod: ShooterPoseModifier
|
|||||||
var _target_strafe: float = 0.0
|
var _target_strafe: float = 0.0
|
||||||
var _target_fwd: float = 0.0
|
var _target_fwd: float = 0.0
|
||||||
var _target_ads: float = 0.0
|
var _target_ads: float = 0.0
|
||||||
|
var _target_wall: float = 0.0
|
||||||
var _cur_strafe: float = 0.0
|
var _cur_strafe: float = 0.0
|
||||||
var _cur_fwd: float = 0.0
|
var _cur_fwd: float = 0.0
|
||||||
var _cur_ads: float = 0.0
|
var _cur_ads: float = 0.0
|
||||||
var _cur_slide: float = 0.0
|
var _cur_slide: float = 0.0
|
||||||
|
var _cur_wall: float = 0.0
|
||||||
const POSE_SMOOTH := 10.0
|
const POSE_SMOOTH := 10.0
|
||||||
|
|
||||||
|
|
||||||
@@ -184,12 +187,30 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
|
|||||||
|
|
||||||
# ── Animation state ───────────────────────────────────────────────────────────
|
# ── Animation state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
var _prev_state: String = ""
|
||||||
|
var _land_lock: float = 0.0 # seconds left where the Land one-shot owns playback
|
||||||
|
|
||||||
|
|
||||||
## Same contract as HumanoidModel.update_state(). Called by the movement
|
## Same contract as HumanoidModel.update_state(). Called by the movement
|
||||||
## controller each frame with either local or network-synced state.
|
## controller each frame with either local or network-synced state.
|
||||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||||
if not loaded or not animation_player:
|
if not loaded or not animation_player:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# A heavy landing plays the Land one-shot before locomotion resumes.
|
||||||
|
if _land_lock > 0.0:
|
||||||
|
_land_lock -= get_process_delta_time()
|
||||||
|
if _land_lock > 0.0 and state in ["ground", "idle"]:
|
||||||
|
_prev_state = state
|
||||||
|
return
|
||||||
|
if state in ["ground", "idle"] and _prev_state == "air" \
|
||||||
|
and _vertical_speed() < -12.0 and _resolved_clips.has("Land"):
|
||||||
|
_land_lock = 0.25
|
||||||
|
_play_clip("Land")
|
||||||
|
_prev_state = state
|
||||||
|
return
|
||||||
|
_prev_state = state
|
||||||
|
|
||||||
var clip := "Idle"
|
var clip := "Idle"
|
||||||
match state:
|
match state:
|
||||||
"ground", "idle":
|
"ground", "idle":
|
||||||
@@ -202,8 +223,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
elif speed > 0.5:
|
elif speed > 0.5:
|
||||||
clip = "Walk"
|
clip = "Walk"
|
||||||
"air":
|
"air":
|
||||||
# Composed, weapon-ready airborne — not a flailing fall.
|
# Rising = jump, falling = the fall loop.
|
||||||
clip = "Jump"
|
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
|
||||||
"slide":
|
"slide":
|
||||||
clip = "Slide"
|
clip = "Slide"
|
||||||
"wall_run":
|
"wall_run":
|
||||||
@@ -243,6 +264,12 @@ func set_locomotion(strafe: float, fwd: float, ads: float) -> void:
|
|||||||
_target_ads = clampf(ads, 0.0, 1.0)
|
_target_ads = clampf(ads, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
## Wall side during a wall run: -1 wall on left, +1 wall on right, 0 none.
|
||||||
|
## Drives a whole-body lean into the wall.
|
||||||
|
func set_wall_side(side: float) -> void:
|
||||||
|
_target_wall = clampf(side, -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not _pose_mod:
|
if not _pose_mod:
|
||||||
return
|
return
|
||||||
@@ -252,10 +279,13 @@ func _process(delta: float) -> void:
|
|||||||
_cur_ads = lerpf(_cur_ads, _target_ads, t)
|
_cur_ads = lerpf(_cur_ads, _target_ads, t)
|
||||||
var slide_target := 1.0 if _pose_mod.state == "slide" else 0.0
|
var slide_target := 1.0 if _pose_mod.state == "slide" else 0.0
|
||||||
_cur_slide = lerpf(_cur_slide, slide_target, t)
|
_cur_slide = lerpf(_cur_slide, slide_target, t)
|
||||||
|
var wall_target := _target_wall if _pose_mod.state == "wall_run" else 0.0
|
||||||
|
_cur_wall = lerpf(_cur_wall, wall_target, t)
|
||||||
_pose_mod.strafe = _cur_strafe
|
_pose_mod.strafe = _cur_strafe
|
||||||
_pose_mod.fwd = _cur_fwd
|
_pose_mod.fwd = _cur_fwd
|
||||||
_pose_mod.ads = _cur_ads
|
_pose_mod.ads = _cur_ads
|
||||||
_pose_mod.slide = _cur_slide
|
_pose_mod.slide = _cur_slide
|
||||||
|
_pose_mod.wall = _cur_wall
|
||||||
|
|
||||||
|
|
||||||
func _play_clip(canonical: String) -> void:
|
func _play_clip(canonical: String) -> void:
|
||||||
@@ -268,6 +298,14 @@ func _play_clip(canonical: String) -> void:
|
|||||||
_current_clip = clip_name
|
_current_clip = clip_name
|
||||||
|
|
||||||
|
|
||||||
|
## Vertical velocity of the body this model is attached to (0 if detached).
|
||||||
|
func _vertical_speed() -> float:
|
||||||
|
var p := get_parent()
|
||||||
|
if p is CharacterBody3D:
|
||||||
|
return p.velocity.y
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
# ── Third-person weapon ───────────────────────────────────────────────────────
|
# ── Third-person weapon ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
## Attach a weapon (by weapon script path) to the right hand bone so other
|
## Attach a weapon (by weapon script path) to the right hand bone so other
|
||||||
@@ -356,6 +394,7 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var fwd: float = 0.0 # -1 back .. +1 forward
|
var fwd: float = 0.0 # -1 back .. +1 forward
|
||||||
var ads: float = 0.0 # 0 hip .. 1 aiming
|
var ads: float = 0.0 # 0 hip .. 1 aiming
|
||||||
var slide: float = 0.0 # 0 .. 1 slide blend
|
var slide: float = 0.0 # 0 .. 1 slide blend
|
||||||
|
var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean)
|
||||||
var state: String = "idle"
|
var state: String = "idle"
|
||||||
var weapon_held: bool = false
|
var weapon_held: bool = false
|
||||||
|
|
||||||
@@ -392,6 +431,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
_apply_lean(skel)
|
_apply_lean(skel)
|
||||||
if slide > 0.01:
|
if slide > 0.01:
|
||||||
_apply_slide(skel)
|
_apply_slide(skel)
|
||||||
|
if absf(wall) > 0.01:
|
||||||
|
_apply_wall_lean(skel)
|
||||||
if weapon_held:
|
if weapon_held:
|
||||||
_apply_weapon(skel)
|
_apply_weapon(skel)
|
||||||
|
|
||||||
@@ -406,6 +447,13 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
for n in SPINE:
|
for n in SPINE:
|
||||||
_add_space(skel, _idx.get(n, -1), per)
|
_add_space(skel, _idx.get(n, -1), per)
|
||||||
|
|
||||||
|
# Wall run: roll the torso into the wall (+wall = wall on the right).
|
||||||
|
func _apply_wall_lean(skel: Skeleton3D) -> void:
|
||||||
|
var roll := Quaternion(Vector3(0, 0, 1), wall * 0.35)
|
||||||
|
var per := Quaternion.IDENTITY.slerp(roll, 1.0 / SPINE.size())
|
||||||
|
for n in SPINE:
|
||||||
|
_add_space(skel, _idx.get(n, -1), per)
|
||||||
|
|
||||||
# Slide: lean the whole torso back, then pitch the head up to look forward.
|
# Slide: lean the whole torso back, then pitch the head up to look forward.
|
||||||
func _apply_slide(skel: Skeleton3D) -> void:
|
func _apply_slide(skel: Skeleton3D) -> void:
|
||||||
var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide)
|
var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide)
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_movement_speed")
|
client_rep_config.add_property(":synced_movement_speed")
|
||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
client_rep_config.add_property(":synced_grapple_point")
|
client_rep_config.add_property(":synced_grapple_point")
|
||||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||||
client_rep_config.add_property(":synced_skin_id")
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
|
|||||||
+52
-35
@@ -3,10 +3,10 @@ class_name MovementParams
|
|||||||
|
|
||||||
# ── Ground ────────────────────────────────────────────────────────────────────
|
# ── Ground ────────────────────────────────────────────────────────────────────
|
||||||
@export var walk_speed: float = 11.0
|
@export var walk_speed: float = 11.0
|
||||||
@export var crouch_speed: float = 4.0
|
@export var crouch_speed: float = 4.5
|
||||||
@export var ground_friction: float = 10.0
|
@export var ground_friction: float = 10.0
|
||||||
@export var ground_acceleration: float = 50.0
|
@export var ground_acceleration: float = 60.0
|
||||||
@export var ground_deceleration: float = 40.0
|
@export var ground_deceleration: float = 45.0
|
||||||
|
|
||||||
# ── Jump / Air ────────────────────────────────────────────────────────────────
|
# ── Jump / Air ────────────────────────────────────────────────────────────────
|
||||||
@export var jump_velocity: float = 8.5
|
@export var jump_velocity: float = 8.5
|
||||||
@@ -14,70 +14,84 @@ class_name MovementParams
|
|||||||
@export var jump_buffer: float = 0.12
|
@export var jump_buffer: float = 0.12
|
||||||
@export var air_control: float = 0.35
|
@export var air_control: float = 0.35
|
||||||
@export var air_acceleration: float = 30.0
|
@export var air_acceleration: float = 30.0
|
||||||
@export var max_air_speed: float = 16.0
|
@export var max_air_speed: float = 18.0
|
||||||
@export var gravity: float = 22.0
|
@export var gravity: float = 22.0
|
||||||
@export var fall_multiplier: float = 2.2
|
@export var fall_multiplier: float = 2.0
|
||||||
@export var low_jump_multiplier: float = 1.6
|
@export var low_jump_multiplier: float = 1.6
|
||||||
|
|
||||||
# ── Air Strafe (Quake-style) ──────────────────────────────────────────────────
|
# ── Air Strafe (Quake-style) ──────────────────────────────────────────────────
|
||||||
@export var air_strafe_accel: float = 80.0
|
# Classic accelerate: only the velocity component along wish_dir is capped, so
|
||||||
@export var air_strafe_max_gain: float = 1.0
|
# turning while strafing genuinely gains speed (up to max_air_speed overall).
|
||||||
|
@export var air_strafe_accel: float = 55.0
|
||||||
|
@export var air_wish_speed_cap: float = 1.2 # per-tick projection cap (m/s), quake-style
|
||||||
|
|
||||||
# ── Bunny Hop ─────────────────────────────────────────────────────────────────
|
# ── Bunny Hop ─────────────────────────────────────────────────────────────────
|
||||||
@export var bunny_hop_impulse: float = 1.15
|
@export var bunny_hop_impulse: float = 1.1
|
||||||
@export var bunny_hop_speed_gain: float = 0.5
|
@export var bunny_hop_speed_gain: float = 0.7
|
||||||
@export var bunny_hop_speed_cap: float = 16.0
|
@export var bunny_hop_speed_cap: float = 20.0
|
||||||
|
|
||||||
# ── Slide ─────────────────────────────────────────────────────────────────────
|
# ── Slide ─────────────────────────────────────────────────────────────────────
|
||||||
@export var slide_speed: float = 14.0
|
@export var slide_speed: float = 14.0
|
||||||
@export var slide_friction: float = 0.96
|
@export var slide_boost: float = 3.0 # flat entry boost when grounded and fast
|
||||||
@export var slide_min_speed: float = 5.0
|
@export var slide_friction_flat: float = 2.2 # m/s^2 decel on flat ground (low = long slides)
|
||||||
@export var slide_duration: float = 0.8
|
@export var slide_slope_accel: float = 16.0 # downhill acceleration from gravity projection
|
||||||
@export var slide_cooldown: float = 0.4 # Prevents rapid slide re-entry
|
@export var slide_min_speed: float = 4.0
|
||||||
@export var slide_jump_speed_boost: float = 1.5 # Extra m/s added when jumping out of slide
|
@export var slide_cooldown: float = 0.35 # prevents rapid slide re-entry
|
||||||
|
@export var slide_steer_rate: float = 2.5 # how fast slide direction bends toward input
|
||||||
|
@export var slide_jump_speed_boost: float = 1.5 # extra m/s added when jumping out of slide
|
||||||
|
|
||||||
# ── Wall Run ──────────────────────────────────────────────────────────────────
|
# ── Wall Run ──────────────────────────────────────────────────────────────────
|
||||||
@export var wall_run_speed: float = 12.0
|
@export var wall_run_speed: float = 13.0
|
||||||
@export var wall_run_vertical_speed: float = 4.0
|
@export var wall_run_accel: float = 18.0 # accelerate toward run speed (no hard set)
|
||||||
@export var wall_run_duration: float = 1.2
|
@export var wall_run_duration: float = 1.8
|
||||||
@export var wall_run_gravity: float = 6.0
|
@export var wall_run_gravity: float = 5.0
|
||||||
|
@export var wall_run_entry_max_up: float = 3.5 # keep this much upward momentum on attach
|
||||||
@export var wall_run_auto_jump_speed: float = 10.0
|
@export var wall_run_auto_jump_speed: float = 10.0
|
||||||
@export var wall_run_jump_horizontal: float = 8.0
|
@export var wall_run_jump_horizontal: float = 8.0
|
||||||
@export var wall_run_jump_off_normal: float = 5.0
|
@export var wall_run_jump_off_normal: float = 6.0
|
||||||
@export var wall_angle_threshold: float = 70.0
|
@export var wall_angle_threshold: float = 70.0
|
||||||
@export var wall_detect_distance: float = 0.7
|
@export var wall_detect_distance: float = 0.8
|
||||||
@export var wall_ray_up_height: float = 0.6
|
@export var wall_ray_up_height: float = 0.6
|
||||||
@export var wall_ray_down_height: float = 0.0
|
@export var wall_ray_down_height: float = 0.0
|
||||||
|
|
||||||
# ── Wall Climb ────────────────────────────────────────────────────────────────
|
# ── Wall Climb ────────────────────────────────────────────────────────────────
|
||||||
@export var wall_climb_speed: float = 6.0
|
@export var wall_climb_speed: float = 6.5
|
||||||
@export var wall_climb_duration: float = 3.0
|
@export var wall_climb_duration: float = 2.2
|
||||||
@export var wall_climb_max_angle: float = 75.0
|
@export var wall_climb_max_angle: float = 75.0
|
||||||
@export var wall_climb_vault_forward: float = 16.0
|
@export var wall_climb_vault_forward: float = 14.0
|
||||||
@export var wall_climb_vault_up: float = 14.0
|
@export var wall_climb_vault_up: float = 12.0
|
||||||
@export var wall_climb_vault_height_check: float = 1.0
|
@export var wall_climb_vault_height_check: float = 1.0
|
||||||
|
|
||||||
# ── Jumping ───────────────────────────────────────────────────────────────────
|
# ── Jumping ───────────────────────────────────────────────────────────────────
|
||||||
@export var jump_cooldown: float = 0.27
|
@export var jump_cooldown: float = 0.12
|
||||||
|
|
||||||
# ── Wall Cling ────────────────────────────────────────────────────────────────
|
# ── Wall Cling ────────────────────────────────────────────────────────────────
|
||||||
@export var wall_cling_slide_speed: float = -1.5
|
@export var wall_cling_slide_speed: float = -1.5
|
||||||
@export var wall_cling_stamina_drain: float = 2.0
|
@export var wall_cling_stamina_drain: float = 1.0
|
||||||
@export var wall_cling_max_stamina: float = 2.0
|
@export var wall_cling_max_stamina: float = 2.5
|
||||||
|
|
||||||
# ── Grapple ───────────────────────────────────────────────────────────────────
|
# ── Grapple ───────────────────────────────────────────────────────────────────
|
||||||
@export var grapple_range: float = 30.0
|
@export var grapple_range: float = 35.0
|
||||||
@export var grapple_pull_force: float = 25.0
|
@export var grapple_pull_force: float = 18.0 # constant reel toward the hook
|
||||||
|
@export var grapple_reel_force: float = 22.0 # extra reel while holding forward
|
||||||
@export var grapple_jump_boost: float = 12.0
|
@export var grapple_jump_boost: float = 12.0
|
||||||
@export var grapple_spring_strength: float = 10.0
|
@export var grapple_spring_strength: float = 14.0
|
||||||
@export var grapple_air_control: float = 20.0
|
@export var grapple_air_control: float = 24.0
|
||||||
|
@export var grapple_shoot_speed: float = 70.0 # hook travel speed (m/s)
|
||||||
|
|
||||||
# ── Dash ──────────────────────────────────────────────────────────────────────
|
# ── Dash ──────────────────────────────────────────────────────────────────────
|
||||||
@export var dash_speed: float = 10.0
|
@export var dash_speed: float = 13.0
|
||||||
@export var dash_duration: float = 0.25
|
@export var dash_duration: float = 0.18
|
||||||
@export var dash_cooldown: float = 10.0
|
@export var dash_cooldown: float = 2.0
|
||||||
@export var dash_invulnerability_time: float = 0.1
|
@export var dash_invulnerability_time: float = 0.1
|
||||||
|
|
||||||
|
# ── Landing feel ──────────────────────────────────────────────────────────────
|
||||||
|
@export var land_soft_speed: float = 8.0 # fall speed where landing feedback starts
|
||||||
|
@export var land_heavy_speed: float = 18.0 # fall speed considered a heavy landing
|
||||||
|
|
||||||
|
# ── Crouch ────────────────────────────────────────────────────────────────────
|
||||||
|
@export var crouch_transition_speed: float = 12.0 # capsule/camera height lerp rate
|
||||||
|
|
||||||
# ── Rocket Jump ───────────────────────────────────────────────────────────────
|
# ── Rocket Jump ───────────────────────────────────────────────────────────────
|
||||||
@export var rocket_jump_self_damage: float = 15.0
|
@export var rocket_jump_self_damage: float = 15.0
|
||||||
@export var rocket_jump_up_impulse: float = 20.0
|
@export var rocket_jump_up_impulse: float = 20.0
|
||||||
@@ -106,3 +120,6 @@ class_name MovementParams
|
|||||||
@export var head_bob_amplitude: float = 0.04
|
@export var head_bob_amplitude: float = 0.04
|
||||||
@export var wall_run_tilt_angle: float = 12.0
|
@export var wall_run_tilt_angle: float = 12.0
|
||||||
@export var wall_run_tilt_speed: float = 8.0
|
@export var wall_run_tilt_speed: float = 8.0
|
||||||
|
@export var slide_tilt_angle: float = 4.0
|
||||||
|
@export var land_dip_scale: float = 0.012 # camera dip per m/s of landing speed
|
||||||
|
@export var land_dip_max: float = 0.25 # max camera dip on heavy landings
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ class_name MovementStateMachine
|
|||||||
|
|
||||||
## Generic state machine for player movement.
|
## Generic state machine for player movement.
|
||||||
## Each state is a Node child; the machine switches between them.
|
## Each state is a Node child; the machine switches between them.
|
||||||
|
##
|
||||||
|
## The machine owns cross-state concerns so individual states stay small:
|
||||||
|
## - input snapshot (written by the controller each tick)
|
||||||
|
## - jump buffering / coyote time / jump cooldown
|
||||||
|
## - crouch capsule resizing (smoothly lerped, single owner)
|
||||||
|
## - dash cooldown (per-instance, not shared between players)
|
||||||
|
## - grapple hook raycast + travel
|
||||||
|
## - wall detection helpers
|
||||||
|
## - chain-bonus bookkeeping
|
||||||
|
|
||||||
signal state_changed(from_state: String, to_state: String)
|
signal state_changed(from_state: String, to_state: String)
|
||||||
signal movement_event(event_name: String, data: Dictionary)
|
signal movement_event(event_name: String, data: Dictionary)
|
||||||
@@ -19,6 +28,7 @@ var wish_dir_world: Vector3 = Vector3.ZERO
|
|||||||
var input_jump_pressed: bool = false
|
var input_jump_pressed: bool = false
|
||||||
var input_jump_just_pressed: bool = false
|
var input_jump_just_pressed: bool = false
|
||||||
var input_crouch: bool = false
|
var input_crouch: bool = false
|
||||||
|
var input_sprint: bool = false
|
||||||
var input_dash: bool = false
|
var input_dash: bool = false
|
||||||
var input_grapple: bool = false
|
var input_grapple: bool = false
|
||||||
var input_grapple_just_pressed: bool = false
|
var input_grapple_just_pressed: bool = false
|
||||||
@@ -38,6 +48,7 @@ var last_ground_time: float = 0.0
|
|||||||
var jump_buffer_time: float = 0.0
|
var jump_buffer_time: float = 0.0
|
||||||
var coyote_timer: float = 0.0
|
var coyote_timer: float = 0.0
|
||||||
var jump_cooldown_timer: float = 0.0
|
var jump_cooldown_timer: float = 0.0
|
||||||
|
var dash_cooldown_timer: float = 0.0
|
||||||
var current_jump_count: int = 0
|
var current_jump_count: int = 0
|
||||||
var chain_timer: float = 0.0
|
var chain_timer: float = 0.0
|
||||||
var chain_count: int = 0
|
var chain_count: int = 0
|
||||||
@@ -48,6 +59,9 @@ var wall_cooldown_timer: float = 0.0 # Prevents instant re-attachment after wa
|
|||||||
var is_crouched: bool = false
|
var is_crouched: bool = false
|
||||||
var can_wall_climb: bool = true
|
var can_wall_climb: bool = true
|
||||||
|
|
||||||
|
# Smooth capsule crouch: the machine is the single owner of capsule height.
|
||||||
|
var _capsule_current_height: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Ensure grapple state is injected
|
# Ensure grapple state is injected
|
||||||
@@ -87,9 +101,11 @@ func _physics_process(delta: float) -> void:
|
|||||||
else:
|
else:
|
||||||
jump_buffer_time = maxf(jump_buffer_time - delta, 0.0)
|
jump_buffer_time = maxf(jump_buffer_time - delta, 0.0)
|
||||||
|
|
||||||
# Update jump cooldown
|
# Update timers
|
||||||
if jump_cooldown_timer > 0.0:
|
jump_cooldown_timer = maxf(jump_cooldown_timer - delta, 0.0)
|
||||||
jump_cooldown_timer = maxf(jump_cooldown_timer - delta, 0.0)
|
dash_cooldown_timer = maxf(dash_cooldown_timer - delta, 0.0)
|
||||||
|
slide_cooldown_timer = maxf(slide_cooldown_timer - delta, 0.0)
|
||||||
|
wall_cooldown_timer = maxf(wall_cooldown_timer - delta, 0.0)
|
||||||
|
|
||||||
# Update chain timer
|
# Update chain timer
|
||||||
if chain_timer > 0.0 and on_ground:
|
if chain_timer > 0.0 and on_ground:
|
||||||
@@ -112,21 +128,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
movement_event.emit("grapple_latch", {})
|
movement_event.emit("grapple_latch", {})
|
||||||
switch_to("grapple")
|
switch_to("grapple")
|
||||||
|
|
||||||
# Manage global crouch state
|
# Manage global crouch state (slide keeps the capsule low)
|
||||||
var want_crouch = input_crouch
|
is_crouched = input_crouch or current_state == "slide"
|
||||||
if current_state == "slide":
|
_update_crouch_capsule(delta)
|
||||||
want_crouch = true
|
|
||||||
if want_crouch != is_crouched:
|
|
||||||
is_crouched = want_crouch
|
|
||||||
_apply_crouch(is_crouched)
|
|
||||||
|
|
||||||
# Update slide cooldown
|
|
||||||
if slide_cooldown_timer > 0.0:
|
|
||||||
slide_cooldown_timer = maxf(slide_cooldown_timer - delta, 0.0)
|
|
||||||
|
|
||||||
# Update wall cooldown
|
|
||||||
if wall_cooldown_timer > 0.0:
|
|
||||||
wall_cooldown_timer = maxf(wall_cooldown_timer - delta, 0.0)
|
|
||||||
|
|
||||||
if current_state.is_empty():
|
if current_state.is_empty():
|
||||||
return
|
return
|
||||||
@@ -157,6 +161,33 @@ func switch_to(new_state_name: String, data: Dictionary = {}) -> void:
|
|||||||
state_changed.emit(prev, new_state_name)
|
state_changed.emit(prev, new_state_name)
|
||||||
|
|
||||||
|
|
||||||
|
## Called by states when the player touches down. Emits the landing event so
|
||||||
|
## camera/audio/animation can react proportionally to impact speed.
|
||||||
|
func notify_landed(fall_speed: float) -> void:
|
||||||
|
on_ground = true
|
||||||
|
current_jump_count = 0
|
||||||
|
can_wall_climb = true
|
||||||
|
if fall_speed > params.land_soft_speed:
|
||||||
|
movement_event.emit("land", {
|
||||||
|
"fall_speed": fall_speed,
|
||||||
|
"heavy": fall_speed >= params.land_heavy_speed,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
## Shared jump executed from ground-like states. Keeps horizontal momentum.
|
||||||
|
func do_jump(extra_boost: float = 0.0) -> void:
|
||||||
|
player.velocity.y = params.jump_velocity + extra_boost
|
||||||
|
current_jump_count = 1
|
||||||
|
on_ground = false
|
||||||
|
coyote_timer = 0.0
|
||||||
|
jump_buffer_time = 0.0
|
||||||
|
jump_cooldown_timer = params.jump_cooldown
|
||||||
|
register_chain_mechanic("jump")
|
||||||
|
movement_event.emit("jump", {})
|
||||||
|
if player.jump_player:
|
||||||
|
player.jump_player.play()
|
||||||
|
|
||||||
|
|
||||||
func _try_start_grapple() -> void:
|
func _try_start_grapple() -> void:
|
||||||
if not player or not player.camera:
|
if not player or not player.camera:
|
||||||
return
|
return
|
||||||
@@ -170,9 +201,7 @@ func _try_start_grapple() -> void:
|
|||||||
if not hit.is_empty():
|
if not hit.is_empty():
|
||||||
grapple_point = hit.position
|
grapple_point = hit.position
|
||||||
grapple_length = origin.distance_to(grapple_point)
|
grapple_length = origin.distance_to(grapple_point)
|
||||||
|
grapple_travel_time = grapple_length / params.grapple_shoot_speed
|
||||||
# Travel at 45 m/s
|
|
||||||
grapple_travel_time = grapple_length / 45
|
|
||||||
grapple_shoot_time = 0.0
|
grapple_shoot_time = 0.0
|
||||||
is_grapple_shooting = true
|
is_grapple_shooting = true
|
||||||
movement_event.emit("grapple_shoot", {})
|
movement_event.emit("grapple_shoot", {})
|
||||||
@@ -308,8 +337,30 @@ func detect_wall_forward() -> Dictionary:
|
|||||||
return {"hit": true, "normal": normal.normalized(), "is_short": hit_high.is_empty()}
|
return {"hit": true, "normal": normal.normalized(), "is_short": hit_high.is_empty()}
|
||||||
|
|
||||||
|
|
||||||
func _apply_crouch(crouched: bool) -> void:
|
## Perform an instant vault over a short wall: forward+up impulse, camera kick
|
||||||
var shape_node = null
|
## via the rig, cooldown so we don't immediately re-detect the same wall.
|
||||||
|
func do_vault() -> void:
|
||||||
|
var look_dir := -player.global_transform.basis.z
|
||||||
|
var h_look := Vector3(look_dir.x, 0.0, look_dir.z)
|
||||||
|
if h_look.length_squared() > 0.01:
|
||||||
|
h_look = h_look.normalized()
|
||||||
|
player.velocity = h_look * params.wall_climb_vault_forward
|
||||||
|
player.velocity.y = params.wall_climb_vault_up
|
||||||
|
if player.vault_player:
|
||||||
|
player.vault_player.play()
|
||||||
|
movement_event.emit("vault", {})
|
||||||
|
var rig = player.get_node_or_null("HeadPivot")
|
||||||
|
if rig and rig.has_method("add_pitch_impulse"):
|
||||||
|
rig.add_pitch_impulse(6.0)
|
||||||
|
wall_cooldown_timer = 0.3
|
||||||
|
register_chain_mechanic("vault")
|
||||||
|
|
||||||
|
|
||||||
|
## Smoothly lerp the collision capsule toward the crouch/stand height and keep
|
||||||
|
## the capsule's bottom anchored so shrinking doesn't lift the player off the
|
||||||
|
## floor. Standing back up is blocked while there's no headroom.
|
||||||
|
func _update_crouch_capsule(delta: float) -> void:
|
||||||
|
var shape_node: CollisionShape3D = null
|
||||||
for child in player.get_children():
|
for child in player.get_children():
|
||||||
if child is CollisionShape3D and child.shape is CapsuleShape3D:
|
if child is CollisionShape3D and child.shape is CapsuleShape3D:
|
||||||
shape_node = child
|
shape_node = child
|
||||||
@@ -317,19 +368,38 @@ func _apply_crouch(crouched: bool) -> void:
|
|||||||
if not shape_node:
|
if not shape_node:
|
||||||
return
|
return
|
||||||
var shape = shape_node.shape as CapsuleShape3D
|
var shape = shape_node.shape as CapsuleShape3D
|
||||||
var head = player.get_node_or_null("HeadPivot")
|
if _capsule_current_height <= 0.0:
|
||||||
|
_capsule_current_height = shape.height
|
||||||
|
|
||||||
|
var target_height := original_capsule_height * (0.5 if is_crouched else 1.0)
|
||||||
|
|
||||||
|
# Don't stand up into a ceiling
|
||||||
|
if not is_crouched and target_height > _capsule_current_height + 0.01:
|
||||||
|
var space_state := player.get_world_3d().direct_space_state
|
||||||
|
var from := player.global_position + Vector3.UP * (_capsule_current_height * 0.5)
|
||||||
|
var to := player.global_position + Vector3.UP * (original_capsule_height * 0.55)
|
||||||
|
var ray := PhysicsRayQueryParameters3D.create(from, to)
|
||||||
|
ray.exclude = [player.get_rid()]
|
||||||
|
if not space_state.intersect_ray(ray).is_empty():
|
||||||
|
target_height = _capsule_current_height # hold until clear
|
||||||
|
|
||||||
|
var t := 1.0 - exp(-params.crouch_transition_speed * delta)
|
||||||
|
_capsule_current_height = lerpf(_capsule_current_height, target_height, t)
|
||||||
|
if absf(_capsule_current_height - target_height) < 0.005:
|
||||||
|
_capsule_current_height = target_height
|
||||||
|
|
||||||
|
shape.height = _capsule_current_height
|
||||||
|
# Keep feet planted: offset the shape down by half the height loss.
|
||||||
|
shape_node.position.y = -(original_capsule_height - _capsule_current_height) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
## 0 (standing) → 1 (fully crouched); used by the camera rig for eye height.
|
||||||
|
func get_crouch_factor() -> float:
|
||||||
|
if original_capsule_height <= 0.0 or _capsule_current_height <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return clampf((original_capsule_height - _capsule_current_height)
|
||||||
|
/ (original_capsule_height * 0.5), 0.0, 1.0)
|
||||||
|
|
||||||
if crouched:
|
|
||||||
shape.height = original_capsule_height * 0.5
|
|
||||||
shape_node.position.y = -original_capsule_height * 0.25
|
|
||||||
if head:
|
|
||||||
head.position.y = 0.7 - (original_capsule_height * 0.5)
|
|
||||||
else:
|
|
||||||
shape.height = original_capsule_height
|
|
||||||
shape_node.position.y = 0.0
|
|
||||||
if head:
|
|
||||||
head.position.y = 0.7
|
|
||||||
|
|
||||||
func get_dash_cooldown_remaining() -> float:
|
func get_dash_cooldown_remaining() -> float:
|
||||||
var now := Time.get_ticks_msec() / 1000.0
|
return dash_cooldown_timer
|
||||||
return maxf(0.0, params.dash_cooldown - (now - StateDash._last_dash_time))
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ var synced_is_crouching: bool = false
|
|||||||
var synced_position: Vector3 = Vector3.ZERO
|
var synced_position: Vector3 = Vector3.ZERO
|
||||||
var synced_velocity: Vector3 = Vector3.ZERO
|
var synced_velocity: Vector3 = Vector3.ZERO
|
||||||
var synced_is_ads: bool = false
|
var synced_is_ads: bool = false
|
||||||
|
var synced_wall_side: float = 0.0 # -1 wall left, +1 wall right (wall-run lean)
|
||||||
@export var synced_skin_id: String = ""
|
@export var synced_skin_id: String = ""
|
||||||
@export var synced_weapon_path: String = ""
|
@export var synced_weapon_path: String = ""
|
||||||
|
|
||||||
@@ -876,11 +877,14 @@ func _physics_process(_delta: float) -> void:
|
|||||||
if visual.has_method("set_locomotion"):
|
if visual.has_method("set_locomotion"):
|
||||||
var d := _local_move_dir()
|
var d := _local_move_dir()
|
||||||
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
||||||
|
if visual.has_method("set_wall_side"):
|
||||||
|
visual.set_wall_side(sm.wall_side)
|
||||||
|
|
||||||
# Publish state for remote peers
|
# Publish state for remote peers
|
||||||
synced_movement_state = sm.current_state
|
synced_movement_state = sm.current_state
|
||||||
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
||||||
synced_is_crouching = sm.input_crouch
|
synced_is_crouching = sm.input_crouch
|
||||||
|
synced_wall_side = sm.wall_side
|
||||||
synced_position = position
|
synced_position = position
|
||||||
synced_velocity = velocity
|
synced_velocity = velocity
|
||||||
|
|
||||||
@@ -914,6 +918,14 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
|||||||
grapple_shoot_player.play()
|
grapple_shoot_player.play()
|
||||||
elif ev == "grapple_latch":
|
elif ev == "grapple_latch":
|
||||||
grapple_latch_player.play()
|
grapple_latch_player.play()
|
||||||
|
elif ev == "land":
|
||||||
|
# Landing thud: reuse the footstep sample, pitched down and louder
|
||||||
|
# with impact. Ground state resets pitch/volume before each step.
|
||||||
|
if footstep_player:
|
||||||
|
var heavy: bool = data.get("heavy", false)
|
||||||
|
footstep_player.pitch_scale = 0.55 if heavy else 0.7
|
||||||
|
footstep_player.volume_db = 2.0 if heavy else -2.0
|
||||||
|
footstep_player.play()
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
# Remote players: interpolate toward the owner's synced transform and
|
# Remote players: interpolate toward the owner's synced transform and
|
||||||
@@ -946,6 +958,8 @@ func _process(delta: float) -> void:
|
|||||||
if visual.has_method("set_locomotion"):
|
if visual.has_method("set_locomotion"):
|
||||||
var d := _local_move_dir()
|
var d := _local_move_dir()
|
||||||
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
||||||
|
if visual.has_method("set_wall_side"):
|
||||||
|
visual.set_wall_side(synced_wall_side)
|
||||||
# Check for weapon changes
|
# Check for weapon changes
|
||||||
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
||||||
visual.set_weapon(synced_weapon_path)
|
visual.set_weapon(synced_weapon_path)
|
||||||
|
|||||||
@@ -28,27 +28,23 @@ func update(delta: float) -> void:
|
|||||||
grav *= params.low_jump_multiplier
|
grav *= params.low_jump_multiplier
|
||||||
vel.y -= grav * delta
|
vel.y -= grav * delta
|
||||||
|
|
||||||
# ── Air control ───────────────────────────────────────────────────────
|
# ── Air control: Quake-style accelerate ───────────────────────────────
|
||||||
|
# Only the velocity component ALONG wish_dir is capped, so turning while
|
||||||
|
# strafing genuinely gains speed. Total input-driven speed is bounded by
|
||||||
|
# max_air_speed; externally-gained speed (dash, rockets) is never clamped.
|
||||||
var wish_dir: Vector3 = machine.wish_dir_world
|
var wish_dir: Vector3 = machine.wish_dir_world
|
||||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||||
|
|
||||||
if wish_dir.length_squared() > 0.01:
|
if wish_dir.length_squared() > 0.01:
|
||||||
wish_dir = wish_dir.normalized()
|
wish_dir = wish_dir.normalized()
|
||||||
var current_speed = hvel.length()
|
var wish_speed: float = minf(machine.get_effective_speed(params.walk_speed), params.max_air_speed)
|
||||||
|
var current_along := hvel.dot(wish_dir)
|
||||||
# Add a strong force in the wish direction
|
var add_speed := wish_speed - current_along
|
||||||
var air_accel = params.air_strafe_accel * delta
|
if add_speed > 0.0:
|
||||||
var new_hvel = hvel + wish_dir * air_accel
|
var accel_speed: float = minf(params.air_strafe_accel * delta, add_speed)
|
||||||
|
# Quake's trick: cap per-tick projection so sharp turns give the gain
|
||||||
# Limit the speed so we don't gain speed purely from air control.
|
accel_speed = minf(accel_speed, params.air_wish_speed_cap)
|
||||||
# We only allow air control to change our direction (strafing)
|
hvel += wish_dir * accel_speed
|
||||||
# or to accelerate us up to our walk speed if we started slow.
|
|
||||||
var max_allowed_speed = maxf(current_speed, params.walk_speed)
|
|
||||||
|
|
||||||
if new_hvel.length() > max_allowed_speed:
|
|
||||||
new_hvel = new_hvel.normalized() * max_allowed_speed
|
|
||||||
|
|
||||||
hvel = new_hvel
|
|
||||||
else:
|
else:
|
||||||
# No input: slight air drag (very subtle)
|
# No input: slight air drag (very subtle)
|
||||||
hvel *= (1.0 - 0.5 * delta)
|
hvel *= (1.0 - 0.5 * delta)
|
||||||
@@ -61,8 +57,8 @@ func update(delta: float) -> void:
|
|||||||
|
|
||||||
# ── Landing ───────────────────────────────────────────────────────────
|
# ── Landing ───────────────────────────────────────────────────────────
|
||||||
if player.is_on_floor():
|
if player.is_on_floor():
|
||||||
machine.on_ground = true
|
var fall_speed: float = maxf(-vel.y, 0.0)
|
||||||
machine.current_jump_count = 0
|
machine.notify_landed(fall_speed)
|
||||||
|
|
||||||
# Bunny hop: if jump was buffered or pressed on landing frame
|
# Bunny hop: if jump was buffered or pressed on landing frame
|
||||||
if (machine.input_jump_pressed or machine.jump_buffer_time > 0.0) and machine.jump_cooldown_timer <= 0.0:
|
if (machine.input_jump_pressed or machine.jump_buffer_time > 0.0) and machine.jump_cooldown_timer <= 0.0:
|
||||||
@@ -105,26 +101,7 @@ func update(delta: float) -> void:
|
|||||||
var fwd_wall = machine.detect_wall_forward()
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
if fwd_wall.hit:
|
if fwd_wall.hit:
|
||||||
if fwd_wall.is_short:
|
if fwd_wall.is_short:
|
||||||
# Instant Vault
|
machine.do_vault()
|
||||||
var look_dir := -player.global_transform.basis.z
|
|
||||||
var h_look := Vector3(look_dir.x, 0.0, look_dir.z)
|
|
||||||
if h_look.length_squared() > 0.01:
|
|
||||||
h_look = h_look.normalized()
|
|
||||||
player.velocity = h_look * params.wall_climb_vault_forward
|
|
||||||
player.velocity.y = params.wall_climb_vault_up
|
|
||||||
if player.vault_player:
|
|
||||||
player.vault_player.play()
|
|
||||||
|
|
||||||
# Quick camera animation (tilt up and forward bob)
|
|
||||||
var camera = player.camera
|
|
||||||
if camera:
|
|
||||||
var tween = player.create_tween()
|
|
||||||
tween.tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x + 10.0, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", -0.2, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.chain().tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", 0.0, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
|
|
||||||
machine.wall_cooldown_timer = 0.3
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Check if moving directly into wall and looking generally towards it
|
# Check if moving directly into wall and looking generally towards it
|
||||||
@@ -150,6 +127,6 @@ func update(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# ── Dash ──────────────────────────────────────────────────────────────
|
# ── Dash ──────────────────────────────────────────────────────────────
|
||||||
if machine.input_dash:
|
if machine.input_dash and machine.dash_cooldown_timer <= 0.0:
|
||||||
machine.switch_to("dash")
|
machine.switch_to("dash")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
extends Node
|
extends Node
|
||||||
class_name StateDash
|
class_name StateDash
|
||||||
|
|
||||||
|
## Short burst dash. Cooldown lives on the machine (per-player instance) and is
|
||||||
|
## checked by the states that enter dash, so entering this state always dashes.
|
||||||
|
## Momentum is fully preserved on exit — the dash ADDS speed to your run.
|
||||||
|
|
||||||
var machine: MovementStateMachine
|
var machine: MovementStateMachine
|
||||||
var params: MovementParams:
|
var params: MovementParams:
|
||||||
get: return machine.params
|
get: return machine.params
|
||||||
|
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
var direction: Vector3 = Vector3.ZERO
|
var direction: Vector3 = Vector3.ZERO
|
||||||
var _exit_speed: float = 0.0
|
var _dash_speed: float = 0.0
|
||||||
var _last_vel: Vector3 = Vector3.ZERO
|
|
||||||
|
|
||||||
# Cooldown tracked across dash instances
|
|
||||||
static var _last_dash_time: float = -999.0
|
|
||||||
|
|
||||||
|
|
||||||
func enter(_data: Dictionary = {}) -> void:
|
func enter(_data: Dictionary = {}) -> void:
|
||||||
# Check cooldown
|
|
||||||
var now := Time.get_ticks_msec() / 1000.0
|
|
||||||
if now - _last_dash_time < params.dash_cooldown:
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
_last_dash_time = now
|
machine.dash_cooldown_timer = params.dash_cooldown
|
||||||
|
|
||||||
var player := machine.player
|
var player := machine.player
|
||||||
if player.dash_player:
|
if player.dash_player:
|
||||||
@@ -38,6 +32,7 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
direction = direction.normalized()
|
direction = direction.normalized()
|
||||||
|
|
||||||
machine.register_chain_mechanic("dash")
|
machine.register_chain_mechanic("dash")
|
||||||
|
machine.movement_event.emit("dash", {})
|
||||||
|
|
||||||
var current_hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
var current_hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
||||||
var current_speed := current_hvel.length()
|
var current_speed := current_hvel.length()
|
||||||
@@ -47,46 +42,39 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
|
|
||||||
if proj >= 0.0:
|
if proj >= 0.0:
|
||||||
# Dashing forward or diagonally forward: add base dash speed to the projected speed
|
# Dashing forward or diagonally forward: add base dash speed to the projected speed
|
||||||
_exit_speed = proj + base_dash_speed
|
_dash_speed = proj + base_dash_speed
|
||||||
else:
|
else:
|
||||||
# Dashing backward or diagonally backward:
|
# Dashing backward or diagonally backward:
|
||||||
# Reflect current momentum into the new direction, smoothly scaling from base_dash_speed (at sideways) to current_speed (at exactly backward).
|
# Reflect current momentum into the new direction, smoothly scaling from
|
||||||
|
# base_dash_speed (at sideways) to current_speed (at exactly backward).
|
||||||
var backward_factor = -proj / current_speed if current_speed > 0.001 else 0.0
|
var backward_factor = -proj / current_speed if current_speed > 0.001 else 0.0
|
||||||
_exit_speed = lerpf(base_dash_speed, maxf(current_speed, base_dash_speed), backward_factor)
|
_dash_speed = lerpf(base_dash_speed, maxf(current_speed, base_dash_speed), backward_factor)
|
||||||
|
|
||||||
machine.player.velocity = direction * _exit_speed
|
machine.player.velocity = direction * _dash_speed
|
||||||
machine.on_ground = false
|
machine.on_ground = false
|
||||||
_last_vel = machine.player.velocity
|
|
||||||
|
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
# Preserve dash velocity on exit (don't cut speed abruptly)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
func update(delta: float) -> void:
|
func update(delta: float) -> void:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
|
var player := machine.player
|
||||||
|
|
||||||
if elapsed > params.dash_duration:
|
if elapsed > params.dash_duration:
|
||||||
machine.player.velocity = direction * _exit_speed * 0.85
|
# Keep the full dash velocity — the dash is a speed investment
|
||||||
if machine.player.is_on_floor():
|
player.velocity = direction * _dash_speed
|
||||||
|
if player.is_on_floor():
|
||||||
machine.on_ground = true
|
machine.on_ground = true
|
||||||
machine.switch_to("ground")
|
machine.switch_to("ground")
|
||||||
else:
|
else:
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Maintain dash velocity
|
# Maintain dash velocity, flat trajectory
|
||||||
var player := machine.player
|
var vel := direction * _dash_speed
|
||||||
var vel: Vector3 = player.velocity
|
|
||||||
|
|
||||||
if vel.distance_squared_to(_last_vel) > 100.0:
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
vel = direction * _exit_speed
|
|
||||||
if player.is_on_floor():
|
if player.is_on_floor():
|
||||||
vel.y = -0.5 # Snap to floor to handle ramps
|
vel.y = -0.5 # Snap to floor to handle ramps
|
||||||
|
|
||||||
player.velocity = vel
|
player.velocity = vel
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
_last_vel = player.velocity
|
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
extends Node
|
extends Node
|
||||||
class_name StateGrapple
|
class_name StateGrapple
|
||||||
|
|
||||||
|
## Swing grapple with active rope control:
|
||||||
|
## - the rope acts as a hard pendulum constraint at its current length
|
||||||
|
## - holding forward reels in (rope shortens, converts to speed)
|
||||||
|
## - holding back pays rope out (up to the original latch length)
|
||||||
|
## - strafe input steers tangentially around the swing sphere
|
||||||
|
## - jump detaches with a boost along your current motion
|
||||||
|
|
||||||
var machine: MovementStateMachine
|
var machine: MovementStateMachine
|
||||||
var params: MovementParams:
|
var params: MovementParams:
|
||||||
get: return machine.params
|
get: return machine.params
|
||||||
|
|
||||||
|
var _rope_length: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func enter(_data: Dictionary = {}) -> void:
|
func enter(_data: Dictionary = {}) -> void:
|
||||||
machine.on_ground = false
|
machine.on_ground = false
|
||||||
machine.wall_normal = Vector3.ZERO
|
machine.wall_normal = Vector3.ZERO
|
||||||
machine.wall_side = 0.0
|
machine.wall_side = 0.0
|
||||||
machine.register_chain_mechanic("grapple")
|
machine.register_chain_mechanic("grapple")
|
||||||
|
_rope_length = machine.grapple_length
|
||||||
|
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
func update(delta: float) -> void:
|
func update(delta: float) -> void:
|
||||||
var player := machine.player
|
var player := machine.player
|
||||||
var vel: Vector3 = player.velocity
|
var vel: Vector3 = player.velocity
|
||||||
@@ -24,16 +37,26 @@ func update(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
var grapple_pos: Vector3 = machine.grapple_point
|
var grapple_pos: Vector3 = machine.grapple_point
|
||||||
var current_dist: float = player.global_position.distance_to(grapple_pos)
|
var to_point: Vector3 = grapple_pos - player.global_position
|
||||||
var dir_to_point: Vector3 = (grapple_pos - player.global_position).normalized()
|
var current_dist: float = to_point.length()
|
||||||
|
if current_dist < 0.01:
|
||||||
|
machine.switch_to("air")
|
||||||
|
return
|
||||||
|
var dir_to_point: Vector3 = to_point / current_dist
|
||||||
|
|
||||||
# Jump to detach and get a boost
|
# Jump to detach and get a boost
|
||||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||||
vel.y = params.jump_velocity
|
vel.y = maxf(vel.y, params.jump_velocity)
|
||||||
# Add a directional boost if jumping while swinging
|
# Boost along current motion so the release keeps the swing's energy
|
||||||
|
var h_vel := Vector3(vel.x, 0.0, vel.z)
|
||||||
|
if h_vel.length_squared() > 0.1:
|
||||||
|
var boost := h_vel.normalized() * params.grapple_jump_boost * 0.5
|
||||||
|
vel.x += boost.x
|
||||||
|
vel.z += boost.z
|
||||||
|
# Plus a steer boost toward held direction
|
||||||
var h_look = machine.wish_dir_world
|
var h_look = machine.wish_dir_world
|
||||||
if h_look.length_squared() > 0.1:
|
if h_look.length_squared() > 0.1:
|
||||||
var h_boost = h_look.normalized() * params.grapple_jump_boost
|
var h_boost = h_look.normalized() * params.grapple_jump_boost * 0.5
|
||||||
vel.x += h_boost.x
|
vel.x += h_boost.x
|
||||||
vel.z += h_boost.z
|
vel.z += h_boost.z
|
||||||
|
|
||||||
@@ -48,39 +71,40 @@ func update(delta: float) -> void:
|
|||||||
# Apply gravity
|
# Apply gravity
|
||||||
vel.y -= params.gravity * delta
|
vel.y -= params.gravity * delta
|
||||||
|
|
||||||
# Apply slight inward pull (reeling in)
|
# Base reel: constant gentle pull toward the hook
|
||||||
vel += dir_to_point * params.grapple_pull_force * delta
|
vel += dir_to_point * params.grapple_pull_force * delta
|
||||||
|
|
||||||
# Pendulum / Rope physics
|
# ── Active rope control ───────────────────────────────────────────────
|
||||||
# If the player tries to go further than the original rope length, pull them back aggressively
|
var fwd_input := -machine.input_dir.y # +1 holding forward, -1 back
|
||||||
var max_len: float = machine.grapple_length
|
if fwd_input > 0.1:
|
||||||
if current_dist > max_len:
|
# Reel in: shorten rope and pull hard — converts to swing speed
|
||||||
# Project velocity onto the tangent of the sphere (prevent moving further away)
|
vel += dir_to_point * params.grapple_reel_force * fwd_input * delta
|
||||||
|
_rope_length = maxf(_rope_length - 8.0 * fwd_input * delta, 2.0)
|
||||||
|
elif fwd_input < -0.1:
|
||||||
|
# Pay out rope back toward the original latch length
|
||||||
|
_rope_length = minf(_rope_length + 8.0 * -fwd_input * delta, machine.grapple_length)
|
||||||
|
|
||||||
|
# Keep the working length taut to where we actually are (swinging inside
|
||||||
|
# the sphere shortens the constraint, giving crisp Tarzan arcs)
|
||||||
|
_rope_length = minf(_rope_length, maxf(current_dist, 2.0))
|
||||||
|
|
||||||
|
# ── Pendulum constraint at the current rope length ────────────────────
|
||||||
|
if current_dist > _rope_length:
|
||||||
|
# Kill outward radial velocity (the rope is taut and inextensible)
|
||||||
var radial_vel = vel.project(dir_to_point)
|
var radial_vel = vel.project(dir_to_point)
|
||||||
# If radial_vel is pointing AWAY from the grapple point (dot < 0), kill it
|
|
||||||
if radial_vel.dot(dir_to_point) < 0:
|
if radial_vel.dot(dir_to_point) < 0:
|
||||||
vel -= radial_vel
|
vel -= radial_vel
|
||||||
|
# Spring correction toward the sphere surface
|
||||||
|
var diff = current_dist - _rope_length
|
||||||
|
vel += dir_to_point * diff * params.grapple_spring_strength * delta
|
||||||
|
|
||||||
# Add a spring force to pull them back to the sphere
|
# ── Tangential steering (strafe around the swing sphere) ──────────────
|
||||||
var diff = current_dist - max_len
|
var strafe_input := machine.input_dir.x
|
||||||
var spring_force = dir_to_point * diff * params.grapple_spring_strength
|
if absf(strafe_input) > 0.1:
|
||||||
vel += spring_force * delta
|
var right := player.global_transform.basis.x
|
||||||
|
var tangent_steer := right - right.project(dir_to_point)
|
||||||
# Air control while swinging
|
if tangent_steer.length_squared() > 0.01:
|
||||||
var wish_dir: Vector3 = machine.wish_dir_world
|
vel += tangent_steer.normalized() * params.grapple_air_control * strafe_input * delta
|
||||||
if wish_dir.length_squared() > 0.01:
|
|
||||||
wish_dir = wish_dir.normalized()
|
|
||||||
# Add force tangentially to the rope
|
|
||||||
# We want to steer, but not pull away from or directly into the rope.
|
|
||||||
var tangent_wish = wish_dir - wish_dir.project(dir_to_point)
|
|
||||||
if tangent_wish.length_squared() > 0.01:
|
|
||||||
vel += tangent_wish.normalized() * params.grapple_air_control * delta
|
|
||||||
|
|
||||||
player.velocity = vel
|
player.velocity = vel
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
|
|
||||||
# Remove ground detach so players can grapple along the floor
|
|
||||||
# if player.is_on_floor():
|
|
||||||
# machine.on_ground = true
|
|
||||||
# machine.switch_to("ground")
|
|
||||||
# return
|
|
||||||
|
|||||||
+73
-134
@@ -7,6 +7,7 @@ var params: MovementParams:
|
|||||||
|
|
||||||
var _footstep_timer: float = 0.0
|
var _footstep_timer: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func enter(_data: Dictionary = {}) -> void:
|
func enter(_data: Dictionary = {}) -> void:
|
||||||
machine.on_ground = true
|
machine.on_ground = true
|
||||||
machine.current_jump_count = 0
|
machine.current_jump_count = 0
|
||||||
@@ -16,9 +17,6 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
if rig:
|
if rig:
|
||||||
rig.clear_wall_tilt()
|
rig.clear_wall_tilt()
|
||||||
|
|
||||||
# Ensure capsule is correct when entering ground state
|
|
||||||
_update_capsule_height()
|
|
||||||
|
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
pass
|
pass
|
||||||
@@ -28,36 +26,19 @@ func update(delta: float) -> void:
|
|||||||
var player := machine.player
|
var player := machine.player
|
||||||
var vel: Vector3 = player.velocity
|
var vel: Vector3 = player.velocity
|
||||||
|
|
||||||
# ── Jump (coyote time + jump buffer) ──────────────────────────────────
|
# ── Jump (direct press w/ coyote, or buffered from before landing) ────
|
||||||
if machine.input_jump_just_pressed and machine.coyote_timer > 0.0 and machine.jump_cooldown_timer <= 0.0:
|
if machine.jump_cooldown_timer <= 0.0:
|
||||||
player.velocity.y = params.jump_velocity
|
var pressed := machine.input_jump_just_pressed and machine.coyote_timer > 0.0
|
||||||
machine.current_jump_count = 1
|
var buffered := machine.on_ground and machine.jump_buffer_time > 0.0
|
||||||
machine.on_ground = false
|
if pressed or buffered:
|
||||||
machine.coyote_timer = 0.0
|
machine.do_jump()
|
||||||
machine.register_chain_mechanic("jump")
|
machine.switch_to("air")
|
||||||
machine.jump_cooldown_timer = params.jump_cooldown
|
return
|
||||||
if player.jump_player:
|
|
||||||
player.jump_player.play()
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Jump buffer: player pressed jump just before landing
|
|
||||||
if machine.on_ground and machine.jump_buffer_time > 0.0 and machine.jump_cooldown_timer <= 0.0:
|
|
||||||
player.velocity.y = params.jump_velocity
|
|
||||||
machine.current_jump_count = 1
|
|
||||||
machine.on_ground = false
|
|
||||||
machine.jump_buffer_time = 0.0
|
|
||||||
machine.register_chain_mechanic("jump")
|
|
||||||
machine.jump_cooldown_timer = params.jump_cooldown
|
|
||||||
if player.jump_player:
|
|
||||||
player.jump_player.play()
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
||||||
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
||||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||||
if hspeed > params.slide_min_speed:
|
if hspeed > params.slide_min_speed + 1.0:
|
||||||
machine.switch_to("slide")
|
machine.switch_to("slide")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -80,7 +61,7 @@ func update(delta: float) -> void:
|
|||||||
vel.x = hvel.x
|
vel.x = hvel.x
|
||||||
vel.z = hvel.z
|
vel.z = hvel.z
|
||||||
|
|
||||||
# ── Gravity (correct direction: downward) ─────────────────────────────
|
# ── Gravity / floor snap ──────────────────────────────────────────────
|
||||||
if not player.is_on_floor():
|
if not player.is_on_floor():
|
||||||
vel.y -= params.gravity * delta
|
vel.y -= params.gravity * delta
|
||||||
else:
|
else:
|
||||||
@@ -94,56 +75,17 @@ func update(delta: float) -> void:
|
|||||||
|
|
||||||
# ── Stair Stepping ────────────────────────────────────────────────────
|
# ── Stair Stepping ────────────────────────────────────────────────────
|
||||||
if wish_dir.length_squared() > 0.01 and not machine.input_crouch:
|
if wish_dir.length_squared() > 0.01 and not machine.input_crouch:
|
||||||
for i in range(player.get_slide_collision_count()):
|
_try_step_up(player, wish_dir, hvel)
|
||||||
var col = player.get_slide_collision(i)
|
|
||||||
if abs(col.get_normal().y) < 0.3: # Hit a wall
|
|
||||||
var max_step_height = 0.95
|
|
||||||
var space_state = player.get_world_3d().direct_space_state
|
|
||||||
var feet_y = player.global_position.y - (machine.original_capsule_height / 2.0)
|
|
||||||
|
|
||||||
# Raycast down from above the step
|
|
||||||
var fwd = wish_dir.normalized()
|
|
||||||
var ray_start = player.global_position + fwd * 0.65
|
|
||||||
ray_start.y = feet_y + max_step_height + 0.1
|
|
||||||
var ray_end = ray_start - Vector3.UP * (max_step_height + 0.2)
|
|
||||||
|
|
||||||
var ray = PhysicsRayQueryParameters3D.create(ray_start, ray_end)
|
|
||||||
ray.exclude = [player.get_rid()]
|
|
||||||
var hit = space_state.intersect_ray(ray)
|
|
||||||
|
|
||||||
if not hit.is_empty() and hit.normal.y > 0.7:
|
|
||||||
var step_height = hit.position.y - feet_y
|
|
||||||
if step_height > 0.01 and step_height <= max_step_height:
|
|
||||||
# Check if we have headroom to move up
|
|
||||||
var head_ray_start = player.global_position
|
|
||||||
head_ray_start.y += (machine.original_capsule_height / 2.0)
|
|
||||||
var head_ray_end = head_ray_start + Vector3.UP * (step_height + 0.1)
|
|
||||||
var head_ray = PhysicsRayQueryParameters3D.create(head_ray_start, head_ray_end)
|
|
||||||
head_ray.exclude = [player.get_rid()]
|
|
||||||
|
|
||||||
if space_state.intersect_ray(head_ray).is_empty():
|
|
||||||
# Check if there is enough space to move forward after stepping up
|
|
||||||
var test_transform = player.global_transform
|
|
||||||
test_transform.origin.y += step_height + 0.05
|
|
||||||
if not player.test_move(test_transform, fwd * 0.15):
|
|
||||||
# We can safely step up
|
|
||||||
player.global_position.y += step_height + 0.01
|
|
||||||
if player.head_pivot and player.head_pivot.has_method("add_step_offset"):
|
|
||||||
player.head_pivot.add_step_offset(-(step_height + 0.01))
|
|
||||||
# Push slightly forward to get onto the step
|
|
||||||
player.global_position += fwd * 0.1
|
|
||||||
# Restore horizontal velocity that was lost from hitting the wall
|
|
||||||
player.velocity.x = hvel.x
|
|
||||||
player.velocity.z = hvel.z
|
|
||||||
break
|
|
||||||
|
|
||||||
_update_capsule_height()
|
|
||||||
|
|
||||||
var current_hspeed = Vector2(player.velocity.x, player.velocity.z).length()
|
var current_hspeed = Vector2(player.velocity.x, player.velocity.z).length()
|
||||||
if current_hspeed > 1.0:
|
if current_hspeed > 1.0 and player.is_on_floor():
|
||||||
_footstep_timer -= delta
|
_footstep_timer -= delta
|
||||||
if _footstep_timer <= 0.0:
|
if _footstep_timer <= 0.0:
|
||||||
if player.footstep_player:
|
if player.footstep_player:
|
||||||
|
# Reset pitch/volume (the landing thud reuses this player) and
|
||||||
|
# add slight variation so steps don't machine-gun.
|
||||||
|
player.footstep_player.pitch_scale = randf_range(0.92, 1.08)
|
||||||
|
player.footstep_player.volume_db = -6.0
|
||||||
player.footstep_player.play()
|
player.footstep_player.play()
|
||||||
_footstep_timer = max(0.2, 3.0 / current_hspeed)
|
_footstep_timer = max(0.2, 3.0 / current_hspeed)
|
||||||
else:
|
else:
|
||||||
@@ -162,82 +104,79 @@ func update(delta: float) -> void:
|
|||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Wall interaction (Run, Climb, Vault) ──────────────────────────────
|
# ── Wall interaction (Vault, Climb) ───────────────────────────────────
|
||||||
if machine.input_dir.length() > 0.1 and machine.input_dir.y <= 0.0 and machine.wall_cooldown_timer <= 0.0 and not machine.input_crouch:
|
if machine.input_dir.length() > 0.1 and machine.input_dir.y <= 0.0 \
|
||||||
|
and machine.wall_cooldown_timer <= 0.0 and not machine.input_crouch:
|
||||||
var fwd_wall = machine.detect_wall_forward()
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
if fwd_wall.hit:
|
if fwd_wall.hit:
|
||||||
if fwd_wall.is_short:
|
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||||
# Instant Vault
|
if fwd_wall.is_short and hspeed > 3.0:
|
||||||
var look_dir := -player.global_transform.basis.z
|
machine.do_vault()
|
||||||
var h_look := Vector3(look_dir.x, 0.0, look_dir.z)
|
|
||||||
if h_look.length_squared() > 0.01:
|
|
||||||
h_look = h_look.normalized()
|
|
||||||
player.velocity = h_look * params.wall_climb_vault_forward
|
|
||||||
player.velocity.y = params.wall_climb_vault_up
|
|
||||||
if player.vault_player:
|
|
||||||
player.vault_player.play()
|
|
||||||
|
|
||||||
# Quick camera animation (tilt up and forward bob)
|
|
||||||
var camera = player.camera
|
|
||||||
if camera:
|
|
||||||
var tween = player.create_tween()
|
|
||||||
tween.tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x + 10.0, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", -0.2, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.chain().tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", 0.0, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
|
|
||||||
machine.wall_cooldown_timer = 0.3
|
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
else:
|
elif not fwd_wall.is_short:
|
||||||
# Check if moving directly into wall and looking generally towards it
|
# Check if moving directly into wall and looking generally towards it
|
||||||
var look_dir := -player.global_transform.basis.z
|
var look_dir := -player.global_transform.basis.z
|
||||||
var looking_towards := look_dir.dot(-fwd_wall.normal) > 0.0
|
var looking_towards := look_dir.dot(-fwd_wall.normal) > 0.0
|
||||||
var moving_towards := wish_dir.dot(-fwd_wall.normal) > cos(deg_to_rad(params.wall_climb_max_angle))
|
var moving_towards := machine.wish_dir_world.dot(-fwd_wall.normal) > cos(deg_to_rad(params.wall_climb_max_angle))
|
||||||
|
|
||||||
if looking_towards and moving_towards and machine.can_wall_climb:
|
if looking_towards and moving_towards and machine.can_wall_climb:
|
||||||
machine.wall_normal = fwd_wall.normal
|
machine.wall_normal = fwd_wall.normal
|
||||||
machine.switch_to("wall_climb")
|
machine.switch_to("wall_climb")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fallback to wall run
|
|
||||||
var wall_n := machine.detect_wall_horizontal()
|
|
||||||
if wall_n != Vector3.ZERO:
|
|
||||||
if not player.is_on_floor():
|
|
||||||
var w_look_dir := -player.global_transform.basis.z
|
|
||||||
var h_look := Vector3(w_look_dir.x, 0.0, w_look_dir.z).normalized()
|
|
||||||
|
|
||||||
# Require looking mostly along the wall (angle > 41 degrees from normal)
|
|
||||||
# and inputting mostly along the wall (angle > 31 degrees from normal, allows W+A/D diagonally into wall)
|
|
||||||
if abs(h_look.dot(wall_n)) < 0.75 and abs(wish_dir.dot(wall_n)) < 0.85:
|
|
||||||
machine.switch_to("wall_run")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ── Dash ──────────────────────────────────────────────────────────────
|
# ── Dash ──────────────────────────────────────────────────────────────
|
||||||
if machine.input_dash:
|
if machine.input_dash and machine.dash_cooldown_timer <= 0.0:
|
||||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
machine.switch_to("dash")
|
||||||
if hspeed > 0.1 or machine.input_dir.length() > 0.1:
|
return
|
||||||
machine.switch_to("dash")
|
|
||||||
return
|
|
||||||
|
func _try_step_up(player: CharacterBody3D, wish_dir: Vector3, hvel: Vector3) -> void:
|
||||||
|
for i in range(player.get_slide_collision_count()):
|
||||||
|
var col = player.get_slide_collision(i)
|
||||||
|
if abs(col.get_normal().y) < 0.3: # Hit a wall
|
||||||
|
var max_step_height = 0.95
|
||||||
|
var space_state = player.get_world_3d().direct_space_state
|
||||||
|
var feet_y = player.global_position.y - (machine.original_capsule_height / 2.0)
|
||||||
|
|
||||||
|
# Raycast down from above the step
|
||||||
|
var fwd = wish_dir.normalized()
|
||||||
|
var ray_start = player.global_position + fwd * 0.65
|
||||||
|
ray_start.y = feet_y + max_step_height + 0.1
|
||||||
|
var ray_end = ray_start - Vector3.UP * (max_step_height + 0.2)
|
||||||
|
|
||||||
|
var ray = PhysicsRayQueryParameters3D.create(ray_start, ray_end)
|
||||||
|
ray.exclude = [player.get_rid()]
|
||||||
|
var hit = space_state.intersect_ray(ray)
|
||||||
|
|
||||||
|
if not hit.is_empty() and hit.normal.y > 0.7:
|
||||||
|
var step_height = hit.position.y - feet_y
|
||||||
|
if step_height > 0.01 and step_height <= max_step_height:
|
||||||
|
# Check if we have headroom to move up
|
||||||
|
var head_ray_start = player.global_position
|
||||||
|
head_ray_start.y += (machine.original_capsule_height / 2.0)
|
||||||
|
var head_ray_end = head_ray_start + Vector3.UP * (step_height + 0.1)
|
||||||
|
var head_ray = PhysicsRayQueryParameters3D.create(head_ray_start, head_ray_end)
|
||||||
|
head_ray.exclude = [player.get_rid()]
|
||||||
|
|
||||||
|
if space_state.intersect_ray(head_ray).is_empty():
|
||||||
|
# Check if there is enough space to move forward after stepping up
|
||||||
|
var test_transform = player.global_transform
|
||||||
|
test_transform.origin.y += step_height + 0.05
|
||||||
|
if not player.test_move(test_transform, fwd * 0.15):
|
||||||
|
# We can safely step up
|
||||||
|
player.global_position.y += step_height + 0.01
|
||||||
|
if player.head_pivot and player.head_pivot.has_method("add_step_offset"):
|
||||||
|
player.head_pivot.add_step_offset(-(step_height + 0.01))
|
||||||
|
# Push slightly forward to get onto the step
|
||||||
|
player.global_position += fwd * 0.1
|
||||||
|
# Restore horizontal velocity that was lost from hitting the wall
|
||||||
|
player.velocity.x = hvel.x
|
||||||
|
player.velocity.z = hvel.z
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
func _get_camera_rig():
|
func _get_camera_rig():
|
||||||
if machine.player and machine.player.has_node("HeadPivot"):
|
if machine.player and machine.player.has_node("HeadPivot"):
|
||||||
return machine.player.get_node("HeadPivot")
|
return machine.player.get_node("HeadPivot")
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
func _update_capsule_height() -> void:
|
|
||||||
var shape = _get_capsule()
|
|
||||||
if shape:
|
|
||||||
if machine.input_crouch:
|
|
||||||
shape.height = machine.original_capsule_height * 0.5
|
|
||||||
else:
|
|
||||||
shape.height = machine.original_capsule_height
|
|
||||||
|
|
||||||
|
|
||||||
func _get_capsule() -> CapsuleShape3D:
|
|
||||||
for child in machine.player.get_children():
|
|
||||||
if child is CollisionShape3D and child.shape is CapsuleShape3D:
|
|
||||||
return child.shape as CapsuleShape3D
|
|
||||||
return null
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
extends Node
|
extends Node
|
||||||
class_name StateSlide
|
class_name StateSlide
|
||||||
|
|
||||||
|
## Momentum slide. Friction is a flat deceleration on level ground but gravity
|
||||||
|
## projected along the floor accelerates you downhill, so slopes are the fast
|
||||||
|
## route. The capsule stays low via the machine's crouch handling (slide counts
|
||||||
|
## as crouched); no capsule fiddling here.
|
||||||
|
|
||||||
var machine: MovementStateMachine
|
var machine: MovementStateMachine
|
||||||
var params: MovementParams:
|
var params: MovementParams:
|
||||||
get: return machine.params
|
get: return machine.params
|
||||||
|
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
var _saved_capsule_height: float = 0.0
|
|
||||||
var _slide_direction: Vector3 = Vector3.ZERO
|
var _slide_direction: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
@@ -17,12 +21,6 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
if machine.player.slide_player and not machine.player.slide_player.playing:
|
if machine.player.slide_player and not machine.player.slide_player.playing:
|
||||||
machine.player.slide_player.play()
|
machine.player.slide_player.play()
|
||||||
|
|
||||||
# Save original capsule height and halve it
|
|
||||||
var shape: CapsuleShape3D = _get_capsule()
|
|
||||||
if shape:
|
|
||||||
_saved_capsule_height = shape.height
|
|
||||||
shape.height = _saved_capsule_height * 0.5
|
|
||||||
|
|
||||||
# Slide in the direction of current velocity (momentum-based)
|
# Slide in the direction of current velocity (momentum-based)
|
||||||
var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
||||||
if hvel.length_squared() > 0.01:
|
if hvel.length_squared() > 0.01:
|
||||||
@@ -33,23 +31,21 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
_slide_direction.y = 0.0
|
_slide_direction.y = 0.0
|
||||||
_slide_direction = _slide_direction.normalized()
|
_slide_direction = _slide_direction.normalized()
|
||||||
|
|
||||||
# Set initial slide velocity
|
# Entry speed: keep momentum, add a flat boost when actually moving fast on
|
||||||
var slide_speed := maxf(hvel.length(), params.slide_speed)
|
# the ground (rewards slide-cancelling sprints without making crouch a brake)
|
||||||
machine.player.velocity.x = _slide_direction.x * machine.get_effective_speed(slide_speed)
|
var entry_speed := hvel.length()
|
||||||
machine.player.velocity.z = _slide_direction.z * machine.get_effective_speed(slide_speed)
|
if machine.player.is_on_floor() and entry_speed > params.walk_speed * 0.8:
|
||||||
|
entry_speed += params.slide_boost
|
||||||
|
entry_speed = maxf(entry_speed, params.slide_speed * 0.75)
|
||||||
|
machine.player.velocity.x = _slide_direction.x * machine.get_effective_speed(entry_speed)
|
||||||
|
machine.player.velocity.z = _slide_direction.z * machine.get_effective_speed(entry_speed)
|
||||||
|
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
machine.slide_cooldown_timer = params.slide_cooldown
|
machine.slide_cooldown_timer = params.slide_cooldown
|
||||||
|
|
||||||
if machine.player.slide_player:
|
if machine.player.slide_player:
|
||||||
machine.player.slide_player.stop()
|
machine.player.slide_player.stop()
|
||||||
|
|
||||||
# Restore original capsule height
|
|
||||||
var shape: CapsuleShape3D = _get_capsule()
|
|
||||||
if shape and _saved_capsule_height > 0.0:
|
|
||||||
shape.height = _saved_capsule_height
|
|
||||||
|
|
||||||
|
|
||||||
func update(delta: float) -> void:
|
func update(delta: float) -> void:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
@@ -59,28 +55,46 @@ func update(delta: float) -> void:
|
|||||||
player.slide_player.play()
|
player.slide_player.play()
|
||||||
|
|
||||||
var vel: Vector3 = player.velocity
|
var vel: Vector3 = player.velocity
|
||||||
|
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||||
|
var hspeed := hvel.length()
|
||||||
|
|
||||||
# ── Apply friction to horizontal velocity ─────────────────────────────
|
# ── Slope physics: gravity projected along the floor plane ────────────
|
||||||
vel.x *= pow(params.slide_friction, delta * 10.0)
|
var on_slope := false
|
||||||
vel.z *= pow(params.slide_friction, delta * 10.0)
|
if player.is_on_floor():
|
||||||
|
var floor_n := player.get_floor_normal()
|
||||||
|
if floor_n.y < 0.999:
|
||||||
|
# Downhill direction on this slope
|
||||||
|
var downhill := (Vector3.DOWN - floor_n * Vector3.DOWN.dot(floor_n))
|
||||||
|
if downhill.length_squared() > 0.0001:
|
||||||
|
downhill = downhill.normalized()
|
||||||
|
var steepness := 1.0 - floor_n.y # 0 flat .. ~0.3 steep
|
||||||
|
var slope_pull := downhill * params.slide_slope_accel * steepness * 8.0
|
||||||
|
hvel += Vector3(slope_pull.x, 0.0, slope_pull.z) * delta
|
||||||
|
on_slope = downhill.dot(_slide_direction) > 0.1
|
||||||
|
|
||||||
# ── Allow slight steering ─────────────────────────────────────────────
|
# ── Friction: flat decel on level ground, nearly free downhill ────────
|
||||||
|
if hspeed > 0.01:
|
||||||
|
var friction := params.slide_friction_flat
|
||||||
|
if on_slope:
|
||||||
|
friction *= 0.15
|
||||||
|
var new_speed := maxf(hvel.length() - friction * delta, 0.0)
|
||||||
|
if hvel.length() > 0.001:
|
||||||
|
hvel = hvel.normalized() * new_speed
|
||||||
|
|
||||||
|
# ── Steering: bend velocity toward input without changing speed ───────
|
||||||
var wish_dir := machine.wish_dir_world
|
var wish_dir := machine.wish_dir_world
|
||||||
if wish_dir.length_squared() > 0.01:
|
if wish_dir.length_squared() > 0.01 and hvel.length_squared() > 0.01:
|
||||||
wish_dir = wish_dir.normalized()
|
wish_dir = wish_dir.normalized()
|
||||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
var speed_now := hvel.length()
|
||||||
var current_speed := hvel.length()
|
var steered := hvel.lerp(wish_dir * speed_now, params.slide_steer_rate * delta)
|
||||||
|
if steered.length_squared() > 0.01:
|
||||||
|
hvel = steered.normalized() * speed_now
|
||||||
|
_slide_direction = hvel.normalized()
|
||||||
|
|
||||||
# Slowly bend velocity towards wish_dir
|
vel.x = hvel.x
|
||||||
var steer_speed = 3.0 * delta
|
vel.z = hvel.z
|
||||||
var new_hvel = hvel.lerp(wish_dir * current_speed, steer_speed)
|
|
||||||
vel.x = new_hvel.x
|
|
||||||
vel.z = new_hvel.z
|
|
||||||
|
|
||||||
if new_hvel.length_squared() > 0.01:
|
# ── Gravity (for slopes / edges) ──────────────────────────────────────
|
||||||
_slide_direction = new_hvel.normalized()
|
|
||||||
|
|
||||||
# ── Gravity (for slopes) ──────────────────────────────────────────────
|
|
||||||
if not player.is_on_floor():
|
if not player.is_on_floor():
|
||||||
vel.y -= params.gravity * delta
|
vel.y -= params.gravity * delta
|
||||||
else:
|
else:
|
||||||
@@ -97,10 +111,41 @@ func update(delta: float) -> void:
|
|||||||
machine.on_ground = false
|
machine.on_ground = false
|
||||||
|
|
||||||
# ── End conditions ────────────────────────────────────────────────────
|
# ── End conditions ────────────────────────────────────────────────────
|
||||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
var out_speed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||||
|
|
||||||
|
# Jump out of slide: momentum-preserving hop
|
||||||
|
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||||
|
var hv := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||||
|
if hv.length_squared() > 0.01:
|
||||||
|
var dir := hv.normalized()
|
||||||
|
var jump_speed := maxf(out_speed, minf(out_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
||||||
|
player.velocity.x = dir.x * jump_speed
|
||||||
|
player.velocity.z = dir.z * jump_speed
|
||||||
|
player.velocity.y = params.jump_velocity
|
||||||
|
machine.current_jump_count = 1
|
||||||
|
machine.jump_cooldown_timer = params.jump_cooldown
|
||||||
|
machine.on_ground = false
|
||||||
|
machine.register_chain_mechanic("slide_jump")
|
||||||
|
machine.movement_event.emit("jump", {})
|
||||||
|
if player.jump_player:
|
||||||
|
player.jump_player.play()
|
||||||
|
machine.switch_to("air")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Slide straight onto a wall → wall run (keeps the flow going)
|
||||||
|
if not player.is_on_floor() and machine.wall_cooldown_timer <= 0.0 and out_speed > params.slide_min_speed:
|
||||||
|
var wall_n := machine.detect_wall_horizontal()
|
||||||
|
if wall_n != Vector3.ZERO and abs(machine.wish_dir_world.dot(wall_n)) < 0.85:
|
||||||
|
machine.switch_to("wall_run")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Dash out of slide
|
||||||
|
if machine.input_dash and machine.dash_cooldown_timer <= 0.0:
|
||||||
|
machine.switch_to("dash")
|
||||||
|
return
|
||||||
|
|
||||||
# Slide ended: too slow
|
# Slide ended: too slow
|
||||||
if hspeed < params.slide_min_speed:
|
if out_speed < params.slide_min_speed:
|
||||||
machine.switch_to("ground")
|
machine.switch_to("ground")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -113,27 +158,3 @@ func update(delta: float) -> void:
|
|||||||
if not machine.on_ground and machine.coyote_timer <= 0.0:
|
if not machine.on_ground and machine.coyote_timer <= 0.0:
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Jump out of slide
|
|
||||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
|
||||||
player.velocity.y = params.jump_velocity
|
|
||||||
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
|
||||||
if hvel.length_squared() > 0.01:
|
|
||||||
var dir := hvel.normalized()
|
|
||||||
var current_speed := hvel.length()
|
|
||||||
var jump_speed := maxf(current_speed, minf(current_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
|
||||||
player.velocity.x = dir.x * jump_speed
|
|
||||||
player.velocity.z = dir.z * jump_speed
|
|
||||||
machine.current_jump_count = 1
|
|
||||||
machine.jump_cooldown_timer = params.jump_cooldown
|
|
||||||
machine.on_ground = false
|
|
||||||
machine.register_chain_mechanic("slide_jump")
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
func _get_capsule() -> CapsuleShape3D:
|
|
||||||
for child in machine.player.get_children():
|
|
||||||
if child is CollisionShape3D and child.shape is CapsuleShape3D:
|
|
||||||
return child.shape as CapsuleShape3D
|
|
||||||
return null
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
||||||
machine.player.wallrun_player.play()
|
machine.player.wallrun_player.play()
|
||||||
|
|
||||||
# Give an initial upward boost
|
# Carry existing upward momentum into the climb so jump→climb chains flow
|
||||||
var player := machine.player
|
var player := machine.player
|
||||||
player.velocity.y = params.wall_climb_speed
|
player.velocity.y = maxf(player.velocity.y, params.wall_climb_speed)
|
||||||
|
|
||||||
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||||
player.velocity.x = hvel.x * 0.5
|
player.velocity.x = hvel.x * 0.5
|
||||||
@@ -39,7 +39,7 @@ func update(delta: float) -> void:
|
|||||||
_detach()
|
_detach()
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Look/Move away to transition to wall run ──────────────────────────────
|
# ── Look/Move away to transition to wall run ──────────────────────────
|
||||||
var look_dir := -player.global_transform.basis.z
|
var look_dir := -player.global_transform.basis.z
|
||||||
var wish_dir := machine.wish_dir_world
|
var wish_dir := machine.wish_dir_world
|
||||||
|
|
||||||
@@ -59,6 +59,18 @@ func update(delta: float) -> void:
|
|||||||
_detach()
|
_detach()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ── Jump off the wall (backward kick) ─────────────────────────────────
|
||||||
|
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||||
|
player.velocity = machine.wall_normal * params.wall_run_jump_off_normal
|
||||||
|
player.velocity.y = params.jump_velocity
|
||||||
|
machine.register_chain_mechanic("wall_jump")
|
||||||
|
machine.jump_cooldown_timer = params.jump_cooldown
|
||||||
|
machine.movement_event.emit("jump", {})
|
||||||
|
if player.jump_player:
|
||||||
|
player.jump_player.play()
|
||||||
|
_detach()
|
||||||
|
return
|
||||||
|
|
||||||
# ── Ledge Vault Check ─────────────────────────────────────────────────
|
# ── Ledge Vault Check ─────────────────────────────────────────────────
|
||||||
var fwd_wall = machine.detect_wall_forward()
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
if not fwd_wall.hit:
|
if not fwd_wall.hit:
|
||||||
@@ -68,12 +80,14 @@ func update(delta: float) -> void:
|
|||||||
|
|
||||||
if fwd_wall.is_short:
|
if fwd_wall.is_short:
|
||||||
# The upper ray missed, meaning we reached the top of the wall!
|
# The upper ray missed, meaning we reached the top of the wall!
|
||||||
_vault()
|
machine.do_vault()
|
||||||
|
machine.wall_normal = Vector3.ZERO
|
||||||
|
machine.wall_side = 0.0
|
||||||
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Movement ──────────────────────────────────────────────────────────
|
# ── Movement: ease out over the climb ─────────────────────────────────
|
||||||
# Slow down over time
|
var speed_factor: float = pow(maxf(0.0, 1.0 - (elapsed / params.wall_climb_duration)), 0.7)
|
||||||
var speed_factor = maxf(0.0, 1.0 - (elapsed / params.wall_climb_duration))
|
|
||||||
player.velocity.y = params.wall_climb_speed * speed_factor
|
player.velocity.y = params.wall_climb_speed * speed_factor
|
||||||
|
|
||||||
# Push slightly into the wall to maintain contact
|
# Push slightly into the wall to maintain contact
|
||||||
@@ -95,31 +109,3 @@ func _detach() -> void:
|
|||||||
machine.wall_cooldown_timer = 0.3
|
machine.wall_cooldown_timer = 0.3
|
||||||
machine.can_wall_climb = false
|
machine.can_wall_climb = false
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
|
|
||||||
func _vault() -> void:
|
|
||||||
var player := machine.player
|
|
||||||
var look_dir := -player.global_transform.basis.z
|
|
||||||
var h_look := Vector3(look_dir.x, 0.0, look_dir.z)
|
|
||||||
if h_look.length_squared() > 0.01:
|
|
||||||
h_look = h_look.normalized()
|
|
||||||
|
|
||||||
# Impulse
|
|
||||||
player.velocity = h_look * params.wall_climb_vault_forward
|
|
||||||
player.velocity.y = params.wall_climb_vault_up
|
|
||||||
|
|
||||||
if player.vault_player:
|
|
||||||
player.vault_player.play()
|
|
||||||
|
|
||||||
# Quick camera animation (tilt up and forward bob)
|
|
||||||
var camera = player.camera
|
|
||||||
if camera:
|
|
||||||
var tween = player.create_tween()
|
|
||||||
tween.tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x + 10.0, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", -0.2, 0.15).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.chain().tween_property(camera, "rotation_degrees:x", camera.rotation_degrees.x, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
tween.parallel().tween_property(camera, "v_offset", 0.0, 0.2).set_trans(Tween.TRANS_SINE)
|
|
||||||
|
|
||||||
machine.wall_normal = Vector3.ZERO
|
|
||||||
machine.wall_side = 0.0
|
|
||||||
machine.wall_cooldown_timer = 0.3
|
|
||||||
machine.switch_to("air")
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
stamina = params.wall_cling_max_stamina
|
stamina = params.wall_cling_max_stamina
|
||||||
machine.register_chain_mechanic("wall_cling")
|
machine.register_chain_mechanic("wall_cling")
|
||||||
# Kill most velocity but keep slight downward
|
# Keep a touch of momentum so the stop reads as a grab, not a freeze
|
||||||
machine.player.velocity = Vector3.ZERO
|
machine.player.velocity *= 0.15
|
||||||
|
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
@@ -56,6 +56,9 @@ func update(delta: float) -> void:
|
|||||||
machine.wall_cooldown_timer = 0.3
|
machine.wall_cooldown_timer = 0.3
|
||||||
machine.register_chain_mechanic("wall_cling_jump")
|
machine.register_chain_mechanic("wall_cling_jump")
|
||||||
machine.jump_cooldown_timer = params.jump_cooldown
|
machine.jump_cooldown_timer = params.jump_cooldown
|
||||||
|
machine.movement_event.emit("jump", {})
|
||||||
|
if machine.player.jump_player:
|
||||||
|
machine.player.jump_player.play()
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
extends Node
|
extends Node
|
||||||
class_name StateWallRun
|
class_name StateWallRun
|
||||||
|
|
||||||
|
## Wall run that preserves momentum: on attach we keep speed along the wall
|
||||||
|
## (plus some upward carry) and accelerate toward wall_run_speed instead of
|
||||||
|
## hard-setting velocity, so entering fast stays fast and entering slow ramps
|
||||||
|
## up smoothly. Gravity fades in over the run so the start feels planted.
|
||||||
|
|
||||||
var machine: MovementStateMachine
|
var machine: MovementStateMachine
|
||||||
var params: MovementParams:
|
var params: MovementParams:
|
||||||
get: return machine.params
|
get: return machine.params
|
||||||
|
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
var _run_speed: float = 0.0
|
|
||||||
var _last_vel: Vector3 = Vector3.ZERO
|
|
||||||
|
|
||||||
|
|
||||||
var _current_tangent: Vector3 = Vector3.ZERO
|
var _current_tangent: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
func enter(_data: Dictionary = {}) -> void:
|
func enter(_data: Dictionary = {}) -> void:
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
machine.register_chain_mechanic("wall_run")
|
machine.register_chain_mechanic("wall_run")
|
||||||
# Cap the upward momentum so they don't fly up the wall,
|
|
||||||
# and prevent negative y momentum so the wall "catches" them.
|
|
||||||
machine.player.velocity.y = clampf(machine.player.velocity.y, 0.0, 1.5)
|
|
||||||
machine.on_ground = false
|
machine.on_ground = false
|
||||||
_last_vel = machine.player.velocity
|
|
||||||
|
# Keep some upward carry so jumping into a wall run flows; the wall
|
||||||
|
# still "catches" downward momentum.
|
||||||
|
machine.player.velocity.y = clampf(machine.player.velocity.y, 0.0, params.wall_run_entry_max_up)
|
||||||
|
|
||||||
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
||||||
machine.player.wallrun_player.play()
|
machine.player.wallrun_player.play()
|
||||||
@@ -29,11 +31,6 @@ func enter(_data: Dictionary = {}) -> void:
|
|||||||
if hvel.dot(_current_tangent) < 0.0:
|
if hvel.dot(_current_tangent) < 0.0:
|
||||||
_current_tangent = -_current_tangent
|
_current_tangent = -_current_tangent
|
||||||
|
|
||||||
# Preserve speed along the wall if it's faster than base wall_run_speed
|
|
||||||
var base_speed = machine.get_effective_speed(params.wall_run_speed)
|
|
||||||
var projected_speed = hvel.dot(_current_tangent)
|
|
||||||
_run_speed = maxf(base_speed, projected_speed)
|
|
||||||
|
|
||||||
# Camera tilt
|
# Camera tilt
|
||||||
var rig = _get_camera_rig()
|
var rig = _get_camera_rig()
|
||||||
if rig:
|
if rig:
|
||||||
@@ -53,10 +50,7 @@ func exit() -> void:
|
|||||||
func update(delta: float) -> void:
|
func update(delta: float) -> void:
|
||||||
elapsed += delta
|
elapsed += delta
|
||||||
if elapsed > params.wall_run_duration:
|
if elapsed > params.wall_run_duration:
|
||||||
machine.wall_normal = Vector3.ZERO
|
_detach(0.4)
|
||||||
machine.wall_side = 0.0
|
|
||||||
machine.wall_cooldown_timer = 0.3
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
var player := machine.player
|
var player := machine.player
|
||||||
@@ -65,42 +59,35 @@ func update(delta: float) -> void:
|
|||||||
if player.wallrun_player and not player.wallrun_player.playing:
|
if player.wallrun_player and not player.wallrun_player.playing:
|
||||||
player.wallrun_player.play()
|
player.wallrun_player.play()
|
||||||
|
|
||||||
if vel.distance_squared_to(_last_vel) > 100.0:
|
# ── Gradual gravity pull (starts weightless, ramps up) ────────────────
|
||||||
machine.wall_normal = Vector3.ZERO
|
var gravity_factor := 0.05 + 0.95 * pow(elapsed / params.wall_run_duration, 1.5)
|
||||||
machine.wall_side = 0.0
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ── Gradual gravity pull (starts light, increases over time) ──────────
|
|
||||||
var gravity_factor := 0.1 + 0.9 * (elapsed / params.wall_run_duration)
|
|
||||||
vel.y -= params.wall_run_gravity * gravity_factor * delta
|
vel.y -= params.wall_run_gravity * gravity_factor * delta
|
||||||
|
|
||||||
# ── Move along wall tangent ───────────────────────────────────────────
|
# ── Accelerate along wall tangent (keeps momentum, no hard set) ───────
|
||||||
var wall_tangent := machine.wall_normal.cross(Vector3.UP).normalized()
|
var wall_tangent := machine.wall_normal.cross(Vector3.UP).normalized()
|
||||||
# Ensure the new tangent aligns with our locked forward direction
|
|
||||||
if _current_tangent.dot(wall_tangent) < 0.0:
|
if _current_tangent.dot(wall_tangent) < 0.0:
|
||||||
wall_tangent = -wall_tangent
|
wall_tangent = -wall_tangent
|
||||||
_current_tangent = wall_tangent
|
_current_tangent = wall_tangent
|
||||||
|
|
||||||
vel.x = wall_tangent.x * _run_speed
|
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||||
vel.z = wall_tangent.z * _run_speed
|
var along := hvel.dot(wall_tangent)
|
||||||
|
var target_speed: float = maxf(machine.get_effective_speed(params.wall_run_speed), along)
|
||||||
|
along = move_toward(along, target_speed, params.wall_run_accel * delta)
|
||||||
|
# Redirect all horizontal velocity along the wall (kills the into-wall part)
|
||||||
|
hvel = wall_tangent * along
|
||||||
|
vel.x = hvel.x
|
||||||
|
vel.z = hvel.z
|
||||||
|
|
||||||
# ── Look away to break wall run ───────────────────────────────────────
|
# ── Look away to break wall run ───────────────────────────────────────
|
||||||
var look_dir := -player.global_transform.basis.z
|
var look_dir := -player.global_transform.basis.z
|
||||||
if look_dir.dot(machine.wall_normal) > 0.4:
|
if look_dir.dot(machine.wall_normal) > 0.4:
|
||||||
machine.wall_normal = Vector3.ZERO
|
_detach(0.3)
|
||||||
machine.wall_side = 0.0
|
|
||||||
machine.wall_cooldown_timer = 0.3
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Push slightly toward wall to maintain contact ─────────────────────
|
|
||||||
vel -= machine.wall_normal * 2.0
|
|
||||||
|
|
||||||
# ── Wall jump ─────────────────────────────────────────────────────────
|
# ── Wall jump ─────────────────────────────────────────────────────────
|
||||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||||
# Base jump off velocity on current preserved momentum
|
# Base jump off velocity on current preserved momentum
|
||||||
var jump_vel: Vector3 = _current_tangent * _run_speed
|
var jump_vel: Vector3 = _current_tangent * along
|
||||||
|
|
||||||
# Spring off the wall
|
# Spring off the wall
|
||||||
jump_vel += machine.wall_normal * params.wall_run_jump_off_normal
|
jump_vel += machine.wall_normal * params.wall_run_jump_off_normal
|
||||||
@@ -115,6 +102,9 @@ func update(delta: float) -> void:
|
|||||||
machine.wall_cooldown_timer = 0.3
|
machine.wall_cooldown_timer = 0.3
|
||||||
machine.register_chain_mechanic("wall_jump")
|
machine.register_chain_mechanic("wall_jump")
|
||||||
machine.jump_cooldown_timer = params.jump_cooldown
|
machine.jump_cooldown_timer = params.jump_cooldown
|
||||||
|
machine.movement_event.emit("jump", {})
|
||||||
|
if player.jump_player:
|
||||||
|
player.jump_player.play()
|
||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -123,16 +113,16 @@ func update(delta: float) -> void:
|
|||||||
machine.switch_to("wall_cling")
|
machine.switch_to("wall_cling")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ── Push slightly toward wall to maintain contact ─────────────────────
|
||||||
|
vel -= machine.wall_normal * 2.0
|
||||||
|
|
||||||
player.velocity = vel
|
player.velocity = vel
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
_last_vel = player.velocity
|
|
||||||
|
|
||||||
# ── Check still on wall ───────────────────────────────────────────────
|
# ── Check still on wall ───────────────────────────────────────────────
|
||||||
var still_on_wall := machine.detect_wall_horizontal()
|
var still_on_wall := machine.detect_wall_horizontal()
|
||||||
if still_on_wall == Vector3.ZERO:
|
if still_on_wall == Vector3.ZERO:
|
||||||
machine.wall_normal = Vector3.ZERO
|
_detach(0.0)
|
||||||
machine.wall_side = 0.0
|
|
||||||
machine.switch_to("air")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Hit floor during wall run ─────────────────────────────────────────
|
# ── Hit floor during wall run ─────────────────────────────────────────
|
||||||
@@ -144,6 +134,14 @@ func update(delta: float) -> void:
|
|||||||
machine.switch_to("ground")
|
machine.switch_to("ground")
|
||||||
|
|
||||||
|
|
||||||
|
func _detach(cooldown: float) -> void:
|
||||||
|
machine.wall_normal = Vector3.ZERO
|
||||||
|
machine.wall_side = 0.0
|
||||||
|
if cooldown > 0.0:
|
||||||
|
machine.wall_cooldown_timer = cooldown
|
||||||
|
machine.switch_to("air")
|
||||||
|
|
||||||
|
|
||||||
func _get_camera_rig():
|
func _get_camera_rig():
|
||||||
if machine.player and machine.player.has_node("HeadPivot"):
|
if machine.player and machine.player.has_node("HeadPivot"):
|
||||||
return machine.player.get_node("HeadPivot")
|
return machine.player.get_node("HeadPivot")
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_movement_speed")
|
client_rep_config.add_property(":synced_movement_speed")
|
||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
client_rep_config.add_property(":synced_grapple_point")
|
client_rep_config.add_property(":synced_grapple_point")
|
||||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||||
client_rep_config.add_property(":synced_skin_id")
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_movement_speed")
|
client_rep_config.add_property(":synced_movement_speed")
|
||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
client_rep_config.add_property(":synced_grapple_point")
|
client_rep_config.add_property(":synced_grapple_point")
|
||||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||||
client_rep_config.add_property(":synced_skin_id")
|
client_rep_config.add_property(":synced_skin_id")
|
||||||
|
|||||||
Reference in New Issue
Block a user