class_name MountRider extends Node #composant a ajouter en enfant de PlayerCharacter (a cote de InputManagementComponent / RecallInput). #gere la monte d'un SentPokemon dont l'espece a un MountProfile (voir MountRegistry) : # - s'approcher + appuyer sur mount_action (A) pour monter, dismount_action pour descendre # - le joueur garde ses etats Idle / Walk / Run / Jump / Aim / Throw # - Dodge et Crouch sont refuses par la StateMachine tant qu'on est monte # - vitesse, accel et saut sont remplaces par ceux du MountProfile signal mounted(sent : SentPokemon) signal dismounted(sent : SentPokemon) signal candidate_changed(sent : SentPokemon) #pokemon montable a portee (null = aucun) : pour afficher "A : Monter" dans le HUD #@export var mount_action : StringName = &"player_interact" #A (manette) / E (clavier) : cree automatiquement s'il n'existe pas #@export var dismount_action : StringName = &"player_dismount" #Y (manette) / F (clavier) : cree automatiquement s'il n'existe pas @export var mount_range : float = 2.5 #distance horizontale max entre le joueur et le pokemon @export var max_height_diff : float = 2.0 @export var blocked_states : PackedStringArray = ["DodgeState", "CrouchState"] #etats refuses pendant la monte @export var swallow_jump_on_mount : bool = true #si A sert aussi a sauter : l'appui qui monte ne declenche pas de saut @export var camera_follow_speed : float = 8.0 #vitesse a laquelle la camera monte a hauteur de selle @export_group("Hints") @export var mount_hint : Hint #mount.tres @export var dismount_hint : Hint #dismount.tres @export var jump_hint : Hint #jump.tres var mount_action : StringName: get: return mount_hint.action var dismount_action : StringName: get: return dismount_hint.action const GROUNDED_STATES : Array[String] = ["Idle", "Walk", "Run"] #monter / descendre n'est possible que depuis ces etats @onready var player : PlayerCharacter = get_parent() @onready var visual_root : Node3D = %VisualRoot @onready var cam_holder : Node3D = %CameraHolder @onready var state_machine : StateMachine = %StateMachine var mount : SentPokemon = null var profile : MountProfile = null var candidate : SentPokemon = null var _seat_applied : Vector3 = Vector3.ZERO var _cam_rise : float = 0.0 func _ready() -> void: process_physics_priority = -10 #avant la StateMachine : l'appui qui monte peut ainsi etre intercepte avant le saut _ensure_action(mount_action, func(): var pad := InputEventJoypadButton.new() pad.button_index = JOY_BUTTON_A InputMap.action_add_event(mount_action, pad) var key := InputEventKey.new() key.physical_keycode = KEY_E InputMap.action_add_event(mount_action, key)) _ensure_action(dismount_action, func(): var pad := InputEventJoypadButton.new() pad.button_index = JOY_BUTTON_X InputMap.action_add_event(dismount_action, pad) var key := InputEventKey.new() key.physical_keycode = KEY_F InputMap.action_add_event(dismount_action, key)) func _refresh_hints() -> void: #une seule source de verite pour les hints de la monture var list : Array[Hint] = [] if mount != null: if _can_switch_now(): list.append(dismount_hint) #pas de descente en plein vol list.append(jump_hint) elif candidate != null: list.append(mount_hint) Hints.show_hints(self, list) func _exit_tree() -> void: Hints.clear_hints(self) func _ensure_action(action : StringName, add_events : Callable) -> void: if InputMap.has_action(action): return InputMap.add_action(action) add_events.call() func is_mounted() -> bool: return mount != null func _physics_process(_delta : float) -> void: if mount != null: var can_dismount : bool = _can_switch_now() _refresh_hints() if Input.is_action_just_pressed(dismount_action) and can_dismount: dismount() return _set_candidate(_find_candidate() if _can_switch_now() else null) if candidate != null and Input.is_action_just_pressed(mount_action): mount_up(candidate) func _process(delta : float) -> void: if mount != null and !is_instance_valid(mount): dismount() #le pokemon a disparu sans passer par recall() var rise_target : float = 0.0 if mount != null: #la monture suit le joueur (position + orientation du modele) mount.global_position = player.global_position var yaw : float = visual_root.global_rotation.y if !SentPokemon.MODEL_FACES_PLUS_Z: yaw += PI mount.global_rotation = Vector3(0.0, yaw, 0.0) #le joueur est assis a la selle (seat_offset est exprime dans le repere de la monture) var seat : Vector3 = mount.global_basis * profile.seat_offset visual_root.position += seat - _seat_applied _seat_applied = seat rise_target = profile.seat_offset.y #la camera monte progressivement a hauteur de selle (en delta : ne gene pas les autres reglages de hauteur) var new_rise : float = lerpf(_cam_rise, rise_target, clampf(camera_follow_speed * delta, 0.0, 1.0)) if absf(new_rise - rise_target) < 0.001: new_rise = rise_target cam_holder.position.y += new_rise - _cam_rise _cam_rise = new_rise #---------- detection ---------- func _can_switch_now() -> bool: return player.is_on_floor() and state_machine.curr_state_name in GROUNDED_STATES func _find_candidate() -> SentPokemon: var best : SentPokemon = null var best_dist : float = mount_range for node in get_tree().get_nodes_in_group(SentPokemon.GROUP): var s := node as SentPokemon if s == null or s.is_queued_for_deletion() or s.rider != null: continue if !_is_rideable_here(s): continue var diff : Vector3 = s.global_position - player.global_position if absf(diff.y) > max_height_diff: continue var dist : float = Vector2(diff.x, diff.z).length() if dist <= best_dist: best_dist = dist best = s return best func _is_rideable_here(s : SentPokemon) -> bool: var p : MountProfile = MountRegistry.get_profile(s.pokemon.species) if p == null: return false #sur terre : une monture terrestre, ou volante (qui pourra decoller). Une monture aquatique seule reste a quai. return p.can_use_surface(MountProfile.Surface.GROUND) or p.can_use_surface(MountProfile.Surface.AIR) func _set_candidate(s : SentPokemon) -> void: if s == candidate: return candidate = s candidate_changed.emit(s) _refresh_hints() #---------- monter / descendre ---------- func mount_up(s : SentPokemon) -> void: if mount != null: return mount = s profile = MountRegistry.get_profile(s.pokemon.species) s.rider = player s.recalled.connect(_on_mount_recalled) _set_candidate(null) _refresh_hints() player.is_mounted = true player.enter_mount_stats(profile.mount_speed, profile.mount_accel, profile.mount_deccel, -1.0 if profile.can_use_surface(MountProfile.Surface.AIR) else profile.mount_jump_height) #monture volante : le vol libre remplacera le saut for state_name in blocked_states: state_machine.set_state_blocked(state_name, true) if swallow_jump_on_mount: _swallow_jump_this_frame() mounted.emit(s) func dismount() -> void: if mount == null and profile == null: return var s : SentPokemon = mount mount = null profile = null _refresh_hints() player.is_mounted = false player.exit_mount_stats() _release_blocked_states_next_frame() visual_root.position -= _seat_applied _seat_applied = Vector3.ZERO if is_instance_valid(s): s.rider = null if s.recalled.is_connected(_on_mount_recalled): s.recalled.disconnect(_on_mount_recalled) dismounted.emit(s) func _on_mount_recalled() -> void: dismount() #le pokemon monte vient d'etre rappele (ex : relance de ce meme pokemon depuis le HUD) func _swallow_jump_this_frame() -> void: state_machine.set_state_blocked("JumpState", true) await get_tree().physics_frame state_machine.set_state_blocked("JumpState", false) func _release_blocked_states_next_frame() -> void: #Dodge/Crouch restent refuses pendant la frame de la descente : si la touche de descente est aussi celle de la roulade, #le meme appui ne declenche pas de roulade une fois descendu await get_tree().physics_frame if mount != null: return #on est remonte entre-temps : les etats restent bloques for state_name in blocked_states: state_machine.set_state_blocked(state_name, false)