More work on the AI, refactored stat stage multipliers

This commit is contained in:
Maruno17
2023-04-09 22:26:48 +01:00
parent 5d9cc71a99
commit a22c5ea89c
21 changed files with 618 additions and 651 deletions

View File

@@ -231,7 +231,6 @@ class Battle::AI
if targets
# Reset the base score for the move (each target will add its own score)
score = 0
# TODO: Distinguish between affected foes and affected allies?
affected_targets = 0
# Get a score for the move against each target in turn
orig_move = @move.move # In case move is Mirror Move and changes depending on the target

View File

@@ -62,7 +62,7 @@ class Battle::AI
# Calculate amount that stat will be raised by
increment = stat_changes[idx + 1]
increment *= 2 if !fixed_change && !@battle.moldBreaker && target.has_active_ability?(:SIMPLE)
increment = [increment, 6 - target.stages[stat]].min # The actual stages gained
increment = [increment, Battle::Battler::STAT_STAGE_MAXIMUM - target.stages[stat]].min # The actual stages gained
# Count this as a valid stat raise
real_stat_changes.push([stat, increment]) if increment > 0
end
@@ -98,8 +98,10 @@ class Battle::AI
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
# TODO: Exception if target knows Baton Pass/Stored Power?
return true if target.has_move_with_function?("SwitchOutUserPassOnEffects",
"PowerHigherWithUserPositiveStatStages")
case stat
when :ATTACK
return false if !target.check_for_move { |m| m.physicalMove?(m.type) &&
@@ -146,24 +148,18 @@ class Battle::AI
#=============================================================================
def get_target_stat_raise_score_generic(score, target, stat_changes, desire_mult = 1)
total_increment = stat_changes.sum { |change| change[1] }
# TODO: Just return if the target's foe is predicted to use a phazing move
# (one that switches the target out).
# TODO: Don't prefer if foe is faster than target and is predicted to deal
# lethal damage.
# TODO: Don't prefer if foe is slower than target but is predicted to be
# able to 2HKO the target.
# TODO: Prefer if foe is semi-invulnerable and target is faster (can't hit
# the foe anyway).
# Prefer if move is a status move and it's the user's first/second turn
if @user.turnCount < 2 && @move.statusMove?
score += total_increment * desire_mult * 5
end
# Prefer if user is at high HP, don't prefer if user is at low HP
if target.index != @user.index
score += total_increment * desire_mult * ((100 * @user.hp / @user.totalhp) - 50) / 8 # +6 to -6 per stage
if @trainer.has_skill_flag?("HPAware")
# Prefer if user is at high HP, don't prefer if user is at low HP
if target.index != @user.index
score += total_increment * desire_mult * ((100 * @user.hp / @user.totalhp) - 50) / 8 # +6 to -6 per stage
end
# 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
# 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
# TODO: Look at abilities that trigger upon stat raise. There are none.
return score
end
@@ -173,15 +169,16 @@ class Battle::AI
#=============================================================================
def get_target_stat_raise_score_one(score, target, stat, increment, desire_mult = 1)
# Figure out how much the stat will actually change by
stage_mul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8]
stage_div = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2]
max_stage = Battle::Battler::STAT_STAGE_MAXIMUM
stage_mul = Battle::Battler::STAT_STAGE_MULTIPLIERS
stage_div = Battle::Battler::STAT_STAGE_DIVISORS
if [:ACCURACY, :EVASION].include?(stat)
stage_mul = [3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9]
stage_div = [9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3]
stage_mul = Battle::Battler::ACC_EVA_STAGE_MULTIPLIERS
stage_div = Battle::Battler::ACC_EVA_STAGE_DIVISORS
end
old_stage = target.stages[stat]
new_stage = old_stage + increment
inc_mult = (stage_mul[new_stage + 6].to_f * stage_div[old_stage + 6]) / (stage_div[new_stage + 6] * stage_mul[old_stage + 6])
inc_mult = (stage_mul[new_stage + max_stage].to_f * stage_div[old_stage + max_stage]) / (stage_div[new_stage + max_stage] * stage_mul[old_stage + max_stage])
inc_mult -= 1
inc_mult *= desire_mult
# Stat-based score changes
@@ -304,7 +301,6 @@ class Battle::AI
# inversion does not happen if the move could target a foe but is targeting an
# ally, but only because it is inverted in def pbGetMoveScoreAgainstTarget
# instead.
# TODO: Revisit this method as parts may need rewriting.
#=============================================================================
def get_score_for_target_stat_drop(score, target, stat_changes, whole_effect = true,
fixed_change = false, ignore_contrary = false)
@@ -358,8 +354,8 @@ class Battle::AI
end
# Calculate amount that stat will be lowered by
decrement = stat_changes[idx + 1]
decrement *= 2 if !fixed_change && !@battle.moldBreaker && @user.has_active_ability?(:SIMPLE)
decrement = [decrement, 6 + target.stages[stat]].min # The actual stages lost
decrement *= 2 if !fixed_change && !@battle.moldBreaker && target.has_active_ability?(:SIMPLE)
decrement = [decrement, Battle::Battler::STAT_STAGE_MAXIMUM + target.stages[stat]].min # The actual stages lost
# Count this as a valid stat drop
real_stat_changes.push([stat, decrement]) if decrement > 0
end
@@ -390,12 +386,12 @@ class Battle::AI
# TODO: Make sure the move's actual damage category is taken into account,
# i.e. CategoryDependsOnHigherDamagePoisonTarget and
# CategoryDependsOnHigherDamageIgnoreTargetAbility.
# TODO: Revisit this method as parts may need rewriting.
#=============================================================================
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
@@ -437,46 +433,40 @@ class Battle::AI
#=============================================================================
# Make score changes based on the general concept of lowering stats at all.
# TODO: Revisit this method as parts may need rewriting.
# TODO: All comments in this method may be inaccurate.
#=============================================================================
def get_target_stat_drop_score_generic(score, target, stat_changes, desire_mult = 1)
total_decrement = stat_changes.sum { |change| change[1] }
# TODO: Just return if target is predicted to switch out (except via Baton Pass).
# TODO: Don't prefer if target is faster than user and is predicted to deal
# lethal damage.
# TODO: Don't prefer if target is slower than user but is predicted to be able
# to 2HKO user.
# TODO: Don't prefer if target is semi-invulnerable and user is faster.
# Prefer if move is a status move and it's the user's first/second turn
if @user.turnCount < 2 && @move.statusMove?
score += total_decrement * desire_mult * 5
end
# Prefer if user is at high HP, don't prefer if user is at low HP
if target.index != @user.index
score += total_decrement * desire_mult * ((100 * @user.hp / @user.totalhp) - 50) / 8 # +6 to -6 per stage
if @trainer.has_skill_flag?("HPAware")
# Prefer if user is at high HP, don't prefer if user is at low HP
if target.index != @user.index
score += total_decrement * desire_mult * ((100 * @user.hp / @user.totalhp) - 50) / 8 # +6 to -6 per stage
end
# 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
# 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
# TODO: Look at abilities that trigger upon stat lowering.
return score
end
#=============================================================================
# Make score changes based on the lowering of a specific stat.
# TODO: Revisit this method as parts may need rewriting.
#=============================================================================
def get_target_stat_drop_score_one(score, target, stat, decrement, desire_mult = 1)
# Figure out how much the stat will actually change by
stage_mul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8]
stage_div = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2]
max_stage = Battle::Battler::STAT_STAGE_MAXIMUM
stage_mul = Battle::Battler::STAT_STAGE_MULTIPLIERS
stage_div = Battle::Battler::STAT_STAGE_DIVISORS
if [:ACCURACY, :EVASION].include?(stat)
stage_mul = [3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9]
stage_div = [9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3]
stage_mul = Battle::Battler::ACC_EVA_STAGE_MULTIPLIERS
stage_div = Battle::Battler::ACC_EVA_STAGE_DIVISORS
end
old_stage = target.stages[stat]
new_stage = old_stage - decrement
dec_mult = (stage_mul[old_stage + 6].to_f * stage_div[new_stage + 6]) / (stage_div[old_stage + 6] * stage_mul[new_stage + 6])
dec_mult = (stage_mul[old_stage + max_stage].to_f * stage_div[new_stage + max_stage]) / (stage_div[old_stage + max_stage] * stage_mul[new_stage + max_stage])
dec_mult -= 1
dec_mult *= desire_mult
# Stat-based score changes

View File

@@ -1,27 +1,3 @@
# TODO: Check all lingering effects to see if the AI needs to adjust itself
# because of them.
#===============================================================================
#
#===============================================================================
# TODO:
# => Don't prefer damaging move if it won't KO, user has Stance Change and
# is in shield form, and user is slower than the target
# => Check memory for past damage dealt by a target's non-high priority move,
# and prefer move if user is slower than the target and another hit from
# the same amount will KO the user
# => Check memory for past damage dealt by a target's priority move, and don't
# prefer the move if user is slower than the target and can't move faster
# than it because of priority
# => Check memory for whether target has previously used Quick Guard, and
# don't prefer move if so
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
# Don't prefer hitting a wild shiny Pokémon.
#===============================================================================
@@ -37,233 +13,78 @@ Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:shiny_target,
)
#===============================================================================
# Adjust score based on how much damage it can deal.
# Prefer the move even more if it's predicted to do enough damage to KO the
# target.
# TODO: Review score modifier.
# => If target has previously used a move that will hurt the user by 30% of
# its current HP or more, moreso don't prefer a status move.
# => Include EOR damage in this?
# => Prefer move if it will KO the target (moreso if user is slower than target)
# Prefer Shadow moves (for flavour).
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:predicted_damage,
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:shadow_moves,
proc { |score, move, user, target, ai, battle|
if move.damagingMove?
dmg = move.rough_damage
if move.rough_type == :SHADOW
old_score = score
score += ([25.0 * dmg / target.hp, 30].min).to_i
PBDebug.log_score_change(score - old_score, "damaging move (predicted damage #{dmg} = #{100 * dmg / target.hp}% of target's HP)")
if dmg > target.hp * 1.1 # Predicted to KO the target
old_score = score
score += 10
PBDebug.log_score_change(score - old_score, "predicted to KO the target")
end
score += 10
PBDebug.log_score_change(score - old_score, "prefer using a Shadow move")
end
next score
}
)
#===============================================================================
# Account for accuracy of move.
# TODO: Review score modifier.
# If user is frozen, prefer a move that can thaw the user.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:move_accuracy,
proc { |score, move, user, target, ai, battle|
acc = move.rough_accuracy.to_i
if acc < 90
Battle::AI::Handlers::GeneralMoveScore.add(:thawing_move_when_frozen,
proc { |score, move, user, ai, battle|
if ai.trainer.medium_skill? && user.status == :FROZEN
old_score = score
score -= (0.2 * (100 - acc)).to_i # -2 (89%) to -19 (1%)
PBDebug.log_score_change(score - old_score, "accuracy (predicted #{acc}%)")
if move.move.thawsUser?
score += 20
PBDebug.log_score_change(score - old_score, "move will thaw the user")
elsif user.check_for_move { |m| m.thawsUser? }
score -= 20 # Don't prefer this move if user knows another move that thaws
PBDebug.log_score_change(score - old_score, "user knows another move will thaw it")
end
end
next score
}
)
#===============================================================================
# Don't prefer attacking the target if they'd be semi-invulnerable.
# TODO: Review score modifier.
# 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(:target_semi_invulnerable,
proc { |score, move, user, target, ai, battle|
# TODO: Also consider the move's priority compared to that of the move the
# target is using.
if move.rough_accuracy > 0 && user.faster_than?(target) &&
(target.battler.semiInvulnerable? || target.effects[PBEffects::SkyDrop] >= 0)
miss = true
miss = false if user.has_active_ability?(:NOGUARD) || target.has_active_ability?(:NOGUARD)
if ai.trainer.high_skill? && miss
# Knows what can get past semi-invulnerability
if target.effects[PBEffects::SkyDrop] >= 0 ||
target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSky",
"TwoTurnAttackInvulnerableInSkyParalyzeTarget",
"TwoTurnAttackInvulnerableInSkyTargetCannotAct")
miss = false if move.move.hitsFlyingTargets?
elsif target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderground")
miss = false if move.move.hitsDiggingTargets?
elsif target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderwater")
miss = false if move.move.hitsDivingTargets?
end
end
if miss
Battle::AI::Handlers::GeneralMoveScore.add(:priority_move_against_faster_target,
proc { |score, move, user, ai, battle|
if ai.trainer.high_skill? && target.faster_than?(user) && move.rough_priority(user) > 0
# User is at risk of being knocked out
if ai.trainer.has_skill_flag?("HPAware") && user.hp < user.totalhp / 3
old_score = score
score = Battle::AI::MOVE_USELESS_SCORE
PBDebug.log_score_change(score - old_score, "target is semi-invulnerable")
score += 8
PBDebug.log_score_change(score - old_score, "user at low HP and move has priority over faster target")
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Less prefer two-turn moves, as the foe can see it coming and prepare for
# it.
#===============================================================================
# If target is frozen, don't prefer moves that could thaw them.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:thawing_move_against_frozen_target,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && target.status == :FROZEN
if move.rough_type == :FIRE || (Settings::MECHANICS_GENERATION >= 6 && move.move.thawsUser?)
# Target is predicted to be knocked out by the move
if move.damaging_move? && move.rough_damage >= target.hp
old_score = score
score -= 20
PBDebug.log_score_change(score - old_score, "thaws the target")
score += 8
PBDebug.log_score_change(score - old_score, "target at low HP and move has priority over faster target")
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Prefer move if it has a high critical hit rate, critical hits are
# possible but not certain, and target has raised defences/user has
# lowered offences (Atk/Def or SpAtk/SpDef, whichever is relevant).
#===============================================================================
# Prefer flinching external effects (note that move effects which cause
# flinching are dealt with in the function code part of score calculation).
# TODO: Review score modifier.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:external_flinching_effects,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && move.damagingMove? && !move.move.flinchingMove?
if (battle.moldBreaker || !target.has_active_ability?([:INNERFOCUS, :SHIELDDUST])) &&
target.effects[PBEffects::Substitute] == 0
if user.has_active_item?([:KINGSROCK, :RAZORFANG]) ||
user.has_active_ability?(:STENCH)
old_score = score
score += 8
PBDebug.log_score_change(score - old_score, "flinching")
end
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer contact move if making contact with the target could
# trigger an effect that's bad for the user (Static, etc.).
# => Also check if target has previously used Spiky Shield/King's Shield/
# Baneful Bunker, and don't prefer move if so
#===============================================================================
#
#===============================================================================
# TODO: Prefer a contact move if making contact with the target could trigger
# an effect that's good for the user (Poison Touch/Pickpocket).
#===============================================================================
#
#===============================================================================
# TODO: Prefer a higher priority move if the user is slower than the foe(s) and
# the user is at risk of being knocked out. Consider whether the foe(s)
# have priority moves of their own? Limit this to prefer priority damaging
# moves?
#===============================================================================
# Don't prefer damaging moves if the target is Biding, unless the move will deal
# enough damage to KO the target before it retaliates (assuming the move is used
# repeatedly until the target retaliates). Doesn't do a score change if the user
# will be immune to Bide's damage.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:damaging_a_biding_target,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && target.effects[PBEffects::Bide] > 0 && move.damagingMove?
eff = user.effectiveness_of_type_against_battler(:NORMAL, target) # Bide is Normal type
if !Effectiveness.ineffective?(eff)
dmg = move.rough_damage
eor_dmg = target.rough_end_of_round_damage
hits_possible = target.effects[PBEffects::Bide] - 1
eor_dmg *= hits_possible
hits_possible += 1 if user.faster_than?(target)
if dmg * hits_possible + eor_dmg < target.hp * 1.05
old_score = score
score -= 20
PBDebug.log_score_change(score - old_score, "don't want to damage the Biding target")
end
end
end
next score
}
)
#===============================================================================
# Don't prefer damaging moves that will knock out the target if they are using
# Destiny Bond.
# TODO: Review score modifier.
# => Also don't prefer damaging moves if user is slower than the target, move
# is likely to be lethal, and target has previously used Destiny Bond
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:knocking_out_a_destiny_bonder,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && move.damagingMove? && target.effects[PBEffects::DestinyBond]
dmg = move.rough_damage
if dmg > target.hp * 1.05 # Predicted to KO the target
old_score = score
score -= 20
score -= 10 if battle.pbAbleNonActiveCount(user.idxOwnSide) == 0
PBDebug.log_score_change(score - old_score, "don't want to KO the Destiny Bonding target")
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer Fire-type moves if target has previously used Powder and is
# faster than the user.
#===============================================================================
#
#===============================================================================
# TODO: Check memory for whether target has previously used Ion Deluge, and
# don't prefer move if it's Normal-type and target is immune because
# of its ability (Lightning Rod, etc.).
#===============================================================================
# Don't prefer a move that can be Magic Coated if the target (or any foe if the
# move doesn't have a target) knows Magic Coat/has Magic Bounce.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:target_can_Magic_Coat_or_Bounce_move,
proc { |score, move, user, target, ai, battle|
# TODO: Modify the semiInvulnerable? check to only apply if the target will
# still be invulnerable when the user acts, i.e. compare speeds?
if move.statusMove? && move.move.canMagicCoat? &&
target.opposes?(user) && !target.battler.semiInvulnerable?
if move.statusMove? && move.move.canMagicCoat? && target.opposes?(user) &&
(target.faster_than?(user) || !target.battler.semiInvulnerable?)
old_score = score
if !battle.moldBreaker && target.has_active_ability?(:MAGICBOUNCE)
score = Battle::AI::MOVE_USELESS_SCORE
PBDebug.log_score_change(score - old_score, "useless because target will Magic Bounce it")
elsif target.has_move_with_function?("BounceBackProblemCausingStatusMoves")
elsif target.has_move_with_function?("BounceBackProblemCausingStatusMoves") &&
target.can_attack? && !target.battler.semiInvulnerable?
score -= 7
PBDebug.log_score_change(score - old_score, "target knows Magic Coat and could bounce it")
end
@@ -277,15 +98,13 @@ Battle::AI::Handlers::GeneralMoveScore.add(:any_foe_can_Magic_Coat_or_Bounce_mov
if move.statusMove? && move.move.canMagicCoat? && move.pbTarget(user.battler).num_targets == 0
old_score = score
ai.each_foe_battler(user.side) do |b, i|
# TODO: Modify the semiInvulnerable? check to only apply if the target
# will still be invulnerable when the user acts, i.e. compare
# speeds?
next if b.battler.semiInvulnerable?
next if user.faster_than?(b) && b.battler.semiInvulnerable?
if b.has_active_ability?(:MAGICBOUNCE) && !battle.moldBreaker
score = Battle::AI::MOVE_USELESS_SCORE
PBDebug.log_score_change(score - old_score, "useless because a foe will Magic Bounce it")
break
elsif b.has_move_with_function?("BounceBackProblemCausingStatusMoves")
elsif b.has_move_with_function?("BounceBackProblemCausingStatusMoves") &&
b.can_attack? && !b.battler.semiInvulnerable?
score -= 7
PBDebug.log_score_change(score - old_score, "a foe knows Magic Coat and could bounce it")
break
@@ -316,52 +135,6 @@ Battle::AI::Handlers::GeneralMoveScore.add(:any_battler_can_Snatch_move,
}
)
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
#===============================================================================
#
#===============================================================================
# TODO: Prefer Shadow moves (for flavour).
#===============================================================================
# If user is frozen, prefer a move that can thaw the user.
#===============================================================================
Battle::AI::Handlers::GeneralMoveScore.add(:thawing_move_when_frozen,
proc { |score, move, user, ai, battle|
if ai.trainer.medium_skill? && user.status == :FROZEN
old_score = score
if move.move.thawsUser?
score += 20
PBDebug.log_score_change(score - old_score, "move will thaw the user")
elsif user.check_for_move { |m| m.thawsUser? }
score -= 20 # Don't prefer this move if user knows another move that thaws
PBDebug.log_score_change(score - old_score, "user knows another move will thaw it")
end
end
next score
}
)
#===============================================================================
# Don't prefer a dancing move if the target has the Dancer ability.
#===============================================================================
Battle::AI::Handlers::GeneralMoveScore.add(:dance_move_against_dancer,
proc { |score, move, user, ai, battle|
if move.move.danceMove?
old_score = score
ai.each_foe_battler(user.side) do |b, i|
score -= 10 if b.has_active_ability?(:DANCER)
end
PBDebug.log_score_change(score - old_score, "don't want to use a dance move because a foe has Dancer")
end
next score
}
)
#===============================================================================
# Pick a good move for the Choice items.
# TODO: Review score modifier.
@@ -371,9 +144,9 @@ Battle::AI::Handlers::GeneralMoveScore.add(:good_move_for_choice_item,
if ai.trainer.medium_skill?
if user.has_active_item?([:CHOICEBAND, :CHOICESPECS, :CHOICESCARF]) ||
user.has_active_ability?(:GORILLATACTICS)
old_score = score
# Really don't prefer status moves (except Trick)
if move.statusMove? && move.function != "UserTargetSwapItems"
old_score = score
score -= 25
PBDebug.log_score_change(score - old_score, "don't want to be Choiced into a status move")
next score
@@ -400,7 +173,6 @@ Battle::AI::Handlers::GeneralMoveScore.add(:good_move_for_choice_item,
# Prefer damaging moves if the foe is down to their last Pokémon (opportunistic).
# Prefer damaging moves if the AI is down to its last Pokémon but the foe has
# more (desperate).
# TODO: Review score modifier.
#===============================================================================
Battle::AI::Handlers::GeneralMoveScore.add(:damaging_move_and_either_side_no_reserves,
proc { |score, move, user, ai, battle|
@@ -426,11 +198,225 @@ Battle::AI::Handlers::GeneralMoveScore.add(:damaging_move_and_either_side_no_res
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer a move that is stopped by Wide Guard if any foe has
# previously used Wide Guard.
# TODO: Don't prefer Fire-type moves if target has previously used Powder and is
# faster than the user.
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer sound move if user hasn't been Throat Chopped but a foe has
# previously used Throat Chop.
# TODO: Don't prefer Normal-type moves if target has previously used Ion Deluge
# and is immune to Electric moves.
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer a move that is stopped by Wide Guard if any foe has
# previously used Wide Guard.
#===============================================================================
# Don't prefer attacking the target if they'd be semi-invulnerable.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:target_semi_invulnerable,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && move.rough_accuracy > 0 &&
(target.battler.semiInvulnerable? || target.effects[PBEffects::SkyDrop] >= 0)
next score if user.has_active_ability?(:NOGUARD) || target.has_active_ability?(:NOGUARD)
priority = move.rough_priority
if priority > 0 || (priority == 0 && user.faster_than?(target)) # User goes first
miss = true
if ai.trainer.high_skill?
# Knows what can get past semi-invulnerability
if target.effects[PBEffects::SkyDrop] >= 0 ||
target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSky",
"TwoTurnAttackInvulnerableInSkyParalyzeTarget",
"TwoTurnAttackInvulnerableInSkyTargetCannotAct")
miss = false if move.move.hitsFlyingTargets?
elsif target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderground")
miss = false if move.move.hitsDiggingTargets?
elsif target.battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderwater")
miss = false if move.move.hitsDivingTargets?
end
end
if miss
old_score = score
score = Battle::AI::MOVE_USELESS_SCORE
PBDebug.log_score_change(score - old_score, "target is semi-invulnerable")
end
end
end
next score
}
)
#===============================================================================
# Account for accuracy of move.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:predicted_accuracy,
proc { |score, move, user, target, ai, battle|
acc = move.rough_accuracy.to_i
if acc < 90
old_score = score
score -= (0.25 * (100 - acc)).to_i # -2 (89%) to -24 (1%)
PBDebug.log_score_change(score - old_score, "accuracy (predicted #{acc}%)")
end
next score
}
)
#===============================================================================
# Adjust score based on how much damage it can deal.
# Prefer the move even more if it's predicted to do enough damage to KO the
# target.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:predicted_damage,
proc { |score, move, user, target, ai, battle|
if move.damagingMove?
dmg = move.rough_damage
old_score = score
if target.effects[PBEffects::Substitute] > 0
target_hp = target.effects[PBEffects::Substitute]
score += ([15.0 * dmg / target.effects[PBEffects::Substitute], 20].min).to_i
PBDebug.log_score_change(score - old_score, "damaging move (predicted damage #{dmg} = #{100 * dmg / target.hp}% of target's Substitute)")
else
score += ([25.0 * dmg / target.hp, 30].min).to_i
PBDebug.log_score_change(score - old_score, "damaging move (predicted damage #{dmg} = #{100 * dmg / target.hp}% of target's HP)")
if ai.trainer.has_skill_flag?("HPAware") && dmg > target.hp * 1.1 # Predicted to KO the target
old_score = score
score += 10
PBDebug.log_score_change(score - old_score, "predicted to KO the target")
end
end
end
next score
}
)
#===============================================================================
# Prefer flinching external effects (note that move effects which cause
# flinching are dealt with in the function code part of score calculation).
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:external_flinching_effects,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && move.damagingMove? && !move.move.flinchingMove? &&
user.faster_than?(target) && target.effects[PBEffects::Substitute] == 0
if user.has_active_item?([:KINGSROCK, :RAZORFANG]) ||
user.has_active_ability?(:STENCH)
if battle.moldBreaker || !target.has_active_ability?([:INNERFOCUS, :SHIELDDUST])
old_score = score
score += 8
PBDebug.log_score_change(score - old_score, "added chance to cause flinching")
end
end
end
next score
}
)
#===============================================================================
# If target is frozen, don't prefer moves that could thaw them.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:thawing_move_against_frozen_target,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && target.status == :FROZEN
if move.rough_type == :FIRE || (Settings::MECHANICS_GENERATION >= 6 && move.move.thawsUser?)
old_score = score
score -= 20
PBDebug.log_score_change(score - old_score, "thaws the target")
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Prefer a contact move if making contact with the target could trigger
# an effect that's good for the user (Poison Touch/Pickpocket).
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer contact move if making contact with the target could
# trigger an effect that's bad for the user (Static, etc.).
# => Also check if target has previously used Spiky Shield/King's Shield/
# Baneful Bunker, and don't prefer move if so
#===============================================================================
# Don't prefer damaging moves that will knock out the target if they are using
# Destiny Bond or Grudge.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:knocking_out_a_destiny_bonder_or_grudger,
proc { |score, move, user, target, ai, battle|
if (ai.trainer.has_skill_flag?("HPAware") || ai.trainer.high_skill?) && move.damagingMove? &&
(target.effects[PBEffects::DestinyBond] || target.effects[PBEffects::Grudge])
priority = move.rough_priority
if priority > 0 || (priority == 0 && user.faster_than?(target)) # User goes first
if move.rough_damage > target.hp * 1.1 # Predicted to KO the target
old_score = score
if target.effects[PBEffects::DestinyBond]
score -= 20
score -= 10 if battle.pbAbleNonActiveCount(user.idxOwnSide) == 0
PBDebug.log_score_change(score - old_score, "don't want to KO the Destiny Bonding target")
elsif target.effects[PBEffects::Grudge]
score -= 15
score -= 7 if battle.pbAbleNonActiveCount(user.idxOwnSide) == 0
PBDebug.log_score_change(score - old_score, "don't want to KO the Grudge-using target")
end
end
end
end
next score
}
)
#===============================================================================
#
#===============================================================================
# TODO: Don't prefer damaging moves if the target is using Rage and they benefit
# from the raised Attack.
#===============================================================================
# Don't prefer damaging moves if the target is Biding, unless the move will deal
# enough damage to KO the target before it retaliates (assuming the move is used
# repeatedly until the target retaliates). Doesn't do a score change if the user
# will be immune to Bide's damage.
#===============================================================================
Battle::AI::Handlers::GeneralMoveAgainstTargetScore.add(:damaging_a_biding_target,
proc { |score, move, user, target, ai, battle|
if ai.trainer.medium_skill? && target.effects[PBEffects::Bide] > 0 && move.damagingMove?
eff = user.effectiveness_of_type_against_battler(:NORMAL, target) # Bide is Normal type
if !Effectiveness.ineffective?(eff)
# Worth damaging the target if it can be knocked out before Bide ends
if ai.trainer.has_skill_flag?("HPAware")
dmg = move.rough_damage
eor_dmg = target.rough_end_of_round_damage
hits_possible = target.effects[PBEffects::Bide] - 1
eor_dmg *= hits_possible
hits_possible += 1 if user.faster_than?(target)
next score if dmg * hits_possible + eor_dmg > target.hp * 1.1
end
old_score = score
score -= 20
PBDebug.log_score_change(score - old_score, "don't want to damage the Biding target")
end
end
next score
}
)
#===============================================================================
# Don't prefer a dancing move if the target has the Dancer ability.
#===============================================================================
Battle::AI::Handlers::GeneralMoveScore.add(:dance_move_against_dancer,
proc { |score, move, user, ai, battle|
if move.move.danceMove?
old_score = score
ai.each_foe_battler(user.side) do |b, i|
score -= 10 if b.has_active_ability?(:DANCER)
end
PBDebug.log_score_change(score - old_score, "don't want to use a dance move because a foe has Dancer")
end
next score
}
)

View File

@@ -17,6 +17,8 @@
# ConsiderSwitching (can choose to switch out Pokémon)
# ReserveLastPokemon (don't switch it in if possible)
# UsePokemonInOrder (uses earliest-listed Pokémon possible)
#
# TODO: Add more skill flags.
#===============================================================================
class Battle::AI::AITrainer
attr_reader :side, :trainer_index
@@ -53,7 +55,6 @@ class Battle::AI::AITrainer
if @trainer
@trainer.flags.each { |flag| @skill_flags.push(flag) }
end
# TODO: Add skill flags depending on @skill.
if @skill > 0
@skill_flags.push("PredictMoveFailure")
@skill_flags.push("ScoreMoves")
@@ -74,7 +75,6 @@ class Battle::AI::AITrainer
# 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.
# TODO: Have flag "DontReserveLastPokemon" which negates "ReserveLastPokemon".
end
def has_skill_flag?(flag)

View File

@@ -15,34 +15,34 @@ class Battle::AI::AIBattler
def refresh_battler
old_party_index = @party_index
@battler = @ai.battle.battlers[@index]
@party_index = @battler.pokemonIndex
@party_index = battler.pokemonIndex
if @party_index != old_party_index
# TODO: Start of battle or Pokémon switched/shifted; recalculate roles,
# etc.
end
end
def pokemon; return @battler.pokemon; end
def level; return @battler.level; end
def hp; return @battler.hp; end
def totalhp; return @battler.totalhp; end
def fainted?; return @battler.fainted?; end
def status; return @battler.status; end
def statusCount; return @battler.statusCount; end
def gender; return @battler.gender; end
def turnCount; return @battler.turnCount; end
def effects; return @battler.effects; end
def stages; return @battler.stages; end
def statStageAtMax?(stat); return @battler.statStageAtMax?(stat); end
def statStageAtMin?(stat); return @battler.statStageAtMin?(stat); end
def moves; return @battler.moves; end
def pokemon; return battler.pokemon; end
def level; return battler.level; end
def hp; return battler.hp; end
def totalhp; return battler.totalhp; end
def fainted?; return battler.fainted?; end
def status; return battler.status; end
def statusCount; return battler.statusCount; end
def gender; return battler.gender; end
def turnCount; return battler.turnCount; end
def effects; return battler.effects; end
def stages; return battler.stages; end
def statStageAtMax?(stat); return battler.statStageAtMax?(stat); end
def statStageAtMin?(stat); return battler.statStageAtMin?(stat); end
def moves; return battler.moves; end
def wild?
return @ai.battle.wildBattle? && opposes?
end
def name
return sprintf("%s (%d)", @battler.name, @index)
return sprintf("%s (%d)", battler.name, @index)
end
def opposes?(other = nil)
@@ -50,10 +50,10 @@ class Battle::AI::AIBattler
return other.side != @side
end
def idxOwnSide; return @battler.idxOwnSide; end
def pbOwnSide; return @battler.pbOwnSide; end
def idxOpposingSide; return @battler.idxOpposingSide; end
def pbOpposingSide; return @battler.pbOpposingSide; end
def idxOwnSide; return battler.idxOwnSide; end
def pbOwnSide; return battler.pbOwnSide; end
def idxOpposingSide; return battler.idxOpposingSide; end
def pbOpposingSide; return battler.pbOpposingSide; end
#=============================================================================
@@ -63,44 +63,44 @@ class Battle::AI::AIBattler
# Future Sight/Doom Desire
# TODO
# Wish
if @ai.battle.positions[@index].effects[PBEffects::Wish] == 1 && @battler.canHeal?
if @ai.battle.positions[@index].effects[PBEffects::Wish] == 1 && battler.canHeal?
ret -= @ai.battle.positions[@index].effects[PBEffects::WishAmount]
end
# Sea of Fire
if @ai.battle.sides[@side].effects[PBEffects::SeaOfFire] > 1 &&
@battler.takesIndirectDamage? && !has_type?(:FIRE)
battler.takesIndirectDamage? && !has_type?(:FIRE)
ret += self.totalhp / 8
end
# Grassy Terrain (healing)
if @ai.battle.field.terrain == :Grassy && @battler.affectedByTerrain? && @battler.canHeal?
if @ai.battle.field.terrain == :Grassy && battler.affectedByTerrain? && battler.canHeal?
ret -= [battler.totalhp / 16, 1].max
end
# Leftovers/Black Sludge
if has_active_item?(:BLACKSLUDGE)
if has_type?(:POISON)
ret -= [battler.totalhp / 16, 1].max if @battler.canHeal?
ret -= [battler.totalhp / 16, 1].max if battler.canHeal?
else
ret += [battler.totalhp / 8, 1].max if @battler.takesIndirectDamage?
ret += [battler.totalhp / 8, 1].max if battler.takesIndirectDamage?
end
elsif has_active_item?(:LEFTOVERS)
ret -= [battler.totalhp / 16, 1].max if @battler.canHeal?
ret -= [battler.totalhp / 16, 1].max if battler.canHeal?
end
# Aqua Ring
if self.effects[PBEffects::AquaRing] && @battler.canHeal?
if self.effects[PBEffects::AquaRing] && battler.canHeal?
amt = battler.totalhp / 16
amt = (amt * 1.3).floor if has_active_item?(:BIGROOT)
ret -= [amt, 1].max
end
# Ingrain
if self.effects[PBEffects::Ingrain] && @battler.canHeal?
if self.effects[PBEffects::Ingrain] && battler.canHeal?
amt = battler.totalhp / 16
amt = (amt * 1.3).floor if has_active_item?(:BIGROOT)
ret -= [amt, 1].max
end
# Leech Seed
if self.effects[PBEffects::LeechSeed] >= 0
if @battler.takesIndirectDamage?
ret += [battler.totalhp / 8, 1].max if @battler.takesIndirectDamage?
if battler.takesIndirectDamage?
ret += [battler.totalhp / 8, 1].max if battler.takesIndirectDamage?
end
else
@ai.each_battler do |b, i|
@@ -111,31 +111,33 @@ class Battle::AI::AIBattler
end
end
# Hyper Mode (Shadow Pokémon)
# TODO
if battler.inHyperMode?
ret += [battler.totalhp / 24, 1].max
end
# Poison/burn/Nightmare
if self.status == :POISON
if has_active_ability?(:POISONHEAL)
ret -= [battler.totalhp / 8, 1].max if @battler.canHeal?
elsif @battler.takesIndirectDamage?
ret -= [battler.totalhp / 8, 1].max if battler.canHeal?
elsif battler.takesIndirectDamage?
mult = 2
mult = [self.effects[PBEffects::Toxic] + 1, 16].min if self.statusCount > 0 # Toxic
ret += [mult * battler.totalhp / 16, 1].max
end
elsif self.status == :BURN
if @battler.takesIndirectDamage?
if battler.takesIndirectDamage?
amt = (Settings::MECHANICS_GENERATION >= 7) ? self.totalhp / 16 : self.totalhp / 8
amt = (amt / 2.0).round if has_active_ability?(:HEATPROOF)
ret += [amt, 1].max
end
elsif @battler.asleep? && self.statusCount > 1 && self.effects[PBEffects::Nightmare]
ret += [battler.totalhp / 4, 1].max if @battler.takesIndirectDamage?
elsif battler.asleep? && self.statusCount > 1 && self.effects[PBEffects::Nightmare]
ret += [battler.totalhp / 4, 1].max if battler.takesIndirectDamage?
end
# Curse
if self.effects[PBEffects::Curse]
ret += [battler.totalhp / 4, 1].max if @battler.takesIndirectDamage?
ret += [battler.totalhp / 4, 1].max if battler.takesIndirectDamage?
end
# Trapping damage
if self.effects[PBEffects::Trapping] > 1 && @battler.takesIndirectDamage?
if self.effects[PBEffects::Trapping] > 1 && battler.takesIndirectDamage?
amt = (Settings::MECHANICS_GENERATION >= 6) ? self.totalhp / 8 : self.totalhp / 16
if @ai.battlers[self.effects[PBEffects::TrappingUser]].has_active_item?(:BINDINGBAND)
amt = (Settings::MECHANICS_GENERATION >= 6) ? self.totalhp / 6 : self.totalhp / 8
@@ -143,16 +145,16 @@ class Battle::AI::AIBattler
ret += [amt, 1].max
end
# Perish Song
# TODO
return 999_999 if self.effects[PBEffects::PerishSong] == 1
# Bad Dreams
if @battler.asleep? && self.statusCount > 1 && @battler.takesIndirectDamage?
if battler.asleep? && self.statusCount > 1 && battler.takesIndirectDamage?
@ai.each_battler do |b, i|
next if i == @index || !b.battler.near?(@battler) || !b.has_active_ability?(:BADDREAMS)
next if i == @index || !b.battler.near?(battler) || !b.has_active_ability?(:BADDREAMS)
ret += [battler.totalhp / 8, 1].max
end
end
# Sticky Barb
if has_active_item?(:STICKYBARB) && @battler.takesIndirectDamage?
if has_active_item?(:STICKYBARB) && battler.takesIndirectDamage?
ret += [battler.totalhp / 8, 1].max
end
return ret
@@ -163,22 +165,26 @@ class Battle::AI::AIBattler
def base_stat(stat)
ret = 0
case stat
when :ATTACK then ret = @battler.attack
when :DEFENSE then ret = @battler.defense
when :SPECIAL_ATTACK then ret = @battler.spatk
when :SPECIAL_DEFENSE then ret = @battler.spdef
when :SPEED then ret = @battler.speed
when :ATTACK then ret = battler.attack
when :DEFENSE then ret = battler.defense
when :SPECIAL_ATTACK then ret = battler.spatk
when :SPECIAL_DEFENSE then ret = battler.spdef
when :SPEED then ret = battler.speed
end
return ret
end
def rough_stat(stat)
return @battler.pbSpeed if stat == :SPEED && @ai.trainer.high_skill?
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[stat] + 6
return battler.pbSpeed if stat == :SPEED && @ai.trainer.high_skill?
stage_mul = Battle::Battler::STAT_STAGE_MULTIPLIERS
stage_div = Battle::Battler::STAT_STAGE_DIVISORS
if [:ACCURACY, :EVASION].include?(stat)
stage_mul = Battle::Battler::ACC_EVA_STAGE_MULTIPLIERS
stage_div = Battle::Battler::ACC_EVA_STAGE_DIVISORS
end
stage = battler.stages[stat] + Battle::Battler::STAT_STAGE_MAXIMUM
value = base_stat(stat)
return (value.to_f * stageMul[stage] / stageDiv[stage]).floor
return (value.to_f * stage_mul[stage] / stage_div[stage]).floor
end
def faster_than?(other)
@@ -190,8 +196,8 @@ class Battle::AI::AIBattler
#=============================================================================
def types; return @battler.types; end
def pbTypes(withExtraType = false); return @battler.pbTypes(withExtraType); end
def types; return battler.types; end
def pbTypes(withExtraType = false); return battler.pbTypes(withExtraType); end
def has_type?(type)
return false if !type
@@ -201,19 +207,20 @@ class Battle::AI::AIBattler
# TODO: Also make a def effectiveness_of_move_against_battler which calls
# pbCalcTypeModSingle instead of effectiveness_of_type_against_single_battler_type.
# Why?
def effectiveness_of_type_against_battler(type, user = nil)
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
return ret if !type
return ret if type == :GROUND && has_type?(:FLYING) && has_active_item?(:IRONBALL)
# Get effectivenesses
if type == :SHADOW
if @battler.shadowPokemon?
if battler.shadowPokemon?
ret = Effectiveness::NOT_VERY_EFFECTIVE_MULTIPLIER
else
ret = Effectiveness::SUPER_EFFECTIVE_MULTIPLIER
end
else
@battler.pbTypes(true).each do |defend_type|
battler.pbTypes(true).each do |defend_type|
ret *= effectiveness_of_type_against_single_battler_type(type, defend_type, user)
end
ret *= 2 if self.effects[PBEffects::TarShot] && type == :FIRE
@@ -223,39 +230,39 @@ class Battle::AI::AIBattler
#=============================================================================
def ability_id; return @battler.ability_id; end
def ability; return @battler.ability; end
def ability_id; return battler.ability_id; end
def ability; return battler.ability; end
def ability_active?
return @battler.abilityActive?
return battler.abilityActive?
end
def has_active_ability?(ability, ignore_fainted = false)
return @battler.hasActiveAbility?(ability, ignore_fainted)
return battler.hasActiveAbility?(ability, ignore_fainted)
end
def has_mold_breaker?
return @ai.move.function == "IgnoreTargetAbility" || @battler.hasMoldBreaker?
return @ai.move.function == "IgnoreTargetAbility" || battler.hasMoldBreaker?
end
#=============================================================================
def item_id; return @battler.item_id; end
def item; return @battler.item; end
def item_id; return battler.item_id; end
def item; return battler.item; end
def item_active?
return @battler.itemActive?
return battler.itemActive?
end
def has_active_item?(item)
return @battler.hasActiveItem?(item)
return battler.hasActiveItem?(item)
end
#=============================================================================
def check_for_move
ret = false
@battler.eachMove do |move|
battler.eachMove do |move|
next if move.pp == 0 && move.total_pp > 0
next unless yield move
ret = true
@@ -266,7 +273,7 @@ class Battle::AI::AIBattler
def has_damaging_move_of_type?(*types)
check_for_move do |m|
return true if m.damagingMove? && types.include?(m.pbCalcType(@battler))
return true if m.damagingMove? && types.include?(m.pbCalcType(battler))
end
return false
end
@@ -303,24 +310,24 @@ class Battle::AI::AIBattler
def can_become_trapped?
return false if fainted?
# Ability/item effects that allow switching no matter what
if ability_active? && Battle::AbilityEffects.triggerCertainSwitching(ability, @battler, @ai.battle)
if ability_active? && Battle::AbilityEffects.triggerCertainSwitching(ability, battler, @ai.battle)
return false
end
if item_active? && Battle::ItemEffects.triggerCertainSwitching(item, @battler, @ai.battle)
if item_active? && Battle::ItemEffects.triggerCertainSwitching(item, battler, @ai.battle)
return false
end
# Other certain switching effects
return false if Settings::MORE_TYPE_EFFECTS && has_type?(:GHOST)
# Other certain trapping effects
return false if @battler.trappedInBattle?
return false if battler.trappedInBattle?
# Trapping abilities/items
ai.each_foe_battler(side) do |b, i|
if b.ability_active? &&
Battle::AbilityEffects.triggerTrappingByTarget(b.ability, @battler, b.battler, @ai.battle)
Battle::AbilityEffects.triggerTrappingByTarget(b.ability, battler, b.battler, @ai.battle)
return false
end
if b.item_active? &&
Battle::ItemEffects.triggerTrappingByTarget(b.item, @battler, b.battler, @ai.battle)
Battle::ItemEffects.triggerTrappingByTarget(b.item, battler, b.battler, @ai.battle)
return false
end
end
@@ -432,9 +439,11 @@ class Battle::AI::AIBattler
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)
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,
@@ -493,17 +502,17 @@ class Battle::AI::AIBattler
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) }
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)
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? }
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)
ret = 0 if !battler.isSpecies?(:PALKIA) || !has_damaging_move_of_type?(:DRAGON, :WATER)
when :SOULDEW
if !@battler.isSpecies?(:LATIAS) && !@battler.isSpecies?(:LATIOS)
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)
@@ -511,7 +520,7 @@ class Battle::AI::AIBattler
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)) ||
ret = 0 if (!battler.isSpecies?(:CUBONE) && !battler.isSpecies?(:MAROWAK)) ||
!check_for_move { |m| m.physicalMove?(m.type) }
end
# Prefer if this battler knows Fling and it will do a lot of damage/have an
@@ -568,18 +577,18 @@ class Battle::AI::AIBattler
ret += (cured_status && status == cured_status) ? 6 : -6
when :PERSIMBERRY
# Confusion cure
ret += (effects[PBEffects::Confusion] > 1) ? 6 : -6
ret += (self.effects[PBEffects::Confusion] > 1) ? 6 : -6
when :LUMBERRY
# Any status/confusion cure
ret += (status != :NONE || effects[PBEffects::Confusion] > 1) ? 6 : -6
ret += (status != :NONE || self.effects[PBEffects::Confusion] > 1) ? 6 : -6
when :MENTALHERB
# Cure mental effects
if effects[PBEffects::Attract] >= 0 ||
effects[PBEffects::Taunt] > 1 ||
effects[PBEffects::Encore] > 1 ||
effects[PBEffects::Torment] ||
effects[PBEffects::Disable] > 1 ||
effects[PBEffects::HealBlock] > 1
if self.effects[PBEffects::Attract] >= 0 ||
self.effects[PBEffects::Taunt] > 1 ||
self.effects[PBEffects::Encore] > 1 ||
self.effects[PBEffects::Torment] ||
self.effects[PBEffects::Disable] > 1 ||
self.effects[PBEffects::HealBlock] > 1
ret += 6
else
ret -= 6
@@ -604,13 +613,13 @@ class Battle::AI::AIBattler
ret = ret * 3 / 2 if GameData::Item.get(item).is_berry? && has_active_ability?(:RIPEN)
when :WHITEHERB
# Resets lowered stats
ret += (@battler.hasLoweredStatStages?) ? 8 : -8
ret += (battler.hasLoweredStatStages?) ? 8 : -8
when :MICLEBERRY
# Raises accuracy of next move
ret += (@ai.stat_raise_worthwhile?(self, :ACCURACY, true)) ? 6 : -6
when :LANSATBERRY
# Focus energy
ret += (effects[PBEffects::FocusEnergy] < 2) ? 6 : -6
ret += (self.effects[PBEffects::FocusEnergy] < 2) ? 6 : -6
when :LEPPABERRY
# Restore PP
ret += 6
@@ -903,54 +912,53 @@ class Battle::AI::AIBattler
# 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")
# TODO: Ideally replace the above list of ratings with context-sensitive
# calculations. Should they all go in this method, or should there be
# more handlers for each ability?
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
return 0 if !has_damaging_move_of_type?(:FIRE)
ret = 0 if !has_damaging_move_of_type?(:FIRE)
when :CUTECHARM, :RIVALRY
return 0 if gender == 2
ret = 0 if gender == 2
when :FRIENDGUARD, :HEALER, :SYMBOISIS, :TELEPATHY
has_ally = false
each_ally(@side) { |b, i| has_ally = true }
return 0 if !has_ally
ret = 0 if !has_ally
when :GALEWINGS
return 0 if !check_for_move { |m| m.type == :FLYING }
ret = 0 if !check_for_move { |m| m.type == :FLYING }
when :HUGEPOWER, :PUREPOWER
return 0 if !ai.stat_raise_worthwhile?(self, :ATTACK, true)
ret = 0 if !ai.stat_raise_worthwhile?(self, :ATTACK, true)
when :IRONFIST
return 0 if !check_for_move { |m| m.punchingMove? }
ret = 0 if !check_for_move { |m| m.punchingMove? }
when :LIQUIDVOICE
return 0 if !check_for_move { |m| m.soundMove? }
ret = 0 if !check_for_move { |m| m.soundMove? }
when :MEGALAUNCHER
return 0 if !check_for_move { |m| m.pulseMove? }
ret = 0 if !check_for_move { |m| m.pulseMove? }
when :OVERGROW
return 0 if !has_damaging_move_of_type?(:GRASS)
ret = 0 if !has_damaging_move_of_type?(:GRASS)
when :PRANKSTER
return 0 if !check_for_move { |m| m.statusMove? }
ret = 0 if !check_for_move { |m| m.statusMove? }
when :PUNKROCK
return 1 if !check_for_move { |m| m.damagingMove? && m.soundMove? }
ret = 1 if !check_for_move { |m| m.damagingMove? && m.soundMove? }
when :RECKLESS
return 0 if !check_for_move { |m| m.recoilMove? }
ret = 0 if !check_for_move { |m| m.recoilMove? }
when :ROCKHEAD
return 0 if !check_for_move { |m| m.recoilMove? && !m.is_a?(Battle::Move::CrashDamageIfFailsUnusableInGravity) }
ret = 0 if !check_for_move { |m| m.recoilMove? && !m.is_a?(Battle::Move::CrashDamageIfFailsUnusableInGravity) }
when :RUNAWAY
return 0 if wild?
ret = 0 if wild?
when :SANDFORCE
return 2 if !has_damaging_move_of_type?(:GROUND, :ROCK, :STEEL)
ret = 2 if !has_damaging_move_of_type?(:GROUND, :ROCK, :STEEL)
when :SKILLLINK
return 0 if !check_for_move { |m| m.is_a?(Battle::Move::HitTwoToFiveTimes) }
ret = 0 if !check_for_move { |m| m.is_a?(Battle::Move::HitTwoToFiveTimes) }
when :STEELWORKER
return 0 if !has_damaging_move_of_type?(:GRASS)
ret = 0 if !has_damaging_move_of_type?(:GRASS)
when :SWARM
return 0 if !has_damaging_move_of_type?(:BUG)
ret = 0 if !has_damaging_move_of_type?(:BUG)
when :TORRENT
return 0 if !has_damaging_move_of_type?(:WATER)
ret = 0 if !has_damaging_move_of_type?(:WATER)
when :TRIAGE
return 0 if !check_for_move { |m| m.healingMove? }
ret = 0 if !check_for_move { |m| m.healingMove? }
end
ret = BASE_ABILITY_RATINGS[ability] || 0
return ret
end
@@ -966,22 +974,22 @@ class Battle::AI::AIBattler
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
end
# Foresight
if (user&.has_active_ability?(:SCRAPPY) || @battler.effects[PBEffects::Foresight]) &&
if (user&.has_active_ability?(:SCRAPPY) || self.effects[PBEffects::Foresight]) &&
defend_type == :GHOST
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
end
# Miracle Eye
if @battler.effects[PBEffects::MiracleEye] && defend_type == :DARK
if self.effects[PBEffects::MiracleEye] && defend_type == :DARK
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
end
elsif Effectiveness.super_effective_type?(type, defend_type)
# Delta Stream's weather
if @battler.effectiveWeather == :StrongWinds && defend_type == :FLYING
if battler.effectiveWeather == :StrongWinds && defend_type == :FLYING
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
end
end
# Grounded Flying-type Pokémon become susceptible to Ground moves
if !@battler.airborne? && defend_type == :FLYING && type == :GROUND
if !battler.airborne? && defend_type == :FLYING && type == :GROUND
ret = Effectiveness::NORMAL_EFFECTIVE_MULTIPLIER
end
return ret

View File

@@ -105,20 +105,21 @@ class Battle::AI::AIMove
def rough_damage
base_dmg = base_power
return base_dmg if @move.is_a?(Battle::Move::FixedDamageMove)
stage_mul = [2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8]
stage_div = [8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2]
max_stage = Battle::Battler::STAT_STAGE_MAXIMUM
stage_mul = Battle::Battler::STAT_STAGE_MULTIPLIERS
stage_div = Battle::Battler::STAT_STAGE_DIVISORS
# Get the user and target of this move
user = @ai.user
user_battler = user.battler
target = @ai.target
target_battler = target.battler
# Get the move's type
calc_type = rough_type
# Decide whether the move will definitely be a critical hit
is_critical = rough_critical_hit_stage >= Battle::Move::CRITICAL_HIT_RATIOS.length
# Decide whether the move has 50% chance of higher of being a critical hit
# TODO: Make this a gradient/probability rather than all-or-nothing?
crit_stage = rough_critical_hit_stage
is_critical = crit_stage >= Battle::Move::CRITICAL_HIT_RATIOS.length ||
Battle::Move::CRITICAL_HIT_RATIOS[crit_stage] <= 2
##### Calculate user's attack stat #####
if ["CategoryDependsOnHigherDamagePoisonTarget",
"CategoryDependsOnHigherDamageIgnoreTargetAbility"].include?(function)
@@ -126,17 +127,15 @@ class Battle::AI::AIMove
end
atk, atk_stage = @move.pbGetAttackStats(user.battler, target.battler)
if !target.has_active_ability?(:UNAWARE) || @ai.battle.moldBreaker
atk_stage = 6 if is_critical && atk_stage < 6
atk_stage = max_stage if is_critical && atk_stage < max_stage
atk = (atk.to_f * stage_mul[atk_stage] / stage_div[atk_stage]).floor
end
##### Calculate target's defense stat #####
defense, def_stage = @move.pbGetDefenseStats(user.battler, target.battler)
if !user.has_active_ability?(:UNAWARE) || @ai.battle.moldBreaker
def_stage = 6 if is_critical && def_stage > 6
def_stage = max_stage if is_critical && def_stage > max_stage
defense = (defense.to_f * stage_mul[def_stage] / stage_div[def_stage]).floor
end
##### Calculate all multiplier effects #####
multipliers = {
:power_multiplier => 1.0,
@@ -154,7 +153,6 @@ class Battle::AI::AIMove
multipliers[:power_multiplier] *= 4 / 3.0
end
end
# Ability effects that alter damage
if user.ability_active?
# NOTE: These abilities aren't suitable for checking at the start of the
@@ -166,7 +164,6 @@ class Battle::AI::AIMove
)
end
end
if !@ai.battle.moldBreaker
user_battler.allAllies.each do |b|
next if !b.abilityActive?
@@ -198,7 +195,6 @@ class Battle::AI::AIMove
)
end
end
# Item effects that alter damage
# NOTE: Type-boosting gems aren't suitable for checking at the start of the
# round.
@@ -218,23 +214,18 @@ class Battle::AI::AIMove
target.item, user_battler, target_battler, @move, multipliers, base_dmg, calc_type
)
end
# Parental Bond
if user.has_active_ability?(:PARENTALBOND)
multipliers[:power_multiplier] *= (Settings::MECHANICS_GENERATION >= 7) ? 1.25 : 1.5
end
# Me First
# TODO
# Helping Hand - n/a
# Charge
if @ai.trainer.medium_skill? &&
user.effects[PBEffects::Charge] > 0 && calc_type == :ELECTRIC
multipliers[:power_multiplier] *= 2
end
# Mud Sport and Water Sport
if @ai.trainer.medium_skill?
if calc_type == :ELECTRIC
@@ -253,7 +244,6 @@ class Battle::AI::AIMove
end
end
end
# Terrain moves
if @ai.trainer.medium_skill?
terrain_multiplier = (Settings::MECHANICS_GENERATION >= 8) ? 1.3 : 1.5
@@ -268,7 +258,6 @@ class Battle::AI::AIMove
multipliers[:power_multiplier] /= 2 if calc_type == :DRAGON && target_battler.affectedByTerrain?
end
end
# Badge multipliers
if @ai.trainer.high_skill? && @ai.battle.internalBattle && target_battler.pbOwnedByPlayer?
# Don't need to check the Atk/Sp Atk-boosting badges because the AI
@@ -279,12 +268,10 @@ class Battle::AI::AIMove
multipliers[:defense_multiplier] *= 1.1
end
end
# Multi-targeting attacks
if @ai.trainer.high_skill? && targets_multiple_battlers?
multipliers[:final_damage_multiplier] *= 0.75
end
# Weather
if @ai.trainer.medium_skill?
case user_battler.effectiveWeather
@@ -309,7 +296,6 @@ class Battle::AI::AIMove
end
end
end
# Critical hits
if is_critical
if Settings::NEW_CRITICAL_HIT_RATE_MECHANICS
@@ -318,9 +304,7 @@ class Battle::AI::AIMove
multipliers[:final_damage_multiplier] *= 2
end
end
# Random variance - n/a
# STAB
if calc_type && user.has_type?(calc_type)
if user.has_active_ability?(:ADAPTABILITY)
@@ -329,17 +313,14 @@ class Battle::AI::AIMove
multipliers[:final_damage_multiplier] *= 1.5
end
end
# Type effectiveness
typemod = target.effectiveness_of_type_against_battler(calc_type, user)
multipliers[:final_damage_multiplier] *= typemod
# Burn
if @ai.trainer.high_skill? && user.status == :BURN && physicalMove?(calc_type) &&
@move.damageReducedByBurn? && !user.has_active_ability?(:GUTS)
multipliers[:final_damage_multiplier] /= 2
end
# Aurora Veil, Reflect, Light Screen
if @ai.trainer.medium_skill? && !@move.ignoresReflect? && !is_critical &&
!user.has_active_ability?(:INFILTRATOR)
@@ -363,18 +344,14 @@ class Battle::AI::AIMove
end
end
end
# Minimize
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
##### Main damage calculation #####
base_dmg = [(base_dmg * multipliers[:power_multiplier]).round, 1].max
atk = [(atk * multipliers[:attack_multiplier]).round, 1].max
@@ -428,10 +405,11 @@ class Battle::AI::AIMove
return 0 if modifiers[:base_accuracy] < 0
return 100 if modifiers[:base_accuracy] == 0
# Calculation
accStage = [[modifiers[:accuracy_stage], -6].max, 6].min + 6
evaStage = [[modifiers[:evasion_stage], -6].max, 6].min + 6
stageMul = [3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9]
stageDiv = [9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3]
max_stage = Battle::Battler::STAT_STAGE_MAXIMUM
accStage = [[modifiers[:accuracy_stage], -max_stage].max, max_stage].min + max_stage
evaStage = [[modifiers[:evasion_stage], -max_stage].max, max_stage].min + max_stage
stageMul = Battle::Battler::ACC_EVA_STAGE_MULTIPLIERS
stageDiv = Battle::Battler::ACC_EVA_STAGE_DIVISORS
accuracy = 100.0 * stageMul[accStage] / stageDiv[accStage]
evasion = 100.0 * stageMul[evaStage] / stageDiv[evaStage]
accuracy = (accuracy * modifiers[:accuracy_multiplier]).round