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 if await _blocked_before_hit(battle, actor, target, move): 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 var dealt : int = await battle.deal_damage(actor, target, int(calc["damage"])) total_dealt += dealt landed += 1 await _after_hit(battle, actor, target, move, dealt) 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: #capacites de meteo : une seule fois, quelles que soient les cibles var weather : int = BattleField.weather_for_move(move.api_name) if weather != BattleField.Weather.NONE: if !await battle.set_weather(weather, actor): await battle.message("Mais cela échoue !") return 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 target != actor and await _blocked_before_hit(battle, actor, target, move): 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.") #---------- crochets des talents ---------- static func _blocked_before_hit(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> bool: #un talent du defenseur peut annuler la capacite (Absorb Eau, Levitation...) for effect in target.mon.get_effects(): if await effect.on_before_hit(battle, actor, target, move): return true return false static func _after_hit(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData, damage : int) -> void: #talents de contact du defenseur (Statik, Peau Dure...) for effect in target.mon.get_effects(): await effect.on_after_hit(battle, actor, target, move, damage) #---------- 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)