Tales-of-PKMN/game/player/StateMachine/AimState.gd

149 lines
5.6 KiB
GDScript

extends State
class_name AimState
var state_name : String = "Aim"
var play_char : CharacterBody3D
@export var aim_action : StringName = &"player_aim_camera"
@export var next_target_action : StringName = &"player_aim_next_target"
@export var prev_target_action : StringName = &"player_aim_prev_target"
@export var unlock_action : StringName = &"player_aim_unlock"
@export_group("Targeting")
@export var throw_range : float = 20.0
@export var max_aim_angle : float = 35.0 #angle max (deg) entre l'axe camera et la cible pour etre candidate
@export var auto_lock_angle : float = 12.0 #si une cible passe sous cet angle, on se verrouille dessus
@export var lose_range_margin : float = 1.25 #marge avant de perdre une cible deja verrouillee
@export var check_line_of_sight : bool = true
@export_flags_3d_physics var line_of_sight_mask : int = 1
@export_group("Movement")
@export var aim_move_speed : float = 3.0
@export var aim_move_accel : float = 8.0
func enter(play_char_ref : CharacterBody3D) -> void:
play_char = play_char_ref
verifications()
func verifications() -> void:
play_char.floor_snap_length = 1.0
play_char.cam_holder.cam_aimed = true #adapte si ton CameraHolder gere ca autrement
func exit() -> void:
release_target()
if play_char: play_char.cam_holder.cam_aimed = false
func physics_update(delta : float) -> void:
if applies(): return
play_char.gravity_apply(delta)
if input_management(): return
update_target()
move(delta)
func applies() -> bool:
if !play_char.is_on_floor() and play_char.velocity.y < 0.0:
transitioned.emit(self, "InairState")
return true
return false
func input_management() -> bool:
if !Input.is_action_pressed(aim_action):
transitioned.emit(self, play_char.walk_or_run if play_char.move_dir else "IdleState")
return true
if Input.is_action_just_pressed(unlock_action):
release_target()
elif Input.is_action_just_pressed(next_target_action):
cycle_target(1)
elif Input.is_action_just_pressed(prev_target_action):
cycle_target(-1)
return false
func move(delta : float) -> void:
play_char.move_dir = Input.get_vector(play_char.move_left_action, play_char.move_right_action, play_char.move_forward_action, play_char.move_backward_action).rotated(-play_char.cam_holder.global_rotation.y)
if play_char.move_dir and play_char.is_on_floor():
play_char.velocity.x = lerp(play_char.velocity.x, play_char.move_dir.x * aim_move_speed, aim_move_accel * delta)
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * aim_move_speed, aim_move_accel * delta)
else:
play_char.velocity.x = lerp(play_char.velocity.x, 0.0, play_char.move_deccel * delta)
play_char.velocity.z = lerp(play_char.velocity.z, 0.0, play_char.move_deccel * delta)
#---------- targeting ----------
func update_target() -> void:
var current : AimTarget = play_char.aim_target
#verrouillage existant : on le garde tant qu'il est valide
if is_instance_valid(current):
if is_target_valid(current, lose_range_margin): return
release_target()
#visee libre : auto-lock sur la meilleure cible proche du centre de la camera
var best : AimTarget = null
var best_angle : float = auto_lock_angle
for t in get_candidates():
var angle : float = get_angle_to_camera(t) - t.priority
if angle < best_angle:
best_angle = angle
best = t
if best: set_target(best)
func get_candidates() -> Array[AimTarget]:
var result : Array[AimTarget] = []
for node in get_tree().get_nodes_in_group("aim_targets"):
var t := node as AimTarget
if t and is_target_valid(t, 1.0): result.append(t)
return result
func is_target_valid(t : AimTarget, range_mult : float) -> bool:
if !t.enabled: return false
if play_char.global_position.distance_to(t.global_position) > throw_range * range_mult: return false
if get_angle_to_camera(t) > max_aim_angle * range_mult: return false
if check_line_of_sight and !has_line_of_sight(t): return false
return true
func get_angle_to_camera(t : AimTarget) -> float:
var cam : Camera3D = play_char.cam_holder.cam
var to_t : Vector3 = t.global_position - cam.global_position
return rad_to_deg((-cam.global_basis.z).angle_to(to_t))
func has_line_of_sight(t : AimTarget) -> bool:
var query := PhysicsRayQueryParameters3D.create(
play_char.cam_holder.cam.global_position, t.global_position, line_of_sight_mask, [play_char.get_rid()])
var hit : Dictionary = play_char.get_world_3d().direct_space_state.intersect_ray(query)
#pas d'obstacle, ou l'obstacle est le proprietaire de la cible lui-meme
return hit.is_empty() or hit["collider"].is_ancestor_of(t)
func cycle_target(direction : int) -> void:
#trie les cibles de gauche a droite (par rapport a la camera) et passe a la suivante/precedente
var cam : Camera3D = play_char.cam_holder.cam
var forward : Vector3 = -cam.global_basis.z
var list : Array[AimTarget] = get_candidates()
if list.is_empty(): return
list.sort_custom(func(a : AimTarget, b : AimTarget) -> bool:
return signed_angle(forward, a, cam) < signed_angle(forward, b, cam))
var idx : int = list.find(play_char.aim_target)
idx = 0 if idx == -1 else posmod(idx + direction, list.size())
set_target(list[idx])
func signed_angle(forward : Vector3, t : AimTarget, cam : Camera3D) -> float:
var to_t : Vector3 = t.global_position - cam.global_position
return Vector2(forward.x, forward.z).angle_to(Vector2(to_t.x, to_t.z))
func set_target(t : AimTarget) -> void:
if play_char.aim_target == t: return
release_target()
play_char.aim_target = t
play_char.cam_holder.lock_target = t
t.set_locked(true)
func release_target() -> void:
if !play_char: return
if is_instance_valid(play_char.aim_target): play_char.aim_target.set_locked(false)
play_char.aim_target = null
play_char.cam_holder.lock_target = null