From 6867a4ece9e911b9b2bb3f4fc07a320a44effd12 Mon Sep 17 00:00:00 2001 From: TheThomaas Date: Mon, 5 Oct 2026 14:06:18 +0200 Subject: [PATCH] Send Pokemon in overworld --- game/core/game_data.gd | 37 ++++++ game/player/aim/thrown_ball.gd | 54 +++++++++ game/player/aim/thrown_ball.gd.uid | 1 + game/player/input/recall_input.gd | 26 +++++ game/player/input/recall_input.gd.uid | 1 + game/player/player_character.tscn | 20 ++++ game/player/states/aim_state.gd | 32 ++++++ game/player/states/throw_state.gd | 155 ++++++++++++++++++++++++++ game/player/states/throw_state.gd.uid | 1 + game/pokemon/sent_pokemon.gd | 73 ++++++++++++ game/pokemon/sent_pokemon.gd.uid | 1 + 11 files changed, 401 insertions(+) create mode 100644 game/player/aim/thrown_ball.gd create mode 100644 game/player/aim/thrown_ball.gd.uid create mode 100644 game/player/input/recall_input.gd create mode 100644 game/player/input/recall_input.gd.uid create mode 100644 game/player/states/throw_state.gd create mode 100644 game/player/states/throw_state.gd.uid create mode 100644 game/pokemon/sent_pokemon.gd create mode 100644 game/pokemon/sent_pokemon.gd.uid diff --git a/game/core/game_data.gd b/game/core/game_data.gd index c58b16a..6448036 100644 --- a/game/core/game_data.gd +++ b/game/core/game_data.gd @@ -1,15 +1,52 @@ extends Node # 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 player_team : PokemonTeam = PokemonTeam.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: #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. 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: var config := ConfigFile.new() config.set_value("progress", "story_progress", story_progress) diff --git a/game/player/aim/thrown_ball.gd b/game/player/aim/thrown_ball.gd new file mode 100644 index 0000000..3f55819 --- /dev/null +++ b/game/player/aim/thrown_ball.gd @@ -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() diff --git a/game/player/aim/thrown_ball.gd.uid b/game/player/aim/thrown_ball.gd.uid new file mode 100644 index 0000000..3d982ce --- /dev/null +++ b/game/player/aim/thrown_ball.gd.uid @@ -0,0 +1 @@ +uid://c4nec4y6jc5eg diff --git a/game/player/input/recall_input.gd b/game/player/input/recall_input.gd new file mode 100644 index 0000000..8d542a4 --- /dev/null +++ b/game/player/input/recall_input.gd @@ -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() diff --git a/game/player/input/recall_input.gd.uid b/game/player/input/recall_input.gd.uid new file mode 100644 index 0000000..f6c72ec --- /dev/null +++ b/game/player/input/recall_input.gd.uid @@ -0,0 +1 @@ +uid://c820hnwuwpmlg diff --git a/game/player/player_character.tscn b/game/player/player_character.tscn index 5b20eb2..92d60e9 100644 --- a/game/player/player_character.tscn +++ b/game/player/player_character.tscn @@ -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://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="Script" uid="uid://c820hnwuwpmlg" path="res://game/player/input/recall_input.gd" id="13_siyau"] [sub_resource type="Curve" id="Curve_kxr4l"] _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] 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] script = ExtResource("8_j4mf1") @@ -96,3 +113,6 @@ script = ExtResource("10_mi63v") [node name="InputManagementComponent" type="Node" parent="." unique_id=1981951371] script = ExtResource("2_r5dqo") + +[node name="RecallInput" type="Node" parent="." unique_id=1544887696] +script = ExtResource("13_siyau") diff --git a/game/player/states/aim_state.gd b/game/player/states/aim_state.gd index 09f9498..129127f 100644 --- a/game/player/states/aim_state.gd +++ b/game/player/states/aim_state.gd @@ -9,6 +9,7 @@ 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 @@ -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 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 @@ -38,6 +51,9 @@ func verifications() -> void: 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 @@ -71,6 +87,15 @@ func input_management() -> bool: 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() @@ -78,6 +103,13 @@ func input_management() -> bool: 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) diff --git a/game/player/states/throw_state.gd b/game/player/states/throw_state.gd new file mode 100644 index 0000000..e5baf95 --- /dev/null +++ b/game/player/states/throw_state.gd @@ -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 diff --git a/game/player/states/throw_state.gd.uid b/game/player/states/throw_state.gd.uid new file mode 100644 index 0000000..404578d --- /dev/null +++ b/game/player/states/throw_state.gd.uid @@ -0,0 +1 @@ +uid://c3jh22p4w3xco diff --git a/game/pokemon/sent_pokemon.gd b/game/pokemon/sent_pokemon.gd new file mode 100644 index 0000000..82da5ec --- /dev/null +++ b/game/pokemon/sent_pokemon.gd @@ -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() diff --git a/game/pokemon/sent_pokemon.gd.uid b/game/pokemon/sent_pokemon.gd.uid new file mode 100644 index 0000000..d7fd2a8 --- /dev/null +++ b/game/pokemon/sent_pokemon.gd.uid @@ -0,0 +1 @@ +uid://bc3a1pvuq6dy6