BattleSim/server/bw/attachment.coffee

1581 lines
43 KiB
CoffeeScript

{Protocol} = require '../../shared/protocol'
{Weather} = require '../../shared/weather'
util = require './util'
Query = require './queries'
{_} = require 'underscore'
@Attachment = Attachment = {}
@Status = Status = {}
class @Attachments
constructor: ->
@attachments = []
push: (attachmentClass, options={}, attributes={}) ->
throw new Error("Passed a non-existent Attachment.") if !attachmentClass?
return null if Query.untilFalse('shouldAttach', @all(), attachmentClass, options) == false
return null if attachmentClass.preattach?(options, attributes) == false
attachment = @get(attachmentClass)
if !attachment?
attachment = new attachmentClass()
for attribute, value of attributes
attachment[attribute] = value
@attachments.push(attachment)
attachment.initialize?(options)
return null if attachment.layers == attachment.maxLayers
attachment.layers++
return attachment
unattach: (klass) ->
index = @indexOf(klass)
if index >= 0
attachment = @attachments.splice(index, 1)[0]
attachment.unattach?()
attachment.attached = false
attachment
unattachAll: (condition) ->
condition ||= -> true
i = 0
while i < @attachments.length
attachment = @attachments[i]
if condition(attachment)
attachment.unattach?()
@attachments.splice(i, 1)
else
i++
# Returns a list of attachments that can be passed to another Pokemon.
getPassable: ->
passable = @attachments.filter((attachment) -> attachment.passable)
passable.map((a) -> a.constructor)
indexOf: (attachment) ->
@attachments.map((a) -> a.constructor).indexOf(attachment)
get: (attachment) ->
@attachments[@indexOf(attachment)]
contains: (attachment) ->
@indexOf(attachment) != -1
all: ->
_.clone(@attachments)
size: ->
@attachments.length
# Attachments represents a pokemon's state. Some examples are
# status effects, entry hazards, and fire spin's trapping effect.
# Attachments are "attached" with Pokemon.attach(), and after
# that the attachment can be retrieved with Attachment.pokemon
class @BaseAttachment
name: "BaseAttachment"
maxLayers: 1
constructor: ->
@layers = 0
@attached = true
valid: ->
return false if !@attached
return false if @battle?.isOver()
return false if @item && @pokemon?.item && @pokemon?.isItemBlocked()
return false if @ability && @pokemon?.isAbilityBlocked()
return false if @isAliveCheck() == false
return true
isAliveCheck: ->
@pokemon && @pokemon.isAlive()
# initialize: ->
# unattach: ->
# calculateWeight: (weight) -> weight
# afterBeingHit: (move, user, target, damage, isDirect) ->
# afterSuccessfulHit: (move, user, target, damage) ->
# beforeMove: (move, user, targets) ->
# isImmune: (type) ->
# switchOut: ->
# switchIn: ->
# beginTurn: ->
# endTurn: ->
# update: (owner) ->
# editBoosts: (stages) ->
# afterFaint: ->
# shouldBlockExecution: (move, user) ->
# Pokemon-specific attachments
# TODO: Turn Attachment into abstract class
# TODO: Move into own PokemonAttachment
# editHp: (stat) -> stat
# editAttack: (stat) -> stat
# editSpeed: (stat) -> stat
# editSpecialAttack: (stat) -> stat
# editDefense: (stat) -> stat
# editSpecialDefense: (stat) -> stat
# Used for effects like Tailwind or Reflect.
class @TeamAttachment extends @BaseAttachment
name: "TeamAttachment"
# Used for effects like Trick Room or Magic Room.
class @BattleAttachment extends @BaseAttachment
name: "BattleAttachment"
# An attachment that removes itself when a pokemon
# deactivates.
class @VolatileAttachment extends @BaseAttachment
name: "VolatileAttachment"
volatile: true
class @Attachment.Flinch extends @VolatileAttachment
name: "FlinchAttachment"
beforeMove: (move, user, targets) ->
@battle.cannedText('FLINCH', @pokemon)
@pokemon.boost(speed: 1) if @pokemon.hasAbility("Steadfast")
false
endTurn: ->
@pokemon.unattach(@constructor)
class @Attachment.Confusion extends @VolatileAttachment
name: "ConfusionAttachment"
passable: true
initialize: (attributes = {}) ->
cannedText = attributes.cannedText ? 'CONFUSION_START'
@turns = @battle?.rng.randInt(1, 4, "confusion turns") || 1
@pokemon?.tell(Protocol.POKEMON_ATTACH, @name)
@battle?.cannedText(cannedText, @pokemon)
@turn = 0
@preattach: (options, attributes) ->
{pokemon} = attributes
{source} = options
return false if (pokemon.team?.has(Attachment.Safeguard) && source != pokemon)
unattach: ->
@pokemon?.tell(Protocol.POKEMON_UNATTACH, @name)
beforeMove: (move, user, targets) ->
@battle.cannedText('IS_CONFUSED', @pokemon)
@turn++
if @turn > @turns
@battle.cannedText('CONFUSION_END', @pokemon)
@pokemon.unattach(@constructor)
else if @battle.rng.next('confusion') < 0.5
@battle.cannedText('CONFUSION_HURT_SELF', @pokemon)
damage = @battle.confusionMove.calculateDamage(@battle, user, user)
user.damage(damage, source: "move")
return false
class @Attachment.Disable extends @VolatileAttachment
name: "DisableAttachment"
@preattach: (options, attributes) ->
{pokemon} = attributes
move = options.move ? pokemon.lastMove
return false if !move? || !pokemon.knows(move) || pokemon.pp(move) <= 0
initialize: (attributes = {}) ->
@blockedMove = attributes.move ? @pokemon.lastMove
@turns = 4
@battle.cannedText('DISABLE_START', @pokemon, @blockedMove.name)
@pokemon.blockMove(@blockedMove)
beginTurn: ->
@pokemon.blockMove(@blockedMove)
beforeMove: (move, user, target) ->
if move == @blockedMove
@battle.cannedText('DISABLE_CONTINUE', @pokemon, move.name)
return false
endTurn: ->
@turns--
if @turns == 0
@battle.cannedText('DISABLE_END', @pokemon)
@pokemon.unattach(@constructor)
class @Attachment.Yawn extends @VolatileAttachment
name: 'YawnAttachment'
@preattach: (options, attributes) ->
{pokemon} = attributes
return false if pokemon.hasStatus()
return false if pokemon.team.has(Attachment.Safeguard)
initialize: (attributes = {}) ->
{@source} = attributes
@turn = 0
@battle.cannedText('YAWN_BEGIN', @pokemon)
endTurn: ->
@turn += 1
if @turn == 2
@pokemon.attach(Status.Sleep, {@source, bypassSafeguard: true})
@pokemon.unattach(@constructor)
# TODO: Does weight get lowered if speed does not change?
class @Attachment.Autotomize extends @VolatileAttachment
name: "AutotomizeAttachment"
maxLayers: -1
calculateWeight: (weight) ->
Math.max(weight - 100 * @layers, .1)
class @Attachment.Nightmare extends @VolatileAttachment
name: "NightmareAttachment"
@preattach: (options, attributes) ->
{pokemon} = attributes
return false if !pokemon.hasStatus(Status.Sleep)
initialize: ->
@battle.message("#{@pokemon.name} began having a nightmare!")
endTurn: ->
if @pokemon.has(Status.Sleep)
amount = Math.floor(@pokemon.stat('hp') / 4)
if @pokemon.damage(amount)
@battle.message "#{@pokemon.name} is locked in a nightmare!"
else
@pokemon.unattach(@constructor)
class @Attachment.Safeguard extends @TeamAttachment
name: "SafeguardAttachment"
initialize: (attributes) ->
{@source} = attributes
@turns = 5
@turn = 0
endTurn: ->
@turn++
if @turn >= @turns
@battle.cannedText('SAFEGUARD_END', @source)
@team.unattach(@constructor)
class @Attachment.Taunt extends @VolatileAttachment
name: "TauntAttachment"
initialize: (attributes) ->
@turns = 4
@turns = 3 if @battle.willMove(@pokemon)
@turn = 0
@battle.cannedText('TAUNT_START', @pokemon)
beginTurn: ->
for move in @pokemon.moves
if move.power == 0
@pokemon.blockMove(move)
beforeMove: (move, user, targets) ->
# TODO: user is always == pokemon. Will this change?
if user == @pokemon && move.power == 0
@battle.cannedText('TAUNT_PREVENT', @pokemon, move.name)
return false
endTurn: ->
@turn++
if @turn >= @turns
@battle.cannedText('TAUNT_END', @pokemon)
@pokemon.unattach(@constructor)
class @Attachment.HealBlock extends @VolatileAttachment
name: "HealBlockAttachment"
initialize: (attributes) ->
@turns = 5
@turn = 0
@battle.cannedText('HEAL_BLOCK_START', @pokemon)
beginTurn: ->
for move in @pokemon.moves
if move.hasFlag("heal")
@pokemon.blockMove(move)
beforeMove: (move, user, targets) ->
# TODO: user is always == pokemon. Will this change?
if user == @pokemon && move.hasFlag("heal")
@battle.cannedText('HEAL_BLOCK_PREVENT', @pokemon, move.name)
return false
endTurn: ->
@turn++
if @turn >= @turns
@battle.cannedText('HEAL_BLOCK_END', @pokemon)
@pokemon.unattach(@constructor)
class @Attachment.Tailwind extends @TeamAttachment
name: "TailwindAttachment"
initialize: ->
@turns = 4
@turn = 0
editSpeed: (speed) ->
2 * speed
endTurn: ->
@turn++
if @turn >= @turns
@battle.cannedText('TAILWIND_END')
@team.unattach(@constructor)
class @Attachment.Wish extends @TeamAttachment
name: "WishAttachment"
initialize: (attributes) ->
{user} = attributes
@amount = Math.round(user.stat('hp') / 2)
@wisherName = user.name
@slot = @team.indexOf(user)
@turns = 2
@turn = 0
endTurn: ->
@turn++
if @turn >= @turns
pokemon = @team.at(@slot)
if !pokemon.isFainted()
@battle.cannedText('WISH_END', @wisherName)
pokemon.heal(@amount)
@team.unattach(@constructor)
class @Attachment.PerishSong extends @VolatileAttachment
name: "PerishSongAttachment"
passable: true
initialize: ->
@turns = 4
@turn = 0
@battle.cannedText('PERISH_SONG_START')
endTurn: ->
@turn++
@battle.cannedText('PERISH_SONG_CONTINUE', @pokemon, @turns - @turn)
if @turn >= @turns
@pokemon.faint()
@pokemon.unattach(@constructor)
class @Attachment.Roost extends @VolatileAttachment
name: "RoostAttachment"
initialize: ->
@oldTypes = @pokemon.types
@pokemon.types = (type for type in @pokemon.types when type != 'Flying')
if @pokemon.types.length == 0 then @pokemon.types = [ 'Normal' ]
endTurn: ->
@pokemon.types = @oldTypes
@pokemon.unattach(@constructor)
class @Attachment.Encore extends @VolatileAttachment
name: "EncoreAttachment"
initialize: ->
@turns = 4
@turns = 3 if @battle.willMove(@pokemon)
@turn = 0
@move = @pokemon.lastMove
beginTurn: ->
@pokemon.lockMove(@move)
endTurn: ->
@turn++
if @turn >= @turns || @pokemon.pp(@move) == 0
@battle.cannedText('ENCORE_END', @pokemon)
@pokemon.unattach(@constructor)
class @Attachment.Torment extends @VolatileAttachment
name: "TormentAttachment"
initialize: ->
@battle.cannedText('TORMENT_START', @pokemon)
beginTurn: ->
@pokemon.blockMove(@pokemon.lastMove) if @pokemon.lastMove?
class @Attachment.Spikes extends @TeamAttachment
name: "SpikesAttachment"
maxLayers: 3
initialize: ->
id = @team.playerId
@battle.cannedText('SPIKES_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
return if pokemon.isImmune("Ground")
fraction = (10 - 2 * @layers)
hp = pokemon.stat('hp')
damage = Math.floor(hp / fraction)
if pokemon.damage(damage)
@battle.cannedText('SPIKES_HURT', pokemon)
unattach: ->
id = @team.playerId
@battle.cannedText('SPIKES_END', @battle.getPlayerIndex(id))
class @Attachment.StealthRock extends @TeamAttachment
name: "StealthRockAttachment"
initialize: ->
id = @team.playerId
@battle.cannedText('STEALTH_ROCK_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
multiplier = util.typeEffectiveness("Rock", pokemon.types)
hp = pokemon.stat('hp')
damage = ((hp * multiplier) >> 3)
if pokemon.damage(damage)
@battle.cannedText('STEALTH_ROCK_HURT', pokemon)
unattach: ->
id = @team.playerId
@battle.cannedText('STEALTH_ROCK_END', @battle.getPlayerIndex(id))
class @Attachment.ToxicSpikes extends @TeamAttachment
name: "ToxicSpikesAttachment"
maxLayers: 2
initialize: ->
id = @team.playerId
@battle.cannedText('TOXIC_SPIKES_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
if pokemon.hasType("Poison") && !pokemon.isImmune("Ground")
@team.unattach(@constructor)
return if pokemon.isImmune("Poison") || pokemon.isImmune("Ground")
if @layers == 1
pokemon.attach(Status.Poison)
else
pokemon.attach(Status.Toxic)
unattach: ->
id = @team.playerId
@battle.cannedText('TOXIC_SPIKES_END', @battle.getPlayerIndex(id))
# A trap created by Fire Spin, Magma Storm, Bind, Clamp, etc
class @Attachment.Trap extends @VolatileAttachment
name: "TrapAttachment"
initialize: (attributes) ->
{@moveName, @user, @turns} = attributes
@user.attach(Attachment.TrapLeash, target: @pokemon)
beginTurn: ->
@pokemon.blockSwitch()
endTurn: ->
# For the first numTurns turns it will damage, and at numTurns + 1 it will wear off.
# Therefore, if @turns = 5, this attachment should actually last for 6 turns.
if @turns == 0
@pokemon.unattach(@constructor)
else
amount = Math.floor(@pokemon.stat('hp') / @getDamagePerTurn())
if @pokemon.damage(amount)
@battle.cannedText('TRAP_HURT', @pokemon, @moveName)
@turns -= 1
getDamagePerTurn: ->
if @user.hasItem("Binding Band")
8
else
16
unattach: ->
@battle.cannedText('FREE_FROM', @pokemon, @moveName)
@user.unattach(Attachment.TrapLeash)
delete @user
# If the creator if fire spin switches out, the trap will end
# TODO: What happens if another ability removes the trap, and then firespin is used again?
class @Attachment.TrapLeash extends @VolatileAttachment
name: "TrapLeashAttachment"
initialize: (attributes) ->
{@target} = attributes
unattach: ->
@target.unattach(Attachment.Trap)
delete @target
# Has a 50% chance to immobilize a Pokemon before it moves.
class @Attachment.Attract extends @VolatileAttachment
name: "AttractAttachment"
@preattach: (options, attributes) ->
{source} = options
{pokemon} = attributes
return false if (!(pokemon.gender == 'M' && source.gender == 'F') &&
!(pokemon.gender == 'F' && source.gender == 'M'))
initialize: (attributes) ->
{@source} = attributes
if @pokemon.hasItem("Destiny Knot") && !@source.has(Attachment.Attract)
@pokemon.removeItem()
@source.attach(Attachment.Attract, source: @pokemon)
@battle.message("#{@pokemon.name} fell in love with #{@source.name}!")
beforeMove: (move, user, targets) ->
if @source not in @battle.getOpponents(@pokemon)
@pokemon.unattach(@constructor)
return
if @battle.rng.next('attract chance') < .5
@battle.message "#{@pokemon.name} is immobilized by love!"
return false
class @Attachment.FocusEnergy extends @VolatileAttachment
name: "FocusEnergyAttachment"
passable: true
class @Attachment.MicleBerry extends @VolatileAttachment
name: "MicleBerryAttachment"
initialize: ->
@turns = 1
editAccuracy: (accuracy) ->
Math.floor(accuracy * 1.2)
endTurn: ->
if @turns == 0
@pokemon.unattach(@constructor)
else
@turns--
class @Attachment.Metronome extends @VolatileAttachment
name: "MetronomeAttachment"
maxLayers: 5
initialize: (attributes) ->
{@move} = attributes
beforeMove: (move) ->
@pokemon.unattach(@constructor) if move != @move
class @Attachment.Screen extends @TeamAttachment
name: "ScreenAttachment"
initialize: (attributes) ->
{user} = attributes
@turns = (if user?.hasItem("Light Clay") then 8 else 5)
endTurn: ->
@turns--
if @turns == 0
@team.unattach(@constructor)
unattach: ->
@team.tell(Protocol.TEAM_UNATTACH, @name)
class @Attachment.Reflect extends @Attachment.Screen
name: "ReflectAttachment"
modifyDamageTarget: (move, user) ->
if move.isPhysical() && !user.crit && !user.hasAbility("Infiltrator")
return 0x800
return 0x1000
class @Attachment.LightScreen extends @Attachment.Screen
name: "LightScreenAttachment"
modifyDamageTarget: (move, user) ->
if move.isSpecial() && !user.crit && !user.hasAbility("Infiltrator")
return 0x800
return 0x1000
class @Attachment.Identify extends @VolatileAttachment
name: "IdentifyAttachment"
initialize: (attributes) ->
{@types} = attributes
editBoosts: (stages) ->
stages.evasion = 0
stages
isImmune: (type) ->
return false if type in @types
class @Attachment.DefenseCurl extends @VolatileAttachment
name: "DefenseCurl"
class @Attachment.FocusPunch extends @VolatileAttachment
name: "FocusPunchAttachment"
beforeMove: (move, user, targets) ->
hit = user.lastHitBy
return if !hit?
if !hit.move.isNonDamaging() && hit.turn == @battle.turn && hit.direct
@battle.message "#{user.name} lost its focus and couldn't move!"
return false
afterMove: ->
@pokemon.unattach(@constructor)
class @Attachment.MagnetRise extends @VolatileAttachment
name: "MagnetRiseAttachment"
passable: true
initialize: ->
@turns = 5
isImmune: (type) ->
return true if type == "Ground"
endTurn: ->
@turns -= 1
@pokemon.unattach(@constructor) if @turns == 0
class @Attachment.LockOn extends @VolatileAttachment
name: "LockOnAttachment"
initialize: (attributes) ->
{@target} = attributes
@turns = 2
# Effect hardcoded in Move#willMiss
endTurn: ->
@turns -= 1
@pokemon.unattach(@constructor) if @turns == 0
class @Attachment.Minimize extends @VolatileAttachment
name: "MinimizeAttachment"
class @Attachment.MeanLook extends @VolatileAttachment
name: "MeanLookAttachment"
initialize: (attributes) ->
{@user} = attributes
@user.attach(Attachment.MeanLookLeash, target: @pokemon)
beginTurn: ->
@pokemon.blockSwitch()
unattach: ->
@user.unattach(Attachment.MeanLookLeash)
delete @user
class @Attachment.MeanLookLeash extends @VolatileAttachment
name: "MeanLookAttachment"
initialize: (attributes) ->
{@target} = attributes
unattach: ->
@target.unattach(Attachment.MeanLook)
delete @target
class @Attachment.Recharge extends @VolatileAttachment
name: "RechargeAttachment"
initialize: ->
@turns = 2
beginTurn: ->
@pokemon.blockSwitch()
@pokemon.blockMoves()
id = @battle.getOwner(@pokemon)
@battle.recordMove(id, @battle.getMove("Recharge"))
beforeMove: (move, user, targets) ->
@battle.message "#{user.name} must recharge!"
return false
endTurn: ->
@turns -= 1
@pokemon.unattach(@constructor) if @turns == 0
class @Attachment.Momentum extends @VolatileAttachment
name: "MomentumAttachment"
maxLayers: 5
initialize: (attributes) ->
{@move} = attributes
@turns = 1
beginTurn: ->
@pokemon.blockSwitch()
@pokemon.lockMove(@move)
endTurn: ->
@turns -= 1
@pokemon.unattach(@constructor) if @turns == 0 || @layers == @maxLayers
class @Attachment.MeFirst extends @VolatileAttachment
name: "MeFirstAttachment"
modifyAttack: ->
0x1800
endTurn: ->
@pokemon.unattach(@constructor)
class @Attachment.Charge extends @VolatileAttachment
name: "ChargeAttachment"
initialize: ->
@turns = 2
modifyAttack: (move, target) ->
return 0x2000 if move.getType(@battle, @pokemon, target) == 'Electric'
return 0x1000
endTurn: ->
@turns -= 1
@pokemon.unattach(@constructor) if @turns == 0
class @Attachment.LeechSeed extends @VolatileAttachment
name: "LeechSeedAttachment"
passable: true
initialize: (attributes) ->
{@source} = attributes
@slot = @source.team.indexOf(@source)
@battle.cannedText('LEECH_SEED_START', @pokemon)
endTurn: ->
user = @source.team.at(@slot)
return if user.isFainted() || @pokemon.isFainted()
hp = @pokemon.stat('hp')
damage = Math.min(Math.floor(hp / 8), @pokemon.currentHP)
if @pokemon.damage(damage)
user.drain(damage, @pokemon)
@battle.cannedText('LEECH_SEED_HURT', @pokemon)
unattach: ->
@battle.cannedText('FREE_FROM', @pokemon, "Leech Seed")
class @Attachment.ProtectCounter extends @VolatileAttachment
name: "ProtectCounterAttachment"
maxLayers: -1
successMultiplier: 2
successChance: ->
x = Math.pow(@successMultiplier, @layers - 1)
if @layers > 8 then Math.pow(2, 32) else x
endTurn: ->
@turns--
@pokemon.unattach(@constructor) if @turns == 0
class @Attachment.Protect extends @VolatileAttachment
name: "ProtectAttachment"
initialize: ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
shouldBlockExecution: (move, user) ->
return true if move.hasFlag("protect")
endTurn: ->
@pokemon.unattach(@constructor)
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.Endure extends @VolatileAttachment
name: "EndureAttachment"
endTurn: ->
@pokemon.unattach(@constructor)
transformHealthChange: (amount, options) ->
if amount >= @pokemon.currentHP
@battle.message "#{@pokemon.name} endured the hit!"
return @pokemon.currentHP - 1
return amount
class @Attachment.Curse extends @VolatileAttachment
name: "CurseAttachment"
passable: true
endTurn: ->
amount = Math.floor(@pokemon.stat('hp') / 4)
if @pokemon.damage(amount)
@battle.message "#{@pokemon.name} was afflicted by the curse!"
class @Attachment.DestinyBond extends @VolatileAttachment
name: "DestinyBondAttachment"
isAliveCheck: -> true
initialize: ->
@battle.cannedText('DESTINY_BOND_START', @pokemon)
afterFaint: ->
pokemon = @battle.currentPokemon
if pokemon? && pokemon.isAlive()
pokemon.faint()
@battle.cannedText('DESTINY_BOND_CONTINUE', @pokemon)
beforeMove: (move, user, targets) ->
@pokemon.unattach(@constructor)
class @Attachment.Grudge extends @VolatileAttachment
name: "GrudgeAttachment"
isAliveCheck: -> true
afterFaint: ->
hit = @pokemon.lastHitBy
return if !hit
{team, slot, move, turn} = hit
pokemon = team.at(slot)
if pokemon.isAlive() && !move.isNonDamaging()
pokemon.setPP(move, 0)
@battle.message "#{pokemon.name}'s #{move.name} lost all its PP due to the grudge!"
beforeMove: (move, user, targets) ->
@pokemon.unattach(@constructor)
class @Attachment.Pursuit extends @VolatileAttachment
name: "PursuitAttachment"
informSwitch: (switcher) ->
team = switcher.team
return if team.has(Attachment.BatonPass)
pursuit = @battle.getMove('Pursuit')
@battle.cancelAction(@pokemon)
@pokemon.attach(Attachment.PursuitModifiers)
# TODO: You will have to record the target for 2v2.
@battle.performMove(@pokemon, pursuit)
@pokemon.unattach(Attachment.PursuitModifiers)
@pokemon.unattach(@constructor)
beforeMove: ->
@pokemon.unattach(@constructor)
endTurn: ->
@pokemon.unattach(@constructor)
class @Attachment.PursuitModifiers extends @VolatileAttachment
name: "PursuitModifiersAttachment"
editAccuracy: ->
0 # Always hits
class @Attachment.Substitute extends @VolatileAttachment
name: "SubstituteAttachment"
passable: true
reinitializeOnPass: true
initialize: (attributes) ->
{@hp} = attributes || this
@pokemon?.tell(Protocol.POKEMON_ATTACH, @name)
transformHealthChange: (damage, options = {}) ->
if options.direct != false
# Substitute does not trigger on direct damage
return damage
@hp -= damage
if @hp <= 0
@battle.cannedText('SUBSTITUTE_END', @pokemon)
@hp = 0
else
@battle.cannedText('SUBSTITUTE_HURT', @pokemon)
return 0
failsOnSub: (move, user) ->
@pokemon != user && move.isNonDamaging() &&
!move.isDirectHit(@battle, user, @pokemon)
shouldBlockExecution: (move, user) ->
if @failsOnSub(move, user)
move.fail(@battle, user)
return true
afterBeingHit: (move, user, target) ->
@pokemon.unattach(@constructor) if @hp <= 0
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.Stockpile extends @VolatileAttachment
name: "StockpileAttachment"
maxLayers: 3
class @Attachment.Rage extends @VolatileAttachment
name: "RageAttachment"
beforeMove: (move, user, targets) ->
@pokemon.unattach(@constructor)
afterBeingHit: (move, user, target) ->
return if move.isNonDamaging()
target.boost(attack: 1)
@battle.message "#{target.name}'s rage is building!"
class @Attachment.ChipAway extends @VolatileAttachment
name: "ChipAwayAttachment"
editBoosts: (stages) ->
stages.evasion = 0
stages.defense = 0
stages.specialDefense = 0
stages
class @Attachment.AquaRing extends @VolatileAttachment
name: "AquaRingAttachment"
passable: true
endTurn: ->
amount = Math.floor(@pokemon.stat('hp') / 16)
# Aqua Ring is considered a drain move for the purposes of Big Root.
@pokemon.drain(amount, @pokemon)
@battle.message "Aqua Ring restored #{@pokemon.name}'s HP!"
class @Attachment.Ingrain extends @VolatileAttachment
name: "IngrainAttachment"
passable: true
initialize: ->
@battle.message("#{@pokemon.name} planted its roots!")
endTurn: ->
amount = Math.floor(@pokemon.stat('hp') / 16)
# Ingrain is considered a drain move for the purposes of Big Root.
@pokemon.drain(amount, @pokemon)
@battle.message "#{@pokemon.name} absorbed nutrients with its roots!"
beginTurn: ->
@pokemon.blockSwitch()
shouldPhase: (phaser) ->
@battle.message "#{@pokemon.name} anchored itself with its roots!"
return false
shouldBlockExecution: (move, user) ->
if move == @battle.getMove("Telekinesis")
move.fail(@battle, user)
return true
isImmune: (type) ->
return false if type == 'Ground'
class @Attachment.Embargo extends @VolatileAttachment
name: "EmbargoAttachment"
passable: true
initialize: ->
@turns = 5
@pokemon.blockItem()
@battle.message("#{@pokemon.name} can't use items anymore!")
beginTurn: ->
@pokemon.blockItem()
endTurn: ->
@turns--
if @turns == 0
@battle.message "#{@pokemon.name} can use items again!"
@pokemon.unattach(@constructor)
class @Attachment.Charging extends @VolatileAttachment
name: "ChargingAttachment"
initialize: (attributes) ->
{@message, @vulnerable, @move, @condition} = attributes
@charging = false
beforeMove: (move, user, targets) ->
if user.hasItem("Power Herb")
@battle.message "#{user.name} became fully charged due to its Power Herb!"
@charging = true
user.removeItem()
if @charging || @condition?(@battle, move, user, targets)
@pokemon.unattach(@constructor)
return
@charging = true
@battle.message @message.replace("$1", user.name)
return false
beginTurn: ->
# TODO: Add targets
id = @battle.getOwner(@pokemon)
@battle.recordMove(id, @move)
editEvasion: (accuracy, move) ->
return -1 if @vulnerable && @charging &&
(move not in @vulnerable.map((v) => @battle.getMove(v)))
return accuracy
unattach: ->
delete @move
delete @message
delete @vulnerable
class @Attachment.FuryCutter extends @VolatileAttachment
name: "FuryCutterAttachment"
maxLayers: 3
initialize: (attributes) ->
{@move} = attributes
beforeMove: (move, user, targets) ->
@pokemon.unattach(@constructor) if move != @move
class @Attachment.Imprison extends @VolatileAttachment
name: "ImprisonAttachment"
initialize: (attributes) ->
{@moves} = attributes
for pokemon in @battle.getOpponents(@pokemon)
pokemon.attach(Attachment.ImprisonPrevention, {@moves})
beginTurn: ->
for pokemon in @battle.getOpponents(@pokemon)
pokemon.attach(Attachment.ImprisonPrevention, {@moves})
switchOut: ->
for pokemon in @battle.getOpponents(@pokemon)
pokemon.unattach(Attachment.ImprisonPrevention)
class @Attachment.ImprisonPrevention extends @VolatileAttachment
name: "ImprisonPreventionAttachment"
initialize: (attributes) ->
{@moves} = attributes
beginTurn: ->
@pokemon.blockMove(move) for move in @moves
beforeMove: (move, user, targets) ->
if move in @moves
@battle.message "#{user.name} can't use the sealed #{move.name}!"
return false
class @Attachment.Present extends @VolatileAttachment
name: "PresentAttachment"
initialize: (attributes) ->
{@power} = attributes
endTurn: ->
@pokemon.unattach(@constructor)
# Lucky Chant's CH prevention is inside Move#isCriticalHit.
class @Attachment.LuckyChant extends @TeamAttachment
name: "LuckyChantAttachment"
initialize: ->
@turns = 5
endTurn: ->
@turns--
if @turns == 0
# TODO: Less hacky?
id = (id for id in @battle.playerIds when @battle.getTeam(id) == @team)[0]
@battle.message "#{id}'s team's Lucky Chant wore off!"
@team.unattach(@constructor)
class @Attachment.LunarDance extends @TeamAttachment
name: "LunarDanceAttachment"
switchIn: (pokemon) ->
@battle.message "#{pokemon.name} became cloaked in mystical moonlight!"
pokemon.setHP(pokemon.stat('hp'))
pokemon.cureStatus()
pokemon.resetAllPP()
@team.unattach(@constructor)
class @Attachment.HealingWish extends @TeamAttachment
name: "HealingWishAttachment"
switchIn: (pokemon) ->
@battle.message "The healing wish came true for #{pokemon.name}!"
pokemon.setHP(pokemon.stat('hp'))
pokemon.cureStatus()
@team.unattach(@constructor)
class @Attachment.MagicCoat extends @VolatileAttachment
name: "MagicCoatAttachment"
initialize: ->
@bounced = false
shouldBlockExecution: (move, user) ->
return unless move.hasFlag("reflectable")
return if user.get(Attachment.MagicCoat)?.bounced
return if @bounced
@bounced = true
@battle.cannedText('BOUNCE_MOVE', @pokemon, move.name)
move.execute(@battle, @pokemon, [ user ])
return true
shouldBlockFieldExecution: (move, userId) ->
return unless move.hasFlag("reflectable")
return if @bounced
team = @battle.getTeam(userId)
for p in team.getActiveAlivePokemon()
return if p.get(Attachment.MagicCoat)?.bounced
for p in @team.getActiveAlivePokemon()
continue unless p.has(Attachment.MagicCoat)
@bounced = true
@battle.cannedText('BOUNCE_MOVE', p, move.name)
@battle.executeMove(move, p, [ userId ])
return true
endTurn: ->
if @pokemon?
@pokemon.unattach(@constructor)
else if @team?
@team.unattach(@constructor)
class @Attachment.Telekinesis extends @VolatileAttachment
name: "TelekinesisAttachment"
initialize: ->
@turns = 3
@battle.cannedText('TELEKINESIS_START', @pokemon)
editEvasion: (accuracy, move) ->
if move.hasFlag("ohko") then accuracy else 0
isImmune: (type) ->
return true if type == 'Ground'
endTurn: ->
@turns--
if @turns == 0
@battle.cannedText('TELEKINESIS_END', @pokemon)
@pokemon.unattach(@constructor)
class @Attachment.SmackDown extends @VolatileAttachment
name: "SmackDownAttachment"
isImmune: (type) ->
return false if type == 'Ground'
shouldBlockExecution: (move, user) ->
if move in [ @battle.getMove("Telekinesis"), @battle.getMove("Magnet Rise") ]
move.fail(@battle, user)
return true
class @Attachment.EchoedVoice extends @BattleAttachment
name: "EchoedVoiceAttachment"
maxLayers: 4
initialize: ->
@turns = 2
endTurn: ->
@turns--
@battle.unattach(@constructor) if @turns == 0
class @Attachment.Rampage extends @VolatileAttachment
name: "RampageAttachment"
maxLayers: -1
initialize: (attributes) ->
{@move} = attributes
@turns = @battle.rng.randInt(2, 3, "rampage turns")
@turn = 0
beginTurn: ->
@pokemon.blockSwitch()
@pokemon.lockMove(@move)
afterMove: ->
@turn++
if @turn >= @turns
@pokemon.attach(Attachment.Confusion, cannedText: 'FATIGUE')
@pokemon.unattach(@constructor)
else
# afterSuccessfulHit increases the number of layers. If the number of
# layers is not keeping up with the number of turns passed, then the
# Pokemon's move was interrupted and we should stop rampaging.
@pokemon.unattach(@constructor) if @turn > @layers
# The way Trick Room reverses turn order is implemented in Battle#sortActions.
class @Attachment.TrickRoom extends @BattleAttachment
name: "TrickRoomAttachment"
initialize: ->
@turns = 5
endTurn: ->
@turns--
if @turns == 0
@battle.unattach(@constructor)
unattach: ->
@battle.cannedText('TRICK_ROOM_END')
class @Attachment.Transform extends @VolatileAttachment
name: "TransformAttachment"
@preattach: (attributes) ->
{target} = attributes
!target.has(Attachment.Transform)
initialize: (attributes) ->
@pokemon.activateAbility()
{target} = attributes
# Save old data
{@ability, @moves, @stages, @baseStats, @evs} = @pokemon
{@types, @gender, @weight, @ppHash, @maxPPHash} = @pokemon
# This data is safe to be copied.
@pokemon.copyAbility(target.ability, reveal: false)
@pokemon.gender = target.gender
@pokemon.weight = target.weight
# The rest aren't.
@pokemon.moves = _.clone(target.moves)
@pokemon.types = _.clone(target.types)
@pokemon.evs = _.extend({}, target.evs, hp: @pokemon.evs.hp)
@pokemon.baseStats = _.extend({}, target.baseStats, hp: @pokemon.baseStats.hp)
@pokemon.setBoosts(target.stages)
@pokemon.resetAllPP(5)
# Send updated information to the client
@pokemon.changeSprite(target.species, target.forme)
@pokemon.tellPlayer(Protocol.MOVESET_UPDATE, @pokemon.movesetJSON())
unattach: ->
# Restore old data
@pokemon.ability = @ability
@pokemon.moves = @moves
@pokemon.types = @types
@pokemon.gender = @gender
@pokemon.weight = @weight
@pokemon.ppHash = @ppHash
@pokemon.maxPPHash = @maxPPHash
@pokemon.baseStats = @baseStats
@pokemon.evs = @evs
@pokemon.setBoosts(@stages)
@pokemon.changeSprite(@pokemon.species, @pokemon.forme)
@pokemon.tellPlayer(Protocol.MOVESET_UPDATE, @pokemon.movesetJSON())
class @Attachment.Illusion extends @BattleAttachment
name: "IllusionAttachment"
@preattach: (attributes) ->
#{target} = attributes
#!target.has(Attachment.Illusion)
initialize: (attributes) ->
{target} = attributes
if @pokemon.currentHP == @pokemon.stat('hp')
@originalname = @pokemon.name
@pokemon.name = target.name
# Send updated information to the client
@pokemon.tell(Protocol.ILLUSION_CHANGE, true)
@pokemon.tell(Protocol.NAME_CHANGE, @pokemon.name)
unattach: ->
@pokemon.name = @pokemon.originalname
@pokemon.tell(Protocol.NAME_CHANGE, @pokemon.originalname)
@pokemon.changeSprite(@pokemon.species, @pokemon.forme)
@pokemon.tell(Protocol.ILLUSION_CHANGE, false)
@battle.cannedText('ILLUSION_BROKE', @pokemon)
afterBeingHit: (move, user, target) ->
if move.isDirectHit(@battle, user, @pokemon) and !move.isNonDamaging()
console.log('unattached')
@pokemon.unattach(@constructor)
afterFaint: ->
@pokemon.unattach(@constructor)
class @Attachment.Fling extends @VolatileAttachment
name: "FlingAttachment"
initialize: ->
@item = null
beforeMove: (move, user, targets) ->
# The move may be changed by something like Encore
return if move != @battle.getMove("Fling")
if user.hasItem() && user.hasTakeableItem() && !user.isItemBlocked()
@item = user.getItem()
user.removeItem()
endTurn: ->
@pokemon.unattach(@constructor)
class @Attachment.Gravity extends @BattleAttachment
name: "GravityAttachment"
initialize: ->
@turns = 5
@beginTurn()
beginTurn: ->
for pokemon in @battle.getActivePokemon()
pokemon.attach(Attachment.GravityPokemon)
for move in pokemon.moves
pokemon.blockMove(move) if move.hasFlag("gravity")
endTurn: ->
@turns--
if @turns == 0
@battle.message "Gravity turned to normal!"
@battle.unattach(@constructor)
class @Attachment.GravityPokemon extends @VolatileAttachment
name: "GravityPokemonAttachment"
beforeMove: (move, user, target) ->
if move.hasFlag("gravity")
@battle.message "#{user.name} can't use #{move.name} because of gravity!"
return false
editAccuracy: (accuracy) ->
Math.floor(accuracy * 5 / 3)
isImmune: (type) ->
return false if type == 'Ground'
shouldIgnoreImmunity: (moveType, target) ->
return target.hasType("Flying") && moveType == 'Ground'
endTurn: ->
@pokemon.unattach(@constructor)
class @Attachment.DelayedAttack extends @TeamAttachment
name: "DelayedAttackAttachment"
initialize: (attributes) ->
{@move, @user} = attributes
@slot = 0
@turns = 3
endTurn: ->
@turns--
if @turns == 0
pokemon = @team.at(@slot)
if pokemon.isAlive()
@battle.message "#{pokemon.name} took the #{@move.name} attack!"
isDirect = @move.isDirectHit(@battle, @user, pokemon)
damage = @move.hit(@battle, @user, pokemon, 1, isDirect)
@move.afterHit(@battle, @user, pokemon, damage, isDirect)
@team.unattach(@constructor)
class @Attachment.BatonPass extends @TeamAttachment
name: "BatonPassAttachment"
initialize: (attributes) ->
{@slot, @attachments, @stages} = attributes
switchIn: (pokemon) ->
return if @slot != @team.indexOf(pokemon)
# Nasty stitching of attachments to the recipient.
for attachment in @attachments
attachment.pokemon = pokemon
attachment.team = pokemon.team
attachment.battle = pokemon.battle
attachment.attached = true
index = (pokemon.attachments.attachments.push(attachment)) - 1
if attachment.reinitializeOnPass
pokemon.attachments.attachments[index]?.initialize?()
pokemon.setBoosts(@stages)
@team.unattach(@constructor)
class @Attachment.FlashFire extends @VolatileAttachment
name: "FlashFireAttachment"
modifyBasePower: (move, target) ->
return 0x1000 if move.getType(@battle, @pokemon, target) != 'Fire'
return 0x1800
class @Attachment.Unburden extends @VolatileAttachment
name: "UnburdenAttachment"
editSpeed: (speed) ->
if @pokemon.hasAbility("Unburden") && !@pokemon.hasItem()
2 * speed
else
@pokemon.unattach(@constructor)
speed
# Cancels the opponent's ability for one turn
class @Attachment.AbilityCancel extends @VolatileAttachment
name: "AbilityCancelAttachment"
initialize: ->
if !@pokemon.isAbilityBlocked()
@shouldUnblock = true
@pokemon.blockAbility()
unattach: ->
if @shouldUnblock then @pokemon.unblockAbility()
endTurn: ->
@pokemon.unattach(@constructor)
# Suppresses the opponent's ability until they switch out
class @Attachment.AbilitySuppress extends @VolatileAttachment
name: "AbilitySuppressAttachment"
passable: true
@preattach: (options, attributes) ->
{pokemon} = attributes
return false if !pokemon.hasChangeableAbility()
initialize: ->
@pokemon.blockAbility()
beginTurn: this::initialize
class @Attachment.SleepClause extends @BaseAttachment
name: "SleepClause"
shouldAttach: (attachment, options) ->
{source} = options
return if attachment != Status.Sleep
return if !source || source.team == @pokemon.team
pokemonSleptByOtherTeams = @pokemon.team.filter (p) =>
return false if p.isFainted()
source = p.get(Status.Sleep)?.source
return source && source.team != @pokemon.team
# Attach if we have no pokemon slept by other teams.
return pokemonSleptByOtherTeams.length == 0
class @StatusAttachment extends @BaseAttachment
name: "StatusAttachment"
@status: true
@preattach: (options, attributes) ->
{battle, pokemon} = attributes
{source, force, bypassSafeguard} = options
force ?= false
if !force
return false if pokemon.hasStatus()
return false if (pokemon.team?.has(Attachment.Safeguard) && source != pokemon && !bypassSafeguard)
return false unless @worksOn(battle, pokemon)
if (source && this in [ Status.Toxic, Status.Burn, Status.Poison, Status.Paralyze ] && pokemon.hasAbility("Synchronize")) or
(source && this in [ Status.Toxic, Status.Burn, Status.Poison, Status.Paralyze ] && pokemon.hasAbility("Protean Maxima") && pokemon.getForme == "mega-dark")
return false if source == pokemon
source.attach(this) # Do not attach source
battle.message "#{pokemon.name} synchronized its status with #{source.name}!"
else
pokemon.cureStatus(message: false)
pokemon.status = this
return true
initialize: (attributes = {}) ->
# We store the source for use in other places, like Sleep Clause.
{@source} = attributes
@battle?.cannedText("#{@constructor.name.toUpperCase()}_START", @pokemon)
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
@worksOn: (battle, pokemon) ->
true
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
@pokemon.status = null
class @Status.Paralyze extends @StatusAttachment
name: "Paralyze"
beforeMove: (move, user, targets) ->
if @battle.rng.next('paralyze chance') < .25
@battle.cannedText('PARALYZE_CONTINUE', @pokemon)
return false
editSpeed: (stat) ->
if @pokemon.hasAbility("Quick Feet") then stat else stat >> 2
class @Status.Freeze extends @StatusAttachment
name: "Freeze"
@worksOn: (battle, pokemon) ->
!(pokemon.hasType("Ice") || battle?.hasWeather(Weather.SUN))
beforeMove: (move, user, targets) ->
if move.thawsUser || @battle.rng.next('unfreeze chance') < .2
@pokemon.cureStatus()
else
@battle.cannedText('FREEZE_CONTINUE', @pokemon)
return false
afterBeingHit: (move, user, target) ->
if !move.isNonDamaging() && move.type == 'Fire'
@pokemon.cureStatus()
class @Status.Poison extends @StatusAttachment
name: "Poison"
@worksOn: (battle, pokemon) ->
!(pokemon.hasType("Poison") || pokemon.hasType("Steel"))
endTurn: ->
return if @pokemon.hasAbility("Poison Heal")
if @pokemon.damage(@pokemon.stat('hp') >> 3)
@battle.cannedText('POISON_CONTINUE', @pokemon)
class @Status.Toxic extends @StatusAttachment
name: "Toxic"
@worksOn: (battle, pokemon) ->
!(pokemon.hasType("Poison") || pokemon.hasType("Steel"))
initialize: ->
super()
@counter = 0
switchOut: ->
@counter = 0
endTurn: ->
@counter = Math.min(@counter + 1, 15)
return if @pokemon.hasAbility("Poison Heal")
if @pokemon.damage(Math.max(@pokemon.stat('hp') >> 4, 1) * @counter)
@battle.cannedText('POISON_CONTINUE', @pokemon)
class @Status.Sleep extends @StatusAttachment
name: "Sleep"
initialize: (attributes) ->
super(attributes)
@counter = 0
{@turns} = attributes
if !@turns && @battle?
@turns = @battle.rng.randInt(1, 3, "sleep turns")
switchOut: ->
@counter = 0
beforeMove: (move, user, targets) ->
@counter += 1 if @pokemon.hasAbility("Early Bird")
@counter += 1
if @counter > @turns
@pokemon.cureStatus()
else
@battle.cannedText('SLEEP_CONTINUE', @pokemon)
return false unless move.usableWhileAsleep
class @Status.Burn extends @StatusAttachment
name: "Burn"
@worksOn: (battle, pokemon) ->
!pokemon.hasType("Fire")
endTurn: ->
if @pokemon.damage(@pokemon.stat('hp') >> 3)
@battle.cannedText('BURN_CONTINUE', @pokemon)