1
0
mirror of https://gitlab.com/Deukhoofd/BattleSim.git synced 2025-10-27 18:00:03 +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

220
server/achievements.coffee Normal file
View File

@@ -0,0 +1,220 @@
# Handles giving achievements to players
# Due to time constraints, this was rushed and not very generalized. This module can be expanded
# on in the future.
{_} = require 'underscore'
async = require 'async'
config = require './config'
redis = require './redis'
ratings = require './ratings'
{Conditions} = require '../shared/conditions'
request = require 'request'
authHeaders = {AUTHUSER: process.env.AUTHUSER, AUTHTOKEN: process.env.AUTHTOKEN}
request = request.defaults(json: true, headers: authHeaders, timeout: 30 * 1000)
ACHIEVEMENT_KEY = 'achievements'
winCount = (count) ->
inner = (ratioData, streakData) ->
ratioData.win >= count
return inner
streak = (count) ->
inner = (ratioData, streakData) ->
streakData.streak >= count
return inner
ACHIEVEMENTS = [
{
id: 1
name: "Taste of Victory"
condition: "Won on PokeBattle for the first time"
medium_image: "1_TasteOfVictory_50x50.png"
conditionFn: winCount(1)
},
{
id: 2
name: "Centurian"
name: "Centurion"
condition: "Won 100 times"
medium_image: "100_Centurian_50x50.png"
conditionFn: winCount(100)
},
{
id: 3
name: "Jackpot!"
condition: "Lucky 777 victories"
medium_image: "777_Jackpot_50x50.png"
conditionFn: winCount(777)
},
{
id: 4
name: "Ladder Monster"
condition: "Won a beastly 2500 matches"
medium_image: "2500_LadderMonster_50x50_1.png"
conditionFn: winCount(2500)
},
{
id: 5
name: "Mile High Club"
condition: "Won 5280 ladder matches"
medium_image: "5280_MileHighClub_50x50.png"
conditionFn: winCount(5280)
},
{
id: 6
name: "Better than Phil"
condition: "More than 7086 victories"
medium_image: "7087_BetterThanPhil_50x50.png"
conditionFn: winCount(7087)
},
{
id: 7
name: "KAKAROT!"
condition: "What? More than 9000 victories?!"
medium_image: "9001_KAKAROT_50x50.png"
conditionFn: winCount(9001)
},
# ladder ranking achievements go here
{
id: 26
name: "On a Roll!"
condition: "Won 5 consecutive ladder matches"
medium_image: "5_OnARoll_50x50.png"
conditionFn: streak(5)
},
{
id: 27
name: "Perfect Ten!"
condition: "Won 10 consecutive ladder matches"
medium_image: "10_PerfectTen_50x50.png"
conditionFn: streak(10)
},
{
id: 28
name: "Incredible!"
condition: "Won 15 consecutive ladder matches"
medium_image: "15_Incredible_50x50.png"
conditionFn: streak(15)
},
{
id: 29
name: "Unreal!"
condition: "Won 20 consecutive ladder matches"
medium_image: "20_Unreal_50x50.png"
conditionFn: streak(20)
},
{
id: 30
name: "Impossible!"
condition: "Won 30 consecutive ladder matches"
medium_image: "30_Impossible_50x50.png"
conditionFn: streak(30)
},
{
id: 31
name: "The One"
condition: "You've won 50 in a row! Amazing!"
medium_image: "50_TheOne_50x50.png"
conditionFn: streak(50)
}
]
# Checks what achievements a player is eligible for
# The achievements are then awarded to the player
# Note: In the current implementation, playerId is actually a name
@checkAndAwardAchievements = (server, playerId, ratingKey, next = ->) ->
checkAchievements ratingKey, (err, achievements) ->
return next(err) if err
return next() if achievements.length == 0
filterEarned playerId, achievements, (err, achievements) ->
return next(err) if err
return next() if achievements.length == 0
notifyServer playerId, achievements, (err, byStatus) ->
return next(err) if err
# TODO: Handle errors, probably add them to some queue to retry
# Currently its fine, as playing another battle will do a re-evalation
# but in the future this may not be the case!
# Flag all of the achievements that have been earned
achievementsToFlag = _.union(byStatus.success, byStatus.duplicate)
flagEarned(playerId, achievementsToFlag) if achievementsToFlag.length > 0
# for each new achievement, notify the user if said user is online
if byStatus.success.length > 0
user = server.getUser(playerId)
if user
user.send('achievementsEarned', byStatus.success)
next()
# Registers a battle with the achievement system
# If the battle is not eligible for achievements, it is ignored
@registerBattle = (server, battle) ->
return if battle.format != 'xy1000'
return unless Conditions.RATED_BATTLE in battle.conditions
battle.once 'ratingsUpdated', =>
for player in battle.players
@checkAndAwardAchievements(server, player.id, player.ratingKey)
# Returns the achievements a player is eligible for, including already earned ones
checkAchievements = (ratingKey, next) ->
ratings.getRatio ratingKey, (err, ratio) ->
ratings.getStreak ratingKey, (err, streak) ->
achievements = ACHIEVEMENTS.filter((o) -> o.conditionFn(ratio, streak))
# Send everything except the conditionFn attribute
results = (_(a).omit('conditionFn') for a in achievements)
next(null, results)
# Removes the achievements that have already been earned from the list of achievements
filterEarned = (playerId, achievements, next) ->
ids = _(achievements).pluck('id')
redis.hmget "#{ACHIEVEMENT_KEY}:#{playerId}", ids, (err, flagged) ->
return next(err) if err
filtered = achievements.filter((a, i) -> !flagged[i])
next(null, filtered)
# Flags achievements that have been earned in redis so we don't bother the webservice with it
flagEarned = (playerId, achievements, next) ->
hash = {}
hash[a.id] = true for a in achievements
redis.hmset("#{ACHIEVEMENT_KEY}:#{playerId}", hash)
# Notifies the server about achievements to add to the user
# All achievements are separated by server result and passed to next
# Note: In the current implementation, playerId is actually a name
# If playerId is refactored to a numerical id, the server will need to be updated
notifyServer = (playerId, achievements, next) ->
achievementsByStatus =
success: []
duplicate: []
error: []
if config.IS_LOCAL
achievementsByStatus.success = achievements
return next(null, achievementsByStatus)
calls = for achievement in achievements
do (achievement) -> (callback) ->
request.post {
url: "https://www.pokebattle.com/api/v1/achievements/"
json: { user: playerId, achievement: achievement.name }
}, (err, res, data) ->
status = "success"
status = "error" if err
status = "duplicate" if data?.awarded
achievementsByStatus[status].push(achievement)
callback()
async.parallel calls, (err, results) ->
next(null, achievementsByStatus)

68
server/alts.coffee Normal file
View File

@@ -0,0 +1,68 @@
redis = require './redis'
ALT_LIMIT = 5
ALTS_KEY = "alts"
MAX_ALT_LENGTH = 15
# NOTE: Alt names should not be lowercased, but user ids should be.
# Runs basic username validation for alts
@isAltNameValid = (altName) ->
return false if !altName
altName = altName.trim()
return false if altName.length == 0 || altName.length > MAX_ALT_LENGTH
return false if altName.match(/\ \ /)
return false if not altName.match(/^[-_ a-zA-Z0-9]+$/)
return true
# Retrieves a list of alts registered to the user
# next is a callback with args (error, altsList)
@listUserAlts = (userId, next) ->
userId = String(userId).toLowerCase()
redis.lrange "#{userId}:alts", 0, -1, (err, alts) ->
return next(err) if err
next(null, alts)
# Creates an alt for the user with the given id
# next is a callback with args (error, success)
# TODO: Should this use a transaction?
@createAlt = (userId, altName, next) ->
userId = String(userId).toLowerCase()
altListKey = "#{userId}:alts"
redis.llen altListKey, (err, amount) ->
return next(err) if err
if amount >= ALT_LIMIT
return next(new Error("You have run out of alts. You cannot create another one."))
redis.hsetnx ALTS_KEY, altName, userId, (err, success) ->
return next(err) if err
return next(new Error("This alt name is already in use")) if not success
# If we got this far, adding the alt succeeded
# Add it to the list of user's alts, and then call next
redis.rpush altListKey, altName, ->
next(null, true)
# Checks if the user owns a particular alt name.
# Always returns true if altName is null (meaning no alt)
@isAltOwnedBy = (userId, altName, next) ->
return next(undefined, true) if not altName
userId = String(userId).toLowerCase()
redis.hget ALTS_KEY, altName, (err, assignedUserId) ->
return next(err) if err
next(null, assignedUserId == userId)
@getAltOwner = (altName, next) ->
redis.hget ALTS_KEY, altName, (err, assignedUserId) ->
return next(err) if err
return next(null, assignedUserId)
# Generates a unique id for a given id + altName combination.
# If altName is null, the original id is returned
@uniqueId = (id, altName) ->
return id if not altName
"#{id}:#{altName}"
# The inverse of uniqueId
@getIdOwner = (uniqueId) ->
uniqueId.split(':')[0]

209
server/auth.coffee Normal file
View File

@@ -0,0 +1,209 @@
{_} = require 'underscore'
crypto = require('crypto')
querystring = require('querystring');
Cookies = require( "cookies" )
request = require 'request'
authHeaders = {AUTHUSER: process.env.AUTHUSER, AUTHTOKEN: process.env.AUTHTOKEN}
request = request.defaults(json: true, headers: authHeaders, timeout: 30 * 1000)
config = require './config'
redis = require './redis'
USER_KEY = "users"
AUTH_KEY = "auth"
BANS_KEY = "bans"
MUTE_KEY = "mute"
return_sso_url="http://91.121.152.74:8000/"
secretstring = config.SECRET_KEY
loggedin = false
# This middleware checks if a user is authenticated through the site. If yes,
# then information about the user is stored in req.user. In addition, we store
# a token associated with that user into req.user.token.
#
# User information is also stored in redis.
exports.middleware = -> (req, res, next) ->
cookies = new Cookies( req, res )
return next() if req.path.match(/^\/css|^\/js|^\/fonts|^\/Sprites|^\/replays\/\b/)
return next() if req.path.match(/^\/leaderboard/) # add some proper site authentication later instead
authenticate req, (body) ->
if !loggedin
if req.query.sso
sso = req.query.sso
sig = req.query.sig
cryptedret = crypto.createHmac('SHA256', secretstring).update(sso).digest('hex')
if cryptedret != sig
return
else
base64decode = new Buffer(sso, 'base64').toString('ascii')
userobject = querystring.parse(base64decode)
nonce = cookies.get("nonce")
noncereturn = userobject.nonce
if noncereturn != nonce
return
else
setdatabasedata(req, cookies, userobject, next)
else
parameters = getparameters(req)
nonce = new Date().getTime() + '' + new Date().getMilliseconds();
cookies.set("nonce", nonce)
return_sso_url="http://91.121.152.74:8000" + req.url
payload = "nonce=" + nonce + "&return_sso_url=" + return_sso_url
base64payload = new Buffer(payload).toString('base64')
urlencoded = encodeURIComponent(base64payload)
crypted = crypto.createHmac('SHA256', secretstring).update(base64payload).digest('hex')
return res.redirect("http://91.121.152.74/session/sso_provider?sso=" + urlencoded + "&sig=" + crypted)
exports.matchToken = (req, id, token, next) ->
hmac = crypto.createHmac('sha256', config.SECRET_KEY)
rawcookies = req.headers.cookie
rawcookiesarr = rawcookies.split('; ')
noncearr = rawcookiesarr.filter (x) -> x.substring(0, 6) == "nonce="
if (noncearr[0])
noncecookie = noncearr[0].replace("nonce=", "")
if token != noncecookie
return next(new Error("Invalid session!"))
redis.shard 'hget', USER_KEY, id, (err, jsonString) ->
if err then return next(err)
json = JSON.parse(jsonString)
return next(new Error("Invalid session!")) if !json
exports.getAuth json.name, (err, authLevel) ->
if err then return next(err)
json.authority = authLevel
return next(null, json)
setdatabasedata = (req, cookies, object, next) ->
username = object.username
id = object.external_id
admin = object.admin
mod = object.moderator
req.user = {}
req.user.token = cookies.get("nonce")
req.user.id = id
req.user.name = username
if username == "Deukhoofd" && admin == "true"
exports.setAuth username, exports.levels.OWNER
else if admin == "true"
exports.setAuth username, exports.levels.ADMIN
else if mod == "true"
exports.setAuth username, exports.levels.MOD
else
exports.setAuth username, exports.levels.USER
toJSON = {
name : username,
token : cookies.get("nonce")
id : id
}
redis.shard('hset', USER_KEY, id, JSON.stringify(toJSON), next)
authenticate = (req, next) ->
return next()
getparameters = (req) ->
return("1")
generateUsername = (req) ->
name = req.param('user')
return name if name
{SpeciesData} = require './xy/data'
randomName = (name for name of SpeciesData)
randomName = randomName[Math.floor(Math.random() * randomName.length)]
randomName = randomName.split(/\s+/)[0]
randomName += "Fan" + Math.floor(Math.random() * 10000)
randomName
generateId = (req) ->
req.param('id') || Math.floor(1000000 * Math.random())
generateUser = (req) ->
{id: generateId(req), username: generateUsername(req)}
# Authorization
exports.levels =
USER : 1
DRIVER : 2
MOD : 3
MODERATOR : 3
ADMIN : 4
ADMINISTRATOR : 4
OWNER : 5
LEVEL_VALUES = (value for key, value of exports.levels)
exports.getAuth = (id, next) ->
id = String(id).toLowerCase()
redis.hget AUTH_KEY, id, (err, auth) ->
if err then return next(err)
auth = parseInt(auth, 10) || exports.levels.USER
next(null, auth)
exports.setAuth = (id, newAuthLevel, next) ->
id = String(id).toLowerCase()
if newAuthLevel not in LEVEL_VALUES
next(new Error("Incorrect auth level: #{newAuthLevel}"))
redis.hset(AUTH_KEY, id, newAuthLevel, next)
# Ban
# Length is in seconds.
exports.ban = (id, reason, length, next) ->
id = id.toLowerCase()
key = "#{BANS_KEY}:#{id}"
if length > 0
redis.setex(key, length, reason, next)
else
redis.set(key, reason, next)
exports.unban = (id, next) ->
id = String(id).toLowerCase()
redis.del("#{BANS_KEY}:#{id}", next)
exports.getBanReason = (id, next) ->
id = String(id).toLowerCase()
redis.get("#{BANS_KEY}:#{id}", next)
exports.getBanTTL = (id, next) ->
id = String(id).toLowerCase()
key = "#{BANS_KEY}:#{id}"
redis.exists key, (err, result) ->
if !result
# In older versions of Redis, TTL returns -1 if key doesn't exist.
return next(null, -2)
else
redis.ttl(key, next)
# Mute
# Length is in seconds.
exports.mute = (id, reason, length, next) ->
id = String(id).toLowerCase()
key = "#{MUTE_KEY}:#{id}"
if length > 0
redis.setex(key, length, reason, next)
else
redis.set(key, reason, next)
exports.unmute = (id, next) ->
id = String(id).toLowerCase()
key = "#{MUTE_KEY}:#{id}"
redis.del(key, next)
exports.getMuteTTL = (id, next) ->
id = String(id).toLowerCase()
key = "#{MUTE_KEY}:#{id}"
redis.exists key, (err, result) ->
if !result
# In older versions of Redis, TTL returns -1 if key doesn't exist.
return next(null, -2)
else
redis.ttl(key, next)

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 ] # ???
]

350
server/commands.coffee Normal file
View File

@@ -0,0 +1,350 @@
{_} = require('underscore')
async = require('async')
alts = require('./alts')
auth = require('./auth')
ratings = require('./ratings')
errors = require('../shared/errors')
exports.Commands = Commands = {}
exports.HelpDescriptions = HelpDescriptions = {}
desc = (description) ->
desc.lastDescription = description
parseArguments = (args) ->
args = Array::slice.call(args, 0)
hash = {}
if typeof args[args.length - 1] == 'function'
hash.callback = args.pop()
hash.args = args
hash
# Returns a 2-tuple, where the first element is the length (null for no length)
# and the second element is the reason.
parseLengthAndReason = (reasons) ->
length = null
return [null, ''] if reasons.length == 0
possibleLength = reasons[0].trim()
if /^[\dmshdyMw]+$/.test(possibleLength)
length = parseLength(possibleLength)
reasons = reasons[1...]
return [length, reasons.join(',').trim()]
parseLength = (length) ->
time = 0
for member in length.match(/\d+[mshdyMw]?/g)
first = parseInt(member, 10) # Truncates any letter after the number
last = member.substr(-1)
switch last
when 's'
time += first
when 'h'
time += first * 60 * 60
when 'd'
time += first * 60 * 60 * 24
break;
when 'w'
time += first * 60 * 60 * 24 * 7
when 'M'
time += first * 60 * 60 * 24 * 30
when 'y'
time += first * 60 * 60 * 24 * 30 * 12
else # minutes by default
time += first * 60
return time
prettyPrintTime = (seconds) ->
units = ["second", "minute", "hour", "day", "week", "month", "year"]
intervals = [60, 60, 24, 7, 4, 12, Infinity]
times = []
for interval, i in intervals
remainder = (seconds % interval)
seconds = Math.floor(seconds / interval)
unit = units[i]
unit += 's' if remainder != 1
times.push("#{remainder} #{unit}") if remainder > 0
break if seconds == 0
return times.reverse().join(", ")
makeCommand = (commandNames..., func) ->
authority = func.authority || auth.levels.USER
HelpDescriptions[authority] ?= {}
for commandName in commandNames
Commands[commandName] = func
# Generate description
description = ""
if commandNames.length > 1
aliases = commandNames[1...].map((n) -> "/#{n}").join(', ')
description += " <i>Also #{aliases}. </i>"
description += desc.lastDescription
HelpDescriptions[authority][commandNames[0]] = description
delete desc.lastDescription
makeModCommand = (commandNames..., func) ->
func.authority = auth.levels.MOD
makeCommand(commandNames..., func)
makeAdminCommand = (commandNames..., func) ->
func.authority = auth.levels.ADMIN
makeCommand(commandNames..., func)
makeOwnerCommand = (commandNames..., func) ->
func.authority = auth.levels.OWNER
makeCommand(commandNames..., func)
@executeCommand = (server, user, room, commandName, args...) ->
{args, callback} = parseArguments(args)
callback ||= ->
func = Commands[commandName]
if !func
message = "Invalid command: #{commandName}. Type /help to see a list."
user.error(errors.COMMAND_ERROR, room.name, message)
callback()
else if !func.authority || user.authority >= func.authority
Commands[commandName]?.call(server, user, room, callback, args...)
else
user.error(errors.COMMAND_ERROR, room.name, "You have insufficient authority.")
callback()
#######################
# Command definitions #
#######################
desc "Gets a single username's rating on this server. Usage: /rating username"
makeCommand "rating", "ranking", "rank", (user, room, next, username) ->
username ||= user.name
alts.getAltOwner username, (err, owner) ->
altKey = alts.uniqueId(owner, username)
commands = [
ratings.getRating.bind(ratings, username)
ratings.getRank.bind(ratings, username)
ratings.getRatio.bind(ratings, username)
]
commands.push(ratings.getRating.bind(ratings, altKey),
ratings.getRank.bind(ratings, altKey),
ratings.getRatio.bind(ratings, altKey)) if owner?
async.parallel commands, (err, results) ->
return user.error(errors.COMMAND_ERROR, room.name, err.message) if err
messages = []
messages.push collectRatingResults(username, results[...3], isOwner: username == user.name)
messages.push collectRatingResults("(Alt) #{username}", results[3...], isOwner: owner == user.name) if owner?
messages = _.compact(messages)
if messages.length == 0
user.announce(room.name, 'error', "Could not find rating for #{username}.")
else
user.announce(room.name, 'success', "#{messages.join('<br>')}")
next()
collectRatingResults = (username, results, options = {}) ->
isOwner = options.isOwner ? false
[rating, rank, ratios] = results
return if !rank
ratio = []
ratio.push("Rank: #{rank}")
ratio.push("Win: #{ratios.win}")
if isOwner
total = _.reduce(_.values(ratios), ((x, y) -> x + y), 0)
ratio.push("Lose: #{ratios.lose}")
ratio.push("Tie: #{ratios.draw}")
ratio.push("Total: #{total}")
"<b>#{username}'s rating:</b> #{rating} (#{ratio.join(' / ')})"
desc "Finds all the battles a username is playing in on this server.
Usage: /battles username"
makeCommand "battles", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /battles username")
return next()
battleIds = @getVisibleUserBattles(username)
links = battleIds.map (id) ->
"<span class='fake_link spectate' data-battle-id='#{id}'>#{id[...6]}</span>"
message = if battleIds.length == 0
"#{username} is not playing any battles."
else
"#{username}'s battles: #{links.join(" | ")}"
user.announce(room.name, 'success', message)
next()
desc "Default length is 10 minutes, up to a maximum of two days. To specify different lengths, use 1m2h3d4w (minute, hour, day, week). Usage: /mute username, length, reason"
makeModCommand "mute", (user, room, next, username, reason...) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /mute username, length, reason")
return next()
[length, reason] = parseLengthAndReason(reason)
# Enforce a length for non-admins.
if user.authority < auth.levels.ADMIN
length = 10 * 60 if !length? || length <= 0
length = Math.min(parseLength("2d"), length) # max of two days
@mute(username, reason, length)
message = "#{user.name} muted #{username} for #{prettyPrintTime(length)}"
message += " (#{reason})" if reason.length > 0
room.announce('warning', message)
next()
desc "Unmutes a username. Usage: /unmute username"
makeModCommand "unmute", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /unmute username")
return next()
auth.getMuteTTL username, (err, ttl) =>
if ttl == -2
user.error(errors.COMMAND_ERROR, room.name, "#{username} is already unmuted!")
return next()
else
@unmute(username)
message = "#{user.name} unmuted #{username}"
room.announce('warning', message)
next()
desc "Default length is one hour, up to a maximum of one day. To specify different lengths, use 1m2h3d (minute, hour, day). Usage: /ban username, length, reason"
makeModCommand "ban", (user, room, next, username, reason...) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /ban username, length, reason")
return next()
[length, reason] = parseLengthAndReason(reason)
# Enforce a length for non-admins
if user.authority < auth.levels.ADMIN
length = 60 * 60 if !length? || length <= 0
length = Math.min(parseLength("1d"), length) # max of one day
@ban(username, reason, length)
message = "#{user.name} banned #{username}"
message += " for #{prettyPrintTime(length)}" if length
message += " (#{reason})" if reason.length > 0
room.announce('warning', message)
next()
desc "Unbans a username. Usage: /unban username"
makeModCommand "unban", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /unban username")
return next()
auth.getBanTTL username, (err, ttl) =>
if ttl == -2
user.error(errors.COMMAND_ERROR, room.name, "#{username} is already unbanned!")
return next()
else
@unban username, =>
message = "#{user.name} unbanned #{username}"
room.announce('warning', message)
return next()
desc "Finds the current ips under use by a user"
makeModCommand "ip", (user, room, next, nameOrIp) ->
if !nameOrIp
user.error(errors.COMMAND_ERROR, "Usage: /ip username")
return next()
checkedUser = @users.get(nameOrIp)
if checkedUser
ips = checkedUser.sparks.map((spark) -> spark.address.ip)
ips = _.chain(ips).compact().unique().value()
user.announce(room.name, 'success', "#{nameOrIp}'s IP addresses: #{ips.join(', ')}")
else
users = []
for checkedUser in @users.getUsers()
for spark in checkedUser.sparks
if spark.address.ip == nameOrIp
users.push(checkedUser.name)
break
user.announce(room.name, 'success', "Users with IP #{nameOrIp}: #{users.join(', ')}")
next()
desc "Prevents new battles from starting. Usage: /lockdown [on|off]"
makeAdminCommand "lockdown", (user, room, next, option = "on") ->
if option not in [ "on", "off" ]
user.error(errors.COMMAND_ERROR, room.name, "Usage: /lockdown [on|off]")
return next()
if option == 'on' then @lockdown() else @unlockdown()
next()
desc "Voices a username permanently. Usage: /voice username"
makeAdminCommand "voice", "driver", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /voice username")
return next()
auth.setAuth username, auth.levels.DRIVER, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.DRIVER)
return next()
desc "Mods a username permanently. Usage: /mod username"
makeAdminCommand "mod", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /mod username")
return next()
auth.setAuth username, auth.levels.MOD, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.MOD)
return next()
desc "Admins a username permanently. Usage: /admin username"
makeOwnerCommand "admin", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /admin username")
return next()
auth.setAuth username, auth.levels.ADMIN, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.ADMIN)
return next()
desc "Deauthes a username permanently. Usage: /deauth username"
makeOwnerCommand "deauth", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /deauth username")
return next()
auth.setAuth username, auth.levels.USER, (err, result) =>
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
@setAuthority(username, auth.levels.USER)
return next()
desc "Changes the topic message. Usage: /topic message"
makeAdminCommand "topic", (user, room, next, topicPieces...) ->
room.setTopic(topicPieces.join(','))
next()
desc "Announces something to the entire server. Usage: /wall message"
makeModCommand "wall", "announce", (user, room, next, pieces...) ->
message = pieces.join(',')
return next() if !message
@announce("<strong>#{user.name}:</strong> #{message}")
next()
desc "Finds all alts associated with a username, or the main username of an alt"
makeModCommand "whois", (user, room, next, username) ->
if !username
user.error(errors.COMMAND_ERROR, room.name, "Usage: /whois username")
return next()
messages = []
alts.getAltOwner username, (err, ownerName) ->
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
ownerName ?= username
messages.push("<b>Main account:</b> #{ownerName}")
alts.listUserAlts username, (err, alts) ->
if err
user.error(errors.COMMAND_ERROR, room.name, err.message)
return next()
messages.push("<b>Alts:</b> #{alts.join(', ')}") if alts.length > 0
user.announce(room.name, 'success', messages.join(' | '))
return next()
desc "Evaluates a script in the context of the server."
makeOwnerCommand "eval", (user, room, next, pieces...) ->
source = pieces.join(',')
return next() if !source
try
result = (new Function("with(this) { return #{source} }")).call(this)
user.announce(room.name, 'success', "> #{result}")
catch e
user.error(errors.COMMAND_ERROR, room.name, "EVAL ERROR: #{e.message}")
next()

360
server/conditions.coffee Normal file
View File

@@ -0,0 +1,360 @@
{_} = require('underscore')
{Conditions} = require('../shared/conditions')
{Protocol} = require('../shared/protocol')
pbv = require('../shared/pokebattle_values')
gen = require('./generations')
alts = require('./alts')
ConditionHash = {}
createCondition = (condition, effects = {}) ->
ConditionHash[condition] = effects
# Attaches each condition to the Battle facade.
@attach = (battleFacade) ->
battle = battleFacade.battle
for condition in battle.conditions
if condition not of ConditionHash
throw new Error("Undefined condition: #{condition}")
hash = ConditionHash[condition] || {}
# Attach each condition's event listeners
for eventName, callback of hash.attach
battle.on(eventName, callback)
# Extend battle with each function
# TODO: Attach to prototype, and only once.
for funcName, funcRef of hash.extend
battle[funcName] = funcRef
for funcName, funcRef of hash.extendFacade
battleFacade[funcName] = funcRef
# validates an entire team
@validateTeam = (conditions, team, genData) ->
errors = []
for condition in conditions
if condition not of ConditionHash
throw new Error("Undefined condition: #{condition}")
validator = ConditionHash[condition].validateTeam
continue if !validator
errors.push(validator(team, genData)...)
return errors
# validates a single pokemon
@validatePokemon = (conditions, pokemon, genData, prefix) ->
errors = []
for condition in conditions
if condition not of ConditionHash
throw new Error("Undefined condition: #{condition}")
validator = ConditionHash[condition].validatePokemon
continue if !validator
errors.push(validator(pokemon, genData, prefix)...)
return errors
createPBVCondition = (totalPBV) ->
createCondition Conditions["PBV_#{totalPBV}"],
validateTeam: (team, genData) ->
errors = []
if pbv.determinePBV(genData, team) > totalPBV
errors.push "Total team PBV cannot surpass #{totalPBV}."
if team.length != 6
errors.push "Your team must have 6 pokemon."
return errors
validatePokemon: (pokemon, genData, prefix) ->
errors = []
MAX_INDIVIDUAL_PBV = Math.floor(totalPBV / 3)
individualPBV = pbv.determinePBV(genData, pokemon)
if individualPBV > MAX_INDIVIDUAL_PBV
errors.push "#{prefix}: This Pokemon's PBV is #{individualPBV}. Individual
PBVs cannot go over 1/3 the total (over #{MAX_INDIVIDUAL_PBV} PBV)."
return errors
createPBVCondition(1000)
createPBVCondition(500)
createCondition Conditions.SLEEP_CLAUSE,
attach:
initialize: ->
for team in @getTeams()
for p in team.pokemon
p.attach(@getAttachment("SleepClause"))
createCondition Conditions.SPECIES_CLAUSE,
validateTeam: (team, genData) ->
errors = []
species = team.map((p) -> p.species)
species.sort()
for i in [1...species.length]
speciesName = species[i - 1]
if speciesName == species[i]
errors.push("Cannot have the same species: #{speciesName}")
while speciesName == species[i]
i++
return errors
createCondition Conditions.EVASION_CLAUSE,
validatePokemon: (pokemon, genData, prefix) ->
{moves, ability} = pokemon
errors = []
# Check evasion abilities
if ability in [ "Moody" ]
errors.push("#{prefix}: #{ability} is banned under Evasion Clause.")
# Check evasion moves
for moveName in moves || []
move = genData.MoveData[moveName]
continue if !move
if move.primaryBoostStats? && move.primaryBoostStats.evasion > 0 &&
move.primaryBoostTarget == 'self'
errors.push("#{prefix}: #{moveName} is banned under Evasion Clause.")
return errors
createCondition Conditions.OHKO_CLAUSE,
validatePokemon: (pokemon, genData, prefix) ->
{moves} = pokemon
errors = []
# Check OHKO moves
for moveName in moves || []
move = genData.MoveData[moveName]
continue if !move
if "ohko" in move.flags
errors.push("#{prefix}: #{moveName} is banned under One-Hit KO Clause.")
return errors
createCondition Conditions.PRANKSTER_SWAGGER_CLAUSE,
validatePokemon: (pokemon, genData, prefix) ->
errors = []
if "Swagger" in pokemon.moves && "Prankster" == pokemon.ability
errors.push("#{prefix}: A Pokemon can't have both Prankster and Swagger.")
return errors
createCondition Conditions.UNRELEASED_BAN,
validatePokemon: (pokemon, genData, prefix) ->
# Check for unreleased items
errors = []
if pokemon.item && genData.ItemData[pokemon.item]?.unreleased
errors.push("#{prefix}: The item '#{pokemon.item}' is unreleased.")
# Check for unreleased abilities
forme = genData.FormeData[pokemon.species][pokemon.forme || "default"]
if forme.unreleasedHidden && pokemon.ability == forme.hiddenAbility &&
forme.hiddenAbility not in forme.abilities
errors.push("#{prefix}: The ability #{pokemon.ability} is unreleased.")
# Check for unreleased Pokemon
if forme.unreleased
errors.push("#{prefix}: The Pokemon #{pokemon.species} is unreleased.")
return errors
createCondition Conditions.RATED_BATTLE,
attach:
end: (winnerId) ->
return if !winnerId
index = @getPlayerIndex(winnerId)
loserId = @playerIds[1 - index]
ratings = require './ratings'
winner = @getPlayer(winnerId)
loser = @getPlayer(loserId)
winnerId = winner.ratingKey
loserId = loser.ratingKey
ratings.getRatings [ winnerId, loserId ], (err, oldRatings) =>
ratings.updatePlayers winnerId, loserId, ratings.results.WIN, (err, result) =>
return @message "An error occurred updating rankings :(" if err
oldRating = Math.floor(oldRatings[0])
newRating = Math.floor(result[0])
@cannedText('RATING_UPDATE', index, oldRating, newRating)
oldRating = Math.floor(oldRatings[1])
newRating = Math.floor(result[1])
@cannedText('RATING_UPDATE', 1 - index, oldRating, newRating)
@emit('ratingsUpdated')
@sendUpdates()
createCondition Conditions.TIMED_BATTLE,
attach:
initialize: ->
@playerTimes = {}
@lastActionTimes = {}
now = Date.now()
# Set up initial values
for id in @playerIds
@playerTimes[id] = now + @TEAM_PREVIEW_TIMER
# Set up timers and event listeners
check = () =>
@startBattle()
@sendUpdates()
@teamPreviewTimerId = setTimeout(check, @TEAM_PREVIEW_TIMER)
@once('end', => clearTimeout(@teamPreviewTimerId))
@once('start', => clearTimeout(@teamPreviewTimerId))
start: ->
nowTime = Date.now()
for id in @playerIds
@playerTimes[id] = nowTime + @DEFAULT_TIMER
# Remove first turn since we'll be increasing it again.
@playerTimes[id] -= @TIMER_PER_TURN_INCREASE
@startTimer()
requestActions: (playerId) ->
# If a player has selected a move, then there's an amount of time spent
# between move selection and requesting another action that was "lost".
# We grant this back here.
if @lastActionTimes[playerId]
now = Date.now()
leftoverTime = now - @lastActionTimes[playerId]
delete @lastActionTimes[playerId]
@addTime(playerId, leftoverTime)
# In either case, we tell people that this player's timer resumes.
@send('resumeTimer', @id, @getPlayerIndex(playerId))
addAction: (playerId, action) ->
# Record the last action for use
@lastActionTimes[playerId] = Date.now()
@recalculateTimers()
undoCompletedRequest: (playerId) ->
delete @lastActionTimes[playerId]
@recalculateTimers()
# Show players updated times
beginTurn: ->
remainingTimes = []
for id in @playerIds
@addTime(id, @TIMER_PER_TURN_INCREASE)
remainingTimes.push(@timeRemainingFor(id))
@send('updateTimers', @id, remainingTimes)
continueTurn: ->
for id in @playerIds
@send('pauseTimer', @id, @getPlayerIndex(id))
spectateBattle: (user) ->
playerId = user.name
remainingTimes = (@timeRemainingFor(id) for id in @playerIds)
user.send('updateTimers', @id, remainingTimes)
# Pause timer for players who have already chosen a move.
if @hasCompletedRequests(playerId)
index = @getPlayerIndex(playerId)
timeSinceLastAction = Date.now() - @lastActionTimes[playerId]
user.send('pauseTimer', @id, index, timeSinceLastAction)
extend:
DEFAULT_TIMER: 3 * 60 * 1000 # three minutes
TIMER_PER_TURN_INCREASE: 15 * 1000 # fifteen seconds
TIMER_CAP: 3 * 60 * 1000 # three minutes
TEAM_PREVIEW_TIMER: 1.5 * 60 * 1000 # 1 minute and 30 seconds
startTimer: (msecs) ->
msecs ?= @DEFAULT_TIMER
@timerId = setTimeout(@declareWinner.bind(this), msecs)
@once('end', => clearTimeout(@timerId))
addTime: (id, msecs) ->
@playerTimes[id] += msecs
remainingTime = @timeRemainingFor(id)
if remainingTime > @TIMER_CAP
diff = remainingTime - @TIMER_CAP
@playerTimes[id] -= diff
@recalculateTimers()
@playerTimes[id]
recalculateTimers: ->
playerTimes = for id in @playerIds
if @lastActionTimes[id] then Infinity else @timeRemainingFor(id)
leastTime = Math.min(playerTimes...)
clearTimeout(@timerId)
if 0 < leastTime < Infinity
@timerId = setTimeout(@declareWinner.bind(this), leastTime)
else if leastTime <= 0
@declareWinner()
timeRemainingFor: (playerId) ->
endTime = @playerTimes[playerId]
nowTime = @lastActionTimes[playerId] || Date.now()
return endTime - nowTime
playersWithLeastTime: ->
losingIds = []
leastTimeRemaining = Infinity
for id in @playerIds
timeRemaining = @timeRemainingFor(id)
if timeRemaining < leastTimeRemaining
losingIds = [ id ]
leastTimeRemaining = timeRemaining
else if timeRemaining == leastTimeRemaining
losingIds.push(id)
return losingIds
declareWinner: ->
loserIds = @playersWithLeastTime()
loserId = @rng.choice(loserIds, "timer")
index = @getPlayerIndex(loserId)
winnerIndex = 1 - index
@timerWin(winnerIndex)
timerWin: (winnerIndex) ->
@tell(Protocol.TIMER_WIN, winnerIndex)
@emit('end', @playerIds[winnerIndex])
@sendUpdates()
createCondition Conditions.TEAM_PREVIEW,
attach:
initialize: ->
@arranging = true
@arranged = {}
beforeStart: ->
@tell(Protocol.TEAM_PREVIEW)
start: ->
@arranging = false
arrangements = @getArrangements()
@tell(Protocol.REARRANGE_TEAMS, arrangements...)
for playerId, i in @playerIds
team = @getTeam(playerId)
team.arrange(arrangements[i])
extendFacade:
arrangeTeam: (playerId, arrangement) ->
return false if @battle.hasStarted()
return false if arrangement not instanceof Array
team = @battle.getTeam(playerId)
return false if !team
return false if arrangement.length != team.size()
for index, i in arrangement
return false if isNaN(index)
return false if !team.pokemon[index]
return false if arrangement.indexOf(index, i + 1) != -1
@battle.arrangeTeam(playerId, arrangement)
@battle.sendUpdates()
return true
extend:
arrangeTeam: (playerId, arrangement) ->
return false unless @arranging
@arranged[playerId] = arrangement
if _.difference(@playerIds, Object.keys(@arranged)).length == 0
@startBattle()
getArrangements: ->
for playerId in @playerIds
@arranged[playerId] || [0...@getTeam(playerId).size()]
createCondition Conditions.VISIBLE_TEAM,
attach:
initialize: ->
@tell(Protocol.VISIBLE_TEAM)

68
server/database.coffee Normal file
View File

@@ -0,0 +1,68 @@
{_} = require('underscore')
{Formats} = require('../shared/conditions')
config = require('../knexfile')[process.env.NODE_ENV || 'development']
knex = require('knex')(config)
bookshelf = require('bookshelf')(knex)
# Postgres 9.2+ support the JSON datatype. Other versions/DBs do not.
# So if the JSON data type is supported, then loading will load as JSON.
jsonify = (contents) ->
if _.isObject(contents)
contents
else if !contents || !contents.length
{}
else
JSON.parse(contents)
Team = bookshelf.Model.extend
tableName: 'teams'
hasTimestamps: ['created_at', 'updated_at']
toJSON: -> {
id: @id
name: @get('name')
generation: @get('generation')
pokemon: jsonify(@get('contents'))
}
Teams = bookshelf.Collection.extend
model: Team
Battle = bookshelf.Model.extend
tableName: 'battles'
hasTimestamps: ['created_at', 'updated_at']
# TODO: Find (and jsonify) asset versions
getName: ->
@get('name') || @getPlayerNames().join(' vs. ') || 'Untitled'
getFormat: ->
Formats[@get('format')].humanName
getPlayerNames: ->
# players is denormalized. It's an array with a comma delimiter.
@get('players')?.split(',') || []
version: (js) ->
jsonify(@get('versions'))[js]
toJSON: -> {
id: @get('battle_id')
name: @getName()
format: @get('format')
numActive: @get('num_active')
players: @getPlayerNames()
contents: jsonify(@get('contents'))
created_at: @get('created_at')
}
SavedBattle = bookshelf.Model.extend
tableName: 'saved_battles'
hasTimestamps: ['created_at', 'updated_at']
SavedBattles = bookshelf.Collection.extend
model: SavedBattle
module.exports = {Team, Teams, Battle, SavedBattle, SavedBattles, knex}

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 @@
{@FormeData, @SpeciesData} = require('./pokemon')

View File

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

13
server/elo.coffee Normal file
View File

@@ -0,0 +1,13 @@
elo = require('elo-rank')()
createPlayer = ->
{rating: 1000}
calculate = (player, matches, options = {}) ->
playerRating = player.rating
for {opponent, score} in matches
expected = elo.getExpected(playerRating, opponent.rating)
playerRating = elo.updateRating(expected, score, playerRating)
{rating: playerRating}
module.exports = {createPlayer, calculate}

98
server/generations.coffee Normal file
View File

@@ -0,0 +1,98 @@
{_} = require('underscore')
learnsets = require '../shared/learnsets'
@ALL_GENERATIONS = [ 'rb', 'gs', 'rs', 'dp', 'bw', 'xy', 'in' ]
@SUPPORTED_GENERATIONS = [ 'xy', 'in' ]
@DEFAULT_GENERATION = 'in'
@INT_TO_GENERATION = {}
for gen, i in @ALL_GENERATIONS
@INT_TO_GENERATION[i + 1] = gen
@GENERATION_TO_INT = {}
for gen, i in @ALL_GENERATIONS
@GENERATION_TO_INT[gen] = (i + 1)
@GenerationJSON = {}
# TODO: Get rid of this once we have all data in.
maybeRequire = (path) ->
try
require(path)
trymerge = (one, two) ->
try
returnobj = one
for key, value of two
returnobj[key] = value
returnobj
for gen, i in @ALL_GENERATIONS
SpeciesData = maybeRequire("./#{gen}/data/data_species.json") || {}
PokeData = maybeRequire("./#{gen}/data/data_formes.json") || {}
DeltaData = maybeRequire("./#{gen}/data/delta_formes.json") || {}
MoveData = maybeRequire("./#{gen}/data/data_moves.json") || {}
ItemData = maybeRequire("./#{gen}/data/data_items.json") || {}
AbilityData = maybeRequire("./#{gen}/data/data_abilities.json") || {}
if Object.keys(DeltaData).length != 0
FormeData = trymerge(DeltaData, PokeData)
else
FormeData = PokeData
PokemonList = (name for name of FormeData)
ItemList = (name for name of ItemData)
MoveList = []
TypeList = []
AbilityList = []
MoveMap = {}
AbilityMap = {}
TypeMap = {}
for pokemonName, pokemonData of FormeData
for formeName, formeData of pokemonData
# Add types
for type in formeData.types
TypeList.push(type)
TypeMap[type] ?= []
TypeMap[type].push([pokemonName, formeName])
# Add abilities
abilities = []
abilities.push(formeData.abilities...)
abilities.push(formeData.hiddenAbility) if formeData.hiddenAbility
for ability in abilities
AbilityMap[ability] ?= []
AbilityMap[ability].push([pokemonName, formeName])
AbilityList = Object.keys(AbilityData)
MoveList = Object.keys(MoveData)
TypeList = _.chain(TypeList).uniq().sort().value()
@GenerationJSON[gen.toUpperCase()] =
SpeciesData : SpeciesData
FormeData : FormeData
MoveData : MoveData
ItemData : ItemData
AbilityData : AbilityData
PokemonList : PokemonList
ItemList : ItemList
MoveList : MoveList
AbilityList : AbilityList
TypeList : TypeList
MoveMap : MoveMap
AbilityMap : AbilityMap
TypeMap : TypeMap
# Now add moves for every generation
for gen in @ALL_GENERATIONS
thisGen = @GenerationJSON[gen.toUpperCase()]
for pokemonName, pokemonData of thisGen.FormeData
for formeName, formeData of pokemonData
pokemon = {species: pokemonName, forme: formeName}
# Add moves
moves = learnsets.learnableMoves(@GenerationJSON, pokemon, @GENERATION_TO_INT[gen])
for moveName in moves
thisGen.MoveMap[moveName] ?= []
thisGen.MoveMap[moveName].push([pokemonName, formeName])

157
server/glicko2.coffee Normal file
View File

@@ -0,0 +1,157 @@
# Formula from Glickman's paper:
# http://glicko.net/glicko/glicko2.pdf
config =
TAU: 0.5
DEFAULT_RATING: 1500
DEFAULT_DEVIATION: 350
DEFAULT_VOLATILITY: 0.06
CONVERGENCE_TOLERANCE: 0.000001
GLICKO_CONSTANT = 173.1718
PI_SQUARED = Math.PI * Math.PI
ratingFromGlicko = (rating) ->
(rating - 1500) / GLICKO_CONSTANT
deviationFromGlicko = (deviation) ->
deviation / GLICKO_CONSTANT
g = (deviation) ->
1 / Math.sqrt(1 + 3 * deviation * deviation / PI_SQUARED)
E = (mu, opponentMu, opponentPhi) ->
1 / (1 + Math.exp(-g(opponentPhi) * (mu - opponentMu)))
_extractPlayer = (player) ->
{rating, deviation, volatility} = player
createPlayer = ->
rating: config.DEFAULT_RATING
deviation: config.DEFAULT_DEVIATION
volatility: config.DEFAULT_VOLATILITY
calculate = (player, matches, options = {}) ->
# Step 1: Initialize tau. Players with no rating are assumed to have been
# handled already using the createPlayer function.
tau = options.systemConstant || config.TAU
extractPlayer = (options.extractPlayer || _extractPlayer)
glickoPlayer = extractPlayer(player)
playerRating = ratingFromGlicko(glickoPlayer.rating)
playerDeviation = deviationFromGlicko(glickoPlayer.deviation)
playerVolatility = glickoPlayer.volatility
# Step 2: Convert from Glicko to Glicko2
gPlayers = []
ePlayers = []
scores = []
for match in matches
{opponent, score} = match
glickoOpponent = extractPlayer(opponent)
opponentRating = ratingFromGlicko(opponent.rating)
opponentDeviation = deviationFromGlicko(opponent.deviation)
gPlayers.push g(opponentDeviation)
ePlayers.push E(playerRating, opponentRating, opponentDeviation)
scores.push score
# Step 3: Compute estimated variance (v)
estimatedVariance = calculateEstimatedVariance(gPlayers, ePlayers)
# Step 4: Compute estimated improvement in rating (delta)
improvementSum = calculateImprovementSum(gPlayers, ePlayers, scores)
# Step 5: Determine new volatility (delta prime)
newVolatility = calculateNewVolatility(improvementSum, playerDeviation,
estimatedVariance, playerVolatility, tau)
# Step 6: Update rating deviation to new pre-rating period value
periodValue = playerDeviation * playerDeviation + newVolatility * newVolatility
# Step 7: Determine new rating and rating deviation
newDeviation = 1 / Math.sqrt(1 / periodValue + 1 / estimatedVariance)
newRating = playerRating + newDeviation * newDeviation * improvementSum
# Step 8: Convert back to Glicko scale
rating = GLICKO_CONSTANT * newRating + config.DEFAULT_RATING
deviation = GLICKO_CONSTANT * newDeviation
volatility = newVolatility
# Return results
return {rating, deviation, volatility}
calculateImprovementSum = (gPlayers, ePlayers, scores) ->
improvementSum = 0.0
for gPlayer, i in gPlayers
improvementSum += gPlayer * (scores[i] - ePlayers[i])
improvementSum
calculateEstimatedVariance = (gPlayers, ePlayers) ->
estimatedVariance = 0.0
for gPlayer, i in gPlayers
gSquared = gPlayer * gPlayer
ePlayer = ePlayers[i]
estimatedVariance += gSquared * ePlayer * (1.0 - ePlayer)
estimatedVariance = 1.0 / estimatedVariance
estimatedVariance
calculateNewVolatility = (improvementSum, deviation, variance, volatility, tau) ->
# Step 5.1
A = a = Math.log(volatility * volatility)
improvement = variance * improvementSum
deviationSquared = deviation * deviation
varianceSquared = variance * variance
improvementSquared = improvement * improvement
tauSquared = tau * tau
convergenceTolerance = config.CONVERGENCE_TOLERANCE
fIter = (x) -> f(deviationSquared, improvementSquared, tauSquared, variance, a, x)
# Step 5.2
if improvementSquared > varianceSquared + variance
B = Math.log(improvementSquared - varianceSquared - variance)
else
absTau = Math.abs(tau)
k = 1
k += 1 while fIter(a - k * absTau) < 0
B = a - k * absTau
# Step 5.3
fA = fIter(A)
fB = fIter(B)
while Math.abs(B - A) > convergenceTolerance
# Step 5.4a
C = A + (A - B) * fA / (fB - fA)
fC = fIter(C)
# Step 5.4b
if fC * fB < 0 then [A, fA] = [B, fB] else fA /= 2
# Step 5.4c
[B, fB] = [C, fC]
# Step 5.5
return Math.exp(A / 2)
f = (deviationSquared, improvementSquared, tauSquared, variance, a, x) ->
expX = Math.exp(x)
denominator = deviationSquared + variance + expX
((expX * (improvementSquared - denominator) /
(2 * denominator * denominator)) - ((x - a) / tauSquared))
getRatingEstimate = (rating, deviation) ->
return 0 if deviation > 100
rds = deviation * deviation
sqr = sqrt(15.905694331435 * (rds + 221781.21786254))
inner = (1500.0 - rating) * Math.PI / sqr
return Math.floor(10000.0 / (1.0 + Math.pow(10.0, inner)) + 0.5) / 100.0
module.exports = {
calculate
calculateImprovementSum
calculateNewVolatility
calculateEstimatedVariance
createPlayer
ratingFromGlicko
deviationFromGlicko
getRatingEstimate
config
g
E
}

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 @@
{@FormeData, @SpeciesData} = require('./pokemon')

View File

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

144
server/in/attachment.coffee Normal file
View File

@@ -0,0 +1,144 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/attachment.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
delete @Status.Sleep::switchOut
# In XY, electric pokemon are immune to paralysis
@Status.Paralyze.worksOn = (battle, pokemon) ->
!pokemon.hasType("Electric")
# In XY, Scald and Steam Eruption thaw the target
@Status.Freeze::afterBeingHit = (move, user, target) ->
if !move.isNonDamaging() && move.type == 'Fire' ||
move.name in ["Scald", "Steam Eruption"]
@pokemon.cureStatus()
# In XY, Protect-like moves have a chance of success corresponding to the
# power of 3, instead of the power of 2 in previous generations.
@Attachment.ProtectCounter::successMultiplier = 3
# In XY, partial-trapping moves deal more damage at the end of each turn
@Attachment.Trap::getDamagePerTurn = ->
if @user.hasItem("Binding Band")
6
else
8
class @Attachment.KingsShield extends @VolatileAttachment
name: "KingsShieldAttachment"
initialize: ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
shouldBlockExecution: (move, user) ->
if move.hasFlag("protect") && !move.isNonDamaging()
if move.hasFlag("contact") then user.boost(attack: -2, @pokemon)
return true
endTurn: ->
@pokemon.unattach(@constructor)
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.SpikyShield extends @VolatileAttachment
name: "SpikyShieldAttachment"
initialize: ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
shouldBlockExecution: (move, user) ->
if move.hasFlag("protect")
if move.hasFlag("contact") then user.damage(user.stat('hp') >> 3)
return true
endTurn: ->
@pokemon.unattach(@constructor)
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.StickyWeb extends @TeamAttachment
name: "StickyWebAttachment"
initialize: ->
id = @team.playerId
@battle.cannedText('STICKY_WEB_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
if !pokemon.isImmune("Ground")
@battle.cannedText('STICKY_WEB_CONTINUE', pokemon)
# The source is not actually an opposing Pokemon, but in order for Defiant
# to work properly, the source should not be the pokemon itself.
pokemon.boost(speed: -1, @battle.getAllOpponents(pokemon)[0])
unattach: ->
id = @team.playerId
@battle.cannedText('STICKY_WEB_END', @battle.getPlayerIndex(id))
class @Attachment.Livewire extends @TeamAttachment
name: "LiveWireAttachment"
maxLayers: 1
initialize: ->
id = @team.playerId
@battle.cannedText('LIVEWIRE_START', @battle.getPlayerIndex(id))
#On switch in
switchIn: (pokemon) ->
#if pokemon is part electric and not immune to ground, remove livewire. Also if has ground and is not immune to ground
if (pokemon.hasType("Electric") && !pokemon.isImmune("Ground")) || (pokemon.hasType("Ground") && !pokemon.isImmune("Ground"))
@team.unattach(@constructor)
#if pokemon is immune to electric, nothing happens
if pokemon.isImmune("Electric")
return
#if pokemon is immune to ground, show miss message and nothing further happens
if pokemon.isImmune("Ground")
@battle.cannedText('LIVEWIRE_MISS', pokemon)
return
rng = @battle?.rng.randInt(1, 100, "will livewire paralyze?")
livewirechance = 70
if rng < livewirechance
pokemon.cureStatus()
pokemon.attach(Status.Paralyze)
@battle.cannedText('LIVEWIRE_HURT', pokemon)
else
@battle.cannedText('LIVEWIRE_MISS', pokemon)
unattach: ->
id = @team.playerId
@battle.cannedText('LIVEWIRE_END', @battle.getPlayerIndex(id))
class @Attachment.FireRock extends @TeamAttachment
name: "FireRockAttachment"
initialize: ->
id = @team.playerId
@battle.cannedText('FIRE_ROCK_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
multiplier = util.typeEffectiveness("Fire", pokemon.types)
hp = pokemon.stat('hp')
damage = ((hp * multiplier) >> 3)
if pokemon.damage(damage)
@battle.cannedText('FIRE_ROCK_HURT', pokemon)
unattach: ->
id = @team.playerId
@battle.cannedText('FIRE_ROCK_END', @battle.getPlayerIndex(id))
class @Attachment.Pendulum extends @VolatileAttachment
name: "PendulumAttachment"
maxLayers: 5
initialize: (attributes) ->
{@move} = attributes
beforeMove: (move) ->
@pokemon.unattach(@constructor) if move != @move

52
server/in/battle.coffee Normal file
View File

@@ -0,0 +1,52 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/battle.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
{Ability} = require('./data/abilities')
@Battle::generation = 'in'
@Battle::actionMap['mega'] =
priority: -> 5
action: (action) ->
@performMegaEvolution(action.pokemon)
@Battle::performMegaEvolution = (pokemon) ->
[ species, forme ] = pokemon.item.mega
pokemon.changeForme(forme)
ability = @FormeData[species][forme]["abilities"][0]
ability = Ability[ability.replace(/\s+/g, '')]
pokemon.copyAbility(ability, reveal: false)
# We set the original ability to this ability so that the ability
# is not reset upon switch out.
pokemon.originalAbility = ability
pokemon.originalForme = forme
# Generate and display mega-evolution message
if species == 'Zoroark'
alivemons = pokemon.team.getAlivePokemon()
lastalivemon = alivemons[alivemons.length-1]
possibleform = @FormeData[lastalivemon.species]
if 'mega' of possibleform
pieces = forme.split('-').map((s) -> s[0].toUpperCase() + s.substr(1))
pieces.splice(1, 0, lastalivemon.species)
megaFormeName = pieces.join(" ")
@message "#{pokemon.name} Mega Evolved into #{megaFormeName}!"
else
return
else
pieces = forme.split('-').map((s) -> s[0].toUpperCase() + s.substr(1))
pieces.splice(1, 0, species)
megaFormeName = pieces.join(" ")
@message "#{pokemon.name} Mega Evolved into #{megaFormeName}!"
# Retrofit `recordMove` to also record mega evolutions.
oldRecordMove = @Battle::recordMove
@Battle::recordMove = (playerId, move, forSlot = 0, options = {}) ->
pokemon = @getTeam(playerId).at(forSlot)
if options.megaEvolve && !@getAction(pokemon) && pokemon.canMegaEvolve()
if @pokemonActions.filter((o) -> o.type == 'mega' && o.pokemon.team == pokemon.team).length == 0
@addAction(type: 'mega', pokemon: pokemon)
oldRecordMove.apply(this, arguments)

View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/battle_controller.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

View File

@@ -0,0 +1,392 @@
{Weather} = require '../../../shared/weather'
# Retcon weather abilities to only last 5 turns.
makeWeatherAbility = (name, weather) ->
makeAbility name, ->
this::switchIn = ->
return if @battle.hasWeather(weather)
moveName = switch weather
when Weather.SUN then "Sunny Day"
when Weather.RAIN then "Rain Dance"
when Weather.SAND then "Sandstorm"
when Weather.HAIL then "Hail"
when Weather.MOON then "New Moon"
else throw new Error("#{weather} ability not supported.")
@pokemon.activateAbility()
move = @battle.getMove(moveName)
move.changeWeather(@battle, @pokemon)
# Import old abilities
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/abilities.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
# Retcon old abilities
# Effect Spore now does not affect Grass-type Pokemon,
# Pokemon with Overcoat, or Pokemon holding Safety Goggles
oldEffectSpore = Ability.EffectSpore::afterBeingHit
Ability.EffectSpore::afterBeingHit = (move, user, target, damage) ->
unless user.hasType("Grass") || user.hasAbility("Overcoat") || user.hasItem("Safety Goggles")
oldEffectSpore.apply(this, arguments)
# Oblivious now also prevents and cures Taunt
makeAttachmentImmuneAbility("Oblivious", [Attachment.Attract, Attachment.Taunt])
# Overcoat now also prevents powder moves from working.
Ability.Overcoat::shouldBlockExecution = (move, user) ->
if move.hasFlag("powder")
@pokemon.activateAbility()
return true
# New ability interfaces
makeNormalTypeChangeAbility = (name, newType) ->
makeAbility name, ->
this::editMoveType = (type, target) ->
return newType if type == 'Normal' && @pokemon != target
return type
this::modifyBasePower = (move, target) ->
return 0x14CD if move.type == 'Normal'
return 0x1000
makeNormalTypeChangeAbility("Aerilate", "Flying")
makeNormalTypeChangeAbility("Pixilate", "Fairy")
makeNormalTypeChangeAbility("Refrigerate", "Ice")
makeAuraAbility = (name, type) ->
makeAbility name, ->
this::modifyBasePower = (move, target) ->
return 0x1000 if move.getType(@battle, @pokemon, target) != type
for pokemon in @battle.getActiveAlivePokemon()
return 0xC00 if pokemon.hasAbility("Aura Break")
return 0x1547
makeAuraAbility("Dark Aura", "Dark")
makeAuraAbility("Fairy Aura", "Fairy")
# New unique abilities
makeAttachmentImmuneAbility("Aroma Veil", [Attachment.Attract, Attachment.Disable,
Attachment.Encore, Attachment.Taunt, Attachment.Torment], cure: false) # TODO: Add Heal Block
# Implemented in makeAuraAbility
makeAbility "Aura Break"
makeAbility 'Bulletproof', ->
this::isImmune = (type, move) ->
if move?.hasFlag('bullet')
@pokemon.activateAbility()
return true
# TODO: Cheek Pouch
makeAbility "Cheek Pouch"
makeAbility "Competitive", ->
this::afterEachBoost = (boostAmount, source) ->
return if source.team == @pokemon.team
@pokemon.activateAbility()
@pokemon.boost(specialAttack: 2) if boostAmount < 0
# TODO: Flower Veil
makeAbility "Flower Veil"
makeAbility "Fur Coat", ->
this::modifyBasePowerTarget = (move) ->
if move.isPhysical() then 0x800 else 0x1000
makeAbility 'Gale Wings', ->
this::editPriority = (priority, move) ->
# TODO: Test if Gale Wings works with Hidden Power Flying.
return priority + 1 if move.type == 'Flying'
return priority
makeAbility "Gooey", ->
this::isAliveCheck = -> true
this::afterBeingHit = (move, user) ->
if move.hasFlag("contact")
user.boost(speed: -1, @pokemon)
@pokemon.activateAbility()
# TODO: Grass Pelt
makeAbility "Grass Pelt"
# TODO: Magician
makeAbility "Magician"
makeAbility 'Mega Launcher', ->
this::modifyBasePower = (move, target) ->
return 0x1800 if move.hasFlag("pulse")
return 0x1000
makeAbility 'Parental Bond', ->
this::calculateNumberOfHits = (move, targets) ->
# Do nothing if this move is multi-hit, has multiple targets, or is status.
return if move.minHits != 1 || targets.length > 1 || move.isNonDamaging()
return 2
this::modifyDamage = (move, target, hitNumber) ->
return 0x800 if hitNumber == 2 && move.maxHits == 1
return 0x1000
makeAbility 'Protean', ->
this::beforeMove = (move, user, targets) ->
type = move.getType(@battle, user, targets[0])
return if user.types.length == 1 && user.types[0] == type
user.types = [ type ]
@pokemon.activateAbility()
@battle.cannedText('TRANSFORM_TYPE', user, type)
makeAbility 'Stance Change', ->
this::beforeMove = (move, user, targets) ->
newForme = switch
when !move.isNonDamaging() then "blade"
when move == @battle.getMove("King's Shield") then "default"
if newForme && !@pokemon.isInForme(newForme) && @pokemon.species == 'Aegislash'
@pokemon.activateAbility()
@pokemon.changeForme(newForme)
humanized = (if newForme == "blade" then "Blade" else "Shield")
@battle.message("Changed to #{humanized} Forme!")
true
makeAbility "Strong Jaw", ->
this::modifyBasePower = (move) ->
return 0x1800 if move.hasFlag("bite")
return 0x1000
# TODO: Sweet Veil (2v2)
makeAttachmentImmuneAbility("Sweet Veil", [Status.Sleep], cure: false)
# TODO: Symbiosis
makeAbility "Symbiosis"
makeAbility "Tough Claws", ->
this::modifyBasePower = (move) ->
return 0x14CD if move.hasFlag("contact")
return 0x1000
makeWeatherAbility("Noctem", Weather.MOON)
makeAbility 'Heliophobia', ->
this::endTurn = ->
amount = Math.floor(@pokemon.stat('hp') / 8)
if @battle.hasWeather(Weather.SUN)
@pokemon.setHP(@pokemon.currentHP - amount)
else if @battle.hasWeather(Weather.MOON)
@pokemon.setHP(@pokemon.currentHP + amount)
makeWeatherSpeedAbility("Shadow Dance", Weather.MOON)
makeAbility "Amplifier", ->
this::modifyBasePower = (move) ->
return 0x1800 if move.hasFlag("sound")
return 0x1000
makeAbility "Athenian", ->
this::modifyAttack = (move) ->
if move.isSpecial() then 0x2000 else 0x1000
makeTypeImmuneAbility("Castle Moat", "Water", "specialDefense")
makeAbility 'Ethereal Shroud', ->
this::typeEffectiveness = (move, user) ->
movetype = move.type
usertypes = user.types
effect = util.typeEffectiveness(movetype, usertypes)
ghosteffect = util.typeEffectiveness(movetype, ["Ghost"])
if ghosteffect < 1
effect *= ghosteffect
return effect
makeAbility 'Foundry', ->
this::editMoveType = (type, target) ->
return "Fire" if type == 'Rock' && @pokemon != target
return type
this::modifyBasePower = (move, target) ->
return 0x14CD if move.type == 'Rock'
return 0x1000
makeAbility 'Hubris', ->
this::afterSuccessfulHit = (move, user, target) ->
if target.isFainted()
@pokemon.activateAbility()
@pokemon.boost(specialAttack: 1)
makeWeatherSpeedAbility("Ice Cleats", Weather.HAIL)
makeAbility 'Irrelephant', ->
this::shouldIgnoreImmunity = (moveType, target) ->
return true
makeAbility 'Pendulum', ->
this::modifyBasePower = (move, target) ->
attachment = @pokemon.get(Attachment.Pendulum)
layers = attachment?.layers || 0
0x1000 + layers * 0x333
this::afterSuccessfulHit = (move, user, target) ->
user.attach(Attachment.Pendulum, {move})
makeAbility 'Prism Guard', ->
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)
# TODO: Protean Maxima
makeAbility "Protean Maxima", ->
this::beforeMove = (move, user, targets) ->
type = move.getType(@battle, user, targets[0])
newForme = switch
when type == "Normal" then "mega"
when type == "Water" then "mega-water"
when type == "Electric" then "mega-electric"
when type == "Fire" then "mega-fire"
when type == "Psychic" then "mega-psychic"
when type == "Dark" then "mega-dark"
when type == "Grass" then "mega-grass"
when type == "Ice" then "mega-ice"
when type == "Fairy" then "mega-fairy"
else ""
if newForme != "" && !@pokemon.isInForme(newForme) && @pokemon.species == 'Eevee'
@pokemon.activateAbility()
@pokemon.changeForme(newForme)
@battle.message("Changed to #{type} Forme!")
if newForme == 'mega-psychic'
@pokemon.attach(Attachment.MagicCoat)
@team.attach(Attachment.MagicCoat)
true
this::shouldBlockExecution = (move, user) ->
forme = @pokemon.getForme()
console.log(forme)
if @pokemon.isInForme("mega-water") && @pokemon.species == 'Eevee'
return if move.getType(@battle, user, @pokemon) != "Water" || user == @pokemon
@pokemon.activateAbility()
amount = @pokemon.stat('hp') >> 2
if @pokemon.heal(amount)
@battle.cannedText('RECOVER_HP', @pokemon)
return true
else if @pokemon.isInForme("mega-electric") && @pokemon.species == 'Eevee'
return if move.getType(@battle, user, @pokemon) != "Electric" || user == @pokemon
@pokemon.activateAbility()
amount = @pokemon.stat('hp') >> 2
if @pokemon.heal(amount)
@battle.cannedText('RECOVER_HP', @pokemon)
return true
else if @pokemon.isInForme("mega-fire") and @pokemon.species == 'Eevee'
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
this::beginTurn = this::switchIn = ->
forme = @pokemon.getForme()
if @pokemon.isInForme("mega-psychic") and @pokemon.species == 'Eevee'
@pokemon.attach(Attachment.MagicCoat)
@team.attach(Attachment.MagicCoat)
this::switchIn = ->
forme = @pokemon.getForme()
@doubleSpeed = @battle.hasWeather(Weather.SUN) if @pokemon.isInForme("mega-grass") and @pokemon.species == 'Eevee'
this::informWeather = (newWeather) ->
forme = @pokemon.getForme()
@doubleSpeed = (Weather.SUN == newWeather) if @pokemon.isInForme("mega-grass") and @pokemon.species == 'Eevee'
this::editSpeed = (speed) ->
forme = @pokemon.getForme()
if @doubleSpeed and @pokemon.isInForme("mega-grass") and @pokemon.species == 'Eevee'
2 * speed
else
speed
this::isWeatherDamageImmune = (currentWeather) ->
forme = @pokemon.getForme()
return true if Weather.SUN == currentWeather and @pokemon.isInForme("mega-grass")
return true if Weather.HAIL == currentWeather and forme == @pokemon.isInForme("mega-ice")
this::editEvasion = (accuracy) ->
forme = @pokemon.getForme()
if @battle.hasWeather(Weather.HAIL) and @pokemon.isInForme("mega-ice") and @pokemon.species == 'Eevee'
Math.floor(.8 * accuracy)
else
accuracy
this::afterBeingHit = (move, user, target, damage, isDirect) ->
forme = @pokemon.getForme()
return if @pokemon.isInForme("mega-ice") or @pokemon.species != 'Eevee'
return if !move.hasFlag("contact")
return if @battle.rng.next("contact status") >= .3
return if !isDirect
@pokemon.activateAbility()
user.attach(Attachment.Attract, source: @pokemon)
#mega-dark is hardcoded in bw/attachments
makeLowHealthAbility("Psycho Call", "Psychic")
makeLowHealthAbility("Shadow Call", "Dark")
makeLowHealthAbility("Spirit Call", "Ghost")
# TODO: Just kidding, this is done
makeAbility "Regurgitation"
makeAbility 'Spectral Jaws', ->
this::modifyBasePower = (move) ->
return 0x14CD if move.hasFlag("bite")
return 0x1000
makeAbility 'Speed Swap', ->
this::switchIn = ->
if @battle.attach(Attachment.TrickRoom)
@battle.cannedText('TRICK_ROOM_START', @pokemon)
else
@battle.unattach(Attachment.TrickRoom)
makeAbility 'Synthetic Alloy', ->
this::typeEffectiveness = (move) ->
return 1 if move.type = "Fire"
makeAbility 'Venomous', ->
this::afterSuccessfulHit = (move, user, target) ->
if move.shouldTriggerSecondary && move.ailmentId == "poison"
target.attach(Status.Toxic)
makeTypeImmuneAbility("Wind Force", "Flying", "speed")
makeAbility 'Winter Joy', ->
this::modifyBasePower = (move) ->
boostmonths = [1,2,11,12]
badmonths = [5,6,7,8]
today = new Date
mm = today.getMonth() + 1
if mm in boostmonths
return 0x1666
else if mm in badmonths
return 0xB33
else
return 0x1000
makeAbility 'Illusion', ->
this::initialize = ->
alivemons = @pokemon.team.getAlivePokemon()
lastalivemon = alivemons[alivemons.length-1]
@pokemon.attach(Attachment.Illusion, target: lastalivemon)

View File

@@ -0,0 +1,641 @@
{
"Amplifier": {
"description": "Sound-based moves are boosted by 1.5x."
},
"Athenian": {
"description": "Doubles the Pok<6F>mon's Special Attack stat."
},
"Castle Moat": {
"description": "Absorbs Water-type moves to boost Sp.Def."
},
"Ethereal Shroud": {
"description": "Grants the user Ghost-type associated immunities and resistances."
},
"Foundry": {
"description": "Rock-type moves, when used, melt and become Fire Type."
},
"Heliophobia": {
"description": "This Pokemon heals itself when the New Moon is active."
},
"Hubris": {
"description": "Boosts Special Attack after knocking out."
},
"Ice Cleats": {
"description": "Speed is doubled in Hail."
},
"Irrelephant": {
"description": "Immunities are irrelephant to this Pok<6F>mon's attacks."
},
"Noctem": {
"description": "The Pok<6F>mon summons darkness and blots out the sky as it enters the battle."
},
"Pendulum": {
"description": "Consecutively using the same move increases its damage."
},
"Prism Guard": {
"description": "Inflicts damage to the foe from non-contact moves."
},
"Protean Maxima": {
"description": "The Pok<6F>mon changes form, stats and ability based on the type of move it uses."
},
"Psycho Call": {
"description": "Powers up Psychic-type moves in a pinch."
},
"Regurgitation": {
"description": "The Pokemon this one has captured does damage. Currently does nothing"
},
"Shadow Call": {
"description": "Powers up Dark-type moves in a pinch."
},
"Shadow Dance": {
"description": "Boosts the Speed stat when the New Moon is active."
},
"Spectral Jaws": {
"description": "All biting moves are Special and have a 30% boost."
},
"Speed Swap": {
"description": "Activates the Trick Room effect on entering the battlefield."
},
"Spirit Call": {
"description": "Powers up Ghost-type moves in a pinch."
},
"Synthetic Alloy": {
"description": "Pok<6F>mon with Synthetic Alloy take neutral damage from Fire-type attacks regardless of their type-specific weaknesses."
},
"Venomous": {
"description": "A Pok<6F>mon with Venomous always causes bad poison instead of regular poison."
},
"Wind Force": {
"description": "Flying-type moves boost this Pokemon<6F>s speed instead of damage it."
},
"Winter Joy": {
"description": "Strengthened in winter and weakened in summer."
},
"Adaptability": {
"description": "This Pokemon's attacks that match one of its types have a STAB modifier of 2 instead of 1.5."
},
"Delta Stream": {
"description": "Affects weather and eliminates all of the Flying type's weaknesses."
},
"Aerilate": {
"description": "This Pokemon's Normal-type moves become Flying-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Aftermath": {
"description": "If this Pokemon is knocked out with a contact move, that move's user loses 1/4 of its maximum HP, rounded down. If any active Pokemon has the Ability Damp, this effect is prevented."
},
"Air Lock": {
"description": "While this Pokemon is active, the effects of weather conditions are disabled."
},
"Analytic": {
"description": "The power of this Pokemon's move is multiplied by 1.3 if it is the last to move in a turn. Does not affect Doom Desire and Future Sight."
},
"Anger Point": {
"description": "If this Pokemon, but not its substitute, is struck by a critical hit, its Attack is raised by 12 stages."
},
"Anticipation": {
"description": "On switch-in, this Pokemon is alerted if any opposing Pokemon has an attack that is super effective on this Pokemon or an OHKO move. Counter, Metal Burst, and Mirror Coat count as attacking moves of their respective types, while Hidden Power, Judgment, Natural Gift, Techno Blast, and Weather Ball are considered Normal-type moves."
},
"Arena Trap": {
"description": "Prevents adjacent opposing Pokemon from choosing to switch out unless they are immune to trapping or have immunity to Ground."
},
"Aroma Veil": {
"description": "This Pokemon and its allies cannot be affected by Attract, Disable, Encore, Heal Block, Taunt, or Torment."
},
"Aura Break": {
"description": "While this Pokemon is active, the effects of the Abilities Dark Aura and Fairy Aura are reversed, multiplying the power of Dark- and Fairy-type moves, respectively, by 3/4 instead of 1.33."
},
"Bad Dreams": {
"description": "Causes adjacent opposing Pokemon to lose 1/8 of their maximum HP, rounded down, at the end of each turn if they are asleep."
},
"Battle Armor": {
"description": "This Pokemon cannot be struck by a critical hit."
},
"Big Pecks": {
"description": "Prevents other Pokemon from lowering this Pokemon's Defense stat stage."
},
"Blaze": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Fire-type attack."
},
"Bulletproof": {
"description": "This Pokemon is immune to bullet moves."
},
"Cheek Pouch": {
"description": "If this Pokemon eats a Berry, it restores 1/3 of its maximum HP, rounded down, in addition to the Berry's effect."
},
"Chlorophyll": {
"description": "If Sunny Day is active, this Pokemon's Speed is doubled."
},
"Clear Body": {
"description": "Prevents other Pokemon from lowering this Pokemon's stat stages."
},
"Cloud Nine": {
"description": "While this Pokemon is active, the effects of weather conditions are disabled."
},
"Color Change": {
"description": "This Pokemon's type changes to match the type of the last move that hit it. This effect applies after all hits from a multi-hit move; Sheer Force prevents it from activating if the attack has a secondary effect."
},
"Competitive": {
"description": "This Pokemon's Special Attack is raised by 2 stages for each of its stat stages that is lowered by an opposing Pokemon."
},
"Compoundeyes": {
"description": "This Pokemon's moves have their accuracy multiplied by 1.3."
},
"Contrary": {
"description": "If this Pokemon has a stat stage raised it is lowered instead, and vice versa."
},
"Cursed Body": {
"description": "If this Pokemon is hit by an attack, there is a 30% chance that move gets disabled unless one of the attacker's moves is already disabled."
},
"Cute Charm": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will become infatuated if it is of the opposite gender."
},
"Damp": {
"description": "While this Pokemon is active, Self-Destruct, Explosion, and the Ability Aftermath are prevented from having an effect."
},
"Dark Aura": {
"description": "While this Pokemon is active, the power of Dark-type moves used by active Pokemon is multiplied by 1.33."
},
"Defeatist": {
"description": "While this Pokemon has 1/2 or less of its maximum HP, rounded down, its Attack and Special Attack are halved."
},
"Defiant": {
"description": "This Pokemon's Attack is raised by 2 stages for each of its stat stages that is lowered by an opposing Pokemon."
},
"Download": {
"description": "On switch-in, this Pokemon's Attack or Special Attack is raised by 1 stage based on the weaker combined defensive stat of all opposing Pokemon. Attack is raised if their Defense is lower, and Special Attack is raised if their Special Defense is the same or lower."
},
"Drizzle": {
"description": "On switch-in, this Pokemon summons Rain Dance."
},
"Drought": {
"description": "On switch-in, this Pokemon summons Sunny Day."
},
"Dry Skin": {
"description": "This Pokemon is immune to Water-type moves and restores 1/4 of its maximum HP, rounded down, when hit by a Water-type move. The power of Fire-type moves is multiplied by 1.25 when used on this Pokemon. At the end of each turn, this Pokemon restores 1/8 of its maximum HP, rounded down, if the weather is Rain Dance, and loses 1/8 of its maximum HP, rounded down, if the weather is Sunny Day."
},
"Early Bird": {
"description": "This Pokemon's sleep counter drops by 2 instead of 1."
},
"Effect Spore": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be poisoned, paralyzed, or fall asleep."
},
"Fairy Aura": {
"description": "While this Pokemon is active, the power of Fairy-type moves used by active Pokemon is multiplied by 1.33."
},
"Filter": {
"description": "This Pokemon receives 3/4 damage from supereffective attacks."
},
"Flame Body": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be burned."
},
"Flare Boost": {
"description": "While this Pokemon is burned, the power of its special attacks is multiplied by 1.5."
},
"Flash Fire": {
"description": "This Pokemon is immune to Fire-type moves. The first time it is hit by a Fire-type move, its attacking stat is multiplied by 1.5 while using a Fire-type attack as long as it remains active and has this Ability. If this Pokemon is frozen, it cannot be defrosted by Fire-type attacks."
},
"Flower Gift": {
"description": "If this Pokemon is a Cherrim and Sunny Day is active, it changes to Sunshine Form and the Attack and Special Defense of it and its allies are multiplied by 1.5."
},
"Flower Veil": {
"description": "Grass-type Pokemon on this Pokemon's side cannot have their stat stages lowered by other Pokemon or have a major status condition inflicted on them by other Pokemon."
},
"Forecast": {
"description": "If this Pokemon is a Castform, its type changes to the current weather condition's type, except Sandstorm."
},
"Forewarn": {
"description": "On switch-in, this Pokemon is alerted to the move with the highest power, at random, known by an opposing Pokemon."
},
"Friend Guard": {
"description": "This Pokemon's allies receive 3/4 damage from other Pokemon's attacks."
},
"Frisk": {
"description": "On switch-in, this Pokemon identifies the held items of all opposing Pokemon."
},
"Fur Coat": {
"description": "This Pokemon's Defense is doubled."
},
"Gale Wings": {
"description": "This Pokemon's Flying-type moves have their priority increased by 1."
},
"Gluttony": {
"description": "When this Pokemon has 1/2 or less of its maximum HP, rounded down, it uses certain Berries early."
},
"Gooey": {
"description": "This Pokemon causes Pokemon making contact with it to have their Speed lowered by 1 stage."
},
"Grass Pelt": {
"description": "If Grassy Terrain is active, this Pokemon's Defense is multiplied by 1.5."
},
"Guts": {
"description": "If this Pokemon has a major status condition, its Attack is multiplied by 1.5; burn's physical damage halving is ignored."
},
"Harvest": {
"description": "If the last item this Pokemon used is a Berry, there is a 50% chance it gets restored at the end of each turn. If Sunny Day is active, this chance is 100%."
},
"Healer": {
"description": "There is a 30% chance of curing an adjacent ally's major status condition at the end of each turn."
},
"Heatproof": {
"description": "The power of Fire-type attacks against this Pokemon is halved, and any burn damage taken is 1/16 of its maximum HP, rounded down."
},
"Heavy Metal": {
"description": "This Pokemon's weight is doubled."
},
"Honey Gather": {
"description": "No competitive use."
},
"Huge Power": {
"description": "This Pokemon's Attack is doubled."
},
"Hustle": {
"description": "This Pokemon's Attack is multiplied by 1.5 and the accuracy of its physical attacks is multiplied by 0.8."
},
"Hydration": {
"description": "This Pokemon has its major status condition cured at the end of each turn if Rain Dance is active."
},
"Hyper Cutter": {
"description": "Prevents other Pokemon from lowering this Pokemon's Attack stat stage."
},
"Ice Body": {
"description": "If Hail is active, this Pokemon restores 1/16 of its maximum HP, rounded down, at the end of each turn. This Pokemon takes no damage from Hail."
},
"Illuminate": {
"description": "No competitive use."
},
"Illusion": {
"description": "When this Pokemon switches in, it appears as the last unfainted Pokemon in its party until it takes direct damage from another Pokemon's attack. This Pokemon's actual level and HP are displayed instead of those of the mimicked Pokemon."
},
"Immunity": {
"description": "This Pokemon cannot be poisoned. Gaining this Ability while poisoned cures it."
},
"Imposter": {
"description": "On switch-in, this Pokemon Transforms into the opposing Pokemon that is facing it. If there is no Pokemon at that position, this Pokemon does not Transform."
},
"Infiltrator": {
"description": "This Pokemon's moves ignore substitutes and the opposing side's Reflect, Light Screen, Safeguard, and Mist."
},
"Inner Focus": {
"description": "This Pokemon cannot be made to flinch."
},
"Insomnia": {
"description": "This Pokemon cannot fall asleep. Gaining this Ability while asleep cures it."
},
"Intimidate": {
"description": "On switch-in, this Pokemon lowers the Attack of adjacent opposing Pokemon by 1 stage. Pokemon behind a substitute are immune."
},
"Iron Barbs": {
"description": "This Pokemon causes Pokemon making contact with it to lose 1/8 of their maximum HP, rounded down."
},
"Iron Fist": {
"description": "This Pokemon's punch-based attacks have their power multiplied by 1.2."
},
"Justified": {
"description": "This Pokemon's Attack is raised by 1 stage after it is damaged by a Dark-type attack."
},
"Keen Eye": {
"description": "Prevents other Pokemon from lowering this Pokemon's accuracy stat stage. This Pokemon ignores a target's evasiveness stat stage."
},
"Klutz": {
"description": "This Pokemon's held item has no effect. This Pokemon cannot use Fling successfully. Macho Brace, Power Anklet, Power Band, Power Belt, Power Bracer, Power Lens, and Power Weight still have their effects."
},
"Leaf Guard": {
"description": "If Sunny Day is active, this Pokemon cannot gain a major status condition and Rest will fail for it."
},
"Levitate": {
"description": "This Pokemon is immune to Ground. Gravity, Ingrain, Smack Down, and Iron Ball nullify the immunity."
},
"Light Metal": {
"description": "This Pokemon's weight is halved."
},
"Lightningrod": {
"description": "This Pokemon is immune to Electric-type moves and raises its Special Attack by 1 stage when hit by an Electric-type move. If this Pokemon is not the target of a single-target Electric-type move used by another Pokemon, this Pokemon redirects that move to itself if it is within the range of that move."
},
"Limber": {
"description": "This Pokemon cannot be paralyzed. Gaining this Ability while paralyzed cures it."
},
"Liquid Ooze": {
"description": "This Pokemon damages those draining HP from it for as much as they would heal."
},
"Magic Bounce": {
"description": "This Pokemon blocks certain status moves and instead uses the move against the original user."
},
"Magic Guard": {
"description": "This Pokemon can only be damaged by direct attacks. Curse and Substitute on use, Belly Drum, Pain Split, Struggle recoil, and confusion damage are considered direct damage."
},
"Magician": {
"description": "If this Pokemon has no item, it steals the item off a Pokemon it hits with an attack. Does not affect Doom Desire and Future Sight."
},
"Magma Armor": {
"description": "This Pokemon cannot be frozen. Gaining this Ability while frozen cures it."
},
"Magnet Pull": {
"description": "Prevents adjacent opposing Steel-type Pokemon from choosing to switch out unless they are immune to trapping."
},
"Marvel Scale": {
"description": "If this Pokemon has a major status condition, its Defense is multiplied by 1.5."
},
"Mega Launcher": {
"description": "This Pokemon's pulse moves have their power multiplied by 1.5. Heal Pulse restores 3/4 of a target's maximum HP, rounded half down."
},
"Minus": {
"description": "If an active ally has this Ability or the Ability Plus, this Pokemon's Special Attack is multiplied by 1.5."
},
"Mold Breaker": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Moody": {
"description": "This Pokemon has a random stat raised by 2 stages and another stat lowered by 1 stage at the end of each turn."
},
"Motor Drive": {
"description": "This Pokemon is immune to Electric-type moves and raises its Speed by 1 stage when hit by an Electric-type move."
},
"Moxie": {
"description": "This Pokemon's Attack is raised by 1 stage if it attacks and knocks out another Pokemon."
},
"Multiscale": {
"description": "If this Pokemon is at full HP, damage taken from attacks is halved."
},
"Multitype": {
"description": "If this Pokemon is an Arceus, its type changes to match its held Plate."
},
"Mummy": {
"description": "Pokemon making contact with this Pokemon have their Ability changed to Mummy. Does not affect the Ability Multitype or Stance Change."
},
"Natural Cure": {
"description": "This Pokemon has its major status condition cured when it switches out."
},
"No Guard": {
"description": "Every move used by or against this Pokemon will always hit."
},
"Normalize": {
"description": "This Pokemon's moves are changed to be Normal type. This effect comes before other effects that change a move's type."
},
"Oblivious": {
"description": "This Pokemon cannot be infatuated or taunted. Gaining this Ability while affected cures it."
},
"Overcoat": {
"description": "This Pokemon is immune to powder moves and damage from Sandstorm or Hail."
},
"Overgrow": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Grass-type attack."
},
"Own Tempo": {
"description": "This Pokemon cannot be confused. Gaining this Ability while confused cures it."
},
"Parental Bond": {
"description": "This Pokemon's damaging attacks become multi-hit moves that hit twice. The second hit has its damage halved. Does not affect multi-hit moves or moves that have multiple targets."
},
"Pickpocket": {
"description": "If this Pokemon has no item, it steals the item off a Pokemon that makes contact with it."
},
"Pickup": {
"description": "If this Pokemon has no item, it finds one used by an adjacent Pokemon this turn."
},
"Pixilate": {
"description": "This Pokemon's Normal-type moves become Fairy-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Plus": {
"description": "If an active ally has this Ability or the Ability Minus, this Pokemon's Special Attack is multiplied by 1.5."
},
"Poison Heal": {
"description": "If this Pokemon is poisoned, it restores 1/8 of its maximum HP, rounded down, at the end of each turn instead of losing HP."
},
"Poison Point": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be poisoned."
},
"Poison Touch": {
"description": "This Pokemon's contact moves have a 30% chance of poisoning."
},
"Prankster": {
"description": "This Pokemon's non-damaging moves have their priority increased by 1."
},
"Pressure": {
"description": "If this Pokemon is the target of an opposing Pokemon's move, that move loses one additional PP."
},
"Protean": {
"description": "This Pokemon's type changes to match the type of the move it is about to use. This effect comes after all effects that change a move's type."
},
"Pure Power": {
"description": "This Pokemon's Attack is doubled."
},
"Quick Feet": {
"description": "If this Pokemon has a major status condition, its Speed is multiplied by 1.5; the Speed drop from paralysis is ignored."
},
"Rain Dish": {
"description": "If Rain Dance is active, this Pokemon restores 1/16 of its maximum HP, rounded down, at the end of each turn."
},
"Rattled": {
"description": "This Pokemon's Speed is raised by 1 stage if hit by a Bug-, Dark-, or Ghost-type attack."
},
"Reckless": {
"description": "This Pokemon's attacks with recoil or crash damage have their power multiplied by 1.2. Does not affect Struggle."
},
"Refrigerate": {
"description": "This Pokemon's Normal-type moves become Ice-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Regenerator": {
"description": "This Pokemon restores 1/3 of its maximum HP, rounded down, when it switches out."
},
"Rivalry": {
"description": "This Pokemon's attacks have their power multiplied by 1.25 against targets of the same gender or multiplied by 0.75 against targets of the opposite gender. There is no modifier if either this Pokemon or the target is genderless."
},
"Rock Head": {
"description": "This Pokemon does not take recoil damage besides Struggle, Life Orb, and crash damage."
},
"Rough Skin": {
"description": "This Pokemon causes Pokemon making contact with it to lose 1/8 of their maximum HP, rounded down."
},
"Run Away": {
"description": "No competitive use."
},
"Sand Force": {
"description": "If Sandstorm is active, this Pokemon's Ground-, Rock-, and Steel-type attacks have their power multiplied by 1.3. This Pokemon takes no damage from Sandstorm."
},
"Sand Rush": {
"description": "If Sandstorm is active, this Pokemon's Speed is doubled. This Pokemon takes no damage from Sandstorm."
},
"Sand Stream": {
"description": "On switch-in, this Pokemon summons Sandstorm."
},
"Sand Veil": {
"description": "If Sandstorm is active, this Pokemon's evasiveness is multiplied by 1.25. This Pokemon takes no damage from Sandstorm."
},
"Sap Sipper": {
"description": "This Pokemon is immune to Grass-type moves and raises its Attack by 1 stage when hit by a Grass-type move."
},
"Scrappy": {
"description": "This Pokemon can hit Ghost types with Normal- and Fighting-type moves."
},
"Serene Grace": {
"description": "This Pokemon's moves have their secondary effect chance doubled."
},
"Shadow Tag": {
"description": "Prevents adjacent opposing Pokemon from choosing to switch out unless they are immune to trapping or also have this Ability."
},
"Shed Skin": {
"description": "This Pokemon has a 33% chance to have its major status condition cured at the end of each turn."
},
"Sheer Force": {
"description": "This Pokemon's attacks with secondary effects have their power multiplied by 1.3, but the secondary effects are removed."
},
"Shell Armor": {
"description": "This Pokemon cannot be struck by a critical hit."
},
"Shield Dust": {
"description": "This Pokemon is not affected by the secondary effect of another Pokemon's attack."
},
"Simple": {
"description": "If this Pokemon's stat stages are raised or lowered, the effect is doubled instead."
},
"Skill Link": {
"description": "This Pokemon's multi-hit attacks always hit the maximum number of times."
},
"Slow Start": {
"description": "On switch-in, this Pokemon's Attack and Speed are halved for 5 turns."
},
"Sniper": {
"description": "If this Pokemon strikes with a critical hit, the damage is multiplied by 1.5."
},
"Snow Cloak": {
"description": "If Hail is active, this Pokemon's evasiveness is multiplied by 1.25. This Pokemon takes no damage from Hail."
},
"Snow Warning": {
"description": "On switch-in, this Pokemon summons Hail."
},
"Solar Power": {
"description": "If Sunny Day is active, this Pokemon's Special Attack is multiplied by 1.5 and it loses 1/8 of its maximum HP, rounded down, at the end of each turn."
},
"Solid Rock": {
"description": "This Pokemon receives 3/4 damage from supereffective attacks."
},
"Soundproof": {
"description": "This Pokemon is immune to sound-based moves, including Heal Bell."
},
"Speed Boost": {
"description": "This Pokemon's Speed is raised by 1 stage at the end of each full turn it has been on the field."
},
"Stall": {
"description": "This Pokemon moves last among Pokemon using the same or greater priority moves."
},
"Stance Change": {
"description": "If this Pokemon is an Aegislash, it changes to Blade Forme before attempting to use an attacking move, and changes to Shield Forme before attempting to use King's Shield."
},
"Static": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be paralyzed."
},
"Steadfast": {
"description": "If this Pokemon flinches, its Speed is raised by 1 stage."
},
"Stench": {
"description": "This Pokemon's attacks without a chance to flinch have a 10% chance to flinch."
},
"Sticky Hold": {
"description": "This Pokemon cannot lose its held item due to another Pokemon's attack."
},
"Storm Drain": {
"description": "This Pokemon is immune to Water-type moves and raises its Special Attack by 1 stage when hit by a Water-type move. If this Pokemon is not the target of a single-target Water-type move used by another Pokemon, this Pokemon redirects that move to itself if it is within the range of that move."
},
"Strong Jaw": {
"description": "This Pokemon's bite-based moves have their power multiplied by 1.5."
},
"Sturdy": {
"description": "If this Pokemon is at full HP, it survives one hit with at least 1 HP. OHKO moves fail when used against this Pokemon."
},
"Suction Cups": {
"description": "This Pokemon cannot be forced to switch out by another Pokemon's attack or item."
},
"Super Luck": {
"description": "This Pokemon's critical hit ratio is raised by 1 stage."
},
"Swarm": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Bug-type attack."
},
"Sweet Veil": {
"description": "This Pokemon and its allies cannot fall asleep."
},
"Swift Swim": {
"description": "If Rain Dance is active, this Pokemon's Speed is doubled."
},
"Symbiosis": {
"description": "If an ally uses its item, this Pokemon gives its item to that ally immediately. Does not activate if the ally's item was stolen or knocked off."
},
"Synchronize": {
"description": "If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon, that Pokemon receives the same major status condition."
},
"Tangled Feet": {
"description": "This Pokemon's evasiveness is doubled as long as it is confused."
},
"Technician": {
"description": "This Pokemon's attacks of 60 Base Power or less have their power multiplied by 1.5. Does affect Struggle."
},
"Telepathy": {
"description": "This Pokemon does not take damage from attacks made by its allies."
},
"Teravolt": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Thick Fat": {
"description": "If a Pokemon uses a Fire- or Ice-type attack against this Pokemon, that Pokemon's attacking stat is halved while using the attack."
},
"Tinted Lens": {
"description": "This Pokemon's attacks that are not very effective on a target have their damage doubled."
},
"Torrent": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Water-type attack."
},
"Tough Claws": {
"description": "This Pokemon's contact moves have their power multiplied by 1.3."
},
"Toxic Boost": {
"description": "While this Pokemon is poisoned, the power of its physical attacks is multiplied by 1.5."
},
"Trace": {
"description": "On switch-in, this Pokemon copies a random adjacent opposing Pokemon's Ability. If there is no Ability that can be copied at that time, this Ability will activate as soon as an Ability can be copied. Abilities that cannot be copied are Flower Gift, Forecast, Illusion, Imposter, Multitype, Stance Change, Trace, and Zen Mode."
},
"Truant": {
"description": "This Pokemon skips every other turn instead of using a move."
},
"Turboblaze": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Unaware": {
"description": "This Pokemon ignores other Pokemon's Attack, Special Attack, and accuracy stat stages when taking damage, and ignores other Pokemon's Defense, Special Defense, and evasiveness stat stages when dealing damage."
},
"Unburden": {
"description": "If this Pokemon loses its held item for any reason, its Speed is doubled. This boost is lost if it switches out or gains a new item or Ability."
},
"Unnerve": {
"description": "While this Pokemon is active, it prevents opposing Pokemon from using their Berries."
},
"Victory Star": {
"description": "This Pokemon and its allies' moves have their accuracy multiplied by 1.1."
},
"Vital Spirit": {
"description": "This Pokemon cannot fall asleep. Gaining this Ability while asleep cures it."
},
"Volt Absorb": {
"description": "This Pokemon is immune to Electric-type moves and restores 1/4 of its maximum HP, rounded down, when hit by an Electric-type move."
},
"Water Absorb": {
"description": "This Pokemon is immune to Water-type moves and restores 1/4 of its maximum HP, rounded down, when hit by a Water-type move."
},
"Water Veil": {
"description": "This Pokemon cannot be burned. Gaining this Ability while burned cures it."
},
"Weak Armor": {
"description": "If a physical attack hits this Pokemon, its Defense is lowered by 1 stage and its Speed is raised by 1 stage."
},
"White Smoke": {
"description": "Prevents other Pokemon from lowering this Pokemon's stat stages."
},
"Wonder Guard": {
"description": "This Pokemon can only be damaged by supereffective moves and indirect damage."
},
"Wonder Skin": {
"description": "All non-damaging moves have their accuracy changed to 50% when used on this Pokemon, unless the move cannot miss."
},
"Zen Mode": {
"description": "If this Pokemon is a Darmanitan, it changes to Zen Mode if it has 1/2 or less of its maximum HP at the end of a turn. If Darmanitan's HP is above 1/2 of its maximum HP at the end of a turn, it changes back to Standard Mode. If Darmanitan loses this Ability while in Zen Mode it reverts to Standard Mode immediately."
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

13332
server/in/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 @@
{ }

View File

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

View File

@@ -0,0 +1,52 @@
GEM_BOOST_AMOUNT = 0x14CD
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/items.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
makeTypeResistBerry 'Roseli Berry', 'Fairy'
makeBoostOnTypeItem 'Luminous Moss', 'Water', specialDefense: 1
makeBoostOnTypeItem 'Snowball', 'Ice', attack: 1
makePlateItem 'Pixie Plate', 'Fairy'
makeItem "Assault Vest", ->
this::beginTurn = ->
for move in @pokemon.moves
if move.isNonDamaging()
@pokemon.blockMove(move)
this::editSpecialDefense = (defense) ->
Math.floor(defense * 1.5)
makeItem "Kee Berry", ->
this.eat = (battle, owner) ->
owner.boost(defense: 1)
this::afterBeingHit = (move, user) ->
if move.isPhysical()
@battle.message("#{@pokemon.name}'s #{@displayName} berry activated!")
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeItem "Maranga Berry", ->
this.eat = (battle, owner) ->
owner.boost(specialDefense: 1)
this::afterBeingHit = (move, user) ->
if move.isSpecial()
@battle.message("#{@pokemon.name}'s #{@displayName} berry activated!")
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeItem "Safety Goggles", ->
this::isWeatherDamageImmune = -> true
this::shouldBlockExecution = (move, user) ->
return true if move.hasFlag("powder")
makeItem "Weakness Policy", ->
this::afterBeingHit = (move, user, target, damage, isDirect) ->
if isDirect && !move.isNonDamaging() &&
move.typeEffectiveness(@battle, user, @pokemon) > 1
@pokemon.boost(attack: 2, specialAttack: 2)
@pokemon.useItem()

141
server/in/data/moves.coffee Normal file
View File

@@ -0,0 +1,141 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/moves.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
makeChargeMove 'Bounce', ["Gust", "Thunder", "Twister", "Sky Uppercut", "Hurricane", "Smack Down", "Thousand Arrows"], "$1 sprang up!"
extendMove "Defog", ->
@entryHazards.push(Attachment.StickyWeb)
@selectPokemon = (battle, user, target) ->
[ target, user ]
extendMove 'Facade', ->
@burnCalculation = -> 1
extendMove 'Fell Stinger', ->
@afterSuccessfulHit = (battle, user, target) ->
user.boost(attack: 2) if target.isFainted()
makeChargeMove 'Fly', ["Gust", "Thunder", "Twister", "Sky Uppercut", "Hurricane", "Smack Down", "Thousand Arrows"], "$1 flew up high!"
extendMove 'Freeze-Dry', ->
@superEffectiveAgainst = "Water"
makeChargeMove 'Geomancy', "$1 is absorbing power!"
extendMove 'Knock Off', ->
@basePower = (battle, user, target) ->
multiplier = (if target.hasTakeableItem() then 1.5 else 1.0)
Math.floor(multiplier * @power)
extendMove 'Happy Hour', ->
@afterSuccessfulHit = (battle, user, target) ->
battle.message "Everyone is caught up in the happy atmosphere!"
extendMove 'Hidden Power', ->
@basePower = -> @power
makeProtectCounterMove "King's Shield", (battle, user, targets) ->
user.attach(Attachment.KingsShield)
makeTrappingMove "Infestation"
extendMove "Metronome", ->
@impossibleMoves.push("Belch", "Celebrate", "Crafty Shield", "Diamond Storm",
"Happy Hour", "Hold Hands", "Hyperspace Hole", "King's Shield", "Light of Ruin",
"Mat Block", "Spiky Shield", "Steam Eruption", "Thousand Arrows", "Thousand Waves")
extendMove 'Nature Power', ->
@execute = (battle, user, targets) ->
# In Wi-Fi battles, Tri Attack is always chosen.
battle.message "#{@name} turned into Tri Attack!"
triAttack = battle.getMove('Tri Attack')
battle.executeMove(triAttack, user, targets)
extendMove "Parting Shot", ->
@afterSuccessfulHit = (battle, user, target) ->
target.boost(attack: -1, specialAttack: -1, user)
battle.forceSwitch(user)
makeChargeMove 'Phantom Force', [], "$1 vanished instantly!", (battle) ->
battle.hasWeather(Weather.MOON)
extendMove "Rapid Spin", ->
@entryHazards.push(Attachment.StickyWeb)
extendMove 'Skill Swap', ->
@canSwapSameAbilities = true
makeProtectCounterMove "Spiky Shield", (battle, user, targets) ->
user.attach(Attachment.SpikyShield)
makeOpponentFieldMove 'Sticky Web', (battle, user, opponentId) ->
team = battle.getTeam(opponentId)
if !team.attach(Attachment.StickyWeb)
@fail(battle, user)
extendMove 'Topsy-Turvy', ->
@afterSuccessfulHit = (battle, user, target) ->
if target.hasBoosts()
boosts = {}
for stage, value of target.stages
boosts[stage] = -value
target.setBoosts(boosts)
battle.message "#{target.name}'s stat changes were all reversed!"
else
@fail(battle, user)
extendMove 'Toxic', ->
@canMiss = (battle, user, target) ->
return !user.hasType("Poison")
extendMove 'Venom Drench', ->
@use = (battle, user, target) ->
if !target.has(Status.Poison)
@fail(battle, user)
return false
target.boost(attack: -1, specialAttack: -1, speed: -1)
#insurgence shit beneath this
makeWeatherMove 'New Moon', Weather.MOON
makeChargeMove 'Lunar Cannon', [], "$1 absorbed darkness!", (battle) ->
battle.hasWeather(Weather.MOON)
extendMove 'Surf', ->
@basePower = (battle, user, target) ->
if battle.hasWeather(Weather.MOON)
Math.floor(@power * 1.5)
else
@power
extendMove 'Freeze Shock', ->
@basePower = (battle, user, target) ->
if battle.hasWeather(Weather.MOON)
Math.floor(@power * 0.3)
else
@power
extendMove 'Dragonify', ->
@afterSuccessfulHit = (battle, user, target) ->
if (target.types.length == 1 && target.types[0] == 'Dragon') || target.ability.displayName != 'Multitype'
@fail(battle, user)
else
target.types = [ 'Dragon' ]
battle.cannedText('TRANSFORM_TYPE', target, 'Dragon')
makeOpponentFieldMove 'Livewire', (battle, user, opponentId) ->
team = battle.getTeam(opponentId)
if !team.attach(Attachment.Livewire)
@fail(battle, user)
extendMove 'Wildfire', ->
@afterSuccessfulHit = (battle, user, target) ->
if target.hasType("Grass")
for p, i in target.team.pokemon
weakness = p.effectivenessOf(["Fire"], user: user, move: this)
console.log(weakness)
if !(p.isFainted()) && weakness >= 2
p.attach(Status.Burn)

View File

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

29
server/in/move.coffee Normal file
View File

@@ -0,0 +1,29 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/move.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Move::criticalMultiplier = 1.5
@Move::determineCriticalHitFromLevel = (level, rand) ->
switch level
when -1
true
when 1
rand < 0.0625
when 2
rand < 0.125
when 3
rand < 0.5
else
rand < 1
@Move::numHitsMessage = (hitNumber) ->
times = (if hitNumber == 1 then "time" else "times")
return "Hit #{hitNumber} #{times}!"
# In XY, voice moves and Infiltrator deal direct damage.
oldIsDirectHit = @Move::isDirectHit
@Move::isDirectHit = (battle, user, target) ->
return true if @hasFlag("sound")
return true if user.hasAbility("Infiltrator") && user.isActive()
return oldIsDirectHit.apply(this, arguments)

36
server/in/pokemon.coffee Normal file
View File

@@ -0,0 +1,36 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/pokemon.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Pokemon::canMegaEvolve = ->
return false if !@hasItem()
return false if @item.type != 'megastone'
[ species, forme ] = @item.mega
return false if @species != species || @forme != 'default'
return false if @team?.filter((p) -> /^mega/.test(p.forme)).length > 0
return true
oldBlockSwitch = @Pokemon::blockSwitch
@Pokemon::blockSwitch = ->
oldBlockSwitch.apply(this, arguments) if !@hasType("Ghost")
oldHasTakeableItem = @Pokemon::hasTakeableItem
@Pokemon::hasTakeableItem = ->
return false if oldHasTakeableItem.apply(this, arguments) == false
if @item.type == 'megastone'
[ species, forme ] = @item.mega
return false if @species == species
return true
# Powder moves no longer affect Grass-type Pokemon.
oldShouldBlockExecution = @Pokemon::shouldBlockExecution
@Pokemon::shouldBlockExecution = (move, user) ->
if move.hasFlag("powder") && @hasType("Grass")
move.fail(@battle, user)
return true
oldShouldBlockExecution.apply(this, arguments)
# In XY, Stance Change is another specially hardcoded ability that cannot change
oldHasChangeableAbility = @Pokemon::hasChangeableAbility
@Pokemon::hasChangeableAbility = ->
!@hasAbility("Stance Change") && oldHasChangeableAbility.call(this)

216
server/in/priorities.coffee Normal file
View File

@@ -0,0 +1,216 @@
{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
Ability.StanceChange
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.Pendulum
Attachment.Grudge
Attachment.Rage
Attachment.Charging
Attachment.FuryCutter
Ability.Protean
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.StickyWeb
Attachment.StealthRock
Attachment.FireRock
Attachment.Spikes
Attachment.ToxicSpikes
Attachment.Livewire
# 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.SpikyShield
Attachment.KingsShield
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.KingsShield
Attachment.SpikyShield
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
Ability.Overcoat
Item.SafetyGoggles
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
Ability.Bulletproof
]

3
server/in/queries.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/queries.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

3
server/in/rng.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/rng.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

3
server/in/team.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/team.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8')))

29
server/in/util.coffee Normal file
View File

@@ -0,0 +1,29 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/util.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Type.Fairy = Type.Fairy = 17
@Type["???"] = Type["???"] = 18
typeChart = [
# Nor Fir Wat Ele Gra Ice Fig Poi Gro Fly Psy Bug Roc Gho Dra Dar Ste Fai, ???
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, .5, 0, 1, 1, .5, 1, 1 ], # Nor
[ 1, .5, .5, 1, 2, 2, 1, 1, 1, 1, 1, 2, .5, 1, .5, 1, 2, 1, 1 ], # Fir
[ 1, 2, .5, 1, .5, 1, 1, 1, 2, 1, 1, 1, 2, 1, .5, 1, 1, 1, 1 ], # Wat
[ 1, 1, 2, .5, .5, 1, 1, 1, 0, 2, 1, 1, 1, 1, .5, 1, 1, 1, 1 ], # Ele
[ 1, .5, 2, 1, .5, 1, 1, .5, 2, .5, 1, .5, 2, 1, .5, 1, .5, 1, 1 ], # Gra
[ 1, .5, .5, 1, 2, .5, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, .5, 1, 1 ], # Ice
[ 2, 1, 1, 1, 1, 2, 1, .5, 1, .5, .5, .5, 2, 0, 1, 2, 2, .5, 1 ], # Fig
[ 1, 1, 1, 1, 2, 1, 1, .5, .5, 1, 1, 1, .5, .5, 1, 1, 0, 2, 1 ], # Poi
[ 1, 2, 1, 2, .5, 1, 1, 2, 1, 0, 1, .5, 2, 1, 1, 1, 2, 1, 1 ], # Gro
[ 1, 1, 1, .5, 2, 1, 2, 1, 1, 1, 1, 2, .5, 1, 1, 1, .5, 1, 1 ], # Fly
[ 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, .5, 1, 1, 1, 1, 0, .5, 1, 1 ], # Psy
[ 1, .5, 1, 1, 2, 1, .5, .5, 1, .5, 2, 1, 1, .5, 1, 2, .5, .5, 1 ], # Bug
[ 1, 2, 1, 1, 1, 2, .5, 1, .5, 2, 1, 2, 1, 1, 1, 1, .5, 1, 1 ], # Roc
[ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, .5, 1, 1, 1 ], # Gho
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, .5, 0, 1 ], # Dra
[ 1, 1, 1, 1, 1, 1, .5, 1, 1, 1, 2, 1, 1, 2, 1, .5, 1, .5, 1 ], # Dar
[ 1, .5, .5, .5, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, .5, 2, 1 ], # Ste
[ 1, .5, 1, 1, 1, 1, 2, .5, 1, 1, 1, 1, 1, 1, 2, 2, .5, 1, 1 ], # Fai
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] # ???
]

432
server/index.coffee Normal file
View File

@@ -0,0 +1,432 @@
http = require 'http'
Primus = require 'primus'
Emitter = require('primus-emitter')
express = require 'express'
path = require 'path'
{_} = require 'underscore'
{BattleServer} = require './server'
commands = require './commands'
auth = require('./auth')
generations = require './generations'
{Room} = require('./rooms')
errors = require '../shared/errors'
assets = require('../assets')
database = require('./database')
redis = require('./redis')
ratings = require('./ratings')
config = require('./config')
alts = require('./alts')
replays = require('./replays')
modify = require('./modify')
MAX_MESSAGE_LENGTH = 250
MAX_RANK_DISPLAYED = 100
# A MD5 hash of all the JavaScript files used by the client. This is passed to
# each new connection via the .jade template, and when the client connects. If
# the two versions differ, the server had restarted at some point and now is
# serving new client files.
CLIENT_VERSION = assets.getVersion()
@createServer = (port) ->
app = express()
httpServer = http.createServer(app)
primus = new Primus(httpServer, transformer: 'sockjs')
primus.use('emitter', Emitter)
primus.save(path.join(__dirname, "../client/vendor/js/primus.js"))
server = new BattleServer()
# Configuration
app.set("views", "client/templates")
app.set('view engine', 'jade')
app.use(express.logger()) if config.IS_LOCAL
app.use(express.compress()) # gzip
app.use(express.cookieParser())
app.use(auth.middleware())
app.use(express.methodOverride())
app.use(app.router)
app.use(express.static(path.join(__dirname, "../public"))) if config.IS_LOCAL
# Helpers
app.locals.asset_path = assets.getAbsolute
# Routing
renderHomepage = (req, res) ->
res.render('index.jade', user: req.user, CLIENT_VERSION: CLIENT_VERSION)
app.get("/", renderHomepage)
app.get("/battles/:id", renderHomepage)
app.get("/replays/:id", replays.routes.show)
app.delete("/replays/:id", replays.routes.destroy)
app.get("/replays", replays.routes.index)
app.get '/leaderboard', (req, res) ->
page = req.param('page')
perPage = req.param('per_page')
ratings.listRatings page, perPage, (err, results) ->
if err
res.json(500, err.message)
else
res.render('leaderboard.jade', players: results)
app.get '/leaderboard/json', (req, res) ->
page = req.param('page')
perPage = req.param('per_page')
ratings.listRatings page, perPage, (err, results) ->
if err
res.json(500, err.message)
else
res.json(players: results)
app.get '/pokeuse/json', (req, res) ->
user= req.user
res.json(400) if user.name != "Deukhoofd"
page = req.param('page')
perPage = req.param('per_page')
q = new database.Teams()
q = q.query('orderBy', 'created_at')
.fetch()
.then (teams) ->
teams = teams.toJSON()
pkmn = getpokeusedata(teams)
res.json(teams: pkmn)
getpokeusedata = (json) ->
occurences = []
for team in json
for pokemon in team.pokemon
specie = pokemon.species
if specie != null
inarr = _.where(occurences, {name: specie})
if inarr.length == 0
newobj = {name: specie, occurence: 1}
occurences.push(newobj)
else
inarr[0].occurence = inarr[0].occurence + 1
occurences = _.sortBy(occurences, "occurence").reverse()
totalpokemon = 0
for pkmn in occurences
totalpokemon += pkmn.occurence
for pkmn in occurences
pkmn.percentage = (pkmn.occurence/totalpokemon) * 100
return occurences
app.get '/pokeuse', (req, res) ->
modifyUsers = ["Deukhoofd", "Simply"]
app.get '/modify', (req, res) ->
if req.user.name not in modifyUsers
res.json(400)
else
res.render('modify.jade')
app.get '/modify/formes', (req, res) ->
if req.user.name not in modifyUsers
res.json(400)
else
FormeData = require("./in/data/data_formes.json")
res.render('modify/formes.jade', data: FormeData)
lobby = new Room("Lobby")
server.rooms.push(lobby)
# Start responding to websocket clients
primus.on 'connection', (spark) ->
spark.send('version', CLIENT_VERSION)
spark.on 'login', (id, token) ->
return unless _.isFinite(id)
return unless _.isString(token)
requ = spark.request
auth.matchToken requ, id, token, (err, json) ->
if err
return spark.send('errorMessage', errors.INVALID_SESSION)
auth.getBanTTL json.name, (err, ttl) ->
if err
return spark.send('errorMessage', errors.INVALID_SESSION)
else if ttl != -2 # -2 means the ban does not exist
auth.getBanReason json.name, (err, reason) ->
spark.send('errorMessage', errors.BANNED, reason, Number(ttl))
spark.end()
return
else
user = server.findOrCreateUser(json, spark)
if !user.name || !user.id
console.error("MISSING INFORMATION: #{json}")
spark.end()
return
attachEvents(user, spark)
server.join(spark)
spark.loggedIn = true
spark.send('loginSuccess')
lobby.add(spark)
# After stuff
alts.listUserAlts user.name, (err, alts) ->
spark.send('altList', alts)
primus.on 'error', (err) ->
console.error(err.message, err.stack)
primus.on 'disconnection', (spark) ->
return unless spark.loggedIn
server.leave(spark)
spark.emit("cancelFindBattle") unless spark.user.hasSparks()
attachEvents = (user, spark) ->
spark.on 'sendChat', (roomId, message) ->
return unless _.isString(message)
message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
return unless 0 < message.length < MAX_MESSAGE_LENGTH
return unless room = server.getRoom(roomId)
server.limit user, 'chat', max: 5, duration: 3000, (err, limit) ->
if err || limit.remaining == 0
server.runIfUnmuted user, roomId, ->
server.mute(user.name, '[AUTOMATED] Chat rate-limit.', 10 * 60)
room = server.getRoom(roomId)
room.announce('warning', "#{user.name} was automatically muted for 10 minutes.")
else if message[0] == '/' && message[1] == '/'
message = message[1...]
server.userMessage(user, room, message)
else if message[0] == '/'
command = message.replace(/\s+.*$/, '')
args = message.substr(command.length).replace(/^\s+/, '')
command = command.substr(1)
args = args.split(',')
commands.executeCommand(server, user, room, command, args...)
else
server.userMessage(user, room, message)
if limit.remaining == 1
user.announce(room.name, 'warning', 'You are chatting too fast. Please slow down.')
spark.on 'leaveChatroom', (roomId) ->
return unless _.isString(roomId)
server.getRoom(roomId)?.remove(spark)
#########
# TEAMS #
#########
# Takes a temporary id and team JSON. Saves to server, and returns the real
# unique id that was persisted onto the DB.
spark.on 'saveTeam', (team, callback) ->
return unless _.isObject(team)
return unless _.isFunction(callback)
attributes = _.pick(team, 'id', 'name', 'generation')
attributes['trainer_id'] = user.id
attributes['contents'] = JSON.stringify(team.pokemon)
new database.Team(attributes)
.save().then (team) ->
callback(team.id)
spark.on 'requestTeams', ->
q = new database.Teams()
if user.authority != auth.levels.OWNER
q = q.query('where', trainer_id: user.id)
q = q.query('orderBy', 'created_at')
.fetch()
.then (teams) ->
spark.send('receiveTeams', teams.toJSON())
spark.on 'destroyTeam', (teamId) ->
return unless _.isFinite(teamId)
attributes = {
id: teamId
}
attributes['trainer_id'] = user.id unless config.IS_LOCAL
database.Team.query().where(attributes).delete()
.then ->
# Do nothing, just execute the promise. We assume it was deleted.
return
.catch (err) ->
console.error(err)
####################
# PRIVATE MESSAGES #
####################
spark.on 'privateMessage', (toUser, message) ->
return unless _.isString(toUser)
return unless _.isString(message)
message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
return unless 0 < message.length < MAX_MESSAGE_LENGTH
if server.users.contains(toUser)
recipient = server.users.get(toUser)
recipient.send('privateMessage', user.name, user.name, message)
user.send('privateMessage', toUser, user.name, message)
else
user.error(errors.PRIVATE_MESSAGE, toUser, "This user is offline.")
##############
# CHALLENGES #
##############
spark.on 'challenge', (challengeeId, generation, team, conditions, altName) ->
return unless _.isString(challengeeId)
return unless _.isString(generation)
return unless _.isObject(team)
return unless _.isArray(conditions)
return unless !altName || _.isString(altName)
alts.isAltOwnedBy user.name, altName, (err, valid) ->
return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid
server.registerChallenge(user, challengeeId, generation, team, conditions, altName)
spark.on 'cancelChallenge', (challengeeId) ->
return unless _.isString(challengeeId)
server.cancelChallenge(user, challengeeId)
spark.on 'acceptChallenge', (challengerId, team, altName) ->
return unless _.isString(challengerId)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
alts.isAltOwnedBy user.name, altName, (err, valid) ->
return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid
server.acceptChallenge(user, challengerId, team, altName)
spark.on 'rejectChallenge', (challengerId) ->
return unless _.isString(challengerId)
server.rejectChallenge(user, challengerId)
########
# ALTS #
########
spark.on 'createAlt', (altName) ->
altName = String(altName).trim()
if !alts.isAltNameValid(altName)
return user.error(errors.INVALID_ALT_NAME, "Invalid Alt Name")
alts.createAlt user.name, altName, (err, success) ->
return user.error(errors.INVALID_ALT_NAME, err.message) if err
user.send('altCreated', altName) if success
###########
# REPLAYS #
###########
spark.on 'saveReplay', (battleId, callback) ->
battle = server.findBattle(battleId)
return callback?("The battle could not be found.") unless battle
return callback?("The battle is not yet done.") unless battle.isOver()
replays.create(user, battle.battle) # unwrap the facade
.then((replayId) -> callback?(null, replayId))
.catch replays.TooManyBattlesSaved, (err) ->
callback?(err.message)
.catch (err) ->
callback?('Something went wrong saving the replay.')
console.error(err.message)
console.error(err.stack)
###########
# BATTLES #
###########
spark.on 'getBattleList', (callback) ->
return unless _.isFunction(callback)
# TODO: Make this more efficient
# TODO: Order by age
# NOTE: Cache this? Even something like a 5 second expiration
# may improve server performance greatly
currentTime = Date.now()
battleMetadata = ([
controller.battle.id,
controller.battle.playerNames[0],
controller.battle.playerNames[1],
currentTime - controller.battle.createdAt
] for controller in server.getOngoingBattles())
callback(battleMetadata)
spark.on 'findBattle', (format, team, altName=null) ->
return unless _.isString(format)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
# Note: If altName == null, then isAltOwnedBy will return true
alts.isAltOwnedBy user.name, altName, (err, valid) ->
if not valid
user.error(errors.INVALID_ALT_NAME, "You do not own this alt")
else
validationErrors = server.queuePlayer(user.name, team, format, altName)
if validationErrors.length > 0
user.error(errors.FIND_BATTLE, validationErrors)
spark.on 'cancelFindBattle', ->
server.removePlayer(user.name)
user.send("findBattleCanceled")
spark.on 'sendMove', (battleId, moveName, slot, forTurn, options, callback) ->
return unless _.isString(moveName)
return unless _.isFinite(slot)
return unless _.isFinite(forTurn)
return unless !options || _.isObject(options)
return unless _.isFunction(callback)
if battle = server.findBattle(battleId)
battle.makeMove(user.name, moveName, slot, forTurn, options)
callback()
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'sendSwitch', (battleId, toSlot, fromSlot, forTurn, callback) ->
return unless _.isFinite(toSlot)
return unless _.isFinite(fromSlot)
return unless _.isFinite(forTurn)
return unless _.isFunction(callback)
if battle = server.findBattle(battleId)
battle.makeSwitch(user.name, toSlot, fromSlot, forTurn)
callback()
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'sendCancelAction', (battleId, forTurn) ->
return unless _.isFinite(forTurn)
if battle = server.findBattle(battleId)
battle.undoCompletedRequest(user.name, forTurn)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'arrangeTeam', (battleId, arrangement) ->
return unless _.isArray(arrangement)
if battle = server.findBattle(battleId)
battle.arrangeTeam(user.name, arrangement)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'spectateBattle', (battleId) ->
if battle = server.findBattle(battleId)
battle.add(spark)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'forfeit', (battleId) ->
if battle = server.findBattle(battleId)
battle.forfeit(user.name)
else
user.error(errors.BATTLE_DNE, battleId)
battleSearch = ->
server.beginBattles (err, battleIds) ->
if err then return
for id in battleIds
battle = server.findBattle(id)
playerIds = battle.getPlayerIds()
ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey)
ratings.getRanks ratingKeys, (err, fullRanks) ->
ranks = _.compact(fullRanks)
return unless ranks.length == fullRanks.length
if 1 <= Math.max(ranks...) <= MAX_RANK_DISPLAYED
playerNames = battle.getPlayerNames()
playerNames = playerNames.map((p, i) -> "#{p} (Rank ##{ranks[i]})")
message = """A high-level match is being played!
<span class="fake_link spectate" data-battle-id="#{id}">
#{playerNames.join(" vs. ")}</span>!"""
lobby.message(message)
setTimeout(battleSearch, 5 * 1000)
battleSearch()
httpServer.listen(port)
primus

30
server/logger.coffee Normal file
View File

@@ -0,0 +1,30 @@
{_} = require 'underscore'
LOG_KEY = "log"
LOG_MAX = 100
redis = require('./redis')
log = (message, context={}, next) ->
context = _.extend({}, @context, context)
obj = {message, context}
objStr = JSON.stringify(obj)
redis.lpush LOG_KEY, objStr, (err) ->
return next(err) if err
redis.ltrim LOG_KEY, 0, LOG_MAX, (err, count) ->
return next(err) if err
next() if err
return objStr
withContext = (context) ->
context = _.extend({}, @context, context)
return {
context: context
log: log
withContext: withContext
}
@log = log
@withContext = withContext

5
server/modify.coffee Normal file
View File

@@ -0,0 +1,5 @@
{_} = require('underscore')
formes = (req, res) ->
console.log("1")

31
server/player.coffee Normal file
View File

@@ -0,0 +1,31 @@
class @Player
constructor: (@user) ->
@id = @user.id
@queue = []
tell: (args...) ->
@queue.push(args)
attachToTeam: (attachment) ->
@team.attach(attachment, team: @team)
shouldBlockFieldExecution: (move, user) ->
@team.shouldBlockFieldExecution(move, user)
has: (attachment) ->
@team.has(attachment)
get: (attachment) ->
@team.get(attachment)
switch: (pokemon, toIndex) ->
@team.switch(pokemon, toIndex)
send: (args...) ->
@user.send? args...
toJSON: ->
if @user.toJSON
@user.toJSON()
else
{@id}

134
server/queue.coffee Normal file
View File

@@ -0,0 +1,134 @@
ratings = require('./ratings')
alts = require('./alts')
INITIAL_RANGE = 100
RANGE_INCREMENT = 100
class QueuedPlayer
constructor: (player) ->
@player = player
@range = INITIAL_RANGE
@rating = null # needs to be updated by getRatings
intersectsWith: (other) ->
leftMin = @rating - (@range / 2)
leftMax = @rating + (@range / 2)
rightMin = other.rating - (other.range / 2)
rightMax = other.rating + (other.range / 2)
return false if leftMin > rightMax
return false if leftMax < rightMin
true
# A queue of users waiting for a battle
class @BattleQueue
constructor: ->
@queue = {}
@newPlayers = []
@recentlyMatched = {}
@length = 0
# Adds a player to the queue.
# "name" can either be the real name, or an alt
add: (playerId, name, team, ratingKey=playerId) ->
return false if !playerId
return false if playerId of @queue
playerObject = {id: playerId, name, team, ratingKey}
player = new QueuedPlayer(playerObject)
@queue[playerId] = player
@newPlayers.push(player)
@length += 1
return true
remove: (playerIds) ->
playerIds = Array(playerIds) if playerIds not instanceof Array
for playerId in playerIds
if playerId of @queue
delete @queue[playerId]
@length -= 1
queuedPlayers: ->
Object.keys(@queue)
hasUserId: (playerId) ->
@queue[playerId]?
hasRecentlyMatched: (player1Id, player2Id) ->
players = [player1Id, player2Id].sort()
key = "#{players[0]}:#{players[1]}"
@recentlyMatched[key]?
addRecentMatch: (player1Id, player2Id) ->
players = [player1Id, player2Id].sort()
key = "#{players[0]}:#{players[1]}"
@recentlyMatched[key] = true
setTimeout((=> delete @recentlyMatched[key]), 30 * 60 * 1000) # expire in 30 minutes
size: ->
@length
# An internal function which loads ratings for newly queued players
# and removes them from the newly queued list
updateNewPlayers: (next) ->
ratingKeys = (queued.player.ratingKey for queued in @newPlayers)
return next(null) if ratingKeys.length == 0
ratings.getRatings ratingKeys, (err, returnedRatings) =>
if err then return next(err)
ratings.setActive ratingKeys, (err) =>
if err then return next(err)
# Update the ratings in the player objects
for rating, i in returnedRatings
continue unless @hasUserId(@newPlayers[i].player.id)
@newPlayers[i].rating = rating
# reset the new players list, we're done
@newPlayers.splice(0, @newPlayers.length)
next(null)
# Returns an array of pairs. Each pair is a queue object that contains
# a player and team key, corresponding to the player socket and player's team.
pairPlayers: (next) ->
return next(null, []) if @size() == 0
@updateNewPlayers (err) =>
if err then return next(err, null)
sortedPlayers = (queued for id, queued of @queue)
sortedPlayers.sort((a, b) -> a.rating - b.rating)
alreadyMatched = (false for [0...sortedPlayers.length])
pairs = []
for leftIdx in [0...sortedPlayers.length]
continue if alreadyMatched[leftIdx]
for rightIdx in [(leftIdx + 1)...sortedPlayers.length]
continue if alreadyMatched[rightIdx]
left = sortedPlayers[leftIdx]
right = sortedPlayers[rightIdx]
leftPlayer = left.player
rightPlayer = right.player
# Continue if these two players already played
continue if @hasRecentlyMatched(leftPlayer.id, rightPlayer.id)
# If the rating difference is too large break out, we have no possible match for left
break unless left.intersectsWith(right)
# Everything checks out, so make the pair and break out
pairs.push([leftPlayer, rightPlayer])
@remove([leftPlayer.id, rightPlayer.id])
@addRecentMatch(leftPlayer.id, rightPlayer.id)
alreadyMatched[leftIdx] = alreadyMatched[rightIdx] = true
break
# Expand the range of all unmatched players
queued.range += RANGE_INCREMENT for id, queued of @queue
# Return the list of paired players
next(null, pairs)

239
server/ratings.coffee Normal file
View File

@@ -0,0 +1,239 @@
redis = require './redis'
async = require 'async'
alts = require './alts'
@algorithm = require('./elo')
@DECAY_AMOUNT = 5
DEFAULT_PLAYER = @algorithm.createPlayer()
USERS_RATED_KEY = "users:rated"
USERS_ACTIVE_KEY = "users:active"
RATINGS_KEY = "ratings"
RATINGS_ATTRIBUTES = Object.keys(DEFAULT_PLAYER)
RATINGS_SUBKEYS = {}
for attribute in RATINGS_ATTRIBUTES
RATINGS_SUBKEYS[attribute] = [RATINGS_KEY, attribute].join(':')
RATINGS_MAXKEY = "ratings:max"
RATINGS_PER_PAGE = 15
ALGORITHM_OPTIONS =
systemConstant: 0.2 # Glicko2 tau
@DEFAULT_RATING = DEFAULT_PLAYER['rating']
@results =
WIN : 1
DRAW : 0.5 # In earlier generations, it's possible to draw.
LOSE : 0
@setActive = (idArray, next) ->
idArray = [idArray] if idArray not instanceof Array
idArray = idArray.map((id) -> id.toLowerCase())
redis.sadd(USERS_ACTIVE_KEY, idArray, next)
RATIOS_KEY = 'ratios'
RATIOS_ATTRIBUTES = Object.keys(@results).map((key) -> key.toLowerCase())
RATIOS_SUBKEYS = {}
for attribute in RATIOS_ATTRIBUTES
RATIOS_SUBKEYS[attribute] = [RATIOS_KEY, attribute].join(':')
RATIOS_STREAK_KEY = "#{RATIOS_KEY}:streak"
RATIOS_MAXSTREAK_KEY = "#{RATIOS_KEY}:maxstreak"
# Used internally by the ratings system to update
# the max rating of user when a rating changes
# Id can either be the actual id, or an alt id
updateMaxRating = (id, next) =>
id = alts.getIdOwner(id).toLowerCase()
alts.listUserAlts id, (err, altNames) =>
return next(err) if err
# Retrieve a list of all rating Keys
ids = (alts.uniqueId(id, name) for name in altNames)
ids.push(id)
@getRatings ids, (err, results) ->
return next(err) if err
redis.zadd(RATINGS_MAXKEY, Math.max(results...), id)
next(null)
# Update the max ratings for multiple players
updateMaxRatings = (ids, next) ->
ops = ids.map (id) ->
(callback) -> updateMaxRating(id, callback)
async.parallel ops, next
updateMaxStreak = (id, next) =>
@getStreak id, (err, results) ->
return next(err) if err
if results.streak > results.maxStreak
redis.hmset RATIOS_MAXSTREAK_KEY, id, results.streak, next
else
next(err)
@getPlayer = (id, next) ->
id = id.toLowerCase()
multi = redis.multi()
for attribute in RATINGS_ATTRIBUTES
multi = multi.zscore(RATINGS_SUBKEYS[attribute], id)
multi.exec (err, results) ->
return next(err) if err
object = {}
for value, i in results
attribute = RATINGS_ATTRIBUTES[i]
value ||= DEFAULT_PLAYER[attribute]
object[attribute] = Number(value)
return next(null, object)
@getRating = (id, next) ->
id = id.toLowerCase()
exports.getPlayer id, (err, player) ->
return next(err) if err
return next(null, Number(player.rating))
# Returns the maximum rating for a user among that user and his/her alts
@getMaxRating = (id, next) ->
id = id.toLowerCase()
redis.zscore RATINGS_MAXKEY, id, (err, rating) ->
return next(err) if err
rating ||= 0
next(null, Number(rating))
@setRating = (id, newRating, next) =>
@setRatings([id], [newRating], next)
@setRatings = (idArray, newRatingArray, next) ->
idArray = idArray.map((id) -> id.toLowerCase())
multi = redis.multi()
multi = multi.sadd(USERS_RATED_KEY, idArray)
for id, i in idArray
newRating = newRatingArray[i]
multi = multi.zadd(RATINGS_SUBKEYS['rating'], newRating, id)
multi.exec (err) ->
return next(err) if err
updateMaxRatings(idArray, next)
@getPlayers = (idArray, next) ->
idArray = idArray.map((id) -> id.toLowerCase())
callbacks = idArray.map (id) =>
(callback) => @getPlayer(id, callback)
async.parallel(callbacks, next)
@getRatings = (idArray, next) ->
idArray = idArray.map((id) -> id.toLowerCase())
exports.getPlayers idArray, (err, players) ->
return next(err) if err
return next(null, players.map((p) -> Number(p.rating)))
@getRank = (id, next) ->
id = id.toLowerCase()
redis.zrevrank RATINGS_SUBKEYS['rating'], id, (err, rank) ->
return next(err) if err
return next(null, null) if !rank?
return next(null, rank + 1) # rank starts at 0
@getRanks = (idArray, next) ->
idArray = idArray.map((id) -> id.toLowerCase())
multi = redis.multi()
for id in idArray
multi = multi.zrevrank(RATINGS_SUBKEYS['rating'], id)
multi.exec (err, ranks) ->
return next(err) if err
ranks = ranks.map (rank) ->
# Rank starts at 0, but it can also be null (doesn't exist).
if rank? then rank + 1 else null
next(null, ranks)
@updatePlayer = (id, score, object, next) ->
id = id.toLowerCase()
multi = redis.multi()
attribute = switch score
when 1 then 'win'
when 0 then 'lose'
else 'draw'
multi = multi.hincrby(RATIOS_SUBKEYS[attribute], id, 1)
if attribute == "win"
multi = multi.hincrby(RATIOS_STREAK_KEY, id, 1)
else
multi = multi.hset(RATIOS_STREAK_KEY, id, 0)
multi = multi.sadd(USERS_RATED_KEY, id)
for attribute in RATINGS_ATTRIBUTES
value = object[attribute]
multi = multi.zadd(RATINGS_SUBKEYS[attribute], value, id)
multi.exec (err) ->
return next(err) if err
async.parallel([
updateMaxRating.bind(this, id)
updateMaxStreak.bind(this, id)
], next)
@updatePlayers = (id, opponentId, score, next) ->
if score < 0 || score > 1
return next(new Error("Invalid match result: #{score}"))
id = id.toLowerCase()
opponentId = opponentId.toLowerCase()
opponentScore = 1.0 - score
exports.getPlayers [id, opponentId], (err, results) =>
return next(err) if err
[player, opponent] = results
winnerMatches = [{opponent, score}]
loserMatches = [{opponent: player, score: opponentScore}]
newWinner = exports.algorithm.calculate(player, winnerMatches, ALGORITHM_OPTIONS)
newLoser = exports.algorithm.calculate(opponent, loserMatches, ALGORITHM_OPTIONS)
async.parallel [
@updatePlayer.bind(this, id, score, newWinner)
@updatePlayer.bind(this, opponentId, opponentScore, newLoser)
], (err, results) =>
return next(err) if err
@getRatings([id, opponentId], next)
@resetRating = (id, next) ->
@resetRatings([id], next)
@resetRatings = (idArray, next) ->
idArray = idArray.map((id) -> id.toLowerCase())
multi = redis.multi()
multi = multi.srem(USERS_RATED_KEY, idArray)
for attribute, key of RATINGS_SUBKEYS
multi = multi.zrem(key, idArray)
multi.exec (err) ->
return next(err) if err
updateMaxRatings(idArray, next)
@listRatings = (page = 1, perPage = RATINGS_PER_PAGE, next) ->
if arguments.length == 2 && typeof perPage == 'function'
[perPage, next] = [RATINGS_PER_PAGE, perPage]
page -= 1
start = page * perPage
end = start + (perPage - 1)
redis.zrevrange RATINGS_MAXKEY, start, end, 'WITHSCORES', (err, r) ->
return next(err) if err
array = []
for i in [0...r.length] by 2
username = r[i]
score = Number(r[i + 1]) # redis returns scores as strings
array.push(username: username, score: score)
next(null, array)
@getRatio = (id, next) ->
id = id.toLowerCase()
multi = redis.multi()
for attribute, key of RATIOS_SUBKEYS
multi = multi.hget(key, id)
multi.exec (err, results) ->
return next(err) if err
hash = {}
for attribute, i in RATIOS_ATTRIBUTES
hash[attribute] = Number(results[i]) || 0
return next(null, hash)
@getStreak = (id, next) ->
id = id.toLowerCase()
multi = redis.multi()
multi = multi.hget(RATIOS_STREAK_KEY, id)
multi = multi.hget(RATIOS_MAXSTREAK_KEY, id)
multi.exec (err, results) ->
next(null, {streak: results[0], maxStreak: results[1]})

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 @@
{@FormeData, @SpeciesData} = require('./pokemon')

165
server/rb/data/moves.yml Normal file
View File

@@ -0,0 +1,165 @@
absorb: {power: 20, pp: 25, type: grass}
acid: {power: 40, pp: 30, type: poison}
acid-armor: {power: 0, pp: 40, type: poison}
agility: {power: 0, pp: 30, type: psychic}
amnesia: {power: 0, pp: 20, type: psychic}
aurora-beam: {power: 65, pp: 20, type: ice}
barrage: {power: 15, pp: 20, type: normal}
barrier: {power: 0, pp: 30, type: psychic}
bide: {power: 1, pp: 10, type: normal}
bind: {power: 15, pp: 20, type: normal}
bite: {power: 60, pp: 25, type: dark}
blizzard: {power: 120, pp: 5, type: ice}
body-slam: {power: 85, pp: 15, type: normal}
bone-club: {power: 65, pp: 20, type: ground}
bonemerang: {power: 50, pp: 10, type: ground}
bubble: {power: 20, pp: 30, type: water}
bubblebeam: {power: 65, pp: 20, type: water}
clamp: {power: 35, pp: 15, type: water}
comet-punch: {power: 18, pp: 15, type: normal}
confuse-ray: {power: 0, pp: 10, type: ghost}
confusion: {power: 50, pp: 25, type: psychic}
constrict: {power: 10, pp: 35, type: normal}
conversion: {power: 0, pp: 30, type: normal}
counter: {power: 1, pp: 20, type: fighting}
crabhammer: {power: 90, pp: 10, type: water}
cut: {power: 50, pp: 30, type: normal}
defense-curl: {power: 0, pp: 40, type: normal}
dig: {power: 80, pp: 10, type: ground}
disable: {power: 0, pp: 20, type: normal}
dizzy-punch: {power: 70, pp: 10, type: normal}
double-edge: {power: 120, pp: 15, type: normal}
double-kick: {power: 30, pp: 30, type: fighting}
double-team: {power: 0, pp: 15, type: normal}
doubleslap: {power: 15, pp: 10, type: normal}
dragon-rage: {power: 1, pp: 10, type: dragon}
dream-eater: {power: 100, pp: 15, type: psychic}
drill-peck: {power: 80, pp: 20, type: flying}
earthquake: {power: 100, pp: 10, type: ground}
egg-bomb: {power: 100, pp: 10, type: normal}
ember: {power: 40, pp: 25, type: fire}
explosion: {power: 250, pp: 5, type: normal}
fire-blast: {power: 120, pp: 5, type: fire}
fire-punch: {power: 75, pp: 15, type: fire}
fire-spin: {power: 35, pp: 15, type: fire}
fissure: {power: 1, pp: 5, type: ground}
flamethrower: {power: 95, pp: 15, type: fire}
flash: {power: 0, pp: 20, type: normal}
fly: {power: 90, pp: 15, type: flying}
focus-energy: {power: 0, pp: 30, type: normal}
fury-attack: {power: 15, pp: 20, type: normal}
fury-swipes: {power: 18, pp: 15, type: normal}
glare: {power: 0, pp: 30, type: normal}
growl: {power: 0, pp: 40, type: normal}
growth: {power: 0, pp: 40, type: normal}
guillotine: {power: 1, pp: 5, type: normal}
gust: {power: 40, pp: 35, type: flying}
harden: {power: 0, pp: 30, type: normal}
haze: {power: 0, pp: 30, type: ice}
headbutt: {power: 70, pp: 15, type: normal}
hi-jump-kick: {power: 130, pp: 10, type: fighting}
horn-attack: {power: 65, pp: 25, type: normal}
horn-drill: {power: 1, pp: 5, type: normal}
hydro-pump: {power: 120, pp: 5, type: water}
hyper-beam: {power: 150, pp: 5, type: normal}
hyper-fang: {power: 80, pp: 15, type: normal}
hypnosis: {power: 0, pp: 20, type: psychic}
ice-beam: {power: 95, pp: 10, type: ice}
ice-punch: {power: 75, pp: 15, type: ice}
jump-kick: {power: 100, pp: 10, type: fighting}
karate-chop: {power: 50, pp: 25, type: fighting}
kinesis: {power: 0, pp: 15, type: psychic}
leech-life: {power: 20, pp: 15, type: bug}
leech-seed: {power: 0, pp: 10, type: grass}
leer: {power: 0, pp: 30, type: normal}
lick: {power: 20, pp: 30, type: ghost}
light-screen: {power: 0, pp: 30, type: psychic}
lovely-kiss: {power: 0, pp: 10, type: normal}
low-kick: {power: 1, pp: 20, type: fighting}
meditate: {power: 0, pp: 40, type: psychic}
mega-drain: {power: 40, pp: 15, type: grass}
mega-kick: {power: 120, pp: 5, type: normal}
mega-punch: {power: 80, pp: 20, type: normal}
metronome: {power: 0, pp: 10, type: normal}
mimic: {power: 0, pp: 10, type: normal}
minimize: {power: 0, pp: 20, type: normal}
mirror-move: {power: 0, pp: 20, type: flying}
mist: {power: 0, pp: 30, type: ice}
night-shade: {power: 1, pp: 15, type: ghost}
pay-day: {power: 40, pp: 20, type: normal}
peck: {power: 35, pp: 35, type: flying}
petal-dance: {power: 120, pp: 10, type: grass}
pin-missile: {power: 14, pp: 20, type: bug}
poison-gas: {power: 0, pp: 40, type: poison}
poison-sting: {power: 15, pp: 35, type: poison}
poisonpowder: {power: 0, pp: 35, type: poison}
pound: {power: 40, pp: 35, type: normal}
psybeam: {power: 65, pp: 20, type: psychic}
psychic: {power: 90, pp: 10, type: psychic}
psywave: {power: 1, pp: 15, type: psychic}
quick-attack: {power: 40, pp: 30, type: normal}
rage: {power: 20, pp: 20, type: normal}
razor-leaf: {power: 55, pp: 25, type: grass}
razor-wind: {power: 80, pp: 10, type: normal}
recover: {power: 0, pp: 10, type: normal}
reflect: {power: 0, pp: 20, type: psychic}
rest: {power: 0, pp: 10, type: psychic}
roar: {power: 0, pp: 20, type: normal}
rock-slide: {power: 75, pp: 10, type: rock}
rock-throw: {power: 50, pp: 15, type: rock}
rolling-kick: {power: 60, pp: 15, type: fighting}
sand-attack: {power: 0, pp: 15, type: ground}
scratch: {power: 40, pp: 35, type: normal}
screech: {power: 0, pp: 40, type: normal}
seismic-toss: {power: 1, pp: 20, type: fighting}
selfdestruct: {power: 200, pp: 5, type: normal}
sharpen: {power: 0, pp: 30, type: normal}
sing: {power: 0, pp: 15, type: normal}
skull-bash: {power: 100, pp: 15, type: normal}
sky-attack: {power: 140, pp: 5, type: flying}
slam: {power: 80, pp: 20, type: normal}
slash: {power: 70, pp: 20, type: normal}
sleep-powder: {power: 0, pp: 15, type: grass}
sludge: {power: 65, pp: 20, type: poison}
smog: {power: 20, pp: 20, type: poison}
smokescreen: {power: 0, pp: 20, type: normal}
softboiled: {power: 0, pp: 10, type: normal}
solarbeam: {power: 120, pp: 10, type: grass}
sonicboom: {power: 1, pp: 20, type: normal}
spike-cannon: {power: 20, pp: 15, type: normal}
splash: {power: 0, pp: 40, type: normal}
spore: {power: 0, pp: 15, type: grass}
stomp: {power: 65, pp: 20, type: normal}
strength: {power: 80, pp: 15, type: normal}
string-shot: {power: 0, pp: 40, type: bug}
struggle: {power: 50, pp: null, type: normal}
stun-spore: {power: 0, pp: 30, type: grass}
submission: {power: 80, pp: 25, type: fighting}
substitute: {power: 0, pp: 10, type: normal}
super-fang: {power: 1, pp: 10, type: normal}
supersonic: {power: 0, pp: 20, type: normal}
surf: {power: 95, pp: 15, type: water}
swift: {power: 60, pp: 20, type: normal}
swords-dance: {power: 0, pp: 30, type: normal}
tackle: {power: 50, pp: 35, type: normal}
tail-whip: {power: 0, pp: 30, type: normal}
take-down: {power: 90, pp: 20, type: normal}
teleport: {power: 0, pp: 20, type: psychic}
thrash: {power: 120, pp: 10, type: normal}
thunder: {power: 120, pp: 10, type: electric}
thunder-wave: {power: 0, pp: 20, type: electric}
thunderbolt: {power: 95, pp: 15, type: electric}
thunderpunch: {power: 75, pp: 15, type: electric}
thundershock: {power: 40, pp: 30, type: electric}
toxic: {power: 0, pp: 10, type: poison}
transform: {power: 0, pp: 10, type: normal}
tri-attack: {power: 80, pp: 10, type: normal}
twineedle: {power: 25, pp: 20, type: bug}
vicegrip: {power: 55, pp: 30, type: normal}
vine-whip: {power: 35, pp: 15, type: grass}
water-gun: {power: 40, pp: 25, type: water}
waterfall: {power: 80, pp: 15, type: water}
whirlwind: {power: 0, pp: 20, type: normal}
wing-attack: {power: 60, pp: 35, type: flying}
withdraw: {power: 0, pp: 40, type: water}
wrap: {power: 15, pp: 20, type: normal}

View File

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

27
server/redis.coffee Normal file
View File

@@ -0,0 +1,27 @@
redis = require 'redis'
# Connect to redis
if process.env.REDIS_DB_URL
parts = require("url").parse(process.env.REDIS_DB_URL)
db = redis.createClient(parts.port, parts.hostname)
db.auth(parts.auth.split(":")[1]) if parts.auth
else
db = redis.createClient()
db.on 'error', (err) ->
console.error(err.stack)
if process.env.NODE_ENV == 'test'
# Select test database
db.select(1)
db.shard = (command, key, id, args...) ->
if command[0] != 'h'
throw new Error('Sharding does not work with non-hashes.')
[id, size] = [Number(id), 512]
[division, remainder] = [ Math.floor(id / size), (id % size) ]
key = [ key, division ].join(':')
db[command](key, remainder, args...)
# Export database variable
module.exports = db

87
server/replays.coffee Normal file
View File

@@ -0,0 +1,87 @@
{_} = require('underscore')
database = require('./database')
assets = require('../assets')
@MAX_SAVED_BATTLES = 15
class TooManyBattlesSaved extends Error
constructor: (count) ->
@name = "TooManyBattlesSaved"
@message = "You have already saved #{count} replays. The maximum is #{exports.MAX_SAVED_BATTLES}."
Error.captureStackTrace(this, TooManyBattlesSaved)
exports.TooManyBattlesSaved = TooManyBattlesSaved
@routes =
show: (req, res) ->
database.Battle
.where(battle_id: req.params.id)
.fetch()
.then (replay) ->
res.render('replays/show', bodyClass: 'no-sidebar', replay: replay)
.catch (err) ->
console.error(err.stack)
res.render('replays/show', bodyClass: 'no-sidebar', replay: null)
index: (req, res) ->
console.log(req.user)
q = new database.SavedBattles
q= q.query()
q = q.where(user_id: req.user.id) if req.user.name != "Deukhoofd"
q= q.select('battle_id')
.then (battleIds) ->
battleIds = _.pluck(battleIds, 'battle_id')
if battleIds.length > 0
database.Battle
.where('battle_id', 'in', battleIds)
.fetchAll(columns: ['battle_id', 'players', 'name', 'format', 'created_at'])
else
[]
.then (replays) ->
res.render('replays/index', bodyClass: 'no-sidebar', replays: replays)
.catch (err) ->
console.error(err.stack)
res.render('replays/index', bodyClass: 'no-sidebar', replays: [])
destroy: (req, res) ->
battleId = req.param('id')
database.SavedBattle.query()
.where(user_id: req.user.id, battle_id: battleId)
.delete()
.then ->
database.knex(database.SavedBattle::tableName)
.where(battle_id: battleId).count('*')
.then (results) ->
# If no more saves of this replay exist, delete the replay itself.
if Number(results[0].count) == 0
database.Battle.query().where(battle_id: battleId).delete()
.then ->
res.json(ok: true)
.catch (err) ->
console.error(err.message)
console.error(err.stack)
res.json(ok: false)
@create = (user, battle) ->
database.knex(database.SavedBattle::tableName)
.where(user_id: user.id).count('*')
.then (results) ->
count = Number(results[0].count)
if count >= exports.MAX_SAVED_BATTLES
throw new exports.TooManyBattlesSaved(count)
.then ->
new database.Battle({
battle_id: battle.id
format: battle.format
num_active: battle.numActive
players: battle.playerNames.join(',')
contents: JSON.stringify(battle.log)
versions: assets.asHash()
}).save().catch (err) ->
throw err unless /violates unique constraint/.test(err.message)
.then ->
new database.SavedBattle(user_id: user.id, battle_id: battle.id)
.save().catch (err) ->
throw err unless /violates unique constraint/.test(err.message)
.then ->
battle.id

65
server/rooms.coffee Normal file
View File

@@ -0,0 +1,65 @@
{_} = require('underscore')
{EventEmitter} = require('events')
redis = require('./redis')
errors = require('../shared/errors')
class @Room extends EventEmitter
constructor: (@name) ->
@users = {}
@userCounts = {}
@sparks = []
add: (spark) ->
return if spark in @sparks
@send('joinChatroom', @name, @userJSON(spark.user)) unless @users[spark.user.id]
@sparks.push(spark)
userId = spark.user.id
if userId not of @users
@userCounts[userId] = 1
@users[userId] = spark.user
else
@userCounts[userId] += 1
spark.send('listChatroom', @name, @toJSON())
remove: (spark) ->
index = @sparks.indexOf(spark)
return if index == -1
@sparks.splice(index, 1)
userId = spark.user.id
@userCounts[userId] -= 1
if @userCounts[userId] == 0
@send('leaveChatroom', @name, @transformName(spark.user.name))
delete @users[userId]
delete @userCounts[userId]
userMessage: (user, message) ->
@send('userMessage', @name, @transformName(user.name), message)
message: (message) ->
@send('rawMessage', @name, message)
announce: (klass, message) ->
@send('announce', @name, klass, message)
send: ->
user.send.apply(user, arguments) for name, user of @users
userJSON: (user) ->
json = user.toJSON(alt: @transformName(user.name))
# Hook to transform a user's name to something else. Does the identity func.
transformName: (name) ->
name
# Set the room's topic. Does not work for battle rooms.
# TODO: Or rather, it shouldn't work for battle rooms. Once a distinction is
# possible, block it for battle rooms
setTopic: (topic) ->
redis.hset "topic", "main", topic
@send('topic', topic) if topic
toJSON: ->
for name, user of @users
@userJSON(user)

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 @@
{@FormeData, @SpeciesData} = require('./pokemon')

View File

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

31
server/schedule.coffee Normal file
View File

@@ -0,0 +1,31 @@
schedule = require('node-schedule')
ratings = require('./ratings')
redis = require('./redis')
DEFAULT_RATING = ratings.algorithm.createPlayer().rating
@createScheduler = ->
jobs = []
# Artificial Elo decay per day
jobs.push schedule.scheduleJob hour: 0, minute: 0, second: 0, ->
# TODO: Turn into a lua script
job = this
multi = redis.multi()
multi = multi.sdiff('users:rated', 'users:active')
multi = multi.del('users:active')
multi.exec (err, results) ->
throw new Error(err) if err
[ids, didDelete] = results
return job.emit('finished') if ids.length == 0
ratings.getRatings ids, (err, oldRatings) ->
throw new Error(err) if err
newRatings = oldRatings.map (rating) ->
if rating < DEFAULT_RATING
rating
else
Math.max(rating - ratings.DECAY_AMOUNT, DEFAULT_RATING)
ratings.setRatings ids, newRatings, (err) ->
throw new Error(err) if err
job.emit('finished')
return jobs

483
server/server.coffee Normal file
View File

@@ -0,0 +1,483 @@
{createHmac} = require 'crypto'
{_} = require 'underscore'
Limiter = require 'ratelimiter'
{User} = require('./user')
{BattleQueue} = require './queue'
{UserStore} = require './user_store'
async = require('async')
gen = require './generations'
auth = require('./auth')
learnsets = require '../shared/learnsets'
{Conditions, SelectableConditions, Formats, DEFAULT_FORMAT} = require '../shared/conditions'
pbv = require '../shared/pokebattle_values'
config = require './config'
errors = require '../shared/errors'
redis = require('./redis')
alts = require './alts'
achievements = require './achievements'
FIND_BATTLE_CONDITIONS = [
Conditions.TEAM_PREVIEW
Conditions.RATED_BATTLE
Conditions.TIMED_BATTLE
Conditions.SLEEP_CLAUSE
Conditions.EVASION_CLAUSE
Conditions.SPECIES_CLAUSE
Conditions.PRANKSTER_SWAGGER_CLAUSE
Conditions.OHKO_CLAUSE
Conditions.UNRELEASED_BAN
]
MAX_NICKNAME_LENGTH = 15
class @BattleServer
constructor: ->
@queues = {}
for format of Formats
@queues[format] = new BattleQueue()
@battles = {}
# A hash mapping users to battles.
@userBattles = {}
# same as user battles, but indexed by name and does not include alts
@visibleUserBattles = {}
# A hash mapping user ids to challenges
# challenges[challengeeId][challengerId] = {generation: 'xy', team: []}
@challenges = {}
# A hash mapping ids to users
@users = new UserStore()
@rooms = []
# rate limiters
@limiters = {}
# Battles can start.
@unlockdown()
hasRoom: (roomId) ->
!!@getRoom(roomId)
getRoom: (roomId) ->
_.find(@rooms, (room) -> room.name == roomId)
# Creates a new user or finds an existing one, and adds a spark to it
findOrCreateUser: (json, spark) ->
user = @users.get(json.name)
user = @users.add(json, spark)
user
getUser: (userId) ->
@users.get(userId)
join: (spark) ->
@showTopic(spark)
for battleId of @userBattles[spark.user.name]
battle = @battles[battleId]
battle.add(spark)
battle.sendRequestTo(spark.user.name)
battle.sendUpdates()
@limiters[spark.user.id] ?= {}
return spark
leave: (spark) ->
for room in @rooms
room.remove(spark)
@users.remove(spark)
return if spark.user.hasSparks()
delete @limiters[spark.user.id]
@stopChallenges(spark.user)
showTopic: (player) ->
redis.hget "topic", "main", (err, topic) ->
player.send('topic', topic) if topic
registerChallenge: (player, challengeeId, format, team, conditions, altName) ->
if @isLockedDown()
errorMessage = "The server is locked. No new battles can start at this time."
player.error(errors.PRIVATE_MESSAGE, challengeeId, errorMessage)
return false
else if !@users.contains(challengeeId)
errorMessage = "This user is offline."
player.error(errors.PRIVATE_MESSAGE, challengeeId, errorMessage)
return false
else if player.name == challengeeId
errorMessage = "You cannot challenge yourself."
player.error(errors.PRIVATE_MESSAGE, challengeeId, errorMessage)
return false
else if @challenges[player.name]?[challengeeId] ||
@challenges[challengeeId]?[player.name]
errorMessage = "A challenge already exists between you two."
player.error(errors.PRIVATE_MESSAGE, challengeeId, errorMessage)
return false
# Do not allow rated battles or other unallowed conditions.
if _.difference(conditions, SelectableConditions).length > 0
player.error(errors.FIND_BATTLE, 'This battle cannot have certain conditions.')
return false
err = @validateTeam(team, format, conditions)
if err.length > 0
# TODO: Use a modal error instead
player.error(errors.FIND_BATTLE, err)
return false
@challenges[player.name] ?= {}
@challenges[player.name][challengeeId] = {format, team, conditions, challengerName: player.name, altName}
challengee = @users.get(challengeeId)
challengee.send("challenge", player.name, format, conditions)
return true
acceptChallenge: (player, challengerId, team, altName) ->
if !@challenges[challengerId]?[player.name]?
errorMessage = "The challenge no longer exists."
player.error(errors.PRIVATE_MESSAGE, challengerId, errorMessage)
return null
challenge = @challenges[challengerId][player.name]
err = @validateTeam(team, challenge.format, challenge.conditions)
if err.length > 0
# TODO: Use a modal error instead
player.error(errors.FIND_BATTLE, err)
return null
teams = [
{
id: challengerId,
name: challenge.altName || challenge.challengerName,
team: challenge.team,
ratingKey: alts.uniqueId(challengerId, challenge.altName)
}
{
id: player.name,
name: altName || player.name,
team: team,
ratingKey: alts.uniqueId(player.name, altName)
}
]
id = @createBattle(challenge.format, teams, challenge.conditions)
challenger = @users.get(challengerId)
challenger.send("challengeSuccess", player.name)
player.send("challengeSuccess", challengerId)
delete @challenges[challengerId][player.name]
return id
rejectChallenge: (player, challengerId) ->
if !@challenges[challengerId]?[player.name]?
errorMessage = "The challenge no longer exists."
player.error(errors.PRIVATE_MESSAGE, challengerId, errorMessage)
return false
delete @challenges[challengerId][player.name]
player.send("rejectChallenge", challengerId)
challenger = @users.get(challengerId)
challenger.send("rejectChallenge", player.name)
cancelChallenge: (player, challengeeId) ->
if !@challenges[player.name]?[challengeeId]?
errorMessage = "The challenge no longer exists."
player.error(errors.PRIVATE_MESSAGE, challengeeId, errorMessage)
return false
delete @challenges[player.name][challengeeId]
player.send("cancelChallenge", challengeeId)
challengee = @users.get(challengeeId)
challengee.send("cancelChallenge", player.name)
stopChallenges: (player) ->
playerId = player.name
for challengeeId of @challenges[playerId]
@cancelChallenge(player, challengeeId)
delete @challenges[playerId]
for challengerId of @challenges
if @challenges[challengerId][playerId]
@rejectChallenge(player, challengerId)
# Adds the player to the queue. Note that there is no validation on whether altName
# is correct, so make
queuePlayer: (playerId, team, format = DEFAULT_FORMAT, altName) ->
if @isLockedDown()
err = ["The server is restarting after all battles complete. No new battles can start at this time."]
else if format != DEFAULT_FORMAT
# TODO: Implement ratings for other formats
err = ["The server doesn't support this ladder at this time. Please ask for challenges instead."]
else
err = @validateTeam(team, format, FIND_BATTLE_CONDITIONS)
if err.length == 0
name = @users.get(playerId).name
ratingKey = alts.uniqueId(playerId, altName)
@queues[format].add(playerId, altName || name, team, ratingKey)
return err
queuedPlayers: (format = DEFAULT_FORMAT) ->
@queues[format].queuedPlayers()
removePlayer: (playerId, format = DEFAULT_FORMAT) ->
return false if format not of @queues
@queues[format].remove(playerId)
return true
beginBattles: (next) ->
array = for format in Object.keys(Formats)
do (format) => (callback) =>
@queues[format].pairPlayers (err, pairs) =>
if err then return callback(err)
# Create a battle for each pair
battleIds = []
for pair in pairs
id = @createBattle(format, pair, FIND_BATTLE_CONDITIONS)
battleIds.push(id)
callback(null, battleIds)
async.parallel array, (err, battleIds) ->
return next(err) if err
next(null, _.flatten(battleIds))
return true
# Creates a battle and returns its battleId
createBattle: (rawFormat = DEFAULT_FORMAT, pair = [], conditions = []) ->
format = Formats[rawFormat]
generation = format.generation
conditions = conditions.concat(format.conditions)
{Battle} = require("../server/#{generation}/battle")
{BattleController} = require("../server/#{generation}/battle_controller")
playerIds = pair.map((user) -> user.name)
battleId = @generateBattleId(playerIds)
battle = new Battle(battleId, pair, format: rawFormat, conditions: _.clone(conditions))
@battles[battleId] = new BattleController(battle)
for player in pair
# Add user to spectators
# TODO: player.id should be using player.name, but alts present a problem.
user = @users.get(player.id)
battle.add(spark) for spark in user.sparks
# Add/remove player ids to/from user battles
@userBattles[player.id] ?= {}
@userBattles[player.id][battleId] = true
# Add the player to the list if its not an alt
if player.id == player.ratingKey # hacky - but no alternative right now
@visibleUserBattles[player.id] ?= {}
@visibleUserBattles[player.id][battleId] = true
battle.once 'end', @removeUserBattle.bind(this, player.id, player.name, battleId)
battle.once 'expire', @removeBattle.bind(this, battleId)
# Add the battle to the achievements system
# Uneligible battles are ignored by this function
achievements.registerBattle(this, battle)
@rooms.push(battle)
@battles[battleId].beginBattle()
battleId
# Generate a random ID for a new battle.
generateBattleId: (players) ->
hmac = createHmac('sha1', config.SECRET_KEY)
hmac.update((new Date).toISOString())
for id in players
hmac.update(id)
hmac.digest('hex')
# Returns the battle with battleId.
findBattle: (battleId) ->
@battles[battleId]
getUserBattles: (userId) ->
(id for id, value of @userBattles[userId])
# Returns all non-alt battles the user is playing in
getVisibleUserBattles: (username) ->
(id for id, value of @visibleUserBattles[username])
getOngoingBattles: ->
# TODO: This is very inefficient. Improve this.
_.chain(@battles).values().reject((b) -> b.battle.isOver()).value()
removeUserBattle: (userId, username, battleId) ->
delete @userBattles[userId][battleId]
delete @visibleUserBattles[username]?[battleId]
removeBattle: (battleId) ->
for room, i in @rooms
if room.name == battleId
@rooms.splice(i, 1)
break
delete @battles[battleId]
# A length of -1 denotes a permanent ban.
ban: (username, reason, length = -1) ->
auth.ban(username, reason, length)
if user = @users.get(username)
user.error(errors.BANNED, reason, length)
user.close()
unban: (username, next) ->
auth.unban(username, next)
mute: (username, reason, length) ->
auth.mute(username, reason, length)
unmute: (username) ->
auth.unmute(username)
announce: (message) ->
for room in @rooms
room.announce("warning", message)
userMessage: (user, room, message) ->
@runIfUnmuted user, room.name, ->
room.userMessage(user, message)
runIfUnmuted: (user, roomId, next) ->
auth.getMuteTTL user.name, (err, ttl) ->
if ttl == -2
next()
else
user.announce(roomId, 'warning', "You are muted for another #{ttl} seconds!")
setAuthority: (user, newAuthority) ->
user = @users.get(user) if user not instanceof User
user.authority = newAuthority if user
limit: (player, kind, options, next) ->
attributes =
max: options.max
duration: options.duration
id: player.id
db: redis
@limiters[player.id][kind] ?= new Limiter(attributes)
@limiters[player.id][kind].get(next)
lockdown: ->
@canBattlesStart = false
for user in @users.getUsers()
@stopChallenges(user)
@announce("<strong>The server is restarting!</strong> We're waiting for all battles to finish to push some updates. No new battles may start at this time.")
unlockdown: ->
@canBattlesStart = true
@announce("<strong>Battles have been unlocked!</strong> You may battle again.")
isLockedDown: ->
!@canBattlesStart
# Returns an empty array if the given team is valid, an array of errors
# otherwise.
validateTeam: (team, format = DEFAULT_FORMAT, conditions = []) ->
return [ "Invalid format: #{format}." ] if format not of Formats
format = Formats[format]
return [ "Invalid team format." ] if team not instanceof Array
return [ "Team must have 1 to 6 Pokemon." ] unless 1 <= team.length <= 6
conditions = conditions.concat(format.conditions)
genData = gen.GenerationJSON[format.generation.toUpperCase()]
err = require('./conditions').validateTeam(conditions, team, genData)
return err if err.length > 0
err = team.map (pokemon, i) =>
@validatePokemon(conditions, pokemon, i + 1, format.generation)
return _.flatten(err)
# Returns an empty array if the given Pokemon is valid, an array of errors
# otherwise.
validatePokemon: (conditions, pokemon, slot, generation = gen.DEFAULT_GENERATION) ->
genData = gen.GenerationJSON[generation.toUpperCase()]
{SpeciesData, FormeData, MoveData} = genData
err = []
prefix = "Slot ##{slot}"
if !pokemon.species
err.push("#{prefix}: No species given.")
return err
species = SpeciesData[pokemon.species]
if !species
err.push("#{prefix}: Invalid species: #{pokemon.species}.")
return err
prefix += " (#{pokemon.species})"
@normalizePokemon(pokemon, generation)
forme = FormeData[pokemon.species][pokemon.forme]
if !forme
err.push("#{prefix}: Invalid forme: #{pokemon.forme}.")
return err
if forme.isBattleOnly
err.push("#{prefix}: #{pokemon.forme} forme is battle-only.")
return err
unless 0 < pokemon.name.length <= MAX_NICKNAME_LENGTH
err.push("#{prefix}: Nickname cannot be blank or be
#{MAX_NICKNAME_LENGTH} characters or higher.")
return err
if pokemon.name != pokemon.species && pokemon.name of SpeciesData
err.push("#{prefix}: Nickname cannot be another Pokemon's name.")
return err
if /[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/.test(pokemon.name)
err.push("#{prefix}: Nickname cannot contain some special characters.")
return err
if isNaN(pokemon.level)
err.push("#{prefix}: Invalid level: #{pokemon.level}.")
# TODO: 100 is a magic constant
else if !(1 <= pokemon.level <= 120)
err.push("#{prefix}: Level must be between 1 and 120.")
if pokemon.gender not in [ "M", "F", "Genderless" ]
err.push("#{prefix}: Invalid gender: #{pokemon.gender}.")
if species.genderRatio == -1 && pokemon.gender != "Genderless"
err.push("#{prefix}: Must be genderless.")
if species.genderRatio == 0 && pokemon.gender != "M"
err.push("#{prefix}: Must be male.")
if species.genderRatio == 8 && pokemon.gender != "F"
err.push("#{prefix}: Must be female.")
if (typeof pokemon.evs != "object")
err.push("#{prefix}: Invalid evs.")
if (typeof pokemon.ivs != "object")
err.push("#{prefix}: Invalid ivs.")
if !_.chain(pokemon.evs).values().all((ev) -> 0 <= ev <= 255).value()
err.push("#{prefix}: EVs must be between 0 and 255.")
if !_.chain(pokemon.ivs).values().all((iv) -> 0 <= iv <= 31).value()
err.push("#{prefix}: IVs must be between 0 and 31.")
if _.values(pokemon.evs).reduce(((x, y) -> x + y), 0) > 510
err.push("#{prefix}: EV total must be less than 510.")
if pokemon.ability not in forme["abilities"] &&
pokemon.ability != forme["hiddenAbility"]
err.push("#{prefix}: Invalid ability.")
if pokemon.moves not instanceof Array
err.push("#{prefix}: Invalid moves.")
# TODO: 4 is a magic constant
else if !(1 <= pokemon.moves.length <= 4)
err.push("#{prefix}: Must have 1 to 4 moves.")
else if !_(pokemon.moves).all((name) -> MoveData[name]?)
invalidMove = _(pokemon.moves).find((name) -> !MoveData[name]?)
err.push("#{prefix}: Invalid move name: #{invalidMove}")
else if !learnsets.checkMoveset(gen.GenerationJSON, pokemon,
gen.GENERATION_TO_INT[generation], pokemon.moves)
err.push("#{prefix}: Invalid moveset.")
err.push require('./conditions').validatePokemon(conditions, pokemon, genData, prefix)...
return err
# Normalizes a Pokemon by setting default values where applicable.
# Assumes that the Pokemon is a real Pokemon (i.e. its species/forme is valid)
normalizePokemon: (pokemon, generation = gen.DEFAULT_GENERATION) ->
{SpeciesData, FormeData} = gen.GenerationJSON[generation.toUpperCase()]
pokemon.forme ?= "default"
pokemon.name ?= pokemon.species
pokemon.ability ?= FormeData[pokemon.species][pokemon.forme]?["abilities"][0]
if !pokemon.gender?
{genderRatio} = SpeciesData[pokemon.species]
if genderRatio == -1 then pokemon.gender = "Genderless"
else if Math.random() < (genderRatio / 8) then pokemon.gender = "F"
else pokemon.gender = "M"
pokemon.evs ?= {}
pokemon.ivs ?= {}
pokemon.level ?= 100
pokemon.level = Math.floor(pokemon.level)
return pokemon

52
server/user.coffee Normal file
View File

@@ -0,0 +1,52 @@
{_} = require 'underscore'
class @User
constructor: (attributes) ->
if _.isObject(attributes)
{@id, @name, @authority} = attributes
else
@id = attributes
@name ?= @id
@sparks = []
addSpark: (spark) ->
unless @hasSpark(spark)
@sparks.push(spark)
spark.user = this
removeSpark: (spark) ->
index = @sparks.indexOf(spark)
@sparks.splice(index, 1) if index != -1
hasSpark: (spark) ->
spark in @sparks
hasSparks: ->
@sparks.length >= 1
toJSON: (options = {}) ->
displayedName = options.alt ? @name
isAlt = (displayedName != @name)
json = {
'id': displayedName
}
if isAlt
json['isAlt'] = true
else
json['authority'] = @authority if @authority
json
send: ->
spark.send.apply(spark, arguments) for spark in @sparks
error: (args...) ->
@send("errorMessage", args...)
message: (roomId, msg) ->
@send("rawMessage", roomId, msg)
announce: (roomId, klass, msg) ->
@send("announce", roomId, klass, msg)
close: ->
spark.end() for spark in @sparks

41
server/user_store.coffee Normal file
View File

@@ -0,0 +1,41 @@
{EventEmitter} = require('events')
{User} = require('./user')
class @UserStore extends EventEmitter
constructor: ->
super()
@users = {}
add: (json, spark) ->
id = json.name || json
user = (@users[@key(id)] ||= new User(json))
user.addSpark(spark)
user
remove: (spark) ->
id = @key(spark.user.name)
user = @users[id]
if user
user.removeSpark(spark)
delete @users[id] unless user.hasSparks()
return user
contains: (id) ->
@get(id)?
get: (id) ->
@users[@key(id)]
getUsers: ->
(user for key, user of @users)
key: (id) ->
String(id).toLowerCase()
send: ->
for key, user of @users
user.send.apply(user, arguments)
toJSON: ->
for key, user of @users
user.toJSON()

View File

@@ -0,0 +1,78 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/attachment.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
delete @Status.Sleep::switchOut
# In XY, electric pokemon are immune to paralysis
@Status.Paralyze.worksOn = (battle, pokemon) ->
!pokemon.hasType("Electric")
# In XY, Scald and Steam Eruption thaw the target
@Status.Freeze::afterBeingHit = (move, user, target) ->
if !move.isNonDamaging() && move.type == 'Fire' ||
move.name in ["Scald", "Steam Eruption"]
@pokemon.cureStatus()
# In XY, Protect-like moves have a chance of success corresponding to the
# power of 3, instead of the power of 2 in previous generations.
@Attachment.ProtectCounter::successMultiplier = 3
# In XY, partial-trapping moves deal more damage at the end of each turn
@Attachment.Trap::getDamagePerTurn = ->
if @user.hasItem("Binding Band")
6
else
8
class @Attachment.KingsShield extends @VolatileAttachment
name: "KingsShieldAttachment"
initialize: ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
shouldBlockExecution: (move, user) ->
if move.hasFlag("protect") && !move.isNonDamaging()
if move.hasFlag("contact") then user.boost(attack: -2, @pokemon)
return true
endTurn: ->
@pokemon.unattach(@constructor)
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.SpikyShield extends @VolatileAttachment
name: "SpikyShieldAttachment"
initialize: ->
@pokemon.tell(Protocol.POKEMON_ATTACH, @name)
shouldBlockExecution: (move, user) ->
if move.hasFlag("protect")
if move.hasFlag("contact") then user.damage(user.stat('hp') >> 3)
return true
endTurn: ->
@pokemon.unattach(@constructor)
unattach: ->
@pokemon.tell(Protocol.POKEMON_UNATTACH, @name)
class @Attachment.StickyWeb extends @TeamAttachment
name: "StickyWebAttachment"
initialize: ->
id = @team.playerId
@battle.cannedText('STICKY_WEB_START', @battle.getPlayerIndex(id))
switchIn: (pokemon) ->
if !pokemon.isImmune("Ground")
@battle.cannedText('STICKY_WEB_CONTINUE', pokemon)
# The source is not actually an opposing Pokemon, but in order for Defiant
# to work properly, the source should not be the pokemon itself.
pokemon.boost(speed: -1, @battle.getAllOpponents(pokemon)[0])
unattach: ->
id = @team.playerId
@battle.cannedText('STICKY_WEB_END', @battle.getPlayerIndex(id))

39
server/xy/battle.coffee Normal file
View File

@@ -0,0 +1,39 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/battle.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
{Ability} = require('./data/abilities')
@Battle::generation = 'xy'
@Battle::actionMap['mega'] =
priority: -> 5
action: (action) ->
@performMegaEvolution(action.pokemon)
@Battle::performMegaEvolution = (pokemon) ->
[ species, forme ] = pokemon.item.mega
pokemon.changeForme(forme)
ability = @FormeData[species][forme]["abilities"][0]
ability = Ability[ability.replace(/\s+/g, '')]
pokemon.copyAbility(ability, reveal: false)
# We set the original ability to this ability so that the ability
# is not reset upon switch out.
pokemon.originalAbility = ability
pokemon.originalForme = forme
# Generate and display mega-evolution message
pieces = forme.split('-').map((s) -> s[0].toUpperCase() + s.substr(1))
pieces.splice(1, 0, species)
megaFormeName = pieces.join(" ")
@message "#{pokemon.name} Mega Evolved into #{megaFormeName}!"
# Retrofit `recordMove` to also record mega evolutions.
oldRecordMove = @Battle::recordMove
@Battle::recordMove = (playerId, move, forSlot = 0, options = {}) ->
pokemon = @getTeam(playerId).at(forSlot)
if options.megaEvolve && !@getAction(pokemon) && pokemon.canMegaEvolve()
if @pokemonActions.filter((o) -> o.type == 'mega' && o.pokemon.team == pokemon.team).length == 0
@addAction(type: 'mega', pokemon: pokemon)
oldRecordMove.apply(this, arguments)

View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/battle_controller.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

View File

@@ -0,0 +1,169 @@
{Weather} = require '../../../shared/weather'
# Retcon weather abilities to only last 5 turns.
makeWeatherAbility = (name, weather) ->
makeAbility name, ->
this::switchIn = ->
return if @battle.hasWeather(weather)
moveName = switch weather
when Weather.SUN then "Sunny Day"
when Weather.RAIN then "Rain Dance"
when Weather.SAND then "Sandstorm"
when Weather.HAIL then "Hail"
else throw new Error("#{weather} ability not supported.")
@pokemon.activateAbility()
move = @battle.getMove(moveName)
move.changeWeather(@battle, @pokemon)
# Import old abilities
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/abilities.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
# Retcon old abilities
# Effect Spore now does not affect Grass-type Pokemon,
# Pokemon with Overcoat, or Pokemon holding Safety Goggles
oldEffectSpore = Ability.EffectSpore::afterBeingHit
Ability.EffectSpore::afterBeingHit = (move, user, target, damage) ->
unless user.hasType("Grass") || user.hasAbility("Overcoat") || user.hasItem("Safety Goggles")
oldEffectSpore.apply(this, arguments)
# Oblivious now also prevents and cures Taunt
makeAttachmentImmuneAbility("Oblivious", [Attachment.Attract, Attachment.Taunt])
# Overcoat now also prevents powder moves from working.
Ability.Overcoat::shouldBlockExecution = (move, user) ->
if move.hasFlag("powder")
@pokemon.activateAbility()
return true
# New ability interfaces
makeNormalTypeChangeAbility = (name, newType) ->
makeAbility name, ->
this::editMoveType = (type, target) ->
return newType if type == 'Normal' && @pokemon != target
return type
this::modifyBasePower = (move, target) ->
return 0x14CD if move.type == 'Normal'
return 0x1000
makeNormalTypeChangeAbility("Aerilate", "Flying")
makeNormalTypeChangeAbility("Pixilate", "Fairy")
makeNormalTypeChangeAbility("Refrigerate", "Ice")
makeAuraAbility = (name, type) ->
makeAbility name, ->
this::modifyBasePower = (move, target) ->
return 0x1000 if move.getType(@battle, @pokemon, target) != type
for pokemon in @battle.getActiveAlivePokemon()
return 0xC00 if pokemon.hasAbility("Aura Break")
return 0x1547
makeAuraAbility("Dark Aura", "Dark")
makeAuraAbility("Fairy Aura", "Fairy")
# New unique abilities
makeAttachmentImmuneAbility("Aroma Veil", [Attachment.Attract, Attachment.Disable,
Attachment.Encore, Attachment.Taunt, Attachment.Torment], cure: false) # TODO: Add Heal Block
# Implemented in makeAuraAbility
makeAbility "Aura Break"
makeAbility 'Bulletproof', ->
this::isImmune = (type, move) ->
if move?.hasFlag('bullet')
@pokemon.activateAbility()
return true
# TODO: Cheek Pouch
makeAbility "Cheek Pouch"
makeAbility "Competitive", ->
this::afterEachBoost = (boostAmount, source) ->
return if source.team == @pokemon.team
@pokemon.activateAbility()
@pokemon.boost(specialAttack: 2) if boostAmount < 0
# TODO: Flower Veil
makeAbility "Flower Veil"
makeAbility "Fur Coat", ->
this::modifyBasePowerTarget = (move) ->
if move.isPhysical() then 0x800 else 0x1000
makeAbility 'Gale Wings', ->
this::editPriority = (priority, move) ->
# TODO: Test if Gale Wings works with Hidden Power Flying.
return priority + 1 if move.type == 'Flying'
return priority
makeAbility "Gooey", ->
this::isAliveCheck = -> true
this::afterBeingHit = (move, user) ->
if move.hasFlag("contact")
user.boost(speed: -1, @pokemon)
@pokemon.activateAbility()
# TODO: Grass Pelt
makeAbility "Grass Pelt"
# TODO: Magician
makeAbility "Magician"
makeAbility 'Mega Launcher', ->
this::modifyBasePower = (move, target) ->
return 0x1800 if move.hasFlag("pulse")
return 0x1000
makeAbility 'Parental Bond', ->
this::calculateNumberOfHits = (move, targets) ->
# Do nothing if this move is multi-hit, has multiple targets, or is status.
return if move.minHits != 1 || targets.length > 1 || move.isNonDamaging()
return 2
this::modifyDamage = (move, target, hitNumber) ->
return 0x800 if hitNumber == 2 && move.maxHits == 1
return 0x1000
makeAbility 'Protean', ->
this::beforeMove = (move, user, targets) ->
type = move.getType(@battle, user, targets[0])
return if user.types.length == 1 && user.types[0] == type
user.types = [ type ]
@pokemon.activateAbility()
@battle.cannedText('TRANSFORM_TYPE', user, type)
makeAbility 'Stance Change', ->
this::beforeMove = (move, user, targets) ->
newForme = switch
when !move.isNonDamaging() then "blade"
when move == @battle.getMove("King's Shield") then "default"
if newForme && !@pokemon.isInForme(newForme) && @pokemon.species == 'Aegislash'
@pokemon.activateAbility()
@pokemon.changeForme(newForme)
humanized = (if newForme == "blade" then "Blade" else "Shield")
@battle.message("Changed to #{humanized} Forme!")
true
makeAbility "Strong Jaw", ->
this::modifyBasePower = (move) ->
return 0x1800 if move.hasFlag("bite")
return 0x1000
# TODO: Sweet Veil (2v2)
makeAttachmentImmuneAbility("Sweet Veil", [Status.Sleep], cure: false)
# TODO: Symbiosis
makeAbility "Symbiosis"
makeAbility "Tough Claws", ->
this::modifyBasePower = (move) ->
return 0x14CD if move.hasFlag("contact")
return 0x1000

View File

@@ -0,0 +1,566 @@
{
"Adaptability": {
"description": "This Pokemon's attacks that match one of its types have a STAB modifier of 2 instead of 1.5."
},
"Aerilate": {
"description": "This Pokemon's Normal-type moves become Flying-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Aftermath": {
"description": "If this Pokemon is knocked out with a contact move, that move's user loses 1/4 of its maximum HP, rounded down. If any active Pokemon has the Ability Damp, this effect is prevented."
},
"Air Lock": {
"description": "While this Pokemon is active, the effects of weather conditions are disabled."
},
"Analytic": {
"description": "The power of this Pokemon's move is multiplied by 1.3 if it is the last to move in a turn. Does not affect Doom Desire and Future Sight."
},
"Anger Point": {
"description": "If this Pokemon, but not its substitute, is struck by a critical hit, its Attack is raised by 12 stages."
},
"Anticipation": {
"description": "On switch-in, this Pokemon is alerted if any opposing Pokemon has an attack that is super effective on this Pokemon or an OHKO move. Counter, Metal Burst, and Mirror Coat count as attacking moves of their respective types, while Hidden Power, Judgment, Natural Gift, Techno Blast, and Weather Ball are considered Normal-type moves."
},
"Arena Trap": {
"description": "Prevents adjacent opposing Pokemon from choosing to switch out unless they are immune to trapping or have immunity to Ground."
},
"Aroma Veil": {
"description": "This Pokemon and its allies cannot be affected by Attract, Disable, Encore, Heal Block, Taunt, or Torment."
},
"Aura Break": {
"description": "While this Pokemon is active, the effects of the Abilities Dark Aura and Fairy Aura are reversed, multiplying the power of Dark- and Fairy-type moves, respectively, by 3/4 instead of 1.33."
},
"Bad Dreams": {
"description": "Causes adjacent opposing Pokemon to lose 1/8 of their maximum HP, rounded down, at the end of each turn if they are asleep."
},
"Battle Armor": {
"description": "This Pokemon cannot be struck by a critical hit."
},
"Big Pecks": {
"description": "Prevents other Pokemon from lowering this Pokemon's Defense stat stage."
},
"Blaze": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Fire-type attack."
},
"Bulletproof": {
"description": "This Pokemon is immune to bullet moves."
},
"Cheek Pouch": {
"description": "If this Pokemon eats a Berry, it restores 1/3 of its maximum HP, rounded down, in addition to the Berry's effect."
},
"Chlorophyll": {
"description": "If Sunny Day is active, this Pokemon's Speed is doubled."
},
"Clear Body": {
"description": "Prevents other Pokemon from lowering this Pokemon's stat stages."
},
"Cloud Nine": {
"description": "While this Pokemon is active, the effects of weather conditions are disabled."
},
"Color Change": {
"description": "This Pokemon's type changes to match the type of the last move that hit it. This effect applies after all hits from a multi-hit move; Sheer Force prevents it from activating if the attack has a secondary effect."
},
"Competitive": {
"description": "This Pokemon's Special Attack is raised by 2 stages for each of its stat stages that is lowered by an opposing Pokemon."
},
"Compoundeyes": {
"description": "This Pokemon's moves have their accuracy multiplied by 1.3."
},
"Contrary": {
"description": "If this Pokemon has a stat stage raised it is lowered instead, and vice versa."
},
"Cursed Body": {
"description": "If this Pokemon is hit by an attack, there is a 30% chance that move gets disabled unless one of the attacker's moves is already disabled."
},
"Cute Charm": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will become infatuated if it is of the opposite gender."
},
"Damp": {
"description": "While this Pokemon is active, Self-Destruct, Explosion, and the Ability Aftermath are prevented from having an effect."
},
"Dark Aura": {
"description": "While this Pokemon is active, the power of Dark-type moves used by active Pokemon is multiplied by 1.33."
},
"Defeatist": {
"description": "While this Pokemon has 1/2 or less of its maximum HP, rounded down, its Attack and Special Attack are halved."
},
"Defiant": {
"description": "This Pokemon's Attack is raised by 2 stages for each of its stat stages that is lowered by an opposing Pokemon."
},
"Download": {
"description": "On switch-in, this Pokemon's Attack or Special Attack is raised by 1 stage based on the weaker combined defensive stat of all opposing Pokemon. Attack is raised if their Defense is lower, and Special Attack is raised if their Special Defense is the same or lower."
},
"Drizzle": {
"description": "On switch-in, this Pokemon summons Rain Dance."
},
"Drought": {
"description": "On switch-in, this Pokemon summons Sunny Day."
},
"Dry Skin": {
"description": "This Pokemon is immune to Water-type moves and restores 1/4 of its maximum HP, rounded down, when hit by a Water-type move. The power of Fire-type moves is multiplied by 1.25 when used on this Pokemon. At the end of each turn, this Pokemon restores 1/8 of its maximum HP, rounded down, if the weather is Rain Dance, and loses 1/8 of its maximum HP, rounded down, if the weather is Sunny Day."
},
"Early Bird": {
"description": "This Pokemon's sleep counter drops by 2 instead of 1."
},
"Effect Spore": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be poisoned, paralyzed, or fall asleep."
},
"Fairy Aura": {
"description": "While this Pokemon is active, the power of Fairy-type moves used by active Pokemon is multiplied by 1.33."
},
"Filter": {
"description": "This Pokemon receives 3/4 damage from supereffective attacks."
},
"Flame Body": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be burned."
},
"Flare Boost": {
"description": "While this Pokemon is burned, the power of its special attacks is multiplied by 1.5."
},
"Flash Fire": {
"description": "This Pokemon is immune to Fire-type moves. The first time it is hit by a Fire-type move, its attacking stat is multiplied by 1.5 while using a Fire-type attack as long as it remains active and has this Ability. If this Pokemon is frozen, it cannot be defrosted by Fire-type attacks."
},
"Flower Gift": {
"description": "If this Pokemon is a Cherrim and Sunny Day is active, it changes to Sunshine Form and the Attack and Special Defense of it and its allies are multiplied by 1.5."
},
"Flower Veil": {
"description": "Grass-type Pokemon on this Pokemon's side cannot have their stat stages lowered by other Pokemon or have a major status condition inflicted on them by other Pokemon."
},
"Forecast": {
"description": "If this Pokemon is a Castform, its type changes to the current weather condition's type, except Sandstorm."
},
"Forewarn": {
"description": "On switch-in, this Pokemon is alerted to the move with the highest power, at random, known by an opposing Pokemon."
},
"Friend Guard": {
"description": "This Pokemon's allies receive 3/4 damage from other Pokemon's attacks."
},
"Frisk": {
"description": "On switch-in, this Pokemon identifies the held items of all opposing Pokemon."
},
"Fur Coat": {
"description": "This Pokemon's Defense is doubled."
},
"Gale Wings": {
"description": "This Pokemon's Flying-type moves have their priority increased by 1."
},
"Gluttony": {
"description": "When this Pokemon has 1/2 or less of its maximum HP, rounded down, it uses certain Berries early."
},
"Gooey": {
"description": "This Pokemon causes Pokemon making contact with it to have their Speed lowered by 1 stage."
},
"Grass Pelt": {
"description": "If Grassy Terrain is active, this Pokemon's Defense is multiplied by 1.5."
},
"Guts": {
"description": "If this Pokemon has a major status condition, its Attack is multiplied by 1.5; burn's physical damage halving is ignored."
},
"Harvest": {
"description": "If the last item this Pokemon used is a Berry, there is a 50% chance it gets restored at the end of each turn. If Sunny Day is active, this chance is 100%."
},
"Healer": {
"description": "There is a 30% chance of curing an adjacent ally's major status condition at the end of each turn."
},
"Heatproof": {
"description": "The power of Fire-type attacks against this Pokemon is halved, and any burn damage taken is 1/16 of its maximum HP, rounded down."
},
"Heavy Metal": {
"description": "This Pokemon's weight is doubled."
},
"Honey Gather": {
"description": "No competitive use."
},
"Huge Power": {
"description": "This Pokemon's Attack is doubled."
},
"Hustle": {
"description": "This Pokemon's Attack is multiplied by 1.5 and the accuracy of its physical attacks is multiplied by 0.8."
},
"Hydration": {
"description": "This Pokemon has its major status condition cured at the end of each turn if Rain Dance is active."
},
"Hyper Cutter": {
"description": "Prevents other Pokemon from lowering this Pokemon's Attack stat stage."
},
"Ice Body": {
"description": "If Hail is active, this Pokemon restores 1/16 of its maximum HP, rounded down, at the end of each turn. This Pokemon takes no damage from Hail."
},
"Illuminate": {
"description": "No competitive use."
},
"Illusion": {
"description": "When this Pokemon switches in, it appears as the last unfainted Pokemon in its party until it takes direct damage from another Pokemon's attack. This Pokemon's actual level and HP are displayed instead of those of the mimicked Pokemon."
},
"Immunity": {
"description": "This Pokemon cannot be poisoned. Gaining this Ability while poisoned cures it."
},
"Imposter": {
"description": "On switch-in, this Pokemon Transforms into the opposing Pokemon that is facing it. If there is no Pokemon at that position, this Pokemon does not Transform."
},
"Infiltrator": {
"description": "This Pokemon's moves ignore substitutes and the opposing side's Reflect, Light Screen, Safeguard, and Mist."
},
"Inner Focus": {
"description": "This Pokemon cannot be made to flinch."
},
"Insomnia": {
"description": "This Pokemon cannot fall asleep. Gaining this Ability while asleep cures it."
},
"Intimidate": {
"description": "On switch-in, this Pokemon lowers the Attack of adjacent opposing Pokemon by 1 stage. Pokemon behind a substitute are immune."
},
"Iron Barbs": {
"description": "This Pokemon causes Pokemon making contact with it to lose 1/8 of their maximum HP, rounded down."
},
"Iron Fist": {
"description": "This Pokemon's punch-based attacks have their power multiplied by 1.2."
},
"Justified": {
"description": "This Pokemon's Attack is raised by 1 stage after it is damaged by a Dark-type attack."
},
"Keen Eye": {
"description": "Prevents other Pokemon from lowering this Pokemon's accuracy stat stage. This Pokemon ignores a target's evasiveness stat stage."
},
"Klutz": {
"description": "This Pokemon's held item has no effect. This Pokemon cannot use Fling successfully. Macho Brace, Power Anklet, Power Band, Power Belt, Power Bracer, Power Lens, and Power Weight still have their effects."
},
"Leaf Guard": {
"description": "If Sunny Day is active, this Pokemon cannot gain a major status condition and Rest will fail for it."
},
"Levitate": {
"description": "This Pokemon is immune to Ground. Gravity, Ingrain, Smack Down, and Iron Ball nullify the immunity."
},
"Light Metal": {
"description": "This Pokemon's weight is halved."
},
"Lightningrod": {
"description": "This Pokemon is immune to Electric-type moves and raises its Special Attack by 1 stage when hit by an Electric-type move. If this Pokemon is not the target of a single-target Electric-type move used by another Pokemon, this Pokemon redirects that move to itself if it is within the range of that move."
},
"Limber": {
"description": "This Pokemon cannot be paralyzed. Gaining this Ability while paralyzed cures it."
},
"Liquid Ooze": {
"description": "This Pokemon damages those draining HP from it for as much as they would heal."
},
"Magic Bounce": {
"description": "This Pokemon blocks certain status moves and instead uses the move against the original user."
},
"Magic Guard": {
"description": "This Pokemon can only be damaged by direct attacks. Curse and Substitute on use, Belly Drum, Pain Split, Struggle recoil, and confusion damage are considered direct damage."
},
"Magician": {
"description": "If this Pokemon has no item, it steals the item off a Pokemon it hits with an attack. Does not affect Doom Desire and Future Sight."
},
"Magma Armor": {
"description": "This Pokemon cannot be frozen. Gaining this Ability while frozen cures it."
},
"Magnet Pull": {
"description": "Prevents adjacent opposing Steel-type Pokemon from choosing to switch out unless they are immune to trapping."
},
"Marvel Scale": {
"description": "If this Pokemon has a major status condition, its Defense is multiplied by 1.5."
},
"Mega Launcher": {
"description": "This Pokemon's pulse moves have their power multiplied by 1.5. Heal Pulse restores 3/4 of a target's maximum HP, rounded half down."
},
"Minus": {
"description": "If an active ally has this Ability or the Ability Plus, this Pokemon's Special Attack is multiplied by 1.5."
},
"Mold Breaker": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Moody": {
"description": "This Pokemon has a random stat raised by 2 stages and another stat lowered by 1 stage at the end of each turn."
},
"Motor Drive": {
"description": "This Pokemon is immune to Electric-type moves and raises its Speed by 1 stage when hit by an Electric-type move."
},
"Moxie": {
"description": "This Pokemon's Attack is raised by 1 stage if it attacks and knocks out another Pokemon."
},
"Multiscale": {
"description": "If this Pokemon is at full HP, damage taken from attacks is halved."
},
"Multitype": {
"description": "If this Pokemon is an Arceus, its type changes to match its held Plate."
},
"Mummy": {
"description": "Pokemon making contact with this Pokemon have their Ability changed to Mummy. Does not affect the Ability Multitype or Stance Change."
},
"Natural Cure": {
"description": "This Pokemon has its major status condition cured when it switches out."
},
"No Guard": {
"description": "Every move used by or against this Pokemon will always hit."
},
"Normalize": {
"description": "This Pokemon's moves are changed to be Normal type. This effect comes before other effects that change a move's type."
},
"Oblivious": {
"description": "This Pokemon cannot be infatuated or taunted. Gaining this Ability while affected cures it."
},
"Overcoat": {
"description": "This Pokemon is immune to powder moves and damage from Sandstorm or Hail."
},
"Overgrow": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Grass-type attack."
},
"Own Tempo": {
"description": "This Pokemon cannot be confused. Gaining this Ability while confused cures it."
},
"Parental Bond": {
"description": "This Pokemon's damaging attacks become multi-hit moves that hit twice. The second hit has its damage halved. Does not affect multi-hit moves or moves that have multiple targets."
},
"Pickpocket": {
"description": "If this Pokemon has no item, it steals the item off a Pokemon that makes contact with it."
},
"Pickup": {
"description": "If this Pokemon has no item, it finds one used by an adjacent Pokemon this turn."
},
"Pixilate": {
"description": "This Pokemon's Normal-type moves become Fairy-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Plus": {
"description": "If an active ally has this Ability or the Ability Minus, this Pokemon's Special Attack is multiplied by 1.5."
},
"Poison Heal": {
"description": "If this Pokemon is poisoned, it restores 1/8 of its maximum HP, rounded down, at the end of each turn instead of losing HP."
},
"Poison Point": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be poisoned."
},
"Poison Touch": {
"description": "This Pokemon's contact moves have a 30% chance of poisoning."
},
"Prankster": {
"description": "This Pokemon's non-damaging moves have their priority increased by 1."
},
"Pressure": {
"description": "If this Pokemon is the target of an opposing Pokemon's move, that move loses one additional PP."
},
"Protean": {
"description": "This Pokemon's type changes to match the type of the move it is about to use. This effect comes after all effects that change a move's type."
},
"Pure Power": {
"description": "This Pokemon's Attack is doubled."
},
"Quick Feet": {
"description": "If this Pokemon has a major status condition, its Speed is multiplied by 1.5; the Speed drop from paralysis is ignored."
},
"Rain Dish": {
"description": "If Rain Dance is active, this Pokemon restores 1/16 of its maximum HP, rounded down, at the end of each turn."
},
"Rattled": {
"description": "This Pokemon's Speed is raised by 1 stage if hit by a Bug-, Dark-, or Ghost-type attack."
},
"Reckless": {
"description": "This Pokemon's attacks with recoil or crash damage have their power multiplied by 1.2. Does not affect Struggle."
},
"Refrigerate": {
"description": "This Pokemon's Normal-type moves become Ice-type moves and have their power multiplied by 1.3. This effect comes after other effects that change a move's type, but before Ion Deluge and Electrify's effects."
},
"Regenerator": {
"description": "This Pokemon restores 1/3 of its maximum HP, rounded down, when it switches out."
},
"Rivalry": {
"description": "This Pokemon's attacks have their power multiplied by 1.25 against targets of the same gender or multiplied by 0.75 against targets of the opposite gender. There is no modifier if either this Pokemon or the target is genderless."
},
"Rock Head": {
"description": "This Pokemon does not take recoil damage besides Struggle, Life Orb, and crash damage."
},
"Rough Skin": {
"description": "This Pokemon causes Pokemon making contact with it to lose 1/8 of their maximum HP, rounded down."
},
"Run Away": {
"description": "No competitive use."
},
"Sand Force": {
"description": "If Sandstorm is active, this Pokemon's Ground-, Rock-, and Steel-type attacks have their power multiplied by 1.3. This Pokemon takes no damage from Sandstorm."
},
"Sand Rush": {
"description": "If Sandstorm is active, this Pokemon's Speed is doubled. This Pokemon takes no damage from Sandstorm."
},
"Sand Stream": {
"description": "On switch-in, this Pokemon summons Sandstorm."
},
"Sand Veil": {
"description": "If Sandstorm is active, this Pokemon's evasiveness is multiplied by 1.25. This Pokemon takes no damage from Sandstorm."
},
"Sap Sipper": {
"description": "This Pokemon is immune to Grass-type moves and raises its Attack by 1 stage when hit by a Grass-type move."
},
"Scrappy": {
"description": "This Pokemon can hit Ghost types with Normal- and Fighting-type moves."
},
"Serene Grace": {
"description": "This Pokemon's moves have their secondary effect chance doubled."
},
"Shadow Tag": {
"description": "Prevents adjacent opposing Pokemon from choosing to switch out unless they are immune to trapping or also have this Ability."
},
"Shed Skin": {
"description": "This Pokemon has a 33% chance to have its major status condition cured at the end of each turn."
},
"Sheer Force": {
"description": "This Pokemon's attacks with secondary effects have their power multiplied by 1.3, but the secondary effects are removed."
},
"Shell Armor": {
"description": "This Pokemon cannot be struck by a critical hit."
},
"Shield Dust": {
"description": "This Pokemon is not affected by the secondary effect of another Pokemon's attack."
},
"Simple": {
"description": "If this Pokemon's stat stages are raised or lowered, the effect is doubled instead."
},
"Skill Link": {
"description": "This Pokemon's multi-hit attacks always hit the maximum number of times."
},
"Slow Start": {
"description": "On switch-in, this Pokemon's Attack and Speed are halved for 5 turns."
},
"Sniper": {
"description": "If this Pokemon strikes with a critical hit, the damage is multiplied by 1.5."
},
"Snow Cloak": {
"description": "If Hail is active, this Pokemon's evasiveness is multiplied by 1.25. This Pokemon takes no damage from Hail."
},
"Snow Warning": {
"description": "On switch-in, this Pokemon summons Hail."
},
"Solar Power": {
"description": "If Sunny Day is active, this Pokemon's Special Attack is multiplied by 1.5 and it loses 1/8 of its maximum HP, rounded down, at the end of each turn."
},
"Solid Rock": {
"description": "This Pokemon receives 3/4 damage from supereffective attacks."
},
"Soundproof": {
"description": "This Pokemon is immune to sound-based moves, including Heal Bell."
},
"Speed Boost": {
"description": "This Pokemon's Speed is raised by 1 stage at the end of each full turn it has been on the field."
},
"Stall": {
"description": "This Pokemon moves last among Pokemon using the same or greater priority moves."
},
"Stance Change": {
"description": "If this Pokemon is an Aegislash, it changes to Blade Forme before attempting to use an attacking move, and changes to Shield Forme before attempting to use King's Shield."
},
"Static": {
"description": "There is a 30% chance a Pokemon making contact with this Pokemon will be paralyzed."
},
"Steadfast": {
"description": "If this Pokemon flinches, its Speed is raised by 1 stage."
},
"Stench": {
"description": "This Pokemon's attacks without a chance to flinch have a 10% chance to flinch."
},
"Sticky Hold": {
"description": "This Pokemon cannot lose its held item due to another Pokemon's attack."
},
"Storm Drain": {
"description": "This Pokemon is immune to Water-type moves and raises its Special Attack by 1 stage when hit by a Water-type move. If this Pokemon is not the target of a single-target Water-type move used by another Pokemon, this Pokemon redirects that move to itself if it is within the range of that move."
},
"Strong Jaw": {
"description": "This Pokemon's bite-based moves have their power multiplied by 1.5."
},
"Sturdy": {
"description": "If this Pokemon is at full HP, it survives one hit with at least 1 HP. OHKO moves fail when used against this Pokemon."
},
"Suction Cups": {
"description": "This Pokemon cannot be forced to switch out by another Pokemon's attack or item."
},
"Super Luck": {
"description": "This Pokemon's critical hit ratio is raised by 1 stage."
},
"Swarm": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Bug-type attack."
},
"Sweet Veil": {
"description": "This Pokemon and its allies cannot fall asleep."
},
"Swift Swim": {
"description": "If Rain Dance is active, this Pokemon's Speed is doubled."
},
"Symbiosis": {
"description": "If an ally uses its item, this Pokemon gives its item to that ally immediately. Does not activate if the ally's item was stolen or knocked off."
},
"Synchronize": {
"description": "If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon, that Pokemon receives the same major status condition."
},
"Tangled Feet": {
"description": "This Pokemon's evasiveness is doubled as long as it is confused."
},
"Technician": {
"description": "This Pokemon's attacks of 60 Base Power or less have their power multiplied by 1.5. Does affect Struggle."
},
"Telepathy": {
"description": "This Pokemon does not take damage from attacks made by its allies."
},
"Teravolt": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Thick Fat": {
"description": "If a Pokemon uses a Fire- or Ice-type attack against this Pokemon, that Pokemon's attacking stat is halved while using the attack."
},
"Tinted Lens": {
"description": "This Pokemon's attacks that are not very effective on a target have their damage doubled."
},
"Torrent": {
"description": "When this Pokemon has 1/3 or less of its maximum HP, rounded down, its attacking stat is multiplied by 1.5 while using a Water-type attack."
},
"Tough Claws": {
"description": "This Pokemon's contact moves have their power multiplied by 1.3."
},
"Toxic Boost": {
"description": "While this Pokemon is poisoned, the power of its physical attacks is multiplied by 1.5."
},
"Trace": {
"description": "On switch-in, this Pokemon copies a random adjacent opposing Pokemon's Ability. If there is no Ability that can be copied at that time, this Ability will activate as soon as an Ability can be copied. Abilities that cannot be copied are Flower Gift, Forecast, Illusion, Imposter, Multitype, Stance Change, Trace, and Zen Mode."
},
"Truant": {
"description": "This Pokemon skips every other turn instead of using a move."
},
"Turboblaze": {
"description": "This Pokemon's moves and their effects ignore the Abilities of other Pokemon."
},
"Unaware": {
"description": "This Pokemon ignores other Pokemon's Attack, Special Attack, and accuracy stat stages when taking damage, and ignores other Pokemon's Defense, Special Defense, and evasiveness stat stages when dealing damage."
},
"Unburden": {
"description": "If this Pokemon loses its held item for any reason, its Speed is doubled. This boost is lost if it switches out or gains a new item or Ability."
},
"Unnerve": {
"description": "While this Pokemon is active, it prevents opposing Pokemon from using their Berries."
},
"Victory Star": {
"description": "This Pokemon and its allies' moves have their accuracy multiplied by 1.1."
},
"Vital Spirit": {
"description": "This Pokemon cannot fall asleep. Gaining this Ability while asleep cures it."
},
"Volt Absorb": {
"description": "This Pokemon is immune to Electric-type moves and restores 1/4 of its maximum HP, rounded down, when hit by an Electric-type move."
},
"Water Absorb": {
"description": "This Pokemon is immune to Water-type moves and restores 1/4 of its maximum HP, rounded down, when hit by a Water-type move."
},
"Water Veil": {
"description": "This Pokemon cannot be burned. Gaining this Ability while burned cures it."
},
"Weak Armor": {
"description": "If a physical attack hits this Pokemon, its Defense is lowered by 1 stage and its Speed is raised by 1 stage."
},
"White Smoke": {
"description": "Prevents other Pokemon from lowering this Pokemon's stat stages."
},
"Wonder Guard": {
"description": "This Pokemon can only be damaged by supereffective moves and indirect damage."
},
"Wonder Skin": {
"description": "All non-damaging moves have their accuracy changed to 50% when used on this Pokemon, unless the move cannot miss."
},
"Zen Mode": {
"description": "If this Pokemon is a Darmanitan, it changes to Zen Mode if it has 1/2 or less of its maximum HP at the end of a turn. If Darmanitan's HP is above 1/2 of its maximum HP at the end of a turn, it changes back to Standard Mode. If Darmanitan loses this Ability while in Zen Mode it reverts to Standard Mode immediately."
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

13155
server/xy/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'

View File

@@ -0,0 +1,52 @@
GEM_BOOST_AMOUNT = 0x14CD
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/items.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
makeTypeResistBerry 'Roseli Berry', 'Fairy'
makeBoostOnTypeItem 'Luminous Moss', 'Water', specialDefense: 1
makeBoostOnTypeItem 'Snowball', 'Ice', attack: 1
makePlateItem 'Pixie Plate', 'Fairy'
makeItem "Assault Vest", ->
this::beginTurn = ->
for move in @pokemon.moves
if move.isNonDamaging()
@pokemon.blockMove(move)
this::editSpecialDefense = (defense) ->
Math.floor(defense * 1.5)
makeItem "Kee Berry", ->
this.eat = (battle, owner) ->
owner.boost(defense: 1)
this::afterBeingHit = (move, user) ->
if move.isPhysical()
@battle.message("#{@pokemon.name}'s #{@displayName} berry activated!")
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeItem "Maranga Berry", ->
this.eat = (battle, owner) ->
owner.boost(specialDefense: 1)
this::afterBeingHit = (move, user) ->
if move.isSpecial()
@battle.message("#{@pokemon.name}'s #{@displayName} berry activated!")
@constructor.eat(@battle, @pokemon)
@pokemon.useItem()
makeItem "Safety Goggles", ->
this::isWeatherDamageImmune = -> true
this::shouldBlockExecution = (move, user) ->
return true if move.hasFlag("powder")
makeItem "Weakness Policy", ->
this::afterBeingHit = (move, user, target, damage, isDirect) ->
if isDirect && !move.isNonDamaging() &&
move.typeEffectiveness(@battle, user, @pokemon) > 1
@pokemon.boost(attack: 2, specialAttack: 2)
@pokemon.useItem()

View File

@@ -0,0 +1,97 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../../bw/data/moves.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
makeChargeMove 'Bounce', ["Gust", "Thunder", "Twister", "Sky Uppercut", "Hurricane", "Smack Down", "Thousand Arrows"], "$1 sprang up!"
extendMove "Defog", ->
@entryHazards.push(Attachment.StickyWeb)
@selectPokemon = (battle, user, target) ->
[ target, user ]
extendMove 'Facade', ->
@burnCalculation = -> 1
extendMove 'Fell Stinger', ->
@afterSuccessfulHit = (battle, user, target) ->
user.boost(attack: 2) if target.isFainted()
makeChargeMove 'Fly', ["Gust", "Thunder", "Twister", "Sky Uppercut", "Hurricane", "Smack Down", "Thousand Arrows"], "$1 flew up high!"
extendMove 'Freeze-Dry', ->
@superEffectiveAgainst = "Water"
makeChargeMove 'Geomancy', "$1 is absorbing power!"
extendMove 'Knock Off', ->
@basePower = (battle, user, target) ->
multiplier = (if target.hasTakeableItem() then 1.5 else 1.0)
Math.floor(multiplier * @power)
extendMove 'Happy Hour', ->
@afterSuccessfulHit = (battle, user, target) ->
battle.message "Everyone is caught up in the happy atmosphere!"
extendMove 'Hidden Power', ->
@basePower = -> @power
makeProtectCounterMove "King's Shield", (battle, user, targets) ->
user.attach(Attachment.KingsShield)
makeTrappingMove "Infestation"
extendMove "Metronome", ->
@impossibleMoves.push("Belch", "Celebrate", "Crafty Shield", "Diamond Storm",
"Happy Hour", "Hold Hands", "Hyperspace Hole", "King's Shield", "Light of Ruin",
"Mat Block", "Spiky Shield", "Steam Eruption", "Thousand Arrows", "Thousand Waves")
extendMove 'Nature Power', ->
@execute = (battle, user, targets) ->
# In Wi-Fi battles, Tri Attack is always chosen.
battle.message "#{@name} turned into Tri Attack!"
triAttack = battle.getMove('Tri Attack')
battle.executeMove(triAttack, user, targets)
extendMove "Parting Shot", ->
@afterSuccessfulHit = (battle, user, target) ->
target.boost(attack: -1, specialAttack: -1, user)
battle.forceSwitch(user)
makeChargeMove 'Phantom Force', [], "$1 vanished instantly!"
extendMove "Rapid Spin", ->
@entryHazards.push(Attachment.StickyWeb)
extendMove 'Skill Swap', ->
@canSwapSameAbilities = true
makeProtectCounterMove "Spiky Shield", (battle, user, targets) ->
user.attach(Attachment.SpikyShield)
makeOpponentFieldMove 'Sticky Web', (battle, user, opponentId) ->
team = battle.getTeam(opponentId)
if !team.attach(Attachment.StickyWeb)
@fail(battle, user)
extendMove 'Topsy-Turvy', ->
@afterSuccessfulHit = (battle, user, target) ->
if target.hasBoosts()
boosts = {}
for stage, value of target.stages
boosts[stage] = -value
target.setBoosts(boosts)
battle.message "#{target.name}'s stat changes were all reversed!"
else
@fail(battle, user)
extendMove 'Toxic', ->
@canMiss = (battle, user, target) ->
return !user.hasType("Poison")
extendMove 'Venom Drench', ->
@use = (battle, user, target) ->
if !target.has(Status.Poison)
@fail(battle, user)
return false
target.boost(attack: -1, specialAttack: -1, speed: -1)

View File

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

29
server/xy/move.coffee Normal file
View File

@@ -0,0 +1,29 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/move.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Move::criticalMultiplier = 1.5
@Move::determineCriticalHitFromLevel = (level, rand) ->
switch level
when -1
true
when 1
rand < 0.0625
when 2
rand < 0.125
when 3
rand < 0.5
else
rand < 1
@Move::numHitsMessage = (hitNumber) ->
times = (if hitNumber == 1 then "time" else "times")
return "Hit #{hitNumber} #{times}!"
# In XY, voice moves and Infiltrator deal direct damage.
oldIsDirectHit = @Move::isDirectHit
@Move::isDirectHit = (battle, user, target) ->
return true if @hasFlag("sound")
return true if user.hasAbility("Infiltrator") && user.isActive()
return oldIsDirectHit.apply(this, arguments)

36
server/xy/pokemon.coffee Normal file
View File

@@ -0,0 +1,36 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/pokemon.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Pokemon::canMegaEvolve = ->
return false if !@hasItem()
return false if @item.type != 'megastone'
[ species, forme ] = @item.mega
return false if @species != species || @forme != 'default'
return false if @team?.filter((p) -> /^mega/.test(p.forme)).length > 0
return true
oldBlockSwitch = @Pokemon::blockSwitch
@Pokemon::blockSwitch = ->
oldBlockSwitch.apply(this, arguments) if !@hasType("Ghost")
oldHasTakeableItem = @Pokemon::hasTakeableItem
@Pokemon::hasTakeableItem = ->
return false if oldHasTakeableItem.apply(this, arguments) == false
if @item.type == 'megastone'
[ species, forme ] = @item.mega
return false if @species == species
return true
# Powder moves no longer affect Grass-type Pokemon.
oldShouldBlockExecution = @Pokemon::shouldBlockExecution
@Pokemon::shouldBlockExecution = (move, user) ->
if move.hasFlag("powder") && @hasType("Grass")
move.fail(@battle, user)
return true
oldShouldBlockExecution.apply(this, arguments)
# In XY, Stance Change is another specially hardcoded ability that cannot change
oldHasChangeableAbility = @Pokemon::hasChangeableAbility
@Pokemon::hasChangeableAbility = ->
!@hasAbility("Stance Change") && oldHasChangeableAbility.call(this)

213
server/xy/priorities.coffee Normal file
View File

@@ -0,0 +1,213 @@
{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
Ability.StanceChange
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
Ability.Protean
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.StickyWeb
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.SpikyShield
Attachment.KingsShield
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.KingsShield
Attachment.SpikyShield
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
Ability.Overcoat
Item.SafetyGoggles
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
Ability.Bulletproof
]

3
server/xy/queries.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/queries.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

3
server/xy/rng.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/rng.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))

3
server/xy/team.coffee Normal file
View File

@@ -0,0 +1,3 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/team.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8')))

29
server/xy/util.coffee Normal file
View File

@@ -0,0 +1,29 @@
coffee = require 'coffee-script'
path = require('path').resolve(__dirname, '../bw/util.coffee')
eval(coffee.compile(require('fs').readFileSync(path, 'utf8'), bare: true))
@Type.Fairy = Type.Fairy = 17
@Type["???"] = Type["???"] = 18
typeChart = [
# Nor Fir Wat Ele Gra Ice Fig Poi Gro Fly Psy Bug Roc Gho Dra Dar Ste Fai, ???
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, .5, 0, 1, 1, .5, 1, 1 ], # Nor
[ 1, .5, .5, 1, 2, 2, 1, 1, 1, 1, 1, 2, .5, 1, .5, 1, 2, 1, 1 ], # Fir
[ 1, 2, .5, 1, .5, 1, 1, 1, 2, 1, 1, 1, 2, 1, .5, 1, 1, 1, 1 ], # Wat
[ 1, 1, 2, .5, .5, 1, 1, 1, 0, 2, 1, 1, 1, 1, .5, 1, 1, 1, 1 ], # Ele
[ 1, .5, 2, 1, .5, 1, 1, .5, 2, .5, 1, .5, 2, 1, .5, 1, .5, 1, 1 ], # Gra
[ 1, .5, .5, 1, 2, .5, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, .5, 1, 1 ], # Ice
[ 2, 1, 1, 1, 1, 2, 1, .5, 1, .5, .5, .5, 2, 0, 1, 2, 2, .5, 1 ], # Fig
[ 1, 1, 1, 1, 2, 1, 1, .5, .5, 1, 1, 1, .5, .5, 1, 1, 0, 2, 1 ], # Poi
[ 1, 2, 1, 2, .5, 1, 1, 2, 1, 0, 1, .5, 2, 1, 1, 1, 2, 1, 1 ], # Gro
[ 1, 1, 1, .5, 2, 1, 2, 1, 1, 1, 1, 2, .5, 1, 1, 1, .5, 1, 1 ], # Fly
[ 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, .5, 1, 1, 1, 1, 0, .5, 1, 1 ], # Psy
[ 1, .5, 1, 1, 2, 1, .5, .5, 1, .5, 2, 1, 1, .5, 1, 2, .5, .5, 1 ], # Bug
[ 1, 2, 1, 1, 1, 2, .5, 1, .5, 2, 1, 2, 1, 1, 1, 1, .5, 1, 1 ], # Roc
[ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, .5, 1, 1, 1 ], # Gho
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, .5, 0, 1 ], # Dra
[ 1, 1, 1, 1, 1, 1, .5, 1, 1, 1, 2, 1, 1, 2, 1, .5, 1, .5, 1 ], # Dar
[ 1, .5, .5, .5, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, .5, 2, 1 ], # Ste
[ 1, .5, 1, 1, 1, 1, 2, .5, 1, 1, 1, 1, 1, 1, 2, 2, .5, 1, 1 ], # Fai
[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] # ???
]