mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-06 06:01:46 +00:00
706 lines
29 KiB
Ruby
706 lines
29 KiB
Ruby
class Battle::AI
|
|
#=============================================================================
|
|
# Get scores for the user's moves (done before any action is assessed)
|
|
# NOTE: A move is only added to the choices array if it has a non-zero score.
|
|
#=============================================================================
|
|
def pbGetMoveScores
|
|
battler = @user.battler
|
|
# Get scores and targets for each move
|
|
choices = []
|
|
# TODO: Split this into two, the first part being the calculation of all
|
|
# predicted damages and the second part being the score calculations
|
|
# (which are based on the predicted damages). Multi-target moves could
|
|
# be fiddly since damages should be calculated for each target but
|
|
# they're all related.
|
|
battler.eachMoveWithIndex do |_m, i|
|
|
next if !@battle.pbCanChooseMove?(battler.index, i, false)
|
|
if @user.wild?
|
|
pbRegisterMoveWild(i, choices)
|
|
else
|
|
pbRegisterMoveTrainer(i, choices)
|
|
end
|
|
end
|
|
# Log the available choices
|
|
if $INTERNAL
|
|
logMsg = "[AI] Move choices for #{battler.pbThis(true)} (#{battler.index}): "
|
|
choices.each_with_index do |c, i|
|
|
logMsg += "#{battler.moves[c[0]].name}=#{c[1]}"
|
|
logMsg += " (target #{c[2]})" if c[2] >= 0
|
|
logMsg += ", " if i < choices.length - 1
|
|
end
|
|
PBDebug.log(logMsg)
|
|
end
|
|
return choices
|
|
end
|
|
|
|
#=============================================================================
|
|
# Get scores for the given move against each possible target
|
|
#=============================================================================
|
|
# Wild Pokémon choose their moves randomly.
|
|
def pbRegisterMoveWild(idxMove, choices)
|
|
battler = @user.battler
|
|
score = 100
|
|
choices.push([idxMove, score, -1]) # Move index, score, target
|
|
# Doubly prefer one of the user's moves (the choice is random but consistent
|
|
# and does not correlate to any other property of the user)
|
|
choices.push([idxMove, score, -1]) if battler.pokemon.personalID % battler.moves.length == idxMove
|
|
end
|
|
|
|
# Trainer Pokémon calculate how much they want to use each of their moves.
|
|
def pbRegisterMoveTrainer(idxMove, choices)
|
|
battler = @user.battler
|
|
move = battler.moves[idxMove]
|
|
target_data = move.pbTarget(battler)
|
|
# TODO: Alter target_data if user has Protean and move is Curse.
|
|
if [:UserAndAllies, :AllAllies, :AllBattlers].include?(target_data.id) ||
|
|
target_data.num_targets == 0
|
|
# If move has no targets, affects the user, a side or the whole field, or
|
|
# specially affects multiple Pokémon and the AI calculates an overall
|
|
# score at once instead of per target
|
|
score = pbGetMoveScore(move)
|
|
choices.push([idxMove, score, -1]) if score > 0
|
|
elsif target_data.num_targets > 1
|
|
# If move affects multiple battlers and you don't choose a particular one
|
|
totalScore = 0
|
|
@battle.allBattlers.each do |b|
|
|
next if !@battle.pbMoveCanTarget?(battler.index, b.index, target_data)
|
|
score = pbGetMoveScore(move, b)
|
|
totalScore += ((battler.opposes?(b)) ? score : -score)
|
|
end
|
|
choices.push([idxMove, totalScore, -1]) if totalScore > 0
|
|
else
|
|
# If move affects one battler and you have to choose which one
|
|
@battle.allBattlers.each do |b|
|
|
next if !@battle.pbMoveCanTarget?(battler.index, b.index, target_data)
|
|
next if target_data.targets_foe && !battler.opposes?(b)
|
|
score = pbGetMoveScore(move, b)
|
|
choices.push([idxMove, score, b.index]) if score > 0
|
|
end
|
|
end
|
|
end
|
|
|
|
#=============================================================================
|
|
# Set some extra class variables for the move/target combo being assessed
|
|
#=============================================================================
|
|
def set_up_move_check(move, target)
|
|
@move.set_up(move, @user)
|
|
@target = (target) ? @battlers[target.index] : @user
|
|
@target&.refresh_battler
|
|
# Determine whether user or target is faster, and store that result so it
|
|
# doesn't need recalculating
|
|
@user_faster = @user.faster_than?(@target)
|
|
end
|
|
|
|
#=============================================================================
|
|
# Returns whether the move will definitely fail (assuming no battle conditions
|
|
# change between now and using the move)
|
|
#=============================================================================
|
|
def pbPredictMoveFailure
|
|
return false if !@trainer.has_skill_flag?("PredictMoveFailure")
|
|
# TODO: Something involving pbCanChooseMove? (see Assault Vest).
|
|
# TODO: Something involving user.usingMultiTurnAttack? (perhaps earlier than
|
|
# this?).
|
|
# User is asleep and will not wake up
|
|
return true if @trainer.medium_skill? && @user.battler.asleep? &&
|
|
@user.statusCount > 1 && !@move.move.usableWhenAsleep?
|
|
# User will be truanting
|
|
return true if @user.has_active_ability?(:TRUANT) && @user.effects[PBEffects::Truant]
|
|
# Move effect-specific checks
|
|
return true if Battle::AI::Handlers.move_will_fail?(@move.function, @move, @user, @target, self, @battle)
|
|
# Immunity to priority moves because of Psychic Terrain
|
|
return true if @battle.field.terrain == :Psychic && @target.battler.affectedByTerrain? &&
|
|
@target.opposes?(@user) && @move.rough_priority(@user) > 0
|
|
# Immunity because of ability (intentionally before type immunity check)
|
|
# TODO: Check for target-redirecting abilities that also provide immunity.
|
|
# 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)
|
|
# Type immunity
|
|
calc_type = @move.pbCalcType(@user.battler)
|
|
typeMod = @move.move.pbCalcTypeMod(calc_type, @user.battler, @target.battler)
|
|
return true if @move.move.pbDamagingMove? && Effectiveness.ineffective?(typeMod)
|
|
# Dark-type immunity to moves made faster by Prankster
|
|
return true if Settings::MECHANICS_GENERATION >= 7 && @user.has_active_ability?(:PRANKSTER) &&
|
|
@target.has_type?(:DARK) && @target.opposes?(@user)
|
|
# Airborne-based immunity to Ground moves
|
|
return true if @move.damagingMove? && calc_type == :GROUND &&
|
|
@target.battler.airborne? && !@move.move.hitsFlyingTargets?
|
|
# Immunity to powder-based moves
|
|
if @move.move.powderMove?
|
|
return true if @target.has_type?(:GRASS) && Settings::MORE_TYPE_EFFECTS
|
|
if Settings::MECHANICS_GENERATION >= 6
|
|
return true if @target.has_active_ability?(:OVERCOAT) ||
|
|
@target.has_active_item?(:SAFETYGOGGLES)
|
|
end
|
|
end
|
|
# Substitute
|
|
return true if @target.effects[PBEffects::Substitute] > 0 && @move.statusMove? &&
|
|
!@move.move.ignoresSubstitute?(@user.battler) && @user.index != @target.index
|
|
return false
|
|
end
|
|
|
|
#=============================================================================
|
|
# Get a score for the given move being used against the given target
|
|
#=============================================================================
|
|
def pbGetMoveScore(move, target = nil)
|
|
set_up_move_check(move, target)
|
|
user_battler = @user.battler
|
|
target_battler = @target.battler
|
|
|
|
# Predict whether the move will fail
|
|
return 10 if pbPredictMoveFailure
|
|
|
|
# Get the base score for the move
|
|
if @move.damagingMove?
|
|
# Is also the predicted damage amount as a percentage of target's current HP
|
|
score = pbGetDamagingMoveBaseScore
|
|
else # Status moves
|
|
# Depends on the move's effect
|
|
score = pbGetStatusMoveBaseScore
|
|
end
|
|
# Modify the score according to the move's effect
|
|
score = Battle::AI::Handlers.apply_move_effect_score(@move.function,
|
|
score, @move, @user, @target, self, @battle)
|
|
|
|
# A score of 0 here means it absolutely should not be used
|
|
return 0 if score <= 0
|
|
|
|
# TODO: High priority checks:
|
|
# => Prefer move if it will KO the target (moreso if user is slower than target)
|
|
# => Don't prefer damaging move if it won't KO, user has Stance Change and
|
|
# is in shield form, and user is slower than the target
|
|
# => Check memory for past damage dealt by a target's non-high priority move,
|
|
# and prefer move if user is slower than the target and another hit from
|
|
# the same amount will KO the user
|
|
# => Check memory for past damage dealt by a target's priority move, and don't
|
|
# prefer the move if user is slower than the target and can't move faster
|
|
# than it because of priority
|
|
# => Discard move if user is slower than the target and target is semi-
|
|
# invulnerable (and move won't hit it)
|
|
# => Check memory for whether target has previously used Quick Guard, and
|
|
# don't prefer move if so
|
|
|
|
# TODO: Low priority checks:
|
|
# => Don't prefer move if user is faster than the target
|
|
# => Prefer move if user is faster than the target and target is semi-
|
|
# invulnerable
|
|
|
|
# Don't prefer a dancing move if the target has the Dancer ability
|
|
# TODO: Check all battlers, not just the target.
|
|
if @move.move.danceMove? && @target.has_active_ability?(:DANCER)
|
|
score /= 2
|
|
end
|
|
|
|
# TODO: Check memory for whether target has previously used Ion Deluge, and
|
|
# don't prefer move if it's Normal-type and target is immune because
|
|
# of its ability (Lightning Rod, etc.).
|
|
|
|
# TODO: Don't prefer sound move if user hasn't been Throat Chopped but
|
|
# target has previously used Throat Chop.
|
|
|
|
# TODO: Prefer move if it has a high critical hit rate, critical hits are
|
|
# possible but not certain, and target has raised defences/user has
|
|
# lowered offences (Atk/Def or SpAtk/SpDef, whichever is relevant).
|
|
|
|
# TODO: Don't prefer damaging moves if target is Destiny Bonding.
|
|
# => Also don't prefer damaging moves if user is slower than the target, move
|
|
# is likely to be lethal, and target has previously used Destiny Bond
|
|
|
|
# TODO: Don't prefer a move that is stopped by Wide Guard if target has
|
|
# previously used Wide Guard.
|
|
|
|
# TODO: Don't prefer Fire-type moves if target has previously used Powder.
|
|
|
|
# TODO: Don't prefer contact move if making contact with the target could
|
|
# trigger an effect that's bad for the user (Static, etc.).
|
|
# => Also check if target has previously used Spiky Shield.King's Shield/
|
|
# Baneful Bunker, and don't prefer move if so
|
|
|
|
# TODO: Prefer a contact move if making contact with the target could trigger
|
|
# an effect that's good for the user (Poison Touch/Pickpocket).
|
|
|
|
# TODO: Don't prefer a status move if user has a damaging move that will KO
|
|
# the target.
|
|
# => If target has previously used a move that will hurt the user by 30% of
|
|
# its current HP or more, moreso don't prefer a status move.
|
|
|
|
# Prefer damaging moves if AI has no more Pokémon or AI is less clever
|
|
if @trainer.medium_skill? && @battle.pbAbleNonActiveCount(user_battler.idxOwnSide) == 0 &&
|
|
!(@trainer.high_skill? && @battle.pbAbleNonActiveCount(target_battler.idxOwnSide) > 0)
|
|
if @move.move.statusMove?
|
|
score *= 0.9
|
|
elsif target_battler.hp <= target_battler.totalhp / 2
|
|
score *= 1.1
|
|
end
|
|
end
|
|
|
|
# Don't prefer attacking the target if they'd be semi-invulnerable
|
|
if @move.accuracy > 0 && @user_faster &&
|
|
(target_battler.semiInvulnerable? || target_battler.effects[PBEffects::SkyDrop] >= 0)
|
|
miss = true
|
|
miss = false if @user.has_active_ability?(:NOGUARD)
|
|
miss = false if @trainer.best_skill? && @target.has_active_ability?(:NOGUARD)
|
|
if @trainer.best_skill? && miss
|
|
# Knows what can get past semi-invulnerability
|
|
if target_battler.effects[PBEffects::SkyDrop] >= 0 ||
|
|
target_battler.inTwoTurnAttack?("TwoTurnAttackInvulnerableInSky",
|
|
"TwoTurnAttackInvulnerableInSkyParalyzeTarget",
|
|
"TwoTurnAttackInvulnerableInSkyTargetCannotAct")
|
|
miss = false if move.hitsFlyingTargets?
|
|
elsif target.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderground")
|
|
miss = false if move.hitsDiggingTargets?
|
|
elsif target.inTwoTurnAttack?("TwoTurnAttackInvulnerableUnderwater")
|
|
miss = false if move.hitsDivingTargets?
|
|
end
|
|
end
|
|
score = 10 if miss
|
|
end
|
|
|
|
# Pick a good move for the Choice items
|
|
if @trainer.medium_skill?
|
|
if @user.has_active_item?([:CHOICEBAND, :CHOICESPECS, :CHOICESCARF]) ||
|
|
@user.has_active_ability?(:GORILLATACTICS)
|
|
# Really don't prefer status moves (except Trick)
|
|
score *= 0.1 if @move.move.statusMove? && @move.move.function != "UserTargetSwapItems"
|
|
# Don't prefer moves of certain types
|
|
move_type = @move.rough_type
|
|
# Most unpreferred types are 0x effective against another type, except
|
|
# Fire/Water/Grass
|
|
# TODO: Actually check through the types for 0x instead of hardcoding
|
|
# them.
|
|
# TODO: Reborn separately doesn't prefer Fire/Water/Grass/Electric, also
|
|
# with a 0.95x score, meaning Electric can be 0.95x twice. Why are
|
|
# these four types not preferred? Maybe because they're all not
|
|
# very effective against Dragon.
|
|
unpreferred_types = [:NORMAL, :FIGHTING, :POISON, :GROUND, :GHOST,
|
|
:FIRE, :WATER, :GRASS, :ELECTRIC, :PSYCHIC, :DRAGON]
|
|
score *= 0.95 if unpreferred_types.include?(move_type)
|
|
# Don't prefer moves with lower accuracy
|
|
score *= @move.accuracy / 100.0 if @move.accuracy > 0
|
|
# Don't prefer moves with low PP
|
|
score *= 0.9 if @move.move.pp < 6
|
|
end
|
|
end
|
|
|
|
# If user is frozen, prefer a move that can thaw the user
|
|
if @trainer.medium_skill? && user_battler.status == :FROZEN
|
|
if @move.move.thawsUser?
|
|
score += 30
|
|
else
|
|
user_battler.eachMove do |m|
|
|
next unless m.thawsUser?
|
|
score -= 30 # Don't prefer this move if user knows another move that thaws
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
# If target is frozen, don't prefer moves that could thaw them
|
|
if @trainer.medium_skill? && target_battler.status == :FROZEN
|
|
if @move.rough_type == :FIRE || (Settings::MECHANICS_GENERATION >= 6 && @move.move.thawsUser?)
|
|
score *= 0.1
|
|
end
|
|
end
|
|
|
|
# Don't prefer hitting a wild shiny Pokémon
|
|
if @target.wild? && target_battler.shiny?
|
|
score *= 0.15
|
|
end
|
|
|
|
# TODO: Discard a move that can be Magic Coated if either opponent has Magic
|
|
# Bounce.
|
|
|
|
# Account for accuracy of move
|
|
accuracy = @move.rough_accuracy
|
|
score *= accuracy / 100.0
|
|
|
|
# Prefer flinching external effects (note that move effects which cause
|
|
# flinching are dealt with in the function code part of score calculation)
|
|
if @trainer.medium_skill?
|
|
if !@target.has_active_ability?([:INNERFOCUS, :SHIELDDUST]) &&
|
|
target_battler.effects[PBEffects::Substitute] == 0
|
|
if @move.move.flinchingMove? ||
|
|
(@move.move.damagingMove? &&
|
|
(@user.has_active_item?([:KINGSROCK, :RAZORFANG]) ||
|
|
@user.has_active_ability?(:STENCH)))
|
|
score *= 1.3
|
|
end
|
|
end
|
|
end
|
|
|
|
# # Adjust score based on how much damage it can deal
|
|
# if move.damagingMove?
|
|
# score = pbGetMoveScoreDamage(score, move, @user, @target, @trainer.skill)
|
|
# else # Status moves
|
|
# # Don't prefer attacks which don't deal damage
|
|
# score -= 10
|
|
# # Account for accuracy of move
|
|
# accuracy = pbRoughAccuracy(move, target)
|
|
# score *= accuracy / 100.0
|
|
# score = 0 if score <= 10 && @trainer.high_skill?
|
|
# end
|
|
score = score.to_i
|
|
score = 0 if score < 0
|
|
return score
|
|
end
|
|
|
|
#=============================================================================
|
|
# Calculate how much damage a move is likely to do to a given target (as a
|
|
# percentage of the target's current HP)
|
|
#=============================================================================
|
|
def pbGetDamagingMoveBaseScore
|
|
# Don't prefer moves that are ineffective because of abilities or effects
|
|
return 0 if @target.immune_to_move?
|
|
user_battler = @user.battler
|
|
target_battler = @target.battler
|
|
|
|
# Calculate how much damage the move will do (roughly)
|
|
calc_damage = @move.rough_damage
|
|
|
|
# TODO: Maybe move this check elsewhere? Note that Reborn's base score does
|
|
# not include this halving, but the predicted damage does.
|
|
# Two-turn attacks waste 2 turns to deal one lot of damage
|
|
calc_damage /= 2 if @move.move.chargingTurnMove?
|
|
|
|
# TODO: Maybe move this check elsewhere?
|
|
# Increased critical hit rate
|
|
if @trainer.medium_skill?
|
|
crit_stage = @move.rough_critical_hit_stage
|
|
if crit_stage >= 0
|
|
crit_fraction = (crit_stage > 50) ? 1 : Battle::Move::CRITICAL_HIT_RATIOS[crit_stage]
|
|
crit_mult = (Settings::NEW_CRITICAL_HIT_RATE_MECHANICS) ? 0.5 : 1
|
|
calc_damage *= (1 + crit_mult / crit_fraction)
|
|
end
|
|
end
|
|
|
|
# Convert damage to percentage of target's remaining HP
|
|
damage_percentage = calc_damage * 100.0 / target_battler.hp
|
|
|
|
# Don't prefer weak attacks
|
|
# damage_percentage /= 2 if damage_percentage < 20
|
|
|
|
# Prefer damaging attack if level difference is significantly high
|
|
# damage_percentage *= 1.2 if user_battler.level - 10 > target_battler.level
|
|
|
|
# Adjust score
|
|
damage_percentage = 110 if damage_percentage > 110 # Treat all lethal moves the same
|
|
damage_percentage += 40 if damage_percentage > 100 # Prefer moves likely to be lethal
|
|
|
|
return damage_percentage.to_i
|
|
end
|
|
|
|
#=============================================================================
|
|
#
|
|
#=============================================================================
|
|
def pbGetStatusMoveBaseScore
|
|
# TODO: Call @target.immune_to_move? here too, not just for damaging moves
|
|
# (only if this status move will be affected).
|
|
|
|
# TODO: Make sure all status moves are accounted for.
|
|
# TODO: Duplicates in Reborn's AI:
|
|
# "SleepTarget" Grass Whistle (15), Hypnosis (15), Sing (15),
|
|
# Lovely Kiss (20), Sleep Powder (20), Spore (60)
|
|
# "PoisonTarget" - Poison Powder (15), Poison Gas (20)
|
|
# "ParalyzeTarget" - Stun Spore (25), Glare (30)
|
|
# "ConfuseTarget" - Teeter Dance (5), Supersonic (10),
|
|
# Sweet Kiss (20), Confuse Ray (25)
|
|
# "RaiseUserAttack1" - Howl (10), Sharpen (10), Medicate (15)
|
|
# "RaiseUserSpeed2" - Agility (15), Rock Polish (25)
|
|
# "LowerTargetAttack1" - Growl (10), Baby-Doll Eyes (15)
|
|
# "LowerTargetAccuracy1" - Sand Attack (5), Flash (10), Kinesis (10), Smokescreen (10)
|
|
# "LowerTargetAttack2" - Charm (10), Feather Dance (15)
|
|
# "LowerTargetSpeed2" - String Shot (10), Cotton Spore (15), Scary Face (15)
|
|
# "LowerTargetSpDef2" - Metal Sound (10), Fake Tears (15)
|
|
case @move.move.function
|
|
when "ConfuseTarget",
|
|
"LowerTargetAccuracy1",
|
|
"LowerTargetEvasion1RemoveSideEffects",
|
|
"UserTargetSwapAtkSpAtkStages",
|
|
"UserTargetSwapDefSpDefStages",
|
|
"UserSwapBaseAtkDef",
|
|
"UserTargetAverageBaseAtkSpAtk",
|
|
"UserTargetAverageBaseDefSpDef",
|
|
"SetUserTypesToUserMoveType",
|
|
"SetTargetTypesToWater",
|
|
"SetUserTypesToTargetTypes",
|
|
"SetTargetAbilityToUserAbility",
|
|
"UserTargetSwapAbilities",
|
|
"PowerUpAllyMove",
|
|
"StartWeakenElectricMoves",
|
|
"StartWeakenFireMoves",
|
|
"EnsureNextMoveAlwaysHits",
|
|
"StartNegateTargetEvasionStatStageAndGhostImmunity",
|
|
"StartNegateTargetEvasionStatStageAndDarkImmunity",
|
|
"ProtectUserSideFromPriorityMoves",
|
|
"ProtectUserSideFromMultiTargetDamagingMoves",
|
|
"BounceBackProblemCausingStatusMoves",
|
|
"StealAndUseBeneficialStatusMove",
|
|
"DisableTargetMovesKnownByUser",
|
|
"DisableTargetHealingMoves",
|
|
"SetAttackerMovePPTo0IfUserFaints",
|
|
"UserEnduresFaintingThisTurn",
|
|
"RestoreUserConsumedItem",
|
|
"StartNegateHeldItems",
|
|
"StartDamageTargetEachTurnIfTargetAsleep",
|
|
"HealUserDependingOnUserStockpile",
|
|
"StartGravity",
|
|
"StartUserAirborne",
|
|
"UserSwapsPositionsWithAlly",
|
|
"StartSwapAllBattlersBaseDefensiveStats",
|
|
"RaiseTargetSpDef1",
|
|
"RaiseGroundedGrassBattlersAtkSpAtk1",
|
|
"RaiseGrassBattlersDef1",
|
|
"AddGrassTypeToTarget",
|
|
"TrapAllBattlersInBattleForOneTurn",
|
|
"EnsureNextCriticalHit",
|
|
"UserTargetSwapBaseSpeed",
|
|
"RedirectAllMovesToTarget",
|
|
"TargetUsesItsLastUsedMoveAgain"
|
|
return 55
|
|
when "RaiseUserAttack1",
|
|
"RaiseUserDefense1",
|
|
"RaiseUserDefense1CurlUpUser",
|
|
"RaiseUserCriticalHitRate2",
|
|
"RaiseUserAtkSpAtk1",
|
|
"RaiseUserAtkSpAtk1Or2InSun",
|
|
"RaiseUserAtkAcc1",
|
|
"RaiseTargetRandomStat2",
|
|
"LowerTargetAttack1",
|
|
"LowerTargetDefense1",
|
|
"LowerTargetAccuracy1",
|
|
"LowerTargetAttack2",
|
|
"LowerTargetSpeed2",
|
|
"LowerTargetSpDef2",
|
|
"ResetAllBattlersStatStages",
|
|
"UserCopyTargetStatStages",
|
|
"SetUserTypesBasedOnEnvironment",
|
|
"DisableTargetUsingSameMoveConsecutively",
|
|
"StartTargetCannotUseItem",
|
|
"LowerTargetAttack1BypassSubstitute",
|
|
"LowerTargetAtkSpAtk1",
|
|
"LowerTargetSpAtk1",
|
|
"TargetNextFireMoveDamagesTarget"
|
|
return 60
|
|
when "SleepTarget",
|
|
"SleepTargetIfUserDarkrai",
|
|
"SleepTargetChangeUserMeloettaForm",
|
|
"PoisonTarget",
|
|
"CureUserBurnPoisonParalysis",
|
|
"RaiseUserAttack1",
|
|
"RaiseUserSpDef1PowerUpElectricMove",
|
|
"RaiseUserEvasion1",
|
|
"RaiseUserSpeed2",
|
|
"LowerTargetAttack1",
|
|
"LowerTargetAtkDef1",
|
|
"LowerTargetAttack2",
|
|
"LowerTargetDefense2",
|
|
"LowerTargetSpeed2",
|
|
"LowerTargetSpAtk2IfCanAttract",
|
|
"LowerTargetSpDef2",
|
|
"ReplaceMoveThisBattleWithTargetLastMoveUsed",
|
|
"ReplaceMoveWithTargetLastMoveUsed",
|
|
"SetUserAbilityToTargetAbility",
|
|
"UseMoveTargetIsAboutToUse",
|
|
"UseRandomMoveFromUserParty",
|
|
"StartHealUserEachTurnTrapUserInBattle",
|
|
"HealTargetHalfOfTotalHP",
|
|
"UserFaintsHealAndCureReplacement",
|
|
"UserFaintsHealAndCureReplacementRestorePP",
|
|
"StartSunWeather",
|
|
"StartRainWeather",
|
|
"StartSandstormWeather",
|
|
"StartHailWeather",
|
|
"RaisePlusMinusUserAndAlliesDefSpDef1",
|
|
"LowerTargetSpAtk2",
|
|
"LowerPoisonedTargetAtkSpAtkSpd1",
|
|
"AddGhostTypeToTarget",
|
|
"LowerTargetAtkSpAtk1SwitchOutUser",
|
|
"RaisePlusMinusUserAndAlliesAtkSpAtk1",
|
|
"HealTargetDependingOnGrassyTerrain"
|
|
return 65
|
|
when "SleepTarget",
|
|
"SleepTargetChangeUserMeloettaForm",
|
|
"SleepTargetNextTurn",
|
|
"PoisonTarget",
|
|
"ConfuseTarget",
|
|
"RaiseTargetSpAtk1ConfuseTarget",
|
|
"RaiseTargetAttack2ConfuseTarget",
|
|
"UserTargetSwapStatStages",
|
|
"StartUserSideImmunityToStatStageLowering",
|
|
"SetUserTypesToResistLastAttack",
|
|
"SetTargetAbilityToSimple",
|
|
"SetTargetAbilityToInsomnia",
|
|
"NegateTargetAbility",
|
|
"TransformUserIntoTarget",
|
|
"UseLastMoveUsedByTarget",
|
|
"UseLastMoveUsed",
|
|
"UseRandomMove",
|
|
"HealUserFullyAndFallAsleep",
|
|
"StartHealUserEachTurn",
|
|
"StartPerishCountsForAllBattlers",
|
|
"SwitchOutTargetStatusMove",
|
|
"TrapTargetInBattle",
|
|
"TargetMovesBecomeElectric",
|
|
"NormalMovesBecomeElectric",
|
|
"PoisonTargetLowerTargetSpeed1"
|
|
return 70
|
|
when "BadPoisonTarget",
|
|
"ParalyzeTarget",
|
|
"BurnTarget",
|
|
"ConfuseTarget",
|
|
"AttractTarget",
|
|
"GiveUserStatusToTarget",
|
|
"RaiseUserDefSpDef1",
|
|
"RaiseUserDefense2",
|
|
"RaiseUserSpeed2",
|
|
"RaiseUserSpeed2LowerUserWeight",
|
|
"RaiseUserSpDef2",
|
|
"RaiseUserEvasion2MinimizeUser",
|
|
"RaiseUserDefense3",
|
|
"MaxUserAttackLoseHalfOfTotalHP",
|
|
"UserTargetAverageHP",
|
|
"ProtectUser",
|
|
"DisableTargetLastMoveUsed",
|
|
"DisableTargetStatusMoves",
|
|
"HealUserHalfOfTotalHP",
|
|
"HealUserHalfOfTotalHPLoseFlyingTypeThisTurn",
|
|
"HealUserPositionNextTurn",
|
|
"HealUserDependingOnWeather",
|
|
"StartLeechSeedTarget",
|
|
"AttackerFaintsIfUserFaints",
|
|
"UserTargetSwapItems",
|
|
"UserMakeSubstitute",
|
|
"UserAddStockpileRaiseDefSpDef1",
|
|
"RedirectAllMovesToUser",
|
|
"InvertTargetStatStages",
|
|
"HealUserByTargetAttackLowerTargetAttack1",
|
|
"HealUserDependingOnSandstorm"
|
|
return 75
|
|
when "ParalyzeTarget",
|
|
"ParalyzeTargetIfNotTypeImmune",
|
|
"RaiseUserAtkDef1",
|
|
"RaiseUserAtkDefAcc1",
|
|
"RaiseUserSpAtkSpDef1",
|
|
"UseMoveDependingOnEnvironment",
|
|
"UseRandomUserMoveIfAsleep",
|
|
"DisableTargetUsingDifferentMove",
|
|
"SwitchOutUserPassOnEffects",
|
|
"AddSpikesToFoeSide",
|
|
"AddToxicSpikesToFoeSide",
|
|
"AddStealthRocksToFoeSide",
|
|
"CurseTargetOrLowerUserSpd1RaiseUserAtkDef1",
|
|
"StartSlowerBattlersActFirst",
|
|
"ProtectUserFromTargetingMovesSpikyShield",
|
|
"StartElectricTerrain",
|
|
"StartGrassyTerrain",
|
|
"StartMistyTerrain",
|
|
"StartPsychicTerrain",
|
|
"CureTargetStatusHealUserHalfOfTotalHP"
|
|
return 80
|
|
when "CureUserPartyStatus",
|
|
"RaiseUserAttack2",
|
|
"RaiseUserSpAtk2",
|
|
"RaiseUserSpAtk3",
|
|
"StartUserSideDoubleSpeed",
|
|
"StartWeakenPhysicalDamageAgainstUserSide",
|
|
"StartWeakenSpecialDamageAgainstUserSide",
|
|
"ProtectUserSideFromDamagingMovesIfUserFirstTurn",
|
|
"ProtectUserFromDamagingMovesKingsShield",
|
|
"ProtectUserBanefulBunker"
|
|
return 85
|
|
when "RaiseUserAtkSpd1",
|
|
"RaiseUserSpAtkSpDefSpd1",
|
|
"LowerUserDefSpDef1RaiseUserAtkSpAtkSpd2",
|
|
"RaiseUserAtk1Spd2",
|
|
"TwoTurnAttackRaiseUserSpAtkSpDefSpd2"
|
|
return 90
|
|
when "SleepTarget",
|
|
"SleepTargetChangeUserMeloettaForm",
|
|
"AddStickyWebToFoeSide",
|
|
"StartWeakenDamageAgainstUserSideIfHail"
|
|
return 100
|
|
end
|
|
# "DoesNothingUnusableInGravity",
|
|
# "StartUserSideImmunityToInflictedStatus",
|
|
# "LowerTargetEvasion1",
|
|
# "LowerTargetEvasion2",
|
|
# "StartPreventCriticalHitsAgainstUserSide",
|
|
# "UserFaintsLowerTargetAtkSpAtk2",
|
|
# "FleeFromBattle",
|
|
# "SwitchOutUserStatusMove"
|
|
# "TargetTakesUserItem",
|
|
# "LowerPPOfTargetLastMoveBy4",
|
|
# "StartTargetAirborneAndAlwaysHitByMoves",
|
|
# "TargetActsNext",
|
|
# "TargetActsLast",
|
|
# "ProtectUserSideFromStatusMoves"
|
|
return 100
|
|
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 there are no calculated choices, pick one at random
|
|
if choices.length == 0
|
|
user_battler.eachMoveWithIndex do |_m, i|
|
|
next if !@battle.pbCanChooseMove?(user_battler.index, i, false)
|
|
choices.push([i, 100, -1]) # Move index, score, target
|
|
end
|
|
if choices.length == 0 # No moves are physically possible to use; use Struggle
|
|
@battle.pbAutoChooseMove(user_battler.index)
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) will auto-use a move or Struggle")
|
|
return
|
|
end
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) doesn't want to use any moves; picking one at random")
|
|
end
|
|
|
|
# Figure out useful information about the choices
|
|
max_score = 0
|
|
choices.each { |c| max_score = c[1] if max_score < c[1] }
|
|
|
|
# Decide whether all choices are bad, and if so, try switching instead
|
|
if @trainer.high_skill? && @user.can_switch_lax?
|
|
badMoves = false
|
|
if (max_score <= 20 && user_battler.turnCount > 2) ||
|
|
(max_score <= 40 && user_battler.turnCount > 5)
|
|
badMoves = true if pbAIRandom(100) < 80
|
|
end
|
|
if !badMoves && max_score < 60 && user_battler.turnCount > 1
|
|
badMoves = choices.none? { |c| user_battler.moves[c[0]].damagingMove? }
|
|
badMoves = false if badMoves && pbAIRandom(100) < 10
|
|
end
|
|
if badMoves && pbEnemyShouldWithdrawEx?(true)
|
|
if $INTERNAL
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) will switch due to terrible moves")
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
# Calculate a minimum score threshold and reduce all move scores by it
|
|
threshold = (max_score * 0.85).floor
|
|
choices.each { |c| c[1] = [c[1] - threshold, 0].max }
|
|
total_score = choices.sum { |c| c[1] }
|
|
|
|
# Pick a move randomly from choices weighted by their scores
|
|
randNum = pbAIRandom(total_score)
|
|
choices.each do |c|
|
|
randNum -= c[1]
|
|
next if randNum >= 0
|
|
@battle.pbRegisterMove(user_battler.index, c[0], false)
|
|
@battle.pbRegisterTarget(user_battler.index, c[2]) if c[2] >= 0
|
|
break
|
|
end
|
|
|
|
# Log the result
|
|
if @battle.choices[user_battler.index][2]
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) will use #{@battle.choices[user_battler.index][2].name}")
|
|
end
|
|
end
|
|
end
|