Waged war against TODO comments in the AI, some refactoring of AI

This commit is contained in:
Maruno17
2023-05-07 23:12:39 +01:00
parent b7a40d0344
commit 7a8754c425
17 changed files with 702 additions and 830 deletions

View File

@@ -94,6 +94,8 @@ module Battle::AI::Handlers
GeneralMoveAgainstTargetScore = HandlerHash.new
ShouldSwitch = HandlerHash.new
ShouldNotSwitch = HandlerHash.new
AbilityRanking = AbilityHandlerHash.new
ItemRanking = ItemHandlerHash.new
def self.move_will_fail?(function_code, *args)
return MoveFailureCheck.trigger(function_code, *args) || false
@@ -151,4 +153,14 @@ module Battle::AI::Handlers
end
return ret
end
def self.modify_ability_ranking(ability, score, *args)
ret = AbilityRanking.trigger(ability, score, *args)
return (ret.nil?) ? score : ret
end
def self.modify_item_ranking(item, score, *args)
ret = ItemRanking.trigger(item, score, *args)
return (ret.nil?) ? score : ret
end
end

View File

@@ -1,3 +1,6 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
# 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
@@ -63,7 +66,6 @@ class Battle::AI
#-----------------------------------------------------------------------------
# TODO: Need a way to allow a ShouldSwitch handler to recommend a replacement.
def choose_best_replacement_pokemon(idxBattler, mandatory = false)
# Get all possible replacement Pokémon
party = @battle.pbParty(idxBattler)
@@ -111,8 +113,16 @@ class Battle::AI
elsif entry_hazard_damage > 0
score -= 50 * entry_hazard_damage / pkmn.hp
end
# TODO: Toxic Spikes.
# TODO: Sticky Web.
if !pkmn.hasItem?(:HEAVYDUTYBOOTS) && !pokemon_airborne?(pkmn)
# Toxic Spikes
if @user.pbOwnSide.effects[PBEffects::ToxicSpikes] > 0
score -= 20 if pokemon_can_be_poisoned?(pkmn)
end
# Sticky Web
if @user.pbOwnSide.effects[PBEffects::ToxicSpikes]
score -= 15
end
end
# Predict effectiveness of foe's last used move against pkmn
each_foe_battler(@user.side) do |b, i|
next if !b.battler.lastMoveUsed
@@ -132,9 +142,16 @@ class Battle::AI
score += m.power * Effectiveness.calculate(m.type, *bTypes) / 10
end
end
# Prefer if pkmn has lower HP and its position will be healed by Wish
position = @battle.positions[idxBattler]
if position.effects[PBEffects::Wish] > 0
amt = position.effects[PBEffects::WishAmount]
if pkmn.totalhp - pkmn.hp > amt * 2 / 3
score += 20 * [pkmn.totalhp - pkmn.hp, amt].min / pkmn.totalhp
end
end
# Prefer if user is about to faint from Perish Song
score += 20 if @user.effects[PBEffects::PerishSong] == 1
# TODO: Prefer if pkmn has lower HP and its position will be healed by Wish.
return score
end
@@ -148,12 +165,9 @@ class Battle::AI
ret += pkmn.totalhp * eff / 8 if !Effectiveness.ineffective?(eff)
end
# Spikes
if @battle.sides[side].effects[PBEffects::Spikes] > 0
if @battle.field.effects[PBEffects::Gravity] > 0 || pkmn.hasItem?(:IRONBALL) ||
!(pkmn.hasType?(:FLYING) || pkmn.hasItem?(:LEVITATE) || pkmn.hasItem?(:AIRBALLOON))
spikes_div = [8, 6, 4][@battle.sides[side].effects[PBEffects::Spikes] - 1]
ret += pkmn.totalhp / spikes_div
end
if @battle.sides[side].effects[PBEffects::Spikes] > 0 && !pokemon_airborne?(pkmn)
spikes_div = [8, 6, 4][@battle.sides[side].effects[PBEffects::Spikes] - 1]
ret += pkmn.totalhp / spikes_div
end
return ret
end
@@ -218,7 +232,6 @@ Battle::AI::Handlers::ShouldSwitch.add(:significant_eor_damage,
# Pokémon can cure its status problem or heal some HP with its ability by
# switching out. Covers all abilities with an OnSwitchOut AbilityEffects
# handler.
# TODO: Add randomness?
#===============================================================================
Battle::AI::Handlers::ShouldSwitch.add(:cure_status_problem_by_switching_out,
proc { |battler, reserves, ai, battle|
@@ -250,15 +263,25 @@ Battle::AI::Handlers::ShouldSwitch.add(:cure_status_problem_by_switching_out,
end
# Not worth curing status problems that still allow actions if at high HP
next false if battler.hp >= battler.totalhp / 2 && ![:SLEEP, :FROZEN].include?(battler.status)
PBDebug.log_ai("#{battler.name} wants to switch to cure its status problem with #{battler.ability.name}")
next true
if ai.pbAIRandom(100) < 70
PBDebug.log_ai("#{battler.name} wants to switch to cure its status problem with #{battler.ability.name}")
next true
end
elsif battler.ability == :REGENERATOR
# Heals 33% HP
# Not worth healing if battler would lose more HP from switching back in later
next false if entry_hazard_damage >= battler.totalhp / 3
# Not worth healing HP if already at high HP
next false if battler.hp >= battler.totalhp / 2
# TODO: Don't bother if user can do decent damage.
if ai.pbAIRandom(100) < 50
# Don't bother if a foe is at low HP and could be knocked out instead
if battler.check_for_move { |m| m.damagingMove? }
weak_foe = false
ai.each_foe_battler(battler.side) do |b, i|
weak_foe = true if b.hp < b.totalhp / 3
break if weak_foe
end
next false if weak_foe
end
if ai.pbAIRandom(100) < 70
PBDebug.log_ai("#{battler.name} wants to switch to heal with #{battler.ability.name}")
next true
end
@@ -276,7 +299,7 @@ Battle::AI::Handlers::ShouldSwitch.add(:wish_healing,
position = battle.positions[battler.index]
next false if position.effects[PBEffects::Wish] == 0
amt = position.effects[PBEffects::WishAmount]
next false if battler.totalhp - battler.hp >= amt * 2 / 3
next false if battler.totalhp - battler.hp >= amt * 2 / 3 # Want to heal itself instead
reserve_wants_healing_more = false
reserves.each do |pkmn|
entry_hazard_damage = ai.calculate_entry_hazard_damage(pkmn, battler.index & 1)
@@ -453,24 +476,6 @@ Battle::AI::Handlers::ShouldSwitch.add(:foe_has_wonder_guard,
break
end
end
# TODO: Check if battler has other useful status moves. CFRU considers
# these (and makes sure they're usable; also ensure they're not
# stopped by a substitute):
# - Inflict sleep/poison/burn/paralysis (not freeze)
# - Inflict confusion (inc. Swagger/Flatter)
# - Start damaging weather (sandstorm/hail)
# - Trick (to give foe an item with EOR damage/status infliction)
# - Other EOR damage moves (Leech Seed, Nightmare, Curse)
# - Perish Song
# - Add third type to target (Trick-or-Treat, Forest's Curse)
# - Worry Seed, Gastro Acid, Entrainment, Simple Beam, Core Enforcer
# - Roar
# - Baton Pass, Teleport
# - Memento (why?)
# - Entry hazards (not sure why; just to stack them up?)
# - Wish (just to set it up?)
# - Tailwind (just to set it up?)
# - Lucky Chant (just to set it up?)
break if has_super_effective_move
end
if !non_wonder_guard_foe_exists && !has_super_effective_move
@@ -500,18 +505,11 @@ Battle::AI::Handlers::ShouldSwitch.add(:foe_has_wonder_guard,
}
)
#===============================================================================
#
#===============================================================================
# TODO: Switch if battler's offensive stats are sufficiently low and it wants to
# use damaging moves (CFRU applies this only to a sweeper).
#===============================================================================
# Pokémon doesn't have an ability that makes it immune to a foe's move, but a
# reserve does (see def pokemon_can_absorb_move?). The foe's move is chosen
# randomly, or is their most powerful move if the trainer's skill level is good
# enough.
# TODO: Add randomness?
#===============================================================================
Battle::AI::Handlers::ShouldSwitch.add(:absorb_foe_move,
proc { |battler, reserves, ai, battle|
@@ -525,11 +523,8 @@ Battle::AI::Handlers::ShouldSwitch.add(:absorb_foe_move,
ai.each_foe_battler(battler.side) do |b, i|
b.moves.each do |move|
next if move.statusMove?
# TODO: Improve on m_power with STAB and attack stat/stages and certain
# other damage-altering effects, including base power calculations
# for moves with variable power.
m_power = move.power
m_power = battler.hp if move.is_a?(Battle::Move::OHKO)
m_power = 100 if move.is_a?(Battle::Move::OHKO)
m_type = move.pbCalcType(b.battler)
foe_moves.push([m_power, m_type, move])
end
@@ -542,16 +537,16 @@ Battle::AI::Handlers::ShouldSwitch.add(:absorb_foe_move,
chosen_move = foe_moves[ai.pbAIRandom(foe_moves.length)] # Random move
end
# Get the chosen move's information
move_power = chosen_move[0]
move_type = chosen_move[1]
move = chosen_move[2]
# TODO: Don't bother if the move's power isn't particularly high? Would need
# to figure out what "particularly high" means, probably involving the
# battler's defences in a rough damage calculation (the attacking part
# of which is above).
# Don't bother if the foe's best move isn't too strong
next false if move_power < 70
# Check battler for absorbing ability
next false if ai.pokemon_can_absorb_move?(battler, move, move_type)
# battler can't absorb move; find a party Pokémon that can
if reserves.any? { |pkmn| ai.pokemon_can_absorb_move?(pkmn, move, move_type) }
next false if ai.pbAIRandom(100) < 70
PBDebug.log_ai("#{battler.name} wants to switch because it can't absorb a foe's move but a reserve can")
next true
end
@@ -559,18 +554,6 @@ Battle::AI::Handlers::ShouldSwitch.add(:absorb_foe_move,
}
)
#===============================================================================
#
#===============================================================================
# TODO: Switch if foe is locked into using a single move and a reserve can
# resist/no sell it.
#===============================================================================
#
#===============================================================================
# TODO: Switch if a foe is using a damaging two-turn attack and a reserve can
# resist/no sell its damage.
#===============================================================================
# Sudden Death rule (at the end of each round, if one side has more able Pokémon
# than the other side, that side wins). Avoid fainting at all costs.
@@ -590,14 +573,6 @@ Battle::AI::Handlers::ShouldSwitch.add(:sudden_death,
}
)
#===============================================================================
#
#===============================================================================
# TODO: Switch if battler is at risk of being KO'd (unless it's at low HP and
# paralysed and can't cure itself/benefit from paralysis, as it'll
# probably not survive anyway). Don't bother if battler is Aegislash and
# could go into Shield Form instead.
#===============================================================================
# Pokémon is within 5 levels of the foe, and foe's last move was super-effective
# and powerful.
@@ -664,19 +639,20 @@ Battle::AI::Handlers::ShouldNotSwitch.add(:lethal_entry_hazards,
#===============================================================================
Battle::AI::Handlers::ShouldNotSwitch.add(:battler_has_super_effective_move,
proc { |battler, reserves, ai, battle|
next false if battler.effects[PBEffects::PerishSong] == 1
next false if battle.rules["suddendeath"]
has_super_effective_move = false
battler.battler.eachMove do |move|
next if move.pp == 0 && move.total_pp > 0
next if move.statusMove?
# TODO: next if move is unusable? This would be complicated to implement.
# NOTE: Ideally this would ignore moves that are unusable, but that would
# be too complicated to implement.
move_type = move.type
move_type = move.pbCalcType(battler.battler) if ai.trainer.medium_skill?
ai.each_foe_battler(battler.side) do |b|
# TODO: next if move can't target b? This would be complicated to
# implement.
# TODO: Check the move's power as well? Do a (rough) damage calculation
# for it and come up with a threshold % HP?
# NOTE: Ideally this would ignore foes that move cannot target, but that
# is complicated enough to implement that I'm not bothering. It's
# also rare that it would matter.
eff = b.effectiveness_of_type_against_battler(move_type, battler)
has_super_effective_move = Effectiveness.super_effective?(eff)
break if has_super_effective_move

View File

@@ -1,3 +1,6 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
HP_HEAL_ITEMS = {
:POTION => 20,
@@ -74,13 +77,6 @@ class Battle::AI
:REVIVALHERB => 7,
:MAXHONEY => 7
}
# TODO: Add more items for the AI to use from their Bag:
# Confusion healing (Yellow Flute, Persim Berry)
# Infatuation healing (Red Flute)
# PP (Either, Max Ether, Leppa Berry, Elixir, Max Elixir)
# Dire Hit (and 2 and 3)
# Guard Spec.
# Poké Flute (awakens all battlers)
#-----------------------------------------------------------------------------
@@ -175,7 +171,7 @@ class Battle::AI
want_to_cure_status = @battlers[battler.index].wants_status_problem?(pkmn.status)
want_to_cure_status = false if pkmn.status == :SLEEP && pkmn.statusCount <= 2
end
want_to_cure_status ||= (battler.effects[PBEffects::Confusion] > 0)
want_to_cure_status ||= (battler.effects[PBEffects::Confusion] > 1)
end
if HP_HEAL_ITEMS.include?(item)
if pkmn.hp < pkmn.totalhp

View File

@@ -1,7 +1,8 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
# Decide whether the opponent should Mega Evolve.
# TODO: Where relevant, pretend the user is Mega Evolved if it isn't but can
# be.
def pbEnemyShouldMegaEvolve?
if @battle.pbCanMegaEvolve?(@user.index) # Simple "always should if possible"
PBDebug.log_ai("#{@user.name} will Mega Evolve")

View File

@@ -1,3 +1,6 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
MOVE_FAIL_SCORE = 20
MOVE_USELESS_SCORE = 60 # Move predicted to do nothing or just be detrimental
@@ -58,8 +61,6 @@ class Battle::AI
@battle.allBattlers.each do |b|
next if redirected_target && b.index != redirected_target
next if !@battle.pbMoveCanTarget?(@user.battler.index, b.index, target_data)
# TODO: Should this sometimes consider targeting an ally? See def
# pbGetMoveScoreAgainstTarget for more information.
next if target_data.targets_foe && !@user.battler.opposes?(b)
PBDebug.log_ai("#{@user.name} is considering using #{orig_move.name} against #{b.name} (#{b.index})...")
score = MOVE_BASE_SCORE
@@ -199,9 +200,6 @@ class Battle::AI
return true if @battle.field.terrain == :Psychic && @target.battler.affectedByTerrain? &&
@target.opposes?(@user) && @move.rough_priority(@user) > 0
# Immunity because of ability
# TODO: If an ally has such an ability, may want to just not prefer the move
# instead of predicting its failure, as might want to hit the ally
# after all.
return true if @move.move.pbImmunityByAbility(@user.battler, @target.battler, false)
# Immunity because of Dazzling/Queenly Majesty
if @move.rough_priority(@user) > 0 && @target.opposes?(@user)
@@ -293,15 +291,6 @@ class Battle::AI
# means the move will fail or do nothing against the target.
# Assumes def set_up_move_check and def set_up_move_check_target have
# previously been called.
# TODO: Add something in here (I think) to specially score moves used against
# an ally and the ally has an ability that will benefit from being hit
# by the move.
# TODO: The above also applies if the move is Heal Pulse or a few other moves
# like that, which CAN target a foe but you'd never do so. Maybe use a
# move flag to determine such moves? The implication is that such moves
# wouldn't apply the "185 - score" bit, which would make their
# MoveHandlers do the opposite calculations to other moves with the same
# targets, but is this desirable?
def pbGetMoveScoreAgainstTarget
# Predict whether the move will fail against the target
if @trainer.has_skill_flag?("PredictMoveFailure") && pbPredictMoveFailureAgainstTarget
@@ -327,9 +316,8 @@ class Battle::AI
PBDebug.log(" move is useless against #{@target.name}")
return -1
end
# TODO: Is this reversal of the score okay?
old_score = score
score = 185 - score
score = ((1.85 * MOVE_BASE_SCORE) - score).to_i
PBDebug.log_score_change(score - old_score, "score inverted (move targets ally but can target foe)")
end
return score

View File

@@ -1,3 +1,6 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
# Main method for calculating the score for moves that raise a battler's
# stat(s).
@@ -89,14 +92,10 @@ class Battle::AI
#-----------------------------------------------------------------------------
# Returns whether the target raising the given stat will have any impact.
# TODO: Make sure the move's actual damage category is taken into account,
# i.e. CategoryDependsOnHigherDamagePoisonTarget and
# CategoryDependsOnHigherDamageIgnoreTargetAbility.
def stat_raise_worthwhile?(target, stat, fixed_change = false)
if !fixed_change
return false if !target.battler.pbCanRaiseStatStage?(stat, @user.battler, @move.move)
end
# TODO: Not worth it if target is predicted to switch out (except via Baton Pass).
# Check if target won't benefit from the stat being raised
return true if target.has_move_with_function?("SwitchOutUserPassOnEffects",
"PowerHigherWithUserPositiveStatStages")
@@ -125,17 +124,29 @@ class Battle::AI
"PowerHigherWithUserPositiveStatStages"
]
if !target.has_move_with_function?(*moves_that_prefer_high_speed)
# TODO: Not worth it if the target is too much slower than its foe(s)
# and can't be made fast enough.
meaningful = false
target_speed = target.rough_stat(:SPEED)
each_foe_battler(target.side) do |b, i|
return true if b.faster_than?(target)
b_speed = b.rough_stat(:SPEED)
meaningful = true if target_speed < b_speed && target_speed * 2.5 > b_speed
break if meaningful
end
return false
return false if !meaningful
end
when :ACCURACY
# TODO: Yes if any of target's moves have lower accuracy, or target is
# affected by accuracy-lowering effects, or if target's foe(s) have
# increased evasion.
min_accuracy = 100
target.battler.moves.each do |m|
next if m.accuracy == 0 || m.is_a?(Battle::Move::OHKO)
min_accuracy = m.accuracy if m.accuracy < min_accuracy
end
if min_accuracy >= 90 && target.stages[:ACCURACY] >= 0
meaningful = false
each_foe_battler(target.side) do |b, i|
meaningful = true if b.stages[:EVASION] > 0
break if meaningful
end
return false if !meaningful
end
when :EVASION
end
return true
@@ -158,7 +169,8 @@ class Battle::AI
# Prefer if target is at high HP, don't prefer if target is at low HP
score += total_increment * desire_mult * ((100 * target.hp / target.totalhp) - 50) / 8 # +6 to -6 per stage
end
# TODO: Look at abilities that trigger upon stat raise. There are none.
# NOTE: There are no abilities that trigger upon stat raise, but this is
# where they would be accounted for if they existed.
return score
end
@@ -230,8 +242,6 @@ class Battle::AI
break
end
end
# TODO: Prefer if the target is able to cause flinching (moves that
# flinch, or has King's Rock/Stench).
# Prefer if the target has Electro Ball or Power Trip/Stored Power
moves_that_prefer_high_speed = [
"PowerHigherWithUserFasterThanTarget",
@@ -380,14 +390,10 @@ class Battle::AI
#-----------------------------------------------------------------------------
# Returns whether the target lowering the given stat will have any impact.
# TODO: Make sure the move's actual damage category is taken into account,
# i.e. CategoryDependsOnHigherDamagePoisonTarget and
# CategoryDependsOnHigherDamageIgnoreTargetAbility.
def stat_drop_worthwhile?(target, stat, fixed_change = false)
if !fixed_change
return false if !target.battler.pbCanLowerStatStage?(stat, @user.battler, @move.move)
end
# TODO: Not worth it if target is predicted to switch out (except via Baton Pass).
# Check if target won't benefit from the stat being lowered
case stat
when :ATTACK
@@ -414,14 +420,22 @@ class Battle::AI
"PowerHigherWithUserPositiveStatStages"
]
if !target.has_move_with_function?(*moves_that_prefer_high_speed)
# TODO: Not worth it if the target is too much faster than its foe(s)
# and can't be brought slow enough.
meaningful = false
target_speed = target.rough_stat(:SPEED)
each_foe_battler(target.side) do |b, i|
return true if !b.faster_than?(target)
b_speed = b.rough_stat(:SPEED)
meaningful = true if target_speed > b_speed && target_speed < b_speed * 2.5
break if meaningful
end
return false
return false if !meaningful
end
when :ACCURACY
meaningful = false
target.battler.moves.each do |m|
meaningful = true if m.accuracy > 0 && !m.is_a?(Battle::Move::OHKO)
break if meaningful
end
return false if !meaningful
when :EVASION
end
return true
@@ -444,7 +458,11 @@ class Battle::AI
# Prefer if target is at high HP, don't prefer if target is at low HP
score += total_decrement * desire_mult * ((100 * target.hp / target.totalhp) - 50) / 8 # +6 to -6 per stage
end
# TODO: Look at abilities that trigger upon stat lowering.
# Don't prefer if target has an ability that triggers upon stat loss
# (Competitive, Defiant)
if target.opposes?(@user) && Battle::AbilityEffects::OnStatLoss[target.ability]
score -= 10
end
return score
end
@@ -532,7 +550,6 @@ class Battle::AI
else
score += 10 * dec_mult
end
# TODO: Prefer if target is poisoned/toxiced/Leech Seeded/cursed.
when :EVASION
# Modify score depending on current stat stage
if old_stage <= -2 && decrement == 1
@@ -579,7 +596,12 @@ class Battle::AI
if b.has_damaging_move_of_type?(:WATER)
ret += (b.opposes?(move_user)) ? 10 : -10
end
# TODO: Check for freezing moves.
# Check for moves that freeze
if b.has_move_with_function?("FreezeTarget", "FreezeFlinchTarget") ||
(b.has_move_with_function?("EffectDependsOnEnvironment") &&
[:Snow, :Ice].include?(@battle.environment))
ret += (b.opposes?(move_user)) ? 5 : -5
end
when :Rain
# Check for Fire/Water moves
if b.has_damaging_move_of_type?(:WATER)
@@ -681,7 +703,6 @@ class Battle::AI
case terrain
when :Electric
# Immunity to sleep
# TODO: Check all battlers for sleep-inducing moves and other effects?
if b.status == :NONE
ret += (b.opposes?(move_user)) ? -8 : 8
end
@@ -701,8 +722,6 @@ class Battle::AI
end
when :Misty
# Immunity to status problems/confusion
# TODO: Check all battlers for status/confusion-inducing moves and other
# effects?
if b.status == :NONE || b.effects[PBEffects::Confusion] == 0
ret += (b.opposes?(move_user)) ? -8 : 8
end

View File

@@ -49,7 +49,6 @@ Battle::AI::Handlers::GeneralMoveScore.add(:thawing_move_when_frozen,
# Prefer using a priority move if the user is slower than the target and...
# - the user is at low HP, or
# - the target is predicted to be knocked out by the move.
# TODO: Less prefer a priority move if any foe knows Quick Guard?
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:priority_move_against_faster_target,
proc { |score, move, user, target, ai, battle|
@@ -66,6 +65,16 @@ Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:priority_move_against_f
score += 8
PBDebug.log_score_change(score - old_score, "target at low HP and move has priority over faster target")
end
# Any foe knows Quick Guard and can protect against priority moves
old_score = score
ai.each_foe_battler(user.side) do |b, i|
next if !b.has_move_with_function?("ProtectUserSideFromPriorityMoves")
next if Settings::MECHANICS_GENERATION <= 5 && b.effects[PBEffects::ProtectRate] > 1
score -= 5
end
if score != old_score
PBDebug.log_score_change(score - old_score, "a foe knows Quick Guard and may protect against priority moves")
end
end
next score
}
@@ -332,8 +341,8 @@ Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:thawing_move_against_fr
#
#===============================================================================
# TODO: Check all effects that trigger upon using a move, including per-hit
# stuff in def pbEffectsOnMakingHit and end-of-move stuff in def
# pbEffectsAfterMove.
# stuff in def pbEffectsOnMakingHit (worse if the move is a multi-hit one)
# and end-of-move stuff in def pbEffectsAfterMove.
#===============================================================================
#

View File

@@ -1,71 +1,9 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
def pbAIRandom(x); return rand(x); end
def pbStdDev(choices)
sum = 0
n = 0
choices.each do |c|
sum += c[1]
n += 1
end
return 0 if n < 2
mean = sum.to_f / n
varianceTimesN = 0
choices.each do |c|
next if c[1] <= 0
deviation = c[1].to_f - mean
varianceTimesN += deviation * deviation
end
# Using population standard deviation
# [(n-1) makes it a sample std dev, would be 0 with only 1 sample]
return Math.sqrt(varianceTimesN / n)
end
#-----------------------------------------------------------------------------
# Move's type effectiveness. For switching. Determines the effectiveness of a
# potential switch-in against an opposing battler.
# TODO: Unused.
def pbCalcTypeModPokemon(pkmn, target_battler)
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
pkmn.types.each do |thisType|
ret *= Effectiveness.calculate(thisType, *target_battler.types)
end
return ret
end
# Assumes that pkmn's ability is not negated by a global effect (e.g.
# Neutralizing Gas).
# pkmn is either a Battle::AI::AIBattler or a Pokemon. move is a Battle::Move.
def pokemon_can_absorb_move?(pkmn, move, move_type)
return false if pkmn.is_a?(Battle::AI::AIBattler) && !pkmn.ability_active?
# Check pkmn's ability
# Anything with a Battle::AbilityEffects::MoveImmunity handler
# TODO: Are there any other absorbing effects? Held item?
case pkmn.ability_id
when :BULLETPROOF
return move.bombMove?
when :FLASHFIRE
return move_type == :FIRE
when :LIGHTNINGROD, :MOTORDRIVE, :VOLTABSORB
return move_type == :ELECTRIC
when :SAPSIPPER
return move_type == :GRASS
when :SOUNDPROOF
return move.soundMove?
when :STORMDRAIN, :WATERABSORB, :DRYSKIN
return move_type == :WATER
when :TELEPATHY
# NOTE: The move is being used by a foe of pkmn.
return false
when :WONDERGUARD
types = pkmn.types
types = pkmn.pbTypes(true) if pkmn.is_a?(Battle::AI::AIBattler)
return Effectiveness.super_effective_type?(move_type, *types)
end
return false
end
#-----------------------------------------------------------------------------
def each_battler
@@ -95,4 +33,439 @@ class Battle::AI
yield battler, i if i != index && i.even? == index.even?
end
end
#-----------------------------------------------------------------------------
# Assumes that pkmn's ability is not negated by a global effect (e.g.
# Neutralizing Gas).
# pkmn is either a Battle::AI::AIBattler or a Pokemon. move is a Battle::Move.
def pokemon_can_absorb_move?(pkmn, move, move_type)
return false if pkmn.is_a?(Battle::AI::AIBattler) && !pkmn.ability_active?
# Check pkmn's ability
# Anything with a Battle::AbilityEffects::MoveImmunity handler
case pkmn.ability_id
when :BULLETPROOF
return move.bombMove?
when :FLASHFIRE
return move_type == :FIRE
when :LIGHTNINGROD, :MOTORDRIVE, :VOLTABSORB
return move_type == :ELECTRIC
when :SAPSIPPER
return move_type == :GRASS
when :SOUNDPROOF
return move.soundMove?
when :STORMDRAIN, :WATERABSORB, :DRYSKIN
return move_type == :WATER
when :TELEPATHY
# NOTE: The move is being used by a foe of pkmn.
return false
when :WONDERGUARD
types = pkmn.types
types = pkmn.pbTypes(true) if pkmn.is_a?(Battle::AI::AIBattler)
return Effectiveness.super_effective_type?(move_type, *types)
end
return false
end
# Used by Toxic Spikes.
def pokemon_can_be_poisoned?(pkmn)
# Check pkmn's immunity to being poisoned
return false if @battle.field.terrain == :Misty
return false if pkmn.hasType?(:POISON)
return false if pkmn.hasType?(:STEEL)
return false if pkmn.hasAbility?(:IMMUNITY)
return false if pkmn.hasAbility?(:PASTELVEIL)
return false if pkmn.hasAbility?(:FLOWERVEIL) && pkmn.hasType?(:GRASS)
return false if pkmn.hasAbility?(:LEAFGUARD) && [:Sun, :HarshSun].include?(@battle.pbWeather)
return false if pkmn.hasAbility?(:COMATOSE) && pkmn.isSpecies?(:KOMALA)
return false if pkmn.hasAbility?(:SHIELDSDOWN) && pkmn.isSpecies?(:MINIOR) && pkmn.form < 7
return true
end
def pokemon_airborne?(pkmn)
return false if pkmn.hasItem?(:IRONBALL)
return false if @battle.field.effects[PBEffects::Gravity] > 0
return true if pkmn.hasType?(:FLYING)
return true if pkmn.hasAbility?(:LEVITATE)
return true if pkmn.hasItem?(:AIRBALLOON)
return false
end
#-----------------------------------------------------------------------------
# These values are taken from the Complete-Fire-Red-Upgrade decomp here:
# https://github.com/Skeli789/Complete-Fire-Red-Upgrade/blob/f7f35becbd111c7e936b126f6328fc52d9af68c8/src/ability_battle_effects.c#L41
BASE_ABILITY_RATINGS = {
10 => [:DELTASTREAM, :DESOLATELAND, :HUGEPOWER, :MOODY, :PARENTALBOND,
:POWERCONSTRUCT, :PRIMORDIALSEA, :PUREPOWER, :SHADOWTAG,
:STANCECHANGE, :WONDERGUARD],
9 => [:ARENATRAP, :DRIZZLE, :DROUGHT, :IMPOSTER, :MAGICBOUNCE, :MAGICGUARD,
:MAGNETPULL, :SANDSTREAM, :SPEEDBOOST],
8 => [:ADAPTABILITY, :AERILATE, :CONTRARY, :DISGUISE, :DRAGONSMAW,
:ELECTRICSURGE, :GALVANIZE, :GRASSYSURGE, :ILLUSION, :LIBERO,
:MISTYSURGE, :MULTISCALE, :MULTITYPE, :NOGUARD, :POISONHEAL,
:PIXILATE, :PRANKSTER, :PROTEAN, :PSYCHICSURGE, :REFRIGERATE,
:REGENERATOR, :RKSSYSTEM, :SERENEGRACE, :SHADOWSHIELD, :SHEERFORCE,
:SIMPLE, :SNOWWARNING, :TECHNICIAN, :TRANSISTOR, :WATERBUBBLE],
7 => [:BEASTBOOST, :BULLETPROOF, :COMPOUNDEYES, :DOWNLOAD, :FURCOAT,
:HUSTLE, :ICESCALES, :INTIMIDATE, :LEVITATE, :LIGHTNINGROD,
:MEGALAUNCHER, :MOLDBREAKER, :MOXIE, :NATURALCURE, :SAPSIPPER,
:SHEDSKIN, :SKILLLINK, :SOULHEART, :STORMDRAIN, :TERAVOLT, :THICKFAT,
:TINTEDLENS, :TOUGHCLAWS, :TRIAGE, :TURBOBLAZE, :UNBURDEN,
:VOLTABSORB, :WATERABSORB],
6 => [:BATTLEBOND, :CHLOROPHYLL, :COMATOSE, :DARKAURA, :DRYSKIN,
:FAIRYAURA, :FILTER, :FLASHFIRE, :FORECAST, :GALEWINGS, :GUTS,
:INFILTRATOR, :IRONBARBS, :IRONFIST, :MIRRORARMOR, :MOTORDRIVE,
:NEUROFORCE, :PRISMARMOR, :QUEENLYMAJESTY, :RECKLESS, :ROUGHSKIN,
:SANDRUSH, :SCHOOLING, :SCRAPPY, :SHIELDSDOWN, :SOLIDROCK, :STAKEOUT,
:STAMINA, :STEELWORKER, :STRONGJAW, :STURDY, :SWIFTSWIM, :TOXICBOOST,
:TRACE, :UNAWARE, :VICTORYSTAR],
5 => [:AFTERMATH, :AIRLOCK, :ANALYTIC, :BERSERK, :BLAZE, :CLOUDNINE,
:COMPETITIVE, :CORROSION, :DANCER, :DAZZLING, :DEFIANT, :FLAREBOOST,
:FLUFFY, :GOOEY, :HARVEST, :HEATPROOF, :INNARDSOUT, :LIQUIDVOICE,
:MARVELSCALE, :MUMMY, :NEUTRALIZINGGAS, :OVERCOAT, :OVERGROW,
:PRESSURE, :QUICKFEET, :ROCKHEAD, :SANDSPIT, :SHIELDDUST, :SLUSHRUSH,
:SWARM, :TANGLINGHAIR, :TORRENT],
4 => [:ANGERPOINT, :BADDREAMS, :CHEEKPOUCH, :CLEARBODY, :CURSEDBODY,
:EARLYBIRD, :EFFECTSPORE, :FLAMEBODY, :FLOWERGIFT, :FULLMETALBODY,
:GORILLATACTICS, :HYDRATION, :ICEFACE, :IMMUNITY, :INSOMNIA,
:JUSTIFIED, :MERCILESS, :PASTELVEIL, :POISONPOINT, :POISONTOUCH,
:RIPEN, :SANDFORCE, :SOUNDPROOF, :STATIC, :SURGESURFER, :SWEETVEIL,
:SYNCHRONIZE, :VITALSPIRIT, :WATERCOMPACTION, :WATERVEIL,
:WHITESMOKE, :WONDERSKIN],
3 => [:AROMAVEIL, :AURABREAK, :COTTONDOWN, :DAUNTLESSSHIELD,
:EMERGENCYEXIT, :GLUTTONY, :GULPMISSLE, :HYPERCUTTER, :ICEBODY,
:INTREPIDSWORD, :LIMBER, :LIQUIDOOZE, :LONGREACH, :MAGICIAN,
:OWNTEMPO, :PICKPOCKET, :RAINDISH, :RATTLED, :SANDVEIL,
:SCREENCLEANER, :SNIPER, :SNOWCLOAK, :SOLARPOWER, :STEAMENGINE,
:STICKYHOLD, :SUPERLUCK, :UNNERVE, :WIMPOUT],
2 => [:BATTLEARMOR, :COLORCHANGE, :CUTECHARM, :DAMP, :GRASSPELT,
:HUNGERSWITCH, :INNERFOCUS, :LEAFGUARD, :LIGHTMETAL, :MIMICRY,
:OBLIVIOUS, :POWERSPOT, :PROPELLORTAIL, :PUNKROCK, :SHELLARMOR,
:STALWART, :STEADFAST, :STEELYSPIRIT, :SUCTIONCUPS, :TANGLEDFEET,
:WANDERINGSPIRIT, :WEAKARMOR],
1 => [:BIGPECKS, :KEENEYE, :MAGMAARMOR, :PICKUP, :RIVALRY, :STENCH],
0 => [:ANTICIPATION, :ASONECHILLINGNEIGH, :ASONEGRIMNEIGH, :BALLFETCH,
:BATTERY, :CHILLINGNEIGH, :CURIOUSMEDICINE, :FLOWERVEIL, :FOREWARN,
:FRIENDGUARD, :FRISK, :GRIMNEIGH, :HEALER, :HONEYGATHER, :ILLUMINATE,
:MINUS, :PLUS, :POWEROFALCHEMY, :QUICKDRAW, :RECEIVER, :RUNAWAY,
:SYMBIOSIS, :TELEPATHY, :UNSEENFIST],
-1 => [:DEFEATIST, :HEAVYMETAL, :KLUTZ, :NORMALIZE, :PERISHBODY, :STALL,
:ZENMODE],
-2 => [:SLOWSTART, :TRUANT]
}
#-----------------------------------------------------------------------------
# TODO: Add more items.
BASE_ITEM_RATINGS = {
4 => [:CHOICEBAND, :CHOICESCARF, :CHOICESPECS, :DEEPSEATOOTH, :LEFTOVERS,
:LIGHTBALL, :THICKCLUB],
3 => [:ADAMANTORB, :GRISEOUSORB, :LIFEORB, :LUSTROUSORB, :SOULDEW],
2 => [:BLACKBELT, :BLACKGLASSES, :CHARCOAL, :DRAGONFANG, :HARDSTONE,
:MAGNET, :METALCOAT, :MIRACLESEED, :MYSTICWATER, :NEVERMELTICE,
:POISONBARB, :SHARPBEAK, :SILKSCARF, :SILVERPOWDER, :SOFTSAND,
:SPELLTAG, :TWISTEDSPOON,
:DRACOPLATE, :DREADPLATE, :EARTHPLATE, :FISTPLATE, :FLAMEPLATE,
:ICICLEPLATE, :INSECTPLATE, :IRONPLATE, :MEADOWPLATE, :MINDPLATE,
:PIXIEPLATE, :SKYPLATE, :SPLASHPLATE, :SPOOKYPLATE, :STONEPLATE,
:TOXICPLATE, :ZAPPLATE,
:ODDINCENSE, :ROCKINCENSE, :ROSEINCENSE, :SEAINCENSE, :WAVEINCENSE,
:MUSCLEBAND, :WISEGLASSES],
1 => [:METRONOME],
-2 => [:LAGGINGTAIL, :STICKYBARB],
-4 => [:BLACKSLUDGE, :FLAMEORB, :IRONBALL, :TOXICORB]
}
end
#===============================================================================
#
#===============================================================================
Battle::AI::Handlers::AbilityRanking.add(:BLAZE,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:FIRE)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:CUTECHARM,
proc { |ability, score, battler, ai|
next 0 if battler.gender == 2
next score
}
)
Battle::AI::Handlers::AbilityRanking.copy(:CUTECHARM, :RIVALRY)
Battle::AI::Handlers::AbilityRanking.add(:FRIENDGUARD,
proc { |ability, score, battler, ai|
has_ally = false
ai.each_ally(battler.side) { |b, i| has_ally = true }
next score if has_ally
next 0
}
)
Battle::AI::Handlers::AbilityRanking.copy(:FRIENDGUARD, :HEALER, :SYMBOISIS, :TELEPATHY)
Battle::AI::Handlers::AbilityRanking.add(:GALEWINGS,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.type == :FLYING }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:HUGEPOWER,
proc { |ability, score, battler, ai|
next score if ai.stat_raise_worthwhile?(battler, :ATTACK, true)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.copy(:HUGEPOWER, :PUREPOWER)
Battle::AI::Handlers::AbilityRanking.add(:IRONFIST,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.punchingMove? }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:LIQUIDVOICE,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.soundMove? }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:MEGALAUNCHER,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.pulseMove? }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:OVERGROW,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:GRASS)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:PRANKSTER,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.statusMove? }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:PUNKROCK,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.damagingMove? && m.soundMove? }
next 1
}
)
Battle::AI::Handlers::AbilityRanking.add(:RECKLESS,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.recoilMove? }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:ROCKHEAD,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.recoilMove? && !m.is_a?(Battle::Move::CrashDamageIfFailsUnusableInGravity) }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:RUNAWAY,
proc { |ability, score, battler, ai|
next 0 if battler.wild?
next score
}
)
Battle::AI::Handlers::AbilityRanking.add(:SANDFORCE,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:GROUND, :ROCK, :STEEL)
next 2
}
)
Battle::AI::Handlers::AbilityRanking.add(:SKILLLINK,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.is_a?(Battle::Move::HitTwoToFiveTimes) }
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:STEELWORKER,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:STEEL)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:SWARM,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:BUG)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:TORRENT,
proc { |ability, score, battler, ai|
next score if battler.has_damaging_move_of_type?(:WATER)
next 0
}
)
Battle::AI::Handlers::AbilityRanking.add(:TRIAGE,
proc { |ability, score, battler, ai|
next score if battler.check_for_move { |m| m.healingMove? }
next 0
}
)
#===============================================================================
#
#===============================================================================
Battle::AI::Handlers::ItemRanking.add(:ADAMANTORB,
proc { |item, score, battler, ai|
next score if battler.battler.isSpecies?(:DIALGA) &&
battler.has_damaging_move_of_type?(:DRAGON, :STEEL)
next 0
}
)
Battle::AI::Handlers::ItemRanking.add(:BLACKSLUDGE,
proc { |item, score, battler, ai|
next 4 if battler.has_type?(:POISON)
next score
}
)
Battle::AI::Handlers::ItemRanking.add(:CHOICEBAND,
proc { |item, score, battler, ai|
next score if battler.check_for_move { |m| m.physicalMove?(m.type) }
next 0
}
)
Battle::AI::Handlers::ItemRanking.copy(:CHOICEBAND, :MUSCLEBAND)
Battle::AI::Handlers::ItemRanking.add(:CHOICESPECS,
proc { |item, score, battler, ai|
next score if battler.check_for_move { |m| m.specialMove?(m.type) }
next 0
}
)
Battle::AI::Handlers::ItemRanking.copy(:CHOICESPECS, :WISEGLASSES)
Battle::AI::Handlers::ItemRanking.add(:DEEPSEATOOTH,
proc { |item, score, battler, ai|
next score if battler.battler.isSpecies?(:CLAMPERL) &&
battler.check_for_move { |m| m.specialMove?(m.type) }
next 0
}
)
Battle::AI::Handlers::ItemRanking.add(:GRISEOUSORB,
proc { |item, score, battler, ai|
next score if battler.battler.isSpecies?(:GIRATINA) &&
battler.has_damaging_move_of_type?(:DRAGON, :GHOST)
next 0
}
)
Battle::AI::Handlers::ItemRanking.add(:IRONBALL,
proc { |item, score, battler, ai|
next 0 if battler.has_move_with_function?("ThrowUserItemAtTarget")
next score
}
)
Battle::AI::Handlers::ItemRanking.add(:LIGHTBALL,
proc { |item, score, battler, ai|
next score if battler.battler.isSpecies?(:PIKACHU) &&
battler.check_for_move { |m| m.damagingMove? }
next 0
}
)
Battle::AI::Handlers::ItemRanking.add(:LUSTROUSORB,
proc { |item, score, battler, ai|
next score if battler.battler.isSpecies?(:PALKIA) &&
battler.has_damaging_move_of_type?(:DRAGON, :WATER)
next 0
}
)
Battle::AI::Handlers::ItemRanking.add(:SOULDEW,
proc { |item, score, battler, ai|
next 0 if !battler.battler.isSpecies?(:LATIAS) && !battler.battler.isSpecies?(:LATIOS)
if Settings::SOUL_DEW_POWERS_UP_TYPES
next 0 if !battler.has_damaging_move_of_type?(:PSYCHIC, :DRAGON)
elsif !battler.check_for_move { |m| m.specialMove?(m.type) }
next 1 # Also boosts SpDef
end
next score
}
)
Battle::AI::Handlers::ItemRanking.add(:THICKCLUB,
proc { |item, score, battler, ai|
next score if (battler.battler.isSpecies?(:CUBONE) || battler.battler.isSpecies?(:MAROWAK)) &&
battler.check_for_move { |m| m.physicalMove?(m.type) }
next 0
}
)
Battle::AI::Handlers::ItemRanking.addIf(:type_boosting_items,
proc { |item|
next [:BLACKBELT, :BLACKGLASSES, :CHARCOAL, :DRAGONFANG, :HARDSTONE,
:MAGNET, :METALCOAT, :MIRACLESEED, :MYSTICWATER, :NEVERMELTICE,
:POISONBARB, :SHARPBEAK, :SILKSCARF, :SILVERPOWDER, :SOFTSAND,
:SPELLTAG, :TWISTEDSPOON,
:DRACOPLATE, :DREADPLATE, :EARTHPLATE, :FISTPLATE, :FLAMEPLATE,
:ICICLEPLATE, :INSECTPLATE, :IRONPLATE, :MEADOWPLATE, :MINDPLATE,
:PIXIEPLATE, :SKYPLATE, :SPLASHPLATE, :SPOOKYPLATE, :STONEPLATE,
:TOXICPLATE, :ZAPPLATE,
:ODDINCENSE, :ROCKINCENSE, :ROSEINCENSE, :SEAINCENSE, :WAVEINCENSE].include?(item)
},
proc { |item, score, battler, ai|
boosters = {
:BUG => [:SILVERPOWDER, :INSECTPLATE],
:DARK => [:BLACKGLASSES, :DREADPLATE],
:DRAGON => [:DRAGONFANG, :DRACOPLATE],
:ELECTRIC => [:MAGNET, :ZAPPLATE],
:FAIRY => [:PIXIEPLATE],
:FIGHTING => [:BLACKBELT, :FISTPLATE],
:FIRE => [:CHARCOAL, :FLAMEPLATE],
:FLYING => [:SHARPBEAK, :SKYPLATE],
:GHOST => [:SPELLTAG, :SPOOKYPLATE],
:GRASS => [:MIRACLESEED, :MEADOWPLATE, :ROSEINCENSE],
:GROUND => [:SOFTSAND, :EARTHPLATE],
:ICE => [:NEVERMELTICE, :ICICLEPLATE],
:NORMAL => [:SILKSCARF],
:POISON => [:POISONBARB, :TOXICPLATE],
:PSYCHIC => [:TWISTEDSPOON, :MINDPLATE, :ODDINCENSE],
:ROCK => [:HARDSTONE, :STONEPLATE, :ROCKINCENSE],
:STEEL => [:METALCOAT, :IRONPLATE],
:WATER => [:MYSTICWATER, :SPLASHPLATE, :SEAINCENSE, :WAVEINCENSE],
}
boosted_type = nil
boosters.each_pair do |type, items|
next if !items.include?(item)
boosted_type = type
break
end
next score if boosted_type && battler.has_damaging_move_of_type?(boosted_type)
next 0
}
)

View File

@@ -1,43 +1,37 @@
#===============================================================================
#
#===============================================================================
class Battle::AI
#=============================================================================
#
#=============================================================================
# TODO: Reborn has the REVENGEKILLER role which compares mon's speed with
# opponent (only when deciding whether to switch mon in) - this
# comparison should be calculated when needed instead of being a role.
module BattleRole
PHAZER = 0
CLERIC = 1
STALL_BREAKER = 2
STATUS_ABSORBER = 3
BATON_PASSER = 4
SPINNER = 5
FIELD_SETTER = 6
WEATHER_SETTER = 7
SWEEPER = 8
PIVOT = 9
PHYSICAL_WALL = 10
SPECIAL_WALL = 11
TANK = 12
TRAPPER = 13
SCREENER = 14
ACE = 15
LEAD = 16
SECOND = 17
end
#-----------------------------------------------------------------------------
# Determine the roles filled by a Pokémon on a given side at a given party
# index.
# Roles are:
# :ace
# :baton_passer
# :cleric
# :field_setter
# :lead
# :phazer
# :physical_wall
# :pivot
# :screener
# :second
# :special_wall
# :spinner
# :stall_breaker
# :status_absorber
# :sweeper
# :tank
# :trapper
# :weather_setter
# NOTE: Reborn has the REVENGEKILLER role which compares mon's speed with
# opponent (only when deciding whether to switch mon in) - this
# comparison should be calculated when needed instead of being a role.
def determine_roles(side, index)
pkmn = @battle.pbParty(side)[index]
ret = []
return ret if !pkmn || pkmn.egg?
# Check for moves indicative of particular roles
hasHealMove = false
hasPivotMove = false
hasHealMove = false
pkmn.moves.each do |m|
next if !m
move = Battle::Move.from_pokemon_move(@battle, m)
@@ -47,92 +41,87 @@ class Battle::AI
"StartPerishCountsForAllBattlers", # Perish Song
"SwitchOutTargetStatusMove", # Roar
"SwitchOutTargetDamagingMove" # Circle Throw
ret.push(BattleRole::PHAZER)
ret.push(:phazer)
when "CureUserPartyStatus" # Aromatherapy/Heal Bell
ret.push(BattleRole::CLERIC)
ret.push(:cleric)
when "DisableTargetStatusMoves" # Taunt
ret.push(BattleRole::STALL_BREAKER)
ret.push(:stall_breaker)
when "HealUserPositionNextTurn" # Wish
ret.push(BattleRole::CLERIC) if pkmn.ev[:HP] == Pokemon::EV_STAT_LIMIT
ret.push(:cleric) if pkmn.ev[:HP] == Pokemon::EV_STAT_LIMIT
when "HealUserFullyAndFallAsleep" # Rest
ret.push(BattleRole::STATUS_ABSORBER)
ret.push(:status_absorber)
when "SwitchOutUserPassOnEffects" # Baton Pass
ret.push(BattleRole::BATON_PASSER)
ret.push(:baton_passer)
when "SwitchOutUserStatusMove", "SwitchOutUserDamagingMove" # Teleport (Gen 8+), U-turn
hasPivotMove = true
ret.push(:pivot) if hasHealMove
when "RemoveUserBindingAndEntryHazards" # Rapid Spin
ret.push(BattleRole::SPINNER)
ret.push(:spinner)
when "StartElectricTerrain", "StartGrassyTerrain",
"StartMistyTerrain", "StartPsychicTerrain" # Terrain moves
ret.push(BattleRole::FIELD_SETTER)
ret.push(:field_setter)
else
ret.push(BattleRole::WEATHER_SETTER) if move.is_a?(Battle::Move::WeatherMove)
ret.push(:weather_setter) if move.is_a?(Battle::Move::WeatherMove)
end
end
# Check EVs, nature and moves for combinations indicative of particular roles
if pkmn.ev[:SPEED] == Pokemon::EV_STAT_LIMIT
if [:MODEST, :ADAMANT, # SpAtk+ Atk-, Atk+ SpAtk-
:TIMID, :JOLLY].include?(pkmn.nature) # Spd+ Atk-, Spd+ SpAtk-
ret.push(BattleRole::SWEEPER)
ret.push(:sweeper)
end
end
if hasHealMove
ret.push(BattleRole::PIVOT) if hasPivotMove
if pkmn.nature.stat_changes.any? { |change| change[0] == :DEFENSE && change[1] > 0 } &&
!pkmn.nature.stat_changes.any? { |change| change[0] == :DEFENSE && change[1] < 0 }
ret.push(BattleRole::PHYSICAL_WALL) if pkmn.ev[:DEFENSE] == Pokemon::EV_STAT_LIMIT
ret.push(:physical_wall) if pkmn.ev[:DEFENSE] == Pokemon::EV_STAT_LIMIT
elsif pkmn.nature.stat_changes.any? { |change| change[0] == :SPECIAL_DEFENSE && change[1] > 0 } &&
!pkmn.nature.stat_changes.any? { |change| change[0] == :SPECIAL_DEFENSE && change[1] < 0 }
ret.push(BattleRole::SPECIAL_WALL) if pkmn.ev[:SPECIAL_DEFENSE] == Pokemon::EV_STAT_LIMIT
ret.push(:special_wall) if pkmn.ev[:SPECIAL_DEFENSE] == Pokemon::EV_STAT_LIMIT
end
else
ret.push(BattleRole::TANK) if pkmn.ev[:HP] == Pokemon::EV_STAT_LIMIT
ret.push(:tank) if pkmn.ev[:HP] == Pokemon::EV_STAT_LIMIT
end
# Check for abilities indicative of particular roles
case pkmn.ability_id
when :REGENERATOR
ret.push(BattleRole::PIVOT)
ret.push(:pivot)
when :GUTS, :QUICKFEET, :FLAREBOOST, :TOXICBOOST, :NATURALCURE, :MAGICGUARD,
:MAGICBOUNCE, :HYDRATION
ret.push(BattleRole::STATUS_ABSORBER)
ret.push(:status_absorber)
when :SHADOWTAG, :ARENATRAP, :MAGNETPULL
ret.push(BattleRole::TRAPPER)
ret.push(:trapper)
when :DROUGHT, :DRIZZLE, :SANDSTREAM, :SNOWWARNING, :PRIMORDIALSEA,
:DESOLATELAND, :DELTASTREAM
ret.push(BattleRole::WEATHER_SETTER)
ret.push(:weather_setter)
when :GRASSYSURGE, :ELECTRICSURGE, :MISTYSURGE, :PSYCHICSURGE
ret.push(BattleRole::FIELD_SETTER)
ret.push(:field_setter)
end
# Check for items indicative of particular roles
case pkmn.item_id
when :LIGHTCLAY
ret.push(BattleRole::SCREENER)
ret.push(:screener)
when :ASSAULTVEST
ret.push(BattleRole::TANK)
ret.push(:tank)
when :CHOICEBAND, :CHOICESPECS
ret.push(BattleRole::STALL_BREAKER)
ret.push(BattleRole::SWEEPER) if pkmn.ev[:SPEED] == Pokemon::EV_STAT_LIMIT
ret.push(:stall_breaker)
ret.push(:sweeper) if pkmn.ev[:SPEED] == Pokemon::EV_STAT_LIMIT
when :CHOICESCARF
ret.push(BattleRole::SWEEPER) if pkmn.ev[:SPEED] == Pokemon::EV_STAT_LIMIT
ret.push(:sweeper) if pkmn.ev[:SPEED] == Pokemon::EV_STAT_LIMIT
when :TOXICORB, :FLAMEORB
ret.push(BattleRole::STATUS_ABSORBER)
ret.push(:status_absorber)
when :TERRAINEXTENDER
ret.push(BattleRole::FIELD_SETTER)
ret.push(:field_setter)
end
# Check for position in team, level relative to other levels in team
partyStarts = @battle.pbPartyStarts(side)
if partyStarts.include?(index + 1) || index == @battle.pbParty(side).length - 1
ret.push(BattleRole::ACE)
ret.push(:ace) # Last in party, assumed to be the best Pokémon
else
ret.push(BattleRole::LEAD) if partyStarts.include?(index)
ret.push(:lead) if partyStarts.include?(index) # First in party
idxTrainer = @battle.pbGetOwnerIndexFromPartyIndex(side, index)
maxLevel = @battle.pbMaxLevelInTeam(side, idxTrainer)
if pkmn.level >= maxLevel
ret.push(BattleRole::SECOND)
ret.push(:second)
else
secondHighest = true
seenHigherLevel = false
@@ -146,10 +135,9 @@ class Battle::AI
end
# NOTE: There can be multiple "second"s if all their levels are equal
# and the highest in the team (and none are the ace).
ret.push(BattleRole::SECOND) if secondHighest
ret.push(:second) if secondHighest
end
end
return ret
end

View File

@@ -18,7 +18,8 @@
# ReserveLastPokemon (don't switch it in if possible)
# UsePokemonInOrder (uses earliest-listed Pokémon possible)
#
# TODO: Add more skill flags.
# Anti-skill flags are skill flags with "Anti" at the beginning. An "AntiXYZ"
# flag will negate the corresponding "XYZ" flag.
#===============================================================================
class Battle::AI::AITrainer
attr_reader :side, :trainer_index
@@ -74,7 +75,11 @@ class Battle::AI::AITrainer
def sanitize_skill_flags
# NOTE: Any skill flag which is shorthand for multiple other skill flags
# should be "unpacked" here.
# TODO: Have a bunch of "AntiX" flags that negate the corresponding "X" flags.
# Remove any skill flag "XYZ" if there is also an "AntiXYZ" skill flag
@skill_flags.each_with_index do |flag, i|
@skill_flags[i] = nil if @skill_flags.include?("Anti" + flag)
end
@skill_flags.compact!
end
def has_skill_flag?(flag)

View File

@@ -18,7 +18,7 @@ class Battle::AI::AIBattler
@party_index = battler.pokemonIndex
if @party_index != old_party_index
# TODO: Start of battle or Pokémon switched/shifted; recalculate roles,
# etc.
# etc. What is etc.?
end
end
@@ -87,7 +87,7 @@ class Battle::AI::AIBattler
ret += [self.totalhp / 8, 1].max if [:Sun, :HarshSun].include?(weather) && battler.takesIndirectDamage?
end
# Future Sight/Doom Desire
# TODO
# NOTE: Not worth estimating the damage from this.
# Wish
if @ai.battle.positions[@index].effects[PBEffects::Wish] == 1 && battler.canHeal?
ret -= @ai.battle.positions[@index].effects[PBEffects::WishAmount]
@@ -230,10 +230,13 @@ class Battle::AI::AIBattler
active_types = pbTypes(true)
return active_types.include?(GameData::Type.get(type).id)
end
alias pbHasType? has_type?
# TODO: Also make a def effectiveness_of_move_against_battler which calls
# pbCalcTypeModSingle instead of effectiveness_of_type_against_single_battler_type.
# Why?
# pbCalcTypeModSingle instead of effectiveness_of_type_against_single_battler_type,
# for moves with custom def pbCalcTypeMod, e.g. Freeze-Dry. When would
# that be used instead? Or rather, when would THIS method be used if
# that existed?
def effectiveness_of_type_against_battler(type, user = nil)
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
return ret if !type
@@ -394,164 +397,49 @@ class Battle::AI::AIBattler
#-----------------------------------------------------------------------------
# TODO: Add more items.
BASE_ITEM_RATINGS = {
:ADAMANTORB => 3,
:BLACKBELT => 2,
:BLACKGLASSES => 2,
:BLACKSLUDGE => -4,
:CHARCOAL => 2,
:CHOICEBAND => 4,
:CHOICESCARF => 4,
:CHOICESPECS => 4,
:DEEPSEATOOTH => 4,
:DRACOPLATE => 2,
:DRAGONFANG => 2,
:DREADPLATE => 2,
:EARTHPLATE => 2,
:FISTPLATE => 2,
:FLAMEORB => -4,
:FLAMEPLATE => 2,
:GRISEOUSORB => 3,
:HARDSTONE => 2,
:ICICLEPLATE => 2,
:INSECTPLATE => 2,
:IRONBALL => -4,
:IRONPLATE => 2,
:LAGGINGTAIL => -2,
:LEFTOVERS => 4,
:LIFEORB => 3,
:LIGHTBALL => 4,
:LUSTROUSORB => 3,
:MAGNET => 2,
:MEADOWPLATE => 2,
:METALCOAT => 2,
:METRONOME => 1,
:MINDPLATE => 2,
:MIRACLESEED => 2,
:MUSCLEBAND => 2,
:MYSTICWATER => 2,
:NEVERMELTICE => 2,
:ODDINCENSE => 2,
:PIXIEPLATE => 2,
:POISONBARB => 2,
:ROCKINCENSE => 2,
:ROSEINCENSE => 2,
:SEAINCENSE => 2,
:SHARPBEAK => 2,
:SILKSCARF => 2,
:SILVERPOWDER => 2,
:SKYPLATE => 2,
:SOFTSAND => 2,
:SOULDEW => 3,
:SPELLTAG => 2,
:SPLASHPLATE => 2,
:SPOOKYPLATE => 2,
:STICKYBARB => -2,
:STONEPLATE => 2,
:THICKCLUB => 4,
:TOXICORB => -4,
:TOXICPLATE => 2,
:TWISTEDSPOON => 2,
:WAVEINCENSE => 2,
:WISEGLASSES => 2,
:ZAPPLATE => 2
}
# Returns a value indicating how beneficial the given ability will be to this
# battler if it has it.
# Return values are typically between -10 and +10. 0 is indifferent, positive
# values mean this battler benefits, negative values mean this battler suffers.
# NOTE: This method assumes the ability isn't being negated. The calculations
# that call this method separately check for it being negated, because
# they need to do something special in that case.
def wants_ability?(ability = :NONE)
ability = ability.id if !ability.is_a?(Symbol) && ability.respond_to?("id")
# Get the base ability rating
ret = 0
Battle::AI::BASE_ABILITY_RATINGS.each_pair do |val, abilities|
next if !abilities.include?(ability)
ret = val
break
end
# Modify the rating based on ability-specific contexts
ret = Battle::AI::Handlers.modify_ability_ranking(ability, ret, self, @ai)
return ret
end
#-----------------------------------------------------------------------------
# Returns a value indicating how beneficial the given item will be to this
# battler if it is holding it.
# Return values are typically between -10 and +10. 0 is indifferent, positive
# values mean this battler benefits, negative values mean this battler suffers.
# TODO: This method shouldn't check for item effect negation, to work the same
# as def wants_ability?.
def wants_item?(item)
item = :NONE if !item
item = item.id if !item.is_a?(Symbol) && item.respond_to?("id")
return 0 if has_active_ability?(:KLUTZ)
# TODO: Unnerve, other item-negating effects.
ret = BASE_ITEM_RATINGS[item] || 0
# TODO: Add more context-sensitive modifications to the ratings from above.
# Should they be moved into a handler?
case item
when :ADAMANTORB
ret = 0 if !battler.isSpecies?(:DIALGA) || !has_damaging_move_of_type?(:DRAGON, :STEEL)
when :BLACKBELT, :BLACKGLASSES, :CHARCOAL, :DRAGONFANG, :HARDSTONE, :MAGNET,
:METALCOAT, :MIRACLESEED, :MYSTICWATER, :NEVERMELTICE, :POISONBARB,
:SHARPBEAK, :SILKSCARF, :SILVERPOWDER, :SOFTSAND, :SPELLTAG,
:TWISTEDSPOON,
:DRACOPLATE, :DREADPLATE, :EARTHPLATE, :FISTPLATE, :FLAMEPLATE,
:ICICLEPLATE, :INSECTPLATE, :IRONPLATE, :MEADOWPLATE, :MINDPLATE,
:PIXIEPLATE, :SKYPLATE, :SPLASHPLATE, :SPOOKYPLATE, :STONEPLATE,
:TOXICPLATE, :ZAPPLATE,
:ODDINCENSE, :ROCKINCENSE, :ROSEINCENSE, :SEAINCENSE, :WAVEINCENSE
boosted_type = {
:BLACKBELT => :FIGHTING,
:BLACKGLASSES => :DARK,
:CHARCOAL => :FIRE,
:DRAGONFANG => :DRAGON,
:HARDSTONE => :ROCK,
:MAGNET => :ELECTRIC,
:METALCOAT => :STEEL,
:MIRACLESEED => :GRASS,
:MYSTICWATER => :WATER,
:NEVERMELTICE => :ICE,
:POISONBARB => :POISON,
:SHARPBEAK => :FLYING,
:SILKSCARF => :NORMAL,
:SILVERPOWDER => :BUG,
:SOFTSAND => :GROUND,
:SPELLTAG => :GHOST,
:TWISTEDSPOON => :PSYCHIC,
:DRACOPLATE => :DRAGON,
:DREADPLATE => :DARK,
:EARTHPLATE => :GROUND,
:FISTPLATE => :FIGHTING,
:FLAMEPLATE => :FIRE,
:ICICLEPLATE => :ICE,
:INSECTPLATE => :BUG,
:IRONPLATE => :STEEL,
:MEADOWPLATE => :GRASS,
:MINDPLATE => :PSYCHIC,
:PIXIEPLATE => :FAIRY,
:SKYPLATE => :FLYING,
:SPLASHPLATE => :WATER,
:SPOOKYPLATE => :GHOST,
:STONEPLATE => :ROCK,
:TOXICPLATE => :POISON,
:ZAPPLATE => :ELECTRIC,
:ODDINCENSE => :PSYCHIC,
:ROCKINCENSE => :ROCK,
:ROSEINCENSE => :GRASS,
:SEAINCENSE => :WATER,
:WAVEINCENSE => :WATER
}[item]
ret = 0 if !has_damaging_move_of_type?(boosted_type)
when :BLACKSLUDGE
ret = 4 if has_type?(:POISON)
when :CHOICEBAND, :MUSCLEBAND
ret = 0 if !check_for_move { |m| m.physicalMove?(m.type) }
when :CHOICESPECS, :WISEGLASSES
ret = 0 if !check_for_move { |m| m.specialMove?(m.type) }
when :DEEPSEATOOTH
ret = 0 if !battler.isSpecies?(:CLAMPERL) || !check_for_move { |m| m.specialMove?(m.type) }
when :GRISEOUSORB
ret = 0 if !battler.isSpecies?(:GIRATINA) || !has_damaging_move_of_type?(:DRAGON, :GHOST)
when :IRONBALL
ret = 0 if has_move_with_function?("ThrowUserItemAtTarget")
when :LIGHTBALL
ret = 0 if !battler.isSpecies?(:PIKACHU) || !check_for_move { |m| m.damagingMove? }
when :LUSTROUSORB
ret = 0 if !battler.isSpecies?(:PALKIA) || !has_damaging_move_of_type?(:DRAGON, :WATER)
when :SOULDEW
if !battler.isSpecies?(:LATIAS) && !battler.isSpecies?(:LATIOS)
ret = 0
elsif Settings::SOUL_DEW_POWERS_UP_TYPES
ret = 0 if !has_damaging_move_of_type?(:PSYCHIC, :DRAGON)
else
ret -= 2 if !check_for_move { |m| m.specialMove?(m.type) } # Also boosts SpDef
end
when :THICKCLUB
ret = 0 if (!battler.isSpecies?(:CUBONE) && !battler.isSpecies?(:MAROWAK)) ||
!check_for_move { |m| m.physicalMove?(m.type) }
# Get the base item rating
ret = 0
Battle::AI::BASE_ITEM_RATINGS.each_pair do |val, items|
next if !items.include?(item)
ret = val
break
end
# Modify the rating based on item-specific contexts
ret = Battle::AI::Handlers.modify_item_ranking(item, ret, self, @ai)
# Prefer if this battler knows Fling and it will do a lot of damage/have an
# additional (negative) effect when flung
if item != :NONE && has_move_with_function?("ThrowUserItemAtTarget")
@@ -660,339 +548,6 @@ class Battle::AI::AIBattler
#-----------------------------------------------------------------------------
# These values are taken from the Complete-Fire-Red-Upgrade decomp here:
# https://github.com/Skeli789/Complete-Fire-Red-Upgrade/blob/f7f35becbd111c7e936b126f6328fc52d9af68c8/src/ability_battle_effects.c#L41
BASE_ABILITY_RATINGS = {
:ADAPTABILITY => 8,
:AERILATE => 8,
:AFTERMATH => 5,
:AIRLOCK => 5,
:ANALYTIC => 5,
:ANGERPOINT => 4,
:ANTICIPATION => 0,
:ARENATRAP => 9,
:AROMAVEIL => 3,
# :ASONECHILLINGNEIGH => 0,
# :ASONEGRIMNEIGH => 0,
:AURABREAK => 3,
:BADDREAMS => 4,
# :BALLFETCH => 0,
# :BATTERY => 0,
:BATTLEARMOR => 2,
:BATTLEBOND => 6,
:BEASTBOOST => 7,
:BERSERK => 5,
:BIGPECKS => 1,
:BLAZE => 5,
:BULLETPROOF => 7,
:CHEEKPOUCH => 4,
# :CHILLINGNEIGH => 0,
:CHLOROPHYLL => 6,
:CLEARBODY => 4,
:CLOUDNINE => 5,
:COLORCHANGE => 2,
:COMATOSE => 6,
:COMPETITIVE => 5,
:COMPOUNDEYES => 7,
:CONTRARY => 8,
:CORROSION => 5,
:COTTONDOWN => 3,
# :CURIOUSMEDICINE => 0,
:CURSEDBODY => 4,
:CUTECHARM => 2,
:DAMP => 2,
:DANCER => 5,
:DARKAURA => 6,
:DAUNTLESSSHIELD => 3,
:DAZZLING => 5,
:DEFEATIST => -1,
:DEFIANT => 5,
:DELTASTREAM => 10,
:DESOLATELAND => 10,
:DISGUISE => 8,
:DOWNLOAD => 7,
:DRAGONSMAW => 8,
:DRIZZLE => 9,
:DROUGHT => 9,
:DRYSKIN => 6,
:EARLYBIRD => 4,
:EFFECTSPORE => 4,
:ELECTRICSURGE => 8,
:EMERGENCYEXIT => 3,
:FAIRYAURA => 6,
:FILTER => 6,
:FLAMEBODY => 4,
:FLAREBOOST => 5,
:FLASHFIRE => 6,
:FLOWERGIFT => 4,
# :FLOWERVEIL => 0,
:FLUFFY => 5,
:FORECAST => 6,
:FOREWARN => 0,
# :FRIENDGUARD => 0,
:FRISK => 0,
:FULLMETALBODY => 4,
:FURCOAT => 7,
:GALEWINGS => 6,
:GALVANIZE => 8,
:GLUTTONY => 3,
:GOOEY => 5,
:GORILLATACTICS => 4,
:GRASSPELT => 2,
:GRASSYSURGE => 8,
# :GRIMNEIGH => 0,
:GULPMISSLE => 3,
:GUTS => 6,
:HARVEST => 5,
# :HEALER => 0,
:HEATPROOF => 5,
:HEAVYMETAL => -1,
# :HONEYGATHER => 0,
:HUGEPOWER => 10,
:HUNGERSWITCH => 2,
:HUSTLE => 7,
:HYDRATION => 4,
:HYPERCUTTER => 3,
:ICEBODY => 3,
:ICEFACE => 4,
:ICESCALES => 7,
# :ILLUMINATE => 0,
:ILLUSION => 8,
:IMMUNITY => 4,
:IMPOSTER => 9,
:INFILTRATOR => 6,
:INNARDSOUT => 5,
:INNERFOCUS => 2,
:INSOMNIA => 4,
:INTIMIDATE => 7,
:INTREPIDSWORD => 3,
:IRONBARBS => 6,
:IRONFIST => 6,
:JUSTIFIED => 4,
:KEENEYE => 1,
:KLUTZ => -1,
:LEAFGUARD => 2,
:LEVITATE => 7,
:LIBERO => 8,
:LIGHTMETAL => 2,
:LIGHTNINGROD => 7,
:LIMBER => 3,
:LIQUIDOOZE => 3,
:LIQUIDVOICE => 5,
:LONGREACH => 3,
:MAGICBOUNCE => 9,
:MAGICGUARD => 9,
:MAGICIAN => 3,
:MAGMAARMOR => 1,
:MAGNETPULL => 9,
:MARVELSCALE => 5,
:MEGALAUNCHER => 7,
:MERCILESS => 4,
:MIMICRY => 2,
# :MINUS => 0,
:MIRRORARMOR => 6,
:MISTYSURGE => 8,
:MOLDBREAKER => 7,
:MOODY => 10,
:MOTORDRIVE => 6,
:MOXIE => 7,
:MULTISCALE => 8,
:MULTITYPE => 8,
:MUMMY => 5,
:NATURALCURE => 7,
:NEUROFORCE => 6,
:NEUTRALIZINGGAS => 5,
:NOGUARD => 8,
:NORMALIZE => -1,
:OBLIVIOUS => 2,
:OVERCOAT => 5,
:OVERGROW => 5,
:OWNTEMPO => 3,
:PARENTALBOND => 10,
:PASTELVEIL => 4,
:PERISHBODY => -1,
:PICKPOCKET => 3,
:PICKUP => 1,
:PIXILATE => 8,
# :PLUS => 0,
:POISONHEAL => 8,
:POISONPOINT => 4,
:POISONTOUCH => 4,
:POWERCONSTRUCT => 10,
# :POWEROFALCHEMY => 0,
:POWERSPOT => 2,
:PRANKSTER => 8,
:PRESSURE => 5,
:PRIMORDIALSEA => 10,
:PRISMARMOR => 6,
:PROPELLORTAIL => 2,
:PROTEAN => 8,
:PSYCHICSURGE => 8,
:PUNKROCK => 2,
:PUREPOWER => 10,
:QUEENLYMAJESTY => 6,
# :QUICKDRAW => 0,
:QUICKFEET => 5,
:RAINDISH => 3,
:RATTLED => 3,
# :RECEIVER => 0,
:RECKLESS => 6,
:REFRIGERATE => 8,
:REGENERATOR => 8,
:RIPEN => 4,
:RIVALRY => 1,
:RKSSYSTEM => 8,
:ROCKHEAD => 5,
:ROUGHSKIN => 6,
# :RUNAWAY => 0,
:SANDFORCE => 4,
:SANDRUSH => 6,
:SANDSPIT => 5,
:SANDSTREAM => 9,
:SANDVEIL => 3,
:SAPSIPPER => 7,
:SCHOOLING => 6,
:SCRAPPY => 6,
:SCREENCLEANER => 3,
:SERENEGRACE => 8,
:SHADOWSHIELD => 8,
:SHADOWTAG => 10,
:SHEDSKIN => 7,
:SHEERFORCE => 8,
:SHELLARMOR => 2,
:SHIELDDUST => 5,
:SHIELDSDOWN => 6,
:SIMPLE => 8,
:SKILLLINK => 7,
:SLOWSTART => -2,
:SLUSHRUSH => 5,
:SNIPER => 3,
:SNOWCLOAK => 3,
:SNOWWARNING => 8,
:SOLARPOWER => 3,
:SOLIDROCK => 6,
:SOULHEART => 7,
:SOUNDPROOF => 4,
:SPEEDBOOST => 9,
:STAKEOUT => 6,
:STALL => -1,
:STALWART => 2,
:STAMINA => 6,
:STANCECHANGE => 10,
:STATIC => 4,
:STEADFAST => 2,
:STEAMENGINE => 3,
:STEELWORKER => 6,
:STEELYSPIRIT => 2,
:STENCH => 1,
:STICKYHOLD => 3,
:STORMDRAIN => 7,
:STRONGJAW => 6,
:STURDY => 6,
:SUCTIONCUPS => 2,
:SUPERLUCK => 3,
:SURGESURFER => 4,
:SWARM => 5,
:SWEETVEIL => 4,
:SWIFTSWIM => 6,
# :SYMBIOSIS => 0,
:SYNCHRONIZE => 4,
:TANGLEDFEET => 2,
:TANGLINGHAIR => 5,
:TECHNICIAN => 8,
# :TELEPATHY => 0,
:TERAVOLT => 7,
:THICKFAT => 7,
:TINTEDLENS => 7,
:TORRENT => 5,
:TOUGHCLAWS => 7,
:TOXICBOOST => 6,
:TRACE => 6,
:TRANSISTOR => 8,
:TRIAGE => 7,
:TRUANT => -2,
:TURBOBLAZE => 7,
:UNAWARE => 6,
:UNBURDEN => 7,
:UNNERVE => 3,
# :UNSEENFIST => 0,
:VICTORYSTAR => 6,
:VITALSPIRIT => 4,
:VOLTABSORB => 7,
:WANDERINGSPIRIT => 2,
:WATERABSORB => 7,
:WATERBUBBLE => 8,
:WATERCOMPACTION => 4,
:WATERVEIL => 4,
:WEAKARMOR => 2,
:WHITESMOKE => 4,
:WIMPOUT => 3,
:WONDERGUARD => 10,
:WONDERSKIN => 4,
:ZENMODE => -1
}
# Returns a value indicating how beneficial the given ability will be to this
# battler if it has it.
# Return values are typically between -10 and +10. 0 is indifferent, positive
# values mean this battler benefits, negative values mean this battler suffers.
# NOTE: This method assumes the ability isn't being negated. The calculations
# that call this method separately check for it being negated, because
# they need to do something special in that case.
def wants_ability?(ability = :NONE)
ability = ability.id if !ability.is_a?(Symbol) && ability.respond_to?("id")
ret = BASE_ABILITY_RATINGS[ability] || 0
# TODO: Add more context-sensitive modifications to the ratings from above.
# Should they be moved into a handler?
case ability
when :BLAZE
ret = 0 if !has_damaging_move_of_type?(:FIRE)
when :CUTECHARM, :RIVALRY
ret = 0 if gender == 2
when :FRIENDGUARD, :HEALER, :SYMBOISIS, :TELEPATHY
has_ally = false
@ai.each_ally(@side) { |b, i| has_ally = true }
ret = 0 if !has_ally
when :GALEWINGS
ret = 0 if !check_for_move { |m| m.type == :FLYING }
when :HUGEPOWER, :PUREPOWER
ret = 0 if !@ai.stat_raise_worthwhile?(self, :ATTACK, true)
when :IRONFIST
ret = 0 if !check_for_move { |m| m.punchingMove? }
when :LIQUIDVOICE
ret = 0 if !check_for_move { |m| m.soundMove? }
when :MEGALAUNCHER
ret = 0 if !check_for_move { |m| m.pulseMove? }
when :OVERGROW
ret = 0 if !has_damaging_move_of_type?(:GRASS)
when :PRANKSTER
ret = 0 if !check_for_move { |m| m.statusMove? }
when :PUNKROCK
ret = 1 if !check_for_move { |m| m.damagingMove? && m.soundMove? }
when :RECKLESS
ret = 0 if !check_for_move { |m| m.recoilMove? }
when :ROCKHEAD
ret = 0 if !check_for_move { |m| m.recoilMove? && !m.is_a?(Battle::Move::CrashDamageIfFailsUnusableInGravity) }
when :RUNAWAY
ret = 0 if wild?
when :SANDFORCE
ret = 2 if !has_damaging_move_of_type?(:GROUND, :ROCK, :STEEL)
when :SKILLLINK
ret = 0 if !check_for_move { |m| m.is_a?(Battle::Move::HitTwoToFiveTimes) }
when :STEELWORKER
ret = 0 if !has_damaging_move_of_type?(:STEEL)
when :SWARM
ret = 0 if !has_damaging_move_of_type?(:BUG)
when :TORRENT
ret = 0 if !has_damaging_move_of_type?(:WATER)
when :TRIAGE
ret = 0 if !check_for_move { |m| m.healingMove? }
end
return ret
end
#-----------------------------------------------------------------------------
private
def effectiveness_of_type_against_single_battler_type(type, defend_type, user = nil)

View File

@@ -17,29 +17,13 @@ class Battle::AI::AIMove
#-----------------------------------------------------------------------------
# pp
# totalpp
# priority
# usableWhenAsleep?
# thawsUser?
# flinchingMove?
# tramplesMinimize?
# hitsFlyingTargets?
# canMagicCoat?
# soundMove?
# bombMove?
# powderMove?
# ignoresSubstitute?
# highCriticalRate?
# ignoresReflect?
def id; return @move.id; end
def name; return @move.name; end
def id; return @move.id; end
def name; return @move.name; end
def physicalMove?(thisType = nil); return @move.physicalMove?(thisType); end
def specialMove?(thisType = nil); return @move.specialMove?(thisType); end
def damagingMove?; return @move.damagingMove?; end
def statusMove?; return @move.statusMove?; end
def function; return @move.function; end
def damagingMove?; return @move.damagingMove?; end
def statusMove?; return @move.statusMove?; end
def function; return @move.function; end
#-----------------------------------------------------------------------------
@@ -177,7 +161,7 @@ class Battle::AI::AIMove
when :SNIPER
multipliers[:final_damage_multiplier] *= 1.5 if is_critical
when :STAKEOUT
# TODO: multipliers[:attack_multiplier] *= 2 if target switches out
# NOTE: Can't predict whether the target will switch out this round.
when :TINTEDLENS
if Effectiveness.resistant_type?(calc_type, *target.pbTypes(true))
multipliers[:final_damage_multiplier] *= 2
@@ -246,8 +230,7 @@ class Battle::AI::AIMove
if user.has_active_ability?(:PARENTALBOND)
multipliers[:power_multiplier] *= (Settings::MECHANICS_GENERATION >= 7) ? 1.25 : 1.5
end
# Me First
# TODO
# Me First - n/a because can't predict the move Me First will use
# Helping Hand - n/a
# Charge
if @ai.trainer.medium_skill? &&
@@ -376,10 +359,10 @@ class Battle::AI::AIMove
if @ai.trainer.medium_skill? && target.effects[PBEffects::Minimize] && @move.tramplesMinimize?
multipliers[:final_damage_multiplier] *= 2
end
# Move-specific base damage modifiers
# TODO
# Move-specific final damage modifiers
# TODO
# NOTE: No need to check pbBaseDamageMultiplier, as it's already accounted
# for in an AI's MoveBasePower handler or can't be checked now anyway.
# NOTE: No need to check pbModifyDamage, as it's already accounted for in an
# AI's MoveBasePower handler.
##### Main damage calculation #####
base_dmg = [(base_dmg * multipliers[:power_multiplier]).round, 1].max
atk = [(atk * multipliers[:attack_multiplier]).round, 1].max

View File

@@ -138,10 +138,10 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("FailsIfUserDamagedThisTu
#===============================================================================
Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("FailsIfTargetActed",
proc { |score, move, user, target, ai, battle|
# Check whether user is faster than its foe(s) and could use this move
# Check whether user is faster than the target and could use this move
next Battle::AI::MOVE_USELESS_SCORE if target.faster_than?(user)
# TODO: Predict the target switching/using an item.
# TODO: Predict the target using a damaging move or Me First.
# Check whether the target has any damaging moves it could use
next Battle::AI::MOVE_USELESS_SCORE if !target.check_for_move { |m| m.damagingMove? }
# Don't risk using this move if target is weak
if ai.trainer.has_skill_flag?("HPAware")
score -= 10 if target.hp <= target.totalhp / 2
@@ -157,7 +157,7 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("FailsIfTargetActed",
Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("CrashDamageIfFailsUnusableInGravity",
proc { |score, move, user, target, ai, battle|
if user.battler.takesIndirectDamage?
score -= (0.4 * (100 - move.rough_accuracy)).to_i # -0 (100%) to -40 (1%)
score -= (0.6 * (100 - move.rough_accuracy)).to_i # -0 (100%) to -60 (1%)
end
next score
}
@@ -367,17 +367,9 @@ Battle::AI::Handlers::MoveEffectScore.add("AddSpikesToFoeSide",
battle.pbParty(user.idxOpposingSide).each_with_index do |pkmn, idxParty|
next if !pkmn || !pkmn.able? || inBattleIndices.include?(idxParty)
if ai.trainer.medium_skill?
# Check affected by entry hazard
next if pkmn.hasItem?(:HEAVYDUTYBOOTS)
# Check can take indirect damage
next if ai.pokemon_airborne?(pkmn)
next if pkmn.hasAbility?(:MAGICGUARD)
# Check airborne
if !pkmn.hasItem?(:IRONBALL) &&
battle.field.effects[PBEffects::Gravity] == 0
next if pkmn.hasType?(:FLYING)
next if pkmn.hasAbility?(:LEVITATE)
next if pkmn.hasItem?(:AIRBALLOON)
end
end
foe_reserves.push(pkmn) # pkmn will be affected by Spikes
end
@@ -403,25 +395,9 @@ Battle::AI::Handlers::MoveEffectScore.add("AddToxicSpikesToFoeSide",
battle.pbParty(user.idxOpposingSide).each_with_index do |pkmn, idxParty|
next if !pkmn || !pkmn.able? || inBattleIndices.include?(idxParty)
if ai.trainer.medium_skill?
# Check affected by entry hazard
next if pkmn.hasItem?(:HEAVYDUTYBOOTS)
# Check pkmn's immunity to being poisoned
next if battle.field.terrain == :Misty
next if pkmn.hasType?(:POISON)
next if pkmn.hasType?(:STEEL)
next if pkmn.hasAbility?(:IMMUNITY)
next if pkmn.hasAbility?(:PASTELVEIL)
next if pkmn.hasAbility?(:FLOWERVEIL) && pkmn.hasType?(:GRASS)
next if pkmn.hasAbility?(:LEAFGUARD) && [:Sun, :HarshSun].include?(battle.pbWeather)
next if pkmn.hasAbility?(:COMATOSE) && pkmn.isSpecies?(:KOMALA)
next if pkmn.hasAbility?(:SHIELDSDOWN) && pkmn.isSpecies?(:MINIOR) && pkmn.form < 7
# Check airborne
if !pkmn.hasItem?(:IRONBALL) &&
battle.field.effects[PBEffects::Gravity] == 0
next if pkmn.hasType?(:FLYING)
next if pkmn.hasAbility?(:LEVITATE)
next if pkmn.hasItem?(:AIRBALLOON)
end
next if ai.pokemon_airborne?(pkmn)
next if !ai.pokemon_can_be_poisoned?(pkmn)
end
foe_reserves.push(pkmn) # pkmn will be affected by Toxic Spikes
end
@@ -447,9 +423,7 @@ Battle::AI::Handlers::MoveEffectScore.add("AddStealthRocksToFoeSide",
battle.pbParty(user.idxOpposingSide).each_with_index do |pkmn, idxParty|
next if !pkmn || !pkmn.able? || inBattleIndices.include?(idxParty)
if ai.trainer.medium_skill?
# Check affected by entry hazard
next if pkmn.hasItem?(:HEAVYDUTYBOOTS)
# Check can take indirect damage
next if pkmn.hasAbility?(:MAGICGUARD)
end
foe_reserves.push(pkmn) # pkmn will be affected by Stealth Rock
@@ -475,15 +449,8 @@ Battle::AI::Handlers::MoveEffectScore.add("AddStickyWebToFoeSide",
battle.pbParty(user.idxOpposingSide).each_with_index do |pkmn, idxParty|
next if !pkmn || !pkmn.able? || inBattleIndices.include?(idxParty)
if ai.trainer.medium_skill?
# Check affected by entry hazard
next if pkmn.hasItem?(:HEAVYDUTYBOOTS)
# Check airborne
if !pkmn.hasItem?(:IRONBALL) &&
battle.field.effects[PBEffects::Gravity] == 0
next if pkmn.hasType?(:FLYING)
next if pkmn.hasAbility?(:LEVITATE)
next if pkmn.hasItem?(:AIRBALLOON)
end
next if ai.pokemon_airborne?(pkmn)
end
foe_reserves.push(pkmn) # pkmn will be affected by Sticky Web
end

View File

@@ -516,7 +516,9 @@ Battle::AI::Handlers::MoveEffectScore.add("CureUserPartyStatus",
proc { |score, move, user, ai, battle|
score = Battle::AI::MOVE_BASE_SCORE # Ignore the scores for each targeted battler calculated earlier
battle.pbParty(user.index).each do |pkmn|
score += 12 if pkmn && pkmn.status != :NONE
next if !pkmn || pkmn.status == :NONE
next if pkmn.status == :SLEEP && pkmn.statusCount == 1 # About to wake up
score += 12
end
next score
}
@@ -1071,6 +1073,7 @@ Battle::AI::Handlers::MoveFailureAgainstTargetCheck.add("NegateTargetAbility",
)
Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("NegateTargetAbility",
proc { |score, move, user, target, ai, battle|
next Battle::AI::MOVE_USELESS_SCORE if !target.ability_active?
target_ability_rating = target.wants_ability?(target.ability_id)
side_mult = (target.opposes?(user)) ? 1 : -1
if target_ability_rating > 0
@@ -1087,8 +1090,8 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("NegateTargetAbility",
#===============================================================================
Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("NegateTargetAbilityIfTargetActed",
proc { |score, move, user, target, ai, battle|
next score if target.effects[PBEffects::Substitute] > 0 || target.effects[PBEffects::GastroAcid]
next score if target.battler.unstoppableAbility?
next score if target.effects[PBEffects::Substitute] > 0
next score if target.battler.unstoppableAbility? || !target.ability_active?
next score if user.faster_than?(target)
target_ability_rating = target.wants_ability?(target.ability_id)
side_mult = (target.opposes?(user)) ? 1 : -1

View File

@@ -559,7 +559,6 @@ Battle::AI::Handlers::MoveEffectScore.add("UserEnduresFaintingThisTurn",
# Don't prefer if the user used a protection move last turn, making this one
# less likely to work
score -= (user.effects[PBEffects::ProtectRate] - 1) * 8
# TODO: Check for combos with Flail/Endeavor?
next score
}
)
@@ -1353,7 +1352,8 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("EnsureNextMoveAlwaysHits
score += 8 if acc <= 50 && acc != 0
score += 8 if m.is_a?(Battle::Move::OHKO)
end
# TODO: Prefer if target has increased evasion.
# Prefer if the target has increased evasion
score += 5 * target.stages[:EVASION] if target.stages[:EVASION] > 0
# Not worth it if the user or the target is at low HP
if ai.trainer.has_skill_flag?("HPAware")
score -= 10 if user.hp < user.totalhp / 2

View File

@@ -15,7 +15,6 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("HitTwoTimes",
num_hits = move.move.pbNumHits(user.battler, [target.battler])
score += 10 if target.effects[PBEffects::Substitute] < dmg * (num_hits - 1) / num_hits
end
# TODO: Consider effects that trigger per hit.
next score
}
)
@@ -80,7 +79,6 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("HitThreeTimesPowersUpWit
dmg = move.rough_damage
score += 10 if target.effects[PBEffects::Substitute] < dmg / 2
end
# TODO: Consider effects that trigger per hit.
next score
}
)
@@ -111,7 +109,6 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("HitTwoToFiveTimes",
num_hits = (user.has_active_ability?(:SKILLLINK)) ? 5 : 3 # 3 is about average
score += 10 if target.effects[PBEffects::Substitute] < dmg * (num_hits - 1) / num_hits
end
# TODO: Consider effects that trigger per hit.
next score
}
)
@@ -183,7 +180,6 @@ Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("HitOncePerUserTeamMember
end
score += 10 if target.effects[PBEffects::Substitute] < dmg * (num_hits - 1) / num_hits
end
# TODO: Consider effects that trigger per hit.
next score
}
)

View File

@@ -159,8 +159,9 @@ Battle::AI::Handlers::MoveFailureAgainstTargetCheck.add("StartTargetCannotUseIte
Battle::AI::Handlers::MoveEffectAgainstTargetScore.add("StartTargetCannotUseItem",
proc { |score, move, user, target, ai, battle|
next Battle::AI::MOVE_USELESS_SCORE if !target.item || !target.item_active?
# TODO: Useless if target's item cannot be negated or it has no effect.
# TODO: Useless if target's item cannot be negated.
item_score = target.wants_item?(target.item_id)
next Battle::AI::MOVE_USELESS_SCORE if item_score <= 0 # Item has no effect or is bad
score += item_score * 2
next score
}