extends State class_name AimState var state_name : String = "Aim" var play_char : CharacterBody3D @export var aim_action : StringName = &"player_aim_camera" @export var switch_target_action : StringName = &"player_aim_switch_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 une cible pour qu'elle soit 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 de distance 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 var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant que la cible est encore au centre var dodging_from_aim : bool = false #true quand on quitte Aim pour rouler, pour ne pas relacher la cible verrouillee var came_from_crouch : bool = false #true si Aim a ete declenche depuis CrouchState, pour y retourner en sortant func enter(play_char_ref : CharacterBody3D) -> void: play_char = play_char_ref auto_lock_blocked = false #curr_state_name n'est pas encore mis a jour a ce stade : il reflete encore l'etat qu'on est en train de quitter came_from_crouch = play_char.state_machine.curr_state_name == "Crouch" verifications() func verifications() -> void: play_char.floor_snap_length = 1.0 play_char.cam_holder.cam_aimed = true func exit() -> void: if dodging_from_aim: #on roule en visant : la camera et la cible verrouillee restent actives pendant la roulade dodging_from_aim = false return 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_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0: dodging_from_aim = true transitioned.emit(self, "DodgeState") return true if !Input.is_action_pressed(aim_action): if came_from_crouch: transitioned.emit(self, "CrouchState") else: 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): auto_lock_blocked = true release_target() elif Input.is_action_just_pressed(switch_target_action): switch_target() 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 que distance et ligne de vue sont valides #(l'angle avec la camera ne compte pas, le joueur peut deplacer la camera librement) if is_instance_valid(current): if is_target_valid(current, lose_range_margin, false): 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 == null: auto_lock_blocked = false #le joueur a ecarte la camera : l'auto-lock est de nouveau permis elif !auto_lock_blocked: 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, true): result.append(t) return result func is_target_valid(t : AimTarget, range_mult : float, check_angle : bool) -> bool: if !t.enabled: return false if play_char.global_position.distance_to(t.global_position) > throw_range * range_mult: return false if check_angle and get_angle_to_camera(t) > max_aim_angle: 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 switch_target() -> void: #passe a la cible suivante, triee de gauche a droite par rapport a la camera (boucle) var cam : Camera3D = play_char.cam_holder.cam var forward : Vector3 = -cam.global_basis.z var list : Array[AimTarget] = get_candidates() #la cible actuelle reste dans la liste meme si la camera est decalee par rapport a elle var current : AimTarget = play_char.aim_target if is_instance_valid(current) and !list.has(current): list.append(current) if list.size() < 2: 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(current) idx = 0 if idx == -1 else (idx + 1) % 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.set_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.set_lock_target(null)