extends PlayerComponent class_name AimTargeting #la visee est un MODE superpose aux etats (comme la posture accroupie), plus un etat a part : # - elle demarre quand un etat au sol voit l'appui sur la visee (begin_aim) ou quand la visee est maintenue a la fin d'une roulade / d'un lancer # - elle dure tant que le bouton est maintenu : fin des qu'il est relache, ou en l'air. Pendant un lancer, elle attend la fin du lancer # - les etats continuent leur travail (marcher, rouler, lancer) : GroundedState ralentit le deplacement et bloque le saut #le composant gere aussi le ciblage : choix de la cible (auto-lock, changement, deverrouillage), camera de visee, reticule et hints. #GroundedState et ThrowState appellent tick() chaque frame tant que la visee est active. #composant a ajouter sous le PlayerCharacter (le nom du node est libre, voir PlayerComponent). @export var throw_range : float = 20.0 #portee du lancer : sert aussi a ThrowResolver pour le point vise sans cible @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 #vitesse de deplacement pendant la visee (remplace celle de la marche / course / crouch) @export var aim_move_accel : float = 8.0 @export_group("Input") @export var unlock_action : StringName = &"player_aim_unlock" @export_group("Hints") @export var switch_hint : Hint #switch_target.tres const TARGET_GROUP : String = "aim_targets" #groupe des AimTarget (aussi utilise par ThrowResolver) var active : bool = false #true tant que la visee est en cours var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant que la cible est encore au centre var switch_target_action : StringName: #l'action vient du Hint : une seule definition get: return switch_hint.action func register_with_player() -> void: player.aim_targeting = self func _ready() -> void: process_physics_priority = -5 #avant la StateMachine : les etats voient une visee deja a jour (fin de visee, en l'air...) func _exit_tree() -> void: Hints.clear_hints(self) #---------- mode visee ---------- func _physics_process(_delta : float) -> void: if !active: return var s : State = player.state_machine.curr_state if s is AirborneState: end_aim() #pas de visee en l'air : il faut reappuyer apres l'atterrissage elif !player.player_input.aim_held() and !(s is ThrowState): end_aim() #bouton relache (pendant un lancer, on attend la fin du lancer : la cible verrouillee compte) func begin_aim() -> void: if active: return active = true auto_lock_blocked = false player.cam_holder.cam_aimed = true var list : Array[Hint] = [] if switch_hint: list.append(switch_hint) Hints.show_hints(self, list) func end_aim() -> void: if !active: return active = false release_target() player.cam_holder.cam_aimed = false AimReticle.hide_instance() Hints.clear_hints(self) func tick() -> void: #appele chaque frame par les etats qui acceptent la visee (GroundedState, ThrowState pendant la charge) if Input.is_action_just_pressed(unlock_action): unlock() #deverrouillage manuel elif switch_hint and Input.is_action_just_pressed(switch_target_action): switch_target() update_target() update_reticle() func unlock() -> void: #deverrouillage manuel : pas de re-verrouillage automatique tant que la cible reste au centre auto_lock_blocked = true release_target() func update_reticle() -> void: #reticule libre au centre de l'ecran, seulement quand aucune cible n'est verrouillee (appele par tick(), et par ThrowState quand on charge un lancer sans viser) if is_instance_valid(player.aim_target): AimReticle.hide_instance() #cible verrouillee : son propre indicateur suffit else: AimReticle.show_instance(self) #---------- ciblage ---------- func update_target() -> void: var current : AimTarget = player.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(TARGET_GROUP): 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 player.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 = player.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( player.cam_holder.cam.global_position, t.global_position, line_of_sight_mask, [player.get_rid()]) var hit : Dictionary = player.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 = player.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 = player.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 player.aim_target == t: return release_target() player.aim_target = t player.cam_holder.set_lock_target(t) t.set_locked(true) func release_target() -> void: if !player: return if is_instance_valid(player.aim_target): player.aim_target.set_locked(false) player.aim_target = null player.cam_holder.set_lock_target(null)