Files
infinitefusion-e18/AI references/Marin's AI/AI_BattlerProjection.rb
2022-08-10 18:38:20 +01:00

1316 lines
48 KiB
Ruby

# TODO:
# Remove Minimize double damage from base damage calculator
# as it is already factored into tramplesMinimize? in the score handlers
=begin
Console.setup_console
Events.onTrainerPartyLoad += proc do |sender, e|
if e[0] # Trainer data should exist to be loaded, but may not exist somehow
trainer = e[0][0] # A PokeBattle_Trainer object of the loaded trainer
items = e[0][1] # An array of the trainer's items they can use
e[0][2] = []
p1 = pbGenPoke(:RAICHU, 55, trainer)
p1.moves = [
# PBMove.new(getConst(PBMoves,:SWORDSDANCE)),
PBMove.new(getConst(PBMoves,:CHARGEBEAM)),
PBMove.new(getConst(PBMoves,:CRUNCH)),
PBMove.new(getConst(PBMoves,:DEFENSECURL)),
PBMove.new(getConst(PBMoves,:SLASH)),
]
p1.abilityflag = 1
# p1.item = getConst(PBItems,:POWERHERB)
# p1.status = PBStatuses::FROZEN
# p1.hp = p.totalhp.to_f / 10 * 3
p1.hp = 1
e[0][2] << p1
# p2 = pbGenPoke(:GOLEM, 55, trainer)
# p2.moves = p1.moves.map { |m| m.clone }
# p2.moves = [
# PBMove.new(getConst(PBMoves,:TACKLE)),
# PBMove.new(getConst(PBMoves,:HEALPULSE))
# ]
# e[0][2] << p2
e[0][2] << pbGenPoke(:LEAVANNY, 50, trainer)
# e[0][2] << pbGenPoke(:MISMAGIUS, 50, trainer)
# e[0][2] << pbGenPoke(:WALREIN, 53, trainer)
# e[0][2] << pbGenPoke(:TYPHLOSION, 50, trainer)
# e[0][2] << pbGenPoke(:GOLEM, 51, trainer)
# e[0][2] << pbGenPoke(:RAICHU, 52, trainer)
items << PBItems::FULLRESTORE
end
end
=end
class MKAI
# If true, the AI will always know the enemy's held item, even if it has not
# been revealed in normal gameplay.
AI_KNOWS_HELD_ITEMS = true
# If true, the AI wil always know the enemy's ability, even if it has not
# been revealed in normal gameplay.
AI_KNOWS_UNSEEN_ABILITIES = true
# If true, the AI will know the enemy's moves, even if they have not been
# revealed in normal gameplay.
AI_KNOWS_ENEMY_MOVES = true
class BattlerProjection
attr_accessor :ai_index
attr_accessor :battler
attr_reader :pokemon
attr_reader :side
attr_reader :damage_taken
attr_reader :damage_dealt
attr_accessor :revealed_ability
attr_accessor :revealed_item
attr_accessor :used_moves
attr_reader :flags
def initialize(side, pokemon, wild_pokemon = false)
@side = side
@pokemon = pokemon
@battler = nil
@ai = @side.ai
@battle = @ai.battle
@damage_taken = []
@damage_dealt = []
@ai_index = nil
@used_moves = []
@revealed_ability = false
@revealed_item = false
@skill = wild_pokemon ? 0 : 200
@flags = {}
end
alias original_missing method_missing
def method_missing(name, *args, &block)
if @battler.respond_to?(name)
MKAI.log("WARNING: Deferring method `#{name}` to @battler.")
return @battler.send(name, *args, &block)
else
return original_missing(name, *args, &block)
end
end
def opposing_side
return @side.opposing_side
end
def index
return @side.index == 0 ? @ai_index * 2 : @ai_index * 2 + 1
end
def hp
return @battler.hp
end
def fainted?
return @pokemon.fainted?
end
def totalhp
return @battler.totalhp
end
def status
return @battler.status
end
def statusCount
return @battler.statusCount
end
def burned?
return @battler.burned?
end
def poisoned?
return @battler.poisoned?
end
def paralyzed?
return @battler.paralyzed?
end
def frozen?
return @battler.frozen?
end
def asleep?
return @battler.asleep?
end
def confused?
return @battler.effects[PBEffects::Confusion] > 0
end
def level
return @battler.level
end
def active?
return !@battler.nil?
end
def effective_attack
stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8]
stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2]
stage = @battler.stages[PBStats::ATTACK] + 6
return (@battler.attack.to_f * stageMul[stage] / stageDiv[stage]).floor
end
def effective_defense
stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8]
stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2]
stage = @battler.stages[PBStats::DEFENSE] + 6
return (@battler.defense.to_f * stageMul[stage] / stageDiv[stage]).floor
end
def effective_spatk
stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8]
stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2]
stage = @battler.stages[PBStats::SPATK] + 6
return (@battler.spatk.to_f * stageMul[stage] / stageDiv[stage]).floor
end
def effective_spdef
stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8]
stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2]
stage = @battler.stages[PBStats::SPDEF] + 6
return (@battler.spdef.to_f * stageMul[stage] / stageDiv[stage]).floor
end
def effective_speed
stageMul = [2,2,2,2,2,2, 2, 3,4,5,6,7,8]
stageDiv = [8,7,6,5,4,3, 2, 2,2,2,2,2,2]
stage = @battler.stages[PBStats::SPEED] + 6
return (@battler.speed.to_f * stageMul[stage] / stageDiv[stage]).floor
end
def faster_than?(target)
return self.effective_speed >= target.effective_speed
end
def has_non_volatile_status?
return burned? || poisoned? || paralyzed? || frozen? || asleep?
end
# If this is true, this Pokémon will be treated as being a physical attacker.
# This means that the Pokémon will be more likely to try to use attack-boosting and
# defense-lowering status moves, and will be even more likely to use strong physical moves
# if any of these status boosts are active.
def is_physical_attacker?
stats = [effective_attack, effective_spatk]
avg = stats.sum / stats.size.to_f
min = (avg + (stats.max - avg) / 4 * 3).floor
avg = avg.floor
# min is the value the base attack must be above (3/4th avg) in order to for
# attack to be seen as a "high" value.
# Count the number of physical moves
physcount = 0
attackBoosters = 0
self.moves.each do |move|
next if move.pp == 0
physcount += 1 if move.physicalMove?
if move.statUp
for i in 0...move.statUp.size / 2
attackBoosters += move.statUp[i * 2 + 1] if move.statUp[i * 2] == PBStats::ATTACK
end
end
end
# If the user doesn't have any physical moves, the Pokémon can never be
# a physical attacker.
return false if physcount == 0
if effective_attack >= min
# Has high attack stat
# All physical moves would be a solid bet since we have a high attack stat.
return true
elsif effective_attack >= avg
# Attack stat is not high, but still above average
# If this Pokémon has any attack-boosting moves, or more than 1 physical move,
# we consider this Pokémon capable of being a physical attacker.
return true if physcount > 1
return true if attackBoosters >= 1
end
return false
end
# If this is true, this Pokémon will be treated as being a special attacker.
# This means that the Pokémon will be more likely to try to use spatk-boosting and
# spdef-lowering status moves, and will be even more likely to use strong special moves
# if any of these status boosts are active.
def is_special_attacker?
stats = [effective_attack, effective_spatk]
avg = stats.sum / stats.size.to_f
min = (avg + (stats.max - avg) / 4 * 3).floor
avg = avg.floor
# min is the value the base attack must be above (3/4th avg) in order to for
# attack to be seen as a "high" value.
# Count the number of physical moves
speccount = 0
spatkBoosters = 0
self.moves.each do |move|
next if move.pp == 0
speccount += 1 if move.specialMove?
if move.statUp
for i in 0...move.statUp.size / 2
spatkBoosters += move.statUp[i * 2 + 1] if move.statUp[i * 2] == PBStats::SPATK
end
end
end
# If the user doesn't have any physical moves, the Pokémon can never be
# a physical attacker.
return false if speccount == 0
if effective_spatk >= min
# Has high spatk stat
# All special moves would be a solid bet since we have a high spatk stat.
return true
elsif effective_spatk >= avg
# Spatk stat is not high, but still above average
# If this Pokémon has any spatk-boosting moves, or more than 1 special move,
# we consider this Pokémon capable of being a special attacker.
return true if speccount > 1
return true if spatkBoosters >= 1
end
return false
end
# Whether the pokemon should mega-evolve
def should_mega_evolve?(idx)
# Always mega evolve if the pokemon is able to
return @battle.pbCanMegaEvolve?(@battler.index)
end
def choose_move
# An array of scores in the format of [move_index, score, target]
scores = []
# Calculates whether to use an item
item_score = get_item_score()
# Yields [score, item, target&]
scores << [:ITEM, *item_score]
# Calculates whether to switch
switch_score = get_switch_score()
# Yields [score, pokemon_index]
scores << [:SWITCH, *switch_score]
MKAI.log("=" * 10 + " Turn #{@battle.turnCount + 1} " + "=" * 10)
# Gets the battler projections of the opposing side
# Calculate a score for each possible target
targets = opposing_side.battlers.clone
@side.battlers.each do |proj|
next if proj == self || proj.nil?
targets << proj
end
targets.each do |target|
next if target.nil?
MKAI.log("Moves for #{@battler.pokemon.name} against #{target.pokemon.name}")
# Calculate a score for all the user's moves
for i in 0...4
move = @battler.moves[i]
if !move.nil?
next if move.pp <= 0
target_type = move.pbTarget(@battler)
target_index = target.index
if PBTargets.noTargets?(target_type)
# If move has no targets, affects the user, a side or the whole field
target_index = -1
else
next if !@battle.pbMoveCanTarget?(@battler.index, target.index, target_type)
end
# Get the move score given a user and a target
score = get_move_score(target, move)
next if score.nil?
score = 1 if score < 1
scores << [i, score.round, target_index, target.pokemon.name]
end
end
end
# If absolutely no good options exist
if scores.size == 0
# Then just try to use the very first move with pp
for i in 0...4
move = @battler.moves[i]
next if move.nil?
if move.pp > 0
next if @battler.effects[PBEffects::DisableMove] == move.id
scores << [i, 1, 0, "internal"]
end
end
end
# If we still don't have any options, that means we have no non-disabled moves with pp left, so we use Struggle.
if scores.size == 0
# Struggle
#scores << [-1, 1000, 0, "internal"]
end
# Map the numeric skill factor to a -4..1 range (not hard bounds)
skill = @skill / -50.0 + 1
# Generate a random choice based on the skill factor and the score weights
idx = MKAI.weighted_factored_rand(skill, scores.map { |e| e[1] })
str = "=" * 30 + "\nSkill: #{@skill}\n"
weights = MKAI.get_weights(skill, scores.map { |e| e[1] })
total = weights.sum
scores.each_with_index do |e, i|
finalPerc = total == 0 ? 0 : (weights[i] / total.to_f * 100).round
if i == 0
# Item
name = PBItems.getName(e[2])
score = e[1]
if score > 0
str += "ITEM #{name}: #{score} (=> #{finalPerc}%)"
str += " << CHOSEN" if idx == 0
str += "\n"
end
elsif i == 1
# Switch
name = @battle.pbParty(@battler.index)[e[2]].name
score = e[1]
if score > 0
str += "SWITCH #{name}: #{score} (=> #{finalPerc}%)"
str += " << CHOSEN" if idx == 1
str += "\n"
end
#elsif i == -1
# str += "STRUGGLE: 100%"
else
move_index, score, target, target_name = e
name = @battler.moves[move_index].name
str += "MOVE(#{target_name}) #{name}: #{score} (=> #{finalPerc}%)"
str += " << CHOSEN" if i == idx
str += "\n"
end
end
str += "=" * 30
MKAI.log(str)
if idx == 0
# Index 0 means an item was chosen
ret = [:ITEM, scores[0][2]]
ret << scores[0][3] if scores[0][3] # Optional target
# TODO: Set to-be-healed flag so Heal Pulse doesn't also heal after healing by item
healing_item = scores[0][4]
if healing_item
self.flags[:will_be_healed]
end
return ret
elsif idx == 1
# Index 1 means switching was chosen
return [:SWITCH, scores[1][2]]
end
# Return [move_index, move_target]
if idx
choice = scores[idx]
move = @battler.moves[choice[0]]
if ["15B", "0D5", "0D6", "0D7", "0D8", "0D9"].include?(move.function)
self.flags[:will_be_healed] = true
elsif move.function == "0DF"
target.flags[:will_be_healed] = true
elsif move.function == "0A1"
@side.flags[:will_luckychant] = true
elsif move.function == "0A2"
@side.flags[:will_reflect] = true
elsif move.function == "0A3"
@side.flags[:will_lightscreen] = true
elsif move.function == "051"
@side.flags[:will_haze] = true
end
return [choice[0], choice[2]]
end
# No choice could be made
# Caller will make sure Struggle is used
end
def end_of_round
@flags = {}
end
# Calculates the score of the move against a specific target
def get_move_score(target, move)
# The target variable is a projection of a battler. We know its species and HP,
# but its item, ability, moves and other properties are not known unless they are
# explicitly shown or mentioned. Knowing these properties can change what our AI
# chooses; if we know the item of our target projection, and it's an Air Balloon,
# we won't choose a Ground move, for instance.
if target.side == @side
# The target is an ally
return nil if move.function != "0DF" # Heal Pulse
# Move score calculation will only continue if the target is not an ally,
# or if it is an ally, then the move must be Heal Pulse (0DF).
end
if move.statusMove?
# Start status moves off with a score of 30.
# Since this makes status moves unlikely to be chosen when the other moves
# have a high base power, all status moves should ideally be addressed individually
# in this method, and used in the optimal scenario for each individual move.
score = 30
MKAI.log("Test move #{move.name} (#{score})...")
# Trigger general score modifier code
score = MKAI::ScoreHandler.trigger_general(score, @ai, self, target, move)
# Trigger status-move score modifier code
score = MKAI::ScoreHandler.trigger_status_moves(score, @ai, self, target, move)
else
# Set the move score to the base power of the move
score = get_move_base_damage(move, target)
MKAI.log("Test move #{move.name} (#{score})...")
# Trigger general score modifier code
score = MKAI::ScoreHandler.trigger_general(score, @ai, self, target, move)
# Trigger damaging-move score modifier code
score = MKAI::ScoreHandler.trigger_damaging_moves(score, @ai, self, target, move)
end
# Trigger move-specific score modifier code
score = MKAI::ScoreHandler.trigger_move(move, score, @ai, self, target)
# Prefer a different move if this move would also hit the user's ally and it is super effective against the ally
# The target is not an ally to begin with (to exclude Heal Pulse and any other good ally-targeting moves)
if target.side != @side
# If the move is a status move, we can assume it has a positive effect and thus would be good for our ally too.
if !move.statusMove?
target_type = move.pbTarget(@battler)
# If the move also targets our ally
if target_type == PBTargets::AllNearOthers || target_type == PBTargets::AllBattlers || target_type == PBTargets::BothSides
# See if we have an ally
if ally = @side.battlers.find { |proj| proj && proj != self && !proj.fainted? }
matchup = ally.calculate_move_matchup(move.id)
# The move would be super effective on our ally
if matchup > 1
decr = (matchup / 2.0 * 75.0).round
score -= decr
MKAI.log("- #{decr} for super effectiveness on ally battler")
end
end
end
end
end
# Take 10% of the final score if the target is immune to this move.
if !move.statusMove? && target_is_immune?(move, target)
score *= 0.1
MKAI.log("* 0.1 for the target being immune")
end
# Take 10% of the final score if the move is disabled and thus unusable
if @battler.effects[PBEffects::DisableMove] == move.id
score *= 0.1
MKAI.log("* 0.1 for the move being disabled")
end
MKAI.log("= #{score}")
return score
end
# Calculates the best item to use and its score
def get_item_score
# Yields [score, item, optional_target, healing_item]
items = @battle.pbGetOwnerItems(@battler.index)
# Item categories
hpItems = {
PBItems::POTION => 20,
PBItems::SUPERPOTION => 50,
PBItems::HYPERPOTION => 200,
PBItems::MAXPOTION => -1,
PBItems::BERRYJUICE => 20,
PBItems::SWEETHEART => 20,
PBItems::FRESHWATER => 50,
PBItems::SODAPOP => 60,
PBItems::LEMONADE => 80,
PBItems::MOOMOOMILK => 100,
PBItems::ORANBERRY => 10,
PBItems::SITRUSBERRY => self.totalhp / 4,
PBItems::ENERGYPOWDER => 50,
PBItems::ENERGYROOT => 200,
PBItems::FULLRESTORE => -1,
}
hpItems[PBItems::RAGECANDYBAR] = 20 if !NEWEST_BATTLE_MECHANICS
singleStatusCuringItems = {
PBItems::AWAKENING => PBStatuses::SLEEP,
PBItems::CHESTOBERRY => PBStatuses::SLEEP,
PBItems::BLUEFLUTE => PBStatuses::SLEEP,
PBItems::ANTIDOTE => PBStatuses::POISON,
PBItems::PECHABERRY => PBStatuses::POISON,
PBItems::BURNHEAL => PBStatuses::BURN,
PBItems::RAWSTBERRY => PBStatuses::BURN,
PBItems::PARALYZEHEAL => PBStatuses::PARALYSIS,
PBItems::CHERIBERRY => PBStatuses::PARALYSIS,
PBItems::ICEHEAL => PBStatuses::FROZEN,
PBItems::ASPEARBERRY => PBStatuses::FROZEN
}
allStatusCuringItems = [
PBItems::FULLRESTORE,
PBItems::FULLHEAL,
PBItems::LAVACOOKIE,
PBItems::OLDGATEAU,
PBItems::CASTELIACONE,
PBItems::LUMIOSEGALETTE,
PBItems::SHALOURSABLE,
PBItems::BIGMALASADA,
PBItems::LUMBERRY,
PBItems::HEALPOWDER
]
xItems = {
PBItems::XATTACK => [PBStats::ATTACK, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XATTACK2 => [PBStats::ATTACK, 2],
PBItems::XATTACK3 => [PBStats::ATTACK, 3],
PBItems::XATTACK6 => [PBStats::ATTACK, 6],
PBItems::XDEFENSE => [PBStats::DEFENSE, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XDEFENSE2 => [PBStats::DEFENSE, 2],
PBItems::XDEFENSE3 => [PBStats::DEFENSE, 3],
PBItems::XDEFENSE6 => [PBStats::DEFENSE, 6],
PBItems::XSPATK => [PBStats::SPATK, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XSPATK2 => [PBStats::SPATK, 2],
PBItems::XSPATK3 => [PBStats::SPATK, 3],
PBItems::XSPATK6 => [PBStats::SPATK, 6],
PBItems::XSPDEF => [PBStats::SPDEF, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XSPDEF2 => [PBStats::SPDEF, 2],
PBItems::XSPDEF3 => [PBStats::SPDEF, 3],
PBItems::XSPDEF6 => [PBStats::SPDEF, 6],
PBItems::XSPEED => [PBStats::SPEED, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XSPEED2 => [PBStats::SPEED, 2],
PBItems::XSPEED3 => [PBStats::SPEED, 3],
PBItems::XSPEED6 => [PBStats::SPEED, 6],
PBItems::XACCURACY => [PBStats::ACCURACY, (NEWEST_BATTLE_MECHANICS) ? 2 : 1],
PBItems::XACCURACY2 => [PBStats::ACCURACY, 2],
PBItems::XACCURACY3 => [PBStats::ACCURACY, 3],
PBItems::XACCURACY6 => [PBStats::ACCURACY, 6]
}
scores = items.map do |item|
if item != PBItems::REVIVE && item != PBItems::MAXREVIVE
# Don't try to use the item if we can't use it on this Pokémon (e.g. due to Embargo)
next [0, item] if !@battle.pbCanUseItemOnPokemon?(item, @battler.pokemon, @battler, nil, false)
# Don't try to use the item if it doesn't have any effect, or some other condition that is not met
next [0, item] if !ItemHandlers.triggerCanUseInBattle(item, @battler.pokemon, @battler, nil, false, @battle, nil, false)
end
score = 0
# The item is a healing item
if hpToGain = hpItems[item]
hpLost = self.totalhp - self.hp
hpToGain = hpLost if hpToGain == -1 || hpToGain > hpLost
hpFraction = hpToGain / self.totalhp.to_f
# If hpFraction is high, then this item will heal almost all our HP.
# If it is low, then this item will heal very little of our total HP.
# We now factor the effectiveness of using this item into this fraction.
# Because using HP items at full health should not be an option, whereas
# using it at 1 HP should always be preferred.
itemEff = hpToGain / hpLost.to_f
itemEff = 0 if hpLost == 0
delayEff = 1.0
if !may_die_next_round?
# If we are likely to survive another hit of the last-used move,
# then we should discourage using healing items this turn because
# we can heal more if we use it later.
delayEff = 0.3
else
# If we are likely to die next round, we have a choice to make.
# It can occur that the target is also a one-shot from this point,
# which will make move scores skyrocket which can mean we won't use our item.
# So, if we are slower than our opponent, we will likely die first without using
# our item and without using our move. So if this is the case, we dramatically increase
# the score of using our item.
last_dmg = last_damage_taken
if last_dmg && !self.faster_than?(last_dmg[0])
delayEff = 2.5
end
end
finalFrac = hpFraction * itemEff * delayEff
score = (finalFrac * 200).round
end
# Single-status-curing items
if statusToCure = singleStatusCuringItems[item]
if self.status == statusToCure
factor = 1.0
factor = 0.5 if statusToCure == PBStatuses::PARALYSIS # Paralysis is not that serious
factor = 1.5 if statusToCure == PBStatuses::BURN && self.is_physical_attacker? # Burned while physical attacking
factor = 2.0 if statusToCure == PBStatuses::POISON && self.statusCount > 0 # Toxic
score += (140 * factor).round
end
end
# All-status-curing items
if allStatusCuringItems.include?(item)
if self.status != PBStatuses::NONE
factor = 1.0
factor = 0.5 if self.status == PBStatuses::PARALYSIS # Paralysis is not that serious
factor = 1.5 if self.status == PBStatuses::BURN && self.is_physical_attacker? # Burned while physical attacking
factor = 2.0 if self.status == PBStatuses::POISON && self.statusCount > 0 # Toxic
score += (120 * factor).round
end
end
# X-Items
if xStatus = xItems[item]
stat, increase = xStatus
# Only use X-Items on the battler's first turn
if @battler.turnCount == 0
factor = 1.0
factor = 2.0 if stat == PBStats::ATTACK && self.is_physical_attacker? ||
stat == PBStats::SPATK && self.is_special_attacker?
score = (80 * factor * increase).round
end
end
# Revive
if item == PBItems::REVIVE || item == PBItems::MAXREVIVE
party = @battle.pbParty(@battler.index)
candidate = nil
party.each do |pkmn|
if pkmn.fainted?
if candidate
if pkmn.level > candidate.level
candidate = pkmn
end
else
candidate = pkmn
end
end
end
if candidate
if items.include?(PBItems::MAXREVIVE) && item == PBItems::REVIVE
score = 200
else
score = 400
end
index = party.index(candidate)
next [score, item, index]
end
end
next [score, item]
end
max_score = 0
chosen_item = 0
chosen_target = nil
scores.each do |score, item, target|
if score >= max_score
max_score = score
chosen_item = item
chosen_target = target
end
end
if chosen_item != 0
return [max_score, chosen_item, chosen_target, !hpItems[chosen_item].nil?] if chosen_target
return [max_score, chosen_item, nil, !hpItems[chosen_item].nil?]
end
return [0, 0]
end
# Calculates the best pokemon to switch to and its score
def get_switch_score
# Yields [score, pokemon_index]
switch = false
# Used to render Future Sight useless
switch_to_dark_type = false
# The AI's party
party = @battle.pbParty(@battler.index)
# If the pokemon is struggling
if !@battle.pbCanChooseAnyMove?(@battler.index)
switch = true
end
# If the pokemon is perish songed and will die next turn
if self.effects[PBEffects::PerishSong] == 1
switch = true
end
# Encored into bad move
if self.effects[PBEffects::Encore] > 0
encored_move_index = @battler.pbEncoredMoveIndex
if encored_move_index >= 0
encored_move = @battler.moves[encored_move_index]
if encored_move.statusMove?
switch = true
else
dmgs = @damage_dealt.select { |e| e[1] == encored_move.id }
if dmgs.size > 0
last_dmg = dmgs[-1]
# Bad move if it did less than 35% damage
if last_dmg[3] < 0.35
switch = true
end
else
# No record of dealing damage with this move,
# which probably means the target is immune somehow,
# or the user happened to miss. Don't risk being stuck in
# a bad move in any case, and switch.
switch = true
end
end
end
end
pos = @battle.positions[@battler.index]
# If Future Sight will hit at the end of the round
if pos.effects[PBEffects::FutureSightCounter] == 1
# And if we have a dark type in our party
if party.any? { |pkmn| pkmn.types.include?(PBTypes::DARK) }
# We should switch to a dark type,
# but not if we're already close to dying anyway.
if !self.may_die_next_round?
switch = true
switch_to_dark_type = true
end
end
end
# Get the optimal switch choice by type
scores = get_optimal_switch_choice
# If we should switch due to effects in battle
if switch
availscores = scores.select { |e| !e[2].fainted? }
# Switch to a dark type instead of the best type matchup
if switch_to_dark_type
availscores = availscores.select { |e| e[2].pokemon.types.include?(PBTypes::DARK) }
end
while availscores.size > 0
hi_off_score, hi_def_score, proj = availscores[0]
eligible = true
eligible = false if proj.battler != nil # Already active
eligible = false if proj.pokemon.egg? # Egg
if eligible
score = (150 * hi_off_score * (switch_to_dark_type ? 2.0 : 1.0)).round
index = party.index(proj.pokemon)
return [score, index]
end
availscores.delete_at(0)
end
end
curr_score = scores.find { |e| e[2] == self }[0]
# If the current battler is not very effective offensively in any of its types,
# then we see if there is a battler that is super effective in at least one of its types.
if curr_score < 1.0
availscores = scores.select { |e| !e[2].fainted? }
while availscores.size > 0
hi_off_score, hi_def_score, proj = availscores[0]
eligible = true
eligible = false if proj.battler != nil # Already active
eligible = false if proj.pokemon.egg? # Egg
if eligible && hi_off_score >= 1.0
# Better choice than the current battler, so let's switch to this pokemon
score = (150 * hi_off_score).round
index = party.index(proj.pokemon)
return [score, index]
end
availscores.delete_at(0)
end
end
return [0, 0]
end
def get_optimal_switch_choice
party = @battle.pbParty(self.index)
scores = party.map do |pkmn|
proj = @ai.pokemon_to_projection(pkmn)
if !proj
raise "No projection found for party member #{pkmn.name}"
end
offensive_score = 1.0
defensive_score = 1.0
self.opposing_side.battlers.each do |target|
next if target.nil?
offensive_score *= proj.get_offense_score(target)
defensive_score *= target.get_offense_score(proj)
end
next [offensive_score, defensive_score, proj]
end
scores.sort! do |a,b|
ret = (b[0] <=> a[0])
next ret if ret != 0
# Tie-breaker for pokemon with identical offensive effectiveness
# Prefer the one with the best defense against the targets
# Lower is better, so a <=> b instead of b <=> a to get ascending order
ret = (a[1] <=> b[1])
next ret if ret != 0
# Tie-breaker for pokemon with identical defensive effectiveness
next b[2].pokemon.level <=> a[2].pokemon.level
end
#MKAI.log(scores.map { |e| e[2].pokemon.name + ": (#{e[0]}, #{e[1]})" }.join("\n"))
return scores
end
# Calculates adjusted base power of a move.
# Used as a starting point for a particular move's score against a target.
# Copied from Essentials.
def get_move_base_damage(move, target)
baseDmg = move.baseDamage
baseDmg = 60 if baseDmg == 1
# Covers all function codes which have their own def pbBaseDamage
case move.function
when "010" # Stomp
baseDmg *= 2 if target.effects[PBEffects::Minimize]
# Sonic Boom, Dragon Rage, Super Fang, Night Shade, Endeavor
when "06A", "06B", "06C", "06D", "06E"
# Multiplied by 2 to favor the idea of guaranteed x damage to the target
baseDmg = move.pbFixedDamage(self,target) * 2
when "06F" # Psywave
baseDmg = @battler.level
when "070" # OHKO
baseDmg = 200
when "071", "072", "073" # Counter, Mirror Coat, Metal Burst
baseDmg = 60
when "075", "076", "0D0", "12D" # Surf, Earthquake, Whirlpool, Shadow Storm
baseDmg = move.pbModifyDamage(baseDmg,@battler,target)
# Gust, Twister, Venoshock, Smelling Salts, Wake-Up Slap, Facade, Hex, Brine,
# Retaliate, Weather Ball, Return, Frustration, Eruption, Crush Grip,
# Stored Power, Punishment, Hidden Power, Fury Cutter, Echoed Voice,
# Trump Card, Flail, Electro Ball, Low Kick, Fling, Spit Up
when "077", "078", "07B", "07C", "07D", "07E", "07F", "080", "085", "087",
"089", "08A", "08B", "08C", "08E", "08F", "090", "091", "092", "097",
"098", "099", "09A", "0F7", "113"
baseDmg = move.pbBaseDamage(baseDmg,@battler,target)
when "086" # Acrobatics
baseDmg *= 2 if @battler.item == 0 || @battler.hasActiveItem?(:FLYINGGEM)
when "08D" # Gyro Ball
targetSpeed = target.effective_speed
userSpeed = self.effective_speed
baseDmg = [[(25 * targetSpeed / userSpeed).floor, 150].min,1].max
when "094" # Present
baseDmg = 50
when "095" # Magnitude
baseDmg = 71
baseDmg *= 2 if target.inTwoTurnAttack?("0CA") # Dig
when "096" # Natural Gift
baseDmg = move.pbNaturalGiftBaseDamage(@battler.item)
when "09B" # Heavy Slam
baseDmg = move.pbBaseDamage(baseDmg,@battler,target)
baseDmg *= 2 if NEWEST_BATTLE_MECHANICS &&
target.effects[PBEffects::Minimize]
when "0A0", "0BD", "0BE" # Frost Breath, Double Kick, Twineedle
baseDmg *= 2
when "0BF" # Triple Kick
baseDmg *= 6 # Hits do x1, x2, x3 baseDmg in turn, for x6 in total
when "0C0" # Fury Attack
if @battler.hasActiveAbility?(:SKILLLINK)
baseDmg *= 5
else
baseDmg = (baseDmg * 19 / 6).floor # Average damage dealt
end
when "0C1" # Beat Up
mult = 0
@battle.eachInTeamFromBattlerIndex(@battler.index) do |pkmn,_i|
mult += 1 if pkmn && pkmn.able? && pkmn.status == PBStatuses::NONE
end
baseDmg *= mult
when "0C4" # Solar Beam
baseDmg = move.pbBaseDamageMultiplier(baseDmg, @battler, target)
when "0D3" # Rollout
baseDmg *= 2 if @battler.effects[PBEffects::DefenseCurl]
when "0D4" # Bide
baseDmg = 40
when "0E1" # Final Gambit
baseDmg = @battler.hp
when "144" # Flying Press
# Flying type is handled separately in the move effectiveness score multiplier
baseDmg *= 2 if target.effects[PBEffects::Minimize]
when "166" # Stomping Tantrum
baseDmg *= 2 if @battler.lastRoundMoveFailed
when "175" # Double Iron Bash
baseDmg *= 2
baseDmg *= 2 if target.effects[PBEffects::Minimize]
end
return baseDmg
end
# Determines if the target is immune to a move.
# Copied from Essentials.
def target_is_immune?(move, target)
type = move.pbCalcType(@battler)
typeMod = move.pbCalcTypeMod(type,@battler,target)
# Type effectiveness
return true if PBTypes.ineffective?(typeMod)
# Immunity due to ability/item/other effects
if isConst?(move.type, PBTypes,:GROUND)
return true if target.airborne? && !move.hitsFlyingTargets?
elsif isConst?(move.type, PBTypes,:FIRE)
return true if target.hasActiveAbility?(:FLASHFIRE)
elsif isConst?(move.type, PBTypes,:WATER)
return true if target.hasActiveAbility?([:DRYSKIN,:STORMDRAIN,:WATERABSORB])
elsif isConst?(move.type, PBTypes,:GRASS)
return true if target.hasActiveAbility?(:SAPSIPPER)
elsif isConst?(move.type, PBTypes,:ELECTRIC)
return true if target.hasActiveAbility?([:LIGHTNINGROD,:MOTORDRIVE,:VOLTABSORB])
end
return true if PBTypes.notVeryEffective?(typeMod) &&
target.hasActiveAbility?(:WONDERGUARD)
return true if move.damagingMove? && @battler.index != target.index && !target.opposes?(@battler) &&
target.hasActiveAbility?(:TELEPATHY)
return true if move.canMagicCoat? && target.hasActiveAbility?(:MAGICBOUNCE) &&
target.opposes?(@battler)
return true if move.soundMove? && target.hasActiveAbility?(:SOUNDPROOF)
return true if move.bombMove? && target.hasActiveAbility?(:BULLETPROOF)
if move.powderMove?
return true if target.pbHasType?(:GRASS)
return true if target.hasActiveAbility?(:OVERCOAT)
return true if target.hasActiveItem?(:SAFETYGOGGLES)
end
return true if target.effects[PBEffects::Substitute]>0 && move.statusMove? &&
!move.ignoresSubstitute?(@battler) && @battler.index != target.index
return true if NEWEST_BATTLE_MECHANICS && @battler.hasActiveAbility?(:PRANKSTER) &&
target.pbHasType?(:DARK) && target.opposes?(@battler)
return true if move.priority > 0 && @battle.field.terrain == PBBattleTerrains::Psychic &&
target.affected_by_terrain? && target.opposes?(@battler)
return false
end
def get_move_accuracy(move, target)
return 100 if target.effects[PBEffects::Minimize] && move.tramplesMinimize?(1)
return 100 if target.effects[PBEffects::Telekinesis] > 0
baseAcc = move.pbBaseAccuracy(@battler, target)
return 100 if baseAcc == 0
return baseAcc
end
def types(type3 = true)
return @battler.pbTypes(type3) if @battler
return @pokemon.types
end
alias pbTypes types
def effects
return @battler.effects
end
def stages
return @battler.stages
end
def is_species?(species)
return @battler.isSpecies?(species)
end
alias isSpecies? is_species?
def has_type?(type)
return @battler.pbHasType?(type)
end
alias pbHasType? has_type?
def ability
return @battler.ability
end
def has_ability?(ability)
return @battler.hasActiveAbility?(ability) && (AI_KNOWS_UNSEEN_ABILITIES || @revealed_ability)
end
alias hasActiveAbility? has_ability?
def has_item?(item)
return @battler.hasActiveItem?(item) && (AI_KNOWS_HELD_ITEMS || @revealed_item)
end
alias hasActiveItem? has_item?
def moves
if @battler.nil?
return @pokemon.moves
elsif AI_KNOWS_ENEMY_MOVES || @side.index == 0
return @battler.moves
else
return @used_moves
end
end
def opposes?(projection)
if projection.is_a?(BattlerProjection)
return @side.index != projection.side.move_index
else
return @battler.index % 2 != projection.index % 2
end
end
def own_side
return @side
end
alias pbOwnSide own_side
def affected_by_terrain?
return @battler.affectedByTerrain?
end
alias affectedByTerrain? affected_by_terrain?
def airborne?
return @battler.airborne?
end
def semi_invulnerable?
return @battler.semiInvulnerable?
end
alias semiInvulnerable? semi_invulnerable?
def in_two_turn_attack?(*args)
return @battler.inTwoTurnAttack?(*args)
end
alias inTwoTurnAttack? in_two_turn_attack?
def can_attract?(target)
return @battler.pbCanAttract?(target)
end
alias pbCanAttract? can_attract?
def takes_indirect_damage?
return @battler.takesIndirectDamage?
end
alias takesIndirectDamage? takes_indirect_damage?
def weight
return @battler.pbWeight
end
alias pbWeight weight
def can_sleep?(inflictor, move, ignore_status = false)
return @battler.pbCanSleep?(inflictor, false, move, ignore_status)
end
def can_poison?(inflictor, move)
return @battler.pbCanPoison?(inflictor, false, move)
end
def can_burn?(inflictor, move)
return @battler.pbCanBurn?(inflictor, false, move)
end
def can_paralyze?(inflictor, move)
return @battler.pbCanParalyze?(inflictor, false, move)
end
def can_freeze?(inflictor, move)
return @battler.pbCanFreeze?(inflictor, false, move)
end
def register_damage_dealt(move, target, damage)
move = move.id if move.is_a?(PokeBattle_Move)
@damage_dealt << [target, move, damage, damage / target.totalhp.to_f]
end
def register_damage_taken(move, user, damage)
user.used_moves << move if !user.used_moves.any? { |m| m.id == move.id }
move = move.id
@damage_taken << [user, move, damage, damage / @battler.totalhp.to_f]
end
def get_damage_by_user(user)
return @damage_taken.select { |e| e[0] == user }
end
def get_damage_by_user_and_move(user, move)
move = move.id if move.is_a?(PokeBattle_Move)
return @damage_taken.select { |e| e[0] == user && e[1] == move }
end
def get_damage_by_move(move)
move = move.id if move.is_a?(PokeBattle_Move)
return @damage_taken.select { |e| e[1] == move }
end
def last_damage_taken
return @damage_taken[-1]
end
def last_damage_dealt
return @damage_dealt[-1]
end
# Estimates how much HP the battler will lose from end-of-round effects,
# such as status conditions or trapping moves
def estimate_hp_difference_at_end_of_round
lost = 0
# Future Sight
@battle.positions.each_with_index do |pos, idxPos|
next if !pos
# Ignore unless future sight hits at the end of the round
next if pos.effects[PBEffects::FutureSightCounter] != 1
# And only if its target is this battler
next if @battle.battlers[idxPos] != @battler
# Find the user of the move
moveUser = nil
@battle.eachBattler do |b|
next if b.opposes?(pos.effects[PBEffects::FutureSightUserIndex])
next if b.pokemonIndex != pos.effects[PBEffects::FutureSightUserPartyIndex]
moveUser = b
break
end
if !moveUser # User isn't in battle, get it from the party
party = @battle.pbParty(pos.effects[PBEffects::FutureSightUserIndex])
pkmn = party[pos.effects[PBEffects::FutureSightUserPartyIndex]]
if pkmn && pkmn.able?
moveUser = PokeBattle_Battler.new(@battle, pos.effects[PBEffects::FutureSightUserIndex])
moveUser.pbInitDummyPokemon(pkmn, pos.effects[PBEffects::FutureSightUserPartyIndex])
end
end
if moveUser && moveUser.pokemon != @battler.pokemon
# We have our move user, and it's not targeting itself
move_id = pos.effects[PBEffects::FutureSightMove]
move = PokeBattle_Move.pbFromPBMove(@battle, PBMove.new(move_id))
# Calculate how much damage a Future Sight hit will do
calcType = move.pbCalcType(moveUser)
@battler.damageState.typeMod = move.pbCalcTypeMod(calcType, moveUser, @battler)
move.pbCalcDamage(moveUser, @battler)
dmg = @battler.damageState.calcDamage
lost += dmg
end
end
if takes_indirect_damage?
# Sea of Fire (Fire Pledge + Grass Pledge)
weather = @battle.pbWeather
if side.effects[PBEffects::SeaOfFire] != 0
unless weather == PBWeather::Rain || weather == PBWeather::HeavyRain ||
has_type?(:FIRE)
lost += @battler.totalhp / 8.0
end
end
# Leech Seed
if self.effects[PBEffects::LeechSeed] >= 0
lost += @battler.totalhp / 8.0
end
# Poison
if poisoned? && !has_ability?(:POISONHEAL)
dmg = statusCount == 0 ? @battler.totalhp / 8.0 : @battler.totalhp * self.effects[PBEffects::Toxic] / 16.0
lost += dmg
end
# Burn
if burned?
lost += (NEWEST_BATTLE_MECHANICS ? @battler.totalhp / 16.0 : @battler.totalhp / 8.0)
end
# Sleep + Nightmare
if asleep? && self.effects[PBEffects::Nightmare]
lost += @battler.totalhp / 4.0
end
# Curse
if self.effects[PBEffects::Curse]
lost += @battler.totalhp / 4.0
end
# Trapping Effects
if self.effects[PBEffects::Trapping] != 0
dmg = (NEWEST_BATTLE_MECHANICS ? b.totalhp / 8.0 : b.totalhp / 16.0)
if @battle.battlers[self.effects[PBEffects::TrappingUser]].hasActiveItem?(:BINDINGBAND)
dmg = (NEWEST_BATTLE_MECHANICS ? b.totalhp / 6.0 : b.totalhp / 8.0)
end
lost += dmg
end
end
return lost
end
def may_die_next_round?
dmg = last_damage_taken
return false if dmg.nil?
# Returns true if the damage from the last move is more than the remaining hp
# This is used in determining if there is a point in using healing moves or items
hplost = dmg[2]
# We will also lose damage from status conditions and end-of-round effects like wrap,
# so we make a rough estimate with those included.
hplost += estimate_hp_difference_at_end_of_round
return hplost >= self.hp
end
def took_more_than_x_damage?(x)
dmg = last_damage_taken
return false if dmg.nil?
# Returns true if the damage from the last move did more than (x*100)% of the total hp damage
return dmg[3] >= x
end
# If the battler can survive another hit from the same move the target used last,
# but the battler will die if it does not heal, then healing is considered necessary.
def is_healing_necessary?(x)
return may_die_next_round? && !took_more_than_x_damage?(x)
end
# Healing is pointless if the target did more damage last round than we can heal
def is_healing_pointless?(x)
return took_more_than_x_damage?(x)
end
def discourage_making_contact_with?(target)
return false if has_ability?(:LONGREACH)
bad_abilities = [:WEAKARMOR, :STAMINA, :IRONBARBS, :ROUGHSKIN, :PERISHBODY]
return true if bad_abilities.any? { |a| target.has_ability?(a) }
return true if target.has_ability?(:CUTECHARM) && target.can_attract?(self)
return true if (target.has_ability?(:GOOEY) || target.has_ability?(:TANGLINGHAIR)) && faster_than?(target)
return true if target.has_item?(:ROCKYHELMET)
return true if target.has_ability?(:EFFECTSPORE) && !has_type?(:GRASS) && !has_ability?(:OVERCOAT) && !has_item?(:OVERCOAT)
return true if (target.has_ability?(:STATIC) || target.has_ability?(:POISONPOINT) || target.has_ability?(:FLAMEBODY)) && !has_non_volatile_status?
end
def get_move_damage(target, move)
calcType = move.pbCalcType(@battler)
target.battler.damageState.typeMod = move.pbCalcTypeMod(calcType, @battler, target.battler)
move.pbCalcDamage(@battler, target.battler)
return target.battler.damageState.calcDamage
end
# Calculates the combined type effectiveness of all user and target types
def calculate_type_matchup(target)
user_types = self.pbTypes(true)
target_types = target.pbTypes(true)
mod = 1.0
user_types.each do |user_type|
target_types.each do |target_type|
user_eff = PBTypes.getEffectiveness(user_type, target_type)
mod *= user_eff / 2.0
target_eff = PBTypes.getEffectiveness(target_type, user_type)
mod *= 2.0 / target_eff
end
end
return mod
end
# Calculates the type effectiveness of a particular move against this user
def calculate_move_matchup(move_id)
move = PokeBattle_Move.pbFromPBMove(@ai.battle, PBMove.new(move_id))
# Calculate the type this move would be if used by us
types = move.pbCalcType(@battler)
types = [types] if !types.is_a?(Array)
user_types = types
target_types = self.pbTypes(true)
mod = 1.0
user_types.each do |user_type|
target_types.each do |target_type|
user_eff = PBTypes.getEffectiveness(user_type, target_type)
mod *= user_eff / 2.0
end
end
return mod
end
# Whether the type matchup between the user and target is favorable
def bad_against?(target)
return calculate_type_matchup(target) < 1.0
end
# Whether the user would be considered an underdog to the target.
# Considers type matchup and level
def underdog?(target)
return true if bad_against?(target)
return true if target.level >= self.level + 5
return false
end
def has_usable_move_type?(type)
return self.moves.any? { |m| m.type == type && m.pp > 0 }
end
def get_offense_score(target)
# Note: self does not have a @battler value as it is a party member, i.e. only a PokeBattle_Pokemon object
# Return 1.0+ value if self is good against the target
user_types = self.pbTypes(true)
target_types = target.pbTypes(true)
max = 0
user_types.each do |user_type|
next unless self.has_usable_move_type?(user_type)
mod = 1.0
target_types.each do |target_type|
eff = PBTypes.getEffectiveness(user_type, target_type) / 2.0
if eff >= 2.0
mod *= eff
else
mod *= eff
end
end
max = mod if mod > max
end
return max
end
end
end