extends State class_name ThrowState var state_name : String = "Throw" var play_char : CharacterBody3D @export var aim_action : StringName = &"player_aim_camera" @export var throw_action : StringName = &"player_throw" #RT / clic gauche : maintenir pour charger, relacher pour lancer @export var cancel_action : StringName = &"player_throw_cancel" #B / clic droit (cree automatiquement s'il n'existe pas) @export_group("Charge") @export var charge_move_speed : float = 3.0 #vitesse de deplacement pendant que RT est maintenue @export var charge_move_accel : float = 8.0 @export_group("Timing") @export var windup_time : float = 0.25 #delai entre le relachement et le depart de la balle (a caler sur l'animation) @export var recovery_time : float = 0.35 #temps de recuperation apres le lancer @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 var land_distance_from_target : float = 1.5 #la balle s'arrete devant une cible de combat @export_group("Targeting") @export var throw_range : float = 20.0 @export_flags_3d_physics var ground_mask : int = 1 var _elapsed : float = 0.0 var _released : bool = false #true des que RT est relachee (phase windup/recovery) var _thrown : bool = false var _keep_aim : bool = false #true quand on retourne a AimState : on garde camera et verrouillage var _pokemon : PokemonInstance = null var _target : Variant = null #AimTarget verrouille au moment du relachement var _free_aim_point : Vector3 = Vector3.ZERO var _came_from_crouch : bool = false #true si le lancer vient du crouch (directement, ou via Aim declenche depuis le crouch) func _ready() -> void: _ensure_action(throw_action, func(): var trigger := InputEventJoypadMotion.new() trigger.axis = JOY_AXIS_TRIGGER_RIGHT trigger.axis_value = 1.0 InputMap.action_add_event(throw_action, trigger) var click := InputEventMouseButton.new() click.button_index = MOUSE_BUTTON_LEFT InputMap.action_add_event(throw_action, click)) _ensure_action(cancel_action, func(): var pad := InputEventJoypadButton.new() pad.button_index = JOY_BUTTON_B InputMap.action_add_event(cancel_action, pad) var click := InputEventMouseButton.new() click.button_index = MOUSE_BUTTON_RIGHT InputMap.action_add_event(cancel_action, click)) func _ensure_action(action : StringName, add_events : Callable) -> void: if InputMap.has_action(action): return InputMap.add_action(action) add_events.call() func enter(play_char_ref : CharacterBody3D) -> void: play_char = play_char_ref _elapsed = 0.0 _released = false _thrown = false _keep_aim = false #curr_state_name n'est pas encore mis a jour a ce stade : il reflete encore l'etat qu'on quitte var previous : String = play_char.state_machine.curr_state_name if previous == "Crouch": _came_from_crouch = true elif previous == "Aim": var aim : Node = play_char.state_machine.states.get("aimstate") _came_from_crouch = aim != null and aim.came_from_crouch else: _came_from_crouch = false _pokemon = GameData.get_selected_pokemon() _target = null _free_aim_point = Vector3.ZERO play_char.floor_snap_length = 1.0 #play_char.godot_plush_skin.set_state("throw") func exit() -> void: #retour a la visee : on garde tout tel quel ; sinon on libere camera et verrouillage (comme AimState) if _keep_aim: _keep_aim = false return AimReticle.hide_instance() _release_aim() func physics_update(delta : float) -> void: if applies(): return play_char.gravity_apply(delta) if _pokemon == null: slow_down(delta) finish() #rien a lancer (equipe vide ou tous K.O.) return #phase 1 : charge, tant que RT est maintenue if !_released: charge(delta) return #phase 2 : windup puis recovery slow_down(delta) if !_thrown and Input.is_action_just_pressed(cancel_action): finish() #annule avant que la balle parte return _elapsed += delta if !_thrown and _elapsed >= windup_time: throw_ball() if _elapsed >= windup_time + recovery_time: finish() func applies() -> bool: if !play_char.is_on_floor() and play_char.velocity.y < 0.0: transitioned.emit(self, "InairState") return true return false #---------- charge ---------- func charge(delta : float) -> void: if Input.is_action_just_pressed(cancel_action): finish() #annule : rien n'est lance return if !Input.is_action_pressed(throw_action): release() return move_while_charging(delta) update_reticle() func release() -> void: #RT relachee : on fige la cible et le point vise, puis le windup demarre _released = true _elapsed = 0.0 _target = play_char.aim_target if is_instance_valid(play_char.aim_target) else null _free_aim_point = _compute_free_aim_point() func move_while_charging(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 * charge_move_speed, charge_move_accel * delta) play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * charge_move_speed, charge_move_accel * delta) else: slow_down(delta) func slow_down(delta : float) -> void: 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) func finish() -> void: if Input.is_action_pressed(aim_action): _keep_aim = true #AimState ne verra que "Throw" comme etat precedent : on lui transmet l'origine crouch var aim : Node = play_char.state_machine.states.get("aimstate") if aim: aim.came_from_crouch = _came_from_crouch transitioned.emit(self, "AimState") return if _came_from_crouch: transitioned.emit(self, "CrouchState") return var dir : Vector2 = Input.get_vector(play_char.move_left_action, play_char.move_right_action, play_char.move_forward_action, play_char.move_backward_action) transitioned.emit(self, play_char.walk_or_run if dir != Vector2.ZERO else "IdleState") func update_reticle() -> void: if is_instance_valid(play_char.aim_target): AimReticle.hide_instance() #cible verrouillee : son propre indicateur suffit else: AimReticle.show_instance(self) #---------- lancer ---------- func throw_ball() -> void: _thrown = true AimReticle.hide_instance() var from : Vector3 = play_char.global_position + Vector3.UP * hand_height var to : Vector3 = _get_landing_point() var flight_time : float = clampf(from.distance_to(to) / ball_speed, min_flight_time, max_flight_time) var scene : Node = play_char.get_tree().current_scene var ball : ThrownBall = ThrownBall.launch(scene, from, to, flight_time, arc_height, ball_scene) #la balle est autonome : l'atterrissage est gere ici meme si le joueur a change d'etat entre-temps ball.landed.connect(_on_ball_landed.bind(_pokemon, _target, from)) func _get_landing_point() -> 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 = play_char.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 _compute_free_aim_point() -> Vector3: #point vise au centre de l'ecran : premier obstacle, sinon fin de portee ramenee au sol var cam : Camera3D = play_char.cam_holder.cam var origin : Vector3 = cam.global_position var far : Vector3 = origin - cam.global_basis.z * throw_range var query := PhysicsRayQueryParameters3D.create(origin, far, ground_mask, [play_char.get_rid()]) var hit : Dictionary = play_char.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 _snap_to_ground(point : Vector3) -> Vector3: var query := PhysicsRayQueryParameters3D.create(point + Vector3.UP * 2.0, point + Vector3.DOWN * 6.0, ground_mask, [play_char.get_rid()]) var hit : Dictionary = play_char.get_world_3d().direct_space_state.intersect_ray(query) return hit["position"] if !hit.is_empty() else point func _starts_battle(target : Object) -> bool: #AimTarget doit avoir : @export var starts_battle : bool = false return "starts_battle" in target and target.starts_battle 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 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("ThrowState : la cible '%s' ne lance pas de combat (starts_battle faux : Encounter vide, ou aim_target.gd pas remplace)." % target.name) #---------- nettoyage ---------- func _release_aim() -> void: #meme comportement que AimState.exit() / release_target() 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) play_char.cam_holder.cam_aimed = false