Tales-of-PKMN/game/battle/move_executor.gd

176 lines
7.8 KiB
GDScript

class_name MoveExecutor
extends RefCounted
#execute une capacite : precision, degats, coups multiples, puis effets guides par les donnees de MoveData
#(statut, changements de stats, vol de PV, contrecoup, soin, peur...).
#les cas tres particuliers (Abri, Vol, Clonage...) viendront en scripts a part.
static func execute(battle : Battle, action : BattleAction) -> void:
var actor : BattleSlot = action.slot
var move : MoveData = action.move
#crochets "avant l'action" : sommeil, gel, paralysie, peur, confusion... (pas de PP consommes si le pokemon n'agit pas)
if !await battle.run_before_action(action): return
if action.move_slot != null and !action.move_slot.use():
await battle.message("%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 battle.emit_event(used)
var targets : Array[BattleSlot] = TargetResolver.resolve(battle, actor, move, action.chosen_target)
if targets.is_empty():
await battle.message("Mais cela échoue !")
return
var damaging : bool = (move.power > 0 or move.meta_category == "ohko") and move.category != "status"
if damaging:
await _use_damaging(battle, actor, move, targets)
else:
await _use_status(battle, actor, move, targets)
#Lutte : contrecoup pour l'utilisateur
if move.api_name == "struggle" and actor.is_active():
await battle.hurt(actor, maxi(1, floori(actor.mon.pokemon.get_max_hp() / 4.0)), "%s est blessé par le contrecoup !" % actor.mon.display_name())
#---------- capacites offensives ----------
static func _use_damaging(battle : Battle, actor : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> void:
var spread : bool = targets.size() > 1
var total_dealt : int = 0
for target in targets:
if !target.is_active(): continue
if move.meta_category == "ohko":
total_dealt += await _use_ohko(battle, actor, target, move)
if battle.check_end(): break
continue
if !_hits(battle, actor, target, move):
await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
continue
var hits : int = _roll_hit_count(battle, move)
var landed : int = 0
var effectiveness : float = 1.0
for i in hits:
if !target.is_active(): break
var calc : Dictionary = DamageCalculator.calculate(actor.mon, target.mon, move, battle.rng, spread)
effectiveness = calc["effectiveness"]
if effectiveness == 0.0: break
total_dealt += await battle.deal_damage(actor, target, int(calc["damage"]))
landed += 1
if calc["critical"]: await battle.emit_event(BattleEvent.make(BattleEvent.Type.CRITICAL, actor, target))
if landed == 0:
if effectiveness == 0.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target))
continue
if landed > 1: await battle.message("Touché %d fois !" % landed)
if effectiveness > 1.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.SUPER_EFFECTIVE, actor, target))
elif effectiveness < 1.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.NOT_VERY_EFFECTIVE, actor, target))
if target.mon.is_fainted():
await battle.emit_event(BattleEvent.make(BattleEvent.Type.FAINTED, target))
else:
await _secondary_effects(battle, actor, target, move)
if battle.check_end(): break
#vol de PV (drain > 0) ou contrecoup (drain < 0), proportionnels aux degats infliges
if total_dealt > 0 and move.drain != 0 and actor.is_active():
var amount : int = maxi(1, floori(total_dealt * absf(move.drain) / 100.0))
if move.drain > 0:
await battle.heal(actor, amount, "%s a absorbé de l'énergie !" % actor.mon.display_name())
else:
await battle.hurt(actor, amount, "%s est blessé par le contrecoup !" % actor.mon.display_name())
static func _use_ohko(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> int:
#capacites K.O. en un coup : echouent si la cible est d'un niveau superieur ; precision 30 % + ecart de niveau
var level_gap : int = actor.mon.pokemon.level - target.mon.pokemon.level
if level_gap < 0:
await battle.message("Mais cela échoue !")
return 0
if TypeChart.get_multiplier(move.type, target.mon.get_types()) == 0.0:
await battle.emit_event(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target))
return 0
if battle.rng.randi_range(1, 100) > 30 + level_gap:
await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
return 0
var dealt : int = await battle.deal_damage(actor, target, target.mon.pokemon.current_hp)
await battle.message("K.O. en un coup !")
if target.mon.is_fainted(): await battle.emit_event(BattleEvent.make(BattleEvent.Type.FAINTED, target))
return dealt
## Effets secondaires d'une capacite offensive, appliques a une cible encore en vie.
static func _secondary_effects(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> void:
#statut
if move.ailment != "none" and move.ailment_chance > 0 and target.is_active():
if battle.rng.randi_range(1, 100) <= move.ailment_chance:
await battle.inflict_status(actor, target, move.ailment, move, false)
#peur : seulement si la cible n'a pas encore agi ce tour
if move.flinch_chance > 0 and target.is_active() and !target.mon.acted_this_turn:
if battle.rng.randi_range(1, 100) <= move.flinch_chance and !target.mon.has_volatile(&"flinch"):
target.mon.add_volatile(FlinchEffect.new())
#changements de stats : "damage+raise" touche l'utilisateur, "damage+lower" la cible
if !move.stat_changes.is_empty() and battle.rng.randi_range(1, 100) <= move.get_stat_chance():
var affected : BattleSlot = actor if move.meta_category == "damage+raise" else target
for sc in move.stat_changes:
await battle.change_stat(affected, sc.stat, sc.change)
#---------- capacites de statut ----------
static func _use_status(battle : Battle, actor : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> void:
var handled : bool = false
for target in targets:
if !target.is_active(): continue
if target != actor and !_hits(battle, actor, target, move):
await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
continue
if move.ailment != "none":
handled = true
var chance : int = move.get_ailment_chance()
if battle.rng.randi_range(1, 100) <= chance:
await battle.inflict_status(actor, target, move.ailment, move, true)
if !move.stat_changes.is_empty():
handled = true
if battle.rng.randi_range(1, 100) <= move.get_stat_chance():
for sc in move.stat_changes:
await battle.change_stat(target, sc.stat, sc.change)
if move.healing > 0:
handled = true
var amount : int = maxi(1, floori(target.mon.pokemon.get_max_hp() * move.healing / 100.0))
var healed : int = await battle.heal(target, amount, "%s récupère des PV !" % target.mon.display_name())
if healed == 0:
await battle.message("Les PV de %s sont déjà au maximum !" % target.mon.display_name())
if !handled:
await battle.message("Mais cela n'a pas d'effet pour l'instant.")
#---------- precision et coups multiples ----------
static func _hits(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> bool:
#precision 0 = ne rate jamais (valeur PokeAPI "null") ; sinon precision de la capacite x boosts de precision/esquive
if move.accuracy <= 0: return true
var chance : float = move.accuracy * actor.mon.accuracy_multiplier_against(target.mon)
return battle.rng.randi_range(1, 100) <= int(chance)
static func _roll_hit_count(battle : Battle, move : MoveData) -> int:
if move.max_hits <= 1: return 1
if move.min_hits >= move.max_hits: return move.max_hits
if move.min_hits == 2 and move.max_hits == 5:
#repartition officielle : 2 ou 3 coups 35 % chacun, 4 ou 5 coups 15 % chacun
var roll : int = battle.rng.randi_range(1, 100)
return 2 if roll <= 35 else (3 if roll <= 70 else (4 if roll <= 85 else 5))
return battle.rng.randi_range(move.min_hits, move.max_hits)