mirror of
https://github.com/infinitefusion/infinitefusion-e18.git
synced 2025-12-07 13:15:01 +00:00
698 lines
28 KiB
Ruby
698 lines
28 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). Note that this requires
|
|
# saving each of the scoresAndTargets entries in here rather than in
|
|
# def pbRegisterMoveTrainer, and only at the very end are they
|
|
# whittled down to one per move which are chosen from. 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
|
|
# 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)
|
|
score *= 2 if battler.pokemon.personalID % battler.moves.length == idxMove
|
|
choices.push([idxMove, 100, -1]) # Move index, score, target
|
|
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
|
|
scoresAndTargets = []
|
|
@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)
|
|
scoresAndTargets.push([score, b.index]) if score > 0
|
|
end
|
|
if scoresAndTargets.length > 0
|
|
# Get the one best target for the move
|
|
scoresAndTargets.sort! { |a, b| b[0] <=> a[0] }
|
|
choices.push([idxMove, scoresAndTargets[0][0], scoresAndTargets[0][1]])
|
|
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
|
|
|
|
#=============================================================================
|
|
# 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
|
|
|
|
# Get the base score for the move
|
|
if @move.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: Discard move if it can be redirected by a non-target's ability
|
|
# (Lightning Rod/Storm Drain). Include checking for a previous use of
|
|
# Ion Deluge and this move being Normal-type.
|
|
# => If non-target is a user's ally, don't prefer move (rather than discarding
|
|
# it)
|
|
|
|
# TODO: Discard move if it's sound-based and user has been Throat Chopped.
|
|
# Don't prefer move if user hasn't been Throat Chopped but target has
|
|
# previously used Throat Chop. The first part of this would probably
|
|
# go elsewhere (damage calc?).
|
|
|
|
# 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 asleep, don't prefer moves that can't be used while asleep
|
|
if @trainer.medium_skill? && user_battler.asleep? && user_battler.statusCount > 1 &&
|
|
!@move.move.usableWhenAsleep?
|
|
score *= 0.2
|
|
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 = 0 # Discard 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 5
|
|
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 10
|
|
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 15
|
|
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 20
|
|
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 25
|
|
when "ParalyzeTarget",
|
|
"ParalyzeTargetIfNotTypeImmune",
|
|
"RaiseUserAtkDef1",
|
|
"RaiseUserAtkDefAcc1",
|
|
"RaiseUserSpAtkSpDef1",
|
|
"UseMoveDependingOnEnvironment",
|
|
"UseRandomUserMoveIfAsleep",
|
|
"DisableTargetUsingDifferentMove",
|
|
"SwitchOutUserPassOnEffects",
|
|
"AddSpikesToFoeSide",
|
|
"AddToxicSpikesToFoeSide",
|
|
"AddStealthRocksToFoeSide",
|
|
"CurseTargetOrLowerUserSpd1RaiseUserAtkDef1",
|
|
"StartSlowerBattlersActFirst",
|
|
"ProtectUserFromTargetingMovesSpikyShield",
|
|
"StartElectricTerrain",
|
|
"StartGrassyTerrain",
|
|
"StartMistyTerrain",
|
|
"StartPsychicTerrain",
|
|
"CureTargetStatusHealUserHalfOfTotalHP"
|
|
return 30
|
|
when "CureUserPartyStatus",
|
|
"RaiseUserAttack2",
|
|
"RaiseUserSpAtk2",
|
|
"RaiseUserSpAtk3",
|
|
"StartUserSideDoubleSpeed",
|
|
"StartWeakenPhysicalDamageAgainstUserSide",
|
|
"StartWeakenSpecialDamageAgainstUserSide",
|
|
"ProtectUserSideFromDamagingMovesIfUserFirstTurn",
|
|
"ProtectUserFromDamagingMovesKingsShield",
|
|
"ProtectUserBanefulBunker"
|
|
return 35
|
|
when "RaiseUserAtkSpd1",
|
|
"RaiseUserSpAtkSpDefSpd1",
|
|
"LowerUserDefSpDef1RaiseUserAtkSpAtkSpd2",
|
|
"RaiseUserAtk1Spd2",
|
|
"TwoTurnAttackRaiseUserSpAtkSpDefSpd2"
|
|
return 40
|
|
when "SleepTarget",
|
|
"SleepTargetChangeUserMeloettaForm",
|
|
"AddStickyWebToFoeSide",
|
|
"StartWeakenDamageAgainstUserSideIfHail"
|
|
return 60
|
|
end
|
|
# "DoesNothingUnusableInGravity",
|
|
# "StartUserSideImmunityToInflictedStatus",
|
|
# "LowerTargetEvasion1",
|
|
# "LowerTargetEvasion2",
|
|
# "StartPreventCriticalHitsAgainstUserSide",
|
|
# "UserFaintsLowerTargetAtkSpAtk2",
|
|
# "FleeFromBattle",
|
|
# "SwitchOutUserStatusMove"
|
|
# "TargetTakesUserItem",
|
|
# "LowerPPOfTargetLastMoveBy4",
|
|
# "StartTargetAirborneAndAlwaysHitByMoves",
|
|
# "TargetActsNext",
|
|
# "TargetActsLast",
|
|
# "ProtectUserSideFromStatusMoves"
|
|
return 0
|
|
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
|
|
|
|
# Figure out useful information about the choices
|
|
totalScore = 0
|
|
maxScore = 0
|
|
choices.each do |c|
|
|
totalScore += c[1]
|
|
maxScore = c[1] if maxScore < c[1]
|
|
end
|
|
|
|
# Find any preferred moves and just choose from them
|
|
if @trainer.high_skill? && maxScore > 100
|
|
stDev = pbStdDev(choices)
|
|
if stDev >= 40 && pbAIRandom(100) < 90
|
|
preferredMoves = []
|
|
choices.each do |c|
|
|
next if c[1] < 200 && c[1] < maxScore * 0.8
|
|
preferredMoves.push(c)
|
|
preferredMoves.push(c) if c[1] == maxScore # Doubly prefer the best move
|
|
end
|
|
if preferredMoves.length > 0
|
|
m = preferredMoves[pbAIRandom(preferredMoves.length)]
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) prefers #{user_battler.moves[m[0]].name}")
|
|
@battle.pbRegisterMove(user_battler.index, m[0], false)
|
|
@battle.pbRegisterTarget(user_battler.index, m[2]) if m[2] >= 0
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
# Decide whether all choices are bad, and if so, try switching instead
|
|
if @trainer.high_skill? && @user.can_switch_lax?
|
|
badMoves = false
|
|
if (maxScore <= 20 && user_battler.turnCount > 2) ||
|
|
(maxScore <= 40 && user_battler.turnCount > 5)
|
|
badMoves = true if pbAIRandom(100) < 80
|
|
end
|
|
if !badMoves && totalScore < 100 && user_battler.turnCount > 1
|
|
badMoves = true
|
|
choices.each do |c|
|
|
next if !user_battler.moves[c[0]].damagingMove?
|
|
badMoves = false
|
|
break
|
|
end
|
|
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
|
|
|
|
# If there are no calculated choices, pick one at random
|
|
if choices.length == 0
|
|
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) doesn't want to use any moves; picking one at random")
|
|
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)
|
|
end
|
|
end
|
|
|
|
# Randomly choose a move from the choices and register it
|
|
randNum = pbAIRandom(totalScore)
|
|
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
|