Send Pokemon in overworld

This commit is contained in:
TheThomaas 2026-10-05 14:06:18 +02:00
parent 9efc974344
commit 6867a4ece9
11 changed files with 401 additions and 0 deletions

View file

@ -1,15 +1,52 @@
extends Node extends Node
# Autoload : GameData # Autoload : GameData
signal selected_pokemon_changed
signal battle_requested(sent : SentPokemon, target : Node) #emis par ThrowState : le systeme de combat s'y branchera
var story_progress : int = 0 # 0 = début, 1 = après 1er badge, 2 = après 2e badge, etc. var story_progress : int = 0 # 0 = début, 1 = après 1er badge, 2 = après 2e badge, etc.
var player_team : PokemonTeam = PokemonTeam.new() var player_team : PokemonTeam = PokemonTeam.new()
var player_storage : PokemonStorage = PokemonStorage.new() var player_storage : PokemonStorage = PokemonStorage.new()
var selected_index : int = -1 #pokemon de l'equipe choisi pour etre envoye (HUD), -1 = aucun
func give_pokemon(pokemon : PokemonInstance) -> PokemonStorage.ReceiveResult: func give_pokemon(pokemon : PokemonInstance) -> PokemonStorage.ReceiveResult:
#point d'entree unique pour obtenir un pokemon (starter, cadeau, capture...) : #point d'entree unique pour obtenir un pokemon (starter, cadeau, capture...) :
#equipe si elle a de la place, sinon PC. A l'appelant d'informer le joueur selon le resultat. #equipe si elle a de la place, sinon PC. A l'appelant d'informer le joueur selon le resultat.
return player_storage.receive_pokemon(player_team, pokemon) return player_storage.receive_pokemon(player_team, pokemon)
#---------- pokemon selectionne (a envoyer) ----------
#seuls les pokemon aptes au combat (non K.O.) peuvent etre selectionnes
func get_selected_pokemon() -> PokemonInstance:
validate_selection()
return player_team.get_pokemon(selected_index)
func validate_selection() -> void:
#corrige la selection si le pokemon n'existe plus, est K.O., ou a change de place
var p : PokemonInstance = player_team.get_pokemon(selected_index)
if p != null and !p.is_fainted(): return
_set_selected(player_team.get_first_able_index())
func select_pokemon(index : int) -> bool:
var p : PokemonInstance = player_team.get_pokemon(index)
if p == null or p.is_fainted(): return false
_set_selected(index)
return true
func cycle_selection(direction : int) -> void:
validate_selection()
var able : Array[int] = []
for i in player_team.size():
if !player_team.get_pokemon(i).is_fainted(): able.append(i)
if able.size() < 2: return
var pos : int = able.find(selected_index)
_set_selected(able[posmod(pos + direction, able.size())])
func _set_selected(index : int) -> void:
if index == selected_index: return
selected_index = index
selected_pokemon_changed.emit()
func save() -> void: func save() -> void:
var config := ConfigFile.new() var config := ConfigFile.new()
config.set_value("progress", "story_progress", story_progress) config.set_value("progress", "story_progress", story_progress)

View file

@ -0,0 +1,54 @@
class_name ThrownBall
extends Node3D
#balle lancee : suit un arc entre deux points, puis emet "landed" et disparait.
#autonome (pas lie a l'etat du joueur) : continue meme si le joueur change d'etat avant l'atterrissage.
signal landed(position : Vector3)
const SPIN_SPEED : float = 12.0
var _start : Vector3
var _end : Vector3
var _arc_height : float = 1.5
var _duration : float = 0.5
var _elapsed : float = 0.0
static func launch(parent : Node, from : Vector3, to : Vector3, duration : float, arc_height : float, visual_scene : PackedScene = null) -> ThrownBall:
var ball := ThrownBall.new()
ball._start = from
ball._end = to
ball._duration = maxf(duration, 0.05)
ball._arc_height = arc_height
parent.add_child(ball)
ball.global_position = from
ball._build_visual(visual_scene)
return ball
func _build_visual(visual_scene : PackedScene) -> void:
if visual_scene:
var visual : Node = visual_scene.instantiate()
add_child(visual)
return
#balle par defaut (sphere rouge) en attendant ton modele
var mesh_instance := MeshInstance3D.new()
var sphere := SphereMesh.new()
sphere.radius = 0.12
sphere.height = 0.24
mesh_instance.mesh = sphere
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.9, 0.15, 0.15)
mesh_instance.material_override = mat
add_child(mesh_instance)
func _process(delta : float) -> void:
_elapsed += delta
var t : float = clampf(_elapsed / _duration, 0.0, 1.0)
global_position = _start.lerp(_end, t) + Vector3.UP * _arc_height * 4.0 * t * (1.0 - t)
rotate_x(SPIN_SPEED * delta)
if t >= 1.0:
set_process(false)
global_position = _end
landed.emit(_end)
queue_free()

View file

@ -0,0 +1 @@
uid://c4nec4y6jc5eg

View file

@ -0,0 +1,26 @@
class_name RecallInput
extends Node
#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
#
#usage : ajouter ce node comme enfant de ton PlayerCharacter (il le detecte via son parent)
@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
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.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()

View file

@ -0,0 +1 @@
uid://c820hnwuwpmlg

View file

@ -9,8 +9,10 @@
[ext_resource type="Script" uid="uid://dr5p8ygj25w6x" path="res://game/player/states/jump_state.gd" id="6_lwgph"] [ext_resource type="Script" uid="uid://dr5p8ygj25w6x" path="res://game/player/states/jump_state.gd" id="6_lwgph"]
[ext_resource type="Script" uid="uid://dn5idrvu68k83" path="res://game/player/states/aim_state.gd" id="7_kxr4l"] [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://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://bwojkp7yrwswt" path="res://game/player/states/dodge_state.gd" id="9_6abew"]
[ext_resource type="Script" uid="uid://d0577tj0c23bu" path="res://game/player/states/crouch_state.gd" id="10_mi63v"] [ext_resource type="Script" uid="uid://d0577tj0c23bu" path="res://game/player/states/crouch_state.gd" id="10_mi63v"]
[ext_resource type="Script" uid="uid://c820hnwuwpmlg" path="res://game/player/input/recall_input.gd" id="13_siyau"]
[sub_resource type="Curve" id="Curve_kxr4l"] [sub_resource type="Curve" id="Curve_kxr4l"]
_limits = [0.0, 10.0, 0.0, 1.0] _limits = [0.0, 10.0, 0.0, 1.0]
@ -84,6 +86,21 @@ script = ExtResource("6_lwgph")
[node name="AimState" type="Node" parent="StateMachine" unique_id=296560188] [node name="AimState" type="Node" parent="StateMachine" unique_id=296560188]
script = ExtResource("7_kxr4l") script = ExtResource("7_kxr4l")
aim_action = null
switch_target_action = null
unlock_action = null
throw_action = null
throw_range = null
max_aim_angle = null
auto_lock_angle = null
lose_range_margin = null
check_line_of_sight = null
line_of_sight_mask = null
aim_move_speed = null
aim_move_accel = null
[node name="ThrowState" type="Node" parent="StateMachine" unique_id=1159570489]
script = ExtResource("8_k0r6x")
[node name="RunState" type="Node" parent="StateMachine" unique_id=1345421775] [node name="RunState" type="Node" parent="StateMachine" unique_id=1345421775]
script = ExtResource("8_j4mf1") script = ExtResource("8_j4mf1")
@ -96,3 +113,6 @@ script = ExtResource("10_mi63v")
[node name="InputManagementComponent" type="Node" parent="." unique_id=1981951371] [node name="InputManagementComponent" type="Node" parent="." unique_id=1981951371]
script = ExtResource("2_r5dqo") script = ExtResource("2_r5dqo")
[node name="RecallInput" type="Node" parent="." unique_id=1544887696]
script = ExtResource("13_siyau")

View file

@ -9,6 +9,7 @@ var play_char : CharacterBody3D
@export var aim_action : StringName = &"player_aim_camera" @export var aim_action : StringName = &"player_aim_camera"
@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_group("Targeting") @export_group("Targeting")
@export var throw_range : float = 20.0 @export var throw_range : float = 20.0
@ -25,6 +26,18 @@ var play_char : CharacterBody3D
var auto_lock_blocked : bool = false #true apres un deverrouillage manuel, tant que la cible est encore au centre 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 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
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: func enter(play_char_ref : CharacterBody3D) -> void:
play_char = play_char_ref play_char = play_char_ref
@ -38,6 +51,9 @@ func verifications() -> void:
play_char.cam_holder.cam_aimed = true play_char.cam_holder.cam_aimed = true
func exit() -> void: func exit() -> void:
if keep_aim_on_exit:
keep_aim_on_exit = false
return
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
@ -71,6 +87,15 @@ func input_management() -> bool:
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 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): if Input.is_action_just_pressed(unlock_action):
auto_lock_blocked = true auto_lock_blocked = true
release_target() release_target()
@ -78,6 +103,13 @@ func input_management() -> bool:
switch_target() switch_target()
return false 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: 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) 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)

View file

@ -0,0 +1,155 @@
extends State
class_name ThrowState
var state_name : String = "Throw"
var play_char : CharacterBody3D
@export var aim_action : StringName = &"player_aim_camera"
@export_group("Timing")
@export var windup_time : float = 0.25 #delai avant que la balle parte (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
var _elapsed : float = 0.0
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 lancer
var _free_aim_point : Vector3 = Vector3.ZERO
func enter(play_char_ref : CharacterBody3D) -> void:
play_char = play_char_ref
_elapsed = 0.0
_thrown = false
_keep_aim = false
_pokemon = GameData.get_selected_pokemon()
_target = play_char.aim_target if is_instance_valid(play_char.aim_target) else null
_free_aim_point = _compute_free_aim_point() #visee libre : le point est fige au moment ou on appuie
play_char.floor_snap_length = 1.0
#play_char.godot_plush_skin.set_state("throw")
func exit() -> void:
#retour a la visee : on garde tout tel quel ; sinon on libere camera et verrouillage (comme AimState)
if _keep_aim:
_keep_aim = false
return
_release_aim()
func physics_update(delta : float) -> void:
if applies(): return
play_char.gravity_apply(delta)
slow_down(delta)
if _pokemon == null:
finish() #rien a lancer (equipe vide ou tous K.O.)
return
_elapsed += delta
if !_thrown and _elapsed >= windup_time:
throw_ball()
if _elapsed >= windup_time + recovery_time:
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
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
transitioned.emit(self, "AimState")
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")
#---------- lancer ----------
func throw_ball() -> void:
_thrown = true
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
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)
#---------- 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

View file

@ -0,0 +1 @@
uid://c3jh22p4w3xco

View file

@ -0,0 +1,73 @@
class_name SentPokemon
extends Node3D
#un pokemon de l'equipe sorti dans le monde (apres un lancer).
#plusieurs peuvent etre sortis en meme temps ; un meme pokemon ne peut l'etre qu'une fois.
signal recalled
const GROUP : StringName = &"sent_pokemon"
const MODEL_FACES_PLUS_Z : bool = true #true si tes modeles regardent vers +Z (cas des glTF) : sinon mets false
static var _counter : int = 0 #ordre d'envoi, pour retrouver le plus recent
var pokemon : PokemonInstance
var model : Node3D
var order : int = 0
static func send(parent : Node, p : PokemonInstance, at : Vector3, look_at_pos : Vector3 = Vector3.INF) -> SentPokemon:
#un pokemon deja sorti est d'abord rappele (il "se deplace" a l'endroit du nouveau lancer)
for existing in parent.get_tree().get_nodes_in_group(GROUP):
if existing is SentPokemon and existing.pokemon == p:
existing.recall()
var sent := SentPokemon.new()
sent.pokemon = p
sent.order = _counter
_counter += 1
parent.add_child(sent)
sent.global_position = at
sent._build_model()
if look_at_pos != Vector3.INF:
var flat : Vector3 = Vector3(look_at_pos.x, at.y, look_at_pos.z)
if flat.distance_to(at) > 0.01:
sent.look_at(flat, Vector3.UP)
if MODEL_FACES_PLUS_Z: sent.rotate_y(PI) #look_at oriente -Z vers la cible
return sent
static func find_for(tree : SceneTree, p : PokemonInstance) -> SentPokemon:
#le SentPokemon de ce pokemon, s'il est actuellement dehors
for n in tree.get_nodes_in_group(GROUP):
if n is SentPokemon and n.pokemon == p and !n.is_queued_for_deletion(): return n
return null
static func get_latest(tree : SceneTree) -> SentPokemon:
#le pokemon sorti le plus recemment
var latest : SentPokemon = null
for n in tree.get_nodes_in_group(GROUP):
if n is SentPokemon and !n.is_queued_for_deletion() and (latest == null or n.order > latest.order):
latest = n
return latest
func _ready() -> void:
add_to_group(GROUP)
func _build_model() -> void:
var scene : PackedScene = pokemon.species.model_scene
if scene:
model = scene.instantiate() as Node3D
if model == null:
#modele de remplacement si l'espece n'a pas de modele 3D
var mesh_instance := MeshInstance3D.new()
var capsule := CapsuleMesh.new()
capsule.radius = 0.3
capsule.height = 1.0
mesh_instance.mesh = capsule
mesh_instance.position.y = 0.5
model = mesh_instance
add_child(model)
func recall() -> void:
recalled.emit()
queue_free()

View file

@ -0,0 +1 @@
uid://bc3a1pvuq6dy6