diff --git a/data/mounts/charizard.tres b/data/mounts/charizard.tres deleted file mode 100644 index 1576557..0000000 --- a/data/mounts/charizard.tres +++ /dev/null @@ -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" diff --git a/game/mount/mount_detector.gd b/game/mount/mount_detector.gd new file mode 100644 index 0000000..f2149c3 --- /dev/null +++ b/game/mount/mount_detector.gd @@ -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) diff --git a/game/mount/mount_detector.gd.uid b/game/mount/mount_detector.gd.uid new file mode 100644 index 0000000..ebc6cbc --- /dev/null +++ b/game/mount/mount_detector.gd.uid @@ -0,0 +1 @@ +uid://dwfv5631jam2c diff --git a/game/mount/mount_rider.gd b/game/mount/mount_rider.gd index 2abf744..f58f197 100644 --- a/game/mount/mount_rider.gd +++ b/game/mount/mount_rider.gd @@ -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) diff --git a/game/mount/mount_seat.gd b/game/mount/mount_seat.gd new file mode 100644 index 0000000..5ec4b98 --- /dev/null +++ b/game/mount/mount_seat.gd @@ -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 diff --git a/game/mount/mount_seat.gd.uid b/game/mount/mount_seat.gd.uid new file mode 100644 index 0000000..2d2ee01 --- /dev/null +++ b/game/mount/mount_seat.gd.uid @@ -0,0 +1 @@ +uid://bjfm64yqkproy diff --git a/game/player/aim/aim_reticule.gd b/game/player/aim/aim_reticule.gd index 198b2dc..5387cf0 100644 --- a/game/player/aim/aim_reticule.gd +++ b/game/player/aim/aim_reticule.gd @@ -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 diff --git a/game/player/aim/aim_targeting.gd b/game/player/aim/aim_targeting.gd new file mode 100644 index 0000000..d4385ea --- /dev/null +++ b/game/player/aim/aim_targeting.gd @@ -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) diff --git a/game/player/aim/aim_targeting.gd.uid b/game/player/aim/aim_targeting.gd.uid new file mode 100644 index 0000000..e298fc5 --- /dev/null +++ b/game/player/aim/aim_targeting.gd.uid @@ -0,0 +1 @@ +uid://qh738m0cmw8b diff --git a/game/player/aim/throw_resolver.gd b/game/player/aim/throw_resolver.gd new file mode 100644 index 0000000..280d8a8 --- /dev/null +++ b/game/player/aim/throw_resolver.gd @@ -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 diff --git a/game/player/aim/throw_resolver.gd.uid b/game/player/aim/throw_resolver.gd.uid new file mode 100644 index 0000000..5363f5b --- /dev/null +++ b/game/player/aim/throw_resolver.gd.uid @@ -0,0 +1 @@ +uid://bxt8qv1xyw2nl diff --git a/game/player/camera/camera_holder.gd b/game/player/camera/camera_holder.gd index 2ff41af..864915b 100644 --- a/game/player/camera/camera_holder.gd +++ b/game/player/camera/camera_holder.gd @@ -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: diff --git a/game/player/camera/camera_holder.tscn b/game/player/camera/camera_holder.tscn index 13e9ea5..7e01b9f 100644 --- a/game/player/camera/camera_holder.tscn +++ b/game/player/camera/camera_holder.tscn @@ -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 diff --git a/game/player/input/input_management_component.gd b/game/player/input/input_management_component.gd index b6c0c2b..e20ef82 100644 --- a/game/player/input/input_management_component.gd +++ b/game/player/input/input_management_component.gd @@ -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 - - 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 +#---------- camera ---------- + +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) \ No newline at end of file diff --git a/game/player/input/input_registry.gd b/game/player/input/input_registry.gd new file mode 100644 index 0000000..a9f8738 --- /dev/null +++ b/game/player/input/input_registry.gd @@ -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 diff --git a/game/player/input/input_registry.gd.uid b/game/player/input/input_registry.gd.uid new file mode 100644 index 0000000..5f450fb --- /dev/null +++ b/game/player/input/input_registry.gd.uid @@ -0,0 +1 @@ +uid://dj1gn8g0eblyi diff --git a/game/player/input/pokemon_recall.gd b/game/player/input/pokemon_recall.gd new file mode 100644 index 0000000..e0b3d60 --- /dev/null +++ b/game/player/input/pokemon_recall.gd @@ -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 diff --git a/game/player/input/pokemon_recall.gd.uid b/game/player/input/pokemon_recall.gd.uid new file mode 100644 index 0000000..2d73ff1 --- /dev/null +++ b/game/player/input/pokemon_recall.gd.uid @@ -0,0 +1 @@ +uid://cwbji81rt6t36 diff --git a/game/player/input/recall_input.gd b/game/player/input/recall_input.gd index 8d542a4..25b77bc 100644 --- a/game/player/input/recall_input.gd +++ b/game/player/input/recall_input.gd @@ -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()) \ No newline at end of file diff --git a/game/player/input/state_hints.gd b/game/player/input/state_hints.gd new file mode 100644 index 0000000..3f4c28e --- /dev/null +++ b/game/player/input/state_hints.gd @@ -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()) diff --git a/game/player/input/state_hints.gd.uid b/game/player/input/state_hints.gd.uid new file mode 100644 index 0000000..c7bd251 --- /dev/null +++ b/game/player/input/state_hints.gd.uid @@ -0,0 +1 @@ +uid://b0j1jxv6uu2y8 diff --git a/game/player/player_character.gd b/game/player/player_character.gd index 4cf5123..3049be3 100644 --- a/game/player/player_character.gd +++ b/game/player/player_character.gd @@ -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 diff --git a/game/player/player_character.tscn b/game/player/player_character.tscn index c40a147..4046815 100644 --- a/game/player/player_character.tscn +++ b/game/player/player_character.tscn @@ -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") diff --git a/game/player/player_component.gd b/game/player/player_component.gd new file mode 100644 index 0000000..6e5b258 --- /dev/null +++ b/game/player/player_component.gd @@ -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 diff --git a/game/player/player_component.gd.uid b/game/player/player_component.gd.uid new file mode 100644 index 0000000..1602579 --- /dev/null +++ b/game/player/player_component.gd.uid @@ -0,0 +1 @@ +uid://jsb4l4g023o3 diff --git a/game/player/states/aim_state.gd b/game/player/states/aim_state.gd deleted file mode 100644 index 346ba12..0000000 --- a/game/player/states/aim_state.gd +++ /dev/null @@ -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 diff --git a/game/player/states/aim_state.gd.uid b/game/player/states/aim_state.gd.uid deleted file mode 100644 index c7b18cf..0000000 --- a/game/player/states/aim_state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dn5idrvu68k83 diff --git a/game/player/states/airborne_state.gd b/game/player/states/airborne_state.gd new file mode 100644 index 0000000..a574e4e --- /dev/null +++ b/game/player/states/airborne_state.gd @@ -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) diff --git a/game/player/states/airborne_state.gd.uid b/game/player/states/airborne_state.gd.uid new file mode 100644 index 0000000..d94fa81 --- /dev/null +++ b/game/player/states/airborne_state.gd.uid @@ -0,0 +1 @@ +uid://d2fq1w57bakgq diff --git a/game/player/states/crouch_state.gd b/game/player/states/crouch_state.gd deleted file mode 100644 index d906ae4..0000000 --- a/game/player/states/crouch_state.gd +++ /dev/null @@ -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) diff --git a/game/player/states/crouch_state.gd.uid b/game/player/states/crouch_state.gd.uid deleted file mode 100644 index f804a98..0000000 --- a/game/player/states/crouch_state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d0577tj0c23bu diff --git a/game/player/states/dodge_state.gd b/game/player/states/dodge_state.gd index 16465ec..cb33ec6 100644 --- a/game/player/states/dodge_state.gd +++ b/game/player/states/dodge_state.gd @@ -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) diff --git a/game/player/states/grounded_state.gd b/game/player/states/grounded_state.gd new file mode 100644 index 0000000..d0b1770 --- /dev/null +++ b/game/player/states/grounded_state.gd @@ -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) diff --git a/game/player/states/grounded_state.gd.uid b/game/player/states/grounded_state.gd.uid new file mode 100644 index 0000000..df7cd67 --- /dev/null +++ b/game/player/states/grounded_state.gd.uid @@ -0,0 +1 @@ +uid://c4f2x2rqadgrx diff --git a/game/player/states/idle_state.gd b/game/player/states/idle_state.gd index 34491dd..7cf5118 100644 --- a/game/player/states/idle_state.gd +++ b/game/player/states/idle_state.gd @@ -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 diff --git a/game/player/states/inair_state.gd b/game/player/states/inair_state.gd new file mode 100644 index 0000000..bd3db16 --- /dev/null +++ b/game/player/states/inair_state.gd @@ -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 diff --git a/game/player/states/inair_state.gd.uid b/game/player/states/inair_state.gd.uid new file mode 100644 index 0000000..4c26c10 --- /dev/null +++ b/game/player/states/inair_state.gd.uid @@ -0,0 +1 @@ +uid://sixraavy7k2h diff --git a/game/player/states/jump_state.gd b/game/player/states/jump_state.gd index 667fdd9..56fbf56 100644 --- a/game/player/states/jump_state.gd +++ b/game/player/states/jump_state.gd @@ -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 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 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 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) diff --git a/game/player/states/mounted_state.gd b/game/player/states/mounted_state.gd deleted file mode 100644 index 3b6a1f4..0000000 --- a/game/player/states/mounted_state.gd +++ /dev/null @@ -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... diff --git a/game/player/states/mounted_state.gd.uid b/game/player/states/mounted_state.gd.uid deleted file mode 100644 index f595117..0000000 --- a/game/player/states/mounted_state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b0slhbduwdqw2 diff --git a/game/player/states/player_state.gd b/game/player/states/player_state.gd new file mode 100644 index 0000000..21fd16c --- /dev/null +++ b/game/player/states/player_state.gd @@ -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) diff --git a/game/player/states/player_state.gd.uid b/game/player/states/player_state.gd.uid new file mode 100644 index 0000000..5b891c2 --- /dev/null +++ b/game/player/states/player_state.gd.uid @@ -0,0 +1 @@ +uid://8ehfi6tgdtb7 diff --git a/game/player/states/run_state.gd b/game/player/states/run_state.gd index 59c630e..0753684 100644 --- a/game/player/states/run_state.gd +++ b/game/player/states/run_state.gd @@ -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 - - verifications() - -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 apply_standing_stats() -> void: + player.move_speed = player.run_speed + player.move_accel = player.run_accel + player.move_deccel = player.run_deccel + +func check_locomotion_switch() -> bool: + return !player.is_running() and request_transition(States.WALK) + +func on_no_move(_delta : float) -> void: + if !player.continious_run: player.locomotion = PlayerCharacter.Locomotion.WALK + request_transition(States.IDLE) diff --git a/game/player/states/state.gd b/game/player/states/state.gd index a08fa22..267df5e 100644 --- a/game/player/states/state.gd +++ b/game/player/states/state.gd @@ -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) diff --git a/game/player/states/state_machine.gd b/game/player/states/state_machine.gd index 757a13c..c8318f3 100644 --- a/game/player/states/state_machine.gd +++ b/game/player/states/state_machine.gd @@ -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 - - var selected : PokemonInstance = GameData.get_selected_pokemon() - if selected == null: return false - - #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 has_state(state_name : String) -> bool: + return states.has(state_name) + +func is_in(state_name : String) -> bool: + return curr_state_name == state_name + +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) + + 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 _exit_tree() -> void: - Hints.clear_hints(self) +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()))]) diff --git a/game/player/states/states.gd b/game/player/states/states.gd new file mode 100644 index 0000000..0cdb35c --- /dev/null +++ b/game/player/states/states.gd @@ -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" diff --git a/game/player/states/states.gd.uid b/game/player/states/states.gd.uid new file mode 100644 index 0000000..a2de7c2 --- /dev/null +++ b/game/player/states/states.gd.uid @@ -0,0 +1 @@ +uid://dkwn6qhgojdt1 diff --git a/game/player/states/throw_state.gd b/game/player/states/throw_state.gd index c9997f4..87cfe88 100644 --- a/game/player/states/throw_state.gd +++ b/game/player/states/throw_state.gd @@ -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") + player.floor_snap_length = 1.0 + #player.godot_plush_skin.set_state("throw") - if resume_released: - resume_released = false - release() #fige la cible verrouillee / le point vise et lance le windup - 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,156 +83,46 @@ 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") + #accroupi ou non : la posture est conservee, Idle / Walk la reappliquent + var dir : Vector2 = input.read_move_raw() + request_ground_state(dir != Vector2.ZERO) -func update_reticle() -> void: - if is_instance_valid(play_char.aim_target): - AimReticle.hide_instance() #cible verrouillee : son propre indicateur suffit - else: - AimReticle.show_instance(self) - #---------- lancer ---------- func throw_ball() -> void: _thrown = true 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] = [] diff --git a/game/player/states/walk_state.gd b/game/player/states/walk_state.gd index 5dd8b02..4f2eb98 100644 --- a/game/player/states/walk_state.gd +++ b/game/player/states/walk_state.gd @@ -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 check_locomotion_switch() -> bool: + return player.is_running() and request_transition(States.RUN) -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 on_no_move(_delta : float) -> void: + request_transition(States.IDLE)