BattleSim/server/bw/battle.coffee

945 lines
30 KiB
CoffeeScript

{_} = 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
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')
@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"
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"
@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}]"