Tales-of-PKMN/game/mount/mount_rider.gd
2026-10-13 09:50:50 +02:00

207 lines
7.9 KiB
GDScript

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)