Compare commits
6 commits
dfa5a48888
...
c4c2bf5f7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c2bf5f7b | ||
|
|
c4de43f990 | ||
|
|
26f0f31ce7 | ||
|
|
1a1ecf602f | ||
|
|
0398ac3345 | ||
|
|
eb6432e7a0 |
80
game/battle/battle.md
Normal file
80
game/battle/battle.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Gérer l'équipe d'un PNJ ou d'un Pokémon sauvage
|
||||||
|
|
||||||
|
*Tales of Pokémon · système de combat*
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Tout se décrit avec des ressources, sans écrire de code :
|
||||||
|
|
||||||
|
- **`TrainerPartyEntry`** : un Pokémon « modèle » (espèce, niveau, surnom, nature, capacités).
|
||||||
|
- **`TrainerData`** : un dresseur PNJ, avec une liste de `TrainerPartyEntry` (6 maximum).
|
||||||
|
- **`BattleEncounter`** : la rencontre complète (qui est en face, format du combat, règles).
|
||||||
|
- **`AimTarget`** : la cible du monde. Son champ `Encounter` contient le `BattleEncounter`, et son champ `World Actor` le node visible dans le monde (caché pendant le combat).
|
||||||
|
|
||||||
|
Ces ressources sont des modèles : à chaque combat, de **vrais Pokémon neufs** sont créés à partir d'elles. Un PNJ retrouve donc son équipe intacte à chaque revanche, et un même `BattleEncounter` peut être partagé entre plusieurs cibles.
|
||||||
|
|
||||||
|
Pour créer une ressource : clic droit dans le dock Système de fichiers > *Nouvelle ressource…*, choisir le type, puis l'enregistrer en `.tres`.
|
||||||
|
|
||||||
|
## Un Pokémon sauvage (1 contre 1)
|
||||||
|
|
||||||
|
1. Crée un `TrainerPartyEntry` : choisis l'**espèce** et le **niveau**.
|
||||||
|
2. Crée un `BattleEncounter` et ajoute l'entrée à `Wild Pokemon`.
|
||||||
|
3. Sur l'`AimTarget` de l'ennemi : assigne le `BattleEncounter` à `Encounter` et le node de l'ennemi à `World Actor`.
|
||||||
|
4. Laisse `Can Flee` coché : le joueur peut fuir un Pokémon sauvage.
|
||||||
|
|
||||||
|
Un Pokémon sauvage n'a pas de banc, il ne change donc jamais de Pokémon.
|
||||||
|
|
||||||
|
## Plusieurs Pokémon sauvages (1 contre plusieurs)
|
||||||
|
|
||||||
|
Ajoute **plusieurs entrées** dans `Wild Pokemon` : chacune devient un adversaire à part entière avec son propre emplacement (3 entrées = 1 contre 3). Chaque adversaire attaque, et peut être ciblé, séparément.
|
||||||
|
|
||||||
|
## Un dresseur PNJ
|
||||||
|
|
||||||
|
1. Crée un `TrainerData` : renseigne `Id`, `Names` (un nom par langue, par exemple `fr` et `en`) et ajoute ses Pokémon dans `Party` (6 maximum, dans l'ordre d'envoi).
|
||||||
|
2. Crée un `BattleEncounter` : assigne le `Trainer`, et **décoche `Can Flee`** (on ne fuit pas un dresseur).
|
||||||
|
3. Sur son `AimTarget` : `Encounter` et `World Actor`, comme pour un sauvage.
|
||||||
|
|
||||||
|
Son premier Pokémon (puis le suivant, pour un combat double) entre en combat, et les autres restent sur son banc. Il en envoie un autre quand le sien est KO, ou parfois de lui-même quand son Pokémon est très désavantagé face au tien.
|
||||||
|
|
||||||
|
## Combat double, boss et autres formats
|
||||||
|
|
||||||
|
Le format vient uniquement du nombre d'emplacements :
|
||||||
|
|
||||||
|
| Format | Réglage du `BattleEncounter` |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 contre 1 sauvage | 1 entrée dans `Wild Pokemon` |
|
||||||
|
| 1 contre plusieurs | plusieurs entrées dans `Wild Pokemon` |
|
||||||
|
| Dresseur, combat simple | `Trainer`, `Trainer Slots` = 1 |
|
||||||
|
| Combat double | `Player Slots` = 2 et `Trainer Slots` = 2 |
|
||||||
|
| Plusieurs contre 1 (boss) | `Player Slots` = 3 et un seul adversaire (une entrée sauvage, ou un dresseur avec `Trainer Slots` = 1) |
|
||||||
|
|
||||||
|
`Player Slots` et `Trainer Slots` vont de 1 à 3. Si le joueur a moins de Pokémon aptes que d'emplacements, il en envoie moins.
|
||||||
|
|
||||||
|
## Capacités et nature
|
||||||
|
|
||||||
|
Pour chaque `TrainerPartyEntry` :
|
||||||
|
|
||||||
|
- **Capacités** : si `Forced Moves` est vide, le Pokémon connaît les capacités apprises par montée de niveau jusqu'à son niveau, et équipe les 4 dernières. Sinon, les capacités listées (4 maximum) sont équipées à la place.
|
||||||
|
- **Nature** : `Random Nature` coché tire une nature au hasard à chaque combat. Décoche-le et choisis `Nature` pour la fixer.
|
||||||
|
- **Surnom** : s'il est vide, le nom de l'espèce s'affiche.
|
||||||
|
|
||||||
|
## Quel Pokémon du joueur combat ?
|
||||||
|
|
||||||
|
- Si le joueur **lance** un Pokémon sur la cible, c'est lui qui entre en premier, puis les suivants de l'équipe pour un combat double.
|
||||||
|
- Si le **dresseur défie** le joueur (dialogue, zone de détection), c'est le premier Pokémon apte de l'équipe, puis le deuxième, etc. Pour cela, appelle depuis ton code :
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
launcher.start_encounter(encounter, position_du_dresseur, null, world_actor)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Après le combat
|
||||||
|
|
||||||
|
- Si `Remove World Actor On Win` est coché, le `World Actor` disparaît après une victoire. Sinon il réapparaît.
|
||||||
|
- Le signal `battle_finished(outcome)` de l'`AimTarget` (et `GameData.battle_finished`) donne le résultat, une valeur de `Battle.Outcome` : `PLAYER_WON`, `PLAYER_LOST`, `FLED` ou `DRAW`. Branche-y tes dialogues, tes récompenses ou la suite de l'histoire.
|
||||||
|
- Après une défaite, l'équipe du joueur est soignée automatiquement (option `Heal Team On Defeat` sur le `BattleLauncher`). C'est provisoire, en attendant ton propre écran de défaite.
|
||||||
|
|
||||||
|
## Bon à savoir
|
||||||
|
|
||||||
|
- Un `TrainerData` ne doit pas dépasser 6 Pokémon : les suivants sont ignorés, avec un avertissement dans la console.
|
||||||
|
- Une entrée sans espèce est ignorée. Si un camp n'a plus aucun Pokémon, le combat ne démarre pas, et la console l'indique.
|
||||||
|
- Pas encore implémentés : les effets des capacités de statut, les objets, le gain d'XP et d'argent, et la capture.
|
||||||
52
game/player/aim/aim_reticule.gd
Normal file
52
game/player/aim/aim_reticule.gd
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
static var _instance : AimReticle
|
||||||
|
|
||||||
|
static func get_instance(from : Node) -> AimReticle:
|
||||||
|
if is_instance_valid(_instance): return _instance
|
||||||
|
_instance = AimReticle.new()
|
||||||
|
from.get_tree().current_scene.add_child(_instance)
|
||||||
|
return _instance
|
||||||
|
|
||||||
|
static func show_instance(from : Node) -> void:
|
||||||
|
get_instance(from).visible = true
|
||||||
|
|
||||||
|
static func hide_instance() -> void:
|
||||||
|
if is_instance_valid(_instance): _instance.visible = false
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
layer = 10
|
||||||
|
visible = false
|
||||||
|
add_child(Drawer.new())
|
||||||
|
|
||||||
|
class Drawer extends Control:
|
||||||
|
@export var radius : float = 14.0
|
||||||
|
@export var tick_length : float = 8.0
|
||||||
|
@export var color : Color = Color(1.0, 1.0, 1.0, 0.9)
|
||||||
|
@export var outline_color : Color = Color(0.0, 0.0, 0.0, 0.5) #contour sombre pour rester lisible sur fond clair
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
resized.connect(queue_redraw)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var c : Vector2 = size / 2.0
|
||||||
|
|
||||||
|
#contour sombre, puis trait clair par-dessus
|
||||||
|
draw_arc(c, radius, 0.0, TAU, 40, outline_color, 4.0, true)
|
||||||
|
draw_circle(c, 3.0, outline_color)
|
||||||
|
for dir : Vector2 in [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]:
|
||||||
|
draw_line(c + dir * (radius + 2.0), c + dir * (radius + 2.0 + tick_length), outline_color, 4.0)
|
||||||
|
|
||||||
|
draw_arc(c, radius, 0.0, TAU, 40, color, 2.0, true)
|
||||||
|
draw_circle(c, 1.5, color)
|
||||||
|
for dir : Vector2 in [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]:
|
||||||
|
draw_line(c + dir * (radius + 2.0), c + dir * (radius + 2.0 + tick_length), color, 2.0)
|
||||||
1
game/player/aim/aim_reticule.gd.uid
Normal file
1
game/player/aim/aim_reticule.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d263upin4ab3e
|
||||||
|
|
@ -10,6 +10,7 @@ var play_char : CharacterBody3D
|
||||||
@export var switch_target_action : StringName = &"player_aim_switch_target"
|
@export var switch_target_action : StringName = &"player_aim_switch_target"
|
||||||
@export var unlock_action : StringName = &"player_aim_unlock"
|
@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 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_group("Targeting")
|
||||||
@export var throw_range : float = 20.0
|
@export var throw_range : float = 20.0
|
||||||
|
|
@ -27,6 +28,7 @@ var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant
|
||||||
var dodging_from_aim : bool = false #true quand on quitte Aim pour rouler, pour ne pas relacher la cible verrouillee
|
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 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 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
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if InputMap.has_action(throw_action): return
|
if InputMap.has_action(throw_action): return
|
||||||
|
|
@ -43,7 +45,10 @@ func enter(play_char_ref : CharacterBody3D) -> void:
|
||||||
play_char = play_char_ref
|
play_char = play_char_ref
|
||||||
auto_lock_blocked = false
|
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
|
#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
|
||||||
came_from_crouch = play_char.state_machine.curr_state_name == "Crouch"
|
#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()
|
verifications()
|
||||||
|
|
||||||
func verifications() -> void:
|
func verifications() -> void:
|
||||||
|
|
@ -51,9 +56,11 @@ func verifications() -> void:
|
||||||
play_char.cam_holder.cam_aimed = true
|
play_char.cam_holder.cam_aimed = true
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
|
charging_throw = false
|
||||||
if keep_aim_on_exit:
|
if keep_aim_on_exit:
|
||||||
keep_aim_on_exit = false
|
keep_aim_on_exit = false
|
||||||
return
|
return
|
||||||
|
AimReticle.hide_instance()
|
||||||
if dodging_from_aim:
|
if dodging_from_aim:
|
||||||
#on roule en visant : la camera et la cible verrouillee restent actives pendant la roulade
|
#on roule en visant : la camera et la cible verrouillee restent actives pendant la roulade
|
||||||
dodging_from_aim = false
|
dodging_from_aim = false
|
||||||
|
|
@ -66,6 +73,7 @@ func physics_update(delta : float) -> void:
|
||||||
play_char.gravity_apply(delta)
|
play_char.gravity_apply(delta)
|
||||||
if input_management(): return
|
if input_management(): return
|
||||||
update_target()
|
update_target()
|
||||||
|
update_reticle()
|
||||||
move(delta)
|
move(delta)
|
||||||
|
|
||||||
func applies() -> bool:
|
func applies() -> bool:
|
||||||
|
|
@ -81,12 +89,22 @@ func input_management() -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if !Input.is_action_pressed(aim_action):
|
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:
|
if came_from_crouch:
|
||||||
transitioned.emit(self, "CrouchState")
|
transitioned.emit(self, "CrouchState")
|
||||||
else:
|
else:
|
||||||
transitioned.emit(self, play_char.walk_or_run if play_char.move_dir else "IdleState")
|
transitioned.emit(self, play_char.walk_or_run if play_char.move_dir else "IdleState")
|
||||||
return true
|
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):
|
if Input.is_action_just_pressed(throw_action):
|
||||||
#si le pokemon selectionne est deja dehors, un nouvel appui le rappelle au lieu de relancer
|
#si le pokemon selectionne est deja dehors, un nouvel appui le rappelle au lieu de relancer
|
||||||
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
var selected : PokemonInstance = GameData.get_selected_pokemon()
|
||||||
|
|
@ -103,10 +121,15 @@ func input_management() -> bool:
|
||||||
switch_target()
|
switch_target()
|
||||||
return false
|
return false
|
||||||
|
|
||||||
func start_throw() -> bool:
|
func can_throw() -> bool:
|
||||||
if GameData.get_selected_pokemon() == null: return false #personne a envoyer
|
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
|
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
|
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")
|
transitioned.emit(self, "ThrowState")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
@ -120,6 +143,12 @@ func move(delta : float) -> void:
|
||||||
play_char.velocity.x = lerp(play_char.velocity.x, 0.0, play_char.move_deccel * delta)
|
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)
|
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 ----------
|
#---------- targeting ----------
|
||||||
|
|
||||||
func update_target() -> void:
|
func update_target() -> void:
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,21 @@ var play_char : CharacterBody3D
|
||||||
var dodge_dir : Vector2 = Vector2.ZERO
|
var dodge_dir : Vector2 = Vector2.ZERO
|
||||||
var dodge_timer : float = 0.0
|
var dodge_timer : float = 0.0
|
||||||
var dodge_duration_ref : 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:
|
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||||
play_char = play_char_ref
|
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
|
||||||
|
|
||||||
verifications()
|
verifications()
|
||||||
|
|
||||||
func verifications() -> void:
|
func verifications() -> void:
|
||||||
|
|
@ -69,7 +80,12 @@ func end_dodge() -> void:
|
||||||
if !play_char.is_on_floor():
|
if !play_char.is_on_floor():
|
||||||
transitioned.emit(self, "InairState")
|
transitioned.emit(self, "InairState")
|
||||||
elif Input.is_action_pressed(play_char.aim_cam_action):
|
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
|
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:
|
elif play_char.move_dir:
|
||||||
transitioned.emit(self, play_char.walk_or_run)
|
transitioned.emit(self, play_char.walk_or_run)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ extends Node
|
||||||
class_name StateMachine
|
class_name StateMachine
|
||||||
|
|
||||||
@export var initial_state : State
|
@export var initial_state : State
|
||||||
|
@export var throw_action : StringName = &"player_throw"
|
||||||
|
|
||||||
|
const THROW_ENTRY_STATES : Array[String] = ["Idle", "Walk", "Run", "Crouch"] #etats depuis lesquels RT lance sans viser
|
||||||
|
|
||||||
var curr_state : State
|
var curr_state : State
|
||||||
var curr_state_name : String
|
var curr_state_name : String
|
||||||
|
|
@ -26,9 +29,30 @@ func _ready() -> void:
|
||||||
|
|
||||||
func _process(delta : float) -> void:
|
func _process(delta : float) -> void:
|
||||||
if curr_state: curr_state.update(delta)
|
if curr_state: curr_state.update(delta)
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if curr_state: curr_state.physics_update(delta)
|
if curr_state:
|
||||||
|
if try_start_throw(): return
|
||||||
|
curr_state.physics_update(delta)
|
||||||
|
|
||||||
|
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:
|
func on_state_child_transition(state : State, new_state_name : String) -> void:
|
||||||
#manage the transition from one state to another
|
#manage the transition from one state to another
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,15 @@ var state_name : String = "Throw"
|
||||||
var play_char : CharacterBody3D
|
var play_char : CharacterBody3D
|
||||||
|
|
||||||
@export var aim_action : StringName = &"player_aim_camera"
|
@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_group("Charge")
|
||||||
|
@export var charge_move_speed : float = 3.0 #vitesse de deplacement pendant que RT est maintenue
|
||||||
|
@export var charge_move_accel : float = 8.0
|
||||||
|
|
||||||
@export_group("Timing")
|
@export_group("Timing")
|
||||||
@export var windup_time : float = 0.25 #delai avant que la balle parte (a caler sur l'animation)
|
@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 var recovery_time : float = 0.35 #temps de recuperation apres le lancer
|
||||||
|
|
||||||
@export_group("Ball")
|
@export_group("Ball")
|
||||||
|
|
@ -24,44 +30,102 @@ var play_char : CharacterBody3D
|
||||||
@export_group("Targeting")
|
@export_group("Targeting")
|
||||||
@export var throw_range : float = 20.0
|
@export var throw_range : float = 20.0
|
||||||
@export_flags_3d_physics var ground_mask : int = 1
|
@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 _elapsed : float = 0.0
|
||||||
|
var _released : bool = false #true des que RT est relachee (phase windup/recovery)
|
||||||
var _thrown : bool = false
|
var _thrown : bool = false
|
||||||
var _keep_aim : bool = false #true quand on retourne a AimState : on garde camera et verrouillage
|
var _keep_aim : bool = false #true quand on retourne a AimState : on garde camera et verrouillage
|
||||||
var _pokemon : PokemonInstance = null
|
var _pokemon : PokemonInstance = null
|
||||||
var _target : Variant = null #AimTarget verrouille au moment du lancer
|
var _target : Variant = null #AimTarget verrouille au moment du relachement
|
||||||
var _free_aim_point : Vector3 = Vector3.ZERO
|
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))
|
||||||
|
|
||||||
|
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:
|
func enter(play_char_ref : CharacterBody3D) -> void:
|
||||||
play_char = play_char_ref
|
play_char = play_char_ref
|
||||||
_elapsed = 0.0
|
_elapsed = 0.0
|
||||||
|
_released = false
|
||||||
_thrown = false
|
_thrown = false
|
||||||
_keep_aim = 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()
|
_pokemon = GameData.get_selected_pokemon()
|
||||||
_target = play_char.aim_target if is_instance_valid(play_char.aim_target) else null
|
_target = null
|
||||||
_free_aim_point = _compute_free_aim_point() #visee libre : le point est fige au moment ou on appuie
|
_free_aim_point = Vector3.ZERO
|
||||||
|
|
||||||
play_char.floor_snap_length = 1.0
|
play_char.floor_snap_length = 1.0
|
||||||
#play_char.godot_plush_skin.set_state("throw")
|
#play_char.godot_plush_skin.set_state("throw")
|
||||||
|
|
||||||
|
if resume_released:
|
||||||
|
resume_released = false
|
||||||
|
release() #fige la cible verrouillee / le point vise et lance le windup
|
||||||
|
|
||||||
func exit() -> void:
|
func exit() -> void:
|
||||||
#retour a la visee : on garde tout tel quel ; sinon on libere camera et verrouillage (comme AimState)
|
#retour a la visee : on garde tout tel quel ; sinon on libere camera et verrouillage (comme AimState)
|
||||||
if _keep_aim:
|
if _keep_aim:
|
||||||
_keep_aim = false
|
_keep_aim = false
|
||||||
return
|
return
|
||||||
|
AimReticle.hide_instance()
|
||||||
_release_aim()
|
_release_aim()
|
||||||
|
|
||||||
func physics_update(delta : float) -> void:
|
func physics_update(delta : float) -> void:
|
||||||
if applies(): return
|
if applies(): return
|
||||||
|
|
||||||
play_char.gravity_apply(delta)
|
play_char.gravity_apply(delta)
|
||||||
slow_down(delta)
|
|
||||||
|
|
||||||
if _pokemon == null:
|
if _pokemon == null:
|
||||||
|
slow_down(delta)
|
||||||
finish() #rien a lancer (equipe vide ou tous K.O.)
|
finish() #rien a lancer (equipe vide ou tous K.O.)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
#phase 1 : charge, tant que RT est maintenue
|
||||||
|
if !_released:
|
||||||
|
charge(delta)
|
||||||
|
return
|
||||||
|
|
||||||
|
#phase 2 : windup puis recovery
|
||||||
|
slow_down(delta)
|
||||||
|
|
||||||
|
if !_thrown and Input.is_action_just_pressed(cancel_action):
|
||||||
|
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
|
||||||
|
return
|
||||||
|
|
||||||
_elapsed += delta
|
_elapsed += delta
|
||||||
if !_thrown and _elapsed >= windup_time:
|
if !_thrown and _elapsed >= windup_time:
|
||||||
throw_ball()
|
throw_ball()
|
||||||
|
|
@ -74,6 +138,49 @@ func applies() -> bool:
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
#---------- charge ----------
|
||||||
|
|
||||||
|
func charge(delta : float) -> void:
|
||||||
|
if Input.is_action_just_pressed(cancel_action):
|
||||||
|
finish() #annule : rien n'est lance
|
||||||
|
return
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed(aim_action):
|
||||||
|
enter_aim_while_charging()
|
||||||
|
return
|
||||||
|
|
||||||
|
if !Input.is_action_pressed(throw_action):
|
||||||
|
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")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
slow_down(delta)
|
||||||
|
|
||||||
func slow_down(delta : float) -> void:
|
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.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)
|
play_char.velocity.z = lerp(play_char.velocity.z, 0.0, play_char.move_deccel * delta)
|
||||||
|
|
@ -81,16 +188,30 @@ func slow_down(delta : float) -> void:
|
||||||
func finish() -> void:
|
func finish() -> void:
|
||||||
if Input.is_action_pressed(aim_action):
|
if Input.is_action_pressed(aim_action):
|
||||||
_keep_aim = true
|
_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")
|
transitioned.emit(self, "AimState")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
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)
|
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")
|
transitioned.emit(self, play_char.walk_or_run if dir != Vector2.ZERO else "IdleState")
|
||||||
|
|
||||||
|
func update_reticle() -> void:
|
||||||
|
if is_instance_valid(play_char.aim_target):
|
||||||
|
AimReticle.hide_instance() #cible verrouillee : son propre indicateur suffit
|
||||||
|
else:
|
||||||
|
AimReticle.show_instance(self)
|
||||||
|
|
||||||
#---------- lancer ----------
|
#---------- lancer ----------
|
||||||
|
|
||||||
func throw_ball() -> void:
|
func throw_ball() -> void:
|
||||||
_thrown = true
|
_thrown = true
|
||||||
|
AimReticle.hide_instance()
|
||||||
|
|
||||||
var from : Vector3 = play_char.global_position + Vector3.UP * hand_height
|
var from : Vector3 = play_char.global_position + Vector3.UP * hand_height
|
||||||
var to : Vector3 = _get_landing_point()
|
var to : Vector3 = _get_landing_point()
|
||||||
|
|
@ -136,6 +257,10 @@ func _on_ball_landed(landing_pos : Vector3, pokemon : PokemonInstance, target :
|
||||||
var scene : Node = get_tree().current_scene if is_inside_tree() else null
|
var scene : Node = get_tree().current_scene if is_inside_tree() else null
|
||||||
if scene == null: return
|
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)
|
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
|
#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 look_pos : Vector3 = target.global_position if target_valid else landing_pos + (landing_pos - thrower_pos)
|
||||||
|
|
@ -147,6 +272,19 @@ func _on_ball_landed(landing_pos : Vector3, pokemon : PokemonInstance, target :
|
||||||
#diagnostic : retire ce message quand tout fonctionne
|
#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)
|
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 ----------
|
#---------- nettoyage ----------
|
||||||
|
|
||||||
func _release_aim() -> void:
|
func _release_aim() -> void:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue