211 lines
8.4 KiB
GDScript
211 lines
8.4 KiB
GDScript
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_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
|
|
|
|
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
|
|
|
|
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:
|
|
if keep_aim_on_exit:
|
|
keep_aim_on_exit = false
|
|
return
|
|
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()
|
|
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 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 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 start_throw() -> bool:
|
|
if GameData.get_selected_pokemon() == null: return false #personne a envoyer
|
|
if !play_char.state_machine.states.has("throwstate"): return false #pas de node ThrowState dans la StateMachine
|
|
keep_aim_on_exit = true
|
|
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)
|
|
|
|
#---------- 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)
|