Files
infinitefusion-e18/Data/Scripts/011_Battle/005_AI/004_AI_Move.rb

253 lines
12 KiB
Ruby

class Battle::AI
#=============================================================================
# Get scores for the user's moves (done before any action is assessed).
#=============================================================================
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|
if !@battle.pbCanChooseMove?(battler.index, i, false) # Unchoosable moves aren't considered
if m.pp == 0 && m.total_pp > 0
PBDebug.log("[AI] #{battler.pbThis} (#{battler.index}) cannot use move #{m.name} as it has no PP left")
else
PBDebug.log("[AI] #{battler.pbThis} (#{battler.index}) cannot choose to use #{m.name}")
end
next
end
pbAddMoveWithScoreToChoices(i, choices)
end
@battle.moldBreaker = false
return choices
end
#=============================================================================
# Get scores for the given move against each possible target.
#=============================================================================
# Wild Pokémon choose their moves randomly.
# Trainer Pokémon calculate how much they want to use each of their moves.
def pbAddMoveWithScoreToChoices(idxMove, choices)
battler = @user.battler
# TODO: Better incorporate this with the below code in future. This is here
# for now because of the num_targets > 1 code below, which would
# produce a score of 100 * the number of targets for a multi-target
# move, making it ridiculously over-preferred.
if @user.wild?
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
return
end
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
# Also includes: BothSides, FoeSide, None, User, UserSide
# 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])
elsif target_data.num_targets > 1
# Includes: AllFoes, AllNearFoes, AllNearOthers
# Would also include UserAndAllies, AllAllies, AllBattlers, but they're above
# If move affects multiple battlers and you don't choose a particular one
# TODO: Should the scores from each target be averaged instead of summed?
total_score = 0
num_targets = 0
@battle.allBattlers.each do |b|
next if !@battle.pbMoveCanTarget?(battler.index, b.index, target_data)
score = pbGetMoveScore(move, b)
total_score += ((battler.opposes?(b)) ? score : -score)
num_targets += 1
end
final_score = (num_targets == 1) ? total_score : 1.5 * total_score / num_targets
choices.push([idxMove, final_score, -1])
else
# Includes: Foe, NearAlly, NearFoe, NearOther, Other, RandomNearFoe, UserOrNearAlly
# 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)
# TODO: This should consider targeting an ally if possible. Scores will
# need to distinguish between harmful and beneficial to target -
# maybe make the score "150 - score" if target is an ally (but
# only if the score is > 10 which is the "will fail" value)?
# Noticeably affects a few moves like Heal Pulse, as well as moves
# that the target can be immune to by an ability (you may want to
# attack the ally anyway so it gains the effect of that ability).
next if target_data.targets_foe && !battler.opposes?(b)
score = pbGetMoveScore(move, b)
choices.push([idxMove, score, b.index])
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)
# TODO: Set @target to nil if there isn't one?
@target = (target) ? @battlers[target.index] : @user
@target&.refresh_battler
@battle.moldBreaker = @user.has_mold_breaker?
# 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).
# TODO: Add skill checks in here for particular calculations?
#=============================================================================
def pbPredictMoveFailure
return false if !@trainer.has_skill_flag?("PredictMoveFailure")
# 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.rough_type
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
return true if @move.move.powderMove? && !@target.battler.affectedByPowder?
# 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 25 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)
# Modify the score according to various other effects
score = Battle::AI::Handlers.apply_general_move_score_modifiers(
score, @move, @user, @target, self, @battle)
score = score.to_i
score = 0 if score < 0
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
if choices.length == 0
@battle.pbAutoChooseMove(user_battler.index)
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) will auto-use a move or Struggle")
return
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 <= 25 && user_battler.turnCount > 2) ||
(max_score <= 50 && user_battler.turnCount > 5)
badMoves = true if pbAIRandom(100) < 80
end
if !badMoves && max_score < 50 && 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)
PBDebug.log("[AI] #{user_battler.pbThis} (#{user_battler.index}) will switch due to terrible moves")
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[3] = [c[1] - threshold, 0].max }
total_score = choices.sum { |c| c[3] }
# Log the available choices
if $INTERNAL
PBDebug.log("[AI] Move choices for #{user_battler.pbThis(true)} (#{user_battler.index}):")
choices.each_with_index do |c, i|
chance = "0"
chance = sprintf("%.1f", 100.0 * c[3] / total_score) if c[3] > 0
while chance.length < 5
chance = " " + chance
end
log_msg = " * #{chance}% chance: #{user_battler.moves[c[0]].name}"
log_msg += " (against target #{c[2]})" if c[2] >= 0
log_msg += " = score #{c[1]}"
PBDebug.log(log_msg)
end
end
# Pick a move randomly from choices weighted by their scores
randNum = pbAIRandom(total_score)
choices.each do |c|
randNum -= c[3]
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(" => will use #{@battle.choices[user_battler.index][2].name}")
end
end
end