extends PlayerComponent class_name ThrowResolver #resout un lancer : trajectoire de la balle, point d'atterrissage, envoi du pokemon et demande de combat. #ThrowState ne gere plus que le deroule (charge, windup, recuperation) et appelle throw_ball(). #composant a ajouter sous le PlayerCharacter (le nom du node est libre, voir PlayerComponent). #il vit aussi longtemps que le joueur : l'atterrissage est gere ici meme si le joueur a change d'etat entre-temps. @export_group("Ball") @export var ball_scene : PackedScene #modele de la balle (optionnel : sphere rouge par defaut) @export var hand_height : float = 1.4 @export var ball_speed : float = 18.0 @export var min_flight_time : float = 0.25 @export var max_flight_time : float = 1.2 @export var arc_height : float = 1.5 @export_group("Landing") @export_flags_3d_physics var ground_mask : int = 1 @export var land_distance_from_target : float = 1.5 #la balle s'arrete devant une cible de combat @export var free_hit_radius : float = 2.0 #lancer sans cible verrouillee : rayon autour du point d'atterrissage pour trouver une cible de combat func register_with_player() -> void: player.throw_resolver = self #---------- lancer ---------- func throw_ball(pokemon : PokemonInstance, target : Variant, free_aim_point : Vector3) -> void: #target : AimTarget verrouille au moment du relachement (ou null), free_aim_point : point vise s'il n'y en a pas var from : Vector3 = player.global_position + Vector3.UP * hand_height var to : Vector3 = _get_landing_point(target, free_aim_point) var flight_time : float = clampf(from.distance_to(to) / ball_speed, min_flight_time, max_flight_time) var scene : Node = get_tree().current_scene var ball : ThrownBall = ThrownBall.launch(scene, from, to, flight_time, arc_height, ball_scene) ball.landed.connect(_on_ball_landed.bind(pokemon, target, from)) #---------- point vise ---------- func compute_free_aim_point() -> Vector3: #point vise au centre de l'ecran : premier obstacle, sinon fin de portee ramenee au sol var cam : Camera3D = player.cam_holder.cam var origin : Vector3 = cam.global_position var far : Vector3 = origin - cam.global_basis.z * player.aim_targeting.throw_range var query := PhysicsRayQueryParameters3D.create(origin, far, ground_mask, [player.get_rid()]) var hit : Dictionary = player.get_world_3d().direct_space_state.intersect_ray(query) if hit.is_empty(): return _snap_to_ground(far) return _snap_to_ground(hit["position"] + hit["normal"] * 0.5) func _get_landing_point(target : Variant, free_aim_point : Vector3) -> Vector3: if !is_instance_valid(target): return free_aim_point var target_pos : Vector3 = target.global_position if _starts_battle(target): #la balle s'arrete devant la cible, du cote du joueur var to_player : Vector3 = player.global_position - target_pos to_player.y = 0.0 if to_player.length() > land_distance_from_target: target_pos += to_player.normalized() * land_distance_from_target return _snap_to_ground(target_pos) func _snap_to_ground(point : Vector3) -> Vector3: var query := PhysicsRayQueryParameters3D.create(point + Vector3.UP * 2.0, point + Vector3.DOWN * 6.0, ground_mask, [player.get_rid()]) var hit : Dictionary = player.get_world_3d().direct_space_state.intersect_ray(query) return hit["position"] if !hit.is_empty() else point #---------- atterrissage ---------- func _on_ball_landed(landing_pos : Vector3, pokemon : PokemonInstance, target : Variant, thrower_pos : Vector3) -> void: var scene : Node = get_tree().current_scene if is_inside_tree() else null if scene == null: return #lancer sans cible verrouillee : si la balle atterrit pres d'une cible de combat, c'est elle qui est visee if !is_instance_valid(target): target = _find_battle_target_near(landing_pos) var target_valid : bool = is_instance_valid(target) #le pokemon regarde la cible, ou dans la direction du lancer s'il n'y en a pas var look_pos : Vector3 = target.global_position if target_valid else landing_pos + (landing_pos - thrower_pos) var sent : SentPokemon = SentPokemon.send(scene, pokemon, landing_pos, look_pos) if target_valid and _starts_battle(target): GameData.battle_requested.emit(sent, target) elif target_valid: #diagnostic : retire ce message quand tout fonctionne push_warning("ThrowResolver : la cible '%s' ne lance pas de combat (starts_battle faux : Encounter vide, ou aim_target.gd pas remplace)." % target.name) func _find_battle_target_near(point : Vector3) -> AimTarget: #cible de combat active la plus proche du point d'atterrissage, dans free_hit_radius var best : AimTarget = null var best_dist : float = free_hit_radius for node in get_tree().get_nodes_in_group(AimTargeting.TARGET_GROUP): var t := node as AimTarget if t == null or !t.enabled or !_starts_battle(t): continue var dist : float = point.distance_to(t.global_position) if dist <= best_dist: best_dist = dist best = t return best func _starts_battle(target : Object) -> bool: #AimTarget doit avoir : @export var starts_battle : bool = false (get() renvoie null si la propriete n'existe pas) return target.get("starts_battle") == true