Allow riding on pokemons
This commit is contained in:
parent
59b0c403ea
commit
00bfe60c32
171
game/mount/mount_rider.gd
Normal file
171
game/mount/mount_rider.gd
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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
|
||||
|
||||
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_Y
|
||||
InputMap.action_add_event(dismount_action, pad)
|
||||
var key := InputEventKey.new()
|
||||
key.physical_keycode = KEY_F
|
||||
InputMap.action_add_event(dismount_action, key))
|
||||
|
||||
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:
|
||||
if Input.is_action_just_pressed(dismount_action) and _can_switch_now():
|
||||
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)
|
||||
|
||||
#---------- 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)
|
||||
|
||||
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
|
||||
|
||||
player.is_mounted = false
|
||||
player.exit_mount_stats()
|
||||
for state_name in blocked_states:
|
||||
state_machine.set_state_blocked(state_name, false)
|
||||
|
||||
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)
|
||||
1
game/mount/mount_rider.gd.uid
Normal file
1
game/mount/mount_rider.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4vkkdl0an244
|
||||
|
|
@ -15,6 +15,8 @@ var was_on_floor : bool = false
|
|||
var walk_or_run : String = "WalkState" #keep in memory if play char was walking or running before being in the air
|
||||
var is_dodging : bool = false #true while DodgeState is active ; hook this up to a hurtbox/i-frame check later
|
||||
var is_crouching : bool = false #true while CrouchState is active ; hook this up to a collision-shape resize later
|
||||
var is_mounted : bool = false #true tant que MountRider a le joueur sur un pokemon
|
||||
var _base_ground_stats : Dictionary = {}
|
||||
|
||||
@export_group("Walk variables")
|
||||
@export var walk_speed : float = 5.0
|
||||
|
|
@ -166,6 +168,45 @@ func gravity_apply(delta : float):
|
|||
if velocity.y >= 0.0: velocity.y -= jump_gravity * delta
|
||||
elif velocity.y < 0.0: velocity.y -= fall_gravity * delta
|
||||
|
||||
func apply_jump_height(height : float) -> void:
|
||||
#source unique des formules de saut (aussi utilisee pour les montures)
|
||||
jump_velocity = ((2.0 * height) / jump_time_to_peak) * -1.0
|
||||
jump_gravity = (2.0 * height) / (jump_time_to_peak * jump_time_to_peak)
|
||||
fall_gravity = (2.0 * height) / (jump_time_to_descent * jump_time_to_descent)
|
||||
|
||||
func enter_mount_stats(speed : float, accel : float, deccel : float, mount_jump_height : float) -> void:
|
||||
#mount_jump_height <= 0 : on garde le saut du joueur
|
||||
if _base_ground_stats.is_empty():
|
||||
_base_ground_stats = {
|
||||
"walk_speed": walk_speed, "walk_accel": walk_accel, "walk_deccel": walk_deccel,
|
||||
"run_speed": run_speed, "run_accel": run_accel, "run_deccel": run_deccel,
|
||||
}
|
||||
walk_speed = speed
|
||||
run_speed = speed
|
||||
walk_accel = accel
|
||||
run_accel = accel
|
||||
walk_deccel = deccel
|
||||
run_deccel = deccel
|
||||
if mount_jump_height > 0.0: apply_jump_height(mount_jump_height)
|
||||
#l'etat courant lit move_* sans repasser par enter()
|
||||
move_speed = speed
|
||||
move_accel = accel
|
||||
move_deccel = deccel
|
||||
|
||||
func exit_mount_stats() -> void:
|
||||
if _base_ground_stats.is_empty(): return
|
||||
walk_speed = _base_ground_stats["walk_speed"]
|
||||
walk_accel = _base_ground_stats["walk_accel"]
|
||||
walk_deccel = _base_ground_stats["walk_deccel"]
|
||||
run_speed = _base_ground_stats["run_speed"]
|
||||
run_accel = _base_ground_stats["run_accel"]
|
||||
run_deccel = _base_ground_stats["run_deccel"]
|
||||
_base_ground_stats.clear()
|
||||
apply_jump_height(jump_height) #jump_height n'est jamais modifie : on revient aux valeurs du joueur
|
||||
move_speed = walk_speed
|
||||
move_accel = walk_accel
|
||||
move_deccel = walk_deccel
|
||||
|
||||
func squash_and_strech(value : float, timing : float):
|
||||
#create a tween that simulate a compression of the model (squash and strech ones)
|
||||
#maily used to accentuate game feel/juice
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
[ext_resource type="Script" uid="uid://bwojkp7yrwswt" path="res://game/player/states/dodge_state.gd" id="9_6abew"]
|
||||
[ext_resource type="Script" uid="uid://d0577tj0c23bu" path="res://game/player/states/crouch_state.gd" id="10_mi63v"]
|
||||
[ext_resource type="Script" uid="uid://c820hnwuwpmlg" path="res://game/player/input/recall_input.gd" id="13_siyau"]
|
||||
[ext_resource type="Script" uid="uid://c4vkkdl0an244" path="res://game/mount/mount_rider.gd" id="14_dkijf"]
|
||||
|
||||
[sub_resource type="Curve" id="Curve_kxr4l"]
|
||||
_limits = [0.0, 10.0, 0.0, 1.0]
|
||||
|
|
@ -86,18 +87,6 @@ script = ExtResource("6_lwgph")
|
|||
|
||||
[node name="AimState" type="Node" parent="StateMachine" unique_id=296560188]
|
||||
script = ExtResource("7_kxr4l")
|
||||
aim_action = null
|
||||
switch_target_action = null
|
||||
unlock_action = null
|
||||
throw_action = null
|
||||
throw_range = null
|
||||
max_aim_angle = null
|
||||
auto_lock_angle = null
|
||||
lose_range_margin = null
|
||||
check_line_of_sight = null
|
||||
line_of_sight_mask = null
|
||||
aim_move_speed = null
|
||||
aim_move_accel = null
|
||||
|
||||
[node name="ThrowState" type="Node" parent="StateMachine" unique_id=1159570489]
|
||||
script = ExtResource("8_k0r6x")
|
||||
|
|
@ -116,3 +105,6 @@ script = ExtResource("2_r5dqo")
|
|||
|
||||
[node name="RecallInput" type="Node" parent="." unique_id=1544887696]
|
||||
script = ExtResource("13_siyau")
|
||||
|
||||
[node name="MountRider" type="Node" parent="." unique_id=639128028]
|
||||
script = ExtResource("14_dkijf")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const THROW_ENTRY_STATES : Array[String] = ["Idle", "Walk", "Run", "Crouch"] #et
|
|||
var curr_state : State
|
||||
var curr_state_name : String
|
||||
var states : Dictionary = {}
|
||||
var blocked_states : Dictionary = {} #noms (en minuscules) des etats dont l'entree est refusee
|
||||
|
||||
@onready var char_ref : CharacterBody3D = $".."
|
||||
|
||||
|
|
@ -62,6 +63,8 @@ func on_state_child_transition(state : State, new_state_name : String) -> void:
|
|||
var new_state = states.get(new_state_name.to_lower())
|
||||
if !new_state: return
|
||||
|
||||
if blocked_states.has(new_state_name.to_lower()): return
|
||||
|
||||
#exit the current state
|
||||
if curr_state: curr_state.exit()
|
||||
|
||||
|
|
@ -70,3 +73,7 @@ func on_state_child_transition(state : State, new_state_name : String) -> void:
|
|||
|
||||
curr_state = new_state
|
||||
curr_state_name = curr_state.state_name
|
||||
|
||||
func set_state_blocked(state_name : String, blocked : bool) -> void:
|
||||
if blocked: blocked_states[state_name.to_lower()] = true
|
||||
else: blocked_states.erase(state_name.to_lower())
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ signal recalled
|
|||
|
||||
const GROUP : StringName = &"sent_pokemon"
|
||||
const MODEL_FACES_PLUS_Z : bool = true #true si tes modeles regardent vers +Z (cas des glTF) : sinon mets false
|
||||
const HEIGHT_TO_METERS : float = 1.0 #PokeAPI donne la taille en decimetres ; mets 1.0 si ton import l'a deja convertie en metres
|
||||
const MIN_HEIGHT : float = 0.3 #bornes de la capsule en metres (augmente MAX_HEIGHT pour une echelle fidele aux tres gros pokemon)
|
||||
const MAX_HEIGHT : float = 8.0
|
||||
const DEFAULT_HEIGHT : float = 1.0 #utilise si la taille n'est pas renseignee (0)
|
||||
|
||||
static var _counter : int = 0 #ordre d'envoi, pour retrouver le plus recent
|
||||
|
||||
var pokemon : PokemonInstance
|
||||
var model : Node3D
|
||||
var order : int = 0
|
||||
var rider : Node3D = null
|
||||
|
||||
static func send(parent : Node, p : PokemonInstance, at : Vector3, look_at_pos : Vector3 = Vector3.INF) -> SentPokemon:
|
||||
#un pokemon deja sorti est d'abord rappele (il "se deplace" a l'endroit du nouveau lancer)
|
||||
|
|
@ -46,7 +51,7 @@ static func get_latest(tree : SceneTree) -> SentPokemon:
|
|||
#le pokemon sorti le plus recemment
|
||||
var latest : SentPokemon = null
|
||||
for n in tree.get_nodes_in_group(GROUP):
|
||||
if n is SentPokemon and !n.is_queued_for_deletion() and (latest == null or n.order > latest.order):
|
||||
if n is SentPokemon and !n.is_queued_for_deletion() and (latest == null or n.order > latest.order) and n.rider == null:
|
||||
latest = n
|
||||
return latest
|
||||
|
||||
|
|
@ -70,20 +75,25 @@ static func create_model(p : PokemonInstance) -> Node3D:
|
|||
static func _create_placeholder(p : PokemonInstance) -> Node3D:
|
||||
var colors : Array[Color] = TypeColors.for_species(p.species)
|
||||
|
||||
#taille issue des donnees de l'espece ; le rayon suit la hauteur pour garder des proportions coherentes
|
||||
var meters : float = p.species.height * HEIGHT_TO_METERS if p.species != null else 0.0
|
||||
var height : float = clampf(meters if meters > 0.0 else DEFAULT_HEIGHT, MIN_HEIGHT, MAX_HEIGHT)
|
||||
var radius : float = clampf(height * 0.3, 0.12, 1.5)
|
||||
|
||||
var body := MeshInstance3D.new()
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = 0.3
|
||||
capsule.height = 1.0
|
||||
capsule.radius = radius
|
||||
capsule.height = maxf(height, radius * 2.0) #une capsule ne peut pas etre plus basse que son diametre
|
||||
body.mesh = capsule
|
||||
body.position.y = 0.5
|
||||
body.position.y = capsule.height * 0.5 #les pieds posent sur le sol
|
||||
body.material_override = _make_material(colors[0])
|
||||
|
||||
#deuxieme type : anneau autour de la capsule
|
||||
if colors.size() > 1:
|
||||
var ring := MeshInstance3D.new()
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = 0.29
|
||||
torus.outer_radius = 0.36
|
||||
torus.inner_radius = radius * 0.97
|
||||
torus.outer_radius = radius * 1.2
|
||||
ring.mesh = torus
|
||||
ring.material_override = _make_material(colors[1])
|
||||
body.add_child(ring)
|
||||
|
|
|
|||
Loading…
Reference in a new issue