Refactored AI switching code, added "UsePokemonInOrder" skill flag

This commit is contained in:
Maruno17
2023-02-14 16:40:52 +00:00
parent a8e024eb3e
commit 81d069eef1
16 changed files with 353 additions and 235 deletions

View File

@@ -6,7 +6,7 @@ class Battle
# battle. # battle.
# NOTE: Messages are only shown while in the party screen when choosing a # NOTE: Messages are only shown while in the party screen when choosing a
# command for the next round. # command for the next round.
def pbCanSwitchLax?(idxBattler, idxParty, partyScene = nil) def pbCanSwitchIn?(idxBattler, idxParty, partyScene = nil)
return true if idxParty < 0 return true if idxParty < 0
party = pbParty(idxBattler) party = pbParty(idxBattler)
return false if idxParty >= party.length return false if idxParty >= party.length
@@ -18,8 +18,7 @@ class Battle
if !pbIsOwner?(idxBattler, idxParty) if !pbIsOwner?(idxBattler, idxParty)
if partyScene if partyScene
owner = pbGetOwnerFromPartyIndex(idxBattler, idxParty) owner = pbGetOwnerFromPartyIndex(idxBattler, idxParty)
partyScene.pbDisplay(_INTL("You can't switch {1}'s Pokémon with one of yours!", partyScene.pbDisplay(_INTL("You can't switch {1}'s Pokémon with one of yours!", owner.name))
owner.name))
end end
return false return false
end end
@@ -35,30 +34,15 @@ class Battle
end end
# Check whether the currently active Pokémon (at battler index idxBattler) can # 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). # switch out.
# NOTE: Messages are only shown while in the party screen when choosing a def pbCanSwitchOut?(idxBattler, partyScene = nil)
# 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
battler = @battlers[idxBattler] battler = @battlers[idxBattler]
return true if battler.fainted? return true if battler.fainted?
# Ability/item effects that allow switching no matter what # Ability/item effects that allow switching no matter what
if battler.abilityActive? && if battler.abilityActive? && Battle::AbilityEffects.triggerCertainSwitching(battler.ability, battler, self)
Battle::AbilityEffects.triggerCertainSwitching(battler.ability, battler, self)
return true return true
end end
if battler.itemActive? && if battler.itemActive? && Battle::ItemEffects.triggerCertainSwitching(battler.item, battler, self)
Battle::ItemEffects.triggerCertainSwitching(battler.item, battler, self)
return true return true
end end
# Other certain switching effects # Other certain switching effects
@@ -72,25 +56,42 @@ class Battle
allOtherSideBattlers(idxBattler).each do |b| allOtherSideBattlers(idxBattler).each do |b|
next if !b.abilityActive? next if !b.abilityActive?
if Battle::AbilityEffects.triggerTrappingByTarget(b.ability, battler, b, self) if Battle::AbilityEffects.triggerTrappingByTarget(b.ability, battler, b, self)
partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", b.pbThis, b.abilityName))
b.pbThis, b.abilityName))
return false return false
end end
end end
allOtherSideBattlers(idxBattler).each do |b| allOtherSideBattlers(idxBattler).each do |b|
next if !b.itemActive? next if !b.itemActive?
if Battle::ItemEffects.triggerTrappingByTarget(b.item, battler, b, self) if Battle::ItemEffects.triggerTrappingByTarget(b.item, battler, b, self)
partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", partyScene&.pbDisplay(_INTL("{1}'s {2} prevents switching!", b.pbThis, b.itemName))
b.pbThis, b.itemName))
return false return false
end end
end end
return true return true
end 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) def pbCanChooseNonActive?(idxBattler)
pbParty(idxBattler).each_with_index do |_pkmn, i| pbParty(idxBattler).each_with_index do |_pkmn, i|
return true if pbCanSwitchLax?(idxBattler, i) return true if pbCanSwitchIn?(idxBattler, i)
end end
return false return false
end end
@@ -113,7 +114,7 @@ class Battle
ret = -1 ret = -1
@scene.pbPartyScreen(idxBattler, canCancel) do |idxParty, partyScene| @scene.pbPartyScreen(idxBattler, canCancel) do |idxParty, partyScene|
if checkLaxOnly if checkLaxOnly
next false if !pbCanSwitchLax?(idxBattler, idxParty, partyScene) next false if !pbCanSwitchIn?(idxBattler, idxParty, partyScene)
elsif !pbCanSwitch?(idxBattler, idxParty, partyScene) elsif !pbCanSwitch?(idxBattler, idxParty, partyScene)
next false next false
end end
@@ -130,7 +131,7 @@ class Battle
# things happening (U-turn, Baton Pass, in def pbEORSwitch). # things happening (U-turn, Baton Pass, in def pbEORSwitch).
def pbSwitchInBetween(idxBattler, checkLaxOnly = false, canCancel = false) def pbSwitchInBetween(idxBattler, checkLaxOnly = false, canCancel = false)
return pbPartyScreen(idxBattler, checkLaxOnly, canCancel) if !@controlPlayer && pbOwnedByPlayer?(idxBattler) return pbPartyScreen(idxBattler, checkLaxOnly, canCancel) if !@controlPlayer && pbOwnedByPlayer?(idxBattler)
return @battleAI.pbDefaultChooseNewEnemy(idxBattler, pbParty(idxBattler)) return @battleAI.pbDefaultChooseNewEnemy(idxBattler)
end end
#============================================================================= #=============================================================================
@@ -206,7 +207,7 @@ class Battle
if random if random
choices = [] # Find all Pokémon that can switch in choices = [] # Find all Pokémon that can switch in
eachInTeamFromBattlerIndex(idxBattler) do |_pkmn, i| eachInTeamFromBattlerIndex(idxBattler) do |_pkmn, i|
choices.push(i) if pbCanSwitchLax?(idxBattler, i) choices.push(i) if pbCanSwitchIn?(idxBattler, i)
end end
return -1 if choices.length == 0 return -1 if choices.length == 0
return choices[pbRandom(choices.length)] return choices[pbRandom(choices.length)]

View File

@@ -140,7 +140,7 @@ class Battle
def pbPartyMenu(idxBattler) def pbPartyMenu(idxBattler)
ret = -1 ret = -1
if @debug if @debug
ret = @battleAI.pbDefaultChooseNewEnemy(idxBattler, pbParty(idxBattler)) ret = @battleAI.pbDefaultChooseNewEnemy(idxBattler)
else else
ret = pbPartyScreen(idxBattler, false, true, true) ret = pbPartyScreen(idxBattler, false, true, true)
end end

View File

@@ -182,7 +182,7 @@ class Battle::Move::SwitchOutTargetStatusMove < Battle::Move
if @battle.trainerBattle? if @battle.trainerBattle?
canSwitch = false canSwitch = false
@battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i| @battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i|
next if !@battle.pbCanSwitchLax?(target.index, i) next if !@battle.pbCanSwitchIn?(target.index, i)
canSwitch = true canSwitch = true
break break
end end

View File

@@ -51,14 +51,33 @@ class Battle::AI
# Choose an action. # Choose an action.
def pbDefaultChooseEnemyCommand(idxBattler) def pbDefaultChooseEnemyCommand(idxBattler)
set_up(idxBattler) set_up(idxBattler)
return if pbEnemyShouldWithdraw? ret = false
return if pbEnemyShouldUseItem? PBDebug.logonerr { ret = pbChooseToSwitchOut }
return if @battle.pbAutoFightMenu(idxBattler) 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? @battle.pbRegisterMegaEvolution(idxBattler) if pbEnemyShouldMegaEvolve?
choices = pbGetMoveScores choices = pbGetMoveScores
pbChooseMove(choices) pbChooseMove(choices)
PBDebug.log("") PBDebug.log("")
end 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 end
#=============================================================================== #===============================================================================
@@ -72,12 +91,14 @@ module Battle::AI::Handlers
MoveBasePower = HandlerHash.new MoveBasePower = HandlerHash.new
GeneralMoveScore = HandlerHash.new GeneralMoveScore = HandlerHash.new
GeneralMoveAgainstTargetScore = HandlerHash.new GeneralMoveAgainstTargetScore = HandlerHash.new
# Move type - uses main battle code via rough_type # TODO: Make HandlerHashes for these?
# Move accuracy - uses main battle code via rough_accuracy # Move type - uses main battle code via rough_type
# Move target # Move accuracy - uses main battle code via rough_accuracy
# Move additional effect chance # Move target
# Move unselectable check # Move additional effect chance
# Move failure check # Move unselectable check
# Move failure check
ShouldSwitch = HandlerHash.new
def self.move_will_fail?(function_code, *args) def self.move_will_fail?(function_code, *args)
return MoveFailureCheck.trigger(function_code, *args) || false return MoveFailureCheck.trigger(function_code, *args) || false
@@ -117,4 +138,13 @@ module Battle::AI::Handlers
end end
return score return score
end 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 end

View File

@@ -2,184 +2,268 @@ class Battle::AI
#============================================================================= #=============================================================================
# Decide whether the opponent should switch Pokémon # Decide whether the opponent should switch Pokémon
#============================================================================= #=============================================================================
def pbEnemyShouldWithdraw? # Called by the AI's def pbDefaultChooseEnemyCommand, and by def pbChooseMove
ret = false # if the only moves known are bad ones (the latter forces a switch). Also
PBDebug.logonerr { ret = pbEnemyShouldWithdrawEx?(false) } # aliased by the Battle Palace and Battle Arena.
return ret def pbChooseToSwitchOut(force_switch = false)
end
def pbEnemyShouldWithdrawEx?(forceSwitch)
return false if @user.wild? return false if @user.wild?
shouldSwitch = forceSwitch return false if !@battle.pbCanSwitchOut?(@user.index)
battler = @user.battler # Don't switch if there is a single foe and it is resting after Hyper Beam
batonPass = -1 # or will Truant (i.e. free turn)
foe = nil if @trainer.high_skill?
moveType = nil foe_can_act = false
# If Pokémon is within 6 levels of the foe, and foe's last move was each_foe_battler(@user.side) do |b|
# super-effective and powerful next if !b.can_attack?
if !shouldSwitch && battler.turnCount > 0 && @trainer.high_skill? foe_can_act = true
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
break break
end end
return false if !foe_can_act
end end
# Pokémon will faint because of bad poisoning at the end of this round, but # Various calculations to decide whether to switch
# would survive at least one more round if it were regular poisoning instead if force_switch
if !shouldSwitch && battler.status == :POISON && battler.statusCount > 0 && @trainer.high_skill? PBDebug.log_ai("#{@user.name} is being forced to switch out")
toxicHP = battler.totalhp / 16 else
nextToxicHP = toxicHP * (battler.effects[PBEffects::Toxic] + 1) should_switch = Battle::AI::Handlers.should_switch?(@user, self, @battle)
if battler.hp <= nextToxicHP && battler.hp > toxicHP * 2 return false if !should_switch
shouldSwitch = true if pbAIRandom(100) < 80
end
end end
# Pokémon is Encored into an unfavourable move # Want to switch; find the best replacement Pokémon
if !shouldSwitch && battler.effects[PBEffects::Encore] > 0 && @trainer.medium_skill? idxParty = choose_best_replacement_pokemon(@user.index, force_switch)
idxEncoredMove = battler.pbEncoredMoveIndex if idxParty < 0 # No good replacement Pokémon found
if idxEncoredMove >= 0 PBDebug.log(" => no good replacement Pokémon, will not switch after all")
scoreSum = 0 return false
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
end end
# If there is a single foe and it is resting after Hyper Beam or is # Prefer using Baton Pass instead of switching
# Truanting (i.e. free turn) baton_pass = -1
if shouldSwitch && @battle.pbSideSize(battler.index + 1) == 1 && @user.battler.eachMoveWithIndex do |m, i|
!battler.pbDirectOpposing.fainted? && @trainer.high_skill? next if m.function != "SwitchOutUserPassOnEffects" # Baton Pass
opp = battler.pbDirectOpposing next if !@battle.pbCanChooseMove?(@user.index, i, false)
if (opp.effects[PBEffects::HyperBeam] > 0 || baton_pass = i
(opp.hasActiveAbility?(:TRUANT) && opp.effects[PBEffects::Truant])) break
shouldSwitch = false if pbAIRandom(100) < 80
end
end end
# Sudden Death rule - I'm not sure what this means if baton_pass >= 0 && @battle.pbRegisterMove(@user.index, baton_pass, false)
if !shouldSwitch && @battle.rules["suddendeath"] && battler.turnCount > 0 PBDebug.log(" => will use Baton Pass to switch out")
if battler.hp <= battler.totalhp / 4 && pbAIRandom(100) < 30 return true
shouldSwitch = true elsif @battle.pbRegisterSwitch(@user.index, idxParty)
elsif battler.hp <= battler.totalhp / 2 && pbAIRandom(100) < 80 PBDebug.log(" => will switch with #{@battle.pbParty(@user.index)[idxParty].name}")
shouldSwitch = true return 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
end end
return false return false
end end
# Choose a replacement Pokémon (called directly from @battle, not part of #=============================================================================
# action choosing).
def pbDefaultChooseNewEnemy(idxBattler, party) def choose_best_replacement_pokemon(idxBattler, mandatory = false)
set_up(idxBattler) # Get all possible replacement Pokémon
enemies = [] party = @battle.pbParty(idxBattler)
idxPartyStart, idxPartyEnd = @battle.pbTeamIndexRangeFromBattlerIndex(idxBattler) idxPartyStart, idxPartyEnd = @battle.pbTeamIndexRangeFromBattlerIndex(idxBattler)
party.each_with_index do |_p, i| reserves = []
if @trainer.has_skill_flag?("ReserveLastPokemon") party.each_with_index do |_pkmn, i|
next if i == idxPartyEnd - 1 && enemies.length > 0 # Ignore ace if possible 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 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 end
return -1 if enemies.length == 0 return -1 if reserves.length == 0
return pbChooseBestNewEnemy(idxBattler, party, enemies) # 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 end
def pbChooseBestNewEnemy(idxBattler, party, enemies) def rate_replacement_pokemon(idxBattler, pkmn, score)
return -1 if !enemies || enemies.length == 0 battler_side = @battle.sides[idxBattler & 1]
best = -1 pkmn_types = pkmn.types
bestSum = 0 entry_hazard_damage = 0
enemies.each do |i| # Stealth Rock
pkmn = party[i] if battler_side.effects[PBEffects::StealthRock] && !pkmn.hasAbility?(:MAGICGUARD) &&
sum = 0 GameData::Type.exists?(:ROCK) && !pkmn.hasItem?(:HEAVYDUTYBOOTS)
pkmn.moves.each do |m| eff = Effectiveness.calculate(:ROCK, *pkmn_types)
next if m.power == 0 if !Effectiveness.ineffective?(eff)
@battle.battlers[idxBattler].allOpposing.each do |b| entry_hazard_damage += pkmn.totalhp * eff / 8
bTypes = b.pbTypes(true)
sum += Effectiveness.calculate(m.type, *bTypes)
end
end
if best == -1 || sum > bestSum
best = i
bestSum = sum
end end
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
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
}
)

View File

@@ -313,7 +313,7 @@ class Battle::AI
end end
if badMoves if badMoves
PBDebug.log_ai("#{@user.name} wants to switch due to terrible moves") 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") PBDebug.log_ai("#{@user.name} won't switch after all")
end end
end end

View File

@@ -173,7 +173,7 @@ Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:flinching_effects,
#=============================================================================== #===============================================================================
# TODO: Don't prefer contact move if making contact with the target could # TODO: Don't prefer contact move if making contact with the target could
# trigger an effect that's bad for the user (Static, etc.). # 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 # Baneful Bunker, and don't prefer move if so
#=============================================================================== #===============================================================================

View File

@@ -14,6 +14,7 @@
# ScoreMoves # ScoreMoves
# PreferMultiTargetMoves # PreferMultiTargetMoves
# ReserveLastPokemon (don't switch it in if possible) # ReserveLastPokemon (don't switch it in if possible)
# UsePokemonInOrder (uses earliest-listed Pokémon possible)
#=============================================================================== #===============================================================================
class Battle::AI::AITrainer class Battle::AI::AITrainer
attr_reader :side, :trainer_index attr_reader :side, :trainer_index
@@ -37,7 +38,9 @@ class Battle::AI::AITrainer
@skill_flags.push("ScoreMoves") @skill_flags.push("ScoreMoves")
@skill_flags.push("PreferMultiTargetMoves") @skill_flags.push("PreferMultiTargetMoves")
end end
if @skill >= 100 if !medium_skill?
@skill_flags.push("UsePokemonInOrder")
elsif best_skill?
# TODO: Also have flag "DontReserveLastPokemon" which negates this. # TODO: Also have flag "DontReserveLastPokemon" which negates this.
@skill_flags.push("ReserveLastPokemon") @skill_flags.push("ReserveLastPokemon")
end end
@@ -47,17 +50,14 @@ class Battle::AI::AITrainer
return @skill_flags.include?(flag) return @skill_flags.include?(flag)
end end
# TODO: This will eventually be replaced by something else, maybe skill flags.
def medium_skill? def medium_skill?
return @skill >= 32 return @skill >= 32
end end
# TODO: This will eventually be replaced by something else, maybe skill flags.
def high_skill? def high_skill?
return @skill >= 48 return @skill >= 48
end end
# TODO: This will eventually be replaced by something else, maybe skill flags.
def best_skill? def best_skill?
return @skill >= 100 return @skill >= 100
end end

View File

@@ -35,6 +35,7 @@ class Battle::AI::AIBattler
def stages; return @battler.stages; end def stages; return @battler.stages; end
def statStageAtMax?(stat); return @battler.statStageAtMax?(stat); end def statStageAtMax?(stat); return @battler.statStageAtMax?(stat); end
def statStageAtMin?(stat); return @battler.statStageAtMin?(stat); end def statStageAtMin?(stat); return @battler.statStageAtMin?(stat); end
def moves; return @battler.moves; end
def wild? def wild?
return @ai.battle.wildBattle? && opposes? return @ai.battle.wildBattle? && opposes?
@@ -300,7 +301,7 @@ class Battle::AI::AIBattler
def can_switch_lax? def can_switch_lax?
return false if wild? return false if wild?
@ai.battle.eachInTeamFromBattlerIndex(@index) do |pkmn, i| @ai.battle.eachInTeamFromBattlerIndex(@index) do |pkmn, i|
return true if @ai.battle.pbCanSwitchLax?(@index, i) return true if @ai.battle.pbCanSwitchIn?(@index, i)
end end
return false return false
end end

View File

@@ -431,6 +431,8 @@ Battle::AI::Handlers::MoveEffectScore.add("EnsureNextCriticalHit",
next Battle::AI::MOVE_USELESS_SCORE if user.effects[PBEffects::LaserFocus] > 0 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 # Useless if the user's critical hit stage ensures critical hits already, or
# critical hits are impossible (e.g. via Lucky Chant) # 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 crit_stage = move.rough_critical_hit_stage
if crit_stage < 0 || if crit_stage < 0 ||
crit_stage >= Battle::Move::CRITICAL_HIT_RATIOS.length || crit_stage >= Battle::Move::CRITICAL_HIT_RATIOS.length ||
@@ -491,7 +493,7 @@ Battle::AI::Handlers::MoveEffectScore.add("StartPreventCriticalHitsAgainstUserSi
next if crit_stage < 0 next if crit_stage < 0
end end
crit_stage += b.effects[PBEffects::FocusEnergy] 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 = [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 } 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 # 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 end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -801,7 +803,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserBanefulBunker",
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -852,7 +854,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromDamagingMovesKingsShie
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -902,7 +904,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromDamagingMovesObstruct"
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -951,7 +953,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserFromTargetingMovesSpikyShi
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -1086,7 +1088,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromPriorityMoves",
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )
@@ -1105,7 +1107,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromMultiTargetDamagin
next Battle::AI::MOVE_USELESS_SCORE if user.effects[PBEffects::ProtectRate] >= 4 next Battle::AI::MOVE_USELESS_SCORE if user.effects[PBEffects::ProtectRate] >= 4
# Score changes for each foe # Score changes for each foe
useless = true 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.index == user.index || !b.can_attack?
next if !b.check_for_move { |m| (Settings::MECHANICS_GENERATION >= 7 || move.damagingMove?) && next if !b.check_for_move { |m| (Settings::MECHANICS_GENERATION >= 7 || move.damagingMove?) &&
m.pbTarget(b.battler).num_targets > 1 } m.pbTarget(b.battler).num_targets > 1 }
@@ -1132,7 +1134,7 @@ Battle::AI::Handlers::MoveEffectScore.add("ProtectUserSideFromMultiTargetDamagin
end end
# Don't prefer if the user used a protection move last turn, making this one # Don't prefer if the user used a protection move last turn, making this one
# less likely to work # 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 next score
} }
) )

View File

@@ -366,7 +366,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl
score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack",
score, move, user, target, ai, battle) score, move, user, target, ai, battle)
# Score for being semi-invulnerable underground # 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? } if b.check_for_move { |m| m.hitsDiggingTargets? }
score -= 8 score -= 8
else else
@@ -386,7 +386,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl
score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack",
score, move, user, target, ai, battle) score, move, user, target, ai, battle)
# Score for being semi-invulnerable underwater # 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? } if b.check_for_move { |m| m.hitsDivingTargets? }
score -= 8 score -= 8
else else
@@ -406,7 +406,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TwoTurnAttackInvulnerabl
score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack", score = Battle::AI::Handlers.apply_move_effect_against_target_score("TwoTurnAttack",
score, move, user, target, ai, battle) score, move, user, target, ai, battle)
# Score for being semi-invulnerable in the sky # 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? } if b.check_for_move { |m| m.hitsFlyingTargets? }
score -= 8 score -= 8
else else

View File

@@ -116,7 +116,7 @@ Battle::AI::Handlers::MoveFailureAgainstTargetCheck.add("SwitchOutTargetStatusMo
if battle.trainerBattle? if battle.trainerBattle?
will_fail = true will_fail = true
battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i| battle.eachInTeamFromBattlerIndex(target.index) do |_pkmn, i|
next if !battle.pbCanSwitchLax?(target.index, i) next if !battle.pbCanSwitchIn?(target.index, i)
will_fail = false will_fail = false
break break
end end
@@ -315,8 +315,8 @@ Battle::AI::Handlers::MoveEffectScore.add("UsedAfterAllyRoundWithDoublePower",
#=============================================================================== #===============================================================================
# #
#=============================================================================== #===============================================================================
Battle::AI::Handlers::MoveEffectScore.add("TargetActsNext", Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("TargetActsNext",
proc { |score, move, user, ai, battle| proc { |score, move, user, target, ai, battle|
# Useless if the target is a foe # Useless if the target is a foe
next Battle::AI::MOVE_USELESS_SCORE if target.opposes?(user) next Battle::AI::MOVE_USELESS_SCORE if target.opposes?(user)
# Compare the speeds of all battlers # Compare the speeds of all battlers

View File

@@ -381,7 +381,7 @@ Battle::AbilityEffects::OnHPDroppedBelowHalf.add(:EMERGENCYEXIT,
end end
# In trainer battles # In trainer battles
next false if battle.pbAllFainted?(battler.idxOpposingSide) 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 next false if !battle.pbCanChooseNonActive?(battler.index) # No Pokémon can switch in
battle.pbShowAbilitySplash(battler, true) battle.pbShowAbilitySplash(battler, true)
battle.pbHideAbilitySplash(battler) battle.pbHideAbilitySplash(battler)

View File

@@ -451,7 +451,7 @@ Battle::ItemEffects::OnStatLoss.add(:EJECTPACK,
battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSkyTargetCannotAct") # Sky Drop battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSkyTargetCannotAct") # Sky Drop
next false if battle.pbAllFainted?(battler.idxOpposingSide) next false if battle.pbAllFainted?(battler.idxOpposingSide)
next false if battler.wild? # Wild Pokémon can't eject 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 next false if !battle.pbCanChooseNonActive?(battler.index) # No Pokémon can switch in
battle.pbCommonAnimation("UseItem", battler) battle.pbCommonAnimation("UseItem", battler)
battle.pbDisplay(_INTL("{1} is switched out by the {2}!", battler.pbThis, battler.itemName)) battle.pbDisplay(_INTL("{1} is switched out by the {2}!", battler.pbThis, battler.itemName))

View File

@@ -177,12 +177,12 @@ class Battle::AI
@justswitched = [false, false, false, false] @justswitched = [false, false, false, false]
end end
unless method_defined?(:_battlePalace_pbEnemyShouldWithdraw?) unless method_defined?(:_battlePalace_pbChooseToSwitchOut)
alias _battlePalace_pbEnemyShouldWithdraw? pbEnemyShouldWithdraw? alias _battlePalace_pbChooseToSwitchOut pbChooseToSwitchOut
end end
def pbEnemyShouldWithdraw? def pbChooseToSwitchOut(force_switch = false)
return _battlePalace_pbEnemyShouldWithdraw? if !@battlePalace return _battlePalace_pbChooseToSwitchOut(force_switch) if !@battlePalace
thispkmn = @user thispkmn = @user
idxBattler = @user.index idxBattler = @user.index
shouldswitch = false shouldswitch = false

View File

@@ -50,7 +50,7 @@ class BattleArenaBattle < Battle
@battleAI.battleArena = true @battleAI.battleArena = true
end 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)) partyScene&.pbDisplay(_INTL("{1} can't be switched out!", @battlers[idxBattler].pbThis))
return false return false
end end
@@ -207,12 +207,12 @@ end
class Battle::AI class Battle::AI
attr_accessor :battleArena attr_accessor :battleArena
unless method_defined?(:_battleArena_pbEnemyShouldWithdraw?) unless method_defined?(:_battleArena_pbChooseToSwitchOut)
alias _battleArena_pbEnemyShouldWithdraw? pbEnemyShouldWithdraw? alias _battleArena_pbChooseToSwitchOut pbChooseToSwitchOut
end end
def pbEnemyShouldWithdraw? def pbChooseToSwitchOut(force_switch = false)
return _battleArena_pbEnemyShouldWithdraw? if !@battleArena return _battleArena_pbChooseToSwitchOut(force_switch) if !@battleArena
return false return false
end end
end end