mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-10 22:54:59 +00:00
1316 lines
48 KiB
Ruby
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 |