Merge pull request 'feat: implement movement state machine architecture and add various sound assets' (#15) from feat/11-adding-wall-climbing-and-valuting-fundamentals into main
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -465,4 +465,26 @@ for i in range(int(sample_rate * flight_duration)):
|
|||||||
swarm_flight_samples.append(math.tanh(raw * 2.0))
|
swarm_flight_samples.append(math.tanh(raw * 2.0))
|
||||||
write_wav('assets/sounds/swarm_flight.wav', swarm_flight_samples)
|
write_wav('assets/sounds/swarm_flight.wav', swarm_flight_samples)
|
||||||
|
|
||||||
|
# 23. Vault Sound (Quick airy whoosh + clothing ruffle)
|
||||||
|
vault_duration = 0.3
|
||||||
|
vault_samples = []
|
||||||
|
prev_v = 0.0
|
||||||
|
for i in range(int(sample_rate * vault_duration)):
|
||||||
|
t = i / sample_rate
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
|
||||||
|
# Airy whoosh
|
||||||
|
alpha = 0.08
|
||||||
|
filtered = prev_v + alpha * (noise - prev_v)
|
||||||
|
prev_v = filtered
|
||||||
|
whoosh_env = math.sin(t * math.pi / vault_duration)
|
||||||
|
whoosh = filtered * whoosh_env * 0.6
|
||||||
|
|
||||||
|
# Ruffle/grab sound
|
||||||
|
ruffle_env = math.exp(-t * 30.0)
|
||||||
|
ruffle = noise * ruffle_env * 0.2
|
||||||
|
|
||||||
|
vault_samples.append(whoosh + ruffle)
|
||||||
|
write_wav('assets/sounds/vault.wav', vault_samples)
|
||||||
|
|
||||||
print("Generated all sounds!")
|
print("Generated all sounds!")
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ class_name MovementParams
|
|||||||
@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.3
|
@export var wall_ray_down_height: float = -0.3
|
||||||
|
|
||||||
|
# ── Wall Climb ────────────────────────────────────────────────────────────────
|
||||||
|
@export var wall_climb_speed: float = 6.0
|
||||||
|
@export var wall_climb_duration: float = 3.0
|
||||||
|
@export var wall_climb_max_angle: float = 55.0
|
||||||
|
@export var wall_climb_vault_forward: float = 16.0
|
||||||
|
@export var wall_climb_vault_up: float = 14.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.27
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ func _ready() -> void:
|
|||||||
grapple_state.set_script(load("res://movement/states/state_grapple.gd"))
|
grapple_state.set_script(load("res://movement/states/state_grapple.gd"))
|
||||||
add_child(grapple_state)
|
add_child(grapple_state)
|
||||||
|
|
||||||
|
# Ensure wall climb state is injected
|
||||||
|
var wall_climb_state = Node.new()
|
||||||
|
wall_climb_state.name = "state_wall_climb"
|
||||||
|
wall_climb_state.set_script(load("res://movement/states/state_wall_climb.gd"))
|
||||||
|
add_child(wall_climb_state)
|
||||||
|
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
if child is Node and child.name.begins_with("state_"):
|
if child is Node and child.name.begins_with("state_"):
|
||||||
states[child.name.replace("state_", "")] = child
|
states[child.name.replace("state_", "")] = child
|
||||||
@@ -214,6 +220,9 @@ func detect_wall_horizontal() -> Vector3:
|
|||||||
ray.exclude = [player.get_rid()]
|
ray.exclude = [player.get_rid()]
|
||||||
var hit := space_state.intersect_ray(ray)
|
var hit := space_state.intersect_ray(ray)
|
||||||
if not hit.is_empty():
|
if not hit.is_empty():
|
||||||
|
var collider = hit.get("collider")
|
||||||
|
if collider is PlayerMovementController:
|
||||||
|
continue
|
||||||
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
||||||
# Wall must be roughly vertical (normal mostly horizontal)
|
# Wall must be roughly vertical (normal mostly horizontal)
|
||||||
if absf(n.y) < 0.3 and n.length_squared() > 0.0:
|
if absf(n.y) < 0.3 and n.length_squared() > 0.0:
|
||||||
@@ -232,6 +241,9 @@ func detect_wall_horizontal() -> Vector3:
|
|||||||
ray.exclude = [player.get_rid()]
|
ray.exclude = [player.get_rid()]
|
||||||
var hit := space_state.intersect_ray(ray)
|
var hit := space_state.intersect_ray(ray)
|
||||||
if not hit.is_empty():
|
if not hit.is_empty():
|
||||||
|
var collider = hit.get("collider")
|
||||||
|
if collider is PlayerMovementController:
|
||||||
|
continue
|
||||||
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
||||||
if absf(n.y) < 0.3 and n.length_squared() > 0.0:
|
if absf(n.y) < 0.3 and n.length_squared() > 0.0:
|
||||||
wall_normal = n.normalized()
|
wall_normal = n.normalized()
|
||||||
@@ -242,6 +254,62 @@ func detect_wall_horizontal() -> Vector3:
|
|||||||
wall_side = 0.0
|
wall_side = 0.0
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
## Utility: detect wall directly in front of the player.
|
||||||
|
## Useful for wall climbing and vaulting over short walls.
|
||||||
|
func detect_wall_forward() -> Dictionary:
|
||||||
|
if not player:
|
||||||
|
return {"hit": false, "normal": Vector3.ZERO, "is_short": false}
|
||||||
|
|
||||||
|
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||||
|
var move_dir := Vector3.ZERO
|
||||||
|
if hvel.length_squared() > 0.1:
|
||||||
|
move_dir = hvel.normalized()
|
||||||
|
else:
|
||||||
|
move_dir = -player.global_transform.basis.z
|
||||||
|
move_dir.y = 0.0
|
||||||
|
if move_dir.length_squared() > 0.01:
|
||||||
|
move_dir = move_dir.normalized()
|
||||||
|
|
||||||
|
if move_dir.length_squared() < 0.1:
|
||||||
|
return {"hit": false, "normal": Vector3.ZERO, "is_short": false}
|
||||||
|
|
||||||
|
var space_state := player.get_world_3d().direct_space_state
|
||||||
|
var dist := params.wall_detect_distance
|
||||||
|
|
||||||
|
# Lower ray (feet/knees)
|
||||||
|
var origin_low := player.global_position + Vector3.UP * params.wall_ray_down_height
|
||||||
|
var ray_low := PhysicsRayQueryParameters3D.create(origin_low, origin_low + move_dir * dist)
|
||||||
|
ray_low.exclude = [player.get_rid()]
|
||||||
|
var hit_low := space_state.intersect_ray(ray_low)
|
||||||
|
|
||||||
|
# Mid ray (chest/head)
|
||||||
|
var origin_mid := player.global_position + Vector3.UP * params.wall_ray_up_height
|
||||||
|
var ray_mid := PhysicsRayQueryParameters3D.create(origin_mid, origin_mid + move_dir * dist)
|
||||||
|
ray_mid.exclude = [player.get_rid()]
|
||||||
|
var hit_mid := space_state.intersect_ray(ray_mid)
|
||||||
|
|
||||||
|
# If both lower rays miss, there is no wall directly in front
|
||||||
|
if hit_low.is_empty() and hit_mid.is_empty():
|
||||||
|
return {"hit": false, "normal": Vector3.ZERO, "is_short": false}
|
||||||
|
|
||||||
|
# Get the normal from the highest point we hit, or the lowest if we only hit low
|
||||||
|
var best_hit = hit_mid if not hit_mid.is_empty() else hit_low
|
||||||
|
|
||||||
|
if best_hit.get("collider") is PlayerMovementController:
|
||||||
|
return {"hit": false, "normal": Vector3.ZERO, "is_short": false}
|
||||||
|
|
||||||
|
var normal: Vector3 = best_hit.get("normal", Vector3.ZERO)
|
||||||
|
if absf(normal.y) >= 0.3:
|
||||||
|
return {"hit": false, "normal": Vector3.ZERO, "is_short": false} # Not a vertical wall
|
||||||
|
|
||||||
|
# Check if wall is short (ledge check) using a top ray
|
||||||
|
var origin_high := player.global_position + Vector3.UP * (params.wall_ray_up_height + params.wall_climb_vault_height_check)
|
||||||
|
var ray_high := PhysicsRayQueryParameters3D.create(origin_high, origin_high + move_dir * dist)
|
||||||
|
ray_high.exclude = [player.get_rid()]
|
||||||
|
var hit_high := space_state.intersect_ray(ray_high)
|
||||||
|
|
||||||
|
return {"hit": true, "normal": normal.normalized(), "is_short": hit_high.is_empty()}
|
||||||
|
|
||||||
|
|
||||||
func _apply_crouch(crouched: bool) -> void:
|
func _apply_crouch(crouched: bool) -> void:
|
||||||
var shape_node = null
|
var shape_node = null
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ var wallrun_player: AudioStreamPlayer
|
|||||||
var wind_player: AudioStreamPlayer
|
var wind_player: AudioStreamPlayer
|
||||||
var jump_player: AudioStreamPlayer
|
var jump_player: AudioStreamPlayer
|
||||||
var double_jump_player: AudioStreamPlayer
|
var double_jump_player: AudioStreamPlayer
|
||||||
|
var vault_player: AudioStreamPlayer
|
||||||
|
|
||||||
var grapple_shoot_player: AudioStreamPlayer
|
var grapple_shoot_player: AudioStreamPlayer
|
||||||
var grapple_latch_player: AudioStreamPlayer
|
var grapple_latch_player: AudioStreamPlayer
|
||||||
@@ -123,6 +124,11 @@ func _setup_audio() -> void:
|
|||||||
double_jump_player.stream = load("res://assets/sounds/double_jump.wav")
|
double_jump_player.stream = load("res://assets/sounds/double_jump.wav")
|
||||||
add_child(double_jump_player)
|
add_child(double_jump_player)
|
||||||
|
|
||||||
|
vault_player = AudioStreamPlayer.new()
|
||||||
|
vault_player.bus = "SFX"
|
||||||
|
vault_player.stream = load("res://assets/sounds/vault.wav")
|
||||||
|
add_child(vault_player)
|
||||||
|
|
||||||
dash_player = AudioStreamPlayer.new()
|
dash_player = AudioStreamPlayer.new()
|
||||||
dash_player.bus = "SFX"
|
dash_player.bus = "SFX"
|
||||||
dash_player.stream = load("res://assets/sounds/dash.wav")
|
dash_player.stream = load("res://assets/sounds/dash.wav")
|
||||||
|
|||||||
@@ -100,11 +100,48 @@ func update(delta: float) -> void:
|
|||||||
player.double_jump_player.play()
|
player.double_jump_player.play()
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Wall run transition ───────────────────────────────────────────────
|
# ── Wall interaction (Run, Climb, Vault) ──────────────────────────────
|
||||||
var wall_n := machine.detect_wall_horizontal()
|
if machine.input_dir.length() > 0.1 and machine.wall_cooldown_timer <= 0.0:
|
||||||
if wall_n != Vector3.ZERO and machine.input_dir.length() > 0.1 and machine.wall_cooldown_timer <= 0.0:
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
machine.switch_to("wall_run")
|
if fwd_wall.hit:
|
||||||
return
|
if fwd_wall.is_short:
|
||||||
|
# Instant 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
|
||||||
|
else:
|
||||||
|
# Check if moving directly into wall and looking generally towards it
|
||||||
|
var look_dir := -player.global_transform.basis.z
|
||||||
|
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))
|
||||||
|
|
||||||
|
if looking_towards and moving_towards:
|
||||||
|
machine.wall_normal = fwd_wall.normal
|
||||||
|
machine.switch_to("wall_climb")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback to wall run
|
||||||
|
var wall_n := machine.detect_wall_horizontal()
|
||||||
|
if wall_n != Vector3.ZERO:
|
||||||
|
machine.switch_to("wall_run")
|
||||||
|
return
|
||||||
|
|
||||||
# ── Dash ──────────────────────────────────────────────────────────────
|
# ── Dash ──────────────────────────────────────────────────────────────
|
||||||
if machine.input_dash:
|
if machine.input_dash:
|
||||||
|
|||||||
@@ -90,6 +90,45 @@ func update(delta: float) -> void:
|
|||||||
|
|
||||||
player.velocity = vel
|
player.velocity = vel
|
||||||
player.move_and_slide()
|
player.move_and_slide()
|
||||||
|
|
||||||
|
# ── Stair Stepping ────────────────────────────────────────────────────
|
||||||
|
if wish_dir.length_squared() > 0.01 and not machine.input_crouch:
|
||||||
|
for i in range(player.get_slide_collision_count()):
|
||||||
|
var col = player.get_slide_collision(i)
|
||||||
|
if absf(col.get_normal().y) < 0.3: # Hit a wall
|
||||||
|
var max_step_height = 0.5
|
||||||
|
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.5
|
||||||
|
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():
|
||||||
|
# We can safely step up
|
||||||
|
player.global_position.y += 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()
|
_update_capsule_height()
|
||||||
|
|
||||||
@@ -116,12 +155,50 @@ func update(delta: float) -> void:
|
|||||||
machine.switch_to("air")
|
machine.switch_to("air")
|
||||||
return
|
return
|
||||||
|
|
||||||
# ── Wall run check ────────────────────────────────────────────────────
|
# ── Wall interaction (Run, Climb, Vault) ──────────────────────────────
|
||||||
var wall_n := machine.detect_wall_horizontal()
|
if machine.input_dir.length() > 0.1 and machine.wall_cooldown_timer <= 0.0 and not machine.input_crouch:
|
||||||
if wall_n != Vector3.ZERO and machine.input_dir.length() > 0.1:
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
if not player.is_on_floor():
|
if fwd_wall.hit:
|
||||||
machine.switch_to("wall_run")
|
if fwd_wall.is_short:
|
||||||
return
|
# Instant 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
|
||||||
|
machine.switch_to("air")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Check if moving directly into wall and looking generally towards it
|
||||||
|
var look_dir := -player.global_transform.basis.z
|
||||||
|
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))
|
||||||
|
|
||||||
|
if looking_towards and moving_towards:
|
||||||
|
machine.wall_normal = fwd_wall.normal
|
||||||
|
machine.switch_to("wall_climb")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback to wall run
|
||||||
|
var wall_n := machine.detect_wall_horizontal()
|
||||||
|
if wall_n != Vector3.ZERO:
|
||||||
|
if not player.is_on_floor():
|
||||||
|
machine.switch_to("wall_run")
|
||||||
|
return
|
||||||
|
|
||||||
# ── Dash ──────────────────────────────────────────────────────────────
|
# ── Dash ──────────────────────────────────────────────────────────────
|
||||||
if machine.input_dash:
|
if machine.input_dash:
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ func update(delta: float) -> void:
|
|||||||
vel.x *= pow(params.slide_friction, delta * 10.0)
|
vel.x *= pow(params.slide_friction, delta * 10.0)
|
||||||
vel.z *= pow(params.slide_friction, delta * 10.0)
|
vel.z *= pow(params.slide_friction, delta * 10.0)
|
||||||
|
|
||||||
|
# ── Allow slight steering ─────────────────────────────────────────────
|
||||||
|
var wish_dir := machine.wish_dir_world
|
||||||
|
if wish_dir.length_squared() > 0.01:
|
||||||
|
wish_dir = wish_dir.normalized()
|
||||||
|
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||||
|
var current_speed := hvel.length()
|
||||||
|
|
||||||
|
# Slowly bend velocity towards wish_dir
|
||||||
|
var steer_speed = 3.0 * delta
|
||||||
|
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:
|
||||||
|
_slide_direction = new_hvel.normalized()
|
||||||
|
|
||||||
# ── Gravity (for slopes) ──────────────────────────────────────────────
|
# ── 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
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
extends Node
|
||||||
|
class_name StateWallClimb
|
||||||
|
|
||||||
|
var machine: MovementStateMachine
|
||||||
|
var params: MovementParams:
|
||||||
|
get: return machine.params
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
func enter(_data: Dictionary = {}) -> void:
|
||||||
|
elapsed = 0.0
|
||||||
|
machine.register_chain_mechanic("wall_climb")
|
||||||
|
machine.on_ground = false
|
||||||
|
|
||||||
|
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
||||||
|
machine.player.wallrun_player.play()
|
||||||
|
|
||||||
|
# Give an initial upward boost
|
||||||
|
var player := machine.player
|
||||||
|
player.velocity.y = params.wall_climb_speed
|
||||||
|
|
||||||
|
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||||
|
player.velocity.x = hvel.x * 0.5
|
||||||
|
player.velocity.z = hvel.z * 0.5
|
||||||
|
|
||||||
|
func exit() -> void:
|
||||||
|
if machine.player.wallrun_player:
|
||||||
|
machine.player.wallrun_player.stop()
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
var player := machine.player
|
||||||
|
|
||||||
|
if player.wallrun_player and not player.wallrun_player.playing:
|
||||||
|
player.wallrun_player.play()
|
||||||
|
|
||||||
|
# ── Time limit ────────────────────────────────────────────────────────
|
||||||
|
if elapsed > params.wall_climb_duration:
|
||||||
|
_detach()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Look/Move away to transition to wall run ──────────────────────────────
|
||||||
|
var look_dir := -player.global_transform.basis.z
|
||||||
|
var wish_dir := machine.wish_dir_world
|
||||||
|
|
||||||
|
var looking_towards := look_dir.dot(-machine.wall_normal) > 0.0
|
||||||
|
var moving_sideways := false
|
||||||
|
if wish_dir.length_squared() > 0.01:
|
||||||
|
moving_sideways = wish_dir.dot(-machine.wall_normal) <= cos(deg_to_rad(params.wall_climb_max_angle))
|
||||||
|
|
||||||
|
if not looking_towards or moving_sideways:
|
||||||
|
# We are looking away or moving sideways, transition to wall run
|
||||||
|
# The wall run state will figure out tangent and side
|
||||||
|
var wall_n = machine.detect_wall_horizontal()
|
||||||
|
if wall_n != Vector3.ZERO:
|
||||||
|
machine.switch_to("wall_run")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_detach()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Ledge Vault Check ─────────────────────────────────────────────────
|
||||||
|
var fwd_wall = machine.detect_wall_forward()
|
||||||
|
if not fwd_wall.hit:
|
||||||
|
# The forward lower ray missed, we fell off the wall completely
|
||||||
|
_detach()
|
||||||
|
return
|
||||||
|
|
||||||
|
if fwd_wall.is_short:
|
||||||
|
# The upper ray missed, meaning we reached the top of the wall!
|
||||||
|
_vault()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Movement ──────────────────────────────────────────────────────────
|
||||||
|
# Slow down over time
|
||||||
|
var speed_factor = maxf(0.0, 1.0 - (elapsed / params.wall_climb_duration))
|
||||||
|
player.velocity.y = params.wall_climb_speed * speed_factor
|
||||||
|
|
||||||
|
# Push slightly into the wall to maintain contact
|
||||||
|
player.velocity -= machine.wall_normal * 2.0
|
||||||
|
|
||||||
|
player.move_and_slide()
|
||||||
|
|
||||||
|
# ── Hit floor during wall climb (should be rare) ──────────────────────
|
||||||
|
if player.is_on_floor():
|
||||||
|
machine.on_ground = true
|
||||||
|
machine.wall_normal = Vector3.ZERO
|
||||||
|
machine.wall_side = 0.0
|
||||||
|
machine.current_jump_count = 0
|
||||||
|
machine.switch_to("ground")
|
||||||
|
|
||||||
|
func _detach() -> void:
|
||||||
|
machine.wall_normal = Vector3.ZERO
|
||||||
|
machine.wall_side = 0.0
|
||||||
|
machine.wall_cooldown_timer = 0.3
|
||||||
|
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")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bdri08bsr5muh
|
||||||
@@ -16,8 +16,8 @@ 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,
|
# Cap the upward momentum so they don't fly up the wall,
|
||||||
# but preserve some if they just jumped onto it.
|
# and prevent negative y momentum so the wall "catches" them.
|
||||||
machine.player.velocity.y = minf(machine.player.velocity.y, 1.5)
|
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
|
_last_vel = machine.player.velocity
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user