{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)