mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-08 13:44:59 +00:00
Refactored AI switching code, added "UsePokemonInOrder" skill flag
This commit is contained in:
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user