988 lines
30 KiB
CoffeeScript
988 lines
30 KiB
CoffeeScript
|
{_} = require 'underscore'
|
||
|
{Attachment, Status, VolatileAttachment} = require '../attachment'
|
||
|
{Weather} = require '../../../shared/weather'
|
||
|
util = require '../util'
|
||
|
|
||
|
@Ability = Ability = {}
|
||
|
|
||
|
makeAbility = (name, func) ->
|
||
|
condensed = name.replace(/\s+/g, '')
|
||
|
class Ability[condensed] extends VolatileAttachment
|
||
|
@displayName: name
|
||
|
displayName: name
|
||
|
ability: true
|
||
|
func?.call(this)
|
||
|
|
||
|
# TODO: Implement.
|
||
|
makeAbility 'Pickup'
|
||
|
|
||
|
# Ability templates
|
||
|
|
||
|
makeWeatherPreventionAbility = (name) ->
|
||
|
makeAbility name, ->
|
||
|
@preventsWeather = true
|
||
|
|
||
|
this::switchIn = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('WEATHER_DISABLED')
|
||
|
|
||
|
makeWeatherPreventionAbility("Air Lock")
|
||
|
makeWeatherPreventionAbility("Cloud Nine")
|
||
|
|
||
|
makeCriticalHitPreventionAbility = (name) ->
|
||
|
makeAbility name, ->
|
||
|
@preventsCriticalHits = true
|
||
|
|
||
|
makeCriticalHitPreventionAbility("Battle Armor")
|
||
|
makeCriticalHitPreventionAbility("Shell Armor")
|
||
|
|
||
|
makeBoostProtectionAbility = (name, protection) ->
|
||
|
makeAbility name, ->
|
||
|
this::transformBoosts = (boosts, source) ->
|
||
|
return boosts if source == @pokemon
|
||
|
didProtect = false
|
||
|
for stat of boosts
|
||
|
if (!protection || stat in protection) && boosts[stat] < 0
|
||
|
didProtect = true
|
||
|
boosts[stat] = 0
|
||
|
@pokemon.activateAbility() if didProtect
|
||
|
boosts
|
||
|
|
||
|
makeBoostProtectionAbility("Big Pecks", [ "defense" ])
|
||
|
makeBoostProtectionAbility("Clear Body")
|
||
|
makeBoostProtectionAbility("Hyper Cutter", [ "attack" ])
|
||
|
makeBoostProtectionAbility("Keen Eye", [ "accuracy" ])
|
||
|
makeBoostProtectionAbility("White Smoke")
|
||
|
|
||
|
makeWeatherSpeedAbility = (name, weather) ->
|
||
|
makeAbility name, ->
|
||
|
this::switchIn = ->
|
||
|
@doubleSpeed = @battle.hasWeather(weather)
|
||
|
|
||
|
this::informWeather = (newWeather) ->
|
||
|
@doubleSpeed = (weather == newWeather)
|
||
|
|
||
|
this::editSpeed = (speed) ->
|
||
|
if @doubleSpeed then 2 * speed else speed
|
||
|
|
||
|
this::isWeatherDamageImmune = (currentWeather) ->
|
||
|
return true if weather == currentWeather
|
||
|
|
||
|
makeWeatherSpeedAbility("Chlorophyll", Weather.SUN)
|
||
|
makeWeatherSpeedAbility("Swift Swim", Weather.RAIN)
|
||
|
makeWeatherSpeedAbility("Sand Rush", Weather.SAND)
|
||
|
|
||
|
makeLowHealthAbility = (name, type) ->
|
||
|
makeAbility name, ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
return 0x1000 if move.getType(@battle, @pokemon, target) != type
|
||
|
return 0x1000 if @pokemon.currentHP > Math.floor(@pokemon.stat('hp') / 3)
|
||
|
return 0x1800
|
||
|
|
||
|
makeLowHealthAbility("Blaze", "Fire")
|
||
|
makeLowHealthAbility("Torrent", "Water")
|
||
|
makeLowHealthAbility("Overgrow", "Grass")
|
||
|
makeLowHealthAbility("Swarm", "Bug")
|
||
|
|
||
|
makeWeatherAbility = makeWeatherAbility ? (name, weather) ->
|
||
|
makeAbility name, ->
|
||
|
this::switchIn = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.setWeather(weather)
|
||
|
|
||
|
makeWeatherAbility("Drizzle", Weather.RAIN)
|
||
|
makeWeatherAbility("Drought", Weather.SUN)
|
||
|
makeWeatherAbility("Sand Stream", Weather.SAND)
|
||
|
makeWeatherAbility("Snow Warning", Weather.HAIL)
|
||
|
|
||
|
makeFilterAbility = (name) ->
|
||
|
makeAbility name, ->
|
||
|
this::modifyDamageTarget = (move, user) ->
|
||
|
if util.typeEffectiveness(move.type, user.types) > 1
|
||
|
0xC00
|
||
|
else
|
||
|
0x1000
|
||
|
|
||
|
makeFilterAbility("Filter")
|
||
|
makeFilterAbility("Solid Rock")
|
||
|
|
||
|
makeContactStatusAbility = (name, attachment) ->
|
||
|
makeAbility name, ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterBeingHit = (move, user, target, damage, isDirect) ->
|
||
|
return if !move.hasFlag("contact")
|
||
|
return if @battle.rng.next("contact status") >= .3
|
||
|
return if !isDirect
|
||
|
@pokemon.activateAbility()
|
||
|
user.attach(attachment, source: @pokemon)
|
||
|
|
||
|
makeContactStatusAbility("Cute Charm", Attachment.Attract)
|
||
|
makeContactStatusAbility("Flame Body", Status.Burn)
|
||
|
makeContactStatusAbility("Poison Point", Status.Poison)
|
||
|
makeContactStatusAbility("Static", Status.Paralyze)
|
||
|
|
||
|
makeStatusBoostAbility = (name, statuses, spectra) ->
|
||
|
makeAbility name, ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
if move.spectra == spectra && statuses.some((s) => @pokemon.has(s))
|
||
|
0x1800
|
||
|
else
|
||
|
0x1000
|
||
|
|
||
|
makeStatusBoostAbility("Flare Boost", [Status.Burn], 'special')
|
||
|
makeStatusBoostAbility("Toxic Boost", [Status.Poison, Status.Toxic], 'physical')
|
||
|
|
||
|
makeHugePowerAbility = (name) ->
|
||
|
makeAbility name, ->
|
||
|
this::modifyAttack = (move) ->
|
||
|
if move.isPhysical() then 0x2000 else 0x1000
|
||
|
|
||
|
makeHugePowerAbility("Huge Power")
|
||
|
makeHugePowerAbility("Pure Power")
|
||
|
|
||
|
makeAttachmentImmuneAbility = (name, immuneAttachments, options = {}) ->
|
||
|
makeAbility name, ->
|
||
|
this::shouldAttach = (attachment) ->
|
||
|
if attachment in immuneAttachments
|
||
|
@pokemon.activateAbility()
|
||
|
return false
|
||
|
return true
|
||
|
|
||
|
shouldCure = options.cure ? true
|
||
|
if shouldCure
|
||
|
this::update = ->
|
||
|
for attachment in immuneAttachments
|
||
|
if @pokemon.has(attachment)
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.unattach(attachment)
|
||
|
|
||
|
makeAttachmentImmuneAbility("Immunity", [Status.Poison, Status.Toxic])
|
||
|
makeAttachmentImmuneAbility("Inner Focus", [Attachment.Flinch], cure: false)
|
||
|
makeAttachmentImmuneAbility("Insomnia", [Status.Sleep])
|
||
|
makeAttachmentImmuneAbility("Limber", [Status.Paralyze])
|
||
|
makeAttachmentImmuneAbility("Magma Armor", [Status.Freeze])
|
||
|
makeAttachmentImmuneAbility("Oblivious", [Attachment.Attract])
|
||
|
makeAttachmentImmuneAbility("Own Tempo", [Attachment.Confusion])
|
||
|
makeAttachmentImmuneAbility("Vital Spirit", [Status.Sleep])
|
||
|
makeAttachmentImmuneAbility("Water Veil", [Status.Burn])
|
||
|
|
||
|
makeContactHurtAbility = (name) ->
|
||
|
makeAbility name, ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterBeingHit = (move, user, target, damage, isDirect) ->
|
||
|
return unless move.hasFlag('contact')
|
||
|
return unless isDirect
|
||
|
amount = user.stat('hp') >> 3
|
||
|
@pokemon.activateAbility()
|
||
|
if user.damage(amount)
|
||
|
@battle.cannedText('POKEMON_HURT', user)
|
||
|
|
||
|
makeContactHurtAbility("Iron Barbs")
|
||
|
makeContactHurtAbility("Rough Skin")
|
||
|
|
||
|
makeRedirectAndBoostAbility = (name, type) ->
|
||
|
makeAbility name, ->
|
||
|
# TODO: This should be implemented as isImmune instead.
|
||
|
# TODO: Type-immunities should come before ability immunities.
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.getType(@battle, user, @pokemon) != type || user == @pokemon
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(specialAttack: 1) unless @pokemon.isImmune(type)
|
||
|
return true
|
||
|
|
||
|
makeRedirectAndBoostAbility("Lightningrod", "Electric")
|
||
|
makeRedirectAndBoostAbility("Storm Drain", "Water")
|
||
|
|
||
|
makeTypeImmuneAbility = (name, type, stat) ->
|
||
|
makeAbility name, ->
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.getType(@battle, user, @pokemon) != type || user == @pokemon
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.message "#{@pokemon.name}'s #{name} increased its #{stat}!"
|
||
|
hash = {}
|
||
|
hash[stat] = 1
|
||
|
@pokemon.boost(hash)
|
||
|
return true
|
||
|
|
||
|
makeTypeImmuneAbility("Motor Drive", "Electric", "speed")
|
||
|
makeTypeImmuneAbility("Sap Sipper", "Grass", "attack")
|
||
|
|
||
|
makeTypeAbsorbMove = (name, type) ->
|
||
|
makeAbility name, ->
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.getType(@battle, user, @pokemon) != type || user == @pokemon
|
||
|
@pokemon.activateAbility()
|
||
|
amount = @pokemon.stat('hp') >> 2
|
||
|
if @pokemon.heal(amount)
|
||
|
@battle.cannedText('RECOVER_HP', @pokemon)
|
||
|
return true
|
||
|
|
||
|
makeTypeAbsorbMove("Water Absorb", "Water")
|
||
|
makeTypeAbsorbMove("Volt Absorb", "Electric")
|
||
|
|
||
|
makeAbilityCancelAbility = (name, cannedText) ->
|
||
|
makeAbility name, ->
|
||
|
this::switchIn = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText(cannedText, @pokemon)
|
||
|
|
||
|
this::beforeMove = (move, pokemon, targets) ->
|
||
|
for target in targets
|
||
|
continue if !@battle.isPokemon(target)
|
||
|
target.attach(Attachment.AbilityCancel)
|
||
|
|
||
|
this::afterMove = (move, pokemon, targets) ->
|
||
|
for target in targets
|
||
|
continue if !@battle.isPokemon(target)
|
||
|
target.unattach(Attachment.AbilityCancel)
|
||
|
|
||
|
makeAbilityCancelAbility('Mold Breaker', 'MOLD_BREAKER')
|
||
|
makeAbilityCancelAbility('Teravolt', 'TERAVOLT')
|
||
|
makeAbilityCancelAbility('Turboblaze', 'TURBOBLAZE')
|
||
|
|
||
|
# Unique Abilities
|
||
|
|
||
|
makeAbility "Adaptability"
|
||
|
|
||
|
makeAbility "Aftermath", ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterFaint = ->
|
||
|
hit = @pokemon.lastHitBy
|
||
|
return if !hit
|
||
|
{team, slot, damage, move, turn} = hit
|
||
|
pokemon = team.at(slot)
|
||
|
if move.hasFlag('contact')
|
||
|
amount = (pokemon.stat('hp') >> 2)
|
||
|
@pokemon.activateAbility()
|
||
|
pokemon.damage(amount)
|
||
|
@battle.cannedText('POKEMON_HURT', pokemon)
|
||
|
|
||
|
makeAbility 'Analytic', ->
|
||
|
this::modifyBasePower = ->
|
||
|
if !@battle.hasActionsLeft() then 0x14CD else 0x1000
|
||
|
|
||
|
makeAbility "Anger Point", ->
|
||
|
this::informCriticalHit = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.message "#{@pokemon.name} maxed its Attack!"
|
||
|
@pokemon.boost(attack: 12)
|
||
|
|
||
|
makeAbility "Anticipation", ->
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
moves = _(opponent.moves for opponent in opponents).flatten()
|
||
|
for move in moves
|
||
|
effectiveness = util.typeEffectiveness(move.type, @pokemon.types) > 1
|
||
|
if effectiveness || move.hasFlag("ohko")
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('ANTICIPATION', @pokemon)
|
||
|
break
|
||
|
|
||
|
makeAbility "Arena Trap", ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
for opponent in opponents
|
||
|
opponent.blockSwitch() unless opponent.isImmune("Ground")
|
||
|
|
||
|
makeAbility "Bad Dreams", ->
|
||
|
this::endTurn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
for opponent in opponents
|
||
|
continue unless opponent.has(Status.Sleep)
|
||
|
amount = opponent.stat('hp') >> 3
|
||
|
@pokemon.activateAbility()
|
||
|
if opponent.damage(amount)
|
||
|
@battle.cannedText('BAD_DREAMS', opponent)
|
||
|
|
||
|
makeAbility "Color Change", ->
|
||
|
this::afterBeingHit = (move, user, target, damage) ->
|
||
|
{type} = move
|
||
|
if !move.isNonDamaging() && !target.hasType(type)
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('COLOR_CHANGE', target, type)
|
||
|
target.types = [ type ]
|
||
|
|
||
|
makeAbility "Compoundeyes", ->
|
||
|
this::editAccuracy = (accuracy) ->
|
||
|
Math.floor(1.3 * accuracy)
|
||
|
|
||
|
# Hardcoded in Pokemon#boost
|
||
|
makeAbility "Contrary"
|
||
|
|
||
|
makeAbility "Cursed Body", ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterBeingHit = (move, user, target, damage, isDirect) ->
|
||
|
return if !isDirect
|
||
|
return if user == target
|
||
|
return if user.has(Attachment.Substitute)
|
||
|
return if @battle.rng.next("cursed body") >= .3
|
||
|
return if user.has(Attachment.Disable)
|
||
|
@pokemon.activateAbility()
|
||
|
user.attach(Attachment.Disable, {move})
|
||
|
|
||
|
# Implementation is done in moves.coffee, specifically makeExplosionMove.
|
||
|
makeAbility 'Damp'
|
||
|
|
||
|
makeAbility 'Defeatist', ->
|
||
|
this::modifyAttack = ->
|
||
|
halfHP = (@pokemon.stat('hp') >> 1)
|
||
|
if @pokemon.currentHP <= halfHP then 0x800 else 0x1000
|
||
|
|
||
|
makeAbility 'Defiant', ->
|
||
|
this::afterEachBoost = (boostAmount, source) ->
|
||
|
return if source.team == @pokemon.team
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(attack: 2) if boostAmount < 0
|
||
|
|
||
|
makeAbility 'Download', ->
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
return if opponents.length == 0
|
||
|
totalDef = opponents.reduce(((s, p) -> s + p.stat('defense')), 0)
|
||
|
totalSpDef = opponents.reduce(((s, p) -> s + p.stat('specialDefense')), 0)
|
||
|
@pokemon.activateAbility()
|
||
|
if totalSpDef <= totalDef
|
||
|
@pokemon.boost(specialAttack: 1)
|
||
|
else
|
||
|
@pokemon.boost(attack: 1)
|
||
|
|
||
|
makeAbility 'Dry Skin', ->
|
||
|
this::modifyBasePowerTarget = (move, user) ->
|
||
|
if move.getType(@battle, user, @pokemon) == 'Fire' then 0x1400 else 0x1000
|
||
|
|
||
|
this::endTurn = ->
|
||
|
amount = (@pokemon.stat('hp') >> 3)
|
||
|
if @battle.hasWeather(Weather.SUN)
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.damage(amount)
|
||
|
else if @battle.hasWeather(Weather.RAIN)
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.heal(amount)
|
||
|
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.getType(@battle, user, @pokemon) != 'Water' || user == @pokemon
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.heal((@pokemon.stat('hp') >> 2))
|
||
|
return true
|
||
|
|
||
|
# Implementation is in Attachment.Sleep
|
||
|
makeAbility 'Early Bird'
|
||
|
|
||
|
makeAbility 'Effect Spore', ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterBeingHit = (move, user, target, damage) ->
|
||
|
return unless move.hasFlag("contact")
|
||
|
switch @battle.rng.randInt(1, 10, "effect spore")
|
||
|
when 1
|
||
|
if user.attach(Status.Sleep)
|
||
|
@pokemon.activateAbility()
|
||
|
when 2
|
||
|
if user.attach(Status.Paralyze)
|
||
|
@pokemon.activateAbility()
|
||
|
when 3
|
||
|
if user.attach(Status.Poison)
|
||
|
@pokemon.activateAbility()
|
||
|
|
||
|
makeAbility 'Flash Fire', ->
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.getType(@battle, user, @pokemon) != 'Fire' || user == @pokemon
|
||
|
if @pokemon.attach(Attachment.FlashFire)
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('FLASH_FIRE', @pokemon)
|
||
|
else
|
||
|
@battle.cannedText('IMMUNITY', @pokemon)
|
||
|
return true
|
||
|
|
||
|
makeAbility 'Forecast'
|
||
|
|
||
|
makeAbility 'Forewarn', ->
|
||
|
VariablePowerMoves =
|
||
|
'Crush Grip' : true
|
||
|
'Dragon Rage' : true
|
||
|
'Endeavor' : true
|
||
|
'Flail' : true
|
||
|
'Frustration' : true
|
||
|
'Grass Knot' : true
|
||
|
'Gyro Ball' : true
|
||
|
'SonicBoom' : true
|
||
|
'Hidden Power' : true
|
||
|
'Low Kick' : true
|
||
|
'Natural Gift' : true
|
||
|
'Night Shade' : true
|
||
|
'Psywave' : true
|
||
|
'Return' : true
|
||
|
'Reversal' : true
|
||
|
'Seismic Toss' : true
|
||
|
'Trump Card' : true
|
||
|
'Wring Out' : true
|
||
|
|
||
|
CounterMoves =
|
||
|
"Counter" : true
|
||
|
"Mirror Coat" : true
|
||
|
"Metal Burst" : true
|
||
|
|
||
|
@consider = consider = (move) ->
|
||
|
if move.hasFlag('ohko')
|
||
|
160
|
||
|
else if CounterMoves[move.name]
|
||
|
120
|
||
|
else if VariablePowerMoves[move.name]
|
||
|
80
|
||
|
else
|
||
|
move.power
|
||
|
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
return if opponents.length == 0
|
||
|
moves = _(opponent.moves for opponent in opponents).flatten()
|
||
|
maxPower = Math.max(moves.map((m) -> consider(m))...)
|
||
|
possibles = moves.filter((m) -> consider(m) == maxPower)
|
||
|
finalMove = @battle.rng.choice(possibles, "forewarn")
|
||
|
pokemon = _(opponents).find((p) -> finalMove in p.moves)
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('FOREWARN', pokemon, finalMove)
|
||
|
|
||
|
makeAbility 'Friend Guard', ->
|
||
|
this::modifyDamageTarget = (move, user) ->
|
||
|
return 0xC00 if user.team == @pokemon.team
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility "Frisk", ->
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
return if opponents.length == 0
|
||
|
# TODO: Do you select from opponents with items, or all alive opponents?
|
||
|
opponent = @battle.rng.choice(opponents, "frisk")
|
||
|
if opponent.hasItem()
|
||
|
@pokemon.activateAbility()
|
||
|
item = opponent.getItem()
|
||
|
@battle.cannedText('FRISK', @pokemon, item)
|
||
|
|
||
|
# Implemented in items.coffee; makePinchBerry
|
||
|
makeAbility "Gluttony"
|
||
|
|
||
|
makeAbility "Guts", ->
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
return 0x1800 if @pokemon.hasStatus() && move.isPhysical()
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Harvest', ->
|
||
|
this::endTurn = ->
|
||
|
return unless @pokemon.lastItem?.type == 'berries'
|
||
|
shouldHarvest = @battle.hasWeather(Weather.SUN)
|
||
|
shouldHarvest ||= @battle.rng.randInt(0, 1, "harvest") == 1
|
||
|
if shouldHarvest
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('HARVEST', @pokemon, @pokemon.lastItem)
|
||
|
@pokemon.setItem(@pokemon.lastItem, clearLastItem: true)
|
||
|
|
||
|
makeAbility 'Healer', ->
|
||
|
this::endTurn = ->
|
||
|
for adjacent in @pokemon.team.getAdjacent(@pokemon)
|
||
|
if @battle.rng.randInt(1, 10, "healer") <= 3
|
||
|
@pokemon.activateAbility()
|
||
|
adjacent.cureStatus()
|
||
|
|
||
|
makeAbility 'Heatproof', ->
|
||
|
this::modifyBasePowerTarget = (move, user) ->
|
||
|
return 0x800 if move.getType(@battle, user, @pokemon) == 'Fire'
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Heavy Metal', ->
|
||
|
this::calculateWeight = (weight) ->
|
||
|
2 * weight
|
||
|
|
||
|
makeAbility 'Honey Gather'
|
||
|
|
||
|
makeAbility 'Hustle', ->
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
return 0x1800 if move.isPhysical()
|
||
|
return 0x1000
|
||
|
|
||
|
this::editAccuracy = (accuracy, move) ->
|
||
|
return Math.floor(0.8 * accuracy) if move.isPhysical()
|
||
|
return accuracy
|
||
|
|
||
|
makeAbility "Hydration", ->
|
||
|
this::endTurn = ->
|
||
|
if @battle.hasWeather(Weather.RAIN) && @pokemon.hasStatus()
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.cureStatus()
|
||
|
|
||
|
makeAbility 'Ice Body', ->
|
||
|
this::endTurn = ->
|
||
|
if @battle.hasWeather(Weather.HAIL)
|
||
|
@pokemon.activateAbility()
|
||
|
amount = @pokemon.stat('hp') >> 4
|
||
|
@pokemon.heal(amount)
|
||
|
|
||
|
this::isWeatherDamageImmune = (weather) ->
|
||
|
return true if weather == Weather.HAIL
|
||
|
|
||
|
makeAbility 'Illuminate'
|
||
|
|
||
|
makeAbility 'Imposter', ->
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getAllOpponents(@pokemon)
|
||
|
index = @team.indexOf(@pokemon)
|
||
|
opponent = opponents[index]
|
||
|
return if !opponent
|
||
|
return if opponent.isFainted() || opponent.has(Attachment.Substitute)
|
||
|
@pokemon.attach(Attachment.Transform, target: opponent)
|
||
|
|
||
|
# Hardcoded in Move#isDirectHit
|
||
|
# Hardcoded in Attachment.Reflect and Attachment.LightScreen
|
||
|
makeAbility 'Infiltrator'
|
||
|
|
||
|
makeAbility 'Intimidate', ->
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
for opponent in opponents
|
||
|
unless opponent.has(Attachment.Substitute)
|
||
|
@pokemon.activateAbility()
|
||
|
opponent.boost(attack: -1, @pokemon)
|
||
|
|
||
|
makeAbility 'Iron Fist', ->
|
||
|
this::modifyBasePower = (move) ->
|
||
|
if move.hasFlag('punch') then 0x1333 else 0x1000
|
||
|
|
||
|
makeAbility 'Justified', ->
|
||
|
this::afterBeingHit = (move, user, target, damage, isDirect) ->
|
||
|
if !move.isNonDamaging() && move.getType(@battle, user, @pokemon) == 'Dark' && isDirect
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(attack: 1)
|
||
|
|
||
|
makeAbility 'Klutz', ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
@pokemon.blockItem()
|
||
|
|
||
|
makeAbility 'Leaf Guard', ->
|
||
|
this::shouldAttach = (attachment) ->
|
||
|
if attachment.status && @battle.hasWeather(Weather.SUN)
|
||
|
@pokemon.activateAbility()
|
||
|
return false
|
||
|
return true
|
||
|
|
||
|
makeAbility 'Levitate', ->
|
||
|
this::isImmune = (type) ->
|
||
|
return true if type == 'Ground'
|
||
|
|
||
|
makeAbility 'Light Metal', ->
|
||
|
this::calculateWeight = (weight) ->
|
||
|
weight >> 1
|
||
|
|
||
|
# Implemented in Pokemon#drain
|
||
|
makeAbility 'Liquid Ooze'
|
||
|
|
||
|
makeAbility 'Magic Bounce', ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
@pokemon.attach(Attachment.MagicCoat)
|
||
|
@team.attach(Attachment.MagicCoat)
|
||
|
|
||
|
makeAbility 'Magic Guard', ->
|
||
|
this::transformHealthChange = (damage, options) ->
|
||
|
switch options.source
|
||
|
when 'move' then return damage
|
||
|
else return 0
|
||
|
|
||
|
makeAbility 'Magnet Pull', ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
opponents = opponents.filter((p) -> p.hasType("Steel"))
|
||
|
opponent.blockSwitch() for opponent in opponents
|
||
|
|
||
|
makeAbility 'Marvel Scale', ->
|
||
|
this::editDefense = (defense) ->
|
||
|
if @pokemon.hasStatus() then Math.floor(1.5 * defense) else defense
|
||
|
|
||
|
makeAbility 'Minus', ->
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
allies = @team.getActiveAlivePokemon()
|
||
|
if move.isSpecial() && allies.some((p) -> p.has(Ability.Plus))
|
||
|
0x1800
|
||
|
else
|
||
|
0x1000
|
||
|
|
||
|
makeAbility 'Moody', ->
|
||
|
allBoosts = [ "attack", "defense", "speed", "specialAttack",
|
||
|
"specialDefense", "accuracy", "evasion" ]
|
||
|
this::endTurn = ->
|
||
|
possibleRaises = allBoosts.filter (stat) =>
|
||
|
@pokemon.stages[stat] < 6
|
||
|
raiseStat = @battle.rng.choice(possibleRaises, "moody raise")
|
||
|
|
||
|
possibleLowers = allBoosts.filter (stat) =>
|
||
|
@pokemon.stages[stat] > -6 && stat != raiseStat
|
||
|
lowerStat = @battle.rng.choice(possibleLowers, "moody lower")
|
||
|
|
||
|
boosts = {}
|
||
|
boosts[raiseStat] = 2 if raiseStat
|
||
|
boosts[lowerStat] = -1 if lowerStat
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(boosts)
|
||
|
|
||
|
makeAbility 'Moxie', ->
|
||
|
this::afterSuccessfulHit = (move, user, target) ->
|
||
|
if target.isFainted()
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(attack: 1)
|
||
|
|
||
|
makeAbility 'Multiscale', ->
|
||
|
this::modifyDamageTarget = ->
|
||
|
return 0x800 if @pokemon.currentHP == @pokemon.stat('hp')
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Multitype'
|
||
|
|
||
|
makeAbility 'Mummy', ->
|
||
|
this::isAliveCheck = -> true
|
||
|
|
||
|
this::afterBeingHit = (move, user) ->
|
||
|
if move.hasFlag("contact") && user.hasChangeableAbility() && !user.hasAbility("Mummy")
|
||
|
@pokemon.activateAbility()
|
||
|
user.copyAbility(@constructor)
|
||
|
@battle.cannedText('MUMMY', user)
|
||
|
|
||
|
makeAbility 'Natural Cure', ->
|
||
|
this::switchOut = ->
|
||
|
@pokemon.cureStatus(message: false)
|
||
|
|
||
|
# Hardcoded in Move#willMiss
|
||
|
makeAbility 'No Guard'
|
||
|
|
||
|
makeAbility 'Normalize', ->
|
||
|
this::editMoveType = (type, target) ->
|
||
|
return "Normal" if @pokemon != target
|
||
|
return type
|
||
|
|
||
|
makeAbility 'Overcoat', ->
|
||
|
this::isWeatherDamageImmune = -> true
|
||
|
|
||
|
makeAbility 'Pickpocket', ->
|
||
|
this::afterBeingHit = (move, user, target, damage) ->
|
||
|
return if !move.hasFlag("contact") || target.hasItem() || !user.canLoseItem()
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('PICKPOCKET', target, user, user.item)
|
||
|
target.setItem(user.item)
|
||
|
user.removeItem()
|
||
|
|
||
|
makeAbility 'Plus', ->
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
allies = @team.getActiveAlivePokemon()
|
||
|
if move.isSpecial() && allies.some((p) -> p.has(Ability.Minus))
|
||
|
0x1800
|
||
|
else
|
||
|
0x1000
|
||
|
|
||
|
makeAbility 'Poison Heal', ->
|
||
|
# Poison damage neutralization is hardcoded in Attachment.Poison and Toxic.
|
||
|
this::endTurn = ->
|
||
|
# Return early so that:
|
||
|
# 1. We don't trigger ability activation if the pokemon won't be healed.
|
||
|
# 2. Ability activation must happen before HP animation.
|
||
|
return if @pokemon.currentHP == @pokemon.stat('hp')
|
||
|
if @pokemon.has(Status.Poison) || @pokemon.has(Status.Toxic)
|
||
|
@pokemon.activateAbility()
|
||
|
amount = @pokemon.stat('hp') >> 3
|
||
|
@pokemon.heal(amount)
|
||
|
|
||
|
makeAbility 'Prankster', ->
|
||
|
this::editPriority = (priority, move) ->
|
||
|
return priority + 1 if move.isNonDamaging()
|
||
|
return priority
|
||
|
|
||
|
# PP deduction hardcoded in Battle
|
||
|
makeAbility 'Pressure', ->
|
||
|
this::switchIn = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('PRESSURE', @pokemon)
|
||
|
|
||
|
# Speed drop negation hardcoded into Attachment.Paralyze
|
||
|
makeAbility 'Quick Feet', ->
|
||
|
this::editSpeed = (speed) ->
|
||
|
if @pokemon.hasStatus() then Math.floor(1.5 * speed) else speed
|
||
|
|
||
|
makeAbility 'Rain Dish', ->
|
||
|
this::endTurn = ->
|
||
|
return unless @battle.hasWeather(Weather.RAIN)
|
||
|
@pokemon.activateAbility()
|
||
|
amount = @pokemon.stat('hp') >> 4
|
||
|
@pokemon.heal(amount)
|
||
|
|
||
|
makeAbility 'Rattled', ->
|
||
|
this::afterBeingHit = (move, user, target, damage, isDirect) ->
|
||
|
type = move.getType(@battle, user, @pokemon)
|
||
|
if type in [ "Bug", "Ghost", "Dark" ] && !move.isNonDamaging() && isDirect
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.boost(speed: 1)
|
||
|
|
||
|
makeAbility 'Reckless', ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
kickMoves = [ @battle.getMove("Jump Kick"), @battle.getMove("Hi Jump Kick")]
|
||
|
if move.recoil < 0 || move in kickMoves
|
||
|
0x1333
|
||
|
else
|
||
|
0x1000
|
||
|
|
||
|
makeAbility 'Rivalry', ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
return 0x1400 if @pokemon.gender == target.gender
|
||
|
return 0xC00 if (@pokemon.gender == 'F' && target.gender == 'M') ||
|
||
|
(@pokemon.gender == 'M' && target.gender == 'F')
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Regenerator', ->
|
||
|
this::switchOut = ->
|
||
|
amount = Math.floor(@pokemon.stat('hp') / 3)
|
||
|
# Uses setHP directly to bypass Heal Block's effect
|
||
|
@pokemon.setHP(@pokemon.currentHP + amount)
|
||
|
|
||
|
# Hardcoded in move.coffee
|
||
|
makeAbility 'Rock Head'
|
||
|
|
||
|
makeAbility 'Run Away'
|
||
|
|
||
|
makeAbility 'Sand Force', ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
return 0x1000 unless @battle.hasWeather(Weather.SAND)
|
||
|
type = move.getType(@battle, @pokemon, target)
|
||
|
return 0x14CD if type in ['Rock', 'Ground', 'Steel']
|
||
|
return 0x1000
|
||
|
|
||
|
this::isWeatherDamageImmune = (weather) ->
|
||
|
return true if weather == Weather.SAND
|
||
|
|
||
|
makeAbility 'Sand Veil', ->
|
||
|
this::editEvasion = (accuracy) ->
|
||
|
if @battle.hasWeather(Weather.SAND)
|
||
|
Math.floor(.8 * accuracy)
|
||
|
else
|
||
|
accuracy
|
||
|
|
||
|
this::isWeatherDamageImmune = (weather) ->
|
||
|
return true if weather == Weather.SAND
|
||
|
|
||
|
makeAbility 'Scrappy', ->
|
||
|
this::shouldIgnoreImmunity = (moveType, target) ->
|
||
|
return target.hasType('Ghost') && moveType in [ 'Normal', 'Fighting' ]
|
||
|
|
||
|
# Hardcoded in server/bw/data/moves
|
||
|
makeAbility 'Serene Grace'
|
||
|
|
||
|
makeAbility 'Shadow Tag', ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
for opponent in opponents
|
||
|
opponent.blockSwitch() unless opponent.hasAbility('Shadow Tag')
|
||
|
|
||
|
makeAbility 'Shed Skin', ->
|
||
|
this::endTurn = ->
|
||
|
return unless @pokemon.hasStatus()
|
||
|
if @battle.rng.randInt(1, 10, "shed skin") <= 3
|
||
|
@pokemon.cureStatus()
|
||
|
|
||
|
makeAbility 'Sheer Force', ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
return 0x14CD if move.hasSecondaryEffect()
|
||
|
return 0x1000
|
||
|
|
||
|
# Hardcoded in Move#shouldTriggerSecondary
|
||
|
makeAbility 'Shield Dust'
|
||
|
|
||
|
makeAbility 'Simple', ->
|
||
|
this::transformBoosts = (boosts) ->
|
||
|
newBoosts = {}
|
||
|
for stat, boost of boosts
|
||
|
newBoosts[stat] = 2 * boost
|
||
|
newBoosts
|
||
|
|
||
|
makeAbility "Skill Link", ->
|
||
|
this::calculateNumberOfHits = (move, targets) ->
|
||
|
move.maxHits
|
||
|
|
||
|
makeAbility 'Slow Start', ->
|
||
|
this::initialize = ->
|
||
|
@turns = 5
|
||
|
|
||
|
this::switchIn = ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('SLOW_START_START', @pokemon)
|
||
|
|
||
|
this::endTurn = ->
|
||
|
@turns -= 1
|
||
|
if @turns == 0
|
||
|
@battle.cannedText('SLOW_START_END', @pokemon)
|
||
|
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
return 0x800 if move.isPhysical() && @turns > 0
|
||
|
return 0x1000
|
||
|
|
||
|
this::editSpeed = (speed) ->
|
||
|
return speed >> 1 if @turns > 0
|
||
|
return speed
|
||
|
|
||
|
makeAbility 'Sniper', ->
|
||
|
this::modifyDamage = (move, target) ->
|
||
|
return 0x1800 if @pokemon.crit
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Snow Cloak', ->
|
||
|
this::editEvasion = (accuracy) ->
|
||
|
if @battle.hasWeather(Weather.HAIL)
|
||
|
Math.floor(.8 * accuracy)
|
||
|
else
|
||
|
accuracy
|
||
|
|
||
|
this::isWeatherDamageImmune = (weather) ->
|
||
|
return true if weather == Weather.HAIL
|
||
|
|
||
|
makeAbility 'Solar Power', ->
|
||
|
this::modifyAttack = (move, target) ->
|
||
|
return 0x1800 if move.isSpecial() && @battle.hasWeather(Weather.SUN)
|
||
|
return 0x1000
|
||
|
|
||
|
this::endTurn = ->
|
||
|
if @battle.hasWeather(Weather.SUN)
|
||
|
amount = (@pokemon.stat('hp') >> 3)
|
||
|
@pokemon.activateAbility()
|
||
|
@pokemon.damage(amount)
|
||
|
@battle.cannedText('POKEMON_HURT', @pokemon)
|
||
|
|
||
|
makeAbility 'Soundproof', ->
|
||
|
this::isImmune = (type, move) ->
|
||
|
return true if move?.hasFlag('sound')
|
||
|
|
||
|
makeAbility 'Speed Boost', ->
|
||
|
this::endTurn = ->
|
||
|
return if @pokemon.turnsActive <= 0
|
||
|
@pokemon.boost(speed: 1)
|
||
|
|
||
|
makeAbility 'Stall', ->
|
||
|
this::afterTurnOrder = ->
|
||
|
@battle.delay(@pokemon)
|
||
|
|
||
|
# Hardcoded in Attachment.Flinch
|
||
|
makeAbility 'Steadfast'
|
||
|
|
||
|
# Hardcoded in Pokemon#canLoseItem
|
||
|
makeAbility 'Sticky Hold'
|
||
|
|
||
|
makeAbility 'Sturdy', ->
|
||
|
this::transformHealthChange = (amount, options) ->
|
||
|
if @pokemon.currentHP == @pokemon.stat('hp')
|
||
|
if amount >= @pokemon.currentHP && options.source == 'move'
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('ENDURE', @pokemon)
|
||
|
return @pokemon.currentHP - 1
|
||
|
return amount
|
||
|
|
||
|
makeAbility 'Suction Cups', ->
|
||
|
this::shouldPhase = (phaser) ->
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('ANCHOR', @pokemon)
|
||
|
return false
|
||
|
|
||
|
# Hardcoded in Move#criticalHitLevel
|
||
|
makeAbility 'Super Luck'
|
||
|
|
||
|
# Hardcoded in status.coffee
|
||
|
makeAbility 'Synchronize'
|
||
|
|
||
|
makeAbility 'Tangled Feet', ->
|
||
|
this::editEvasion = (evasion) ->
|
||
|
if @pokemon.has(Attachment.Confusion) then evasion >> 1 else evasion
|
||
|
|
||
|
makeAbility 'Technician', ->
|
||
|
this::modifyBasePower = (move, target) ->
|
||
|
return 0x1800 if move.basePower(@battle, @pokemon, target) <= 60
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Telepathy', ->
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move.isNonDamaging() || user == @pokemon
|
||
|
return if user not in @team.pokemon
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('AVOID_ALLIES', @pokemon)
|
||
|
return true
|
||
|
|
||
|
makeAbility 'Thick Fat', ->
|
||
|
this::modifyAttackTarget = (move, user) ->
|
||
|
return 0x800 if move.getType(@battle, user, @pokemon) in [ 'Fire', 'Ice' ]
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Tinted Lens', ->
|
||
|
this::modifyDamage = (move, target) ->
|
||
|
return 0x2000 if move.typeEffectiveness(@battle, @pokemon, target) < 1
|
||
|
return 0x1000
|
||
|
|
||
|
makeAbility 'Trace', ->
|
||
|
bannedAbilities =
|
||
|
"Flower Gift" : true
|
||
|
"Forecast" : true
|
||
|
"Illusion" : true
|
||
|
"Imposter" : true
|
||
|
"Multitype" : true
|
||
|
"Trace" : true
|
||
|
"Zen Mode" : true
|
||
|
|
||
|
this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
abilities = _(opponent.ability for opponent in opponents).compact()
|
||
|
abilities = abilities.filter((a) -> a.displayName not of bannedAbilities)
|
||
|
return if abilities.length == 0
|
||
|
ability = @battle.rng.choice(abilities, "trace")
|
||
|
# TODO: Display whose ability it traced.
|
||
|
shouldshow = {reveal: true}
|
||
|
shouldshow.reveal = false if @pokemon.has(Attachment.Illusion)
|
||
|
@pokemon.copyAbility(ability, shouldshow)
|
||
|
@battle.cannedText('TRACE', ability) if !@pokemon.has(Attachment.Illusion)
|
||
|
|
||
|
makeAbility 'Truant', ->
|
||
|
this::initialize = ->
|
||
|
@truanted = true
|
||
|
|
||
|
this::beforeMove = ->
|
||
|
@truanted = !@truanted
|
||
|
if @truanted
|
||
|
@pokemon.activateAbility()
|
||
|
@battle.cannedText('TRUANT', @pokemon)
|
||
|
return false
|
||
|
|
||
|
# Hardcoded in Move
|
||
|
makeAbility "Unaware"
|
||
|
|
||
|
# Hardcoded in Pokemon#removeItem
|
||
|
makeAbility 'Unburden'
|
||
|
|
||
|
makeAbility 'Unnerve', ->
|
||
|
this::beginTurn = this::switchIn = ->
|
||
|
opponents = @battle.getOpponents(@pokemon)
|
||
|
# TODO: Unnerve likely doesn't last until the end of the turn.
|
||
|
# More research is needed here.
|
||
|
for opponent in opponents
|
||
|
opponent.blockItem() if opponent.item?.type == 'berries'
|
||
|
|
||
|
makeAbility 'Victory Star', ->
|
||
|
this::editAccuracy = (accuracy) ->
|
||
|
Math.floor(accuracy * 1.1)
|
||
|
|
||
|
makeAbility 'Weak Armor', ->
|
||
|
this::afterBeingHit = (move, user) ->
|
||
|
if move.isPhysical() then @pokemon.boost(defense: -1, speed: 1)
|
||
|
|
||
|
makeAbility 'Wonder Guard', ->
|
||
|
this::shouldBlockExecution = (move, user) ->
|
||
|
return if move == @battle.getMove("Struggle")
|
||
|
return if move.isNonDamaging() || user == @pokemon
|
||
|
return if move.typeEffectiveness(@battle, user, @pokemon) > 1
|
||
|
@pokemon.activateAbility()
|
||
|
return true
|
||
|
|
||
|
# Hardcoded in Move#chanceToHit
|
||
|
makeAbility 'Wonder Skin'
|