diff --git a/Data/Scripts/011_Battle/001_Battle/005_Battle_ActionSwitching.rb b/Data/Scripts/011_Battle/001_Battle/005_Battle_ActionSwitching.rb index 96655bfbb..11959fc43 100644 --- a/Data/Scripts/011_Battle/001_Battle/005_Battle_ActionSwitching.rb +++ b/Data/Scripts/011_Battle/001_Battle/005_Battle_ActionSwitching.rb @@ -6,7 +6,7 @@ class Battle # battle. # NOTE: Messages are only shown while in the party screen when choosing a # command for the next round. - def pbCanSwitchLax?(idxBattler, idxParty, partyScene = nil) + def pbCanSwitchIn?(idxBattler, idxParty, partyScene = nil) return true if idxParty < 0 party = pbParty(idxBattler) return false if idxParty >= party.length @@ -18,8 +18,7 @@ class Battle if !pbIsOwner?(idxBattler, idxParty) if partyScene owner = pbGetOwnerFromPartyIndex(idxBattler, idxParty) - partyScene.pbDisplay(_INTL("You can't switch {1}'s Pokémon with one of yours!", - owner.name)) + partyScene.pbDisplay(_INTL("You can't switch {1}'s Pokémon with one of yours!", owner.name)) end return false end @@ -35,30 +34,15 @@ class Battle end # Check whether the currently active Pokémon (at battler index idxBattler) can - # switch out (and that its replacement at party index idxParty can switch in). - # NOTE: Messages are only shown while in the party screen when choosing a - # command for the next round. - def pbCanSwitch?(idxBattler, idxParty = -1, partyScene = nil) - # Check whether party Pokémon can switch in - return false if !pbCanSwitchLax?(idxBattler, idxParty, partyScene) - # Make sure another battler isn't already choosing to switch to the party - # Pokémon - allSameSideBattlers(idxBattler).each do |b| - next if choices[b.index][0] != :SwitchOut || choices[b.index][1] != idxParty - partyScene&.pbDisplay(_INTL("{1} has already been selected.", - pbParty(idxBattler)[idxParty].name)) - return false - end - # Check whether battler can switch out + # switch out. + def pbCanSwitchOut?(idxBattler, partyScene = nil) battler = @battlers[idxBattler] return true if battler.fainted? # Ability/item effects that allow switching no matter what - if battler.abilityActive? && - Battle::AbilityEffects.triggerCertainSwitching(battler.ability, battler, self) + if battler.abilityActive? && Battle::AbilityEffects.triggerCertainSwitching(battler.ability, battler, self) return true end - if battler.itemActive? && - Battle::ItemEffects.triggerCertainSwitching(battler.item, battler, self) + if battler.itemActive? && Battle::ItemEffects.triggerCertainSwitching(battler.item, battler, self) return true end # Other certain switching effects @@ -72,25 +56,42 @@ class Battle allOtherSideBattlers(idxBattler).each do |b| next if !b.abilityActive? if Battle::AbilityEffects.triggerTrappingByTarget(b.ability, battler, b, self) - partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", - b.pbThis, b.abilityName)) + partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", b.pbThis, b.abilityName)) return false end end allOtherSideBattlers(idxBattler).each do |b| next if !b.itemActive? if Battle::ItemEffects.triggerTrappingByTarget(b.item, battler, b, self) - partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", - b.pbThis, b.itemName)) + partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", b.pbThis, b.itemName)) return false end end return true end + # Check whether the currently active Pokémon (at battler index idxBattler) can + # switch out (and that its replacement at party index idxParty can switch in). + # NOTE: Messages are only shown while in the party screen when choosing a + # command for the next round. + def pbCanSwitch?(idxBattler, idxParty = -1, partyScene = nil) + # Check whether party Pokémon can switch in + return false if !pbCanSwitchIn?(idxBattler, idxParty, partyScene) + # Make sure another battler isn't already choosing to switch to the party + # Pokémon + allSameSideBattlers(idxBattler).each do |b| + next if choices[b.index][0] != :SwitchOut || choices[b.index][1] != idxParty + partyScene&.pbDisplay(_INTL("{1} has already been selected.", + pbParty(idxBattler)[idxParty].name)) + return false + end + # Check whether battler can switch out + return pbCanSwitchOut?(idxBattler, partyScene) + end + def pbCanChooseNonActive?(idxBattler) pbParty(idxBattler).each_with_index do |_pkmn, i| - return true if pbCanSwitchLax?(idxBattler, i) + return true if pbCanSwitchIn?(idxBattler, i) end return false end @@ -113,7 +114,7 @@ class Battle ret = -1 @scene.pbPartyScreen(idxBattler, canCancel) do |idxParty, partyScene| if checkLaxOnly - next false if !pbCanSwitchLax?(idxBattler, idxParty, partyScene) + next false if !pbCanSwitchIn?(idxBattler, idxParty, partyScene) elsif !pbCanSwitch?(idxBattler, idxParty, partyScene) next false end @@ -130,7 +131,7 @@ class Battle # things happening (U-turn, Baton Pass, in def pbEORSwitch). def pbSwitchInBetween(idxBattler, checkLaxOnly = false, canCancel = false) return pbPartyScreen(idxBattler, checkLaxOnly, canCancel) if !@controlPlayer && pbOwnedByPlayer?(idxBattler) - return @battleAI.pbDefaultChooseNewEnemy(idxBattler, pbParty(idxBattler)) + return @battleAI.pbDefaultChooseNewEnemy(idxBattler) end #============================================================================= @@ -206,7 +207,7 @@ class Battle if random choices = [] # Find all Pokémon that can switch in eachInTeamFromBattlerIndex(idxBattler) do |_pkmn, i| - choices.push(i) if pbCanSwitchLax?(idxBattler, i) + choices.push(i) if pbCanSwitchIn?(idxBattler, i) end return -1 if choices.length == 0 return choices[pbRandom(choices.length)] diff --git a/Data/Scripts/011_Battle/001_Battle/009_Battle_CommandPhase.rb b/Data/Scripts/011_Battle/001_Battle/009_Battle_CommandPhase.rb index caaa75c04..9f38551f8 100644 --- a/Data/Scripts/011_Battle/001_Battle/009_Battle_CommandPhase.rb +++ b/Data/Scripts/011_Battle/001_Battle/009_Battle_CommandPhase.rb @@ -140,7 +140,7 @@ class Battle def pbPartyMenu(idxBattler) ret = -1 if @debug - ret = @battleAI.pbDefaultChooseNewEnemy(idxBattler, pbParty(idxBattler)) + ret = @battleAI.pbDefaultChooseNewEnemy(idxBattler) else ret = pbPartyScreen(idxBattler, false, true, true) end diff --git a/Data/Scripts/011_Battle/003_Move/013_MoveEffects_SwitchingActing.rb b/Data/Scripts/011_Battle/003_Move/013_MoveEffects_SwitchingActing.rb index ab04bead9..8b3c52c02 100644 --- a/Data/Scripts/011_Battle/003_Move/013_MoveEffects_SwitchingActing.rb +++ b/Data/Scripts/011_Battle/003_Move/013_MoveEffects_SwitchingActing.rb @@ -182,7 +182,7 @@ class Battle::Move::SwitchOutTargetStatusMove < Battle::Move if @battle.trainerBattle? canSwitch = false @battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i| - next if !@battle.pbCanSwitchLax?(target.index, i) + next if !@battle.pbCanSwitchIn?(target.index, i) canSwitch = true break end diff --git a/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb b/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb index ebceb9705..7097b43f4 100644 --- a/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb +++ b/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb @@ -51,14 +51,33 @@ class Battle::AI # Choose an action. def pbDefaultChooseEnemyCommand(idxBattler) set_up(idxBattler) - return if pbEnemyShouldWithdraw? - return if pbEnemyShouldUseItem? - return if @battle.pbAutoFightMenu(idxBattler) + ret = false + PBDebug.logonerr { ret = pbChooseToSwitchOut } + if ret + PBDebug.log("") + return + end + if pbEnemyShouldUseItem? + PBDebug.log("") + return + end + if @battle.pbAutoFightMenu(idxBattler) + PBDebug.log("") + return + end @battle.pbRegisterMegaEvolution(idxBattler) if pbEnemyShouldMegaEvolve? choices = pbGetMoveScores pbChooseMove(choices) PBDebug.log("") end + + # Choose a replacement Pokémon (called directly from @battle, not part of + # action choosing). Must return the party index of a replacement Pokémon if + # possible. + def pbDefaultChooseNewEnemy(idxBattler) + set_up(idxBattler) + return choose_best_replacement_pokemon(idxBattler, true) + end end #=============================================================================== @@ -72,12 +91,14 @@ module Battle::AI::Handlers MoveBasePower = HandlerHash.new GeneralMoveScore = HandlerHash.new GeneralMoveAgainstTargetScore = HandlerHash.new - # Move type - uses main battle code via rough_type - # Move accuracy - uses main battle code via rough_accuracy - # Move target - # Move additional effect chance - # Move unselectable check - # Move failure check + # TODO: Make HandlerHashes for these? + # Move type - uses main battle code via rough_type + # Move accuracy - uses main battle code via rough_accuracy + # Move target + # Move additional effect chance + # Move unselectable check + # Move failure check + ShouldSwitch = HandlerHash.new def self.move_will_fail?(function_code, *args) return MoveFailureCheck.trigger(function_code, *args) || false @@ -117,4 +138,13 @@ module Battle::AI::Handlers end return score end + + def self.should_switch?(*args) + ret = false + ShouldSwitch.each do |id, switch_proc| + ret ||= switch_proc.call(*args) + break if ret + end + return ret + end end diff --git a/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb b/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb index 59b22d5e0..f67df7f0a 100644 --- a/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb +++ b/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb @@ -2,184 +2,268 @@ class Battle::AI #============================================================================= # Decide whether the opponent should switch Pokémon #============================================================================= - def pbEnemyShouldWithdraw? - ret = false - PBDebug.logonerr { ret = pbEnemyShouldWithdrawEx?(false) } - return ret - end - - def pbEnemyShouldWithdrawEx?(forceSwitch) + # Called by the AI's def pbDefaultChooseEnemyCommand, and by def pbChooseMove + # if the only moves known are bad ones (the latter forces a switch). Also + # aliased by the Battle Palace and Battle Arena. + def pbChooseToSwitchOut(force_switch = false) return false if @user.wild? - shouldSwitch = forceSwitch - battler = @user.battler - batonPass = -1 - foe = nil - moveType = nil - # If Pokémon is within 6 levels of the foe, and foe's last move was - # super-effective and powerful - if !shouldSwitch && battler.turnCount > 0 && @trainer.high_skill? - target = battler.pbDirectOpposing(true) - foe = @battlers[target.index] - if !target.fainted? && target.lastMoveUsed && - (target.level - battler.level).abs <= 6 - moveData = GameData::Move.get(target.lastMoveUsed) - moveType = moveData.type - typeMod = @user.effectiveness_of_type_against_battler(moveType, foe) - if Effectiveness.super_effective?(typeMod) && moveData.power > 50 - switchChance = (moveData.power > 70) ? 30 : 20 - shouldSwitch = (pbAIRandom(100) < switchChance) - end - end - end - # Pokémon can't do anything (must have been in battle for at least 5 rounds) - if !shouldSwitch && !@battle.pbCanChooseAnyMove?(battler.index) && - battler.turnCount && battler.turnCount >= 5 - shouldSwitch = true - end - # Pokémon is about to faint because of Perish Song - if !shouldSwitch && battler.effects[PBEffects::PerishSong] == 1 - shouldSwitch = true - end - # Pokémon is Perish Songed and has Baton Pass - if @trainer.high_skill? && battler.effects[PBEffects::PerishSong] == 1 - battler.eachMoveWithIndex do |m, i| - next if m.function != "SwitchOutUserPassOnEffects" # Baton Pass - next if !@battle.pbCanChooseMove?(battler.index, i, false) - batonPass = i + return false if !@battle.pbCanSwitchOut?(@user.index) + # Don't switch if there is a single foe and it is resting after Hyper Beam + # or will Truant (i.e. free turn) + if @trainer.high_skill? + foe_can_act = false + each_foe_battler(@user.side) do |b| + next if !b.can_attack? + foe_can_act = true break end + return false if !foe_can_act end - # Pokémon will faint because of bad poisoning at the end of this round, but - # would survive at least one more round if it were regular poisoning instead - if !shouldSwitch && battler.status == :POISON && battler.statusCount > 0 && @trainer.high_skill? - toxicHP = battler.totalhp / 16 - nextToxicHP = toxicHP * (battler.effects[PBEffects::Toxic] + 1) - if battler.hp <= nextToxicHP && battler.hp > toxicHP * 2 - shouldSwitch = true if pbAIRandom(100) < 80 - end + # Various calculations to decide whether to switch + if force_switch + PBDebug.log_ai("#{@user.name} is being forced to switch out") + else + should_switch = Battle::AI::Handlers.should_switch?(@user, self, @battle) + return false if !should_switch end - # Pokémon is Encored into an unfavourable move - if !shouldSwitch && battler.effects[PBEffects::Encore] > 0 && @trainer.medium_skill? - idxEncoredMove = battler.pbEncoredMoveIndex - if idxEncoredMove >= 0 - scoreSum = 0 - scoreCount = 0 - battler.allOpposing.each do |b| - set_up_move_check(battler.moves[idxEncoredMove]) - scoreSum += pbGetMoveScore([b]) - scoreCount += 1 - end - if scoreCount > 0 && scoreSum / scoreCount <= 20 - shouldSwitch = true if pbAIRandom(100) < 80 - end - end + # Want to switch; find the best replacement Pokémon + idxParty = choose_best_replacement_pokemon(@user.index, force_switch) + if idxParty < 0 # No good replacement Pokémon found + PBDebug.log(" => no good replacement Pokémon, will not switch after all") + return false end - # If there is a single foe and it is resting after Hyper Beam or is - # Truanting (i.e. free turn) - if shouldSwitch && @battle.pbSideSize(battler.index + 1) == 1 && - !battler.pbDirectOpposing.fainted? && @trainer.high_skill? - opp = battler.pbDirectOpposing - if (opp.effects[PBEffects::HyperBeam] > 0 || - (opp.hasActiveAbility?(:TRUANT) && opp.effects[PBEffects::Truant])) - shouldSwitch = false if pbAIRandom(100) < 80 - end + # Prefer using Baton Pass instead of switching + baton_pass = -1 + @user.battler.eachMoveWithIndex do |m, i| + next if m.function != "SwitchOutUserPassOnEffects" # Baton Pass + next if !@battle.pbCanChooseMove?(@user.index, i, false) + baton_pass = i + break end - # Sudden Death rule - I'm not sure what this means - if !shouldSwitch && @battle.rules["suddendeath"] && battler.turnCount > 0 - if battler.hp <= battler.totalhp / 4 && pbAIRandom(100) < 30 - shouldSwitch = true - elsif battler.hp <= battler.totalhp / 2 && pbAIRandom(100) < 80 - shouldSwitch = true - end - end - if shouldSwitch - list = [] - idxPartyStart, idxPartyEnd = @battle.pbTeamIndexRangeFromBattlerIndex(battler.index) - @battle.pbParty(battler.index).each_with_index do |pkmn, i| - next if @trainer.has_skill_flag?("ReserveLastPokemon") && i == idxPartyEnd - 1 # Don't choose to switch in ace - next if !@battle.pbCanSwitch?(battler.index, i) - # If perish count is 1, it may be worth it to switch - # even with Spikes, since Perish Song's effect will end - if battler.effects[PBEffects::PerishSong] != 1 - # Will contain effects that recommend against switching - spikes = battler.pbOwnSide.effects[PBEffects::Spikes] - # Don't switch to this if too little HP - if spikes > 0 - spikesDmg = [8, 6, 4][spikes - 1] - next if pkmn.hp <= pkmn.totalhp / spikesDmg && - !pkmn.hasType?(:FLYING) && !pkmn.hasAbility?(:LEVITATE) - end - end - # moveType is the type of the target's last used move - if moveType && Effectiveness.ineffective?(@user.effectiveness_of_type_against_battler(moveType, foe)) - weight = 65 - typeMod = pbCalcTypeModPokemon(pkmn, battler.pbDirectOpposing(true)) - if Effectiveness.super_effective?(typeMod) - # Greater weight if new Pokemon's type is effective against target - weight = 85 - end - list.unshift(i) if pbAIRandom(100) < weight # Put this Pokemon first - elsif moveType && Effectiveness.resistant?(@user.effectiveness_of_type_against_battler(moveType, foe)) - weight = 40 - typeMod = pbCalcTypeModPokemon(pkmn, battler.pbDirectOpposing(true)) - if Effectiveness.super_effective?(typeMod) - # Greater weight if new Pokemon's type is effective against target - weight = 60 - end - list.unshift(i) if pbAIRandom(100) < weight # Put this Pokemon first - else - list.push(i) # put this Pokemon last - end - end - if list.length > 0 - if batonPass >= 0 && @battle.pbRegisterMove(battler.index, batonPass, false) - PBDebug.log_ai("#{@user.name} will use Baton Pass to avoid Perish Song") - return true - end - if @battle.pbRegisterSwitch(battler.index, list[0]) - PBDebug.log_ai("#{@user.name} will switch with #{@battle.pbParty(battler.index)[list[0]].name}") - return true - end - end + if baton_pass >= 0 && @battle.pbRegisterMove(@user.index, baton_pass, false) + PBDebug.log(" => will use Baton Pass to switch out") + return true + elsif @battle.pbRegisterSwitch(@user.index, idxParty) + PBDebug.log(" => will switch with #{@battle.pbParty(@user.index)[idxParty].name}") + return true end return false end - # Choose a replacement Pokémon (called directly from @battle, not part of - # action choosing). - def pbDefaultChooseNewEnemy(idxBattler, party) - set_up(idxBattler) - enemies = [] + #============================================================================= + + def choose_best_replacement_pokemon(idxBattler, mandatory = false) + # Get all possible replacement Pokémon + party = @battle.pbParty(idxBattler) idxPartyStart, idxPartyEnd = @battle.pbTeamIndexRangeFromBattlerIndex(idxBattler) - party.each_with_index do |_p, i| - if @trainer.has_skill_flag?("ReserveLastPokemon") - next if i == idxPartyEnd - 1 && enemies.length > 0 # Ignore ace if possible + reserves = [] + party.each_with_index do |_pkmn, i| + next if !@battle.pbCanSwitchIn?(idxBattler, i) + if !mandatory + ally_will_switch_with_i = false + @battle.allSameSideBattlers(idxBattler).each do |b| + next if @battle.choices[b.index][0] != :SwitchOut || @battle.choices[b.index][1] != i + ally_will_switch_with_i = true + break + end + next if ally_will_switch_with_i end - enemies.push(i) if @battle.pbCanSwitchLax?(idxBattler, i) + # Ignore ace if possible + if @trainer.has_skill_flag?("ReserveLastPokemon") && i == idxPartyEnd - 1 + next if !mandatory || reserves.length > 0 + end + reserves.push([i, 100]) + break if @trainer.has_skill_flag?("UsePokemonInOrder") && reserves.length > 0 end - return -1 if enemies.length == 0 - return pbChooseBestNewEnemy(idxBattler, party, enemies) + return -1 if reserves.length == 0 + # Rate each possible replacement Pokémon + reserves.each_with_index do |reserve, i| + reserves[i][1] = rate_replacement_pokemon(idxBattler, party[reserve[0]], reserve[1]) + end + reserves.sort! { |a, b| b[1] <=> a[1] } # Sort from highest to lowest rated + # Don't bother choosing to switch if all replacements are poorly rated + if !mandatory + # TODO: Should the current battler be rated as well, to provide a + # threshold instead of using a threshold of 100? + return -1 if reserves[0][1] < 100 # Best replacement rated at <100, don't switch + end + # Return the best rated replacement Pokémon + return reserves[0][0] end - def pbChooseBestNewEnemy(idxBattler, party, enemies) - return -1 if !enemies || enemies.length == 0 - best = -1 - bestSum = 0 - enemies.each do |i| - pkmn = party[i] - sum = 0 - pkmn.moves.each do |m| - next if m.power == 0 - @battle.battlers[idxBattler].allOpposing.each do |b| - bTypes = b.pbTypes(true) - sum += Effectiveness.calculate(m.type, *bTypes) - end - end - if best == -1 || sum > bestSum - best = i - bestSum = sum + def rate_replacement_pokemon(idxBattler, pkmn, score) + battler_side = @battle.sides[idxBattler & 1] + pkmn_types = pkmn.types + entry_hazard_damage = 0 + # Stealth Rock + if battler_side.effects[PBEffects::StealthRock] && !pkmn.hasAbility?(:MAGICGUARD) && + GameData::Type.exists?(:ROCK) && !pkmn.hasItem?(:HEAVYDUTYBOOTS) + eff = Effectiveness.calculate(:ROCK, *pkmn_types) + if !Effectiveness.ineffective?(eff) + entry_hazard_damage += pkmn.totalhp * eff / 8 end end - return best + # Spikes + if battler_side.effects[PBEffects::Spikes] > 0 && !pkmn.hasAbility?(:MAGICGUARD) && + !battler.airborne? && !pkmn.hasItem?(:HEAVYDUTYBOOTS) + if @battle.field.effects[PBEffects::Gravity] > 0 || pkmn.hasItem?(:IRONBALL) || + !(pkmn.hasType?(:FLYING) || pkmn.hasItem?(:LEVITATE) || pkmn.hasItem?(:AIRBALLOON)) + spikes_div = [8, 6, 4][battler_side.effects[PBEffects::Spikes] - 1] + entry_hazard_damage += pkmn.totalhp / spikes_div + end + end + if entry_hazard_damage >= pkmn.hp + score -= 50 # pkmn will just faint + elsif entry_hazard_damage > 0 + score -= 50 * entry_hazard_damage / pkmn.hp + end + # TODO: Toxic Spikes. + # TODO: Sticky Web. + # Predict effectiveness of foe's last used move against pkmn + each_foe_battler(@user.side) do |b, i| + next if !b.battler.lastMoveUsed + move_data = GameData::Move.get(b.battler.lastMoveUsed) + next if move_data.status? + move_type = move_data.type + eff = Effectiveness.calculate(move_type, *pkmn_types) + score -= move_data.power * eff / 5 + end + # Add power * effectiveness / 10 of all moves to score + pkmn.moves.each do |m| + next if m.power == 0 || (m.pp == 0 && m.total_pp > 0) + @battle.battlers[idxBattler].allOpposing.each do |b| + bTypes = b.pbTypes(true) + score += m.power * Effectiveness.calculate(m.type, *bTypes) / 10 + end + end + # Prefer if user is about to faint from Perish Song + score += 10 if @user.effects[PBEffects::PerishSong] == 1 + return score end end + +#=============================================================================== +# If Pokémon is within 5 levels of the foe, and foe's last move was +# super-effective and powerful. +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:high_damage_from_foe, + proc { |battler, ai, battle| + if battler.turnCount > 0 && battler.hp < battler.totalhp / 2 && ai.trainer.high_skill? + target_battler = battler.battler.pbDirectOpposing(true) + foe = ai.battlers[target_battler.index] + if !foe.fainted? && foe.battler.lastMoveUsed && (foe.level - battler.level).abs <= 5 + move_data = GameData::Move.get(foe.battler.lastMoveUsed) + eff = battler.effectiveness_of_type_against_battler(move_data.type, foe) + if Effectiveness.super_effective?(eff) && move_data.power > 70 + switch_chance = (move_data.power > 95) ? 40 : 20 + if ai.pbAIRandom(100) < switch_chance + PBDebug.log_ai("#{battler.name} wants to switch because a foe can do a lot of damage to it") + next true + end + end + end + end + next false + } +) + +#=============================================================================== +# Pokémon can't do anything (must have been in battle for at least 5 rounds). +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:battler_is_useless, + proc { |battler, ai, battle| + if !battle.pbCanChooseAnyMove?(battler.index) && battler.turnCount >= 5 + PBDebug.log_ai("#{battler.name} wants to switch because it can't do anything") + next true + end + next false + } +) + +#=============================================================================== +# Pokémon is Perish Songed and has Baton Pass. +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:perish_song, + proc { |battler, ai, battle| + if battler.effects[PBEffects::PerishSong] == 1 + PBDebug.log_ai("#{battler.name} wants to switch because it is about to faint from Perish Song") + next true + end + next false + } +) + +#=============================================================================== +# Pokémon will faint because of bad poisoning at the end of this round, but +# would survive at least one more round if it were regular poisoning instead. +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:reduce_toxic_to_regular_poisoning, + proc { |battler, ai, battle| + if battler.status == :POISON && battler.statusCount > 0 && ai.trainer.high_skill? + next false if battler.has_active_ability?(:POISONHEAL) + next false if !battler.battler.takesIndirectDamage? + poison_damage = battler.totalhp / 8 + next_toxic_damage = battler.totalhp * (battler.effects[PBEffects::Toxic] + 1) / 16 + if battler.hp <= next_toxic_damage && battler.hp > poison_damage + if ai.pbAIRandom(100) < 80 + PBDebug.log_ai("#{battler.name} wants to switch to reduce toxic to regular poisoning") + next true + end + end + end + next false + } +) + +#=============================================================================== +# Pokémon is Encored into an unfavourable move. +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:bad_encored_move, + proc { |battler, ai, battle| + if battler.effects[PBEffects::Encore] > 0 && ai.trainer.medium_skill? + idxEncoredMove = battler.battler.pbEncoredMoveIndex + if idxEncoredMove >= 0 + ai.set_up_move_check(battler.moves[idxEncoredMove]) + scoreSum = 0 + scoreCount = 0 + battler.battler.allOpposing.each do |b| + scoreSum += ai.pbGetMoveScore([b]) + scoreCount += 1 + end + if scoreCount > 0 && scoreSum / scoreCount <= 20 + if ai.pbAIRandom(100) < 80 + PBDebug.log_ai("#{battler.name} wants to switch because of encoring a bad move") + next true + else + next false + end + end + end + end + next false + } +) + +#=============================================================================== +# Sudden Death rule - I'm not sure what this means. +# TODO: Review switch deciding. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:sudden_death, + proc { |battler, ai, battle| + if battle.rules["suddendeath"] && battler.turnCount > 0 + if battler.hp <= battler.totalhp / 4 && ai.pbAIRandom(100) < 30 + PBDebug.log_ai("#{battler.name} wants to switch because of the sudden death rule") + next true + elsif battler.hp <= battler.totalhp / 2 && ai.pbAIRandom(100) < 80 + PBDebug.log_ai("#{battler.name} wants to switch because of the sudden death rule") + next true + end + end + next false + } +) diff --git a/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb b/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb index 856aa96ec..c643d5a1a 100644 --- a/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb +++ b/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb @@ -313,7 +313,7 @@ class Battle::AI end if badMoves PBDebug.log_ai("#{@user.name} wants to switch due to terrible moves") - return if pbEnemyShouldWithdrawEx?(true) + return if pbChooseToSwitchOut(true) PBDebug.log_ai("#{@user.name} won't switch after all") end end diff --git a/Data/Scripts/011_Battle/005_AI/070_AI_MoveHandlers_GeneralModifiers.rb b/Data/Scripts/011_Battle/005_AI/070_AI_MoveHandlers_GeneralModifiers.rb index 621d10cfd..302a28238 100644 --- a/Data/Scripts/011_Battle/005_AI/070_AI_MoveHandlers_GeneralModifiers.rb +++ b/Data/Scripts/011_Battle/005_AI/070_AI_MoveHandlers_GeneralModifiers.rb @@ -173,7 +173,7 @@ Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:flinching_effects, #=============================================================================== # TODO: Don't prefer contact move if making contact with the target could # trigger an effect that's bad for the user (Static, etc.). -# => Also check if target has previously used Spiky Shield.King's Shield/ +# => Also check if target has previously used Spiky Shield/King's Shield/ # Baneful Bunker, and don't prefer move if so #=============================================================================== diff --git a/Data/Scripts/011_Battle/005_AI/101_AITrainer.rb b/Data/Scripts/011_Battle/005_AI/101_AITrainer.rb index 561ba768d..b5f71d771 100644 --- a/Data/Scripts/011_Battle/005_AI/101_AITrainer.rb +++ b/Data/Scripts/011_Battle/005_AI/101_AITrainer.rb @@ -14,6 +14,7 @@ # ScoreMoves # PreferMultiTargetMoves # ReserveLastPokemon (don't switch it in if possible) +# UsePokemonInOrder (uses earliest-listed Pokémon possible) #=============================================================================== class Battle::AI::AITrainer attr_reader :side, :trainer_index @@ -37,7 +38,9 @@ class Battle::AI::AITrainer @skill_flags.push("ScoreMoves") @skill_flags.push("PreferMultiTargetMoves") end - if @skill >= 100 + if !medium_skill? + @skill_flags.push("UsePokemonInOrder") + elsif best_skill? # TODO: Also have flag "DontReserveLastPokemon" which negates this. @skill_flags.push("ReserveLastPokemon") end @@ -47,17 +50,14 @@ class Battle::AI::AITrainer return @skill_flags.include?(flag) end - # TODO: This will eventually be replaced by something else, maybe skill flags. def medium_skill? return @skill >= 32 end - # TODO: This will eventually be replaced by something else, maybe skill flags. def high_skill? return @skill >= 48 end - # TODO: This will eventually be replaced by something else, maybe skill flags. def best_skill? return @skill >= 100 end diff --git a/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb b/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb index 73a96e1f4..32467868f 100644 --- a/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb +++ b/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb @@ -35,6 +35,7 @@ class Battle::AI::AIBattler def stages; return @battler.stages; end def statStageAtMax?(stat); return @battler.statStageAtMax?(stat); end def statStageAtMin?(stat); return @battler.statStageAtMin?(stat); end + def moves; return @battler.moves; end def wild? return @ai.battle.wildBattle? && opposes? @@ -300,7 +301,7 @@ class Battle::AI::AIBattler def can_switch_lax? return false if wild? @ai.battle.eachInTeamFromBattlerIndex(@index) do |pkmn, i| - return true if @ai.battle.pbCanSwitchLax?(@index, i) + return true if @ai.battle.pbCanSwitchIn?(@index, i) end return false end diff --git a/Data/Scripts/011_Battle/005b_AI move function codes/054_AI_MoveHandlers_MoveAttributes.rb b/Data/Scripts/011_Battle/005b_AI move function codes/054_AI_MoveHandlers_MoveAttributes.rb index 0f898cc13..ca02250f6 100644 --- a/Data/Scripts/011_Battle/005b_AI move function codes/054_AI_MoveHandlers_MoveAttributes.rb +++ b/Data/Scripts/011_Battle/005b_AI move function codes/054_AI_MoveHandlers_MoveAttributes.rb @@ -431,6 +431,8 @@ Battle::AI::Handlers::MoveEffectScore.add("EnsureNextCriticalHit", next Battle::AI::MOVE_USELESS_SCORE if user.effects[PBEffects::LaserFocus] > 0 # Useless if the user's critical hit stage ensures critical hits already, or # critical hits are impossible (e.g. via Lucky Chant) + # TODO: Critical hit rate is calculated using a target, but this move + # doesn't have a target. An error is shown when trying it. crit_stage = move.rough_critical_hit_stage if crit_stage < 0 || crit_stage >= Battle::Move::CRITICAL_HIT_RATIOS.length || @@ -491,7 +493,7 @@ Battle::AI::Handlers::MoveEffectScore.add("StartPreventCriticalHitsAgainstUserSi next if crit_stage < 0 end crit_stage += b.effects[PBEffects::FocusEnergy] - crit_stage += 1 if m.check_for_move { |m| m.highCriticalRate? } + crit_stage += 1 if b.check_for_move { |m| m.highCriticalRate? } crit_stage = [crit_stage, Battle::Move::CRITICAL_HIT_RATIOS.length - 1].min crit_stage = 3 if crit_stage < 3 && m.check_for_move { |m| m.pbCritialOverride(b.battler, user.battler) > 0 } # TODO: Change the score depending on how much of an effect a critical hit @@ -750,7 +752,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUser", end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -801,7 +803,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserBanefulBunker", end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -852,7 +854,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromDamagingMovesKingsShie end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -902,7 +904,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromDamagingMovesObstruct" end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -951,7 +953,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromTargetingMovesSpikyShi end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -1086,7 +1088,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromPriorityMoves", end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) @@ -1105,7 +1107,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromMultiTargetDamagin next Battle::AI::MOVE_USELESS_SCORE if user.effects[PBEffects::ProtectRate] >= 4 # Score changes for each foe useless = true - ai.each_battler(user.side) do |b, i| + ai.each_battler do |b, i| next if b.index == user.index || !b.can_attack? next if !b.check_for_move { |m| (Settings::MECHANICS_GENERATION >= 7 || move.damagingMove?) && m.pbTarget(b.battler).num_targets > 1 } @@ -1132,7 +1134,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromMultiTargetDamagin end # Don't prefer if the user used a protection move last turn, making this one # less likely to work - score -= (user.effects[PBEffects::ProtectRate] - 1) * (Settings::MECHANICS_GENERATION >= 6) ? 12 : 8 + score -= (user.effects[PBEffects::ProtectRate] - 1) * ((Settings::MECHANICS_GENERATION >= 6) ? 12 : 8) next score } ) diff --git a/Data/Scripts/011_Battle/005b_AI move function codes/055_AI_MoveHandlers_MultiHit.rb b/Data/Scripts/011_Battle/005b_AI move function codes/055_AI_MoveHandlers_MultiHit.rb index 364c65fbf..bf362df7f 100644 --- a/Data/Scripts/011_Battle/005b_AI move function codes/055_AI_MoveHandlers_MultiHit.rb +++ b/Data/Scripts/011_Battle/005b_AI move function codes/055_AI_MoveHandlers_MultiHit.rb @@ -366,7 +366,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score, move, user, target, ai, battle) # Score for being semi-invulnerable underground - user.each_foe_battler(user.side) do |b, i| + ai.each_foe_battler(user.side) do |b, i| if b.check_for_move { |m| m.hitsDiggingTargets? } score -= 8 else @@ -386,7 +386,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score, move, user, target, ai, battle) # Score for being semi-invulnerable underwater - user.each_foe_battler(user.side) do |b, i| + ai.each_foe_battler(user.side) do |b, i| if b.check_for_move { |m| m.hitsDivingTargets? } score -= 8 else @@ -406,7 +406,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score, move, user, target, ai, battle) # Score for being semi-invulnerable in the sky - user.each_foe_battler(user.side) do |b, i| + ai.each_foe_battler(user.side) do |b, i| if b.check_for_move { |m| m.hitsFlyingTargets? } score -= 8 else diff --git a/Data/Scripts/011_Battle/005b_AI move function codes/059_AI_MoveHandlers_SwitchingActing.rb b/Data/Scripts/011_Battle/005b_AI move function codes/059_AI_MoveHandlers_SwitchingActing.rb index 4c9f666e2..814d664b2 100644 --- a/Data/Scripts/011_Battle/005b_AI move function codes/059_AI_MoveHandlers_SwitchingActing.rb +++ b/Data/Scripts/011_Battle/005b_AI move function codes/059_AI_MoveHandlers_SwitchingActing.rb @@ -116,7 +116,7 @@ Battle::AI::Handlers::MoveFailureAgainstTargetCheck.add("SwitchOutTargetStatusMo if battle.trainerBattle? will_fail = true battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i| - next if !battle.pbCanSwitchLax?(target.index, i) + next if !battle.pbCanSwitchIn?(target.index, i) will_fail = false break end @@ -315,8 +315,8 @@ Battle::AI::Handlers::MoveEffectScore.add("UsedAfterAllyRoundWithDoublePower", #=============================================================================== # #=============================================================================== -Battle::AI::Handlers::MoveEffectScore.add("TargetActsNext", - proc { |score, move, user, ai, battle| +Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TargetActsNext", + proc { |score, move, user, target, ai, battle| # Useless if the target is a foe next Battle::AI::MOVE_USELESS_SCORE if target.opposes?(user) # Compare the speeds of all battlers diff --git a/Data/Scripts/011_Battle/006_Other battle code/008_Battle_AbilityEffects.rb b/Data/Scripts/011_Battle/006_Other battle code/008_Battle_AbilityEffects.rb index 901b42ff3..98c1bd3fb 100644 --- a/Data/Scripts/011_Battle/006_Other battle code/008_Battle_AbilityEffects.rb +++ b/Data/Scripts/011_Battle/006_Other battle code/008_Battle_AbilityEffects.rb @@ -381,7 +381,7 @@ Battle::AbilityEffects::OnHPDroppedBelowHalf.add(:EMERGENCYEXIT, end # In trainer battles next false if battle.pbAllFainted?(battler.idxOpposingSide) - next false if !battle.pbCanSwitch?(battler.index) # Battler can't switch out + next false if !battle.pbCanSwitchOut?(battler.index) # Battler can't switch out next false if !battle.pbCanChooseNonActive?(battler.index) # No Pokémon can switch in battle.pbShowAbilitySplash(battler, true) battle.pbHideAbilitySplash(battler) diff --git a/Data/Scripts/011_Battle/006_Other battle code/009_Battle_ItemEffects.rb b/Data/Scripts/011_Battle/006_Other battle code/009_Battle_ItemEffects.rb index 2761d3652..7fd08739f 100644 --- a/Data/Scripts/011_Battle/006_Other battle code/009_Battle_ItemEffects.rb +++ b/Data/Scripts/011_Battle/006_Other battle code/009_Battle_ItemEffects.rb @@ -451,7 +451,7 @@ Battle::ItemEffects::OnStatLoss.add(:EJECTPACK, battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSkyTargetCannotAct") # Sky Drop next false if battle.pbAllFainted?(battler.idxOpposingSide) next false if battler.wild? # Wild Pokémon can't eject - next false if !battle.pbCanSwitch?(battler.index) # Battler can't switch out + next false if !battle.pbCanSwitchOut?(battler.index) # Battler can't switch out next false if !battle.pbCanChooseNonActive?(battler.index) # No Pokémon can switch in battle.pbCommonAnimation("UseItem", battler) battle.pbDisplay(_INTL("{1} is switched out by the {2}!", battler.pbThis, battler.itemName)) diff --git a/Data/Scripts/011_Battle/007_Other battle types/003_BattlePalaceBattle.rb b/Data/Scripts/011_Battle/007_Other battle types/003_BattlePalaceBattle.rb index e542addb6..1bbceb54a 100644 --- a/Data/Scripts/011_Battle/007_Other battle types/003_BattlePalaceBattle.rb +++ b/Data/Scripts/011_Battle/007_Other battle types/003_BattlePalaceBattle.rb @@ -177,12 +177,12 @@ class Battle::AI @justswitched = [false, false, false, false] end - unless method_defined?(:_battlePalace_pbEnemyShouldWithdraw?) - alias _battlePalace_pbEnemyShouldWithdraw? pbEnemyShouldWithdraw? + unless method_defined?(:_battlePalace_pbChooseToSwitchOut) + alias _battlePalace_pbChooseToSwitchOut pbChooseToSwitchOut end - def pbEnemyShouldWithdraw? - return _battlePalace_pbEnemyShouldWithdraw? if !@battlePalace + def pbChooseToSwitchOut(force_switch = false) + return _battlePalace_pbChooseToSwitchOut(force_switch) if !@battlePalace thispkmn = @user idxBattler = @user.index shouldswitch = false diff --git a/Data/Scripts/011_Battle/007_Other battle types/004_BattleArenaBattle.rb b/Data/Scripts/011_Battle/007_Other battle types/004_BattleArenaBattle.rb index cb2dc991f..d5b24d124 100644 --- a/Data/Scripts/011_Battle/007_Other battle types/004_BattleArenaBattle.rb +++ b/Data/Scripts/011_Battle/007_Other battle types/004_BattleArenaBattle.rb @@ -50,7 +50,7 @@ class BattleArenaBattle < Battle @battleAI.battleArena = true end - def pbCanSwitchLax?(idxBattler, _idxParty, partyScene = nil) + def pbCanSwitchIn?(idxBattler, _idxParty, partyScene = nil) partyScene&.pbDisplay(_INTL("{1} can't be switched out!", @battlers[idxBattler].pbThis)) return false end @@ -207,12 +207,12 @@ end class Battle::AI attr_accessor :battleArena - unless method_defined?(:_battleArena_pbEnemyShouldWithdraw?) - alias _battleArena_pbEnemyShouldWithdraw? pbEnemyShouldWithdraw? + unless method_defined?(:_battleArena_pbChooseToSwitchOut) + alias _battleArena_pbChooseToSwitchOut pbChooseToSwitchOut end - def pbEnemyShouldWithdraw? - return _battleArena_pbEnemyShouldWithdraw? if !@battleArena + def pbChooseToSwitchOut(force_switch = false) + return _battleArena_pbChooseToSwitchOut(force_switch) if !@battleArena return false end end