{_} = require 'underscore' {User, MaskedUser} = require '../user' {FakeRNG} = require './rng' {Pokemon} = require './pokemon' {Move} = require './move' {Team} = require './team' {Weather} = require '../../shared/weather' {Attachment, Attachments, Status, BaseAttachment} = require './attachment' {Protocol} = require '../../shared/protocol' {CannedText} = require('../../shared/canned_text') Query = require './queries' {Room} = require '../rooms' logger = require '../logger' # Represents a single ongoing battle class @Battle extends Room {Moves, MoveList, SpeciesData, FormeData} = require './data' Moves: Moves MoveList: MoveList SpeciesData: SpeciesData FormeData: FormeData generation: 'bw' # 1 hour ONGOING_BATTLE_TTL: 1 * 60 * 60 * 1000 # 30 minutes ENDED_BATTLE_TTL: 30 * 60 * 1000 actionMap: switch: priority: -> 10 action: (action) -> @performSwitch(action.pokemon, action.to) move: priority: (action) -> {move} = action {priority} = move action.pokemon.editPriority(priority, move) action: (action) -> @performMove(action.pokemon, action.move) constructor: (@id, @players, attributes = {}) -> super(@id) # Number of pokemon on each side of the field @numActive = attributes.numActive || 1 # Which battling format was selected @format = attributes.format # An array of conditions like clauses or team preview that this battle has. # TODO: Remove @conditions = (attributes.conditions && _.clone(attributes.conditions)) @conditions ||= [] # Stores the current turn of the battle @turn = 0 # Stores the actions each player is about to make @pokemonActions = [] # Stores the current and completed requests for action. # Keyed by player.id @requests = {} @completedRequests = {} # Creates a RNG for this battle. @rng = new FakeRNG() # Current battle weather. @weather = Weather.NONE # Current turn duration for the weather. -1 means infinity. @weatherDuration = -1 # Stores last move used @lastMove = null # Stores current pokemon moving @currentPokemon = null # Stores the confusion recoil move as it may be different cross-generations @confusionMove = @getMove('Confusion Recoil') # Stores the Struggle move as it is different cross-generation @struggleMove = @getMove('Struggle') # Stores attachments on the battle itself. @attachments = new Attachments() # Stores an ongoing log of the battle @log = [] # Teams for each player, keyed by player id. @teams = {} # Battle update information for each player, keyed by player id. @queues = {} # Holds all playerIds. The location in this array is the player's index. @playerIds = [] # Holds all player names. @playerNames = [] # Populates @playerIds and creates the teams for each player for player in @players @playerIds.push(player.id) @playerNames.push(player.name) # TODO: Get the actual player object and use player.name @teams[player.id] = new Team(this, player.id, player.name, player.team, @numActive) # Holds battle state information @replacing = false @finished = false @once 'end', (winnerId) -> @finished = true @resetExpiration() # Store when the battle was created @createdAt = Date.now() @resetExpiration() @logger = logger.withContext(battleId: @id) @weakWeather = [Weather.SUN, Weather.RAIN, Weather.SAND, Weather.HAIL, Weather.MOON] @strongWeather = [Weather.DELTASTREAM, Weather.HARSHSUN, Weather.HEAVYRAIN] # Creates a new log messages with context debug: (message, context) -> # TODO: Add more context. Elements such as the turn. @logger.log(message, context) begin: -> @tell(Protocol.INITIALIZE, @getTeams().map((t) -> t.toJSON(hidden: true))) if @listeners('beforeStart').length > 0 @emit('beforeStart') else for id in @playerIds console.log(id) team = @teams[id] allpokemon = team.all() for pokemon in allpokemon if pokemon.hasAbility('Illusion') alivemons = team.getAlivePokemon() lastalivemon = alivemons[alivemons.length-1] pokemon.attach(Attachment.Illusion, lastalivemon) @startBattle() startBattle: -> @emit('start') for playerId in @playerIds team = @teams[playerId] allpokemon = team.all() for pokemon in allpokemon if pokemon.getForme().isItemBased [ species, forme ] = pokemon.item.itemForme if pokemon.species != species pokemon.changeForme("default") if typeof pokemon.item != 'undefined' and typeof pokemon.item.itemForme != 'undefined' [ species, forme ] = pokemon.item.itemForme if species is pokemon.species pokemon.changeForme(forme) @tell(Protocol.START_BATTLE) # TODO: Merge this with performReplacements? for playerId in @playerIds for slot in [0...@numActive] pokemon = @getTeam(playerId).at(slot) continue if !pokemon @performReplacement(pokemon, slot) # TODO: Switch-in events are ordered by speed for pokemon in @getActivePokemon() pokemon.team.switchIn(pokemon) pokemon.turnsActive = 1 @beginTurn() getPlayerIndex: (playerId) -> index = @playerIds.indexOf(playerId) return (if index == -1 then null else index) getPlayerName: (playerId) -> index = @getPlayerIndex(playerId) return (if index? then @playerNames[index] else playerId) getPlayer: (playerId) -> _(@players).find((p) -> p.id == playerId) getTeam: (playerId) -> @teams[playerId] # Returns teams in order of index. getTeams: -> (@getTeam(playerId) for playerId in @playerIds) # Returns non-fainted opposing pokemon of a given pokemon. getOpponents: (pokemon) -> opponents = @getAllOpponents(pokemon) opponents = opponents.filter((p) -> p.isAlive()) opponents # Returns all opposing pokemon of a given pokemon. getAllOpponents: (pokemon) -> opponents = @getOpponentOwners(pokemon) teams = (@getTeam(playerId).slice(0, @numActive) for playerId in opponents) opponents = _.flatten(teams) opponents # Returns all opponent players of a given pokemon. In a 1v1 it returns # an array with only one opponent. getOpponentOwners: (pokemon) -> id = @getOwner(pokemon) (playerId for playerId in @playerIds when id != playerId) # Returns all active pokemon on the field belonging to both players. # Active pokemon include fainted pokemon that have not been switched out. getActivePokemon: -> pokemon = [] for team in @getTeams() pokemon.push(team.getActivePokemon()...) pokemon getActiveAlivePokemon: -> pokemon = @getActivePokemon() pokemon.filter((p) -> p.isAlive()) getActiveFaintedPokemon: -> pokemon = @getActivePokemon() pokemon.filter((p) -> p.isFainted()) # Finds the Player attached to a certain Pokemon. getOwner: (pokemon) -> for playerId in @playerIds return playerId if @getTeam(playerId).contains(pokemon) getSlotNumber: (pokemon) -> pokemon.team.indexOf(pokemon) # Forces the owner of a Pokemon to switch. forceSwitch: (pokemon) -> return false if @isOver() playerId = @getOwner(pokemon) switches = pokemon.team.getAliveBenchedPokemon() slot = @getSlotNumber(pokemon) @cancelAction(pokemon) @requestActions(playerId, [ {switches, slot} ]) # Returns true if the Pokemon has yet to move. willMove: (pokemon) -> action = @getAction(pokemon) action?.type == 'move' # Returns the move associated with a Pokemon. peekMove: (pokemon) -> @getAction(pokemon)?.move changeMove: (pokemon, move) -> action = @getAction(pokemon) if action?.type == 'move' action.move = move # Bumps a Pokemon to the front of a priority bracket. # If no bracket is provided, the Pokemon's current priority bracket is used. bump: (pokemon, bracket) -> if !bracket? action = @getAction(pokemon) return if !action bracket = @actionPriority(action) # Find the priority segment associated with this pokemon index = @pokemonActions.map((o) -> o.pokemon).indexOf(pokemon) segment = @pokemonActions.splice(index, 1)[0] # Put segment in proper place in the queue for action, i in @pokemonActions break if @actionPriority(action) <= bracket @pokemonActions.splice(i, 0, segment) # Delays a Pokemon to the end of a priority bracket. # If no bracket is provided, the Pokemon's current priority bracket is used. delay: (pokemon, bracket) -> if !bracket? action = @getAction(pokemon) return if !action bracket = @actionPriority(action) # Find the priority segment associated with this pokemon index = @pokemonActions.map((o) -> o.pokemon).indexOf(pokemon) segment = @pokemonActions.splice(index, 1)[0] # Put segment in proper place in the queue for i in [(@pokemonActions.length - 1)..0] by -1 action = @pokemonActions[i] break if @actionPriority(action) >= bracket @pokemonActions.splice(i + 1, 0, segment) # Add `string` to a buffer that will be sent to each client. message: (string) -> @tell(Protocol.RAW_MESSAGE, string) # Tells every spectator something. tell: (args...) -> for id, user of @users @tellPlayer(user.name, args...) @log.push(args) true tellPlayer: (id, args...) -> @queues[id] ?= [] @queues[id].push(args) # Sends a message to every spectator. send: -> for id, user of @users user.send.apply(user, arguments) # Passing -1 to turns makes the weather last forever. setWeather: (weatherName, turns=-1) -> console.log(weatherName) console.log(@weakWeather) if weatherName in @weakWeather and @weather in @strongWeather @cannedText("WEATHER_FAIL") return cannedText = switch weatherName when Weather.SUN then "SUN_START" when Weather.RAIN then "RAIN_START" when Weather.SAND then "SAND_START" when Weather.HAIL then "HAIL_START" when Weather.MOON then "MOON_START" when Weather.DELTASTREAM then "DELTASTREAM_START" when Weather.HARSHSUN then "HARSHSUN_START" when Weather.HEAVYRAIN then "HEAVYRAIN_START" else switch @weather when Weather.SUN then "SUN_END" when Weather.RAIN then "RAIN_END" when Weather.SAND then "SAND_END" when Weather.HAIL then "HAIL_END" when Weather.MOON then "MOON_END" when Weather.DELTASTREAM then "DELTASTREAM_END" when Weather.HARSHSUN then "HARSHSUN_END" when Weather.HEAVYRAIN then "HEAVYRAIN_END" @cannedText(cannedText) if cannedText @weather = weatherName @weatherDuration = turns pokemon.informWeather(@weather) for pokemon in @getActiveAlivePokemon() @tell(Protocol.WEATHER_CHANGE, @weather) hasWeather: (weatherName) -> return @weather != Weather.NONE if !weatherName weather = (if @hasWeatherCancelAbilityOnField() then Weather.NONE else @weather) weatherName == weather weatherCannedText: -> switch @weather when Weather.SAND then "SAND_CONTINUE" when Weather.HAIL then "HAIL_CONTINUE" when Weather.MOON then "MOON_CONTINUE" weatherUpkeep: -> if @weatherDuration == 1 @setWeather(Weather.NONE) else if @weatherDuration > 1 @weatherDuration-- activePokemon = @getActivePokemon().filter((p) -> !p.isFainted()) #Handles removing strong weather if there is no pokemon to keep it up anymore if @weather in @strongWeather weatherability = switch @weather when Weather.DELTASTREAM then "Delta Stream" when Weather.HARSHSUN then "Desolate Land" when Weather.HEAVYRAIN then "Primordial Sea" abilityarr = [] for pokemon in activePokemon abilityarr.push(pokemon.ability) console.log(abilityarr) if !weatherability in abilityarr @setWeather(Weather.NONE) cannedText = @weatherCannedText() @cannedText(cannedText) if cannedText? for pokemon in activePokemon continue if pokemon.isWeatherDamageImmune(@weather) damage = pokemon.stat('hp') >> 4 if @hasWeather(Weather.HAIL) if pokemon.damage(damage) @cannedText('HAIL_HURT', pokemon) else if @hasWeather(Weather.SAND) if pokemon.damage(damage) @cannedText('SAND_HURT', pokemon) hasWeatherCancelAbilityOnField: -> _.any @getActivePokemon(), (pokemon) -> pokemon.ability?.preventsWeather # Begins the turn. Actions are requested from each player. If no pokemon can # move, then the battle engine progresses to continueTurn. Otherwise, the # battle waits for user responses. beginTurn: -> @turn++ @tell(Protocol.START_TURN, @turn) pokemon.resetBlocks() for pokemon in @getActivePokemon() @query('beginTurn') @emit('beginTurn') # Send appropriate requests to players for playerId in @playerIds actions = [] for slot in [0...@numActive] team = @getTeam(playerId) pokemon = team.at(slot) continue if !pokemon || @getAction(pokemon) moves = pokemon.validMoves() switches = team.getAliveBenchedPokemon() switches = [] if pokemon.isSwitchBlocked() # This guarantees the user always has a move to pick. moves.push(@struggleMove) if moves.length == 0 actions.push({moves, switches, slot}) team.faintedLastTurn = team.faintedThisTurn team.faintedThisTurn = false @requestActions(playerId, actions) # A callback done after turn order is calculated for the first time. # Use this callback to edit the turn order after players have selected # their orders, but before the turn continues. afterTurnOrder: -> pokemon = @getActiveAlivePokemon() p.afterTurnOrder() for p in pokemon # Continues the turn. This is called once all requests # have been submitted and the battle is ready to continue. continueTurn: -> # We're done processing requests, so cancelling shouldn't be possible anymore. # Clean the completed requests @completedRequests = {} @tell(Protocol.CONTINUE_TURN) @emit('continueTurn') @determineTurnOrder() for action in @pokemonActions action.move?.beforeTurn?(this, action.pokemon) while @hasActionsLeft() action = @pokemonActions.shift() {pokemon} = action continue if pokemon.isFainted() @actionMap[action.type]["action"].call(this, action) @performFaints() # Update Pokemon itself. # TODO: Is this the right place? for active in @getActiveAlivePokemon() active.update() # If a move adds a request to the queue, the request must be resolved # before the battle can continue. break unless @areAllRequestsCompleted() # Performs end turn effects. endTurn: -> @weatherUpkeep() @query("endTurn") for pokemon in @getActivePokemon() pokemon.turnsActive += 1 pokemon.update() @checkForReplacements() attach: (klass, options = {}) -> options = _.clone(options) attachment = @attachments.push(klass, options, battle: this) if attachment then @tell(Protocol.BATTLE_ATTACH, attachment.name) attachment unattach: (klass) -> attachment = @attachments.unattach(klass) if attachment then @tell(Protocol.BATTLE_UNATTACH, attachment.name) attachment get: (attachment) -> @attachments.get(attachment) has: (attachment) -> @attachments.contains(attachment) endBattle: -> return if @finished winnerId = @getWinner() winnerIndex = @getPlayerIndex(winnerId) @tell(Protocol.END_BATTLE, winnerIndex) @emit('end', winnerId) incrementFaintedCounter: -> @faintedCounter = 0 unless @faintedCounter @faintedCounter += 1 @faintedCounter getWinner: -> winner = null # If each player has the same number of pokemon alive, return the owner of # the last pokemon that fainted earliest. teamLength = @getTeam(@playerIds[0]).getAlivePokemon().length playerSize = @playerIds.length count = 1 for i in [1...playerSize] by 1 count++ if teamLength == @getTeam(@playerIds[i]).getAlivePokemon().length if count == playerSize pokemon = _.flatten(@getTeams().map((p) -> p.pokemon)) # Get the owner of the pokemon that fainted last pokemon = pokemon.sort((a, b) -> b.fainted - a.fainted)[0] return pokemon.playerId # Otherwise, return the player with the most pokemon alive. length = 0 for playerId in @playerIds newLength = @getTeam(playerId).getAlivePokemon().length if newLength > length length = newLength winner = playerId return winner isOver: -> @finished || _(@playerIds).any((id) => @getTeam(id).getAlivePokemon().length == 0) hasStarted: -> @turn >= 1 getAllAttachments: -> array = @attachments.all() array.push(@getTeams().map((t) -> t.attachments.all())) array.push(@getActivePokemon().map((p) -> p.attachments.all())) _.flatten(array) isPokemon: (maybePokemon) -> maybePokemon instanceof Pokemon query: (eventName) -> Query(eventName, @getAllAttachments()) # Tells the player to execute a certain move by name. The move is added # to the list of player actions, which are executed once the turn continues. # # player - the player object that will execute the move # moveName - the name of the move to execute # recordMove: (playerId, move, forSlot = 0) -> pokemon = @getTeam(playerId).at(forSlot) action = @addAction(type: 'move', move: move, pokemon: pokemon) @removeRequest(playerId, action, forSlot) # Tells the player to switch with a certain pokemon specified by position. # The switch is added to the list of player actions, which are executed # once the turn continues. # # player - the player object that will execute the move # toPosition - the index of the pokemon to switch to # recordSwitch: (playerId, toPosition, forSlot = 0) -> pokemon = @getTeam(playerId).at(forSlot) action = @addAction(type: 'switch', to: toPosition, pokemon: pokemon) @removeRequest(playerId, action, forSlot) addAction: (action) -> unless @getAction(action.pokemon) @pokemonActions.push(action) @emit('addAction', action.pokemon.playerId, action) return action removeRequest: (playerId, action, forSlot) -> if arguments.length == 2 [action, forSlot] = [null, forSlot] forSlot ?= 0 playerRequests = @requests[playerId] || [] for request, i in playerRequests if request.slot == forSlot if action completed = { request, action } @completedRequests[playerId] ?= [] @completedRequests[playerId].push(completed) playerRequests.splice(i, 1) delete @requests[playerId] if playerRequests.length == 0 break # Cancels the most recent completed request made by a certain player # Returns true if the cancel succeeded, and false if it didn't. undoCompletedRequest: (playerId) -> return false if @isOver() return false if @areAllRequestsCompleted() return false if not @completedRequests[playerId] return false if @completedRequests[playerId].length == 0 return false if playerId not in @playerIds {request, action} = @completedRequests[playerId].pop() # Add the cancelled request to the beginning of @requests @requests[playerId] ?= [] @requests[playerId].unshift(request) # Remove the related pokemon actions. There may be more than one. index = 0 while index < @pokemonActions.length if @pokemonActions[index].pokemon.playerId == playerId @pokemonActions.splice(index, 1) else index += 1 @sendRequestTo(playerId) @emit('undoCompletedRequest', playerId) return true requestFor: (pokemon) -> playerId = @getOwner(pokemon) forSlot = @getSlotNumber(pokemon) actions = @requests[playerId] || [] for action in actions if action.slot == forSlot return action return null hasCompletedRequests: (playerId) -> @completedRequests[playerId]?.length > 0 getAction: (pokemon) -> for action in @pokemonActions if action.pokemon == pokemon && action.type in [ 'move', 'switch' ] return action return null popAction: (pokemon) -> action = @getAction(pokemon) if action index = @pokemonActions.indexOf(action) @pokemonActions.splice(index, 1) action cancelAction: (pokemon) -> action = @popAction(pokemon) action = @popAction(pokemon) while action? requestActions: (playerId, validActions) -> # Normalize actions for the client # TODO: Should not need to do this here. total = 0 for action in validActions {switches, moves} = action if switches? action.switches = switches.map((p) => @getSlotNumber(p)) total += action.switches.length if moves? action.moves = moves.map((m) -> m.name) total += action.moves.length return false if total == 0 @requests[playerId] = validActions @sendRequestTo(playerId) @emit('requestActions', playerId) return true sendRequestTo: (playerId) -> @tellPlayer(playerId, Protocol.REQUEST_ACTIONS, @requests[playerId]) # Returns true if all requests have been completed. False otherwise. areAllRequestsCompleted: -> total = 0 total += (request for request of @requests).length total == 0 checkForReplacements: -> @performFaints() if @isOver() @endBattle() else if @areReplacementsNeeded() @requestFaintedReplacements() else @beginTurn() # Returns true if any player's active Pokemon are fainted. areReplacementsNeeded: -> @getActiveFaintedPokemon().length > 0 # Force people to replace fainted Pokemon. requestFaintedReplacements: -> @replacing = true for playerId in @playerIds team = @getTeam(playerId) fainted = team.getActiveFaintedPokemon() size = fainted.length if size > 0 benched = team.getAliveBenchedPokemon() validActions = ({switches: benched, slot: x} for x in [0...size]) @requestActions(playerId, validActions) determineTurnOrder: -> @sortActions() @afterTurnOrder() @pokemonActions # Uses a Schwartzian transform to cut down on unnecessary calculations. # The game bitshifts priority and subpriority to the end and tacks on speed. # As a result, speed precision is 13 bits long; an overflow happens at 8191. # Trick Room replaces the Pokemon's speed with 0x2710 - speed. sortActions: -> trickRoomed = @has(Attachment.TrickRoom) array = @pokemonActions.map (action) => {pokemon} = action priority = @actionPriority(action) speed = pokemon.stat('speed') speed = 0x2710 - speed if trickRoomed speed &= 8191 integer = (priority << 13) | speed [ action, integer ] array.sort (a, b) => diff = b[1] - a[1] diff = (if @rng.next("turn order") < .5 then -1 else 1) if diff == 0 diff @pokemonActions = array.map (elem) => elem[0] actionPriority: (action) -> throw new Error("Could not find action!") if !action @actionMap[action.type]["priority"].call(this, action) hasActionsLeft: -> @pokemonActions.length > 0 # Executed by @continueTurn performSwitch: (pokemon, toPosition) -> pokemon.team.switch(pokemon, toPosition) performReplacement: (pokemon, toPosition) -> pokemon.team.replace(pokemon, toPosition) # Executed by @beginTurn performReplacements: -> @replacing = false switched = [] while @hasActionsLeft() {pokemon, to} = @pokemonActions.shift() switched.push @performReplacement(pokemon, to) # TODO: Switch-in events are ordered by speed for pokemon in switched pokemon.team.switchIn(pokemon) pokemon.turnsActive = 1 # Pokemon may have fainted upon switch-in; we need to check. @checkForReplacements() # Executed by @continueTurn performMove: (pokemon, move) -> targets = @getTargets(move, pokemon) @cannedText('NO_MOVES_LEFT', pokemon) if move == @struggleMove if pokemon.pp(move) <= 0 # TODO: Send move id instead pokemon.tell(Protocol.MAKE_MOVE, move.name) @cannedText('NO_PP_LEFT') # TODO: Is this the right place...? pokemon.resetRecords() else if pokemon.beforeMove(move, pokemon, targets) != false pokemon.reducePP(move) pressureTargets = targets.filter (t) -> t instanceof Pokemon && t.hasAbility("Pressure") && !t.team.contains(pokemon) for target in pressureTargets pokemon.reducePP(move) @executeMove(move, pokemon, targets) # After the move finishes (whether it executed properly or not, e.g. par) pokemon.afterMove(move, pokemon, targets) pokemon.tell(Protocol.END_MOVE) # TODO: Put in priority queue performFaints: -> # Execute afterFaint events # TODO: If a Pokemon faints in an afterFaint, should it be added to this? pokemon.faint() for pokemon in @getActiveFaintedPokemon() executeMove: (move, pokemon, targets) -> @debug('Battle#executeMove', move: move.name, attacker: pokemon.species) @currentPokemon = pokemon # TODO: Send move id instead pokemon.tell(Protocol.MAKE_MOVE, move.name) move.execute(this, pokemon, targets) # Record last move. @lastMove = move # TODO: Only record if none exists yet for this turn. pokemon.recordMove(move) # TODO: Is this the right place...? pokemon.resetRecords() @currentPokemon = null getTargets: (move, user) -> {team} = user targets = switch move.target when 'user' [ user ] when 'user-or-ally' [ @rng.choice(team.getActivePokemon()) ] when 'ally' # TODO: Actually get selected Pokemon from client team.getAdjacent(user) when 'all-opponents' @getOpponents(user) when 'selected-pokemon' # TODO: Actually get selected Pokemon from client. pokemon = @getOpponents(user) [ @rng.choice(pokemon, "selected pokemon target") ] when 'all-other-pokemon' @getActivePokemon().filter((p) -> p != user) when 'entire-field' @getActivePokemon() when 'random-opponent' pokemon = @getOpponents(user) [ @rng.choice(pokemon) ] when 'users-field' team.pokemon when 'specific-move' move.getTargets(this, user) when 'opponents-field' @getOpponentOwners(user) else throw new Error("Unimplemented target: #{move.target}.") if move.target != 'opponents-field' targets = targets.filter((p) -> p) return targets getMove: (moveName) -> throw new Error("#{moveName} does not exist.") if moveName not of @Moves @Moves[moveName] findMove: (condition) -> for move in @MoveList if condition(move) then return move return null getAttachment: (attachmentName) -> Attachment[attachmentName] getAilmentEffect: (move) -> switch move.ailmentId when "confusion" then Attachment.Confusion when "paralysis" then Status.Paralyze when "freeze" then Status.Freeze when "burn" then Status.Burn when "sleep" then Status.Sleep when "poison" then Status.Poison when "toxic" then Status.Toxic when "yawn" then Attachment.Yawn when "infatuation" then Attachment.Attract when "disable" then Attachment.Disable when "ingrain" then Attachment.Ingrain when "leech-seed" then Attachment.LeechSeed when "torment" then Attachment.Torment when "perish-song" then Attachment.PerishSong when "embargo" then Attachment.Embargo when "telekinesis" then Attachment.Telekinesis when "nightmare" then Attachment.Nightmare when "unknown" switch move.name when "Tri Attack" triAttackEffects = [ Status.Paralyze, Status.Burn, Status.Freeze ] @rng.choice(triAttackEffects, "tri attack effect") else throw new Error("Unrecognized unknown ailment for #{move.name}") else throw new Error("Unrecognized ailment: #{move.ailmentId} for #{move.name}") add: (spark) -> user = spark.user # If this is a player, mask the spectator in case this is an alt player = _(@players).find((p) -> p.id == user.name) # Find the user's index (if any) index = (if player then @getPlayerIndex(player.id) else null) # Start spectating battle spark.send('spectateBattle', @id, @format, @numActive, index, @playerNames, @log) # Add to internal user store super(spark) # If this is a player, send them their own team if player teamJSON = @getTeam(player.id).toJSON() @tellPlayer(player.id, Protocol.RECEIVE_TEAM, teamJSON) @emit('spectateBattle', user) transformName: (name) -> @getPlayerName(name) forfeit: (id) -> return if @isOver() index = @getPlayerIndex(id) return unless index? @tell(Protocol.FORFEIT_BATTLE, index) winnerId = @playerIds[1 - index] @emit('end', winnerId) expire: -> return if @expired @expired = true @tell(Protocol.BATTLE_EXPIRED) @emit('end') if !@finished @emit('expire') resetExpiration: -> clearTimeout(@expirationId) if @expirationId @expirationId = setTimeout((=> @expire() unless @expired), @makeTTL()) makeTTL: -> if @isOver() @ENDED_BATTLE_TTL else @ONGOING_BATTLE_TTL # Sends battle updates to each spectator. sendUpdates: -> for id, user of @users userName = user.name queue = @queues[userName] continue if !queue || queue.length == 0 user.send('updateBattle', @id, queue) delete @queues[userName] cannedText: (type, args...) -> newArgs = [] # Convert any Pokemon in the arguments to its respective player/slot. for arg in args if arg instanceof Pokemon if arg.ability == 'Illusion' newArgs.push(@getPlayerIndex(arg.playerId), arg.team.indexOf(arg.team.size - 1)) else newArgs.push(@getPlayerIndex(arg.playerId), arg.team.indexOf(arg)) else if arg instanceof Move newArgs.push(arg.name) else if _.isObject(arg) && arg.prototype instanceof BaseAttachment newArgs.push(arg.displayName) else newArgs.push(arg) @tell(Protocol.CANNED_TEXT, CannedText[type], newArgs...) toString: -> "[Battle id:#{@id} turn:#{@turn} weather:#{@weather}]"