From d27765896597b68d7059b2679ba3a73cf59396b6 Mon Sep 17 00:00:00 2001 From: Maruno17 Date: Mon, 17 Apr 2023 19:08:42 +0100 Subject: [PATCH] More AI code for deciding when to switch --- .../011_Battle/005_AI/003_AI_Switch.rb | 433 ++++++++++++++---- .../011_Battle/005_AI/004_AI_ChooseMove.rb | 33 +- .../011_Battle/005_AI/005_AI_MegaEvolve.rb | 2 - .../005_AI/008_AI_Move_Utilities.rb | 47 +- .../Scripts/011_Battle/005_AI/009_AI_Roles.rb | 4 +- .../005_AI/020_AI_Move_EffectScoresGeneric.rb | 36 +- .../011_Battle/005_AI/102_AIBattler.rb | 30 +- Data/Scripts/011_Battle/005_AI/103_AIMove.rb | 36 +- .../056_AI_MoveHandlers_Healing.rb | 6 +- .../006_Battle_Clauses.rb | 4 +- .../005_Challenge_BattleRules.rb | 8 +- 11 files changed, 468 insertions(+), 171 deletions(-) diff --git a/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb b/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb index 8f5ccc161..ef396e8f4 100644 --- a/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb +++ b/Data/Scripts/011_Battle/005_AI/003_AI_Switch.rb @@ -25,6 +25,7 @@ class Battle::AI else return false if !@trainer.has_skill_flag?("ConsiderSwitching") reserves = get_non_active_party_pokemon(@user.index) + return false if reserves.empty? should_switch = Battle::AI::Handlers.should_switch?(@user, reserves, self, @battle) if should_switch && @trainer.medium_skill? should_switch = false if Battle::AI::Handlers.should_not_switch?(@user, reserves, self, @battle) @@ -63,8 +64,9 @@ class Battle::AI return ret end - #============================================================================= + #----------------------------------------------------------------------------- + # 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) @@ -134,7 +136,8 @@ class Battle::AI end end # Prefer if user is about to faint from Perish Song - score += 10 if @user.effects[PBEffects::PerishSong] == 1 + 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 @@ -161,7 +164,6 @@ end #=============================================================================== # Pokémon is about to faint because of Perish Song. # TODO: Also switch to remove other negative effects like Disable, Yawn. -# TODO: Review switch deciding. #=============================================================================== Battle::AI::Handlers::ShouldSwitch.add(:perish_song, proc { |battler, reserves, ai, battle| @@ -174,10 +176,9 @@ Battle::AI::Handlers::ShouldSwitch.add(:perish_song, ) #=============================================================================== -# Pokémon will take a significant amount of damage at the end of this round. -# Also, Pokémon has an effect that causes it damage at the end of this round, -# which it can remove by switching. -# TODO: Review switch deciding. +# Pokémon will take a significant amount of damage at the end of this round, or +# it has an effect that causes it damage at the end of this round which it can +# remove by switching. #=============================================================================== Battle::AI::Handlers::ShouldSwitch.add(:significant_eor_damage, proc { |battler, reserves, ai, battle| @@ -217,8 +218,9 @@ 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: Review switch deciding. Add randomness? +# 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| @@ -237,7 +239,7 @@ Battle::AI::Handlers::ShouldSwitch.add(:cure_status_problem_by_switching_out, :WATERBUBBLE => :BURN, :WATERVEIL => :BURN }[battler.ability_id] - if battler.ability_id == :NATURALCURE || (single_status_cure && single_status_cure == battler.status) + if battler.ability == :NATURALCURE || (single_status_cure && single_status_cure == battler.status) # Cures status problem next false if battler.wants_status_problem?(battler.status) next false if battler.status == :SLEEP && battler.statusCount == 1 # Will wake up this round anyway @@ -252,7 +254,7 @@ Battle::AI::Handlers::ShouldSwitch.add(:cure_status_problem_by_switching_out, 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 - elsif battler.ability_id == :REGENERATOR + elsif battler.ability == :REGENERATOR # Heals 33% HP next false if entry_hazard_damage >= battler.totalhp / 3 # Not worth healing HP if already at high HP @@ -267,10 +269,161 @@ Battle::AI::Handlers::ShouldSwitch.add(:cure_status_problem_by_switching_out, } ) +#=============================================================================== +# Pokémon's position is about to be healed by Wish, and a reserve can benefit +# more from that healing than the Pokémon can. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:wish_healing, + proc { |battler, reserves, ai, battle| + 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 + reserve_wants_healing_more = false + reserves.each do |pkmn| + entry_hazard_damage = calculate_entry_hazard_damage(pkmn, battler.index & 1) + next if entry_hazard_damage >= pkmn.hp + reserve_wants_healing_more = (pkmn.totalhp - pkmn.hp - entry_hazard_damage >= amt * 2 / 3) + break if reserve_wants_healing_more + end + if reserve_wants_healing_more + PBDebug.log_ai("#{battler.name} wants to switch because Wish can heal a reserve more") + next true + end + next false + } +) + +#=============================================================================== +# Pokémon is yawning and can't do anything while asleep. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:yawning, + proc { |battler, reserves, ai, battle| + # Yawning and can fall asleep because of it + next false if battler.effects[PBEffects::Yawn] == 0 || !battler.battler.pbCanSleepYawn? + # Doesn't want to be asleep (includes checking for moves usable while asleep) + next false if battler.wants_status_problem?(:SLEEP) + # Can't cure itself of sleep + if battler.ability_active? + next false if [:INSOMNIA, :NATURALCURE, :REGENERATOR, :SHEDSKIN].include?(battler.ability_id) + next false if battler.ability_id == :HYDRATION && [:Rain, :HeavyRain].include?(battler.battler.effectiveWeather) + end + next false if battler.has_active_item?([:CHESTOBERRY, :LUMBERRY]) && battler.battler.canConsumeBerry? + # Ally can't cure sleep + ally_can_heal = false + ai.each_ally(battler.index) do |b, i| + ally_can_heal = b.has_active_ability?(:HEALER) + break if ally_can_heal + end + next false if ally_can_heal + # Doesn't benefit from being asleep/isn't less affected by sleep + next false if battler.has_active_ability?([:EARLYBIRD, :MARVELSCALE]) + # Not trapping another battler in battle + if ai.trainer.high_skill? + next false if ai.battlers.any? do |b| + b.effects[PBEffects::JawLock] == battler.index || + b.effects[PBEffects::MeanLook] == battler.index || + b.effects[PBEffects::Octolock] == battler.index || + b.effects[PBEffects::TrappingUser] == battler.index + end + trapping = false + ai.each_foe_battler(battler.side) do |b, i| + next if b.ability_active? && Battle::AbilityEffects.triggerCertainSwitching(b.ability, b, battle) + next if b.item_active? && Battle::ItemEffects.triggerCertainSwitching(b.item, b, battle) + next if Settings::MORE_TYPE_EFFECTS && b.has_type?(:GHOST) + next if b.trappedInBattle? # Relevant trapping effects are checked above + if battler.ability_active? + trapping = Battle::AbilityEffects.triggerTrappingByTarget(battler.ability, b, battler.battler, battle) + break if trapping + end + if battler.item_active? + trapping = Battle::ItemEffects.triggerTrappingByTarget(battler.item, b, battler.battler, battle) + break if trapping + end + end + next false if trapping + end + # Doesn't have sufficiently raised stats that would be lost by switching + next false if battler.stages.any? { |val| val >= 2 } + PBDebug.log_ai("#{battler.name} wants to switch because it is yawning and can't do anything while asleep") + next true + } +) + +#=============================================================================== +# Pokémon is asleep, won't wake up soon and can't do anything while asleep. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:asleep, + proc { |battler, reserves, ai, battle| + # Asleep and won't wake up this round or next round + next false if battler.status != :SLEEP || battler.statusCount <= 2 + # Doesn't want to be asleep (includes checking for moves usable while asleep) + next false if battler.wants_status_problem?(:SLEEP) + # Doesn't benefit from being asleep + next false if battler.has_active_ability?(:MARVELSCALE) + # Doesn't know Rest (if it does, sleep is expected, so don't apply this check) + next false if battler.check_for_move { |m| m.function == "HealUserFullyAndFallAsleep" } + # Not trapping another battler in battle + if ai.trainer.high_skill? + next false if ai.battlers.any? do |b| + b.effects[PBEffects::JawLock] == battler.index || + b.effects[PBEffects::MeanLook] == battler.index || + b.effects[PBEffects::Octolock] == battler.index || + b.effects[PBEffects::TrappingUser] == battler.index + end + trapping = false + ai.each_foe_battler(battler.side) do |b, i| + next if b.ability_active? && Battle::AbilityEffects.triggerCertainSwitching(b.ability, b, battle) + next if b.item_active? && Battle::ItemEffects.triggerCertainSwitching(b.item, b, battle) + next if Settings::MORE_TYPE_EFFECTS && b.has_type?(:GHOST) + next if b.trappedInBattle? # Relevant trapping effects are checked above + if battler.ability_active? + trapping = Battle::AbilityEffects.triggerTrappingByTarget(battler.ability, b, battler.battler, battle) + break if trapping + end + if battler.item_active? + trapping = Battle::ItemEffects.triggerTrappingByTarget(battler.item, b, battler.battler, battle) + break if trapping + end + end + next false if trapping + end + # Doesn't have sufficiently raised stats that would be lost by switching + next false if battler.stages.any? { |val| val >= 2 } + # 50% chance to not bother + next false if ai.pbAIRandom(100) < 50 + PBDebug.log_ai("#{battler.name} wants to switch because it is asleep and can't do anything") + next true + } +) + +#=============================================================================== +# Pokémon can't use any moves and isn't Destiny Bonding/Grudging/hiding behind a +# Substitute. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:battler_is_useless, + proc { |battler, reserves, ai, battle| + next false if !ai.trainer.medium_skill? + next false if battler.turnCount < 2 # Just switched in, give it a chance + next false if battle.pbCanChooseAnyMove?(battler.index) + next false if battler.effects[PBEffects::DestinyBond] || battler.effects[PBEffects::Grudge] + if battler.effects[PBEffects::Substitute] + hidden_behind_substitute = true + ai.each_foe_battler(battler.side) do |b, i| + next if !b.check_for_move { |m| m.ignoresSubstitute?(b.battler) } + hidden_behind_substitute = false + break + end + next false if hidden_behind_substitute + end + PBDebug.log_ai("#{battler.name} wants to switch because it can't do anything") + next true + } +) + #=============================================================================== # Pokémon can't do anything to a Wonder Guard foe. # TODO: Check other abilities that provide immunities? -# TODO: Review switch deciding. #=============================================================================== Battle::AI::Handlers::ShouldSwitch.add(:foe_has_wonder_guard, proc { |battler, reserves, ai, battle| @@ -350,30 +503,126 @@ Battle::AI::Handlers::ShouldSwitch.add(:foe_has_wonder_guard, ) #=============================================================================== -# If Pokémon is within 5 levels of the foe, and foe's last move was -# super-effective and powerful. +# +#=============================================================================== +# 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| + next false if !ai.trainer.medium_skill? + # Not worth it if the battler is evasive enough + next false if battler.stages[:EVASION] >= 3 + # Not worth it if abilities are being negated + next false if battle.pbCheckGlobalAbility(:NEUTRALIZINGGAS) + # Get the foe move with the highest power (or a random damaging move) + foe_moves = [] + ai.each_foe_battler(battler.side) do |b, i| + b.moves.each do |m| + next if m.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 = m.power + m_power = battler.hp if m.is_a?(Battle::Move::OHKO) + m_type = move.pbCalcType(b.battler) + foe_moves.push([m_power, m_type, m]) + end + end + next false if foe_moves.empty? + if ai.trainer.high_skill? + foe_moves.sort! { |a, b| a[0] <=> b[0] } # Highest power move + chosen_move = foe_moves.last + else + chosen_move = foe_moves[ai.pbAIRandom(foe_moves.length)] # Random move + end + # Get the chosen move's information + 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). + # 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) } + PBDebug.log_ai("#{battler.name} wants to switch because it can't absorb a foe's move but a reserve can") + next true + end + next false + } +) + +#=============================================================================== +# +#=============================================================================== +# 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. +# NOTE: This rule isn't used anywhere. +#=============================================================================== +Battle::AI::Handlers::ShouldSwitch.add(:sudden_death, + proc { |battler, reserves, ai, battle| + next false if !battle.rules["suddendeath"] || battler.turnCount == 0 + if battler.hp <= battler.totalhp / 2 + threshold = 100 * (battler.totalhp - battler.hp) / battler.totalhp + if ai.pbAIRandom(100) < threshold + PBDebug.log_ai("#{battler.name} wants to switch to avoid being KO'd and losing because of the sudden death rule") + next true + end + end + next false + } +) + +#=============================================================================== +# +#=============================================================================== +# 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. # TODO: Review switch deciding. #=============================================================================== Battle::AI::Handlers::ShouldSwitch.add(:high_damage_from_foe, proc { |battler, reserves, ai, battle| - next false if battler.hp >= battler.totalhp / 2 next false if !ai.trainer.high_skill? + next false if battler.hp >= battler.totalhp / 2 big_threat = false ai.each_foe_battler(battler.side) do |b, i| - next if (foe.level - battler.level).abs > 5 + next if (b.level - battler.level).abs > 5 next if !b.battler.lastMoveUsed move_data = GameData::Move.get(b.battler.lastMoveUsed) next if move_data.status? eff = battler.effectiveness_of_type_against_battler(move_data.type, b) - next if !Effectiveness.super_effective?(eff) || move_data.power < 60 + next if !Effectiveness.super_effective?(eff) || move_data.power < 70 switch_chance = (move_data.power > 90) ? 50 : 25 - if ai.pbAIRandom(100) < switch_chance - big_threat = true - break - end + big_threat = (ai.pbAIRandom(100) < switch_chance) + break if big_threat end if big_threat - PBDebug.log_ai("#{battler.name} wants to switch because a foe can do a lot of damage to it") + PBDebug.log_ai("#{battler.name} wants to switch because a foe has a powerful super-effective move") next true end next false @@ -381,74 +630,84 @@ Battle::AI::Handlers::ShouldSwitch.add(:high_damage_from_foe, ) #=============================================================================== -# Pokémon can't do anything (must have been in battle for at least 3 rounds). -# TODO: Review switch deciding. #=============================================================================== -Battle::AI::Handlers::ShouldSwitch.add(:battler_is_useless, - proc { |battler, reserves, ai, battle| - if !battle.pbCanChooseAnyMove?(battler.index) && battler.turnCount >= 3 - PBDebug.log_ai("#{battler.name} wants to switch because it can't do anything") - next true - end - next false - } -) - #=============================================================================== -# Pokémon is Encored into an unfavourable move. -# TODO: Review switch deciding. -#=============================================================================== -Battle::AI::Handlers::ShouldSwitch.add(:bad_encored_move, - proc { |battler, reserves, ai, battle| - next false if battler.effects[PBEffects::Encore] == 0 - idxEncoredMove = battler.battler.pbEncoredMoveIndex - next false if idxEncoredMove < 0 - ai.set_up_move_check(battler.moves[idxEncoredMove]) - scoreSum = 0 - scoreCount = 0 - battler.battler.allOpposing.each do |b| - scoreSum += ai.pbGetMoveScore([b]) - scoreCount += 1 - end - if scoreCount > 0 && scoreSum / scoreCount <= 20 - if ai.pbAIRandom(100) < 80 - PBDebug.log_ai("#{battler.name} wants to switch because of encoring a bad move") - next true - else - next false - end - end - next false - } -) - -#=============================================================================== -# Sudden Death rule - I'm not sure what this means. -# TODO: Review switch deciding. -#=============================================================================== -Battle::AI::Handlers::ShouldSwitch.add(:sudden_death, - proc { |battler, reserves, ai, battle| - if battle.rules["suddendeath"] && battler.turnCount > 0 - if battler.hp <= battler.totalhp / 4 && ai.pbAIRandom(100) < 30 - PBDebug.log_ai("#{battler.name} wants to switch because of the sudden death rule") - next true - elsif battler.hp <= battler.totalhp / 2 && ai.pbAIRandom(100) < 80 - PBDebug.log_ai("#{battler.name} wants to switch because of the sudden death rule") - next true - end - end - next false - } -) #=============================================================================== # Don't bother switching if the battler will just faint from entry hazard damage -# upon switching back in. -# TODO: Allow it if the replacement will be able to get rid of entry hazards? +# upon switching back in, and if no reserve can remove the entry hazard(s). +# Switching out in this case means the battler becomes unusable, so it might as +# well stick around instead and do as much as it can. #=============================================================================== -# Battle::AI::Handlers::ShouldNotSwitch.add(:lethal_entry_hazards, -# proc { |battler, reserves, ai, battle| -# entry_hazard_damage = ai.calculate_entry_hazard_damage(battler.pkmn, battler.side) -# next entry_hazard_damage >= battler.hp -# } -# ) +Battle::AI::Handlers::ShouldNotSwitch.add(:lethal_entry_hazards, + proc { |battler, reserves, ai, battle| + next false if battle.rules["suddendeath"] + # Check whether battler will faint from entry hazard(s) + entry_hazard_damage = ai.calculate_entry_hazard_damage(battler.pkmn, battler.side) + next false if entry_hazard_damage < battler.hp + # Check for Rapid Spin + reserve_can_remove_hazards = false + reserves.each do |pkmn| + pkmn.moves.each do |move| + reserve_can_remove_hazards = (move.function_code == "RemoveUserBindingAndEntryHazards") + break if reserve_can_remove_hazards + end + break if reserve_can_remove_hazards + end + next false if reserve_can_remove_hazards + PBDebug.log_ai("#{battler.name} won't switch after all because it will faint from entry hazards if it switches back in") + next true + } +) + +#=============================================================================== +# Don't bother switching (50% chance) if the battler knows a super-effective +# move. +#=============================================================================== +Battle::AI::Handlers::ShouldNotSwitch.add(:battler_has_super_effective_move, + proc { |battler, reserves, ai, battle| + next false if battle.rules["suddendeath"] + has_super_effective_move = false + 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. + 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? + eff = b.effectiveness_of_type_against_battler(move_type, battler) + has_super_effective_move = Effectiveness.super_effective?(eff) + break if has_super_effective_move + end + break if has_super_effective_move + end + if has_super_effective_move && ai.pbAIRandom(100) < 50 + PBDebug.log_ai("#{battler.name} won't switch after all because it has a super-effective move") + next true + end + next false + } +) + +#=============================================================================== +# Don't bother switching if the battler has 4 or more positive stat stages. +# Negative stat stages are ignored. +# TODO: Ignore this if deciding whether to use Baton Pass (assuming move-scoring +# uses this code). +#=============================================================================== +Battle::AI::Handlers::ShouldNotSwitch.add(:battler_has_very_raised_stats, + proc { |battler, reserves, ai, battle| + next false if battle.rules["suddendeath"] + stat_raises = 0 + battler.stages.each_value { |val| stat_raises += val if val > 0 } + if stat_raises >= 4 + PBDebug.log_ai("#{battler.name} won't switch after all because it has a lot of raised stats") + next true + end + next false + } +) diff --git a/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb b/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb index 45c802874..a559aaacd 100644 --- a/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb +++ b/Data/Scripts/011_Battle/005_AI/004_AI_ChooseMove.rb @@ -9,14 +9,14 @@ class Battle::AI return 0.6 + 0.35 * (([@trainer.skill, 100].min / 100.0) ** 0.5) # 0.635 to 0.95 end - #============================================================================= - # Get scores for the user's moves (done before any action is assessed). + #----------------------------------------------------------------------------- + + # Get scores for the user's moves. # NOTE: For any move with a target type that can target a foe (or which # includes a foe(s) if it has multiple targets), the score calculated # for a target ally will be inverted. The MoveHandlers for those moves # should therefore treat an ally as a foe when calculating a score # against it. - #============================================================================= def pbGetMoveScores choices = [] @user.battler.eachMoveWithIndex do |orig_move, idxMove| @@ -85,6 +85,8 @@ class Battle::AI return choices end + # If the target of a move can be changed by an external effect, this method + # returns the battler index of the new target. def get_redirected_target(target_data) return nil if @move.move.cannotRedirect? return nil if !target_data.can_target_one_foe? || target_data.num_targets != 1 @@ -133,9 +135,9 @@ class Battle::AI end end - #============================================================================= - # Set some extra class variables for the move/target combo being assessed. - #============================================================================= + #----------------------------------------------------------------------------- + + # Set some extra class variables for the move being assessed. def set_up_move_check(move) case move.function when "UseLastMoveUsed" @@ -151,6 +153,7 @@ class Battle::AI @move.set_up(move) end + # Set some extra class variables for the target being assessed. def set_up_move_check_target(target) @target = (target) ? @battlers[target.index] : nil @target&.refresh_battler @@ -164,10 +167,10 @@ class Battle::AI end end - #============================================================================= + #----------------------------------------------------------------------------- + # Returns whether the move will definitely fail (assuming no battle conditions # change between now and using the move). - #============================================================================= def pbPredictMoveFailure # User is asleep and will not wake up return true if @user.battler.asleep? && @user.statusCount > 1 && !@move.move.usableWhenAsleep? @@ -185,6 +188,8 @@ class Battle::AI return false end + # Returns whether the move will definitely fail against the target (assuming + # no battle conditions change between now and using the move). def pbPredictMoveFailureAgainstTarget # Move effect-specific checks return true if Battle::AI::Handlers.move_will_fail_against_target?(@move.function, @move, @user, @target, self, @battle) @@ -220,10 +225,10 @@ class Battle::AI return false end - #============================================================================= + #----------------------------------------------------------------------------- + # Get a score for the given move being used against the given target. # Assumes def set_up_move_check has previously been called. - #============================================================================= def pbGetMoveScore(targets = nil) # Get the base score for the move score = MOVE_BASE_SCORE @@ -280,7 +285,8 @@ class Battle::AI return score end - #============================================================================= + #----------------------------------------------------------------------------- + # Returns the score of @move being used against @target. A return value of -1 # 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 @@ -294,7 +300,6 @@ class Battle::AI # 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 @@ -328,10 +333,10 @@ class Battle::AI return score end - #============================================================================= + #----------------------------------------------------------------------------- + # Make the final choice of which move to use depending on the calculated # scores for each move. Moves with higher scores are more likely to be chosen. - #============================================================================= def pbChooseMove(choices) user_battler = @user.battler # If no moves can be chosen, auto-choose a move or Struggle diff --git a/Data/Scripts/011_Battle/005_AI/005_AI_MegaEvolve.rb b/Data/Scripts/011_Battle/005_AI/005_AI_MegaEvolve.rb index 758bb82c8..5af35aaa6 100644 --- a/Data/Scripts/011_Battle/005_AI/005_AI_MegaEvolve.rb +++ b/Data/Scripts/011_Battle/005_AI/005_AI_MegaEvolve.rb @@ -1,7 +1,5 @@ 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? diff --git a/Data/Scripts/011_Battle/005_AI/008_AI_Move_Utilities.rb b/Data/Scripts/011_Battle/005_AI/008_AI_Move_Utilities.rb index ecfec85b3..94122595d 100644 --- a/Data/Scripts/011_Battle/005_AI/008_AI_Move_Utilities.rb +++ b/Data/Scripts/011_Battle/005_AI/008_AI_Move_Utilities.rb @@ -21,11 +21,11 @@ class Battle::AI return Math.sqrt(varianceTimesN / n) end - #============================================================================= - # Move's type effectiveness - #============================================================================= - # For switching. Determines the effectiveness of a potential switch-in against - # an opposing battler. + #----------------------------------------------------------------------------- + + # 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| @@ -34,9 +34,40 @@ class Battle::AI return ret end - #============================================================================= - # Yields certain AIBattler objects - #============================================================================= + # 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 @battlers.each_with_index do |battler, i| next if !battler || battler.fainted? 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 13a813726..01bd3f4da 100644 --- a/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb +++ b/Data/Scripts/011_Battle/005_AI/009_AI_Roles.rb @@ -26,10 +26,10 @@ class Battle::AI SECOND = 17 end - #============================================================================= + #----------------------------------------------------------------------------- + # Determine the roles filled by a Pokémon on a given side at a given party # index. - #============================================================================= def determine_roles(side, index) pkmn = @battle.pbParty(side)[index] ret = [] diff --git a/Data/Scripts/011_Battle/005_AI/020_AI_Move_EffectScoresGeneric.rb b/Data/Scripts/011_Battle/005_AI/020_AI_Move_EffectScoresGeneric.rb index 5f55c8f34..5e9577451 100644 --- a/Data/Scripts/011_Battle/005_AI/020_AI_Move_EffectScoresGeneric.rb +++ b/Data/Scripts/011_Battle/005_AI/020_AI_Move_EffectScoresGeneric.rb @@ -1,12 +1,10 @@ class Battle::AI - #============================================================================= # Main method for calculating the score for moves that raise a battler's # stat(s). # By default, assumes that a stat raise is a good thing. However, this score # is inverted (by desire_mult) if the target opposes the user. If the move # could target a foe but is targeting an ally, the score is also inverted, but # only because it is inverted again in def pbGetMoveScoreAgainstTarget. - #============================================================================= def get_score_for_target_stat_raise(score, target, stat_changes, whole_effect = true, fixed_change = false, ignore_contrary = false) whole_effect = false if @move.damagingMove? @@ -88,12 +86,12 @@ class Battle::AI return score end - #============================================================================= + #----------------------------------------------------------------------------- + # 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) @@ -143,9 +141,9 @@ class Battle::AI return true end - #============================================================================= + #----------------------------------------------------------------------------- + # Make score changes based on the general concept of raising stats at all. - #============================================================================= def get_target_stat_raise_score_generic(score, target, stat_changes, desire_mult = 1) total_increment = stat_changes.sum { |change| change[1] } # Prefer if move is a status move and it's the user's first/second turn @@ -164,9 +162,7 @@ class Battle::AI return score end - #============================================================================= # Make score changes based on the raising of a specific stat. - #============================================================================= def get_target_stat_raise_score_one(score, target, stat, increment, desire_mult = 1) # Figure out how much the stat will actually change by max_stage = Battle::Battler::STAT_STAGE_MAXIMUM @@ -293,7 +289,8 @@ class Battle::AI return score end - #============================================================================= + #----------------------------------------------------------------------------- + # Main method for calculating the score for moves that lower a battler's # stat(s). # By default, assumes that a stat drop is a good thing. However, this score @@ -301,7 +298,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. - #============================================================================= def get_score_for_target_stat_drop(score, target, stat_changes, whole_effect = true, fixed_change = false, ignore_contrary = false) whole_effect = false if @move.damagingMove? @@ -381,12 +377,12 @@ class Battle::AI return score end - #============================================================================= + #----------------------------------------------------------------------------- + # 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) @@ -431,9 +427,9 @@ class Battle::AI return true end - #============================================================================= + #----------------------------------------------------------------------------- + # Make score changes based on the general concept of lowering stats at all. - #============================================================================= def get_target_stat_drop_score_generic(score, target, stat_changes, desire_mult = 1) total_decrement = stat_changes.sum { |change| change[1] } # Prefer if move is a status move and it's the user's first/second turn @@ -452,9 +448,7 @@ class Battle::AI return score end - #============================================================================= # Make score changes based on the lowering of a specific stat. - #============================================================================= def get_target_stat_drop_score_one(score, target, stat, decrement, desire_mult = 1) # Figure out how much the stat will actually change by max_stage = Battle::Battler::STAT_STAGE_MAXIMUM @@ -559,9 +553,8 @@ class Battle::AI return score end - #============================================================================= - # - #============================================================================= + #----------------------------------------------------------------------------- + def get_score_for_weather(weather, move_user, starting = false) return 0 if @battle.pbCheckGlobalAbility(:AIRLOCK) || @battle.pbCheckGlobalAbility(:CLOUDNINE) @@ -677,9 +670,8 @@ class Battle::AI return ret end - #============================================================================= - # - #============================================================================= + #----------------------------------------------------------------------------- + def get_score_for_terrain(terrain, move_user, starting = false) ret = 0 ret += 4 if starting && terrain != :None && move_user.has_active_item?(:TERRAINEXTENDER) diff --git a/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb b/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb index 3afaa6a2b..700bc6e71 100644 --- a/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb +++ b/Data/Scripts/011_Battle/005_AI/102_AIBattler.rb @@ -55,7 +55,7 @@ class Battle::AI::AIBattler def idxOpposingSide; return battler.idxOpposingSide; end def pbOpposingSide; return battler.pbOpposingSide; end - #============================================================================= + #----------------------------------------------------------------------------- # Returns how much damage this battler will take at the end of this round. def rough_end_of_round_damage @@ -160,7 +160,7 @@ class Battle::AI::AIBattler return ret end - #============================================================================= + #----------------------------------------------------------------------------- def base_stat(stat) ret = 0 @@ -194,7 +194,7 @@ class Battle::AI::AIBattler return (this_speed > other_speed) ^ (@ai.battle.field.effects[PBEffects::TrickRoom] > 0) end - #============================================================================= + #----------------------------------------------------------------------------- def types; return battler.types; end def pbTypes(withExtraType = false); return battler.pbTypes(withExtraType); end @@ -228,7 +228,7 @@ class Battle::AI::AIBattler return ret end - #============================================================================= + #----------------------------------------------------------------------------- def ability_id; return battler.ability_id; end def ability; return battler.ability; end @@ -245,7 +245,7 @@ class Battle::AI::AIBattler return @ai.move.function == "IgnoreTargetAbility" || battler.hasMoldBreaker? end - #============================================================================= + #----------------------------------------------------------------------------- def item_id; return battler.item_id; end def item; return battler.item; end @@ -258,7 +258,7 @@ class Battle::AI::AIBattler return battler.hasActiveItem?(item) end - #============================================================================= + #----------------------------------------------------------------------------- def check_for_move ret = false @@ -283,7 +283,7 @@ class Battle::AI::AIBattler return false end - #============================================================================= + #----------------------------------------------------------------------------- def can_attack? return false if self.effects[PBEffects::HyperBeam] > 0 @@ -334,18 +334,20 @@ class Battle::AI::AIBattler return true end - #============================================================================= + #----------------------------------------------------------------------------- def wants_status_problem?(new_status) return true if new_status == :NONE if ability_active? case ability_id when :GUTS - return true if stat_raise_worthwhile?(self, :ATTACK, true) + return true if ![:SLEEP, :FROZEN].include?(new_status) && + stat_raise_worthwhile?(self, :ATTACK, true) when :MARVELSCALE return true if stat_raise_worthwhile?(self, :DEFENSE, true) when :QUICKFEET - return true if stat_raise_worthwhile?(self, :SPEED, true) + return true if ![:SLEEP, :FROZEN].include?(new_status) && + stat_raise_worthwhile?(self, :SPEED, true) when :FLAREBOOST return true if new_status == :BURN && stat_raise_worthwhile?(self, :SPECIAL_ATTACK, true) when :TOXICBOOST @@ -363,7 +365,7 @@ class Battle::AI::AIBattler return false end - #============================================================================= + #----------------------------------------------------------------------------- # TODO: Add more items. BASE_ITEM_RATINGS = { @@ -544,7 +546,7 @@ class Battle::AI::AIBattler return ret end - #============================================================================= + #----------------------------------------------------------------------------- # Items can be consumed by Stuff Cheeks, Teatime, Bug Bite/Pluck and Fling. def get_score_change_for_consuming_item(item, try_preserving_item = false) @@ -629,7 +631,7 @@ class Battle::AI::AIBattler return ret 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 @@ -962,7 +964,7 @@ class Battle::AI::AIBattler return ret end - #============================================================================= + #----------------------------------------------------------------------------- private diff --git a/Data/Scripts/011_Battle/005_AI/103_AIMove.rb b/Data/Scripts/011_Battle/005_AI/103_AIMove.rb index 9313a3e9b..e0ec221a9 100644 --- a/Data/Scripts/011_Battle/005_AI/103_AIMove.rb +++ b/Data/Scripts/011_Battle/005_AI/103_AIMove.rb @@ -15,7 +15,7 @@ class Battle::AI::AIMove "CategoryDependsOnHigherDamageIgnoreTargetAbility"].include?(function) end - #============================================================================= + #----------------------------------------------------------------------------- # pp # totalpp @@ -41,7 +41,16 @@ class Battle::AI::AIMove def statusMove?; return @move.statusMove?; end def function; return @move.function; end - #============================================================================= + #----------------------------------------------------------------------------- + + def type; return @move.type; end + + def rough_type + return @move.pbCalcType(@ai.user.battler) if @ai.trainer.medium_skill? + return @move.type + end + + #----------------------------------------------------------------------------- def pbTarget(user) return @move.pbTarget((user.is_a?(Battle::AI::AIBattler)) ? user.battler : user) @@ -70,7 +79,7 @@ class Battle::AI::AIMove return num_targets > 1 end - #============================================================================= + #----------------------------------------------------------------------------- def rough_priority(user) ret = @move.pbPriority(user.battler) @@ -81,16 +90,7 @@ class Battle::AI::AIMove return ret end - #============================================================================= - - def type; return @move.type; end - - def rough_type - return @move.pbCalcType(@ai.user.battler) if @ai.trainer.medium_skill? - return @move.type - end - - #============================================================================= + #----------------------------------------------------------------------------- # Returns this move's base power, taking into account various effects that # modify it. @@ -102,6 +102,7 @@ class Battle::AI::AIMove ret, self, @ai.user, @ai.target, @ai, @ai.battle) end + # Full damage calculation. def rough_damage base_dmg = base_power return base_dmg if @move.is_a?(Battle::Move::FixedDamageMove) @@ -363,13 +364,14 @@ class Battle::AI::AIMove return ret end - #============================================================================= + #----------------------------------------------------------------------------- def accuracy return @move.pbBaseAccuracy(@ai.user.battler, @ai.target.battler) if @ai.trainer.medium_skill? return @move.accuracy end + # Full accuracy calculation. def rough_accuracy # Determine user and target user = @ai.user @@ -479,8 +481,10 @@ class Battle::AI::AIMove end end - #============================================================================= + #----------------------------------------------------------------------------- + # Full critical hit chance calculation (returns the determined critical hit + # stage). def rough_critical_hit_stage user = @ai.user user_battler = user.battler @@ -524,7 +528,7 @@ class Battle::AI::AIMove return crit_stage end - #============================================================================= + #----------------------------------------------------------------------------- # Return values: # 0: Regular additional effect chance or isn't an additional effect diff --git a/Data/Scripts/011_Battle/005b_AI move function codes/056_AI_MoveHandlers_Healing.rb b/Data/Scripts/011_Battle/005b_AI move function codes/056_AI_MoveHandlers_Healing.rb index 768fdf025..bcc093dfe 100644 --- a/Data/Scripts/011_Battle/005b_AI move function codes/056_AI_MoveHandlers_Healing.rb +++ b/Data/Scripts/011_Battle/005b_AI move function codes/056_AI_MoveHandlers_Healing.rb @@ -589,9 +589,9 @@ Battle::AI::Handlers::MoveEffectScore.copy("UserFaintsHealAndCureReplacement", Battle::AI::Handlers::MoveFailureAgainstTargetCheck.add("StartPerishCountsForAllBattlers", proc { |move, user, target, ai, battle| next true if target.effects[PBEffects::PerishSong] > 0 - next true if Battle::AbilityEffects.triggerMoveImmunity(target.ability, user.battler, target.battler, - move.move, move.rough_type, battle, false) - next false + next false if !target.ability_active? + next Battle::AbilityEffects.triggerMoveImmunity(target.ability, user.battler, target.battler, + move.move, move.rough_type, battle, false) } ) Battle::AI::Handlers::MoveEffectScore.add("StartPerishCountsForAllBattlers", diff --git a/Data/Scripts/011_Battle/006_Other battle code/006_Battle_Clauses.rb b/Data/Scripts/011_Battle/006_Other battle code/006_Battle_Clauses.rb index 9d27a0953..b69e019b7 100644 --- a/Data/Scripts/011_Battle/006_Other battle code/006_Battle_Clauses.rb +++ b/Data/Scripts/011_Battle/006_Other battle code/006_Battle_Clauses.rb @@ -248,7 +248,7 @@ class Battle::Move::UserFaintsExplosive def pbMoveFailed?(user, targets) if @battle.rules["selfkoclause"] # Check whether no unfainted Pokemon remain in either party - count = @battle.pbAbleNonActiveCount(user.idxOwnSide) + count = @battle.pbAbleNonActiveCount(user.idxOwnSide) count += @battle.pbAbleNonActiveCount(user.idxOpposingSide) if count == 0 @battle.pbDisplay("But it failed!") @@ -257,7 +257,7 @@ class Battle::Move::UserFaintsExplosive end if @battle.rules["selfdestructclause"] # Check whether no unfainted Pokemon remain in either party - count = @battle.pbAbleNonActiveCount(user.idxOwnSide) + count = @battle.pbAbleNonActiveCount(user.idxOwnSide) count += @battle.pbAbleNonActiveCount(user.idxOpposingSide) if count == 0 @battle.pbDisplay(_INTL("{1}'s team was disqualified!", user.pbThis)) diff --git a/Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/005_Challenge_BattleRules.rb b/Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/005_Challenge_BattleRules.rb index 215f252a9..283d376e0 100644 --- a/Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/005_Challenge_BattleRules.rb +++ b/Data/Scripts/018_Alternate battle modes/002_Battle Frontier rules/005_Challenge_BattleRules.rb @@ -1,3 +1,9 @@ +# NOTE: The following clauses have battle code implementing them, but no class +# below to apply them: +# "drawclause" +# "modifiedselfdestructclause" +# "suddendeath" + #=============================================================================== # #=============================================================================== @@ -58,7 +64,7 @@ end # #=============================================================================== class PerishSongClause < BattleRule - def setRule(battle); battle.rules["perishsong"] = true; end + def setRule(battle); battle.rules["perishsongclause"] = true; end end #===============================================================================