Compare commits

...

3 commits

23 changed files with 846 additions and 14 deletions

View file

@ -0,0 +1,80 @@
class_name BattleActor
extends Node3D
#un pokemon visible dans le monde pendant le combat (un par emplacement), avec ses petites animations.
var slot_id : int = -1
var pokemon : PokemonInstance
var model : Node3D
var home : Vector3 = Vector3.ZERO
var _marker : Label3D
func setup(p_pokemon : PokemonInstance, p_slot_id : int) -> void:
process_mode = Node.PROCESS_MODE_ALWAYS #le monde est en pause pendant le combat, pas les acteurs
pokemon = p_pokemon
slot_id = p_slot_id
model = SentPokemon.create_model(p_pokemon)
add_child(model)
#marqueur "▼" au-dessus de la tete (pokemon qui agit / cible en cours de choix)
_marker = Label3D.new()
_marker.text = "▼"
_marker.font_size = 96
_marker.pixel_size = 0.01
_marker.billboard = BaseMaterial3D.BILLBOARD_ENABLED
_marker.no_depth_test = true
_marker.modulate = Color(1.0, 0.85, 0.2)
_marker.position = Vector3(0, 2.3, 0)
_marker.visible = false
add_child(_marker)
func place(position_world : Vector3, look_at_pos : Vector3) -> void:
global_position = position_world
home = position_world
var flat : Vector3 = Vector3(look_at_pos.x, position_world.y, look_at_pos.z)
if flat.distance_to(position_world) > 0.01:
look_at(flat, Vector3.UP)
if SentPokemon.MODEL_FACES_PLUS_Z: rotate_y(PI) #look_at oriente -Z vers la cible
func set_highlight(on : bool) -> void:
if _marker: _marker.visible = on
#---------- animations (awaitables) ----------
func play_enter() -> void:
scale = Vector3.ONE * 0.01
var tween := create_tween()
tween.tween_property(self, "scale", Vector3.ONE, 0.3).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
await tween.finished
func play_leave() -> void:
var tween := create_tween()
tween.tween_property(self, "scale", Vector3.ONE * 0.01, 0.25).set_ease(Tween.EASE_IN)
await tween.finished
func play_attack(toward : Vector3) -> void:
var dir : Vector3 = toward - home
dir.y = 0.0
if dir.length() < 0.01: return
var lunge : Vector3 = home + dir.normalized() * 1.2
var tween := create_tween()
tween.tween_property(self, "global_position", lunge, 0.12).set_ease(Tween.EASE_OUT)
tween.tween_property(self, "global_position", home, 0.2).set_ease(Tween.EASE_IN_OUT)
await tween.finished
func play_hit() -> void:
var tween := create_tween()
for i in 3:
tween.tween_property(self, "global_position", home + Vector3(0.15, 0, 0), 0.04)
tween.tween_property(self, "global_position", home + Vector3(-0.15, 0, 0), 0.04)
tween.tween_property(self, "global_position", home, 0.04)
await tween.finished
func play_faint() -> void:
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(self, "global_position", home + Vector3(0, -0.6, 0), 0.5)
tween.tween_property(self, "scale", Vector3.ONE * 0.01, 0.5)
await tween.finished
visible = false

View file

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

View file

@ -0,0 +1,33 @@
class_name BattleEncounter
extends Resource
#description d'une rencontre : qui est en face et sous quel format.
#a assigner dans l'inspecteur d'un AimTarget (champ "Encounter").
#exemples :
# - 1v1 sauvage : wild_pokemon = [1 entree]
# - 1 contre plusieurs sauvages : wild_pokemon = [3 entrees]
# - dresseur : trainer = un TrainerData, can_flee = false
# - combat double : player_slots = 2 et trainer_slots = 2
# - plusieurs contre 1 (boss) : player_slots = 3 et une seule entree adverse
@export_group("Adversaires")
@export var trainer : TrainerData
@export_range(1, 3) var trainer_slots : int = 1 #pokemon que le dresseur envoie en meme temps
@export var wild_pokemon : Array[TrainerPartyEntry] = [] #chaque entree = un adversaire (1 emplacement)
@export_group("Joueur")
@export_range(1, 3) var player_slots : int = 1 #pokemon du joueur envoyes en meme temps
@export_group("Regles")
@export var can_flee : bool = true #mets false pour un combat de dresseur
@export var remove_world_actor_on_win : bool = true #fait disparaitre le pokemon/dresseur du monde apres la victoire
func build_participants() -> Array[BattleParticipant]:
var result : Array[BattleParticipant] = []
if trainer != null:
result.append(BattleSetup.trainer_participant(trainer, trainer_slots))
for entry in wild_pokemon:
if entry == null: continue
var pokemon : PokemonInstance = entry.create_instance()
if pokemon != null: result.append(BattleSetup.wild_participant(pokemon))
return result

View file

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

View file

@ -0,0 +1,116 @@
class_name BattleLauncher
extends Node
#lance les combats dans le monde. A ajouter UNE FOIS a ta scene de jeu (node "BattleLauncher").
#
#- ecoute GameData.battle_requested (lancer d'un pokemon sur un AimTarget qui a un "Encounter")
#- start_encounter() sert aussi a un dresseur qui defie le joueur (dialogue, zone de detection...)
#
#pendant le combat : le monde est en pause (GameData.in_battle = true), une camera de combat prend le relais,
#puis tout est restitue a la fin.
signal battle_started
signal battle_ended(outcome : int)
@export var heal_team_on_defeat : bool = true #provisoire : evite de bloquer le joueur si toute son equipe est K.O.
var _running : bool = false
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
GameData.battle_requested.connect(_on_battle_requested)
func _on_battle_requested(sent : SentPokemon, target : Node) -> void:
if _running:
push_warning("[BattleLauncher] un combat est deja en cours")
return
if !(target is AimTarget):
push_warning("[BattleLauncher] la cible n'est pas un AimTarget")
return
var aim_target : AimTarget = target
if aim_target.encounter == null:
push_warning("[BattleLauncher] cet AimTarget n'a pas d'Encounter (inspecteur : Battle > Encounter)")
return
await start_encounter(aim_target.encounter, aim_target.global_position, sent, aim_target.world_actor, aim_target)
func start_encounter(encounter : BattleEncounter, enemy_pos : Vector3, sent : SentPokemon = null, world_actor : Node3D = null, target : AimTarget = null) -> Battle.Outcome:
#sent = pokemon lance par le joueur (il passe en premier) ; null si le joueur est defie : l'ordre de l'equipe s'applique
if _running or encounter == null:
push_warning("[BattleLauncher] start_encounter ignore (combat deja en cours ou Encounter vide)")
return Battle.Outcome.NONE
#camera et acteurs doivent etre dans le monde que le joueur voit (SubViewport eventuel du jeu)
var world_viewport : Viewport = get_viewport()
var world_root : Node = world_viewport if world_viewport is SubViewport else get_tree().current_scene
var game_camera : Camera3D = world_viewport.get_camera_3d()
var player_pos : Vector3 = sent.global_position if (sent != null and is_instance_valid(sent)) else _find_player_position()
var ui := BattleUI.new()
get_tree().root.add_child(ui) #directement sous la fenetre : toujours affiche par-dessus tout
var setup := BattleSetup.new()
setup.can_flee = encounter.can_flee
var lead : PokemonInstance = sent.pokemon if (sent != null and is_instance_valid(sent)) else null
setup.add(BattleSetup.player_participant(GameData.player_team, encounter.player_slots, lead, PlayerBattleController.new(ui)))
for participant in encounter.build_participants():
setup.add(participant)
if !setup.is_valid():
var details : PackedStringArray = PackedStringArray()
for participant in setup.participants:
details.append("%s (camp %d, %d pokemon aptes)" % [participant.name, participant.side, participant.team.get_able_pokemon().size()])
push_warning("[BattleLauncher] combat invalide : chaque camp doit avoir un participant avec un pokemon apte. Participants : " + ", ".join(details) + ". Verifie que l'Encounter contient bien une espece (wild_pokemon / trainer) et que ton equipe a un pokemon non K.O.")
ui.queue_free()
return Battle.Outcome.NONE
_running = true
GameData.in_battle = true
get_tree().paused = true
battle_started.emit()
#les pokemon sortis et l'adversaire du monde sont caches : les acteurs du combat les remplacent
var hidden : Array[Node3D] = []
for node in get_tree().get_nodes_in_group(SentPokemon.GROUP):
if node is Node3D and node.visible:
node.visible = false
hidden.append(node)
if world_actor != null and world_actor.visible:
world_actor.visible = false
hidden.append(world_actor)
var presenter := BattleWorldPresenter.new(ui)
var battle := Battle.new(setup, presenter)
presenter.setup(battle, player_pos, enemy_pos, world_root, game_camera)
var outcome : Battle.Outcome = await battle.run()
await presenter.cleanup()
ui.queue_free()
#retour au monde
for node in hidden:
if is_instance_valid(node): node.visible = true
for node in get_tree().get_nodes_in_group(SentPokemon.GROUP):
if node is SentPokemon and node.pokemon != null and node.pokemon.is_fainted():
node.recall() #un pokemon K.O. est rappele
if world_actor != null and is_instance_valid(world_actor):
if outcome == Battle.Outcome.PLAYER_WON and encounter.remove_world_actor_on_win:
world_actor.queue_free()
if outcome == Battle.Outcome.PLAYER_LOST and heal_team_on_defeat:
GameData.player_team.heal_all()
GameData.in_battle = false
get_tree().paused = false
_running = false
if target != null and is_instance_valid(target): target.battle_finished.emit(outcome)
GameData.battle_finished.emit(outcome, target)
battle_ended.emit(outcome)
return outcome
func _find_player_position() -> Vector3:
#ajoute ton PlayerCharacter au groupe "player" (sinon la camera du jeu sert de repere)
var player : Node3D = get_tree().get_first_node_in_group("player") as Node3D
if player != null: return player.global_position
var cam : Camera3D = get_viewport().get_camera_3d()
return cam.global_position if cam != null else Vector3.ZERO

View file

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

348
game/battle/battle_ui.gd Normal file
View file

@ -0,0 +1,348 @@
class_name BattleUI
extends CanvasLayer
#interface du combat : boite de texte, panneaux de PV, menus du joueur.
#manette d'abord (navigation par focus : A valide, B revient), mais chaque element est un Button
#cliquable a la souris ; le survol deplace le focus.
#
#menus : Combat / Pokemon / Fuite -> choix de la capacite -> choix de la cible (si besoin)
signal highlight_requested(slot_ids : Array) #pokemon a marquer d'un "▼" dans le monde
signal _menu_chosen(index : int)
const ControllerIconTextureScript = preload("res://addons/controller_icons/objects/ControllerIconTexture.gd")
var _text_panel : PanelContainer
var _text_label : Label
var _menu_panel : PanelContainer
var _menu_title : Label
var _menu_box : VBoxContainer
var _player_panels : VBoxContainer
var _enemy_panels : VBoxContainer
var _panels : Dictionary = {} #id d'emplacement -> {"panel", "bar", "fill", "hp_label"}
var _menu_active : bool = false
var _menu_allow_back : bool = false
var _focus_callback : Callable = Callable()
var _skip_requested : bool = false
var _prev_mouse_mode : Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _mouse_saved : bool = false
func _ready() -> void:
layer = 50
process_mode = Node.PROCESS_MODE_ALWAYS #le monde est en pause pendant le combat
_build_ui()
_prev_mouse_mode = Input.mouse_mode
_mouse_saved = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _exit_tree() -> void:
if _mouse_saved:
Input.mouse_mode = _prev_mouse_mode
_mouse_saved = false
func _unhandled_input(event : InputEvent) -> void:
if _menu_active:
if _menu_allow_back and event.is_action_pressed(&"ui_cancel"):
_menu_chosen.emit(-1)
get_viewport().set_input_as_handled()
return
#hors menu : A ou clic gauche passe les messages plus vite
if event.is_action_pressed(&"ui_accept") or (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
_skip_requested = true
#---------- construction ----------
func _build_ui() -> void:
var root := Control.new()
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(root)
#PV du joueur : bas gauche, au-dessus de la boite de texte
var player_margin := _corner(root, Control.PRESET_BOTTOM_LEFT, {"left": 32, "bottom": 170})
_player_panels = VBoxContainer.new()
_player_panels.mouse_filter = Control.MOUSE_FILTER_IGNORE
player_margin.add_child(_player_panels)
#PV des adversaires : haut droite
var enemy_margin := _corner(root, Control.PRESET_TOP_RIGHT, {"right": 32, "top": 32})
_enemy_panels = VBoxContainer.new()
_enemy_panels.mouse_filter = Control.MOUSE_FILTER_IGNORE
enemy_margin.add_child(_enemy_panels)
#boite de texte : bas gauche
var text_margin := _corner(root, Control.PRESET_BOTTOM_LEFT, {"left": 32, "bottom": 32})
_text_panel = PanelContainer.new()
_text_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_text_panel.hide()
text_margin.add_child(_text_panel)
_text_label = Label.new()
_text_label.custom_minimum_size = Vector2(620, 80)
_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_text_label.add_theme_font_size_override("font_size", 22)
_text_panel.add_child(_text_label)
#menus : bas droite
var menu_margin := _corner(root, Control.PRESET_BOTTOM_RIGHT, {"right": 32, "bottom": 32})
_menu_panel = PanelContainer.new()
_menu_panel.hide()
menu_margin.add_child(_menu_panel)
var menu_column := VBoxContainer.new()
menu_column.add_theme_constant_override("separation", 6)
_menu_panel.add_child(menu_column)
_menu_title = Label.new()
_menu_title.add_theme_font_size_override("font_size", 20)
menu_column.add_child(_menu_title)
_menu_box = VBoxContainer.new()
_menu_box.add_theme_constant_override("separation", 4)
menu_column.add_child(_menu_box)
func _corner(parent : Control, preset : Control.LayoutPreset, margins : Dictionary) -> MarginContainer:
#IMPORTANT : les ancres se definissent AVANT add_child. Une fois dans l'arbre, set_anchors_preset
#conserve la position actuelle (0, 0) en ajustant les offsets : le conteneur resterait en haut a gauche
#et grandirait hors de l'ecran.
var m := MarginContainer.new()
m.mouse_filter = Control.MOUSE_FILTER_IGNORE
m.set_anchors_preset(preset)
if preset == Control.PRESET_BOTTOM_LEFT or preset == Control.PRESET_BOTTOM_RIGHT:
m.grow_vertical = Control.GROW_DIRECTION_BEGIN
if preset == Control.PRESET_TOP_RIGHT or preset == Control.PRESET_BOTTOM_RIGHT:
m.grow_horizontal = Control.GROW_DIRECTION_BEGIN
for side in margins:
m.add_theme_constant_override("margin_" + side, margins[side])
parent.add_child(m)
return m
#---------- messages ----------
func show_message(text : String, duration : float = 1.1) -> void:
_text_label.text = text
_text_panel.show()
await _wait(duration)
func hide_message() -> void:
_text_panel.hide()
func _wait(duration : float) -> void:
_skip_requested = false
var start : int = Time.get_ticks_msec()
while float(Time.get_ticks_msec() - start) < duration * 1000.0 and !_skip_requested:
await get_tree().process_frame
#---------- panneaux de PV ----------
func set_slot_pokemon(slot : BattleSlot) -> void:
remove_slot_panel(slot.id)
var mon : BattleMon = slot.mon
if mon == null: return
var panel := PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
var box := VBoxContainer.new()
panel.add_child(box)
var name_label := Label.new()
name_label.text = "%s Lv.%d" % [mon.display_name(), mon.pokemon.level]
name_label.add_theme_font_size_override("font_size", 20)
box.add_child(name_label)
var bar := ProgressBar.new()
bar.show_percentage = false
bar.custom_minimum_size = Vector2(240, 12)
var track := StyleBoxFlat.new()
track.bg_color = Color(1, 1, 1, 0.25)
track.set_corner_radius_all(6)
var fill := StyleBoxFlat.new()
fill.set_corner_radius_all(6)
bar.add_theme_stylebox_override("background", track)
bar.add_theme_stylebox_override("fill", fill)
box.add_child(bar)
var hp_label : Label = null
if slot.side == 0: #les PV chiffres ne sont montres que pour le joueur
hp_label = Label.new()
box.add_child(hp_label)
(_player_panels if slot.side == 0 else _enemy_panels).add_child(panel)
var entry : Dictionary = {"panel": panel, "bar": bar, "fill": fill, "hp_label": hp_label}
_panels[slot.id] = entry
_set_hp(entry, mon.pokemon.current_hp, mon.pokemon.get_max_hp(), false)
func animate_hp(slot_id : int, hp : int, max_hp : int) -> void:
var entry : Dictionary = _panels.get(slot_id, {})
if entry.is_empty(): return
_set_hp(entry, hp, max_hp, true)
await get_tree().create_timer(0.55, true).timeout
func _set_hp(entry : Dictionary, hp : int, max_hp : int, animated : bool) -> void:
var bar : ProgressBar = entry["bar"]
bar.max_value = max_hp
if animated:
var tween := create_tween()
tween.tween_property(bar, "value", float(hp), 0.5)
else:
bar.value = hp
var ratio : float = float(hp) / float(max_hp) if max_hp > 0 else 0.0
var fill : StyleBoxFlat = entry["fill"]
fill.bg_color = Color(0.3, 0.8, 0.35) if ratio > 0.5 else (Color(0.95, 0.7, 0.2) if ratio > 0.2 else Color(0.9, 0.25, 0.25))
var hp_label : Label = entry["hp_label"]
if hp_label != null: hp_label.text = "%d/%d" % [hp, max_hp]
func remove_slot_panel(slot_id : int) -> void:
var entry : Dictionary = _panels.get(slot_id, {})
if entry.is_empty(): return
(entry["panel"] as Node).queue_free()
_panels.erase(slot_id)
func clear_panels() -> void:
for id in _panels.keys():
remove_slot_panel(id)
#---------- menus ----------
func ask(title : String, options : Array, allow_back : bool, focus_callback : Callable = Callable()) -> int:
#options : Array de {"text": String, "disabled": bool}. Renvoie l'index choisi, ou -1 pour "Retour".
_clear_menu()
_text_panel.hide()
_menu_title.text = title
for i in options.size():
var b := Button.new()
b.text = options[i]["text"]
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
b.custom_minimum_size = Vector2(320, 44)
var disabled : bool = options[i].get("disabled", false)
b.disabled = disabled
if disabled:
b.focus_mode = Control.FOCUS_NONE
else:
b.pressed.connect(_on_option_pressed.bind(i))
b.focus_entered.connect(_on_option_focused.bind(i))
b.mouse_entered.connect(b.grab_focus) #la souris deplace le focus
_menu_box.add_child(b)
if allow_back:
var back := Button.new()
back.text = "Retour"
back.alignment = HORIZONTAL_ALIGNMENT_LEFT
back.custom_minimum_size = Vector2(320, 44)
back.expand_icon = true
back.add_theme_constant_override("icon_max_width", 28)
var icon : Texture2D = ControllerIconTextureScript.new() #icone de la touche "annuler" (suit le peripherique)
icon.set("path", "ui_cancel")
back.icon = icon
back.pressed.connect(_menu_chosen.emit.bind(-1))
back.mouse_entered.connect(back.grab_focus)
_menu_box.add_child(back)
_menu_active = true
_menu_allow_back = allow_back
_focus_callback = focus_callback
_menu_panel.show()
_focus_first_enabled.call_deferred()
var index : int = await _menu_chosen
_menu_active = false
_focus_callback = Callable()
_menu_panel.hide()
_clear_menu()
return index
func _clear_menu() -> void:
for child in _menu_box.get_children():
_menu_box.remove_child(child)
child.queue_free()
func _focus_first_enabled() -> void:
for child in _menu_box.get_children():
if child is Button and child.focus_mode == Control.FOCUS_ALL:
child.grab_focus()
return
func _on_option_pressed(index : int) -> void:
_menu_chosen.emit(index)
func _on_option_focused(index : int) -> void:
if _focus_callback.is_valid(): _focus_callback.call(index)
#---------- choix du joueur ----------
func request_action(battle : Battle, slot : BattleSlot) -> BattleAction:
highlight_requested.emit([slot.id])
var action : BattleAction = null
while action == null:
var options : Array = [
{"text": "Combat"},
{"text": "Pokémon"},
{"text": "Fuite", "disabled": !battle.setup.can_flee},
]
var choice : int = await ask("Que doit faire %s ?" % slot.mon.display_name(), options, false)
match choice:
0: action = await _choose_move(battle, slot)
1:
var pokemon : PokemonInstance = await _choose_pokemon(battle, slot, true, "Quel Pokémon envoyer ?")
if pokemon != null: action = BattleAction.switch_pokemon(slot, pokemon)
2: action = BattleAction.flee(slot)
highlight_requested.emit([slot.id])
highlight_requested.emit([])
return action
func request_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance:
#pokemon K.O. : le joueur doit en envoyer un autre (pas de retour possible)
highlight_requested.emit([])
var pokemon : PokemonInstance = await _choose_pokemon(battle, slot, false, "%s est K.O. ! Qui envoyer ?" % slot.mon.display_name())
return pokemon
func _choose_move(battle : Battle, slot : BattleSlot) -> BattleAction:
var moves : Array[MoveSlot] = slot.mon.pokemon.active_moves
if battle.get_usable_moves(slot.mon).is_empty():
return BattleAction.use_struggle(slot) #plus aucune capacite utilisable : Lutte, sans menu
var options : Array = []
for ms in moves:
options.append({"text": "%s PP %d/%d" % [ms.move.get_display_name(), ms.current_pp, ms.max_pp], "disabled": !ms.has_pp()})
while true:
var index : int = await ask("Capacité", options, true)
if index < 0: return null
var move_slot : MoveSlot = moves[index]
var target : BattleSlot = null
if TargetResolver.needs_selection(battle, slot, move_slot.move):
target = await _choose_target(battle, slot, move_slot.move)
if target == null: continue #retour au choix de la capacite
return BattleAction.use_move(slot, move_slot, target)
return null
func _choose_target(battle : Battle, slot : BattleSlot, move : MoveData) -> BattleSlot:
var targets : Array[BattleSlot] = TargetResolver.get_selectable_targets(battle, slot, move)
var options : Array = []
for t in targets:
options.append({"text": "%s (%s)" % [t.mon.display_name(), "adversaire" if t.side != slot.side else "allié"]})
var on_focus := func(i : int) -> void: highlight_requested.emit([targets[i].id])
var index : int = await ask("Cible", options, true, on_focus)
highlight_requested.emit([slot.id])
return targets[index] if index >= 0 else null
func _choose_pokemon(battle : Battle, slot : BattleSlot, allow_back : bool, title : String) -> PokemonInstance:
var members : Array[PokemonInstance] = slot.participant.team.members
var available : Array[PokemonInstance] = battle.get_replacement_options(slot)
var options : Array = []
for p in members:
var note : String = ""
if p.is_fainted(): note = " (K.O.)"
elif !available.has(p): note = " (au combat)"
options.append({
"text": "%s Lv.%d PV %d/%d%s" % [p.get_display_name(), p.level, p.current_hp, p.get_max_hp(), note],
"disabled": !available.has(p),
})
var index : int = await ask(title, options, allow_back)
return members[index] if index >= 0 else null

View file

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

View file

@ -0,0 +1,189 @@
class_name BattleWorldPresenter
extends BattlePresenter
#met le combat en scene dans le monde 3D :
#- place les pokemon (un BattleActor par emplacement) face a face, a l'endroit de la rencontre
#- deplace une camera de combat qui cadre l'ensemble
#- joue les animations et affiche les messages / PV via BattleUI
#
#le monde (hors combat) est en pause : tout ce qui est ici fonctionne en mode "toujours".
var ui : BattleUI
var battle : Battle
#formation
var side_distance : float = 7.0 #distance entre les deux camps
var lateral_spacing : float = 2.6 #ecart entre deux pokemon d'un meme camp
var ground_mask : int = 1
#camera
var camera_back : float = 4.5 #recul derriere le camp du joueur
var camera_height : float = 3.4
var camera_move_time : float = 0.9
var _actors : Dictionary = {} #id d'emplacement -> BattleActor
var _formation : Dictionary = {} #id d'emplacement -> position au sol
var _center : Vector3 = Vector3.ZERO
var _axis : Vector3 = Vector3.FORWARD #direction camp joueur -> camp adverse
var _camera : Camera3D = null
var _previous_camera : Camera3D = null
var _world_root : Node = null #ou ajouter camera et acteurs : le monde que le joueur voit
func _init(p_ui : BattleUI) -> void:
ui = p_ui
ui.highlight_requested.connect(_on_highlight_requested)
#---------- mise en place ----------
func setup(p_battle : Battle, player_pos : Vector3, enemy_pos : Vector3, p_world_root : Node, p_game_camera : Camera3D) -> void:
battle = p_battle
_world_root = p_world_root
_previous_camera = p_game_camera
_axis = enemy_pos - player_pos
_axis.y = 0.0
if _axis.length() < 0.1:
#pas de direction evidente : on utilise la direction de la camera du jeu
var cam : Camera3D = _previous_camera
_axis = -cam.global_basis.z if cam else Vector3.FORWARD
_axis.y = 0.0
_axis = _axis.normalized()
_center = (player_pos + enemy_pos) * 0.5
var lateral : Vector3 = _axis.cross(Vector3.UP).normalized()
var count_per_side : Dictionary = {0: 0, 1: 0}
for slot in battle.slots:
count_per_side[slot.side] += 1
for slot in battle.slots:
var n : int = count_per_side[slot.side]
var offset : float = (slot.index - (n - 1) * 0.5) * lateral_spacing
var side_sign : float = -1.0 if slot.side == 0 else 1.0
var pos : Vector3 = _center + _axis * side_sign * side_distance * 0.5 + lateral * offset
_formation[slot.id] = _snap_to_ground(pos)
_start_camera(maxi(count_per_side[0], count_per_side[1]))
func _start_camera(max_slots : int) -> void:
var back : float = camera_back + max_slots * 0.8
var cam_pos : Vector3 = _center - _axis * (side_distance * 0.5 + back) + Vector3.UP * (camera_height + max_slots * 0.3)
var look_target : Vector3 = _center + Vector3.UP * 1.0
var final_transform := Transform3D(Basis.looking_at(look_target - cam_pos, Vector3.UP), cam_pos)
_camera = Camera3D.new()
_world_root.add_child(_camera)
_camera.global_transform = _previous_camera.global_transform if _previous_camera else final_transform
if _previous_camera: _camera.fov = _previous_camera.fov
_camera.make_current()
var tween : Tween = ui.create_tween()
tween.tween_property(_camera, "global_transform", final_transform, camera_move_time).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
func cleanup() -> void:
#fin du combat : ecran propre, camera du jeu reprise
ui.highlight_requested.emit([])
ui.hide_message()
ui.clear_panels()
for actor in _actors.values():
if is_instance_valid(actor): (actor as Node).queue_free()
_actors.clear()
if _camera != null:
if _previous_camera != null and is_instance_valid(_previous_camera):
var tween : Tween = ui.create_tween()
tween.tween_property(_camera, "global_transform", _previous_camera.global_transform, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
await tween.finished
_previous_camera.make_current()
_camera.queue_free()
_camera = null
#---------- evenements ----------
func present(event : BattleEvent) -> void:
match event.type:
BattleEvent.Type.BATTLE_START:
await ui.show_message(event.describe(), 1.0)
BattleEvent.Type.SENT_OUT:
await _show_actor(event.source_slot)
await ui.show_message(event.describe(), 0.9)
BattleEvent.Type.WITHDRAWN:
await ui.show_message(event.describe(), 0.7)
await _remove_actor(event.source_slot)
BattleEvent.Type.MOVE_USED:
var actor : BattleActor = _actors.get(event.source_slot)
if actor: actor.play_attack(_opponents_center(event.source_slot)) #en parallele du message
await ui.show_message(event.describe(), 1.0)
BattleEvent.Type.DAMAGE:
var actor : BattleActor = _actors.get(event.target_slot)
if actor: actor.play_hit()
await ui.animate_hp(event.target_slot, event.hp, event.max_hp)
BattleEvent.Type.FAINTED:
var actor : BattleActor = _actors.get(event.source_slot)
if actor: actor.play_faint()
ui.remove_slot_panel(event.source_slot)
await ui.show_message(event.describe(), 1.0)
BattleEvent.Type.TURN_START:
ui.hide_message() #pas de texte : les menus prennent la suite
_:
#MOVE_MISSED, CRITICAL, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT, FLED, MESSAGE, BATTLE_END...
var text : String = event.describe()
if text != "": await ui.show_message(text, 1.0)
func _show_actor(slot_id : int) -> void:
var slot : BattleSlot = battle.get_slot(slot_id)
if slot == null or slot.mon == null: return
if _actors.has(slot_id):
(_actors[slot_id] as Node).queue_free()
_actors.erase(slot_id)
var actor := BattleActor.new()
_world_root.add_child(actor)
actor.setup(slot.mon.pokemon, slot_id)
var facing : Vector3 = _center + _axis * (side_distance if slot.side == 0 else -side_distance)
actor.place(_formation.get(slot_id, _center), facing)
_actors[slot_id] = actor
ui.set_slot_pokemon(slot)
await actor.play_enter()
func _remove_actor(slot_id : int) -> void:
ui.remove_slot_panel(slot_id)
var actor : BattleActor = _actors.get(slot_id)
if actor == null: return
await actor.play_leave()
actor.queue_free()
_actors.erase(slot_id)
func _on_highlight_requested(slot_ids : Array) -> void:
for id in _actors:
(_actors[id] as BattleActor).set_highlight(slot_ids.has(id))
#---------- utilitaires ----------
func _opponents_center(slot_id : int) -> Vector3:
var slot : BattleSlot = battle.get_slot(slot_id)
if slot == null: return _center
var total : Vector3 = Vector3.ZERO
var count : int = 0
for other in battle.slots:
if other.side != slot.side and _actors.has(other.id):
total += (_actors[other.id] as BattleActor).home
count += 1
return total / count if count > 0 else _center
func _snap_to_ground(point : Vector3) -> Vector3:
var world : World3D = _previous_camera.get_world_3d() if _previous_camera != null else _world_root.get_viewport().world_3d
var space : PhysicsDirectSpaceState3D = world.direct_space_state
var query := PhysicsRayQueryParameters3D.create(point + Vector3.UP * 3.0, point + Vector3.DOWN * 8.0, ground_mask)
var hit : Dictionary = space.intersect_ray(query)
return hit["position"] if !hit.is_empty() else point

View file

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

View file

@ -0,0 +1,15 @@
class_name PlayerBattleController
extends BattleController
#controleur du joueur : les choix passent par les menus de BattleUI (manette, clavier, souris)
var ui : BattleUI
func _init(p_ui : BattleUI) -> void:
ui = p_ui
func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction:
return await ui.request_action(battle, slot)
func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance:
return await ui.request_replacement(battle, slot)

View file

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

View file

@ -2,11 +2,13 @@ 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
signal battle_requested(sent : SentPokemon, target : Node) #emis par ThrowState : BattleLauncher s'y branche
signal battle_finished(outcome : int, target : Node) #emis par BattleLauncher (outcome = valeur de Battle.Outcome)
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 in_battle : bool = false #true pendant un combat (le monde est en pause)
var selected_index : int = -1 #pokemon de l'equipe choisi pour etre envoye (HUD), -1 = aucun
func give_pokemon(pokemon : PokemonInstance) -> PokemonStorage.ReceiveResult:

View file

@ -3,10 +3,15 @@ extends Marker3D
class_name AimTarget
signal locked_changed(is_locked : bool)
signal battle_finished(outcome : int) #emis a la fin d'un combat lance depuis cette cible (valeur de Battle.Outcome)
@export var enabled : bool = true
@export var priority : float = 0.0 #plus c'est haut, plus la cible est favorisee
@export_group("Battle")
@export var encounter : BattleEncounter #si defini, lancer un pokemon sur cette cible demarre ce combat
@export var world_actor : Node3D #le pokemon/dresseur visible dans le monde : cache pendant le combat, supprime apres une victoire
@export_group("Indicator")
@export var indicator : Node3D #optionnel : ton propre visuel (remplace le losange par defaut)
@export var show_default_diamond : bool = true
@ -16,6 +21,10 @@ signal locked_changed(is_locked : bool)
var is_locked : bool = false
#utilise par ThrowState : true si cette cible declenche un combat
var starts_battle : bool:
get: return encounter != null
func _ready() -> void:
add_to_group("aim_targets")

View file

@ -143,6 +143,9 @@ func _on_ball_landed(landing_pos : Vector3, pokemon : PokemonInstance, target :
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)
#---------- nettoyage ----------

View file

@ -54,19 +54,24 @@ func _ready() -> void:
add_to_group(GROUP)
func _build_model() -> void:
var scene : PackedScene = pokemon.species.model_scene
model = create_model(pokemon)
add_child(model)
static func create_model(p : PokemonInstance) -> Node3D:
#modele 3D de l'espece ; capsule de remplacement s'il n'y en a pas
var result : Node3D = null
var scene : PackedScene = p.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
result = scene.instantiate() as Node3D
if result == null:
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)
result = mesh_instance
return result
func recall() -> void:
recalled.emit()

View file

@ -117,7 +117,7 @@ func _unhandled_input(event : InputEvent) -> void:
#---------- construction de l'interface ----------
func _build_ui() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) #le node est deja dans l'arbre : il faut aussi remettre les offsets a zero
var bg := ColorRect.new()
bg.color = Color(0, 0, 0, 0.8)

View file

@ -61,7 +61,7 @@ func _unhandled_input(event : InputEvent) -> void:
#---------- construction de l'interface ----------
func _build_ui() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) #le node est deja dans l'arbre : il faut aussi remettre les offsets a zero
var bg := ColorRect.new()
bg.color = Color(0, 0, 0, 0.8)

View file

@ -137,7 +137,7 @@ func _unhandled_input(event : InputEvent) -> void:
#---------- construction ----------
func _build_ui() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) #le node est deja dans l'arbre : il faut aussi remettre les offsets a zero
mouse_filter = Control.MOUSE_FILTER_IGNORE
_root_box = MarginContainer.new()

View file

@ -24,7 +24,7 @@ func _ready() -> void:
play_menu_music()
fade_in_game_infos()
func _process(delta: float) -> void:
func _process(_delta: float) -> void:
if !is_transitioning:
Start.grab_focus()

View file

@ -23,7 +23,7 @@ func _ready() -> void:
animation_player.play("RESET")
func _unhandled_input(event: InputEvent) -> void:
if is_transitioning or sub_screen != null: return #une page ouverte gere ses propres entrees
if is_transitioning or sub_screen != null or GameData.in_battle: return #une page ouverte gere ses propres entrees
if event.is_action_pressed("pause"):
if get_tree().paused:
resume()

View file

@ -6,9 +6,14 @@
[ext_resource type="Texture2D" uid="uid://djg8y25y06h5n" path="res://assets/textures/grids/Green/texture_09.png" id="4_jcfev"]
[ext_resource type="Texture2D" uid="uid://hu5rfl6qr2nv" path="res://assets/textures/grids/Red/texture_09.png" id="5_rkxil"]
[ext_resource type="Script" uid="uid://ce2wj6d8xb055" path="res://game/player/aim/aim_target.gd" id="6_ajh0j"]
[ext_resource type="Script" uid="uid://u2t6dm1ee3se" path="res://game/battle/battle_encounter.gd" id="7_g80jn"]
[ext_resource type="PackedScene" uid="uid://di22cs2qposdm" path="res://game/ui/hud/ui.tscn" id="7_yebv1"]
[ext_resource type="Script" uid="uid://cqsokrfi2d05y" path="res://game/pokemon/trainer_party_entry.gd" id="8_42ub6"]
[ext_resource type="PackedScene" uid="uid://d1mcef8gl66j" path="res://game/ui/pause_menu/pause_menu.tscn" id="8_g80jn"]
[ext_resource type="PackedScene" uid="uid://dwxpt0s7vn36k" path="res://game/player/player_character.tscn" id="9_42ub6"]
[ext_resource type="Script" uid="uid://chc1mf57j5m4p" path="res://addons/pokeapi_importer/data/move_data.gd" id="9_qk77p"]
[ext_resource type="Script" uid="uid://bx0htbw7ygkww" path="res://game/battle/battle_launcher.gd" id="10_rr16o"]
[ext_resource type="Resource" uid="uid://caim7okcsrjq4" path="res://data/pokeapi/pokemon/0025_pikachu.tres" id="10_ysylv"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_tipki"]
sky_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1)
@ -45,6 +50,18 @@ uv1_triplanar = true
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tipki"]
albedo_texture = ExtResource("5_rkxil")
[sub_resource type="Resource" id="Resource_yebv1"]
script = ExtResource("8_42ub6")
species = ExtResource("10_ysylv")
nickname = "Testkatchu"
metadata/_custom_type_script = "uid://cqsokrfi2d05y"
[sub_resource type="Resource" id="Resource_rr16o"]
script = ExtResource("7_g80jn")
wild_pokemon = Array[ExtResource("8_42ub6")]([SubResource("Resource_yebv1")])
can_flee = false
metadata/_custom_type_script = "uid://u2t6dm1ee3se"
[node name="world" type="Node3D" unique_id=832308324]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=455638012]
@ -105,14 +122,19 @@ material_override = SubResource("StandardMaterial3D_tipki")
use_collision = true
size = Vector3(0.5, 0.5, 0.5)
[node name="AimTarget" type="Marker3D" parent="map/target3" unique_id=1848628150]
[node name="AimTarget" type="Marker3D" parent="map/target3" unique_id=1848628150 node_paths=PackedStringArray("world_actor")]
transform = Transform3D(1, 0, 3.7252903e-09, 0, 1, 0, -3.7252903e-09, 0, 1, 0, 0.030708313, 0)
script = ExtResource("6_ajh0j")
encounter = SubResource("Resource_rr16o")
world_actor = NodePath("..")
[node name="BattleLauncher" type="Node" parent="." unique_id=1234981764]
script = ExtResource("10_rr16o")
[node name="Ui" parent="." unique_id=1881560149 instance=ExtResource("7_yebv1")]
[node name="PauseMenu" parent="." unique_id=871841579 instance=ExtResource("8_g80jn")]
process_mode = 3
[node name="PlayerCharacter" parent="." unique_id=1830301535 instance=ExtResource("9_42ub6")]
[node name="PlayerCharacter" parent="." unique_id=1830301535 groups=["player"] instance=ExtResource("9_42ub6")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.2155721, -0.00040316582, 0.73769236)

View file

@ -37,6 +37,10 @@ window/stretch/aspect="expand"
enabled=PackedStringArray("res://addons/controller_icons/plugin.cfg", "res://addons/pokeapi_importer/plugin.cfg")
[global_group]
player=""
[input]
ui_accept={