Tales-of-PKMN/game/battle/battle.gd
2026-10-18 11:32:51 +02:00

307 lines
10 KiB
GDScript

class_name Battle
extends RefCounted
#moteur de combat au tour par tour, sans aucun visuel.
#gere n emplacements par camp (1v1, 2v2, 1 contre plusieurs, plusieurs contre 1).
#
#usage :
# var battle := Battle.new(setup, mon_presenter)
# var outcome : Battle.Outcome = await battle.run()
#
#a chaque evenement, la presentation est appelee et le combat attend qu'elle ait fini.
signal event_emitted(event : BattleEvent)
enum Outcome { NONE, PLAYER_WON, PLAYER_LOST, FLED, DRAW }
const PLAYER_SIDE : int = 0
static var _struggle : MoveData = null
var setup : BattleSetup
var presenter : BattlePresenter
var rng : RandomNumberGenerator = RandomNumberGenerator.new()
var turn : int = 0
var max_turns : int = 0 #0 = illimite ; sinon le combat s'arrete en match nul apres ce nombre de tours (utile pour les tests)
var outcome : Outcome = Outcome.NONE
var slots : Array[BattleSlot] = []
func _init(p_setup : BattleSetup, p_presenter : BattlePresenter = null) -> void:
setup = p_setup
presenter = p_presenter
if setup.seed_value != 0: rng.seed = setup.seed_value
else: rng.randomize()
_build_slots()
func _build_slots() -> void:
var per_side_count : Dictionary = {}
for participant in setup.participants:
for pokemon in participant.pick_starters():
var slot := BattleSlot.new()
slot.id = slots.size()
slot.side = participant.side
slot.index = int(per_side_count.get(participant.side, 0))
per_side_count[participant.side] = slot.index + 1
slot.participant = participant
slot.set_pokemon(pokemon)
slots.append(slot)
#---------- boucle principale ----------
func run() -> Outcome:
await _emit(BattleEvent.make(BattleEvent.Type.BATTLE_START))
for slot in slots:
await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
if _check_end():
return await _finish()
while outcome == Outcome.NONE:
if max_turns > 0 and turn >= max_turns:
outcome = Outcome.DRAW
break
turn += 1
var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START)
turn_event.amount = turn
await _emit(turn_event)
var actions : Array[BattleAction] = await _collect_actions()
_sort_actions(actions)
for action in actions:
await _execute(action)
if _check_end(): break
if outcome != Outcome.NONE: break
await _replace_fainted()
if _check_end(): break
return await _finish()
func _finish() -> Outcome:
var end_event := BattleEvent.make(BattleEvent.Type.BATTLE_END)
match outcome:
Outcome.PLAYER_WON: end_event.text = "Victoire !"
Outcome.PLAYER_LOST: end_event.text = "Défaite..."
Outcome.FLED: end_event.text = "Vous avez pris la fuite."
Outcome.DRAW: end_event.text = "Match nul (limite de tours atteinte)."
await _emit(end_event)
return outcome
func _emit(event : BattleEvent) -> void:
event_emitted.emit(event)
if presenter != null:
await presenter.present(event)
func _msg(text : String) -> void:
var e := BattleEvent.make(BattleEvent.Type.MESSAGE)
e.text = text
await _emit(e)
#---------- fin de combat ----------
func _side_alive(side : int) -> bool:
for participant in setup.participants:
if participant.side == side and participant.has_able_pokemon(): return true
return false
func _check_end() -> bool:
if outcome != Outcome.NONE: return true
var player_alive : bool = _side_alive(PLAYER_SIDE)
var enemy_alive : bool = _side_alive(1 - PLAYER_SIDE)
if player_alive and enemy_alive: return false
outcome = Outcome.PLAYER_WON if player_alive else Outcome.PLAYER_LOST
return true
#---------- actions du tour ----------
func _collect_actions() -> Array[BattleAction]:
var actions : Array[BattleAction] = []
for slot in slots:
if !slot.is_active(): continue
var action : BattleAction = await slot.participant.controller.choose_action(self, slot)
if action == null: action = BattleAction.use_struggle(slot) #securite : un controleur qui ne decide rien
actions.append(action)
return actions
func _sort_actions(actions : Array[BattleAction]) -> void:
for a in actions:
match a.type:
BattleAction.Type.FLEE: a.priority = 8
BattleAction.Type.SWITCH: a.priority = 7
BattleAction.Type.ITEM: a.priority = 6
_: a.priority = a.move.priority if a.move else 0
a.speed = a.slot.mon.get_stat(PokemonNature.Stat.SPEED)
a.tiebreak = rng.randf()
actions.sort_custom(func(a : BattleAction, b : BattleAction) -> bool:
if a.priority != b.priority: return a.priority > b.priority
if a.speed != b.speed: return a.speed > b.speed
return a.tiebreak > b.tiebreak)
func _execute(action : BattleAction) -> void:
if !action.slot.is_active(): return #mis K.O. avant son tour
match action.type:
BattleAction.Type.MOVE: await _execute_move(action)
BattleAction.Type.SWITCH: await _execute_switch(action)
BattleAction.Type.FLEE: await _execute_flee(action)
func _execute_flee(action : BattleAction) -> void:
if setup.can_flee and action.slot.side == PLAYER_SIDE:
outcome = Outcome.FLED
await _emit(BattleEvent.make(BattleEvent.Type.FLED, action.slot))
else:
await _msg("Impossible de fuir !")
func _execute_switch(action : BattleAction) -> void:
var slot : BattleSlot = action.slot
if !get_replacement_options(slot).has(action.switch_to): return
await _emit(BattleEvent.make(BattleEvent.Type.WITHDRAWN, slot))
slot.set_pokemon(action.switch_to)
await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
func _execute_move(action : BattleAction) -> void:
var actor : BattleSlot = action.slot
var move : MoveData = action.move
if action.move_slot != null:
if !action.move_slot.use():
await _msg("%s n'a plus de PP pour cette capacité !" % actor.mon.display_name())
return
var used := BattleEvent.make(BattleEvent.Type.MOVE_USED, actor)
used.move = move
await _emit(used)
var targets : Array[BattleSlot] = TargetResolver.resolve(self, actor, move, action.chosen_target)
if targets.is_empty():
await _msg("Mais cela échoue !")
return
var spread : bool = targets.size() > 1
var status_message_shown : bool = false
for target in targets:
if !target.is_active(): continue
if move.power <= 0 or move.category == "status":
#les effets des capacites de statut ne sont pas encore implementes
if !status_message_shown:
status_message_shown = true
await _msg("Mais cela n'a pas d'effet pour l'instant.")
continue
if !_hits(move):
await _emit(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
continue
var calc : Dictionary = DamageCalculator.calculate(actor.mon, target.mon, move, rng, spread)
var effectiveness : float = calc["effectiveness"]
if effectiveness == 0.0:
await _emit(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target))
continue
await _apply_damage(actor, target, int(calc["damage"]))
if calc["critical"]: await _emit(BattleEvent.make(BattleEvent.Type.CRITICAL, actor, target))
if effectiveness > 1.0: await _emit(BattleEvent.make(BattleEvent.Type.SUPER_EFFECTIVE, actor, target))
elif effectiveness < 1.0: await _emit(BattleEvent.make(BattleEvent.Type.NOT_VERY_EFFECTIVE, actor, target))
if target.mon.is_fainted(): await _emit(BattleEvent.make(BattleEvent.Type.FAINTED, target))
if _check_end(): return
#Lutte : contrecoup pour l'utilisateur
if move.api_name == "struggle" and actor.mon != null and !actor.mon.is_fainted():
var recoil : int = maxi(1, floori(actor.mon.pokemon.get_max_hp() / 4.0))
await _apply_damage(actor, actor, recoil)
if actor.mon.is_fainted(): await _emit(BattleEvent.make(BattleEvent.Type.FAINTED, actor))
func _apply_damage(source : BattleSlot, target : BattleSlot, amount : int) -> void:
var before : int = target.mon.pokemon.current_hp
target.mon.pokemon.take_damage(amount)
var event := BattleEvent.make(BattleEvent.Type.DAMAGE, source, target)
event.amount = before - target.mon.pokemon.current_hp
event.hp = target.mon.pokemon.current_hp
event.max_hp = target.mon.pokemon.get_max_hp()
await _emit(event)
func _hits(move : MoveData) -> bool:
#precision 0 = ne rate jamais (valeur PokeAPI "null")
return move.accuracy <= 0 or rng.randi_range(1, 100) <= move.accuracy
#---------- remplacements ----------
func _replace_fainted() -> void:
for slot in slots:
if slot.mon == null or !slot.mon.is_fainted(): continue
var options : Array[PokemonInstance] = get_replacement_options(slot)
if options.is_empty():
slot.clear() #plus de remplacant : l'emplacement reste vide
continue
var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot)
if chosen == null or !options.has(chosen): chosen = options[0]
slot.set_pokemon(chosen)
await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
#---------- requetes (utilisees par les controleurs et TargetResolver) ----------
func random_pick(items : Array) -> Variant:
#tirage dans le generateur du combat (et non le hasard global) : un meme seed rejoue exactement le meme combat
return items[rng.randi_range(0, items.size() - 1)]
func get_slot(id : int) -> BattleSlot:
return slots[id] if id >= 0 and id < slots.size() else null
func get_active_slots() -> Array[BattleSlot]:
var result : Array[BattleSlot] = []
for s in slots:
if s.is_active(): result.append(s)
return result
func get_opponents(slot : BattleSlot) -> Array[BattleSlot]:
var result : Array[BattleSlot] = []
for s in slots:
if s.is_active() and s.side != slot.side: result.append(s)
return result
func get_allies(slot : BattleSlot) -> Array[BattleSlot]:
var result : Array[BattleSlot] = []
for s in slots:
if s.is_active() and s.side == slot.side and s != slot: result.append(s)
return result
func get_usable_moves(mon : BattleMon) -> Array[MoveSlot]:
var result : Array[MoveSlot] = []
for ms in mon.pokemon.active_moves:
if ms.has_pp(): result.append(ms)
return result
func get_replacement_options(slot : BattleSlot) -> Array[PokemonInstance]:
#pokemon aptes de l'equipe du participant qui ne sont pas deja en jeu
var result : Array[PokemonInstance] = []
for p in slot.participant.team.members:
if p.is_fainted() or _is_in_play(p): continue
result.append(p)
return result
func _is_in_play(pokemon : PokemonInstance) -> bool:
for s in slots:
if s.mon != null and s.mon.pokemon == pokemon: return true
return false
#---------- Lutte ----------
static func make_struggle() -> MoveData:
if _struggle == null:
_struggle = MoveData.new()
_struggle.api_name = "struggle"
_struggle.names = {"fr": "Lutte", "en": "Struggle"}
_struggle.type = "typeless"
_struggle.category = "physical"
_struggle.power = 50
_struggle.accuracy = 0
_struggle.pp = 1
_struggle.priority = 0
_struggle.target = "random-opponent"
return _struggle