diff --git a/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb b/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb index ae1e266a7..e26773165 100644 --- a/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb +++ b/Data/Scripts/011_Battle/005_AI/001_Battle_AI.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/002_AI_Switch.rb b/Data/Scripts/011_Battle/005_AI/002_AI_Switch.rb index cd0a96936..cb77b63ad 100644 --- a/Data/Scripts/011_Battle/005_AI/002_AI_Switch.rb +++ b/Data/Scripts/011_Battle/005_AI/002_AI_Switch.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/003_AI_Item.rb b/Data/Scripts/011_Battle/005_AI/003_AI_UseItem.rb similarity index 96% rename from Data/Scripts/011_Battle/005_AI/003_AI_Item.rb rename to Data/Scripts/011_Battle/005_AI/003_AI_UseItem.rb index c15d52ff8..1ba07e286 100644 --- a/Data/Scripts/011_Battle/005_AI/003_AI_Item.rb +++ b/Data/Scripts/011_Battle/005_AI/003_AI_UseItem.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/004_AI_MegaEvolve.rb b/Data/Scripts/011_Battle/005_AI/004_AI_MegaEvolve.rb index 5af35aaa6..fb6e016ab 100644 --- a/Data/Scripts/011_Battle/005_AI/004_AI_MegaEvolve.rb +++ b/Data/Scripts/011_Battle/005_AI/004_AI_MegaEvolve.rb @@ -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") diff --git a/Data/Scripts/011_Battle/005_AI/005_AI_ChooseMove.rb b/Data/Scripts/011_Battle/005_AI/005_AI_ChooseMove.rb index 1f391af2b..6c1e8856c 100644 --- a/Data/Scripts/011_Battle/005_AI/005_AI_ChooseMove.rb +++ b/Data/Scripts/011_Battle/005_AI/005_AI_ChooseMove.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/006_AI_ChooseMove_GenericEffects.rb b/Data/Scripts/011_Battle/005_AI/006_AI_ChooseMove_GenericEffects.rb index 3c34d3be5..e68cb5e0a 100644 --- a/Data/Scripts/011_Battle/005_AI/006_AI_ChooseMove_GenericEffects.rb +++ b/Data/Scripts/011_Battle/005_AI/006_AI_ChooseMove_GenericEffects.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/007_AI_ChooseMove_OtherScores.rb b/Data/Scripts/011_Battle/005_AI/007_AI_ChooseMove_OtherScores.rb index ea234c7ad..cd61ac41e 100644 --- a/Data/Scripts/011_Battle/005_AI/007_AI_ChooseMove_OtherScores.rb +++ b/Data/Scripts/011_Battle/005_AI/007_AI_ChooseMove_OtherScores.rb @@ -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. #=============================================================================== # diff --git a/Data/Scripts/011_Battle/005_AI/008_AI_Utilities.rb b/Data/Scripts/011_Battle/005_AI/008_AI_Utilities.rb index 94122595d..9d552b050 100644 --- a/Data/Scripts/011_Battle/005_AI/008_AI_Utilities.rb +++ b/Data/Scripts/011_Battle/005_AI/008_AI_Utilities.rb @@ -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 + } +) diff --git a/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb b/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb index 208157ff5..8072e9243 100644 --- a/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb +++ b/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb @@ -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 diff --git a/Data/Scripts/011_Battle/005_AI/010_AITrainer.rb b/Data/Scripts/011_Battle/005_AI/010_AITrainer.rb index 115b415c2..b1438352f 100644 --- a/Data/Scripts/011_Battle/005_AI/010_AITrainer.rb +++ b/Data/Scripts/011_Battle/005_AI/010_AITrainer.rb @@ -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) diff --git a/Data/Scripts/011_Battle/005_AI/011_AIBattler.rb b/Data/Scripts/011_Battle/005_AI/011_AIBattler.rb index 68b17dd57..c8b4b0d54 100644 --- a/Data/Scripts/011_Battle/005_AI/011_AIBattler.rb +++ b/Data/Scripts/011_Battle/005_AI/011_AIBattler.rb @@ -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) diff --git a/Data/Scripts/011_Battle/005_AI/012_AIMove.rb b/Data/Scripts/011_Battle/005_AI/012_AIMove.rb index c35728768..bd6346b03 100644 --- a/Data/Scripts/011_Battle/005_AI/012_AIMove.rb +++ b/Data/Scripts/011_Battle/005_AI/012_AIMove.rb @@ -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 diff --git a/Data/Scripts/011_Battle/006_AI MoveEffects/001_AI_MoveEffects_Misc.rb b/Data/Scripts/011_Battle/006_AI MoveEffects/001_AI_MoveEffects_Misc.rb index 431b1fbfa..876e2c952 100644 --- a/Data/Scripts/011_Battle/006_AI MoveEffects/001_AI_MoveEffects_Misc.rb +++ b/Data/Scripts/011_Battle/006_AI MoveEffects/001_AI_MoveEffects_Misc.rb @@ -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 diff --git a/Data/Scripts/011_Battle/006_AI MoveEffects/003_AI_MoveEffects_BattlerOther.rb b/Data/Scripts/011_Battle/006_AI MoveEffects/003_AI_MoveEffects_BattlerOther.rb index 7a26b6929..c742c57fb 100644 --- a/Data/Scripts/011_Battle/006_AI MoveEffects/003_AI_MoveEffects_BattlerOther.rb +++ b/Data/Scripts/011_Battle/006_AI MoveEffects/003_AI_MoveEffects_BattlerOther.rb @@ -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 diff --git a/Data/Scripts/011_Battle/006_AI MoveEffects/004_AI_MoveEffects_MoveAttributes.rb b/Data/Scripts/011_Battle/006_AI MoveEffects/004_AI_MoveEffects_MoveAttributes.rb index be3c2c498..cd1e274db 100644 --- a/Data/Scripts/011_Battle/006_AI MoveEffects/004_AI_MoveEffects_MoveAttributes.rb +++ b/Data/Scripts/011_Battle/006_AI MoveEffects/004_AI_MoveEffects_MoveAttributes.rb @@ -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 diff --git a/Data/Scripts/011_Battle/006_AI MoveEffects/005_AI_MoveEffects_MultiHit.rb b/Data/Scripts/011_Battle/006_AI MoveEffects/005_AI_MoveEffects_MultiHit.rb index 02bbed7c9..914788498 100644 --- a/Data/Scripts/011_Battle/006_AI MoveEffects/005_AI_MoveEffects_MultiHit.rb +++ b/Data/Scripts/011_Battle/006_AI MoveEffects/005_AI_MoveEffects_MultiHit.rb @@ -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 } ) diff --git a/Data/Scripts/011_Battle/006_AI MoveEffects/007_AI_MoveEffects_Items.rb b/Data/Scripts/011_Battle/006_AI MoveEffects/007_AI_MoveEffects_Items.rb index 65feab8f0..99b130279 100644 --- a/Data/Scripts/011_Battle/006_AI MoveEffects/007_AI_MoveEffects_Items.rb +++ b/Data/Scripts/011_Battle/006_AI MoveEffects/007_AI_MoveEffects_Items.rb @@ -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 }