1
0
mirror of https://gitlab.com/Deukhoofd/BattleSim.git synced 2025-10-28 02:00:04 +00:00

Lots of stuff

This commit is contained in:
Deukhoofd
2016-02-01 23:19:30 +01:00
commit d7316d5799
6681 changed files with 527969 additions and 0 deletions

1580
server/bw/attachment.coffee Normal file

File diff suppressed because it is too large Load Diff

918
server/bw/battle.coffee Normal file
View File

@@ -0,0 +1,918 @@
{_} = 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)
# 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) ->
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"
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"
@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--
cannedText = @weatherCannedText()
@cannedText(cannedText) if cannedText?
activePokemon = @getActivePokemon().filter((p) -> !p.isFainted())
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}]"

View File

@@ -0,0 +1,128 @@
conditions = require '../conditions'
{_} = require 'underscore'
# Abstracts out sending messages from player to battle.
# Makes the Battle smoothly go into the next turn
# Necessary to separate out making commands and executing commands.
class @BattleController
constructor: (@battle) ->
conditions.attach(this)
@battle.emit('initialize')
BATTLE_DELEGATES = 'getPlayer isOver sendRequestTo
add remove'.trim().split(/\s+/)
for method in BATTLE_DELEGATES
do (method) =>
this::[method] = ->
@battle[method].apply(@battle, arguments)
# Returns all the player ids participating in this battle.
getPlayerIds: ->
@battle.playerIds
# Returns all the names of players participating in this battle.
# These names may be masked by alts
getPlayerNames: ->
@battle.playerNames
# 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.
makeMove: (playerId, moveName, forSlot = 0, forTurn = @battle.turn, options = {}) ->
return false if @battle.isOver()
return false if forTurn != @battle.turn
return false if playerId not in @battle.playerIds
pokemon = @battle.getTeam(playerId).at(forSlot)
return false if !pokemon
request = @battle.requestFor(pokemon)
return false if !request
return false if moveName not in (request.moves || [])
move = @battle.getMove(moveName)
@battle.recordMove(playerId, move, forSlot, options)
@transitionToNextState()
return true
# 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.
makeSwitch: (playerId, toPosition, forSlot = 0, forTurn = @battle.turn) ->
return false if @battle.isOver()
return false if forTurn != @battle.turn
return false if playerId not in @battle.playerIds
pokemon = @battle.getTeam(playerId).at(forSlot)
return false if !pokemon
request = @battle.requestFor(pokemon)
return false if !request
return false if toPosition not in (request.switches || [])
@battle.recordSwitch(playerId, toPosition, forSlot)
@transitionToNextState()
return true
# Tells the player to cancel their latest completed request.
undoCompletedRequest: (playerId, forTurn = @battle.turn) ->
return false if forTurn != @battle.turn
@battle.undoCompletedRequest(playerId)
@sendUpdates()
return true
# Makes a player forfeit.
forfeit: (playerId) ->
return if @battle.isOver()
@battle.forfeit(playerId)
@sendUpdates()
messageSpectators: (user, message) ->
# In case the user is an alt.
userName = @battle.getPlayerName(user.name)
for spectator in @battle.spectators
spectator.send('updateBattleChat', @battle.id, userName, message)
rawMessage: (message) ->
for spectator in @battle.spectators
spectator.send('rawBattleMessage', @battle.id, message)
# Continue or begin a new turn if each player has made an action.
transitionToNextState: ->
return if not @battle.areAllRequestsCompleted()
if @battle.replacing
@battle.performReplacements()
@sendUpdates()
else
@continueTurn()
# Officially starts the battle.
beginBattle: ->
@battle.begin()
@sendUpdates()
beginTurn: ->
@battle.beginTurn()
@sendUpdates()
# Continues the turn. This is called once all requests
# have been submitted and the battle is ready to continue.
#
# If there are no more requests, the engine progresses to endTurn. Otherwise,
# it waits for continueTurn to be called again.
continueTurn: ->
@battle.continueTurn()
# If all requests have been completed, then end the turn.
# Otherwise, wait for further requests to be completed before ending.
if @battle.areAllRequestsCompleted() then @endTurn()
@sendUpdates()
# Calls Battle#endTurn. If all pokemon are fainted, then it
# ends the battle. Otherwise, it will request for new pokemon and wait if
# any replacements are needed, or begins the next turn.
endTurn: ->
@battle.endTurn()
@sendUpdates()
endBattle: ->
@battle.endBattle()
@sendUpdates()
# Sends battle updates to spectators.
sendUpdates: ->
@battle.sendUpdates()

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11270
server/bw/data/data_moves.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
{@Moves, @MoveData, @MoveList} = require './moves'
{@Ability} = require './abilities'
{@Item, @ItemData} = require './items'
{@SpeciesData, @FormeData} = require './pokemon'

633
server/bw/data/items.coffee Normal file
View File

@@ -0,0 +1,633 @@
@ItemData = ItemData = require './data_items.json'
{Attachment, Status, VolatileAttachment} = require('../attachment')
{Weather} = require '../../../shared/weather'
{Protocol} = require '../../../shared/protocol'
util = require '../util'
@Item = Item = {}
makeItem = (name, func) ->
if name not of ItemData
throw new Error("Cannot extend Item '#{name}' because it does not exist.")
condensed = name.replace(/\s+/g, '')
class Item[condensed] extends VolatileAttachment
@displayName: name
displayName: name
item: true
(this[property] = value for property, value of ItemData[name])
func?.call(this)
makePinchBerry = (name, hookName, func) ->
if !func?
func = hookName
hookName = "update"
makeItem name, ->
this.eat = (battle, eater) ->
func.call(this, battle, eater)
this::[hookName] = ->
fraction = (if @pokemon.hasAbility("Gluttony") then 1 else 2)
activationHP = @pokemon.stat('hp') >> fraction
if @pokemon.currentHP <= activationHP
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
# TODO: If the stat is maxed, does anything special happen?
# Is the berry still consumed?
makeStatBoostBerry = (name, boosts) ->
makePinchBerry name, (battle, eater) ->
boostedStats = eater.boost(boosts)
makeFlavorHealingBerry = (name, stat) ->
makeItem name, ->
this.eat = (battle, owner) ->
if owner.heal(Math.floor(owner.stat('hp') / 8))
battle.cannedText('BERRY_RESTORE', owner, this)
if owner.natureBoost(stat) < 1.0
owner.attach(Attachment.Confusion)
this::update = ->
if @pokemon.currentHP <= Math.floor(@pokemon.stat('hp') / 2) && @pokemon.canHeal()
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeHealingBerry = (name, func) ->
makeItem name, ->
this.eat = (battle, owner) ->
if owner.heal(func(owner))
battle.cannedText('BERRY_RESTORE', owner, this)
this::update = ->
if @pokemon.currentHP <= Math.floor(@pokemon.stat('hp') / 2) && @pokemon.canHeal()
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeTypeResistBerry = (name, type) ->
makeItem name, ->
this.eat = ->
this::modifyBasePowerTarget = (move, user) ->
return 0x1000 if move.getType(@battle, user, @pokemon) != type
return 0x1000 if util.typeEffectiveness(type, @pokemon.types) <= 1 && type != 'Normal'
@battle.cannedText('ITEM_WEAKEN', @constructor, @pokemon)
@pokemon.useItem()
return 0x800
makeFeedbackDamageBerry = (name, klass) ->
makeItem name, ->
this.eat = ->
this::afterBeingHit = (move, user, target) ->
return if !move[klass]()
return if target.isFainted()
if user.damage(Math.floor(user.stat('hp') / 8))
@battle.cannedText('POKEMON_HURT_BY_ITEM', user, target, @constructor)
target.useItem()
makeStatusCureBerry = (name, statuses...) ->
makeItem name, ->
this.eat = (battle, owner) ->
for attachment in statuses
if owner.cureAttachment(attachment, message: name)
return true
return false
this::update = ->
if @constructor.eat(@battle, @pokemon) then @pokemon.useItem()
makeOrbItem = (name, species) ->
makeItem name, ->
this::modifyBasePower = (move, target) ->
if @pokemon.species == species && move.type in @pokemon.types
0x1333
else
0x1000
makeStatusOrbItem = (name, status) ->
makeItem name, ->
this::endTurn = ->
@pokemon.attach(status)
makeTypeBoostItem = (name, type) ->
makeItem name, ->
this::modifyBasePower = (move, target) ->
if move.type == type
0x1333
else
0x1000
# Same as makeTypeBoostItem, but sets item.plate = type.
makePlateItem = (name, type) ->
makeTypeBoostItem(name, type)
makeItem(name, -> @plate = type)
# Gem items are one-time use.
GEM_BOOST_AMOUNT = GEM_BOOST_AMOUNT ? 0x1800
makeGemItem = (name, type) ->
makeItem name, ->
this::modifyBasePower = (move, target) ->
if move.type == type
GEM_BOOST_AMOUNT
else
0x1000
this::afterSuccessfulHit = (move, user, target) ->
if move.type == type
@battle.cannedText('GEM_BOOST', @constructor, move)
user.useItem()
makeChoiceItem = (name, func) ->
makeItem name, ->
this::initialize = ->
@move = null
this::beforeMove = (move, user, targets) ->
@move = move
true
this::beginTurn = ->
@pokemon.lockMove(@move) if @move?
func.call(this)
makeWeatherItem = (name, weather) ->
makeItem name, ->
@lengthensWeather = weather
makeSpeciesBoostingItem = (name, speciesArray, statsHash) ->
makeItem name, ->
for stat, boost of statsHash
capitalizedStat = stat[0].toUpperCase() + stat.substr(1)
# TODO: Use modifiers
this::["edit#{capitalizedStat}"] = (stat) ->
isTransformed = @pokemon.has(Attachment.Transform)
if @pokemon.species in speciesArray && !isTransformed
Math.floor(stat * boost)
else
stat
makeSpeciesCriticalItem = (name, species) ->
makeItem name, ->
this::criticalModifier = (sum) ->
sum + (if @pokemon.species == species then 2 else 0)
makeDelayItem = (name) ->
makeItem name, ->
this::afterTurnOrder = ->
@battle.delay(@pokemon)
makeEvasionItem = (name, ratio=0.9) ->
makeItem name, ->
this::editEvasion = (accuracy) ->
Math.floor(accuracy * ratio)
makeFlinchItem = (name) ->
makeItem name, ->
this::afterSuccessfulHit = (move, user, target) ->
multiplier = (if user.hasAbility("Serene Grace") then 2 else 1)
if move.flinchChance == 0 && !move.isNonDamaging() &&
@battle.rng.next("flinch item chance") < .1 * multiplier
target.attach(Attachment.Flinch)
makeCriticalBoostItem = (name) ->
makeItem name, ->
this::criticalModifier = (sum) -> sum + 1
makeBoostOnTypeItem = (name, type, boosts) ->
stats = Object.keys(boosts)
length = stats.length
stats = stats.map (stat) ->
stat[0].toUpperCase() + stat[1...length].replace(/[A-Z]/g, " $1")
stats[length - 1] = "and #{stats[length - 1]}" if length >= 2
stats = stats.join(", ") if length >= 3
stats = stats.join(" ") if length == 2
makeItem name, ->
this::afterBeingHit = (move, user, target) ->
if move.type == type
@battle.cannedText('BERRY_RAISE_STAT', @constructor, user, stats)
target.boost(boosts)
target.useItem()
makeBoostOnTypeItem 'Absorb Bulb', 'Water', specialAttack: 1
makeOrbItem 'Adamant Orb', 'Dialga'
makeFlavorHealingBerry 'Aguav Berry', "specialDefense"
makeItem 'Air Balloon', ->
this::initialize = ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @displayName)
this::afterBeingHit = (move, user, target) ->
return if move.isNonDamaging()
@pokemon.tell(Protocol.POKEMON_UNATTACH, @displayName)
target.removeItem()
this::isImmune = (type) ->
return true if type == 'Ground'
makeStatBoostBerry 'Apicot Berry', specialDefense: 1
makeStatusCureBerry 'Aspear Berry', Status.Freeze
makeTypeResistBerry 'Babiri Berry', 'Steel'
makeHealingBerry 'Berry Juice', -> 20
makeTypeBoostItem 'Black Belt', 'Fighting'
makeTypeBoostItem 'BlackGlasses', 'Dark'
makeItem 'Black Sludge', ->
this::endTurn = ->
maxHP = @pokemon.stat('hp')
if @pokemon.hasType('Poison')
return if maxHP == @pokemon.currentHP
amount = Math.floor(maxHP / 16)
amount = 1 if amount == 0
if @pokemon.heal(amount)
@battle.cannedText('ITEM_RESTORE', @pokemon, @constructor)
else
amount = Math.floor(maxHP / 8)
amount = 1 if amount == 0
if @pokemon.damage(amount)
@battle.cannedText('ITEM_SELF_HURT', @pokemon, @constructor)
makeEvasionItem 'BrightPowder', 0.9
makeGemItem 'Bug Gem', 'Bug'
makeBoostOnTypeItem 'Cell Battery', 'Electric', attack: 1
makeTypeBoostItem 'Charcoal', 'Fire'
makeTypeResistBerry 'Charti Berry', 'Rock'
makeStatusCureBerry 'Cheri Berry', Status.Paralyze
makeStatusCureBerry 'Chesto Berry', Status.Sleep
makeTypeResistBerry 'Chilan Berry', 'Normal'
makeChoiceItem 'Choice Band', ->
this::modifyAttack = (move) ->
if move.isPhysical() then 0x1800 else 0x1000
makeChoiceItem 'Choice Specs', ->
this::modifyAttack = (move) ->
if move.isSpecial() then 0x1800 else 0x1000
makeChoiceItem 'Choice Scarf', ->
this::editSpeed = (stat) ->
Math.floor(stat * 1.5)
makeTypeResistBerry 'Chople Berry', 'Fighting'
makeTypeResistBerry 'Coba Berry', 'Flying'
makeTypeResistBerry 'Colbur Berry', 'Dark'
makePinchBerry 'Custap Berry', 'afterTurnOrder', (battle, eater) ->
battle.cannedText('MOVE_FIRST', eater, this)
battle.bump(eater)
makeWeatherItem 'Damp Rock', Weather.RAIN
makeWeatherItem 'Dark Rock', Weather.MOON
makeGemItem 'Dark Gem', 'Dark'
makeTypeBoostItem 'Dragon Fang', 'Dragon'
makeGemItem 'Dragon Gem', 'Dragon'
makePlateItem 'Draco Plate', 'Dragon'
makePlateItem 'Dread Plate', 'Dark'
makePlateItem 'Earth Plate', 'Ground'
makeItem 'Eject Button', ->
this::afterAllHitsTarget = (move, user) ->
return if move.isNonDamaging()
return if !@battle.forceSwitch(@pokemon)
@battle.cannedText('EJECT_BUTTON', @pokemon)
@pokemon.useItem()
makeGemItem 'Electric Gem', 'Electric'
makeItem 'Enigma Berry', ->
this.eat = ->
this::afterBeingHit = (move, user, target) ->
return if util.typeEffectiveness(move.type, target.types) <= 1
if target.heal(Math.floor(target.stat('hp') / 4))
@battle.cannedText('BERRY_RESTORE', target, @constructor)
target.useItem()
makeItem 'Eviolite', ->
this::editDefense = this::editSpecialDefense = (defense) ->
return Math.floor(1.5 * defense) if @pokemon.nfe
return defense
makeItem 'Expert Belt', ->
this::modifyAttack = (move, target) ->
effectiveness = move.typeEffectiveness(@battle, @pokemon, target)
return 0x1333 if effectiveness > 1
return 0x1000
makeGemItem 'Fighting Gem', 'Fighting'
makeFlavorHealingBerry 'Figy Berry', "attack"
makeGemItem 'Fire Gem', 'Fire'
makePlateItem 'Fist Plate', 'Fighting'
makeStatusOrbItem 'Flame Orb', Status.Burn
makePlateItem 'Flame Plate', 'Fire'
makeItem 'Float Stone', ->
this::calculateWeight = (weight) ->
Math.floor(weight / 2)
makeGemItem 'Flying Gem', 'Flying'
makeItem 'Focus Band', ->
this::transformHealthChange = (amount, options) ->
if amount >= @pokemon.currentHP && @battle.rng.randInt(0, 9, "focus band") == 0 &&
options.source == 'move'
@battle.cannedText('HANG_ON', @pokemon, @constructor)
@pokemon.useItem()
return @pokemon.currentHP - 1
return amount
makeItem 'Focus Sash', ->
this::transformHealthChange = (amount, options) ->
maxHP = @pokemon.stat('hp')
if @pokemon.currentHP == maxHP && amount >= maxHP && options.source == 'move'
@battle.cannedText('HANG_ON', @pokemon, @constructor)
@pokemon.useItem()
return maxHP - 1
return amount
makeDelayItem 'Full Incense'
makeStatBoostBerry 'Ganlon Berry', defense: 1
makeGemItem 'Ghost Gem', 'Ghost'
makeGemItem 'Grass Gem', 'Grass'
makeOrbItem 'Griseous Orb', 'Giratina'
makeGemItem 'Ground Gem', 'Ground'
makeTypeResistBerry 'Haban Berry', 'Dragon'
makeTypeBoostItem 'Hard Stone', 'Rock'
makeWeatherItem 'Heat Rock', Weather.SUN
makeFlavorHealingBerry 'Iapapa Berry', "defense"
makeGemItem 'Ice Gem', 'Ice'
makePlateItem 'Icicle Plate', 'Ice'
makeWeatherItem 'Icy Rock', Weather.HAIL
makePlateItem 'Insect Plate', 'Bug'
makePlateItem 'Iron Plate', 'Steel'
makeFeedbackDamageBerry 'Jaboca Berry', 'isPhysical'
makeTypeResistBerry 'Kasib Berry', 'Ghost'
makeTypeResistBerry 'Kebia Berry', 'Poison'
makeFlinchItem "King's Rock"
makeDelayItem 'Lagging Tail'
# TODO: What happens if the Pokemon already has Focus Energy?
# Does the berry still get eaten? Same goes for the other stat berries.
makePinchBerry 'Lansat Berry', (battle, eater) ->
eater.attach(Attachment.FocusEnergy)
makeEvasionItem 'Lax Incense', 0.9
makeItem 'Leftovers', ->
this::endTurn = ->
maxHP = @pokemon.stat('hp')
return if maxHP == @pokemon.currentHP
amount = Math.floor(maxHP / 16)
amount = 1 if amount == 0
if @pokemon.heal(amount)
@battle.cannedText('ITEM_RESTORE', @pokemon, @constructor)
makeStatBoostBerry 'Liechi Berry', attack: 1
makeItem 'Life Orb', ->
this::modifyAttack = ->
0x14CC
this::afterAllHits = (move) ->
return if move.isNonDamaging()
if @pokemon.damage(Math.floor(@pokemon.stat('hp') / 10))
@battle.cannedText('ITEM_SELF_HURT', @pokemon, @constructor)
makeItem 'Light Clay' # Hardcoded in Attachment.Screen
makeStatusCureBerry 'Lum Berry', Status.Paralyze, Status.Sleep, Status.Poison,
Status.Toxic, Status.Burn, Status.Freeze, Attachment.Confusion
makeOrbItem 'Lustrous Orb', 'Palkia'
makeItem 'Macho Brace', ->
this::editSpeed = (stat) ->
Math.floor(stat / 2)
makeTypeBoostItem 'Magnet', 'Electric'
makeFlavorHealingBerry 'Mago Berry', "speed"
makePlateItem 'Meadow Plate', 'Grass'
makeItem 'Mental Herb', ->
this.activate = (battle, pokemon) ->
for effectName in [ 'Attract', 'Taunt', 'Encore', 'Torment', 'Disable' ]
attachment = Attachment[effectName]
if pokemon.has(attachment)
battle.cannedText('MENTAL_HERB', pokemon)
pokemon.unattach(attachment)
return true
return false
this::update = ->
if @constructor.activate(@battle, @pokemon)
@pokemon.useItem()
makeTypeBoostItem 'Metal Coat', 'Steel'
makeItem 'Metronome', ->
this::modifyBasePower = (move, target) ->
attachment = @pokemon.get(Attachment.Metronome)
layers = attachment?.layers || 0
0x1000 + layers * 0x333
this::afterSuccessfulHit = (move, user, target) ->
user.attach(Attachment.Metronome, {move})
makePinchBerry 'Micle Berry', (battle, eater) ->
eater.attach(Attachment.MicleBerry)
makePlateItem 'Mind Plate', 'Psychic'
makeTypeBoostItem 'Miracle Seed', 'Grass'
makeItem 'Muscle Band', ->
this::modifyBasePower = (move, target) ->
if move.isPhysical()
0x1199
else
0x1000
makeTypeBoostItem 'Mystic Water', 'Water'
makeTypeBoostItem 'NeverMeltIce', 'Ice'
makeGemItem 'Normal Gem', 'Normal'
makeTypeResistBerry 'Occa Berry', 'Fire'
makeTypeBoostItem 'Odd Incense', 'Psychic'
makeHealingBerry 'Oran Berry', -> 10
makeTypeResistBerry 'Passho Berry', 'Water'
makeTypeResistBerry 'Payapa Berry', 'Psychic'
makeStatusCureBerry 'Pecha Berry', Status.Toxic, Status.Poison
makeStatusCureBerry 'Persim Berry', Attachment.Confusion
makeStatBoostBerry 'Petaya Berry', specialAttack: 1
makeTypeBoostItem 'Poison Barb', 'Poison'
makeGemItem 'Poison Gem', 'Poison'
makeGemItem 'Psychic Gem', 'Psychic'
makeItem 'Quick Claw', ->
this::afterTurnOrder = ->
if @battle.rng.next("quick claw") < .2
@battle.cannedText('MOVE_FIRST', @pokemon, @constructor)
@battle.bump(@pokemon)
makeStatusCureBerry 'Rawst Berry', Status.Burn
makeFlinchItem "Razor Fang"
makeItem 'Red Card', ->
this::afterAllHitsTarget = (move, user) ->
return if move.isNonDamaging()
benched = user.team.getAliveBenchedPokemon()
return if benched.length == 0
@battle.cannedText('RED_CARD', @pokemon, user)
@pokemon.useItem()
return if user.shouldPhase(@battle, @pokemon) == false
pokemon = @battle.rng.choice(benched)
index = user.team.indexOf(pokemon)
user.team.switch(user, index)
makeTypeResistBerry 'Rindo Berry', 'Grass'
makeGemItem 'Rock Gem', 'Rock'
makeItem 'Rocky Helmet', ->
this::isAliveCheck = -> true
this::afterBeingHit = (move, user, target) ->
if move.hasFlag("contact")
amount = Math.floor(user.stat('hp') / 6)
if user.damage(amount)
@battle.cannedText('POKEMON_HURT_BY_ITEM', user, target, @constructor)
makeTypeBoostItem 'Rock Incense', 'Rock'
makeTypeBoostItem 'Rose Incense', 'Grass'
makeFeedbackDamageBerry 'Rowap Berry', 'isSpecial'
makeStatBoostBerry 'Salac Berry', speed: 1
makeTypeBoostItem 'Sea Incense', 'Water'
makeTypeBoostItem 'Sharp Beak', 'Flying'
makeItem 'Shell Bell', ->
this::afterSuccessfulHit = (move, user, target, damage) ->
return if damage == 0
if user.heal(Math.floor(damage / 8))
@battle.cannedText('ITEM_RESTORE', user, @constructor)
makeTypeResistBerry 'Shuca Berry', 'Ground'
makeTypeBoostItem 'Silk Scarf', 'Normal'
makeTypeBoostItem 'SilverPowder', 'Bug'
makeHealingBerry 'Sitrus Berry', (owner) -> Math.floor(owner.stat('hp') / 4)
makePlateItem 'Sky Plate', 'Flying'
makeWeatherItem 'Smooth Rock', Weather.SAND
makeTypeBoostItem 'Soft Sand', 'Ground'
makeSpeciesBoostingItem 'Soul Dew', ["Latias", "Latios"],
specialAttack: 1.5, specialDefense: 1.5
makeTypeBoostItem 'Spell Tag', 'Ghost'
makePlateItem 'Splash Plate', 'Water'
makePlateItem 'Spooky Plate', 'Ghost'
# TODO: If there is no stat left to boost, is it still consumed?
makePinchBerry 'Starf Berry', (battle, eater) ->
stats = ["attack", "defense", "specialAttack", "specialDefense", "speed"]
stats = stats.filter((stat) -> eater.stages[stat] != 6)
return if stats.length == 0
index = battle.rng.randInt(0, stats.length - 1, "starf berry stat")
boosts = {}
boosts[stats[index]] = 2
boostedStats = eater.boost(boosts)
makeItem 'Sticky Barb', ->
this::afterBeingHit = (move, user, target) ->
return unless move.hasFlag("contact")
return if user.hasItem()
user.setItem(@constructor)
target.useItem()
this::endTurn = ->
@pokemon.damage(Math.floor(@pokemon.stat('hp') / 8))
makeGemItem 'Steel Gem', 'Steel'
makePlateItem 'Stone Plate', 'Rock'
makeTypeResistBerry 'Tanga Berry', 'Bug'
makeStatusOrbItem 'Toxic Orb', Status.Toxic
makePlateItem 'Toxic Plate', 'Poison'
makeTypeBoostItem 'TwistedSpoon', 'Psychic'
makeTypeResistBerry 'Wacan Berry', 'Electric'
makeGemItem 'Water Gem', 'Water'
makeTypeBoostItem 'Wave Incense', 'Water'
# TODO: What if White Herb is tricked onto a Pokemon? Are all boosts negated?
makeItem 'White Herb', ->
this.activate = (battle, pokemon) ->
triggered = false
boosts = {}
for stat, boost of pokemon.stages
if boost < 0
triggered = true
boosts[stat] = 0
if triggered
pokemon.setBoosts(boosts)
battle.cannedText('WHITE_HERB', pokemon)
return triggered
this::update = ->
if @constructor.activate(@battle, @pokemon)
@pokemon.useItem()
makeItem "Wide Lens", ->
this::editAccuracy = (accuracy) ->
Math.floor(accuracy * 1.1)
makeFlavorHealingBerry 'Wiki Berry', "specialAttack"
makeItem 'Wise Glasses', ->
this::modifyBasePower = (move, target) ->
if move.isSpecial()
0x1199
else
0x1000
makeTypeResistBerry 'Yache Berry', 'Ice'
makePlateItem 'Zap Plate', 'Electric'
makeItem 'Zoom Lens', ->
this::editAccuracy = (accuracy, move, target) ->
return Math.floor(accuracy * 1.2) if @battle.willMove(target)
return accuracy
makeSpeciesBoostingItem("DeepSeaTooth", ["Clamperl"], specialAttack: 2)
makeSpeciesBoostingItem("DeepSeaScale", ["Clamperl"], specialDefense: 2)
makeSpeciesBoostingItem("Light Ball", ["Pikachu"], attack: 2, specialAttack: 2)
makeSpeciesBoostingItem("Thick Club", ["Cubone", "Marowak"], attack: 2)
makeSpeciesBoostingItem("Metal Powder", ["Ditto"],
defense: 2, specialDefense: 2)
makeSpeciesBoostingItem("Quick Powder", ["Ditto"], speed: 2)
makeSpeciesCriticalItem "Lucky Punch", "Chansey"
makeSpeciesCriticalItem "Stick", "Farfetch'd"
makeCriticalBoostItem 'Razor Claw'
makeCriticalBoostItem 'Scope Lens'
makeItem 'Iron Ball', ->
this::editSpeed = (stat) ->
Math.floor(stat / 2)
this::isImmune = (type) ->
return false if type == 'Ground'
makeItem 'Leppa Berry', ->
this.eat = (battle, eater) ->
for move in eater.moves
if eater.pp(move) == 0
eater.setPP(move, 10)
break
this::update = ->
if @pokemon.lastMove? && @pokemon.pp(@pokemon.lastMove) == 0
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
# TODO: Implement Nature Power and implement eat there.
for berry in "Belue Berry, Bluk Berry, Cornn Berry, Durin Berry, Grepa Berry,
Hondew Berry, Kelpsy Berry, Magost Berry, Nanab Berry,
Nomel Berry, Pamtre Berry, Pinap Berry, Pomeg Berry, Qualot Berry,
Rabuta Berry, Razz Berry, Spelon Berry, Tamato Berry,
Watmel Berry, Wepear Berry".split(/,\s+/)
makeItem berry, ->
this.eat = ->
# Ensure we aren't purposefully missing berries that need an `eat` function.
for name, item of Item
if item.type == 'berries' && 'eat' not of item
console.warn "Note: Item '#{item.displayName}' does not have `eat` implemented."
# Make all leftover items
for itemName of ItemData
makeItem(itemName) if itemName.replace(/\s+/, '') not of Item

1956
server/bw/data/moves.coffee Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
@SpeciesData = require('./data_species.json')
@FormeData = require('./data_formes.json')

435
server/bw/move.coffee Normal file
View File

@@ -0,0 +1,435 @@
{Attachment, Status} = require './attachment'
{Weather} = require '../../shared/weather'
{Protocol} = require '../../shared/protocol'
Query = require('./queries')
util = require './util'
# A single Move in the Pokemon engine. Move objects are constructed in
# data/VERSION/moves.coffee, with only one instance per move (for example,
# there is only one Flamethrower). These instances are retrieved by the battle
# engine.
class @Move
criticalMultiplier: 2
constructor: (@name, attributes = {}) ->
@accuracy = attributes.accuracy || 0
@priority = attributes.priority || 0
@power = attributes.power
@target = attributes.target
@description = attributes.description || "No description set"
@type = attributes.type || '???'
@spectra = attributes.damage || '???'
@chLevel = attributes.criticalHitLevel || 1
@flags = attributes.flags || []
@flinchChance = (attributes.flinchChance || 0)
@ailmentChance = (attributes.ailmentChance || 0)
@ailmentId = attributes.ailmentId
@primaryBoostStats = attributes.primaryBoostStats
@primaryBoostTarget = attributes.primaryBoostTarget
@secondaryBoostChance = attributes.secondaryBoostChance || 0
@secondaryBoostStats = attributes.secondaryBoostStats
@secondaryBoostTarget = attributes.secondaryBoostTarget
@pp = attributes.pp
@recoil = attributes.recoil
{@minHits, @maxHits} = attributes
isPhysical: ->
@spectra == 'physical'
isSpecial: ->
@spectra == 'special'
isNonDamaging: ->
@spectra == 'non-damaging'
hasFlag: (flagName) ->
flagName in @flags
hasPrimaryEffect: ->
@primaryBoostStats? || (@ailmentId != "none" && @ailmentChance == 0)
# A secondary effect also includes flinching.
hasSecondaryEffect: ->
(@ailmentChance > 0 && @ailmentId != "none") ||
@flinchChance > 0 || @secondaryBoostChance > 0
# Executes this move on several targets.
# Only override this method if the move does not need to be
# recorded on the enemy pokemon.
execute: (battle, user, targets) ->
# If there are no targets, then the move should automatically fail.
# For example, Helping Hand may not have a target.
return @fail(battle, user) if targets.length == 0
# If there were targets, but they are all no longer alive, then the engine
# outputs a stock message and quits move execution.
targets = targets.filter((p) -> p.isAlive())
if targets.length == 0
battle.cannedText('NO_TARGET')
return
# The move is executing. Run a hook.
@executing(battle, user, targets)
targetsHit = []
totalDamage = 0
for target in targets
continue if @use(battle, user, target, hitNumber) == false
if target.shouldBlockExecution(this, user) == true
@afterFail(battle, user, target)
continue
targetsHit.push(target)
if targetsHit.length > 0
targetSlots = targetsHit.map (target) ->
return [ battle.playerIds.indexOf(target.playerId),
target.team.indexOf(target) ]
user.tell(Protocol.MOVE_SUCCESS, targetSlots, @name)
for target in targetsHit
numHits = @calculateNumberOfHits(battle, user, targets)
wasSlept = user.has(Status.Sleep)
for hitNumber in [1..numHits]
isDirect = @isDirectHit(battle, user, target)
damage = @hit(battle, user, target, hitNumber, isDirect) || 0
@afterHit(battle, user, target, damage, isDirect)
totalDamage += damage
break if target.isFainted() || user.isFainted() ||
(!wasSlept && user.has(Status.Sleep))
if numHits > 1
battle.message @numHitsMessage Math.min(hitNumber, numHits)
# Target faints if it has 0 HP.
for target in targets when target.isFainted()
target.faint()
# Recoil moves
if totalDamage > 0 && @recoil < 0 && !user.hasAbility("Rock Head")
recoil = Math.round(totalDamage * -@recoil / 100)
if user.damage(recoil)
battle.cannedText('RECOIL', user)
# If the move hit 1+ times, query the user's afterAllHits event.
# If the user is affected by Sheer Force, these are all ignored.
if targetsHit.length > 0 &&
(!user.hasAbility("Sheer Force") || !@hasSecondaryEffect())
user.afterAllHits(this)
@afterAllHits(battle, user)
for target in targetsHit
target.afterAllHitsTarget(this, user)
# A hook with a default implementation of returning false on a type immunity.
# If `use` returns false, the `hit` hook is never called.
use: (battle, user, target, hitNumber) ->
if target.isImmune(@getType(battle, user, target), user: user, move: this)
battle.cannedText('IMMUNITY', target)
@afterFail(battle, user, target)
return false
if @willMiss(battle, user, target)
battle.cannedText('MOVE_MISS', target)
@afterMiss(battle, user, target)
return false
# Calculates damage, deals damage, and returns the amount of damage dealt
hit: (battle, user, target, hitNumber, isDirect) ->
damage = @calculateDamage(battle, user, target, hitNumber, isDirect)
if damage > 0
previousHP = target.get(Attachment.Substitute)?.hp ? target.currentHP
damage = target.damage(damage, direct: isDirect, source: "move")
if damage != 0
percent = Math.floor(100 * damage / target.stat('hp'))
battle.cannedText('GOT_HIT', target, percent)
else
currentHP = target.get(Attachment.Substitute)?.hp ? target.currentHP
damage = previousHP - Math.max(0, currentHP)
return damage
# `hit` may be overridden, but we still want to run these callbacks.
afterHit: (battle, user, target, damage, isDirect) ->
# Drain moves
if damage > 0 && @recoil > 0
amount = Math.round(damage * @recoil / 100)
user.drain(amount, target)
battle.cannedText('DRAIN', target)
battle.cannedText('ABSORB', user)
if @shouldTriggerSecondary(battle, user, target, damage, isDirect)
@triggerSecondaryEffect(battle, user, target)
user.afterSuccessfulHit(this, user, target, damage, isDirect)
target.afterBeingHit(this, user, target, damage, isDirect)
@afterSuccessfulHit(battle, user, target, damage, isDirect)
user.update()
target.update()
# Miscellaneous
target.recordHit(user, damage, this, battle.turn, isDirect)
# A hook that runs when the move is finally executing.
executing: (battle, user, targets) ->
# A hook that executes after a pokemon has been successfully damaged by
# a standard move. If execute is overriden, this will not execute.
afterSuccessfulHit: (battle, user, target, damage) ->
# A hook that executes after a pokemon misses an attack. If execute is
# overriden, this will not execute.
afterMiss: (battle, user, target) ->
# A hook that executes after a pokemon fails while using a move (NOT a miss.)
# Examples: Target is immune, target successfully uses Protect/Detect
afterFail: (battle, user, target) ->
# A hook that executes after all hits have completed.
afterAllHits: (battle, user) ->
# A hook that executes when asking if this move should do direct damage
isDirectHit: (battle, user, target) ->
return true if @hasFlag('authentic')
return !target.has(Attachment.Substitute)
# A hook that executes once a move fails.
fail: (battle, user) ->
battle.cannedText('MOVE_FAIL')
numHitsMessage: (hitNumber) ->
return "Hit #{hitNumber} time(s)!"
# A hook that is only used by special "specific-move" targets.
getTargets: (battle, user) ->
throw new Error("Move #{@name} has not implemented getTargets.")
calculateDamage: (battle, user, target, hitNumber=1, isDirect) ->
return 0 if @basePower(battle, user, target, hitNumber) == 0
user.crit = @isCriticalHit(battle, user, target)
damage = @baseDamage(battle, user, target, hitNumber)
# TODO: Multi-target modifier.
damage = @modify(damage, @weatherModifier(battle, user, target))
damage = damage * @criticalMultiplier if user.crit
damage = Math.floor(((100 - battle.rng.randInt(0, 15, "damage roll")) * damage) / 100)
damage = @modify(damage, @stabModifier(battle, user, target))
effectiveness = @typeEffectiveness(battle, user, target)
damage = Math.floor(effectiveness * damage)
damage = Math.floor(@burnCalculation(user) * damage)
damage = Math.max(damage, 1)
damage = @modify(damage, @modifyDamage(battle, user, target, hitNumber))
if effectiveness < 1
battle.cannedText('NOT_VERY_EFFECTIVE')
else if effectiveness > 1
battle.cannedText('SUPER_EFFECTIVE')
if user.crit
battle.cannedText('CRITICAL_HIT')
target.informCriticalHit()
damage
canMiss: (battle, user, target) ->
return true
# An accuracy of 0 means this move always hits. A negative accuracy means the
# target is invulnerable, even for 0-accuracy moves. A move always hits, regardless
# of accuracy and invulnerability, under any of the following conditions:
# * The user or target has No Guard
# * The user is locked onto the target (Lock-On, Mind Reader)
# * The move has a "never-miss" effect built in
# (Helping Hand, XY Toxic used by Poison types)
willMiss: (battle, user, target) ->
return false if user.hasAbility("No Guard") || target.hasAbility("No Guard")
return false if user.get(Attachment.LockOn)?.target == target
return false if !@canMiss(battle, user, target)
accuracy = @chanceToHit(battle, user, target)
accuracy = 100 if accuracy == 0
console.log(accuracy)
battle.rng.randInt(1, 100, "miss") > accuracy
chanceToHit: (battle, user, target) ->
return 0 if user == target
userBoosts = user.editBoosts(ignoreAccuracy: target.hasAbility("Unaware"))
targetBoosts = target.editBoosts(ignoreEvasion: user.hasAbility("Unaware"))
accuracy = @getAccuracy(battle, user, target)
# TODO: Find out how accuracy/evasion is chained and do the rest properly
accuracy = 50 if @isNonDamaging() && accuracy > 50 && target.hasAbility("Wonder Skin")
if userBoosts.accuracy > 0
accuracy = Math.floor(accuracy * (3 + userBoosts.accuracy) / 3)
else if userBoosts.accuracy < 0
accuracy = Math.floor(accuracy / (3 - userBoosts.accuracy) * 3)
if targetBoosts.evasion > 0
accuracy = Math.floor(accuracy / (3 + targetBoosts.evasion) * 3)
else if targetBoosts.evasion < 0
accuracy = Math.floor(accuracy * (3 - targetBoosts.evasion) / 3)
accuracy = user.editAccuracy(accuracy, this, target)
accuracy = target.editEvasion(accuracy, this, user)
accuracy
getAccuracy: (battle, user, target) ->
@accuracy
weatherModifier: (battle, user, target) ->
# TODO: This is wrong.
type = @getType(battle, user, target)
console.log(type)
if type == 'Fire' && battle.hasWeather(Weather.SUN)
0x1800
else if type == 'Fire' && battle.hasWeather(Weather.RAIN)
0x800
else if type == 'Water' && battle.hasWeather(Weather.RAIN)
0x1800
else if type == 'Water' && battle.hasWeather(Weather.SUN)
0x800
else if type == 'Dark' && battle.hasWeather(Weather.MOON)
0x159A
else if type == 'Ghost' && battle.hasWeather(Weather.MOON)
0x159A
else if type == 'Fairy' && battle.hasWeather(Weather.MOON)
0xC00
else
0x1000
stabModifier: (battle, user, target) ->
type = @getType(battle, user, target)
if user.hasType(type)
return 0x2000 if user.hasAbility("Adaptability")
return 0x1800
return 0x1000
ignoresImmunities: ->
@isNonDamaging()
typeEffectiveness: (battle, user, target) ->
type = @getType(battle, user, target)
effect = target.effectivenessOf(type, user: user, move: this)
if target.hasAbility("Ethereal Shroud")
ghosteffect = util.typeEffectiveness(type, ["Ghost"])
if ghosteffect < 1
effect *= ghosteffect
effect
burnCalculation: (user) ->
if @isPhysical() && !user.hasAbility("Guts") && user.has(Status.Burn)
.5
else
1
basePower: (battle, user, target, hitNumber) ->
@power
shouldTriggerSecondary: (battle, user, target, damage, isDirect) ->
return false if !@hasSecondaryEffect()
return false if !isDirect && @secondaryBoostTarget != 'self'
return false if user.hasAbility("Sheer Force")
return false if target.hasAbility("Shield Dust") && @secondaryBoostTarget != 'self'
return true
triggerSecondaryEffect: (battle, user, target) ->
# Multiply chances by 2 if the user has Serene Grace.
chanceMultiplier = (if user.hasAbility("Serene Grace") then 2 else 1)
# Secondary effects
if @ailmentChance > 0 && battle.rng.randInt(0, 99, "secondary effect") < @ailmentChance * chanceMultiplier
target.attach(battle.getAilmentEffect(this), source: user)
# Secondary boosts
if @secondaryBoostChance > 0 && battle.rng.randInt(0, 99, "secondary boost") < @secondaryBoostChance * chanceMultiplier
pokemon = (if @secondaryBoostTarget == 'self' then user else target)
pokemon.boost(@secondaryBoostStats, user)
# Flinching. In the game, flinching is treated subtly different than
# secondary effects. One result is that the Fang moves can both inflict
# a secondary effect as well as flinch.
if @flinchChance > 0 && battle.rng.randInt(0, 99, "flinch") < @flinchChance * chanceMultiplier
target.attach(Attachment.Flinch)
isCriticalHit: (battle, attacker, defender) ->
return false if defender.team?.has(Attachment.LuckyChant)
return false if defender.ability?.preventsCriticalHits
chLevel = @criticalHitLevel(battle, attacker, defender)
rand = battle.rng.next("ch")
@determineCriticalHitFromLevel(chLevel, rand)
determineCriticalHitFromLevel: (level, rand) ->
switch level
when -1
true
when 1
rand < 0.0625
when 2
rand < 0.125
when 3
rand < 0.25
when 4
rand < 1/3
else
rand < .5
criticalHitLevel: (battle, attacker, defender) ->
# -1 means always crits
return @chLevel if @chLevel == -1
stage = @chLevel
stage += 1 if attacker.hasAbility('Super Luck')
stage += 2 if attacker.has(Attachment.FocusEnergy)
stage += attacker.criticalModifier()
stage
modify: (number, modifier) ->
Math.ceil((number * modifier) / 0x1000 - 0.5)
baseDamage: (battle, user, target, hitNumber=1) ->
floor = Math.floor
uStat = @pickAttackStat(user, target)
tStat = @pickDefenseStat(user, target)
if battle.hasWeather(Weather.SAND) && target.hasType("Rock") && @isSpecial()
tStat = @modify(tStat, 0x1800)
damage = floor((2 * user.level) / 5 + 2)
damage *= @basePower(battle, user, target, hitNumber)
damage = @modify(damage, @modifyBasePower(battle, user, target))
damage *= @modify(uStat, @modifyAttack(battle, user, target))
damage = floor(damage / tStat)
damage = floor(damage / 50)
damage += 2
damage
calculateNumberOfHits: (battle, user, targets) ->
numHits = user.calculateNumberOfHits(this, targets)
if numHits
numHits
else if @minHits == @maxHits
@maxHits
else if @minHits == 2 && @maxHits == 5
# hard coding moves like fury swipes to have 2-3 hits have a 1/3 chance, and 4-5 have 1/6th
battle.rng.choice([2, 2, 3, 3, 4, 5], "num hits")
else
battle.rng.randInt(@minHits, @maxHits, "num hits")
modifyBasePower: (battle, user, target) ->
modify = Query.modifiers("modifyBasePower", user.attachments.all(), this, target)
modify = @modify(modify, Query.modifiers("modifyBasePowerTarget", target.attachments.all(), this, user))
modifyDamage: (battle, user, target, hitNumber) ->
modify = Query.modifiers('modifyDamageTarget', target.team.attachments.all(), this, user, hitNumber)
modify = @modify(modify, Query.modifiers('modifyDamage', user.attachments.all(), this, target, hitNumber))
modify = @modify(modify, Query.modifiers('modifyDamageTarget', target.attachments.all(), this, user, hitNumber))
modifyAttack: (battle, user, target) ->
modify = Query.modifiers('modifyAttack', user.attachments.all(), this, target)
modify = @modify(modify, Query.modifiers('modifyAttackTarget', target.attachments.all(), this, user))
getType: (battle, user, target) ->
type = user.editMoveType(@type, target)
type
pickAttackStat: (user, target) ->
stat = (if @isPhysical() then 'attack' else 'specialAttack')
stat = 'specialAttack' if user.hasAbility("Spectral Jaws") and @hasFlag("bite")
user.stat(stat, ignoreNegativeBoosts: user.crit, ignoreOffense: target.hasAbility("Unaware"))
pickDefenseStat: (user, target) ->
stat = (if @isPhysical() then 'defense' else 'specialDefense')
stat = 'specialDefense' if user.hasAbility("Spectral Jaws") and @hasFlag("bite")
target.stat(stat, ignorePositiveBoosts: user.crit, ignoreDefense: user.hasAbility("Unaware"))
toString: ->
"[Move name:#{@name}]"

701
server/bw/pokemon.coffee Normal file
View File

@@ -0,0 +1,701 @@
{_} = require 'underscore'
{Ability, Item, Moves, SpeciesData, FormeData} = require './data'
{Attachment, Status, Attachments} = require './attachment'
{Weather} = require '../../shared/weather'
{Protocol} = require '../../shared/protocol'
Query = require './queries'
util = require './util'
floor = Math.floor
class @Pokemon
# TODO: Take the species obj, not attributes?
constructor: (attributes = {}) ->
# Inject battle and team dependencies
@battle = attributes.battle
@team = attributes.team
@playerId = attributes.playerId
@species = attributes.species || "Missingno"
@name = attributes.name || @species
@forme = attributes.forme || "default"
@level = attributes.level || 120
@gender = attributes.gender || "Genderless"
@shiny = attributes.shiny
@nfe = (SpeciesData[@species]?.evolvesInto?.length > 0)
@tempsprite = null #Used for Mega Zoruark
@originalname = @name
@attachments = new Attachments()
@resetForme()
@nature = attributes.nature
@evs = attributes.evs || {}
@ivs = attributes.ivs || {}
@currentHP = @stat('hp')
@moves = (attributes.moves || []).map (move) -> Moves[move]
@used = {}
@resetAllPP()
@item = Item[attributes.item?.replace(/\s+/g, '')]
@ability = Ability[attributes.ability?.replace(/\s+/g, '')]
@originalAbility = @ability
@originalForme = @forme
@status = null
@stages =
attack: 0
defense: 0
speed: 0
specialDefense: 0
specialAttack: 0
evasion: 0
accuracy: 0
# What moves are blocked, and is switching blocked, and is item blocked
@resetBlocks()
# a record of how long this pokemon has been in play.
@turnsActive = 0
# a record of the last move used by this pokemon.
@lastMove = null
# a record of the last item used by this pokemon.
# if the item is removed by someone else, it is not recorded.
@lastItem = null
# the time the pokemon officially fainted. 0 means never fainted.
@fainted = 0
getForme: (newForme) ->
availableFormes = FormeData[@species] || {}
availableFormes[newForme || @forme]
isInForme: (forme) ->
@forme == forme
changeForme: (newForme) ->
return false if !@getForme(newForme)
if @species == 'Zoroark' && newForme == 'mega'
alivemons = @team.getAlivePokemon()
lastalivemon = alivemons[alivemons.length-1]
possibleform = FormeData[lastalivemon.species]
if 'mega' of possibleform
@changeSprite(lastalivemon.species, 'mega')
else
@changeSprite(newForme)
@forme = newForme
@resetForme()
return true
resetForme: ->
forme = @getForme() || {}
@baseStats = _.clone(forme.stats) || {}
@types = _.clone(forme.types) || []
@weight = forme.weight
changeSprite: (newSpecies, newForme) ->
if arguments.length == 1
[newSpecies, newForme] = [@species, newSpecies]
@tempsprite = newSpecies
@tell(Protocol.SPRITE_CHANGE, newSpecies, newForme)
iv: (stat) -> (if stat of @ivs then @ivs[stat] else 31)
ev: (stat) -> (if stat of @evs then @evs[stat] else 0)
pp: (move) -> @ppHash[move.name]
maxPP: (move) -> @maxPPHash[move.name]
reducePP: (move, amount = 1) ->
@setPP(move, @pp(move) - amount)
setPP: (move, pp) ->
pp = Math.max(pp, 0)
pp = Math.min(pp, @maxPP(move))
@ppHash[move.name] = pp
@battle?.tellPlayer(@playerId,
Protocol.CHANGE_PP,
@battle.getPlayerIndex(@playerId),
@team.pokemon.indexOf(this),
@moves.indexOf(move),
pp)
pp
resetAllPP: (pp) ->
@ppHash = {}
@maxPPHash = {}
if @moves?
for move in @moves
@ppHash[move.name] = @maxPPHash[move.name] = pp || (move.pp * 8/5)
# Gets the stat indexed by key.
# Ex: pokemon.stat('hp')
# TODO: Precalculate the stats in the constructor
stat: (key, options = {}) ->
base = @baseStats[key] || 100
return 1 if base == 1 # For Shedinja. key doesn't have to be hp.
iv = @iv(key)
ev = floor(@ev(key) / 4)
total = if key == 'hp'
floor((2 * base + iv + ev) * (@level / 100) + @level + 10)
else
floor(((2 * base + iv + ev) * (@level / 100) + 5) * @natureBoost(key))
capitalized = key[0].toUpperCase() + key.substr(1)
total = Query.chain("edit#{capitalized}", @attachments.all(), total)
if @team?.attachments
total = Query.chain("edit#{capitalized}", @team.attachments.all(), total)
total = @statBoost(key, total, options) if key != 'hp'
total
# Returns 1.1, 1.0, or 0.9 according to whether a Pokemon's nature corresponds
# to that stat. The default return value is 1.0.
natureBoost: (stat) ->
nature = @nature?.toLowerCase()
if nature of natures
natures[nature][stat] || 1
else
1
statBoost: (statName, total, options = {}) ->
stages = @editBoosts(options)
boost = stages[statName]
if boost >= 0
Math.floor((2 + boost) * total / 2)
else
Math.floor(2 * total / (2 - boost))
# Boosts this pokemon's stats by the given number of stages.
# Returns true whether any stat was boosted, false otherwise.
#
# Example: pokemon.boost(specialAttack: 1, evasion: 2)
#
boost: (boosts, source = this) ->
return false if @isFainted()
boosts = Query.chain('transformBoosts', @attachments.all(), _.clone(boosts), source)
deltaBoosts = {}
didBoost = false
for stat, amount of boosts
amount *= -1 if @ability == Ability.Contrary
if stat not of @stages
throw new Error("Tried to boost non-existent stat #{stat} by #{amount}")
previous = @stages[stat]
@stages[stat] += amount
@stages[stat] = Math.max(-6, @stages[stat])
@stages[stat] = Math.min(6, @stages[stat])
deltaBoosts[stat] = (@stages[stat] - previous)
didBoost ||= (deltaBoosts[stat] != 0)
@afterEachBoost(deltaBoosts[stat], source)
@tell(Protocol.BOOSTS, deltaBoosts) if didBoost
didBoost
positiveBoostCount: ->
count = 0
for stage, total of @stages
count += total if total > 0
count
hasBoosts: ->
for stage, value of @stages
return true if value != 0
return false
# Sets boosts, but also send a message to clients
setBoosts: (boosts) ->
for stat, amount of boosts
@stages[stat] = amount
@tell(Protocol.SET_BOOSTS, boosts)
resetBoosts: ->
@stages.attack = 0
@stages.defense = 0
@stages.speed = 0
@stages.specialAttack = 0
@stages.specialDefense = 0
@stages.accuracy = 0
@stages.evasion = 0
@tell(Protocol.RESET_BOOSTS)
true
hasType: (type) ->
type in @types
hasAbility: (ability) ->
return false unless @ability
return false if @isAbilityBlocked()
if typeof ability == 'string'
@ability.displayName == ability
else
@ability == ability
hasItem: (itemName) ->
if itemName?
@item?.displayName == itemName
else
@item?
hasStatus: ->
return !!@status
has: (attachment) ->
@attachments.contains(attachment)
get: (attachment) ->
@attachments.get(attachment)
cureAttachment: (attachment, options) ->
if attachment.name of Status
@cureStatus(attachment, options)
else
@cureAilment(attachment, options)
cureStatus: (status, options) ->
return false if !@status
[ status, options ] = [ @status, status ] if status?.name not of Status
return false if status != @status
@cureAilment(@status, options)
@status = null
return true
cureAilment: (ailment, options = {}) ->
return false if !@has(ailment)
shouldMessage = options.message ? true
if @battle && shouldMessage
if shouldMessage == true
message = switch ailment
when Status.Paralyze then " was cured of paralysis."
when Status.Burn then " healed its burn!"
when Status.Sleep then " woke up!"
when Status.Toxic then " was cured of its poisoning."
when Status.Poison then " was cured of its poisoning."
when Status.Freeze then " thawed out!"
when Attachment.Confusion then " snapped out of its confusion."
else
source = options.message
message = switch ailment
when Status.Paralyze then "'s #{source} cured its paralysis!"
when Status.Burn then "'s #{source} healed its burn!"
when Status.Sleep then "'s #{source} woke it up!"
when Status.Toxic then "'s #{source} cured its poison!"
when Status.Poison then "'s #{source} cured its poison!"
when Status.Freeze then "'s #{source} defrosted it!"
when Attachment.Confusion then "'s #{source} snapped it out of its confusion!"
@battle.message("#{@name}#{message}")
@unattach(ailment)
return true
setAbility: (ability) ->
@unattach(@ability) if @ability
@ability = ability
initializeAbility: ->
@attach(@ability).switchIn?() if @ability && !@isAbilityBlocked()
# TODO: really ugly copying of ability
copyAbility: (ability, options = {}) ->
shouldShow = options.reveal ? true
@activateAbility() if shouldShow
@setAbility(ability)
@activateAbility() if shouldShow
@initializeAbility()
swapAbilityWith: (target) ->
# Abilities are not revealed during the swap
# if the user and the target are on the same side
if @team != target.team
@activateAbility()
target.activateAbility()
uAbility = @ability
@setAbility(target.ability)
target.setAbility(uAbility)
if @team != target.team
@activateAbility()
target.activateAbility()
@battle.cannedText('SWAP_ABILITY', this)
@initializeAbility()
target.initializeAbility()
hasChangeableAbility: ->
!@hasAbility("Multitype")
setItem: (item, options = {}) ->
if @hasItem() then @removeItem()
@item = item
@lastItem = null if options.clearLastItem
attachment = @attach(@item)
attachment.switchIn?() if !@isItemBlocked()
getItem: ->
@item
useItem: ->
item = @item
@removeItem()
@lastItem = item
removeItem: ->
return unless @item
@attach(Attachment.Unburden) if @hasAbility("Unburden")
@get(@item).switchOut?()
@unattach(@item)
oldItem = @item
@item = null
oldItem
hasTakeableItem: ->
return false if !@hasItem()
return false if @item.type == 'mail'
return false if @item.type == 'key'
return false if @hasAbility("Multitype") && @item.plate
return false if @species == 'Giratina' && @forme == 'origin'
return false if @species == 'Genesect' && /Drive$/.test(@item.displayName)
true
# This differs from hasTakeableItem by virtue of Sticky Hold
canLoseItem: ->
@hasTakeableItem() && !@has(Ability.StickyHold)
canHeal: ->
!@has(Attachment.HealBlock)
isActive: ->
this in @team.getActiveAlivePokemon()
isAlive: ->
!@isFainted()
isFainted: ->
@currentHP <= 0
faint: ->
return if @fainted
if @battle
@battle.message "#{@name} fainted!"
@battle.tell(Protocol.FAINT, @battle.getPlayerIndex(@playerId), @battle.getSlotNumber(this))
# Remove pending actions they had.
@battle.popAction(this)
@fainted = @battle.incrementFaintedCounter()
@setHP(0) if !@isFainted()
# TODO: If a Pokemon faints in an afterFaint, should it be added to this?
Query('afterFaint', @attachments.all())
# TODO: Do fainted Pokémon need attachments in any case?
# If so, #attach will need to be revisited as well.
@unattachAll()
@team.faintedThisTurn = true
damage: (amount, options = {}) ->
amount = Math.max(1, amount)
amount = @transformHealthChange(amount, options)
@setHP(@currentHP - amount)
heal: (amount) ->
if amount > 0 && @currentHP < @stat('hp') && !@canHeal()
@battle.cannedText('HEAL_BLOCK_TRY_HEAL', this)
return false
@setHP(@currentHP + amount)
drain: (amount, source) ->
if @hasItem("Big Root") && !@isItemBlocked()
amount = util.roundHalfDown(amount * 1.3)
amount *= -1 if source != this && source?.hasAbility("Liquid Ooze")
@heal(amount)
transformHealthChange: (damage, options) ->
Query.chain('transformHealthChange', @attachments.all(), damage, options)
editPriority: (priority, move) ->
Query.chain('editPriority', @attachments.all(), priority, move)
editBoosts: (opts = {}) ->
stages = Query.chain('editBoosts', @attachments.all(), _.clone(@stages))
for stat, amt of stages
amt = 0 if opts.ignorePositiveBoosts && amt > 0
amt = 0 if opts.ignoreNegativeBoosts && amt < 0
amt = 0 if opts.ignoreEvasion && stat == 'evasion'
amt = 0 if opts.ignoreAccuracy && stat == 'accuracy'
amt = 0 if opts.ignoreOffense && stat in [ 'attack', 'specialAttack' ]
amt = 0 if opts.ignoreDefense && stat in [ 'defense', 'specialDefense' ]
stages[stat] = amt
return stages
editAccuracy: (accuracy, move, target) ->
Query.chain('editAccuracy', @attachments.all(), accuracy, move, target)
editEvasion: (accuracy, move, user) ->
Query.chain('editEvasion', @attachments.all(), accuracy, move, user)
editMoveType: (type, target) ->
Query.chain('editMoveType', @attachments.all(), type, target)
calculateWeight: ->
Query.chain('calculateWeight', @attachments.all(), @weight)
criticalModifier: ->
Query.chain('criticalModifier', @attachments.all(), 0)
afterEachBoost: (boostAmount, source = this) ->
Query('afterEachBoost', @attachments.all(), boostAmount, source)
afterAllHits: (move) ->
Query('afterAllHits', @attachments.all(), move)
afterAllHitsTarget: (move, user) ->
Query('afterAllHitsTarget', @attachments.all(), move, user)
setHP: (hp) ->
oldHP = @currentHP
@currentHP = Math.min(@stat('hp'), hp)
@currentHP = Math.max(@currentHP, 0)
delta = oldHP - @currentHP
if delta != 0
percent = Math.ceil(100 * @currentHP / @stat('hp'))
@tell(Protocol.CHANGE_HP, percent)
@tellPlayer(Protocol.CHANGE_EXACT_HP, @currentHP)
delta
recordMove: (move) ->
@lastMove = move
@used[move.name] = true
recordHit: (pokemon, damage, move, turn, direct) ->
team = pokemon.team
slot = team.indexOf(pokemon)
@lastHitBy = {team, slot, damage, move, turn, direct}
isImmune: (type, options = {}) ->
b = Query.untilNotNull('isImmune', @attachments.all(), type, options.move)
if b? then return b
return false if options.move?.ignoresImmunities()
multiplier = @effectivenessOf(type, options)
return multiplier == 0
effectivenessOf: (type, options) ->
hash = {}
if options.user
type = options.user.editMoveType(type, this)
hash.ignoreImmunities = options.user.shouldIgnoreImmunity(type, this)
hash.superEffectiveAgainst = options.move?.superEffectiveAgainst
util.typeEffectiveness(type, @types, hash)
isWeatherDamageImmune: (weather) ->
b = Query.untilNotNull('isWeatherDamageImmune', @attachments.all(), weather)
if b? then return b
return true if weather == Weather.HAIL && @hasType("Ice")
return true if weather == Weather.SAND && (@hasType("Ground") ||
@hasType("Rock") || @hasType("Steel"))
return @battle?.hasWeatherCancelAbilityOnField() || false
activate: ->
@turnsActive = 0
@attach(@ability) if @ability
@attach(@item) if @item
switchIn: ->
Query('switchIn', @attachments.all())
switchOut: ->
delete @lastMove
@used = {}
Query('switchOut', @attachments.all())
@attachments.unattachAll((a) -> a.volatile)
@resetForme()
@resetBoosts()
@resetBlocks()
@ability = @originalAbility
@changeForme(@originalForme) if @forme != @originalForme
informSwitch: (switcher) ->
Query('informSwitch', @attachments.all(), switcher)
shouldPhase: (phaser) ->
Query.untilFalse('shouldPhase', @attachments.all(), phaser) != false
shouldIgnoreImmunity: (type, target) ->
Query.untilTrue('shouldIgnoreImmunity', @attachments.all(), type, target)
informCriticalHit: ->
Query('informCriticalHit', @attachments.all())
informWeather: (weather) ->
Query('informWeather', @attachments.all(), weather)
beginTurn: ->
Query('beginTurn', @attachments.all())
beforeMove: (move, user, targets) ->
Query.untilFalse('beforeMove', @attachments.all(), move, user, targets)
afterMove: (move, user, targets) ->
Query('afterMove', @attachments.all(), move, user, targets)
shouldBlockExecution: (move, user) ->
Query.untilTrue('shouldBlockExecution', @attachments.all(), move, user)
update: ->
Query('update', @attachments.all())
afterTurnOrder: ->
Query('afterTurnOrder', @attachments.all())
calculateNumberOfHits: (move, targets) ->
Query.untilNotNull("calculateNumberOfHits", @attachments.all(), move, targets)
resetRecords: ->
@lastHitBy = null
# Hook for when the Pokemon gets hit by a move
afterBeingHit: (move, user, target, damage, isDirect) ->
Query('afterBeingHit', @attachments.all(), move, user, target, damage, isDirect)
afterSuccessfulHit: (move, user, target, damage) ->
Query('afterSuccessfulHit', @attachments.all(), move, user, target, damage)
# Adds an attachment to the list of attachments
attach: (attachment, options={}) ->
if @isFainted()
return false
options = _.clone(options)
@attachments.push(attachment, options, battle: @battle, team: @team, pokemon: this)
# Removes an attachment from the list of attachment
unattach: (klass) ->
# TODO: Do we need to remove circular dependencies?
# Removing them here will result in some unanticipated consequenes.
@attachments.unattach(klass)
unattachAll: ->
@attachments.unattachAll()
# Blocks a move for a single turn
blockMove: (move) ->
@blockedMoves.push(move)
# Blocks all moves for a single turn
blockMoves: ->
@blockMove(move) for move in @moves
isMoveBlocked: (move) ->
return (move in @blockedMoves)
isSwitchBlocked: ->
@switchBlocked
# Returns true if the Pokemon has no item or the item has been blocked.
isItemBlocked: ->
!@item? || @itemBlocked
# Blocks a switch for a single turn
blockSwitch: ->
@switchBlocked = true unless !@isItemBlocked() && @hasItem("Shed Shell")
# Blocks an item for a single turn
blockItem: ->
@itemBlocked = true
# Blocks an ability for a single turn
blockAbility: ->
@abilityBlocked = true
unblockAbility: ->
@abilityBlocked = false
isAbilityBlocked: ->
@abilityBlocked
resetBlocks: ->
@blockedMoves = []
@switchBlocked = false
@itemBlocked = false
@abilityBlocked = false
# Locks the Pokemon into a single move. Does not limit switches.
lockMove: (moveToLock) ->
for move in @validMoves()
@blockMove(move) if move != moveToLock
activateAbility: ->
@tell(Protocol.ACTIVATE_ABILITY, @ability?.displayName)
tell: (protocol, args...) ->
return unless @battle
args = [ @battle.getPlayerIndex(@playerId), @team.indexOf(this), args... ]
@battle.tell(protocol, args...)
tellPlayer: (protocol, args...) ->
return unless @battle
args = [ @battle.getPlayerIndex(@playerId), @team.indexOf(this), args... ]
@battle.tellPlayer(@playerId, protocol, args...)
# Returns whether this Pokemon has this move in its moveset.
knows: (move) ->
move in @moves
# A list of moves that this pokemon can use freely
validMoves: ->
moves = _(@moves).difference(@blockedMoves)
moves = moves.filter((move) => @pp(move) > 0)
moves
toString: ->
"[Pokemon species:#{@species} hp:#{@currentHP}/#{@stat('hp')}]"
movesetJSON: ->
return {
"moves" : @moves.map (m) -> m.name
"moveTypes" : @moves.map (m) -> m.type
"pp" : @moves.map (m) => @pp(m)
"maxPP" : @moves.map (m) => @maxPP(m)
}
toJSON: (options = {}) ->
base =
"species" : @species
"name" : @name
"level" : @level
"gender" : @gender
"boosts" : @stages
"forme" : @forme
"shiny" : @shiny == true
return base if options.hidden
_.extend base, @movesetJSON(),
"hp" : @currentHP
"maxHP" : @stat('hp')
"ivs" :
hp: @iv('hp')
attack: @iv('attack')
defense: @iv('defense')
speed: @iv('speed')
specialAttack: @iv('specialAttack')
specialDefense: @iv('specialDefense')
base["item"] = @item.displayName if @item
base["ability"] = @ability.displayName if @ability
base
# A hash that keys a nature with the stats that it boosts.
# Neutral natures are ignored.
PLUS = 1.1
MINUS = 0.9
natures =
lonely: {attack: PLUS, defense: MINUS}
brave: {attack: PLUS, speed: MINUS}
adamant: {attack: PLUS, specialAttack: MINUS}
naughty: {attack: PLUS, specialDefense: MINUS}
bold: {defense: PLUS, attack: MINUS}
relaxed: {defense: PLUS, speed: MINUS}
impish: {defense: PLUS, specialAttack: MINUS}
lax: {defense: PLUS, specialDefense: MINUS}
timid: {speed: PLUS, attack: MINUS}
hasty: {speed: PLUS, defense: MINUS}
jolly: {speed: PLUS, specialAttack: MINUS}
naive: {speed: PLUS, specialDefense: MINUS}
modest: {specialAttack: PLUS, attack: MINUS}
mild: {specialAttack: PLUS, defense: MINUS}
quiet: {specialAttack: PLUS, speed: MINUS}
rash: {specialAttack: PLUS, specialDefense: MINUS}
calm: {specialDefense: PLUS, attack: MINUS}
gentle: {specialDefense: PLUS, defense: MINUS}
sassy: {specialDefense: PLUS, speed: MINUS}
careful: {specialDefense: PLUS, specialAttack: MINUS}

204
server/bw/priorities.coffee Normal file
View File

@@ -0,0 +1,204 @@
{_} = require('underscore')
{Ability} = require('./data/abilities')
{Item} = require('./data/items')
{Attachment, Status} = require('./attachment')
module.exports = Priorities = {}
Priorities.beforeMove ?= [
# Things that should happen no matter what
Attachment.Pursuit
Attachment.Fling
Attachment.DestinyBond
# Order-dependent
Status.Freeze
Status.Sleep
Ability.Truant
Attachment.Flinch
Attachment.Disable
Attachment.HealBlock
Attachment.GravityPokemon
Attachment.Taunt
Attachment.ImprisonPrevention
Attachment.Confusion
Attachment.Attract
Status.Paralyze
# Things that should happen only if the move starts executing
Attachment.FocusPunch
Attachment.Recharge
Attachment.Metronome
Attachment.Grudge
Attachment.Rage
Attachment.Charging
Attachment.FuryCutter
Item.ChoiceBand
Item.ChoiceScarf
Item.ChoiceSpecs
Ability.MoldBreaker
Ability.Teravolt
Ability.Turboblaze
]
Priorities.switchIn ?= [
Attachment.BatonPass
# Order-dependent
Ability.Unnerve
Attachment.HealingWish
Attachment.LunarDance
Attachment.StealthRock
Attachment.Spikes
Attachment.ToxicSpikes
# TODO: Are these in the correct order?
Ability.AirLock
Ability.CloudNine
Ability.Chlorophyll
Ability.SwiftSwim
Ability.SandRush
Ability.Drizzle
Ability.Drought
Ability.SandStream
Ability.SnowWarning
Ability.MoldBreaker
Ability.Teravolt
Ability.Turboblaze
Ability.Anticipation
Ability.ArenaTrap
Ability.Download
Ability.Forewarn
Ability.Frisk
Ability.Imposter
Ability.Intimidate
Ability.Klutz
Ability.MagicBounce
Ability.MagnetPull
Ability.Pressure
Ability.ShadowTag
Ability.SlowStart
Ability.Trace
]
Priorities.endTurn ?= [
# Non-order-dependent
Attachment.AbilityCancel
Attachment.Flinch
Attachment.Roost
Attachment.MicleBerry
Attachment.LockOn
Attachment.Recharge
Attachment.Momentum
Attachment.MeFirst
Attachment.Charge
Attachment.ProtectCounter
Attachment.Protect
Attachment.Endure
Attachment.Pursuit
Attachment.Present
Attachment.MagicCoat
Attachment.EchoedVoice
Attachment.Rampage
Attachment.Fling
Attachment.DelayedAttack
Ability.SlowStart
# Order-dependent
Ability.RainDish
Ability.DrySkin
Ability.SolarPower
Ability.IceBody
# Team attachments
Attachment.FutureSight
Attachment.DoomDesire
Attachment.Wish
# TODO: Fire Pledge/Grass Pledge
Ability.ShedSkin
Ability.Hydration
Ability.Healer
Item.Leftovers
Item.BlackSludge
Attachment.AquaRing
Attachment.Ingrain
Attachment.LeechSeed
Status.Burn
Status.Toxic
Status.Poison
Ability.PoisonHeal
Attachment.Nightmare
Attachment.Curse
Attachment.Trap
Attachment.Taunt
Attachment.Encore
Attachment.Disable
Attachment.MagnetRise
Attachment.Telekinesis
Attachment.HealBlock
Attachment.Embargo
Attachment.Yawn
Attachment.PerishSong
Attachment.Reflect
Attachment.LightScreen
Attachment.Screen
# Attachment.Mist
Attachment.Safeguard
Attachment.Tailwind
Attachment.LuckyChant
# TODO: Pledge moves
Attachment.Gravity
Attachment.GravityPokemon
Attachment.TrickRoom
# Attachment.WonderRoom
# Attachment.MagicRoom
Attachment.Uproar
Ability.SpeedBoost
Ability.BadDreams
Ability.Harvest
Ability.Moody
Item.ToxicOrb
Item.FlameOrb
Item.StickyBarb
# Ability.ZenMode
]
Priorities.shouldBlockExecution ?= [
# Type-immunity/Levitate (Move#use)
# Wide Guard/Quick Guard
Attachment.Protect
Attachment.MagicCoat
# TODO: Reimplement Magic Bounce as its own thing
Ability.DrySkin
Ability.FlashFire
Ability.Lightningrod
Ability.MotorDrive
Ability.SapSipper
Ability.Soundproof
Ability.StormDrain
Ability.Telepathy
Ability.VoltAbsorb
Ability.WaterAbsorb
Ability.WonderGuard
Attachment.Ingrain
Attachment.Charging
Attachment.SmackDown
Attachment.Substitute
]
Priorities.isImmune ?= [
Attachment.GravityPokemon # Gravity overrides Ground-type immunities.
Attachment.Ingrain
Attachment.SmackDown
Item.IronBall
Attachment.Telekinesis
Ability.Levitate
Attachment.MagnetRise
Item.AirBalloon
Attachment.Identify
Ability.Soundproof
]

46
server/bw/queries.coffee Normal file
View File

@@ -0,0 +1,46 @@
{_} = require('underscore')
orderByPriority = (arrayOfAttachments, eventName) ->
Priorities = require('./priorities')
return _.clone(arrayOfAttachments) if eventName not of Priorities
array = arrayOfAttachments.map (attachment) ->
[ attachment, Priorities[eventName].indexOf(attachment.constructor) ]
array.sort((a, b) -> a[1] - b[1])
array.map((a) -> a[0])
queryUntil = (funcName, conditional, attachments, args...) ->
for attachment in orderByPriority(attachments, funcName)
continue if !attachment.valid()
if funcName of attachment
result = attachment[funcName].apply(attachment, args)
break if conditional(result)
result
module.exports = Query = (funcName, args...) ->
queryUntil(funcName, (-> false), args...)
Query.untilTrue = (funcName, args...) ->
conditional = (result) -> result == true
queryUntil(funcName, conditional, args...)
Query.untilFalse = (funcName, args...) ->
conditional = (result) -> result == false
queryUntil(funcName, conditional, args...)
Query.untilNotNull = (funcName, args...) ->
conditional = (result) -> result?
queryUntil(funcName, conditional, args...)
Query.chain = (funcName, attachments, result, args...) ->
for attachment in orderByPriority(attachments, funcName)
continue if !attachment.valid()
result = attachment[funcName].call(attachment, result, args...) if funcName of attachment
result
Query.modifiers = (funcName, attachments, args...) ->
result = 0x1000
for attachment in orderByPriority(attachments, funcName)
continue unless funcName of attachment && attachment.valid()
modifier = attachment[funcName].apply(attachment, args)
result = Math.floor((result * modifier + 0x800) / 0x1000)
result

15
server/bw/rng.coffee Normal file
View File

@@ -0,0 +1,15 @@
class @FakeRNG
constructor: ->
next: (id) ->
Math.random()
# Returns a random integer N such that min <= N <= max.
randInt: (min, max, id) ->
Math.floor(@next(id) * (max + 1 - min) + min)
# Returns a random element in the array.
# Assumes the array is above length 0.
choice: (array, id) ->
index = @randInt(0, array.length - 1, id || "random choice")
array[index]

133
server/bw/team.coffee Normal file
View File

@@ -0,0 +1,133 @@
{_} = require 'underscore'
{Pokemon} = require './pokemon'
{Attachments} = require './attachment'
{Protocol} = require '../../shared/protocol'
Query = require('./queries')
class @Team
constructor: (@battle, @playerId, @playerName, pokemon, @numActive) ->
@pokemon = pokemon.map (attributes) =>
# TODO: Is there a nicer way of doing these injections?
attributes.battle = @battle
attributes.team = this
attributes.playerId = @playerId
new Pokemon(attributes)
@attachments = new Attachments()
# Has a Pokemon from this team fainted?
@faintedLastTurn = false
@faintedThisTurn = false
arrange: (arrangement) ->
@pokemon = (@pokemon[index] for index in arrangement)
at: (index) ->
@pokemon[index]
all: ->
@pokemon.slice(0)
slice: (args...) ->
@pokemon.slice(args...)
indexOf: (pokemon) ->
@pokemon.indexOf(pokemon)
contains: (pokemon) ->
@indexOf(pokemon) != -1
first: ->
@at(0)
has: (attachment) ->
@attachments.contains(attachment)
get: (attachmentName) ->
@attachments.get(attachmentName)
attach: (attachment, options={}) ->
options = _.clone(options)
attachment = @attachments.push(attachment, options, battle: @battle, team: this)
if attachment then @tell(Protocol.TEAM_ATTACH, attachment.name)
attachment
unattach: (klass) ->
attachment = @attachments.unattach(klass)
if attachment then @tell(Protocol.TEAM_UNATTACH, attachment.name)
attachment
tell: (protocol, args...) ->
playerIndex = @battle.getPlayerIndex(@playerId)
@battle?.tell(protocol, playerIndex, args...)
switch: (pokemon, toPosition) ->
newPokemon = @at(toPosition)
index = @indexOf(pokemon)
playerIndex = @battle.getPlayerIndex(@playerId)
@battle.removeRequest(@playerId, index)
@battle.cancelAction(pokemon)
@battle.tell(Protocol.SWITCH_OUT, playerIndex, index)
p.informSwitch(pokemon) for p in @battle.getOpponents(pokemon)
@switchOut(pokemon)
@replace(pokemon, toPosition)
@switchIn(newPokemon)
replace: (pokemon, toPosition) ->
[ a, b ] = [ @indexOf(pokemon), toPosition ]
[@pokemon[a], @pokemon[b]] = [@pokemon[b], @pokemon[a]]
theSwitch = @at(a)
theSwitch.tell(Protocol.SWITCH_IN, b)
theSwitch
shouldBlockFieldExecution: (move, user) ->
Query.untilTrue('shouldBlockFieldExecution', @attachments.all(), move, user)
switchOut: (pokemon) ->
Query('switchOut', @attachments.all(), pokemon)
pokemon.switchOut()
switchIn: (pokemon) ->
pokemon.activate()
Query('switchIn', @attachments.all(), pokemon)
pokemon.switchIn()
getAdjacent: (pokemon) ->
index = @pokemon.indexOf(pokemon)
adjacent = []
return adjacent if index < 0 || index >= @numActive
adjacent.push(@at(index - 1)) if index > 1
adjacent.push(@at(index + 1)) if index < @numActive - 1
adjacent.filter((p) -> p.isAlive())
getActivePokemon: ->
@pokemon.slice(0, @numActive)
getActiveAlivePokemon: ->
@getActivePokemon().filter((pokemon) -> pokemon.isAlive())
getAlivePokemon: ->
@pokemon.filter((pokemon) -> !pokemon.isFainted())
getActiveFaintedPokemon: ->
@getActivePokemon().filter((pokemon) -> pokemon.isFainted())
getFaintedPokemon: ->
@pokemon.filter((pokemon) -> pokemon.isFainted())
getBenchedPokemon: ->
@pokemon.slice(@numActive)
getAliveBenchedPokemon: ->
@getBenchedPokemon().filter((pokemon) -> !pokemon.isFainted())
size: ->
@pokemon.length
filter: ->
@pokemon.filter.apply(@pokemon, arguments)
toJSON: (options = {}) -> {
"pokemon": @pokemon.map (p) -> p.toJSON(options)
"owner": @playerName
}

55
server/bw/util.coffee Normal file
View File

@@ -0,0 +1,55 @@
@roundHalfDown = (number) ->
Math.ceil(number - .5)
@typeEffectiveness = (userType, againstTypes, options = {}) ->
userType = Type[userType]
effectiveness = 1
for subtype in againstTypes
targetType = Type[subtype]
multiplier = typeChart[userType][targetType]
multiplier = 1 if multiplier == 0 && options.ignoreImmunities
multiplier = 2 if options.superEffectiveAgainst == subtype
effectiveness *= multiplier
effectiveness
@Type = Type =
Normal : 0
Fire : 1
Water : 2
Electric : 3
Grass : 4
Ice : 5
Fighting : 6
Poison : 7
Ground : 8
Flying : 9
Psychic : 10
Bug : 11
Rock : 12
Ghost : 13
Dragon : 14
Dark : 15
Steel : 16
"???" : 17
typeChart = [
# Nor Fir Wat Ele Gra Ice Fig Poi Gro Fly Psy Bug Roc Gho Dra Dar Ste ???
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, .5, 0, 1, 1, .5, 1 ], # Nor
[ 1, .5, .5, 1, 2, 2, 1, 1, 1, 1, 1, 2, .5, 1, .5, 1, 2, 1 ], # Fir
[ 1, 2, .5, 1, .5, 1, 1, 1, 2, 1, 1, 1, 2, 1, .5, 1, 1, 1 ], # Wat
[ 1, 1, 2, .5, .5, 1, 1, 1, 0, 2, 1, 1, 1, 1, .5, 1, 1, 1 ], # Ele
[ 1, .5, 2, 1, .5, 1, 1, .5, 2, .5, 1, .5, 2, 1, .5, 1, .5, 1 ], # Gra
[ 1, .5, .5, 1, 2, .5, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, .5, 1 ], # Ice
[ 2, 1, 1, 1, 1, 2, 1, .5, 1, .5, .5, .5, 2, 0, 1, 2, 2, 1 ], # Fig
[ 1, 1, 1, 1, 2, 1, 1, .5, .5, 1, 1, 1, .5, .5, 1, 1, 0, 1 ], # Poi
[ 1, 2, 1, 2, .5, 1, 1, 2, 1, 0, 1, .5, 2, 1, 1, 1, 2, 1 ], # Gro
[ 1, 1, 1, .5, 2, 1, 2, 1, 1, 1, 1, 2, .5, 1, 1, 1, .5, 1 ], # Fly
[ 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, .5, 1, 1, 1, 1, 0, .5, 1 ], # Psy
[ 1, .5, 1, 1, 2, 1, .5, .5, 1, .5, 2, 1, 1, .5, 1, 2, .5, 1 ], # Bug
[ 1, 2, 1, 1, 1, 2, .5, 1, .5, 2, 1, 2, 1, 1, 1, 1, .5, 1 ], # Roc
[ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, .5, .5, 1 ], # Gho
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, .5, 1 ], # Dra
[ 1, 1, 1, 1, 1, 1, .5, 1, 1, 1, 2, 1, 1, 2, 1, .5, .5, 1 ], # Dar
[ 1, .5, .5, .5, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, .5, 1 ], # Ste
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] # ???
]