Compare commits
3 commits
349987f290
...
4275a88a49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4275a88a49 | ||
|
|
31c3e232f2 | ||
|
|
250976d170 |
|
|
@ -1,10 +0,0 @@
|
|||
[gd_resource type="Resource" script_class="MountProfile" format=3 uid="uid://did0vw42e8fex"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dsgbmr2jqjata" path="res://scripts/mount/mount_profile.gd" id="1_aoce8"]
|
||||
[ext_resource type="Resource" uid="uid://hogb1068ywfe" path="res://data/pokeapi/pokemon/0006_charizard.tres" id="2_onvr1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_aoce8")
|
||||
species = ExtResource("2_onvr1")
|
||||
surfaces = 5
|
||||
metadata/_custom_type_script = "uid://dsgbmr2jqjata"
|
||||
|
|
@ -86,12 +86,11 @@ func start_encounter(encounter : BattleEncounter, enemy_pos : Vector3, sent : Se
|
|||
await presenter.cleanup()
|
||||
ui.queue_free()
|
||||
|
||||
#retour au monde
|
||||
for node in hidden:
|
||||
if is_instance_valid(node): node.visible = true
|
||||
#retour au monde : tous les pokemon sortis rentrent dans leur balle ; l'adversaire du monde reapparait
|
||||
for node in get_tree().get_nodes_in_group(SentPokemon.GROUP):
|
||||
if node is SentPokemon and node.pokemon != null and node.pokemon.is_fainted():
|
||||
node.recall() #un pokemon K.O. est rappele
|
||||
if node is SentPokemon: node.recall()
|
||||
for node in hidden:
|
||||
if is_instance_valid(node) and !(node is SentPokemon): node.visible = true
|
||||
|
||||
if world_actor != null and is_instance_valid(world_actor):
|
||||
if outcome == Battle.Outcome.PLAYER_WON and encounter.remove_world_actor_on_win:
|
||||
|
|
|
|||
25
game/mount/mount_detector.gd
Normal file
25
game/mount/mount_detector.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
class_name MountDetector
|
||||
|
||||
#trouve le pokemon montable le plus proche du joueur. Pas de node ni d'etat : MountRider l'appelle avec ses reglages
|
||||
#(mount_range, max_height_diff), donc ils restent modifiables dans l'inspecteur, y compris en cours de partie.
|
||||
|
||||
static func find_candidate(player : PlayerCharacter, mount_range : float, max_height_diff : float) -> SentPokemon:
|
||||
var best : SentPokemon = null
|
||||
var best_dist : float = mount_range
|
||||
for node in player.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
|
||||
|
||||
static 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)
|
||||
1
game/mount/mount_detector.gd.uid
Normal file
1
game/mount/mount_detector.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dwfv5631jam2c
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
class_name MountRider
|
||||
extends Node
|
||||
extends PlayerComponent
|
||||
|
||||
#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
|
||||
# - Dodge est refuse par la StateMachine et le crouch est desactive (player.can_crouch) tant qu'on est monte
|
||||
# - vitesse, accel et saut sont remplaces par ceux du MountProfile
|
||||
#
|
||||
#le travail est reparti en trois classes :
|
||||
# - MountRider (ici) : cycle monter / descendre, etats bloques, hints, signaux
|
||||
# - MountDetector : trouve le pokemon montable le plus proche
|
||||
# - MountSeat : la monture suit le joueur, le joueur est assis a la selle, la camera monte
|
||||
#l'appui qui monte ou descend est consomme (InputManagementComponent.consume) : il ne declenche pas en plus un saut, une roulade ou un crouch.
|
||||
|
||||
signal mounted(sent : SentPokemon)
|
||||
signal dismounted(sent : SentPokemon)
|
||||
|
|
@ -16,7 +22,7 @@ signal candidate_changed(sent : SentPokemon) #pokemon montable a portee (null =
|
|||
#@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 blocked_states : PackedStringArray = [States.DODGE] #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
|
||||
|
||||
|
|
@ -30,36 +36,48 @@ var mount_action : StringName:
|
|||
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
|
||||
var _seat : MountSeat
|
||||
var _could_dismount : bool = false #derniere valeur de _can_switch_now() pendant la monte : les hints ne sont rafraichis que si elle change
|
||||
|
||||
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))
|
||||
process_physics_priority = -10 #avant la StateMachine : l'appui qui monte est ainsi consomme avant que les etats ne le lisent
|
||||
_seat = MountSeat.new(player, player.visual_root, player.cam_holder)
|
||||
InputRegistry.ensure(mount_action, [InputRegistry.pad_button(JOY_BUTTON_A), InputRegistry.key(KEY_E)])
|
||||
InputRegistry.ensure(dismount_action, [InputRegistry.pad_button(JOY_BUTTON_X), InputRegistry.key(KEY_F)])
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Hints.clear_hints(self)
|
||||
|
||||
func is_mounted() -> bool:
|
||||
return mount != null
|
||||
|
||||
func _physics_process(_delta : float) -> void:
|
||||
if mount != null:
|
||||
var can_dismount : bool = _can_switch_now()
|
||||
if can_dismount != _could_dismount: #le hint de descente apparait / disparait (pas de descente en plein vol)
|
||||
_could_dismount = can_dismount
|
||||
_refresh_hints()
|
||||
if Input.is_action_just_pressed(dismount_action) and can_dismount:
|
||||
dismount()
|
||||
return
|
||||
|
||||
_set_candidate(MountDetector.find_candidate(player, mount_range, max_height_diff) 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()
|
||||
_seat.update(delta, camera_follow_speed)
|
||||
|
||||
func _can_switch_now() -> bool:
|
||||
return player.is_on_floor() and !player.is_crouching and !player.is_aiming and player.state_machine.curr_state is GroundedState #ni accroupi ni en visant (monter / descendre : depuis le sol seulement)
|
||||
|
||||
#---------- hints ----------
|
||||
|
||||
func _refresh_hints() -> void:
|
||||
#une seule source de verite pour les hints de la monture
|
||||
|
|
@ -71,79 +89,6 @@ func _refresh_hints() -> void:
|
|||
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
|
||||
|
|
@ -158,49 +103,45 @@ func mount_up(s : SentPokemon) -> void:
|
|||
profile = MountRegistry.get_profile(s.pokemon.species)
|
||||
s.rider = player
|
||||
s.recalled.connect(_on_mount_recalled)
|
||||
_could_dismount = _can_switch_now()
|
||||
_set_candidate(null)
|
||||
_refresh_hints()
|
||||
_seat.attach(s, profile)
|
||||
|
||||
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()
|
||||
player.state_machine.set_state_blocked(state_name, true)
|
||||
player.can_crouch = false #pas de crouch a dos de pokemon (voir GroundedState)
|
||||
|
||||
#l'appui qui monte ne doit pas faire sauter (si A sert aussi a sauter) : on le consomme jusqu'a ce que le bouton soit relache
|
||||
if swallow_jump_on_mount: player.player_input.consume(player.player_input.jump_action)
|
||||
|
||||
mounted.emit(s)
|
||||
|
||||
func dismount() -> void:
|
||||
if mount == null and profile == null: return
|
||||
var s : SentPokemon = mount
|
||||
if mount == null: return
|
||||
var s : SentPokemon = mount if is_instance_valid(mount) else null #le pokemon a pu disparaitre sans passer par recall()
|
||||
mount = null
|
||||
profile = null
|
||||
_refresh_hints()
|
||||
_seat.detach()
|
||||
|
||||
player.is_mounted = false
|
||||
player.exit_mount_stats()
|
||||
_release_blocked_states_next_frame()
|
||||
|
||||
visual_root.position -= _seat_applied
|
||||
_seat_applied = Vector3.ZERO
|
||||
#plus rien n'est bloque : l'appui qui descend ne declenche ni roulade ni crouch (consomme jusqu'au relachement du bouton)
|
||||
for state_name in blocked_states:
|
||||
player.state_machine.set_state_blocked(state_name, false)
|
||||
player.can_crouch = true
|
||||
player.player_input.consume(player.player_input.dodge_action)
|
||||
player.player_input.consume(player.player_input.crouch_action)
|
||||
|
||||
if is_instance_valid(s):
|
||||
if s != null:
|
||||
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)
|
||||
|
|
|
|||
55
game/mount/mount_seat.gd
Normal file
55
game/mount/mount_seat.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
class_name MountSeat
|
||||
|
||||
#la selle : la monture suit le joueur (position + orientation du modele), le joueur est assis a la selle,
|
||||
#et la camera monte progressivement a hauteur de selle. Pas de node : MountRider le cree et l'appelle chaque frame.
|
||||
#
|
||||
#les decalages (selle, hauteur de camera) sont appliques "en delta" sur visual_root et cam_holder, pour ne pas
|
||||
#ecraser les autres reglages de position. Tout cela est regroupe ici : si un jour ces nodes recoivent un vrai
|
||||
#decalage dedie (ex : un node Seat), il n'y aura que ce fichier a changer.
|
||||
|
||||
var _player : PlayerCharacter
|
||||
var _visual_root : Node3D
|
||||
var _cam_holder : Node3D
|
||||
|
||||
var _mount : SentPokemon = null
|
||||
var _profile : MountProfile = null
|
||||
var _seat_applied : Vector3 = Vector3.ZERO #decalage de selle actuellement ajoute a visual_root.position
|
||||
var _cam_rise : float = 0.0 #hauteur actuellement ajoutee a cam_holder.position.y
|
||||
|
||||
func _init(player : PlayerCharacter, visual_root : Node3D, cam_holder : Node3D) -> void:
|
||||
_player = player
|
||||
_visual_root = visual_root
|
||||
_cam_holder = cam_holder
|
||||
|
||||
func attach(mount : SentPokemon, profile : MountProfile) -> void:
|
||||
_mount = mount
|
||||
_profile = profile
|
||||
|
||||
func detach() -> void:
|
||||
#le joueur retrouve sa position ; la camera redescend progressivement (update continue de tourner)
|
||||
_visual_root.position -= _seat_applied
|
||||
_seat_applied = Vector3.ZERO
|
||||
_mount = null
|
||||
_profile = null
|
||||
|
||||
func update(delta : float, camera_follow_speed : float) -> void:
|
||||
var rise_target : float = 0.0
|
||||
|
||||
if _mount != null and is_instance_valid(_mount):
|
||||
#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
|
||||
1
game/mount/mount_seat.gd.uid
Normal file
1
game/mount/mount_seat.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bjfm64yqkproy
|
||||
|
|
@ -3,7 +3,7 @@ extends CanvasLayer
|
|||
class_name AimReticle
|
||||
|
||||
#reticule 2D au centre de l'ecran (visee libre / charge du lancer)
|
||||
#cree a la volee, une seule instance partagee par AimState et ThrowState
|
||||
#cree a la volee, une seule instance partagee : affichee / masquee par AimTargeting.update_reticle() (appele par tick() et par ThrowState)
|
||||
|
||||
static var _instance : AimReticle
|
||||
|
||||
|
|
|
|||
179
game/player/aim/aim_targeting.gd
Normal file
179
game/player/aim/aim_targeting.gd
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
extends PlayerComponent
|
||||
|
||||
class_name AimTargeting
|
||||
|
||||
#la visee est un MODE superpose aux etats (comme la posture accroupie), plus un etat a part :
|
||||
# - elle demarre quand un etat au sol voit l'appui sur la visee (begin_aim) ou quand la visee est maintenue a la fin d'une roulade / d'un lancer
|
||||
# - elle dure tant que le bouton est maintenu : fin des qu'il est relache, ou en l'air. Pendant un lancer, elle attend la fin du lancer
|
||||
# - les etats continuent leur travail (marcher, rouler, lancer) : GroundedState ralentit le deplacement et bloque le saut
|
||||
#le composant gere aussi le ciblage : choix de la cible (auto-lock, changement, deverrouillage), camera de visee, reticule et hints.
|
||||
#GroundedState et ThrowState appellent tick() chaque frame tant que la visee est active.
|
||||
#composant a ajouter sous le PlayerCharacter (le nom du node est libre, voir PlayerComponent).
|
||||
|
||||
@export var throw_range : float = 20.0 #portee du lancer : sert aussi a ThrowResolver pour le point vise sans cible
|
||||
@export var max_aim_angle : float = 35.0 #angle max (deg) entre l'axe camera et une cible pour qu'elle soit candidate
|
||||
@export var auto_lock_angle : float = 12.0 #si une cible passe sous cet angle, on se verrouille dessus
|
||||
@export var lose_range_margin : float = 1.25 #marge de distance avant de perdre une cible deja verrouillee
|
||||
@export var check_line_of_sight : bool = true
|
||||
@export_flags_3d_physics var line_of_sight_mask : int = 1
|
||||
|
||||
@export_group("Movement")
|
||||
@export var aim_move_speed : float = 3.0 #vitesse de deplacement pendant la visee (remplace celle de la marche / course / crouch)
|
||||
@export var aim_move_accel : float = 8.0
|
||||
|
||||
@export_group("Input")
|
||||
@export var unlock_action : StringName = &"player_aim_unlock"
|
||||
|
||||
@export_group("Hints")
|
||||
@export var switch_hint : Hint #switch_target.tres
|
||||
|
||||
const TARGET_GROUP : String = "aim_targets" #groupe des AimTarget (aussi utilise par ThrowResolver)
|
||||
|
||||
var active : bool = false #true tant que la visee est en cours
|
||||
var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant que la cible est encore au centre
|
||||
|
||||
var switch_target_action : StringName: #l'action vient du Hint : une seule definition
|
||||
get: return switch_hint.action
|
||||
|
||||
func register_with_player() -> void:
|
||||
player.aim_targeting = self
|
||||
|
||||
func _ready() -> void:
|
||||
process_physics_priority = -5 #avant la StateMachine : les etats voient une visee deja a jour (fin de visee, en l'air...)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Hints.clear_hints(self)
|
||||
|
||||
#---------- mode visee ----------
|
||||
|
||||
func _physics_process(_delta : float) -> void:
|
||||
if !active: return
|
||||
var s : State = player.state_machine.curr_state
|
||||
if s is AirborneState:
|
||||
end_aim() #pas de visee en l'air : il faut reappuyer apres l'atterrissage
|
||||
elif !player.player_input.aim_held() and !(s is ThrowState):
|
||||
end_aim() #bouton relache (pendant un lancer, on attend la fin du lancer : la cible verrouillee compte)
|
||||
|
||||
func begin_aim() -> void:
|
||||
if active: return
|
||||
active = true
|
||||
auto_lock_blocked = false
|
||||
player.cam_holder.cam_aimed = true
|
||||
var list : Array[Hint] = []
|
||||
if switch_hint: list.append(switch_hint)
|
||||
Hints.show_hints(self, list)
|
||||
|
||||
func end_aim() -> void:
|
||||
if !active: return
|
||||
active = false
|
||||
release_target()
|
||||
player.cam_holder.cam_aimed = false
|
||||
AimReticle.hide_instance()
|
||||
Hints.clear_hints(self)
|
||||
|
||||
func tick() -> void:
|
||||
#appele chaque frame par les etats qui acceptent la visee (GroundedState, ThrowState pendant la charge)
|
||||
if Input.is_action_just_pressed(unlock_action):
|
||||
unlock() #deverrouillage manuel
|
||||
elif switch_hint and Input.is_action_just_pressed(switch_target_action):
|
||||
switch_target()
|
||||
update_target()
|
||||
update_reticle()
|
||||
|
||||
func unlock() -> void:
|
||||
#deverrouillage manuel : pas de re-verrouillage automatique tant que la cible reste au centre
|
||||
auto_lock_blocked = true
|
||||
release_target()
|
||||
|
||||
func update_reticle() -> void:
|
||||
#reticule libre au centre de l'ecran, seulement quand aucune cible n'est verrouillee (appele par tick(), et par ThrowState quand on charge un lancer sans viser)
|
||||
if is_instance_valid(player.aim_target):
|
||||
AimReticle.hide_instance() #cible verrouillee : son propre indicateur suffit
|
||||
else:
|
||||
AimReticle.show_instance(self)
|
||||
|
||||
#---------- ciblage ----------
|
||||
|
||||
func update_target() -> void:
|
||||
var current : AimTarget = player.aim_target
|
||||
|
||||
#verrouillage existant : on le garde tant que distance et ligne de vue sont valides
|
||||
#(l'angle avec la camera ne compte pas, le joueur peut deplacer la camera librement)
|
||||
if is_instance_valid(current):
|
||||
if is_target_valid(current, lose_range_margin, false): return
|
||||
release_target()
|
||||
|
||||
#visee libre : auto-lock sur la meilleure cible proche du centre de la camera
|
||||
var best : AimTarget = null
|
||||
var best_angle : float = auto_lock_angle
|
||||
for t in get_candidates():
|
||||
var angle : float = get_angle_to_camera(t) - t.priority
|
||||
if angle < best_angle:
|
||||
best_angle = angle
|
||||
best = t
|
||||
|
||||
if best == null:
|
||||
auto_lock_blocked = false #le joueur a ecarte la camera : l'auto-lock est de nouveau permis
|
||||
elif !auto_lock_blocked:
|
||||
set_target(best)
|
||||
|
||||
func get_candidates() -> Array[AimTarget]:
|
||||
var result : Array[AimTarget] = []
|
||||
for node in get_tree().get_nodes_in_group(TARGET_GROUP):
|
||||
var t := node as AimTarget
|
||||
if t and is_target_valid(t, 1.0, true): result.append(t)
|
||||
return result
|
||||
|
||||
func is_target_valid(t : AimTarget, range_mult : float, check_angle : bool) -> bool:
|
||||
if !t.enabled: return false
|
||||
if player.global_position.distance_to(t.global_position) > throw_range * range_mult: return false
|
||||
if check_angle and get_angle_to_camera(t) > max_aim_angle: return false
|
||||
if check_line_of_sight and !has_line_of_sight(t): return false
|
||||
return true
|
||||
|
||||
func get_angle_to_camera(t : AimTarget) -> float:
|
||||
var cam : Camera3D = player.cam_holder.cam
|
||||
var to_t : Vector3 = t.global_position - cam.global_position
|
||||
return rad_to_deg((-cam.global_basis.z).angle_to(to_t))
|
||||
|
||||
func has_line_of_sight(t : AimTarget) -> bool:
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
player.cam_holder.cam.global_position, t.global_position, line_of_sight_mask, [player.get_rid()])
|
||||
var hit : Dictionary = player.get_world_3d().direct_space_state.intersect_ray(query)
|
||||
#pas d'obstacle, ou l'obstacle est le proprietaire de la cible lui-meme
|
||||
return hit.is_empty() or hit["collider"].is_ancestor_of(t)
|
||||
|
||||
func switch_target() -> void:
|
||||
#passe a la cible suivante, triee de gauche a droite par rapport a la camera (boucle)
|
||||
var cam : Camera3D = player.cam_holder.cam
|
||||
var forward : Vector3 = -cam.global_basis.z
|
||||
var list : Array[AimTarget] = get_candidates()
|
||||
|
||||
#la cible actuelle reste dans la liste meme si la camera est decalee par rapport a elle
|
||||
var current : AimTarget = player.aim_target
|
||||
if is_instance_valid(current) and !list.has(current): list.append(current)
|
||||
if list.size() < 2: return
|
||||
|
||||
list.sort_custom(func(a : AimTarget, b : AimTarget) -> bool:
|
||||
return signed_angle(forward, a, cam) < signed_angle(forward, b, cam))
|
||||
|
||||
var idx : int = list.find(current)
|
||||
idx = 0 if idx == -1 else (idx + 1) % list.size()
|
||||
set_target(list[idx])
|
||||
|
||||
func signed_angle(forward : Vector3, t : AimTarget, cam : Camera3D) -> float:
|
||||
var to_t : Vector3 = t.global_position - cam.global_position
|
||||
return Vector2(forward.x, forward.z).angle_to(Vector2(to_t.x, to_t.z))
|
||||
|
||||
func set_target(t : AimTarget) -> void:
|
||||
if player.aim_target == t: return
|
||||
release_target()
|
||||
player.aim_target = t
|
||||
player.cam_holder.set_lock_target(t)
|
||||
t.set_locked(true)
|
||||
|
||||
func release_target() -> void:
|
||||
if !player: return
|
||||
if is_instance_valid(player.aim_target): player.aim_target.set_locked(false)
|
||||
player.aim_target = null
|
||||
player.cam_holder.set_lock_target(null)
|
||||
1
game/player/aim/aim_targeting.gd.uid
Normal file
1
game/player/aim/aim_targeting.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://qh738m0cmw8b
|
||||
103
game/player/aim/throw_resolver.gd
Normal file
103
game/player/aim/throw_resolver.gd
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
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
|
||||
1
game/player/aim/throw_resolver.gd.uid
Normal file
1
game/player/aim/throw_resolver.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bxt8qv1xyw2nl
|
||||
|
|
@ -31,20 +31,18 @@ var cam_distance_mult : float = 1.0 #multiplicateur actuel (interpole)
|
|||
@export var lock_on_speed : float = 8.0
|
||||
var lock_target : Node3D = null
|
||||
|
||||
#keybinding variables
|
||||
var aim_cam_action : StringName
|
||||
var cam_zoom_in_action : StringName
|
||||
var cam_zoom_out_action : StringName
|
||||
var cam_look_up_action : StringName
|
||||
var cam_look_down_action : StringName
|
||||
var cam_look_left_action : StringName
|
||||
var cam_look_right_action : StringName
|
||||
#input : renseigne par InputManagementComponent, le CameraHolder ne connait aucun nom d'action
|
||||
var player_input : InputManagementComponent
|
||||
|
||||
#reference variables
|
||||
@onready var spring_arm : SpringArm3D = %SpringArm3D
|
||||
@onready var cam : Camera3D = %Camera3D
|
||||
@export_group("References")
|
||||
@export var spring_arm : SpringArm3D
|
||||
@export var cam : Camera3D
|
||||
|
||||
func _ready() -> void:
|
||||
if spring_arm == null or cam == null:
|
||||
push_error("CameraHolder : assigne spring_arm et cam dans l'inspecteur.")
|
||||
return
|
||||
#set fov, capture mouse cursor, and enable camera
|
||||
cam.fov = fov_val
|
||||
#Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
|
@ -58,10 +56,6 @@ func set_active(state : bool) -> void:
|
|||
set_process(active)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
#change cam mode (default, aim)
|
||||
#if event.is_action_pressed(aim_cam_action):
|
||||
#cam_aimed = !cam_aimed
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
if has_lock_target(): return
|
||||
var viewport_transform: Transform2D = get_tree().root.get_final_transform()
|
||||
|
|
@ -92,8 +86,7 @@ func _process(delta: float) -> void:
|
|||
if has_lock_target():
|
||||
lock_on_handling(delta)
|
||||
else:
|
||||
var camera_motion = Input.get_vector(cam_look_left_action, cam_look_right_action, cam_look_up_action, cam_look_down_action)
|
||||
rotate_from_vector(camera_motion * joystick_sensibility)
|
||||
if player_input: rotate_from_vector(player_input.read_look_vector() * joystick_sensibility)
|
||||
|
||||
#handle zoom
|
||||
zoom_handling(delta)
|
||||
|
|
@ -108,7 +101,7 @@ func rotate_from_vector(vector : Vector2) -> void:
|
|||
|
||||
func zoom_handling(delta : float) -> void:
|
||||
#zoom in/out cam, and clamp zoom value between min and max zoom values
|
||||
spring_arm.spring_length += Input.get_axis(cam_zoom_in_action, cam_zoom_out_action) * zoom_speed * delta
|
||||
if player_input: spring_arm.spring_length += player_input.read_zoom_axis() * zoom_speed * delta
|
||||
spring_arm.spring_length = clamp(spring_arm.spring_length, min_spring_length, max_spring_length)
|
||||
|
||||
func switch_aim_side() -> void:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
[ext_resource type="Script" uid="uid://ba5gqdhohvdin" path="res://game/player/camera/camera_holder.gd" id="1_bvor0"]
|
||||
|
||||
[node name="CameraHolder" type="Node3D" unique_id=232451612]
|
||||
[node name="CameraHolder" type="Node3D" unique_id=232451612 node_paths=PackedStringArray("spring_arm", "cam")]
|
||||
script = ExtResource("1_bvor0")
|
||||
spring_arm = NodePath("SpringArm3D")
|
||||
cam = NodePath("SpringArm3D/Camera3D")
|
||||
|
||||
[node name="SpringArm3D" type="SpringArm3D" parent="." unique_id=1346420998]
|
||||
unique_name_in_owner = true
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
extends Node
|
||||
extends PlayerComponent
|
||||
|
||||
class_name InputManagementComponent
|
||||
|
||||
#instead of setting up keybinds in 3 differents scripts, it's centralized in this one only
|
||||
#
|
||||
#point d'entree unique de l'input du joueur :
|
||||
# - declare les noms des actions (exports ci-dessous) : c'est la seule definition, ni PlayerCharacter ni CameraHolder n'en gardent de copie
|
||||
# (le CameraHolder lit la camera par read_look_vector / read_zoom_axis)
|
||||
# - fournit les lectures utilisees par les etats (read_move_dir, wants_jump...) : plus aucun etat n'appelle Input directement
|
||||
# sauf pour les actions portees par un Hint (cancel, unlock, switch target, mount, dismount), lues par leur proprietaire
|
||||
|
||||
@export_group("Keybind variables")
|
||||
@export_subgroup("Player character variables")
|
||||
|
|
@ -14,6 +20,7 @@ class_name InputManagementComponent
|
|||
@export var jump_action : StringName = "player_jump"
|
||||
@export var dodge_action : StringName = "player_dodge"
|
||||
@export var crouch_action : StringName = "player_crouch"
|
||||
@export var throw_action : StringName = &"player_throw" #RT / clic gauche : maintenir pour charger, relacher pour lancer
|
||||
@export_subgroup("Camera variables")
|
||||
@export var aim_cam_action : StringName = "player_aim_camera"
|
||||
@export var cam_zoom_in_action : StringName = "player_zoom_camera_in"
|
||||
|
|
@ -23,29 +30,92 @@ class_name InputManagementComponent
|
|||
@export var cam_look_left_action : StringName = "player_look_left"
|
||||
@export var cam_look_right_action : StringName = "player_look_right"
|
||||
|
||||
#references variables
|
||||
@onready var play_char_ref : PlayerCharacter = $".."
|
||||
@onready var cam_holder_ref : CameraHolder = %CameraHolder
|
||||
func register_with_player() -> void:
|
||||
player.player_input = self
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
attribute_keybinds()
|
||||
player.cam_holder.player_input = self #le CameraHolder lit la camera ici, il ne connait aucun nom d'action
|
||||
InputRegistry.ensure(throw_action, [InputRegistry.pad_axis(JOY_AXIS_TRIGGER_RIGHT, 1.0), InputRegistry.mouse(MOUSE_BUTTON_LEFT)])
|
||||
|
||||
func attribute_keybinds() -> void:
|
||||
play_char_ref.move_forward_action = move_forward_action
|
||||
play_char_ref.move_backward_action = move_backward_action
|
||||
play_char_ref.move_left_action = move_left_action
|
||||
play_char_ref.move_right_action = move_right_action
|
||||
play_char_ref.run_action = run_action
|
||||
play_char_ref.jump_action = jump_action
|
||||
play_char_ref.dodge_action = dodge_action
|
||||
play_char_ref.crouch_action = crouch_action
|
||||
play_char_ref.aim_cam_action = aim_cam_action
|
||||
#---------- camera ----------
|
||||
|
||||
cam_holder_ref.aim_cam_action = aim_cam_action
|
||||
cam_holder_ref.cam_zoom_in_action = cam_zoom_in_action
|
||||
cam_holder_ref.cam_zoom_out_action = cam_zoom_out_action
|
||||
cam_holder_ref.cam_look_up_action = cam_look_up_action
|
||||
cam_holder_ref.cam_look_down_action = cam_look_down_action
|
||||
cam_holder_ref.cam_look_left_action = cam_look_left_action
|
||||
cam_holder_ref.cam_look_right_action = cam_look_right_action
|
||||
func read_look_vector() -> Vector2:
|
||||
#rotation de la camera a la manette / aux touches (la souris est geree par le CameraHolder lui-meme)
|
||||
return Input.get_vector(cam_look_left_action, cam_look_right_action, cam_look_up_action, cam_look_down_action)
|
||||
|
||||
func read_zoom_axis() -> float:
|
||||
return Input.get_axis(cam_zoom_in_action, cam_zoom_out_action)
|
||||
|
||||
#---------- deplacement ----------
|
||||
|
||||
func read_move_dir() -> Vector2:
|
||||
#direction voulue, relative a la camera
|
||||
return read_move_raw().rotated(-player.cam_holder.global_rotation.y)
|
||||
|
||||
func read_move_raw() -> Vector2:
|
||||
#direction brute du stick / des touches, sans tenir compte de la camera
|
||||
return Input.get_vector(move_left_action, move_right_action, move_forward_action, move_backward_action)
|
||||
|
||||
#---------- input consomme ----------
|
||||
|
||||
#un appui consomme est ignore par tous les getters ci-dessous jusqu'a ce que le bouton soit relache.
|
||||
#sert quand un meme bouton a deux roles (ex : A monte sur un pokemon ET fait sauter) : celui qui agit en premier consomme l'appui
|
||||
#(avant : MountRider bloquait des etats pendant une frame avec un await). Les composants qui consomment doivent s'executer
|
||||
#avant la StateMachine (process_physics_priority plus petit, comme MountRider).
|
||||
|
||||
var _consumed : Dictionary = {} #action -> true
|
||||
|
||||
func consume(action : StringName) -> void:
|
||||
if Input.is_action_pressed(action): _consumed[action] = true #rien a consommer si le bouton n'est pas enfonce
|
||||
|
||||
func is_consumed(action : StringName) -> bool:
|
||||
if !_consumed.has(action): return false
|
||||
if Input.is_action_pressed(action): return true
|
||||
_consumed.erase(action) #bouton relache : l'action redevient disponible
|
||||
return false
|
||||
|
||||
func _just_pressed(action : StringName) -> bool:
|
||||
return !is_consumed(action) and Input.is_action_just_pressed(action)
|
||||
|
||||
func _held(action : StringName) -> bool:
|
||||
return !is_consumed(action) and Input.is_action_pressed(action)
|
||||
|
||||
#---------- actions au sol ----------
|
||||
|
||||
func wants_dodge() -> bool:
|
||||
return _just_pressed(dodge_action) and player.dodge_cooldown <= 0.0
|
||||
|
||||
func wants_jump() -> bool:
|
||||
#depart d'un saut depuis le sol : tient compte de auto_jump (rester appuye)
|
||||
if player.auto_jump: return _held(jump_action)
|
||||
return _just_pressed(jump_action)
|
||||
|
||||
func jump_pressed() -> bool:
|
||||
return _just_pressed(jump_action)
|
||||
|
||||
func jump_released() -> bool:
|
||||
return Input.is_action_just_released(jump_action)
|
||||
|
||||
func crouch_pressed() -> bool:
|
||||
return _just_pressed(crouch_action)
|
||||
|
||||
func run_pressed() -> bool:
|
||||
return _held(run_action)
|
||||
|
||||
func run_just_pressed() -> bool:
|
||||
return _just_pressed(run_action)
|
||||
|
||||
#---------- visee et lancer ----------
|
||||
|
||||
func aim_pressed() -> bool:
|
||||
return _just_pressed(aim_cam_action)
|
||||
|
||||
func aim_held() -> bool:
|
||||
return _held(aim_cam_action)
|
||||
|
||||
func throw_pressed() -> bool:
|
||||
return _just_pressed(throw_action)
|
||||
|
||||
func throw_held() -> bool:
|
||||
return _held(throw_action)
|
||||
34
game/player/input/input_registry.gd
Normal file
34
game/player/input/input_registry.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
class_name InputRegistry
|
||||
|
||||
#declaration d'actions dans l'InputMap depuis le code : une seule implementation (avant : _ensure_action copie dans ThrowState et MountRider,
|
||||
#et des blocs identiques dans AimState/ThrowState pour throw_action).
|
||||
#ne fait rien si l'action existe deja (par exemple definie dans Projet > Parametres du projet > Map des entrees)
|
||||
|
||||
static func ensure(action : StringName, events : Array) -> void:
|
||||
if InputMap.has_action(action): return
|
||||
InputMap.add_action(action)
|
||||
for event in events:
|
||||
InputMap.action_add_event(action, event)
|
||||
|
||||
#---------- fabriques d'evenements ----------
|
||||
|
||||
static func pad_button(button : JoyButton) -> InputEventJoypadButton:
|
||||
var event := InputEventJoypadButton.new()
|
||||
event.button_index = button
|
||||
return event
|
||||
|
||||
static func pad_axis(axis : JoyAxis, value : float) -> InputEventJoypadMotion:
|
||||
var event := InputEventJoypadMotion.new()
|
||||
event.axis = axis
|
||||
event.axis_value = value
|
||||
return event
|
||||
|
||||
static func mouse(button : MouseButton) -> InputEventMouseButton:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = button
|
||||
return event
|
||||
|
||||
static func key(keycode : Key) -> InputEventKey:
|
||||
var event := InputEventKey.new()
|
||||
event.physical_keycode = keycode
|
||||
return event
|
||||
1
game/player/input/input_registry.gd.uid
Normal file
1
game/player/input/input_registry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dj1gn8g0eblyi
|
||||
19
game/player/input/pokemon_recall.gd
Normal file
19
game/player/input/pokemon_recall.gd
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class_name PokemonRecall
|
||||
|
||||
#regle unique du rappel, partagee par StateMachine, AimState et RecallInput (avant : 3 copies qui pouvaient se contredire) :
|
||||
# - un appui sur RT rappelle le pokemon SELECTIONNE s'il est dehors
|
||||
# - sinon, c'est un lancer (meme si un autre pokemon est deja sorti)
|
||||
# - aucun pokemon selectionne : rien ne se passe
|
||||
|
||||
static func find_selected_out(tree : SceneTree) -> SentPokemon:
|
||||
#le SentPokemon du pokemon selectionne s'il est actuellement dehors, sinon null
|
||||
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
||||
if selected == null: return null
|
||||
return SentPokemon.find_for(tree, selected)
|
||||
|
||||
static func recall_selected(tree : SceneTree) -> bool:
|
||||
#rappelle le pokemon selectionne s'il est dehors. true si un rappel a eu lieu
|
||||
var sent : SentPokemon = find_selected_out(tree)
|
||||
if sent == null: return false
|
||||
sent.recall()
|
||||
return true
|
||||
1
game/player/input/pokemon_recall.gd.uid
Normal file
1
game/player/input/pokemon_recall.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwbji81rt6t36
|
||||
|
|
@ -1,26 +1,17 @@
|
|||
class_name RecallInput
|
||||
extends Node
|
||||
extends PlayerComponent
|
||||
|
||||
#rappel d'un pokemon hors visee : un appui sur RT / clic gauche rappelle le pokemon sorti.
|
||||
#- le pokemon selectionne (HUD) s'il est dehors, sinon le dernier sorti
|
||||
#- pendant la visee et le lancer, c'est AimState / ThrowState qui gerent cet appui
|
||||
#rappel du pokemon selectionne hors visee : un appui sur RT / clic gauche le rappelle s'il est dehors.
|
||||
#- seul le pokemon selectionne (HUD) est rappele : s'il n'est pas dehors, l'appui lance (voir StateMachine.try_start_throw)
|
||||
#- pendant un lancer (ThrowState), c'est lui qui gere cet appui ; la visee n'est plus un etat, elle ne change rien ici
|
||||
#- la regle elle-meme est dans PokemonRecall
|
||||
#
|
||||
#usage : ajouter ce node comme enfant de ton PlayerCharacter (il le detecte via son parent)
|
||||
#usage : ajouter ce node sous ton PlayerCharacter (il le retrouve tout seul, voir PlayerComponent)
|
||||
|
||||
@export var player : PlayerCharacter
|
||||
@export var throw_action : StringName = &"player_throw"
|
||||
@export var ignored_states : Array[String] = ["Aim", "Throw"] #valeurs de state_name
|
||||
|
||||
func _ready() -> void:
|
||||
if player == null: player = get_parent() as PlayerCharacter
|
||||
@export var ignored_states : Array[String] = [States.THROW] #noms de nodes d'etats (voir States)
|
||||
|
||||
func _physics_process(_delta : float) -> void:
|
||||
if player == null or !InputMap.has_action(throw_action): return
|
||||
if !Input.is_action_just_pressed(throw_action): return
|
||||
if player == null or !player.player_input.throw_pressed(): return
|
||||
if player.state_machine.curr_state_name in ignored_states: return
|
||||
|
||||
var target : SentPokemon = null
|
||||
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
||||
if selected: target = SentPokemon.find_for(get_tree(), selected)
|
||||
if target == null: target = SentPokemon.get_latest(get_tree())
|
||||
if target: target.recall()
|
||||
PokemonRecall.recall_selected(get_tree())
|
||||
30
game/player/input/state_hints.gd
Normal file
30
game/player/input/state_hints.gd
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
extends PlayerComponent
|
||||
|
||||
class_name StateHints
|
||||
|
||||
#affiche dans le HUD les hints de l'etat courant du joueur (PlayerState.get_hints). La StateMachine, generique, ne connait pas le HUD :
|
||||
#ce composant ecoute ses changements d'etat et les hints_changed des etats.
|
||||
#composant a ajouter sous le PlayerCharacter (le nom du node est libre, voir PlayerComponent).
|
||||
|
||||
func register_with_player() -> void:
|
||||
player.state_hints = self
|
||||
|
||||
func _ready() -> void:
|
||||
var machine : StateMachine = player.state_machine
|
||||
machine.state_changed.connect(_on_state_changed)
|
||||
for child in machine.get_children():
|
||||
if child is PlayerState: child.hints_changed.connect(_on_hints_changed.bind(child))
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Hints.clear_hints(self)
|
||||
|
||||
func _on_state_changed(_from : State, _to : State) -> void:
|
||||
publish()
|
||||
|
||||
func _on_hints_changed(state : PlayerState) -> void:
|
||||
if state == player.state_machine.curr_state: publish()
|
||||
|
||||
func publish() -> void:
|
||||
var state := player.state_machine.curr_state as PlayerState
|
||||
if state and !state.keep_previous_hints:
|
||||
Hints.show_hints(self, state.get_hints())
|
||||
1
game/player/input/state_hints.gd.uid
Normal file
1
game/player/input/state_hints.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b0j1jxv6uu2y8
|
||||
|
|
@ -12,9 +12,12 @@ var last_input_dir : Vector2
|
|||
var last_frame_position : Vector3
|
||||
var last_frame_velocity : Vector3
|
||||
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
|
||||
enum Locomotion { WALK, RUN }
|
||||
var locomotion : Locomotion = Locomotion.WALK #keep in memory if play char was walking or running before being in the air
|
||||
var walk_or_run : String: #ANCIEN NOM, lecture seule : a supprimer des que plus aucun script ne l'utilise (chercher "walk_or_run" dans le projet)
|
||||
get: return get_locomotion_state()
|
||||
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_crouching : bool = false #posture accroupie (voir GroundedState.toggle_crouch) ; 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 = {}
|
||||
|
||||
|
|
@ -67,32 +70,32 @@ var coyote_jump_on : bool = false
|
|||
@onready var jump_gravity : float = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
|
||||
@onready var fall_gravity : float = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0
|
||||
|
||||
#keybinding variables
|
||||
var move_forward_action : StringName
|
||||
var move_backward_action : StringName
|
||||
var move_left_action : StringName
|
||||
var move_right_action : StringName
|
||||
var run_action : StringName
|
||||
var jump_action : StringName
|
||||
var dodge_action : StringName
|
||||
var crouch_action : StringName
|
||||
var aim_cam_action : StringName
|
||||
|
||||
@export_group("Model variables")
|
||||
@export var model_rot_speed : float = 6.5
|
||||
@export var follow_cam_pos_when_aimed : bool = false
|
||||
|
||||
#references variables
|
||||
@onready var visual_root = %VisualRoot
|
||||
#references variables : rien n'est cherche par son nom
|
||||
#- nodes de la scene : exports a assigner dans l'inspecteur (ils suivent un renommage ou un deplacement du node)
|
||||
@export_group("References")
|
||||
@export var visual_root : Node3D
|
||||
#@onready var godot_plush_skin : GodotPlushSkin = %GodotPlushSkin
|
||||
#@onready var particles_manager : ParticlesManager = %ParticlesManager
|
||||
@onready var cam_holder: CameraHolder = %CameraHolder
|
||||
@onready var state_machine : StateMachine = %StateMachine
|
||||
@export var cam_holder : CameraHolder
|
||||
@export var state_machine : StateMachine
|
||||
#@onready var foot_step_audio = %FootStepAudio
|
||||
#@onready var impact_audio = %ImpactAudio
|
||||
#@onready var wave_audio = %WaveAudio
|
||||
@onready var collision_shape_3d = %CollisionShape3D
|
||||
@export var collision_shape_3d : CollisionShape3D
|
||||
#@onready var floor_check : RayCast3D = %FloorRaycast
|
||||
#- composants : ils s'enregistrent eux-memes (voir PlayerComponent), le joueur ne connait pas leur nom de node
|
||||
var player_input : InputManagementComponent
|
||||
var aim_targeting : AimTargeting #visee : mode superpose aux etats + ciblage (voir AimTargeting)
|
||||
var throw_resolver : ThrowResolver #trajectoire de la balle, atterrissage, envoi du pokemon, combat
|
||||
var state_hints : StateHints #affiche les hints de l'etat courant dans le HUD
|
||||
|
||||
var can_crouch : bool = true
|
||||
var is_aiming : bool: #visee en cours (mode superpose aux etats, voir AimTargeting)
|
||||
get: return aim_targeting != null and aim_targeting.active
|
||||
|
||||
#particles variables
|
||||
#@onready var movement_dust = %MovementDust
|
||||
|
|
@ -102,6 +105,8 @@ var aim_cam_action : StringName
|
|||
var aim_target : AimTarget = null
|
||||
|
||||
func _ready():
|
||||
_validate_wiring()
|
||||
|
||||
#set move variables, and value references
|
||||
move_speed = walk_speed
|
||||
move_accel = walk_accel
|
||||
|
|
@ -114,6 +119,20 @@ func _ready():
|
|||
|
||||
#connect_footstep_audio_effect()
|
||||
|
||||
func _validate_wiring() -> void:
|
||||
#un seul endroit qui dit ce qui manque, au lieu d'une erreur obscure sur un null plus tard
|
||||
var missing : Array[String] = []
|
||||
if visual_root == null: missing.append("visual_root (export)")
|
||||
if cam_holder == null: missing.append("cam_holder (export)")
|
||||
if state_machine == null: missing.append("state_machine (export)")
|
||||
if collision_shape_3d == null: missing.append("collision_shape_3d (export)")
|
||||
if player_input == null: missing.append("un InputManagementComponent sous le joueur")
|
||||
if aim_targeting == null: missing.append("un AimTargeting sous le joueur")
|
||||
if throw_resolver == null: missing.append("un ThrowResolver sous le joueur")
|
||||
if state_hints == null: missing.append("un StateHints sous le joueur")
|
||||
if !missing.is_empty():
|
||||
push_error("PlayerCharacter : dependances manquantes : " + ", ".join(PackedStringArray(missing)))
|
||||
|
||||
func _process(delta: float):
|
||||
modify_model_orientation(delta)
|
||||
|
||||
|
|
@ -225,7 +244,14 @@ func squash_and_strech(value : float, timing : float):
|
|||
|
||||
func update_run_toggle() -> void:
|
||||
if continious_run:
|
||||
if Input.is_action_just_pressed(run_action):
|
||||
walk_or_run = "WalkState" if walk_or_run == "RunState" else "RunState"
|
||||
if player_input.run_just_pressed():
|
||||
locomotion = Locomotion.WALK if is_running() else Locomotion.RUN
|
||||
else:
|
||||
walk_or_run = "RunState" if Input.is_action_pressed(run_action) else "WalkState"
|
||||
locomotion = Locomotion.RUN if player_input.run_pressed() else Locomotion.WALK
|
||||
|
||||
func is_running() -> bool:
|
||||
return locomotion == Locomotion.RUN
|
||||
|
||||
func get_locomotion_state() -> String:
|
||||
#etat au sol correspondant a la derniere allure (voir State.request_locomotion)
|
||||
return States.RUN if is_running() else States.WALK
|
||||
|
|
|
|||
|
|
@ -7,20 +7,21 @@
|
|||
[ext_resource type="Script" uid="uid://dy4wrpvpep1jq" path="res://game/player/states/state_machine.gd" id="3_o4naa"]
|
||||
[ext_resource type="Script" uid="uid://bf6hjwfa74ttj" path="res://game/player/states/idle_state.gd" id="4_38lxl"]
|
||||
[ext_resource type="Script" uid="uid://2agliol5dulc" path="res://game/player/states/walk_state.gd" id="4_lg056"]
|
||||
[ext_resource type="Script" uid="uid://b1rvj44fyaea4" path="res://game/ui/hints/hint.gd" id="6_7qhxe"]
|
||||
[ext_resource type="Script" uid="uid://dr5p8ygj25w6x" path="res://game/player/states/jump_state.gd" id="6_lwgph"]
|
||||
[ext_resource type="Resource" uid="uid://cdx4qndy5xwto" path="res://data/hints/jump.tres" id="7_56w3b"]
|
||||
[ext_resource type="Script" uid="uid://dn5idrvu68k83" path="res://game/player/states/aim_state.gd" id="7_kxr4l"]
|
||||
[ext_resource type="Script" uid="uid://dpcfffs3vt5co" path="res://game/player/states/run_state.gd" id="8_j4mf1"]
|
||||
[ext_resource type="Script" uid="uid://c3jh22p4w3xco" path="res://game/player/states/throw_state.gd" id="8_k0r6x"]
|
||||
[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="Resource" uid="uid://c4fqcdrf6il7v" path="res://data/hints/switch_target.tres" id="11_lapey"]
|
||||
[ext_resource type="Resource" uid="uid://bow3d0e0fcto7" path="res://data/hints/cancel_throw.tres" id="12_iqmup"]
|
||||
[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"]
|
||||
[ext_resource type="Script" uid="uid://sixraavy7k2h" path="res://game/player/states/inair_state.gd" id="16_lapey"]
|
||||
[ext_resource type="Resource" uid="uid://c18ntr4wb56m2" path="res://data/hints/mount.tres" id="20_lapey"]
|
||||
[ext_resource type="Resource" uid="uid://cfl2q6f3sjy1v" path="res://data/hints/dismount.tres" id="21_iqmup"]
|
||||
[ext_resource type="Script" uid="uid://b0j1jxv6uu2y8" path="res://game/player/input/state_hints.gd" id="22_iqmup"]
|
||||
[ext_resource type="Script" uid="uid://qh738m0cmw8b" path="res://game/player/aim/aim_targeting.gd" id="22_p12ry"]
|
||||
[ext_resource type="Script" uid="uid://bxt8qv1xyw2nl" path="res://game/player/aim/throw_resolver.gd" id="23_6dxcy"]
|
||||
|
||||
[sub_resource type="Curve" id="Curve_kxr4l"]
|
||||
_limits = [0.0, 10.0, 0.0, 1.0]
|
||||
|
|
@ -49,13 +50,18 @@ size = Vector3(0.1484375, 0.1640625, 0.907959)
|
|||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_3agn1"]
|
||||
|
||||
[node name="PlayerCharacter" type="CharacterBody3D" unique_id=1830301535]
|
||||
[node name="PlayerCharacter" type="CharacterBody3D" unique_id=1830301535 node_paths=PackedStringArray("visual_root", "cam_holder", "state_machine", "collision_shape_3d")]
|
||||
script = ExtResource("1_pj0hh")
|
||||
nb_jumps_in_air_allowed = 0
|
||||
coyote_jump_cooldown = 1.0
|
||||
in_air_move_speed = Array[Curve]([SubResource("Curve_kxr4l"), SubResource("Curve_j4mf1")])
|
||||
in_air_accel = Array[Curve]([SubResource("Curve_6abew"), SubResource("Curve_mi63v")])
|
||||
hit_wall_cut_velocity = true
|
||||
follow_cam_pos_when_aimed = true
|
||||
visual_root = NodePath("VisualRoot")
|
||||
cam_holder = NodePath("CameraHolder")
|
||||
state_machine = NodePath("StateMachine")
|
||||
collision_shape_3d = NodePath("CollisionShape3D")
|
||||
|
||||
[node name="VisualRoot" type="Node3D" parent="." unique_id=1587327772]
|
||||
unique_name_in_owner = true
|
||||
|
|
@ -87,20 +93,12 @@ initial_state = NodePath("IdleState")
|
|||
|
||||
[node name="IdleState" type="Node" parent="StateMachine" unique_id=1417869633]
|
||||
script = ExtResource("4_38lxl")
|
||||
hints = Array[ExtResource("6_7qhxe")]([ExtResource("7_56w3b")])
|
||||
|
||||
[node name="WalkState" type="Node" parent="StateMachine" unique_id=1524475809]
|
||||
script = ExtResource("4_lg056")
|
||||
hints = Array[ExtResource("6_7qhxe")]([ExtResource("7_56w3b")])
|
||||
|
||||
[node name="JumpState" type="Node" parent="StateMachine" unique_id=711160985]
|
||||
script = ExtResource("6_lwgph")
|
||||
hints = Array[ExtResource("6_7qhxe")]([ExtResource("7_56w3b")])
|
||||
|
||||
[node name="AimState" type="Node" parent="StateMachine" unique_id=296560188]
|
||||
script = ExtResource("7_kxr4l")
|
||||
switch_hint = ExtResource("11_lapey")
|
||||
cancel_hint = ExtResource("12_iqmup")
|
||||
|
||||
[node name="ThrowState" type="Node" parent="StateMachine" unique_id=1159570489]
|
||||
script = ExtResource("8_k0r6x")
|
||||
|
|
@ -108,14 +106,12 @@ cancel_hint = ExtResource("12_iqmup")
|
|||
|
||||
[node name="RunState" type="Node" parent="StateMachine" unique_id=1345421775]
|
||||
script = ExtResource("8_j4mf1")
|
||||
hints = Array[ExtResource("6_7qhxe")]([ExtResource("7_56w3b")])
|
||||
|
||||
[node name="DodgeState" type="Node" parent="StateMachine" unique_id=232179400]
|
||||
script = ExtResource("9_6abew")
|
||||
keep_previous_hints = true
|
||||
|
||||
[node name="CrouchState" type="Node" parent="StateMachine" unique_id=381419367]
|
||||
script = ExtResource("10_mi63v")
|
||||
[node name="InairState" type="Node" parent="StateMachine" unique_id=1405519998]
|
||||
script = ExtResource("16_lapey")
|
||||
|
||||
[node name="InputManagementComponent" type="Node" parent="." unique_id=1981951371]
|
||||
script = ExtResource("2_r5dqo")
|
||||
|
|
@ -128,3 +124,13 @@ script = ExtResource("14_dkijf")
|
|||
mount_hint = ExtResource("20_lapey")
|
||||
dismount_hint = ExtResource("21_iqmup")
|
||||
jump_hint = ExtResource("7_56w3b")
|
||||
|
||||
[node name="AimTargeting" type="Node" parent="." unique_id=1285504499]
|
||||
script = ExtResource("22_p12ry")
|
||||
switch_hint = ExtResource("11_lapey")
|
||||
|
||||
[node name="ThrowResolver" type="Node" parent="." unique_id=1062063502]
|
||||
script = ExtResource("23_6dxcy")
|
||||
|
||||
[node name="StateHints" type="Node" parent="." unique_id=686759706]
|
||||
script = ExtResource("22_iqmup")
|
||||
|
|
|
|||
29
game/player/player_component.gd
Normal file
29
game/player/player_component.gd
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
extends Node
|
||||
|
||||
class_name PlayerComponent
|
||||
|
||||
#base des composants du joueur (InputManagementComponent, AimTargeting, ThrowResolver, MountRider, RecallInput...).
|
||||
#- le joueur est retrouve en remontant l'arbre : le composant peut etre place n'importe ou sous le PlayerCharacter
|
||||
# (directement, ou dans un node de rangement), sans dependre de sa position exacte ni de get_parent().
|
||||
#- un composant qui a une place dans le joueur (ex : player.aim_targeting) s'y enregistre lui-meme dans register_with_player() :
|
||||
# le joueur n'a plus besoin de le chercher par son nom ($AimTargeting) et le node peut etre renomme librement.
|
||||
#_enter_tree() est appele avant tous les _ready() : le joueur est donc deja renseigne quand un composant demarre.
|
||||
|
||||
var player : PlayerCharacter
|
||||
|
||||
func _enter_tree() -> void:
|
||||
player = _find_player()
|
||||
if player == null:
|
||||
push_error("%s : ce composant doit etre place sous un PlayerCharacter." % name)
|
||||
return
|
||||
register_with_player()
|
||||
|
||||
func register_with_player() -> void:
|
||||
pass #a surcharger : player.xxx = self
|
||||
|
||||
func _find_player() -> PlayerCharacter:
|
||||
var n : Node = get_parent()
|
||||
while n != null:
|
||||
if n is PlayerCharacter: return n
|
||||
n = n.get_parent()
|
||||
return null
|
||||
1
game/player/player_component.gd.uid
Normal file
1
game/player/player_component.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://jsb4l4g023o3
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
extends State
|
||||
|
||||
class_name AimState
|
||||
|
||||
var state_name : String = "Aim"
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
@export var aim_action : StringName = &"player_aim_camera"
|
||||
#@export var switch_target_action : StringName = &"player_aim_switch_target"
|
||||
@export var unlock_action : StringName = &"player_aim_unlock"
|
||||
@export var throw_action : StringName = &"player_throw" #RT / clic gauche (cree automatiquement s'il n'existe pas)
|
||||
#@export var cancel_action : StringName = &"player_throw_cancel" #annule la charge d'un lancer (cree par ThrowState)
|
||||
|
||||
@export_group("Targeting")
|
||||
@export var throw_range : float = 20.0
|
||||
@export var max_aim_angle : float = 35.0 #angle max (deg) entre l'axe camera et une cible pour qu'elle soit candidate
|
||||
@export var auto_lock_angle : float = 12.0 #si une cible passe sous cet angle, on se verrouille dessus
|
||||
@export var lose_range_margin : float = 1.25 #marge de distance avant de perdre une cible deja verrouillee
|
||||
@export var check_line_of_sight : bool = true
|
||||
@export_flags_3d_physics var line_of_sight_mask : int = 1
|
||||
|
||||
@export_group("Movement")
|
||||
@export var aim_move_speed : float = 3.0
|
||||
@export var aim_move_accel : float = 8.0
|
||||
|
||||
@export_group("Hints")
|
||||
@export var switch_hint : Hint #switch_target.tres
|
||||
@export var cancel_hint : Hint #cancel_throw.tres (la meme ressource que ThrowState)
|
||||
|
||||
var switch_target_action : StringName:
|
||||
get: return switch_hint.action
|
||||
var cancel_action : StringName:
|
||||
get: return cancel_hint.action
|
||||
|
||||
var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant que la cible est encore au centre
|
||||
var dodging_from_aim : bool = false #true quand on quitte Aim pour rouler, pour ne pas relacher la cible verrouillee
|
||||
var came_from_crouch : bool = false #true si Aim a ete declenche depuis CrouchState, pour y retourner en sortant
|
||||
var keep_aim_on_exit : bool = false #true quand on part vers ThrowState : camera et verrouillage restent en place
|
||||
var charging_throw : bool = false: #true quand on arrive depuis ThrowState en pleine charge : RT est encore maintenue
|
||||
set(value):
|
||||
if charging_throw == value: return
|
||||
charging_throw = value
|
||||
hints_changed.emit()
|
||||
|
||||
func _ready() -> void:
|
||||
if InputMap.has_action(throw_action): return
|
||||
InputMap.add_action(throw_action)
|
||||
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)
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
auto_lock_blocked = false
|
||||
#curr_state_name n'est pas encore mis a jour a ce stade : il reflete encore l'etat qu'on est en train de quitter
|
||||
#si on revient de ThrowState, came_from_crouch a deja ete transmis par ThrowState : on ne l'ecrase pas
|
||||
var previous : String = play_char.state_machine.curr_state_name
|
||||
if previous != "Throw" and previous != "Dodge":
|
||||
came_from_crouch = previous == "Crouch"
|
||||
verifications()
|
||||
|
||||
func verifications() -> void:
|
||||
play_char.floor_snap_length = 1.0
|
||||
play_char.cam_holder.cam_aimed = true
|
||||
|
||||
func exit() -> void:
|
||||
charging_throw = false
|
||||
if keep_aim_on_exit:
|
||||
keep_aim_on_exit = false
|
||||
return
|
||||
AimReticle.hide_instance()
|
||||
if dodging_from_aim:
|
||||
#on roule en visant : la camera et la cible verrouillee restent actives pendant la roulade
|
||||
dodging_from_aim = false
|
||||
return
|
||||
release_target()
|
||||
if play_char: play_char.cam_holder.cam_aimed = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
if applies(): return
|
||||
play_char.gravity_apply(delta)
|
||||
if input_management(): return
|
||||
update_target()
|
||||
update_reticle()
|
||||
move(delta)
|
||||
|
||||
func applies() -> bool:
|
||||
if !play_char.is_on_floor() and play_char.velocity.y < 0.0:
|
||||
transitioned.emit(self, "InairState")
|
||||
return true
|
||||
return false
|
||||
|
||||
func input_management() -> bool:
|
||||
if Input.is_action_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0:
|
||||
dodging_from_aim = true
|
||||
transitioned.emit(self, "DodgeState")
|
||||
return true
|
||||
|
||||
if !Input.is_action_pressed(aim_action):
|
||||
if charging_throw and Input.is_action_pressed(throw_action) and can_throw():
|
||||
transitioned.emit(self, "ThrowState") #visee relachee en pleine charge : on reprend la charge hors visee
|
||||
return true
|
||||
if came_from_crouch:
|
||||
transitioned.emit(self, "CrouchState")
|
||||
else:
|
||||
transitioned.emit(self, play_char.walk_or_run if play_char.move_dir else "IdleState")
|
||||
return true
|
||||
|
||||
if charging_throw:
|
||||
if Input.is_action_just_pressed(cancel_action):
|
||||
charging_throw = false #lancer annule, on reste en visee
|
||||
elif !Input.is_action_pressed(throw_action):
|
||||
charging_throw = false
|
||||
if start_throw(true): return true #RT relachee : le lancer part avec la cible verrouillee
|
||||
|
||||
if Input.is_action_just_pressed(throw_action):
|
||||
#si le pokemon selectionne est deja dehors, un nouvel appui le rappelle au lieu de relancer
|
||||
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
||||
var already_out : SentPokemon = SentPokemon.find_for(get_tree(), selected) if selected else null
|
||||
if already_out:
|
||||
already_out.recall()
|
||||
elif start_throw():
|
||||
return true
|
||||
|
||||
if Input.is_action_just_pressed(unlock_action):
|
||||
auto_lock_blocked = true
|
||||
release_target()
|
||||
elif Input.is_action_just_pressed(switch_target_action):
|
||||
switch_target()
|
||||
return false
|
||||
|
||||
func can_throw() -> bool:
|
||||
if GameData.get_selected_pokemon() == null: return false #personne a envoyer
|
||||
return play_char.state_machine.states.has("throwstate") #sinon : pas de node ThrowState dans la StateMachine
|
||||
|
||||
func start_throw(released : bool = false) -> bool:
|
||||
if !can_throw(): return false
|
||||
keep_aim_on_exit = true
|
||||
var throw : Node = play_char.state_machine.states["throwstate"]
|
||||
throw.resume_released = released #true : on saute la charge, le windup demarre directement
|
||||
transitioned.emit(self, "ThrowState")
|
||||
return true
|
||||
|
||||
func move(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 * aim_move_speed, aim_move_accel * delta)
|
||||
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * aim_move_speed, aim_move_accel * delta)
|
||||
else:
|
||||
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 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)
|
||||
|
||||
#---------- targeting ----------
|
||||
|
||||
func update_target() -> void:
|
||||
var current : AimTarget = play_char.aim_target
|
||||
|
||||
#verrouillage existant : on le garde tant que distance et ligne de vue sont valides
|
||||
#(l'angle avec la camera ne compte pas, le joueur peut deplacer la camera librement)
|
||||
if is_instance_valid(current):
|
||||
if is_target_valid(current, lose_range_margin, false): return
|
||||
release_target()
|
||||
|
||||
#visee libre : auto-lock sur la meilleure cible proche du centre de la camera
|
||||
var best : AimTarget = null
|
||||
var best_angle : float = auto_lock_angle
|
||||
for t in get_candidates():
|
||||
var angle : float = get_angle_to_camera(t) - t.priority
|
||||
if angle < best_angle:
|
||||
best_angle = angle
|
||||
best = t
|
||||
|
||||
if best == null:
|
||||
auto_lock_blocked = false #le joueur a ecarte la camera : l'auto-lock est de nouveau permis
|
||||
elif !auto_lock_blocked:
|
||||
set_target(best)
|
||||
|
||||
func get_candidates() -> Array[AimTarget]:
|
||||
var result : Array[AimTarget] = []
|
||||
for node in get_tree().get_nodes_in_group("aim_targets"):
|
||||
var t := node as AimTarget
|
||||
if t and is_target_valid(t, 1.0, true): result.append(t)
|
||||
return result
|
||||
|
||||
func is_target_valid(t : AimTarget, range_mult : float, check_angle : bool) -> bool:
|
||||
if !t.enabled: return false
|
||||
if play_char.global_position.distance_to(t.global_position) > throw_range * range_mult: return false
|
||||
if check_angle and get_angle_to_camera(t) > max_aim_angle: return false
|
||||
if check_line_of_sight and !has_line_of_sight(t): return false
|
||||
return true
|
||||
|
||||
func get_angle_to_camera(t : AimTarget) -> float:
|
||||
var cam : Camera3D = play_char.cam_holder.cam
|
||||
var to_t : Vector3 = t.global_position - cam.global_position
|
||||
return rad_to_deg((-cam.global_basis.z).angle_to(to_t))
|
||||
|
||||
func has_line_of_sight(t : AimTarget) -> bool:
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
play_char.cam_holder.cam.global_position, t.global_position, line_of_sight_mask, [play_char.get_rid()])
|
||||
var hit : Dictionary = play_char.get_world_3d().direct_space_state.intersect_ray(query)
|
||||
#pas d'obstacle, ou l'obstacle est le proprietaire de la cible lui-meme
|
||||
return hit.is_empty() or hit["collider"].is_ancestor_of(t)
|
||||
|
||||
func switch_target() -> void:
|
||||
#passe a la cible suivante, triee de gauche a droite par rapport a la camera (boucle)
|
||||
var cam : Camera3D = play_char.cam_holder.cam
|
||||
var forward : Vector3 = -cam.global_basis.z
|
||||
var list : Array[AimTarget] = get_candidates()
|
||||
|
||||
#la cible actuelle reste dans la liste meme si la camera est decalee par rapport a elle
|
||||
var current : AimTarget = play_char.aim_target
|
||||
if is_instance_valid(current) and !list.has(current): list.append(current)
|
||||
if list.size() < 2: return
|
||||
|
||||
list.sort_custom(func(a : AimTarget, b : AimTarget) -> bool:
|
||||
return signed_angle(forward, a, cam) < signed_angle(forward, b, cam))
|
||||
|
||||
var idx : int = list.find(current)
|
||||
idx = 0 if idx == -1 else (idx + 1) % list.size()
|
||||
set_target(list[idx])
|
||||
|
||||
func signed_angle(forward : Vector3, t : AimTarget, cam : Camera3D) -> float:
|
||||
var to_t : Vector3 = t.global_position - cam.global_position
|
||||
return Vector2(forward.x, forward.z).angle_to(Vector2(to_t.x, to_t.z))
|
||||
|
||||
func set_target(t : AimTarget) -> void:
|
||||
if play_char.aim_target == t: return
|
||||
release_target()
|
||||
play_char.aim_target = t
|
||||
play_char.cam_holder.set_lock_target(t)
|
||||
t.set_locked(true)
|
||||
|
||||
func release_target() -> void:
|
||||
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)
|
||||
|
||||
func get_hints() -> Array[Hint]:
|
||||
var list : Array[Hint] = [switch_hint]
|
||||
if charging_throw: list.append(cancel_hint)
|
||||
return list
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://dn5idrvu68k83
|
||||
67
game/player/states/airborne_state.gd
Normal file
67
game/player/states/airborne_state.gd
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
extends PlayerState
|
||||
|
||||
class_name AirborneState
|
||||
|
||||
#base des etats en l'air (Jump, Inair), comme GroundedState pour le sol. Chaque etat enfant ne decrit que ses differences :
|
||||
# - on_air_enter() : ce qui se passe a l'entree (JumpState saute, InairState applique un saut coupe)
|
||||
# - check_apex() : condition pour changer d'etat en l'air (JumpState -> InairState quand le perso commence a descendre)
|
||||
# - handle_input() : les inputs propres a l'etat (saut, relachement du saut...)
|
||||
#le reste est commun : gravite, minuteurs de saut, atterrissage, collision avec un mur, controle en l'air.
|
||||
|
||||
#---------- cycle de vie ----------
|
||||
|
||||
func on_enter() -> void:
|
||||
if player.floor_snap_length != 0.0: player.floor_snap_length = 0.0 #sinon le perso reste colle au sol
|
||||
on_air_enter()
|
||||
|
||||
func on_air_enter() -> void:
|
||||
pass
|
||||
|
||||
#---------- boucle ----------
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
if check_air_transitions(delta): return
|
||||
|
||||
player.gravity_apply(delta)
|
||||
|
||||
if handle_input(): return
|
||||
|
||||
move(delta)
|
||||
|
||||
func check_air_transitions(delta : float) -> bool:
|
||||
#atterrissage : retour a l'etat au sol correspondant a la derniere allure (marche / course)
|
||||
if player.is_on_floor():
|
||||
return request_ground_state(player.move_dir != Vector2.ZERO)
|
||||
|
||||
tick_air_timers(delta)
|
||||
if check_apex(): return true
|
||||
|
||||
#si active, coupe la vitesse horizontale quand le perso touche un mur
|
||||
if player.is_on_wall() and player.hit_wall_cut_velocity:
|
||||
player.velocity.x = 0.0
|
||||
player.velocity.z = 0.0
|
||||
return false
|
||||
|
||||
func tick_air_timers(delta : float) -> void:
|
||||
if player.jump_cooldown > 0.0: player.jump_cooldown -= delta
|
||||
if player.coyote_jump_cooldown > 0.0: player.coyote_jump_cooldown -= delta
|
||||
|
||||
func check_apex() -> bool:
|
||||
return false #JumpState surcharge
|
||||
|
||||
func handle_input() -> bool:
|
||||
return false #Jump et Inair surchargent
|
||||
|
||||
#---------- controle en l'air ----------
|
||||
|
||||
func move(delta : float) -> void:
|
||||
player.move_dir = input.read_move_dir()
|
||||
if !player.move_dir or player.is_on_floor(): return
|
||||
|
||||
#selon la derniere allure au sol (marche ou course), on choisit la courbe correspondante pour les valeurs en l'air
|
||||
var idx : int = player.locomotion #WALK = 0, RUN = 1 : l'index de la courbe correspondante
|
||||
if idx >= player.in_air_move_speed.size() or idx >= player.in_air_accel.size(): return #courbes non assignees
|
||||
|
||||
var speed : float = player.in_air_move_speed[idx].sample(player.velocity.length())
|
||||
var accel : float = player.in_air_accel[idx].sample(player.velocity.length())
|
||||
accelerate(speed, accel, delta)
|
||||
1
game/player/states/airborne_state.gd.uid
Normal file
1
game/player/states/airborne_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d2fq1w57bakgq
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
extends State
|
||||
|
||||
class_name CrouchState
|
||||
|
||||
var state_name : String = "Crouch"
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
|
||||
verifications()
|
||||
|
||||
func verifications() -> void:
|
||||
#play_char.godot_plush_skin.set_state("crouch")
|
||||
play_char.move_speed = play_char.crouch_speed
|
||||
play_char.move_accel = play_char.crouch_accel
|
||||
play_char.move_deccel = play_char.crouch_deccel
|
||||
|
||||
play_char.floor_snap_length = 1.0
|
||||
if play_char.jump_cooldown > 0.0: play_char.jump_cooldown = -1.0
|
||||
if play_char.nb_jumps_in_air_allowed < play_char.nb_jumps_in_air_allowed_ref: play_char.nb_jumps_in_air_allowed = play_char.nb_jumps_in_air_allowed_ref
|
||||
if play_char.coyote_jump_cooldown < play_char.coyote_jump_cooldown_ref: play_char.coyote_jump_cooldown = play_char.coyote_jump_cooldown_ref
|
||||
if play_char.has_cut_jump: play_char.has_cut_jump = false
|
||||
|
||||
play_char.is_crouching = true
|
||||
|
||||
func exit() -> void:
|
||||
play_char.is_crouching = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies()
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
|
||||
input_management()
|
||||
|
||||
move(delta)
|
||||
|
||||
func applies() -> void:
|
||||
#quitter le sol (ex: crouch au bord d'une plateforme) bascule vers InairState, comme IdleState
|
||||
if !play_char.is_on_floor() and !play_char.is_on_wall():
|
||||
transitioned.emit(self, "InairState")
|
||||
#le saut est bloque en crouch : on ne consomme pas le jump buffer ici, il restera en attente
|
||||
#et sera pris en compte des la sortie du crouch (Idle/Walk)
|
||||
|
||||
func input_management() -> void:
|
||||
if Input.is_action_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0:
|
||||
transitioned.emit(self, "DodgeState")
|
||||
|
||||
#courir annule automatiquement le crouch
|
||||
if play_char.walk_or_run == "RunState":
|
||||
transitioned.emit(self, "RunState")
|
||||
|
||||
#bascule (toggle) : un nouvel appui ressort du crouch vers l'etat au sol approprie
|
||||
if Input.is_action_just_pressed(play_char.crouch_action):
|
||||
transitioned.emit(self, play_char.walk_or_run if play_char.move_dir else "IdleState")
|
||||
|
||||
if Input.is_action_just_pressed(play_char.aim_cam_action):
|
||||
transitioned.emit(self, "AimState")
|
||||
|
||||
#pas de saut ici : le jump_action est volontairement ignore pendant le crouch
|
||||
|
||||
func move(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():
|
||||
#deplacement accroupi, a vitesse reduite
|
||||
play_char.velocity.x = lerp(play_char.velocity.x, play_char.move_dir.x * play_char.move_speed, play_char.move_accel * delta)
|
||||
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * play_char.move_speed, play_char.move_accel * delta)
|
||||
else:
|
||||
#pas de transition vers Idle ici : Crouch gere lui-meme l'immobilite, comme AimState
|
||||
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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://d0577tj0c23bu
|
||||
|
|
@ -1,56 +1,40 @@
|
|||
extends State
|
||||
extends PlayerState
|
||||
|
||||
class_name DodgeState
|
||||
|
||||
var state_name : String = "Dodge"
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
@export var dodge_speed_curve : Curve #optionnel : controle le "falloff" de vitesse pendant la roulade (1.0 = pleine vitesse, 0.0 = arret). Laisser vide pour une vitesse constante.
|
||||
|
||||
var dodge_dir : Vector2 = Vector2.ZERO
|
||||
var dodge_timer : float = 0.0
|
||||
var dodge_duration_ref : float = 0.0
|
||||
var came_from_crouch : bool = false #true si l'esquive vient du crouch (directement, ou via Aim declenche depuis le crouch)
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
|
||||
#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
|
||||
|
||||
func on_enter() -> void:
|
||||
AimReticle.hide_instance() #pas de reticule pendant la roulade (si la visee est maintenue, elle reprend apres)
|
||||
verifications()
|
||||
|
||||
func verifications() -> void:
|
||||
play_char.floor_snap_length = 1.0
|
||||
play_char.is_dodging = true
|
||||
play_char.dodge_cooldown = play_char.dodge_cooldown_ref
|
||||
player.floor_snap_length = 1.0
|
||||
player.is_dodging = true
|
||||
player.dodge_cooldown = player.dodge_cooldown_ref
|
||||
|
||||
#roule dans la direction actuelle de l'input, ou dans la derniere direction connue si aucune touche n'est tenue (comportement Arceus)
|
||||
dodge_dir = play_char.move_dir if play_char.move_dir != Vector2.ZERO else play_char.last_input_dir
|
||||
dodge_dir = player.move_dir if player.move_dir != Vector2.ZERO else player.last_input_dir
|
||||
if dodge_dir == Vector2.ZERO: dodge_dir = Vector2.DOWN #direction par defaut si le perso n'a encore jamais bouge
|
||||
dodge_dir = dodge_dir.normalized()
|
||||
|
||||
dodge_timer = play_char.dodge_duration
|
||||
dodge_duration_ref = play_char.dodge_duration
|
||||
dodge_timer = player.dodge_duration
|
||||
dodge_duration_ref = player.dodge_duration
|
||||
|
||||
#oriente immediatement le modele dans la direction de la roulade, verrouillee pour toute sa duree
|
||||
play_char.visual_root.rotation.y = -dodge_dir.orthogonal().angle()
|
||||
player.visual_root.rotation.y = -dodge_dir.orthogonal().angle()
|
||||
|
||||
func exit() -> void:
|
||||
play_char.is_dodging = false
|
||||
player.is_dodging = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies()
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
player.gravity_apply(delta)
|
||||
|
||||
move(delta)
|
||||
|
||||
|
|
@ -60,33 +44,25 @@ func physics_update(delta : float) -> void:
|
|||
|
||||
func applies() -> void:
|
||||
#si le perso quitte le sol en roulant (ex: roulade vers un bord), on bascule vers InairState comme les autres etats au sol
|
||||
if !play_char.is_on_floor() and !play_char.is_on_wall():
|
||||
if play_char.velocity.y < 0.0:
|
||||
transitioned.emit(self, "InairState")
|
||||
if !player.is_on_floor() and !player.is_on_wall():
|
||||
if player.velocity.y < 0.0:
|
||||
fall()
|
||||
|
||||
func move(_delta : float) -> void:
|
||||
if !play_char.is_on_floor(): return
|
||||
if !player.is_on_floor(): return
|
||||
|
||||
#falloff de vitesse : pleine vitesse par defaut, ou modele par dodge_speed_curve si assignee
|
||||
var ratio : float = 1.0
|
||||
if dodge_speed_curve:
|
||||
ratio = dodge_speed_curve.sample(1.0 - (dodge_timer / dodge_duration_ref))
|
||||
|
||||
play_char.velocity.x = dodge_dir.x * play_char.dodge_speed * ratio
|
||||
play_char.velocity.z = dodge_dir.y * play_char.dodge_speed * ratio
|
||||
player.velocity.x = dodge_dir.x * player.dodge_speed * ratio
|
||||
player.velocity.z = dodge_dir.y * player.dodge_speed * ratio
|
||||
|
||||
func end_dodge() -> void:
|
||||
#a la fin de la roulade, retour a l'etat approprie
|
||||
if !play_char.is_on_floor():
|
||||
transitioned.emit(self, "InairState")
|
||||
elif Input.is_action_pressed(play_char.aim_cam_action):
|
||||
#AimState ne verra que "Dodge" 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") #le joueur a garde la visee enfoncee pendant la roulade
|
||||
elif came_from_crouch:
|
||||
transitioned.emit(self, "CrouchState")
|
||||
elif play_char.move_dir:
|
||||
transitioned.emit(self, play_char.walk_or_run)
|
||||
#a la fin de la roulade, retour a l'etat approprie (la posture accroupie est conservee : Idle / Walk la reappliquent)
|
||||
if !player.is_on_floor():
|
||||
fall()
|
||||
else:
|
||||
transitioned.emit(self, "IdleState")
|
||||
if input.aim_held(): player.aim_targeting.begin_aim() #visee enfoncee pendant la roulade (sans effet si la visee est deja active)
|
||||
request_ground_state(player.move_dir != Vector2.ZERO)
|
||||
|
|
|
|||
124
game/player/states/grounded_state.gd
Normal file
124
game/player/states/grounded_state.gd
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
extends PlayerState
|
||||
|
||||
class_name GroundedState
|
||||
|
||||
#base des etats au sol (Idle, Walk, Run). Chaque etat enfant ne decrit que ses differences :
|
||||
# - apply_standing_stats() : vitesse / accel / deccel de l'etat quand on est debout
|
||||
# - on_move() / on_no_move() : ce qui se passe avec / sans direction d'input
|
||||
# - check_locomotion_switch() : bascule Walk <-> Run
|
||||
# - is_falling() : condition pour passer en InairState
|
||||
#
|
||||
#le crouch n'est plus un etat mais une POSTURE (player.is_crouching) :
|
||||
# - elle se bascule ici (toggle_crouch) et survit aux etats Dodge / Throw : plus besoin de "came_from_crouch"
|
||||
# - elle est annulee en courant (RunState) et en quittant le sol (State.fall())
|
||||
#la visee (AimTargeting) est un mode du meme genre : tant qu'elle est active, deplacement ralenti, pas de saut, et tick() chaque frame.
|
||||
# - accroupi : stats de deplacement accroupi, pas de saut, pas de jump buffer consomme
|
||||
|
||||
var allows_crouch : bool = true #false : l'appui sur crouch_action est ignore (Run)
|
||||
|
||||
#---------- cycle de vie ----------
|
||||
|
||||
func on_enter() -> void:
|
||||
reset_ground_state()
|
||||
apply_movement_stats()
|
||||
|
||||
func reset_ground_state() -> void:
|
||||
#remet a zero tout ce qui concerne le saut quand on touche / reste au sol
|
||||
player.floor_snap_length = 1.0
|
||||
if player.jump_cooldown > 0.0: player.jump_cooldown = -1.0
|
||||
if player.nb_jumps_in_air_allowed < player.nb_jumps_in_air_allowed_ref: player.nb_jumps_in_air_allowed = player.nb_jumps_in_air_allowed_ref
|
||||
if player.coyote_jump_cooldown < player.coyote_jump_cooldown_ref: player.coyote_jump_cooldown = player.coyote_jump_cooldown_ref
|
||||
if player.has_cut_jump: player.has_cut_jump = false
|
||||
|
||||
#---------- stats de deplacement ----------
|
||||
|
||||
func apply_movement_stats() -> void:
|
||||
if player.is_crouching: apply_crouch_stats()
|
||||
else: apply_standing_stats()
|
||||
|
||||
func apply_standing_stats() -> void:
|
||||
pass #Walk / Run y copient leurs vitesses sur player ; Idle garde celles de l'etat precedent
|
||||
|
||||
func apply_crouch_stats() -> void:
|
||||
player.move_speed = player.crouch_speed
|
||||
player.move_accel = player.crouch_accel
|
||||
player.move_deccel = player.crouch_deccel
|
||||
|
||||
func toggle_crouch() -> void:
|
||||
player.is_crouching = !player.is_crouching
|
||||
apply_movement_stats()
|
||||
|
||||
#---------- boucle ----------
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
if try_start_throw(): return
|
||||
|
||||
if check_ground_transitions(): return
|
||||
|
||||
player.gravity_apply(delta)
|
||||
|
||||
if handle_input(): return
|
||||
|
||||
if player.is_aiming: player.aim_targeting.tick()
|
||||
|
||||
move(delta)
|
||||
|
||||
func try_start_throw() -> bool:
|
||||
#RT depuis le sol (debout ou accroupi, visee ou non) lance un pokemon : ce n'est plus la StateMachine qui s'en charge
|
||||
if !input.throw_pressed(): return false
|
||||
if !player.is_on_floor(): return false
|
||||
|
||||
if GameData.get_selected_pokemon() == null: return false
|
||||
|
||||
#pokemon selectionne deja dehors : l'appui le rappelle (voir RecallInput), on ne lance pas
|
||||
if PokemonRecall.find_selected_out(get_tree()) != null: return false
|
||||
|
||||
if !player.state_machine.has_state(States.THROW): return false
|
||||
return request_transition(States.THROW)
|
||||
|
||||
func check_ground_transitions() -> bool:
|
||||
#etat du sol : on quitte le sol -> InairState ; jump buffer -> JumpState (pas accroupi : le buffer reste en attente)
|
||||
if !player.is_on_floor():
|
||||
if !player.is_on_wall() and is_falling():
|
||||
return fall()
|
||||
return false
|
||||
|
||||
if !player.is_crouching and !player.is_aiming and player.jump_buff_on:
|
||||
player.buffered_jump = true
|
||||
player.jump_buff_on = false
|
||||
return request_transition(States.JUMP)
|
||||
return false
|
||||
|
||||
func is_falling() -> bool:
|
||||
#accroupi, on quitte le sol sans condition sur la vitesse verticale (comme IdleState, qui le surcharge)
|
||||
return player.is_crouching or player.velocity.y < 0.0
|
||||
|
||||
func handle_input() -> bool:
|
||||
#renvoie true si une transition a eu lieu. Priorite : roulade > courir (annule le crouch) > crouch > saut > visee > bascule marche/course
|
||||
if input.wants_dodge() and request_transition(States.DODGE): return true
|
||||
|
||||
if player.is_crouching and player.is_running() and request_transition(States.RUN): return true
|
||||
|
||||
if allows_crouch and player.can_crouch and input.crouch_pressed(): toggle_crouch()
|
||||
|
||||
if !player.is_crouching and !player.is_aiming and input.wants_jump() and request_transition(States.JUMP): return true
|
||||
if input.aim_pressed(): player.aim_targeting.begin_aim() #la visee n'est pas un etat : le deplacement continue, ralenti
|
||||
return check_locomotion_switch()
|
||||
|
||||
func check_locomotion_switch() -> bool:
|
||||
return false #Walk et Run surchargent
|
||||
|
||||
func move(delta : float) -> void:
|
||||
player.move_dir = input.read_move_dir()
|
||||
|
||||
if player.move_dir and player.is_on_floor():
|
||||
on_move(delta)
|
||||
else:
|
||||
on_no_move(delta)
|
||||
|
||||
func on_move(delta : float) -> void:
|
||||
if player.is_aiming: accelerate(player.aim_targeting.aim_move_speed, player.aim_targeting.aim_move_accel, delta)
|
||||
else: accelerate(player.move_speed, player.move_accel, delta)
|
||||
|
||||
func on_no_move(delta : float) -> void:
|
||||
slow_down(delta)
|
||||
1
game/player/states/grounded_state.gd.uid
Normal file
1
game/player/states/grounded_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4f2x2rqadgrx
|
||||
|
|
@ -1,72 +1,9 @@
|
|||
extends State
|
||||
extends GroundedState
|
||||
|
||||
class_name IdleState
|
||||
|
||||
var state_name : String = "Idle"
|
||||
func is_falling() -> bool:
|
||||
return true #Idle bascule en InairState des qu'on n'est plus au sol, sans condition sur la vitesse verticale
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
func enter(char_ref : CharacterBody3D) -> void:
|
||||
#pass play char reference
|
||||
play_char = char_ref
|
||||
|
||||
verifications()
|
||||
|
||||
func verifications() -> void:
|
||||
#manage the appliements that need to be set at the start of the state
|
||||
#play_char.godot_plush_skin.set_state("idle")
|
||||
play_char.floor_snap_length = 1.0
|
||||
if play_char.jump_cooldown > 0.0: play_char.jump_cooldown = -1.0
|
||||
if play_char.nb_jumps_in_air_allowed < play_char.nb_jumps_in_air_allowed_ref: play_char.nb_jumps_in_air_allowed = play_char.nb_jumps_in_air_allowed_ref
|
||||
if play_char.coyote_jump_cooldown < play_char.coyote_jump_cooldown_ref: play_char.coyote_jump_cooldown = play_char.coyote_jump_cooldown_ref
|
||||
if play_char.has_cut_jump: play_char.has_cut_jump = false
|
||||
#if play_char.movement_dust.emitting: play_char.movement_dust.emitting = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies()
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
|
||||
input_management()
|
||||
|
||||
move(delta)
|
||||
|
||||
func applies() -> void:
|
||||
#manage the appliements and state transitions that needs to be sets/checked/performed
|
||||
#every time the play char pass through one of the following : floor-inair-onwall
|
||||
if !play_char.is_on_floor() and !play_char.is_on_wall():
|
||||
transitioned.emit(self, "InairState")
|
||||
if play_char.is_on_floor():
|
||||
if play_char.jump_buff_on:
|
||||
play_char.buffered_jump = true
|
||||
play_char.jump_buff_on = false
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
func input_management() -> void:
|
||||
#manage the state transitions depending on the actions inputs
|
||||
if Input.is_action_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0:
|
||||
transitioned.emit(self, "DodgeState")
|
||||
|
||||
if Input.is_action_just_pressed(play_char.crouch_action):
|
||||
transitioned.emit(self, "CrouchState")
|
||||
|
||||
if Input.is_action_pressed(play_char.jump_action) if play_char.auto_jump else Input.is_action_just_pressed(play_char.jump_action):
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
#if Input.is_action_just_pressed("player_aim_camera"):
|
||||
if Input.is_action_just_pressed(play_char.aim_cam_action):
|
||||
transitioned.emit(self, "AimState")
|
||||
|
||||
func move(delta : float) -> void:
|
||||
#manage the character movement
|
||||
|
||||
#get the move direction depending on the input
|
||||
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():
|
||||
#transition to corresponding state
|
||||
transitioned.emit(self, play_char.walk_or_run)
|
||||
else:
|
||||
#apply smooth stop
|
||||
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 on_move(_delta : float) -> void:
|
||||
request_locomotion() #direction donnee : on passe a Walk ou Run
|
||||
|
|
|
|||
37
game/player/states/inair_state.gd
Normal file
37
game/player/states/inair_state.gd
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
extends AirborneState
|
||||
|
||||
class_name InairState
|
||||
|
||||
@export var jump_buffer_time : float = 0.15 #appui sur saut juste avant d'atterrir : le saut part a l'atterrissage si l'appui date de moins de ce delai (en secondes)
|
||||
|
||||
var _buffer_id : int = 0 #identifie le dernier appui bufferise, pour ne pas qu'un ancien minuteur annule un appui plus recent
|
||||
|
||||
func on_air_enter() -> void:
|
||||
#saut coupe (bouton relache pendant la montee, voir JumpState) : on freine la montee, une seule fois
|
||||
if player.has_cut_jump:
|
||||
player.has_cut_jump = false
|
||||
if player.velocity.y > 0.0: player.velocity.y *= player.jump_cut_multiplier
|
||||
|
||||
func handle_input() -> bool:
|
||||
if !input.jump_pressed(): return false
|
||||
|
||||
#coyote jump : on vient de quitter le sol sans sauter, le saut reste permis un court instant
|
||||
#saut en l'air : il en reste (nb_jumps_in_air_allowed)
|
||||
var coyote : bool = player.coyote_jump_cooldown > 0.0
|
||||
if coyote or player.nb_jumps_in_air_allowed > 0:
|
||||
player.coyote_jump_on = coyote #lu par JumpState.jump() : le coyote jump ne consomme pas de saut en l'air
|
||||
if request_transition(States.JUMP): return true
|
||||
player.coyote_jump_on = false #saut refuse (ex : etat bloque) : on ne laisse pas le drapeau traîner
|
||||
return false
|
||||
|
||||
buffer_jump() #plus de saut possible : on garde l'appui un instant pour l'atterrissage
|
||||
return false
|
||||
|
||||
func buffer_jump() -> void:
|
||||
#jump_buff_on est consomme par GroundedState a l'atterrissage (-> JumpState). Il est retire apres jump_buffer_time
|
||||
#quel que soit l'etat dans lequel on se trouve, d'ou le minuteur du SceneTree et pas un compteur dans physics_update
|
||||
player.jump_buff_on = true
|
||||
_buffer_id += 1
|
||||
var id : int = _buffer_id
|
||||
await get_tree().create_timer(jump_buffer_time).timeout
|
||||
if id == _buffer_id: player.jump_buff_on = false
|
||||
1
game/player/states/inair_state.gd.uid
Normal file
1
game/player/states/inair_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://sixraavy7k2h
|
||||
|
|
@ -1,77 +1,30 @@
|
|||
extends State
|
||||
extends AirborneState
|
||||
|
||||
class_name JumpState
|
||||
|
||||
var state_name : String = "Jump"
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
|
||||
verifications()
|
||||
func on_air_enter() -> void:
|
||||
#play_char.godot_plush_skin.set_state("jump")
|
||||
if player.jump_cooldown < player.jump_cooldown_ref: player.jump_cooldown = player.jump_cooldown_ref
|
||||
player.coyote_jump_cooldown = -1.0 #un saut consomme le coyote jump : pas de saut gratuit en retombant juste apres
|
||||
#if player.movement_dust.emitting: player.movement_dust.emitting = false
|
||||
|
||||
jump()
|
||||
|
||||
func verifications() -> void:
|
||||
#play_char.godot_plush_skin.set_state("jump")
|
||||
if play_char.floor_snap_length != 0.0: play_char.floor_snap_length = 0.0
|
||||
if play_char.jump_cooldown < play_char.jump_cooldown_ref: play_char.jump_cooldown = play_char.jump_cooldown_ref
|
||||
#if play_char.movement_dust.emitting: play_char.movement_dust.emitting = false
|
||||
func check_apex() -> bool:
|
||||
#le perso commence a descendre : la suite de la chute se passe dans InairState
|
||||
return player.velocity.y < 0.0 and fall()
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies(delta)
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
|
||||
input_management()
|
||||
|
||||
move(delta)
|
||||
|
||||
func applies(delta : float) -> void:
|
||||
if !play_char.is_on_floor():
|
||||
if play_char.jump_cooldown > 0.0: play_char.jump_cooldown -= delta
|
||||
if play_char.coyote_jump_cooldown > 0.0: play_char.coyote_jump_cooldown -= delta
|
||||
if play_char.velocity.y < 0.0:
|
||||
transitioned.emit(self, "InairState")
|
||||
|
||||
if play_char.is_on_floor():
|
||||
if play_char.move_dir: transitioned.emit(self, play_char.walk_or_run)
|
||||
else: transitioned.emit(self, "IdleState")
|
||||
|
||||
if play_char.is_on_wall():
|
||||
#if enabled, cut velocity when the play char hit a wall
|
||||
if play_char.hit_wall_cut_velocity:
|
||||
play_char.velocity.x = 0.0
|
||||
play_char.velocity.z = 0.0
|
||||
|
||||
func input_management() -> void:
|
||||
if Input.is_action_just_pressed(play_char.jump_action):
|
||||
func handle_input() -> bool:
|
||||
if input.jump_pressed():
|
||||
jump()
|
||||
|
||||
if Input.is_action_just_released(play_char.jump_action):
|
||||
if input.jump_released():
|
||||
#cut jump in action, allow to manage jump height
|
||||
#(the longer you press the button, the more high the play char goes, similar to the Mario Bros games)
|
||||
play_char.has_cut_jump = true
|
||||
transitioned.emit(self, "InairState")
|
||||
|
||||
func move(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)
|
||||
|
||||
#depending on the previous grounded state (walk or run)
|
||||
#choose corresponding curve, to apply the correct in air values
|
||||
if play_char.move_dir and !play_char.is_on_floor():
|
||||
var in_air_move_speed_val : float
|
||||
var in_air_accel_val : float
|
||||
if play_char.walk_or_run == "WalkState":
|
||||
in_air_move_speed_val = play_char.in_air_move_speed[0].sample(play_char.velocity.length())
|
||||
in_air_accel_val = play_char.in_air_accel[0].sample(play_char.velocity.length())
|
||||
elif play_char.walk_or_run == "RunState":
|
||||
in_air_move_speed_val = play_char.in_air_move_speed[1].sample(play_char.velocity.length())
|
||||
in_air_accel_val = play_char.in_air_accel[1].sample(play_char.velocity.length())
|
||||
|
||||
play_char.velocity.x = lerp(play_char.velocity.x, play_char.move_dir.x * in_air_move_speed_val, in_air_accel_val * delta)
|
||||
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * in_air_move_speed_val, in_air_accel_val * delta)
|
||||
#le freinage de la montee est applique par InairState a son entree
|
||||
player.has_cut_jump = true
|
||||
return fall()
|
||||
return false
|
||||
|
||||
func jump() -> void:
|
||||
#manage the jump behaviour, depending of the different variables and states the character is
|
||||
|
|
@ -79,34 +32,33 @@ func jump() -> void:
|
|||
var can_jump : bool = false #jump condition
|
||||
|
||||
#in air jump
|
||||
if !play_char.is_on_floor():
|
||||
if play_char.coyote_jump_on:
|
||||
play_char.jump_cooldown = play_char.jump_cooldown_ref
|
||||
play_char.coyote_jump_cooldown = -1.0 #so that the character cannot immediately make another coyote jump
|
||||
play_char.coyote_jump_on = false
|
||||
if !player.is_on_floor():
|
||||
if player.coyote_jump_on:
|
||||
player.jump_cooldown = player.jump_cooldown_ref
|
||||
player.coyote_jump_cooldown = -1.0 #so that the character cannot immediately make another coyote jump
|
||||
player.coyote_jump_on = false
|
||||
can_jump = true
|
||||
elif play_char.nb_jumps_in_air_allowed > 0:
|
||||
play_char.nb_jumps_in_air_allowed -= 1
|
||||
play_char.jump_cooldown = play_char.jump_cooldown_ref
|
||||
elif player.nb_jumps_in_air_allowed > 0:
|
||||
player.nb_jumps_in_air_allowed -= 1
|
||||
player.jump_cooldown = player.jump_cooldown_ref
|
||||
can_jump = true
|
||||
|
||||
#on floor jump
|
||||
if play_char.is_on_floor():
|
||||
play_char.jump_cooldown = play_char.jump_cooldown_ref
|
||||
if player.is_on_floor():
|
||||
player.jump_cooldown = player.jump_cooldown_ref
|
||||
can_jump = true
|
||||
|
||||
#jump buffering
|
||||
if play_char.buffered_jump:
|
||||
play_char.buffered_jump = false
|
||||
play_char.nb_jumps_in_air_allowed = play_char.nb_jumps_in_air_allowed_ref
|
||||
if player.buffered_jump:
|
||||
player.buffered_jump = false
|
||||
player.nb_jumps_in_air_allowed = player.nb_jumps_in_air_allowed_ref
|
||||
|
||||
#apply jump
|
||||
if can_jump:
|
||||
play_char.velocity.y = -play_char.jump_velocity
|
||||
player.velocity.y = -player.jump_velocity
|
||||
can_jump = false
|
||||
|
||||
#play squash and strech effect, and display jump particles
|
||||
if play_char.is_on_floor():
|
||||
play_char.squash_and_strech(1.12, 0.1)
|
||||
#play_char.particles_manager.display_particles(play_char.jump_particles, play_char)
|
||||
|
||||
if player.is_on_floor():
|
||||
player.squash_and_strech(1.12, 0.1)
|
||||
#player.particles_manager.display_particles(player.jump_particles, player)
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
# MountedState.gd
|
||||
extends State
|
||||
|
||||
class_name MountedState
|
||||
|
||||
var state_name : String = "Mounted"
|
||||
var play_char : CharacterBody3D
|
||||
var profile : MountProfile # assigné par qui déclenche la monte, avant transitioned.emit(..., "MountedState")
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
play_char.move_speed = profile.mount_speed
|
||||
play_char.move_accel = profile.mount_accel
|
||||
play_char.move_deccel = profile.mount_deccel
|
||||
# instancier profile.mount_scene_override ou profile.species.model_scene,
|
||||
# le positionner, y attacher play_char selon seat_offset...
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://b0slhbduwdqw2
|
||||
48
game/player/states/player_state.gd
Normal file
48
game/player/states/player_state.gd
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
extends State
|
||||
|
||||
class_name PlayerState
|
||||
|
||||
#etat du joueur : ajoute a State le PlayerCharacter, l'input, les hints du HUD et les helpers de mouvement.
|
||||
#les hints sont publiees par StateHints (un composant du joueur) : la StateMachine n'en sait rien.
|
||||
|
||||
signal hints_changed #a emettre quand get_hints() change alors que l'etat est actif
|
||||
|
||||
@export var hints : Array[Hint] = [] #cas simple : se regle dans l'inspecteur
|
||||
@export var keep_previous_hints : bool = false #etats transitoires (Dodge...) : on ne touche pas aux hints affiches
|
||||
|
||||
var player : PlayerCharacter #renseigne par enter() : les etats n'ont plus besoin de le stocker eux-memes
|
||||
var input : InputManagementComponent #lecture de l'input du joueur (raccourci vers player.player_input)
|
||||
|
||||
func enter(new_actor : Node) -> void:
|
||||
player = new_actor as PlayerCharacter
|
||||
input = player.player_input
|
||||
super(new_actor)
|
||||
|
||||
func get_hints() -> Array[Hint]:
|
||||
return hints #a surcharger seulement quand les hints dependent d'une condition
|
||||
|
||||
#---------- transitions du joueur ----------
|
||||
|
||||
func fall() -> bool:
|
||||
#quitter le sol : la posture accroupie est annulee (on retombe debout), puis passage en InairState
|
||||
player.is_crouching = false
|
||||
return request_transition(States.INAIR)
|
||||
|
||||
func request_locomotion() -> bool:
|
||||
#vers Walk ou Run, selon la derniere allure choisie (player.locomotion)
|
||||
return request_transition(player.get_locomotion_state())
|
||||
|
||||
func request_ground_state(moving : bool) -> bool:
|
||||
#retour au sol : Idle si le joueur est immobile, sinon Walk ou Run selon la derniere allure
|
||||
return request_locomotion() if moving else request_transition(States.IDLE)
|
||||
|
||||
#---------- helpers de mouvement ----------
|
||||
|
||||
func accelerate(target_speed : float, accel : float, delta : float) -> void:
|
||||
#vers la vitesse voulue dans la direction de move_dir
|
||||
player.velocity.x = lerpf(player.velocity.x, player.move_dir.x * target_speed, accel * delta)
|
||||
player.velocity.z = lerpf(player.velocity.z, player.move_dir.y * target_speed, accel * delta)
|
||||
|
||||
func slow_down(delta : float) -> void:
|
||||
player.velocity.x = lerpf(player.velocity.x, 0.0, player.move_deccel * delta)
|
||||
player.velocity.z = lerpf(player.velocity.z, 0.0, player.move_deccel * delta)
|
||||
1
game/player/states/player_state.gd.uid
Normal file
1
game/player/states/player_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://8ehfi6tgdtb7
|
||||
|
|
@ -1,70 +1,23 @@
|
|||
extends State
|
||||
extends GroundedState
|
||||
|
||||
class_name RunState
|
||||
|
||||
var state_name : String = "Run"
|
||||
func _init() -> void:
|
||||
allows_crouch = false #pas de crouch en courant
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
func on_enter() -> void:
|
||||
player.is_crouching = false #courir annule la posture accroupie
|
||||
player.locomotion = PlayerCharacter.Locomotion.RUN #utilise par AirborneState pour choisir les courbes en l'air
|
||||
super()
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
func apply_standing_stats() -> void:
|
||||
player.move_speed = player.run_speed
|
||||
player.move_accel = player.run_accel
|
||||
player.move_deccel = player.run_deccel
|
||||
|
||||
verifications()
|
||||
func check_locomotion_switch() -> bool:
|
||||
return !player.is_running() and request_transition(States.WALK)
|
||||
|
||||
func verifications() -> void:
|
||||
#play_char.godot_plush_skin.set_state("run")
|
||||
play_char.walk_or_run = "RunState" #utilise par JumpState/InairState pour choisir les courbes en l'air
|
||||
play_char.move_speed = play_char.run_speed
|
||||
play_char.move_accel = play_char.run_accel
|
||||
play_char.move_deccel = play_char.run_deccel
|
||||
|
||||
play_char.floor_snap_length = 1.0
|
||||
if play_char.jump_cooldown > 0.0: play_char.jump_cooldown = -1.0
|
||||
if play_char.nb_jumps_in_air_allowed < play_char.nb_jumps_in_air_allowed_ref: play_char.nb_jumps_in_air_allowed = play_char.nb_jumps_in_air_allowed_ref
|
||||
if play_char.coyote_jump_cooldown < play_char.coyote_jump_cooldown_ref: play_char.coyote_jump_cooldown = play_char.coyote_jump_cooldown_ref
|
||||
if play_char.has_cut_jump: play_char.has_cut_jump = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies()
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
|
||||
input_management()
|
||||
|
||||
move(delta)
|
||||
|
||||
func applies() -> void:
|
||||
if !play_char.is_on_floor() and !play_char.is_on_wall():
|
||||
if play_char.velocity.y < 0.0:
|
||||
transitioned.emit(self, "InairState")
|
||||
|
||||
if play_char.is_on_floor():
|
||||
if play_char.jump_buff_on:
|
||||
#apply jump buffering
|
||||
play_char.buffered_jump = true
|
||||
play_char.jump_buff_on = false
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
func input_management() -> void:
|
||||
if Input.is_action_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0:
|
||||
transitioned.emit(self, "DodgeState")
|
||||
|
||||
if Input.is_action_pressed(play_char.jump_action) if play_char.auto_jump else Input.is_action_just_pressed(play_char.jump_action):
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
if Input.is_action_just_pressed(play_char.aim_cam_action):
|
||||
transitioned.emit(self, "AimState")
|
||||
|
||||
if play_char.walk_or_run == "WalkState":
|
||||
transitioned.emit(self, "WalkState")
|
||||
|
||||
func move(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():
|
||||
#apply smooth move
|
||||
play_char.velocity.x = lerp(play_char.velocity.x, play_char.move_dir.x * play_char.move_speed, play_char.move_accel * delta)
|
||||
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * play_char.move_speed, play_char.move_accel * delta)
|
||||
else:
|
||||
if !play_char.continious_run: play_char.walk_or_run = "WalkState"
|
||||
transitioned.emit(self, "IdleState")
|
||||
func on_no_move(_delta : float) -> void:
|
||||
if !player.continious_run: player.locomotion = PlayerCharacter.Locomotion.WALK
|
||||
request_transition(States.IDLE)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,26 @@ extends Node
|
|||
|
||||
class_name State
|
||||
|
||||
signal transitioned #it's normal if this signal isn't used in this script, because it's purpose is to be used in the scripts that extends from the State class
|
||||
signal hints_changed #a emettre quand get_hints() change alors que l'etat est actif
|
||||
#etat generique d'une StateMachine : il ne connait ni le joueur, ni le HUD, ni le jeu. Reutilisable pour un PNJ, un pokemon sauvage...
|
||||
#pour le joueur, voir PlayerState (qui ajoute le joueur, l'input, les hints et les helpers de mouvement).
|
||||
#
|
||||
#un etat surcharge on_enter() / exit() / update() / physics_update() et demande ses transitions avec request_transition().
|
||||
|
||||
@export var hints : Array[Hint] = [] #cas simple : se regle dans l'inspecteur
|
||||
@export var keep_previous_hints : bool = false #etats transitoires (Dodge...) : on ne touche pas aux hints affiches
|
||||
var actor : Node #le node pilote par la machine (CharacterBody3D, joueur, PNJ...), renseigne par enter()
|
||||
var machine : StateMachine #renseigne par la StateMachine
|
||||
|
||||
func enter(_char_reference : CharacterBody3D) -> void:
|
||||
#donnees echangees entre etats (renseignees par la StateMachine, jamais ecrites a la main dans un autre etat) :
|
||||
var enter_data : Dictionary = {} #recues de l'etat precedent, lisibles dans on_enter() et ensuite
|
||||
var outgoing_data : Dictionary = {} #envoyees a l'etat suivant par request_transition(), lisibles dans exit()
|
||||
var next_state_name : String = "" #destination, renseignee avant exit() : exit() sait ou on va
|
||||
|
||||
#---------- cycle de vie ----------
|
||||
|
||||
func enter(new_actor : Node) -> void:
|
||||
actor = new_actor
|
||||
on_enter()
|
||||
|
||||
func on_enter() -> void: #a surcharger a la place de enter() : actor est deja assigne
|
||||
pass
|
||||
|
||||
func exit() -> void:
|
||||
|
|
@ -20,5 +33,11 @@ func update(_delta : float) -> void:
|
|||
func physics_update(_delta : float) -> void:
|
||||
pass
|
||||
|
||||
func get_hints() -> Array[Hint]:
|
||||
return hints #a surcharger seulement quand les hints dependent d'une condition
|
||||
#---------- transitions ----------
|
||||
|
||||
func request_transition(new_state_name : String, data : Dictionary = {}) -> bool:
|
||||
#demande une transition et renvoie true seulement si elle a vraiment eu lieu
|
||||
#(la StateMachine peut la refuser : etat bloque, etat inconnu, ou demande d'un etat qui n'est plus l'etat courant)
|
||||
#data : transmise a l'etat suivant (enter_data), et lisible par exit() de celui-ci (outgoing_data)
|
||||
if machine == null: return false
|
||||
return machine.request(self, new_state_name, data)
|
||||
|
|
|
|||
|
|
@ -2,91 +2,87 @@ extends Node
|
|||
|
||||
class_name StateMachine
|
||||
|
||||
@export var initial_state : State
|
||||
@export var throw_action : StringName = &"player_throw"
|
||||
#machine a etats generique : elle ne connait ni le joueur, ni le HUD, ni le lancer. Reutilisable pour n'importe quel acteur
|
||||
#(joueur, PNJ, pokemon sauvage...) : poser la StateMachine sous l'acteur, y mettre des nodes qui heritent de State.
|
||||
#ce qui est propre au joueur vit ailleurs : le lancer dans GroundedState, les hints dans StateHints.
|
||||
|
||||
const THROW_ENTRY_STATES : Array[String] = ["Idle", "Walk", "Run", "Crouch"] #etats depuis lesquels RT lance sans viser
|
||||
signal state_changed(from : State, to : State) #from est null pour l'etat initial
|
||||
|
||||
@export var initial_state : State
|
||||
@export var actor : Node #le node pilote par la machine, passe a chaque etat. Par defaut : le parent
|
||||
|
||||
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 = $".."
|
||||
var curr_state_name : String: #nom du node de l'etat courant (ex : "IdleState")
|
||||
get: return String(curr_state.name) if curr_state else ""
|
||||
var states : Dictionary = {} #nom du node (casse exacte) -> State
|
||||
var blocked_states : Dictionary = {} #noms des etats dont l'entree est refusee
|
||||
var _warned : Dictionary = {} #noms introuvables deja signales : un seul avertissement par nom, pas un par frame
|
||||
|
||||
func _ready() -> void:
|
||||
if actor == null: actor = get_parent()
|
||||
|
||||
#get all the state childrens
|
||||
for child in get_children():
|
||||
if child is State:
|
||||
states[child.name.to_lower()] = child
|
||||
child.transitioned.connect(on_state_child_transition)
|
||||
child.hints_changed.connect(_on_state_hints_changed.bind(child))
|
||||
states[String(child.name)] = child
|
||||
child.machine = self
|
||||
|
||||
#if initial state, transition to it
|
||||
if initial_state:
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
initial_state.enter(char_ref)
|
||||
initial_state.enter(actor)
|
||||
curr_state = initial_state
|
||||
curr_state_name = curr_state.state_name
|
||||
publish_hints()
|
||||
state_changed.emit(null, initial_state)
|
||||
|
||||
func _process(delta : float) -> void:
|
||||
if curr_state: curr_state.update(delta)
|
||||
|
||||
func _physics_process(delta : float) -> void:
|
||||
if curr_state: curr_state.physics_update(delta)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if curr_state:
|
||||
if try_start_throw(): return
|
||||
curr_state.physics_update(delta)
|
||||
#---------- requetes ----------
|
||||
|
||||
func try_start_throw() -> bool:
|
||||
if !(curr_state_name in THROW_ENTRY_STATES): return false
|
||||
if !Input.is_action_just_pressed(throw_action): return false
|
||||
if !char_ref.is_on_floor(): return false
|
||||
func has_state(state_name : String) -> bool:
|
||||
return states.has(state_name)
|
||||
|
||||
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
||||
if selected == null: return false
|
||||
func is_in(state_name : String) -> bool:
|
||||
return curr_state_name == state_name
|
||||
|
||||
#pokemon deja dehors : l'appui le rappelle au lieu de lancer
|
||||
var already_out : SentPokemon = SentPokemon.find_for(get_tree(), selected)
|
||||
if already_out:
|
||||
already_out.recall()
|
||||
return false
|
||||
|
||||
if !states.has("throwstate"): return false
|
||||
on_state_child_transition(curr_state, "ThrowState")
|
||||
return true
|
||||
|
||||
func on_state_child_transition(state : State, new_state_name : String) -> void:
|
||||
#manage the transition from one state to another
|
||||
|
||||
if state != curr_state: return
|
||||
|
||||
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()
|
||||
|
||||
#enter the new state
|
||||
new_state.enter(char_ref)
|
||||
|
||||
curr_state = new_state
|
||||
curr_state_name = curr_state.state_name
|
||||
publish_hints()
|
||||
func is_blocked(state_name : String) -> bool:
|
||||
return blocked_states.has(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())
|
||||
if blocked: blocked_states[state_name] = true
|
||||
else: blocked_states.erase(state_name)
|
||||
|
||||
func _on_state_hints_changed(state : State) -> void:
|
||||
if state == curr_state: publish_hints()
|
||||
#---------- transitions ----------
|
||||
|
||||
func publish_hints() -> void:
|
||||
if curr_state and !curr_state.keep_previous_hints:
|
||||
Hints.show_hints(self, curr_state.get_hints())
|
||||
func request(from : State, new_state_name : String, data : Dictionary = {}) -> bool:
|
||||
#appelee par State.request_transition() : renvoie true si la transition a eu lieu
|
||||
if from != curr_state: return false #l'etat demandeur n'est plus l'etat courant (transition deja faite pendant cette frame)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
Hints.clear_hints(self)
|
||||
var new_state : State = states.get(new_state_name)
|
||||
if new_state == null:
|
||||
_warn_unknown_state(from, new_state_name)
|
||||
return false
|
||||
|
||||
if is_blocked(new_state_name): return false
|
||||
|
||||
#exit the current state : il connait sa destination, et les donnees qu'il envoie (outgoing_data) sont lisibles
|
||||
from.next_state_name = new_state_name
|
||||
from.outgoing_data = data
|
||||
from.exit()
|
||||
from.outgoing_data = {}
|
||||
|
||||
#enter the new state : il recoit les donnees de l'etat qu'on quitte
|
||||
new_state.enter_data = data
|
||||
new_state.enter(actor)
|
||||
|
||||
curr_state = new_state
|
||||
state_changed.emit(from, new_state)
|
||||
return true
|
||||
|
||||
func _warn_unknown_state(from : State, new_state_name : String) -> void:
|
||||
if _warned.has(new_state_name): return
|
||||
_warned[new_state_name] = true
|
||||
push_warning("StateMachine : %s demande une transition vers '%s', mais aucun node d'etat ne porte ce nom (etats disponibles : %s). Verifier le nom du node dans la scene." % [from.name, new_state_name, ", ".join(PackedStringArray(states.keys()))])
|
||||
|
|
|
|||
15
game/player/states/states.gd
Normal file
15
game/player/states/states.gd
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class_name States
|
||||
|
||||
#noms des etats : un etat s'identifie TOUJOURS par le nom de son node dans la StateMachine, avec la casse exacte.
|
||||
#on ecrit States.IDLE et jamais "IdleState" a la main : une faute de frappe devient une erreur a l'edition au lieu d'une
|
||||
#transition ignoree en silence. Pour un nouvel etat (ex : SwimState), ajouter sa constante ici, avec le nom du node.
|
||||
#
|
||||
#les groupes d'etats ne sont plus des listes de noms : on teste le type (curr_state is GroundedState / AirborneState).
|
||||
|
||||
const IDLE := "IdleState"
|
||||
const WALK := "WalkState"
|
||||
const RUN := "RunState"
|
||||
const JUMP := "JumpState"
|
||||
const INAIR := "InairState"
|
||||
const DODGE := "DodgeState"
|
||||
const THROW := "ThrowState"
|
||||
1
game/player/states/states.gd.uid
Normal file
1
game/player/states/states.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dkwn6qhgojdt1
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
extends State
|
||||
extends PlayerState
|
||||
|
||||
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 var cancel_hint : Hint #cancel_throw.tres
|
||||
|
|
@ -22,92 +16,35 @@ var cancel_action : StringName: #l'action vient du Hint : une seule definition
|
|||
@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
|
||||
@export var free_hit_radius : float = 2.0 #lancer sans cible verrouillee : rayon autour du point d'atterrissage pour trouver une cible de combat
|
||||
|
||||
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)
|
||||
var resume_released : bool = false #mis a true par AimState : RT a ete relachee pendant la visee, le lancer demarre directement en windup
|
||||
|
||||
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))
|
||||
InputRegistry.ensure(cancel_action, [InputRegistry.pad_button(JOY_BUTTON_B), InputRegistry.mouse(MOUSE_BUTTON_RIGHT)])
|
||||
|
||||
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
|
||||
func on_enter() -> void:
|
||||
_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")
|
||||
|
||||
if resume_released:
|
||||
resume_released = false
|
||||
release() #fige la cible verrouillee / le point vise et lance le windup
|
||||
player.floor_snap_length = 1.0
|
||||
#player.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()
|
||||
AimReticle.hide_instance() #si la visee est encore active, GroundedState le reaffiche a la frame suivante
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
if applies(): return
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
player.gravity_apply(delta)
|
||||
|
||||
if _pokemon == null:
|
||||
slow_down(delta)
|
||||
|
|
@ -126,8 +63,8 @@ func physics_update(delta : float) -> void:
|
|||
finish() #annule avant que la balle parte
|
||||
return
|
||||
|
||||
if _thrown and Input.is_action_just_pressed(aim_action):
|
||||
finish() #balle partie : on coupe la recuperation, finish() bascule en AimState
|
||||
if _thrown and input.aim_pressed():
|
||||
finish() #balle partie : on coupe la recuperation pour pouvoir viser tout de suite
|
||||
return
|
||||
|
||||
_elapsed += delta
|
||||
|
|
@ -137,10 +74,7 @@ func physics_update(delta : float) -> void:
|
|||
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
|
||||
return !player.is_on_floor() and player.velocity.y < 0.0 and fall()
|
||||
|
||||
#---------- charge ----------
|
||||
|
||||
|
|
@ -149,67 +83,37 @@ func charge(delta : float) -> void:
|
|||
finish() #annule : rien n'est lance
|
||||
return
|
||||
|
||||
if Input.is_action_just_pressed(aim_action):
|
||||
enter_aim_while_charging()
|
||||
return
|
||||
if input.aim_pressed(): player.aim_targeting.begin_aim() #on peut se mettre a viser en pleine charge, sans perdre la charge
|
||||
|
||||
if !Input.is_action_pressed(throw_action):
|
||||
if !input.throw_held():
|
||||
release()
|
||||
return
|
||||
|
||||
move_while_charging(delta)
|
||||
update_reticle()
|
||||
|
||||
func enter_aim_while_charging() -> void:
|
||||
#on passe en visee sans perdre la charge : AimState se charge de relancer le lancer au relachement de RT
|
||||
var aim : Node = play_char.state_machine.states.get("aimstate")
|
||||
if aim == null: return
|
||||
aim.came_from_crouch = _came_from_crouch
|
||||
aim.charging_throw = true
|
||||
_keep_aim = true
|
||||
transitioned.emit(self, "AimState")
|
||||
if player.is_aiming: player.aim_targeting.tick() #cible verrouillee / changement de cible pendant la charge
|
||||
else: player.aim_targeting.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()
|
||||
_target = player.aim_target if is_instance_valid(player.aim_target) else null
|
||||
_free_aim_point = player.throw_resolver.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)
|
||||
player.move_dir = input.read_move_dir()
|
||||
|
||||
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)
|
||||
if player.move_dir and player.is_on_floor():
|
||||
accelerate(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 input.aim_held(): player.aim_targeting.begin_aim() #visee enfoncee : elle continue apres le lancer (sans effet si deja active)
|
||||
|
||||
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)
|
||||
#accroupi ou non : la posture est conservee, Idle / Walk la reappliquent
|
||||
var dir : Vector2 = input.read_move_raw()
|
||||
request_ground_state(dir != Vector2.ZERO)
|
||||
|
||||
#---------- lancer ----------
|
||||
|
||||
|
|
@ -217,88 +121,8 @@ func throw_ball() -> void:
|
|||
_thrown = true
|
||||
hints_changed.emit()
|
||||
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
|
||||
|
||||
#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("ThrowState : 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("aim_targets"):
|
||||
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
|
||||
|
||||
#---------- 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
|
||||
#la balle est autonome : son atterrissage et l'envoi du pokemon sont geres par ThrowResolver, meme si le joueur change d'etat entre-temps
|
||||
player.throw_resolver.throw_ball(_pokemon, _target, _free_aim_point)
|
||||
|
||||
func get_hints() -> Array[Hint]:
|
||||
var list : Array[Hint] = []
|
||||
|
|
|
|||
|
|
@ -1,73 +1,14 @@
|
|||
extends State
|
||||
extends GroundedState
|
||||
|
||||
class_name WalkState
|
||||
|
||||
var state_name : String = "Walk"
|
||||
func apply_standing_stats() -> void:
|
||||
player.move_speed = player.walk_speed
|
||||
player.move_accel = player.walk_accel
|
||||
player.move_deccel = player.walk_deccel
|
||||
|
||||
var play_char : CharacterBody3D
|
||||
|
||||
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||
play_char = play_char_ref
|
||||
|
||||
verifications()
|
||||
|
||||
func verifications() -> void:
|
||||
#play_char.godot_plush_skin.set_state("walk")
|
||||
play_char.move_speed = play_char.walk_speed
|
||||
play_char.move_accel = play_char.walk_accel
|
||||
play_char.move_deccel = play_char.walk_deccel
|
||||
|
||||
play_char.floor_snap_length = 1.0
|
||||
if play_char.jump_cooldown > 0.0: play_char.jump_cooldown = -1.0
|
||||
if play_char.nb_jumps_in_air_allowed < play_char.nb_jumps_in_air_allowed_ref: play_char.nb_jumps_in_air_allowed = play_char.nb_jumps_in_air_allowed_ref
|
||||
if play_char.coyote_jump_cooldown < play_char.coyote_jump_cooldown_ref: play_char.coyote_jump_cooldown = play_char.coyote_jump_cooldown_ref
|
||||
if play_char.has_cut_jump: play_char.has_cut_jump = false
|
||||
#if play_char.movement_dust.emitting: play_char.movement_dust.emitting = false
|
||||
|
||||
func physics_update(delta : float) -> void:
|
||||
applies()
|
||||
|
||||
play_char.gravity_apply(delta)
|
||||
|
||||
input_management()
|
||||
|
||||
move(delta)
|
||||
|
||||
func applies() -> void:
|
||||
if !play_char.is_on_floor() and !play_char.is_on_wall():
|
||||
if play_char.velocity.y < 0.0:
|
||||
transitioned.emit(self, "InairState")
|
||||
|
||||
if play_char.is_on_floor():
|
||||
if play_char.jump_buff_on:
|
||||
#apply jump buffering
|
||||
play_char.buffered_jump = true
|
||||
play_char.jump_buff_on = false
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
func input_management() -> void:
|
||||
if Input.is_action_just_pressed(play_char.dodge_action) and play_char.dodge_cooldown <= 0.0:
|
||||
transitioned.emit(self, "DodgeState")
|
||||
|
||||
if Input.is_action_just_pressed(play_char.crouch_action):
|
||||
transitioned.emit(self, "CrouchState")
|
||||
|
||||
if Input.is_action_pressed(play_char.jump_action) if play_char.auto_jump else Input.is_action_just_pressed(play_char.jump_action) :
|
||||
transitioned.emit(self, "JumpState")
|
||||
|
||||
if Input.is_action_just_pressed(play_char.aim_cam_action):
|
||||
transitioned.emit(self, "AimState")
|
||||
|
||||
if play_char.walk_or_run == "RunState":
|
||||
transitioned.emit(self, "RunState")
|
||||
|
||||
func move(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():
|
||||
#apply smooth move
|
||||
play_char.velocity.x = lerp(play_char.velocity.x, play_char.move_dir.x * play_char.move_speed, play_char.move_accel * delta)
|
||||
play_char.velocity.z = lerp(play_char.velocity.z, play_char.move_dir.y * play_char.move_speed, play_char.move_accel * delta)
|
||||
else:
|
||||
transitioned.emit(self, "IdleState")
|
||||
func check_locomotion_switch() -> bool:
|
||||
return player.is_running() and request_transition(States.RUN)
|
||||
|
||||
func on_no_move(_delta : float) -> void:
|
||||
request_transition(States.IDLE)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ player_target={
|
|||
player_throw={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
|
||||
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(455, 12),"global_position":Vector2(464, 60),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
player_crouch={
|
||||
|
|
|
|||
Loading…
Reference in a new issue