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:
220
server/achievements.coffee
Normal file
220
server/achievements.coffee
Normal 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
68
server/alts.coffee
Normal 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
209
server/auth.coffee
Normal 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
1580
server/bw/attachment.coffee
Normal file
File diff suppressed because it is too large
Load Diff
918
server/bw/battle.coffee
Normal file
918
server/bw/battle.coffee
Normal 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}]"
|
||||
128
server/bw/battle_controller.coffee
Normal file
128
server/bw/battle_controller.coffee
Normal 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()
|
||||
987
server/bw/data/abilities.coffee
Normal file
987
server/bw/data/abilities.coffee
Normal 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'
|
||||
62404
server/bw/data/data_formes.json
Normal file
62404
server/bw/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
1838
server/bw/data/data_items.json
Normal file
1838
server/bw/data/data_items.json
Normal file
File diff suppressed because it is too large
Load Diff
11270
server/bw/data/data_moves.json
Normal file
11270
server/bw/data/data_moves.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
server/bw/data/data_species.json
Normal file
6615
server/bw/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
4
server/bw/data/index.coffee
Normal file
4
server/bw/data/index.coffee
Normal 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
633
server/bw/data/items.coffee
Normal 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
1956
server/bw/data/moves.coffee
Normal file
File diff suppressed because it is too large
Load Diff
2
server/bw/data/pokemon.coffee
Normal file
2
server/bw/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
@SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
435
server/bw/move.coffee
Normal file
435
server/bw/move.coffee
Normal 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
701
server/bw/pokemon.coffee
Normal 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
204
server/bw/priorities.coffee
Normal 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
46
server/bw/queries.coffee
Normal 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
15
server/bw/rng.coffee
Normal 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
133
server/bw/team.coffee
Normal 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
55
server/bw/util.coffee
Normal 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
350
server/commands.coffee
Normal 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
360
server/conditions.coffee
Normal 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
68
server/database.coffee
Normal 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}
|
||||
46819
server/dp/data/data_formes.json
Normal file
46819
server/dp/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
server/dp/data/data_species.json
Normal file
6615
server/dp/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
1
server/dp/data/index.coffee
Normal file
1
server/dp/data/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
{@FormeData, @SpeciesData} = require('./pokemon')
|
||||
2
server/dp/data/pokemon.coffee
Normal file
2
server/dp/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
# @SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
13
server/elo.coffee
Normal file
13
server/elo.coffee
Normal 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
98
server/generations.coffee
Normal 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
157
server/glicko2.coffee
Normal 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
|
||||
}
|
||||
16445
server/gs/data/data_formes.json
Normal file
16445
server/gs/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
server/gs/data/data_species.json
Normal file
6615
server/gs/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
1
server/gs/data/index.coffee
Normal file
1
server/gs/data/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
{@FormeData, @SpeciesData} = require('./pokemon')
|
||||
2
server/gs/data/pokemon.coffee
Normal file
2
server/gs/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
# @SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
144
server/in/attachment.coffee
Normal file
144
server/in/attachment.coffee
Normal 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
52
server/in/battle.coffee
Normal 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)
|
||||
3
server/in/battle_controller.coffee
Normal file
3
server/in/battle_controller.coffee
Normal 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))
|
||||
392
server/in/data/abilities.coffee
Normal file
392
server/in/data/abilities.coffee
Normal 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)
|
||||
|
||||
641
server/in/data/data_abilities.json
Normal file
641
server/in/data/data_abilities.json
Normal 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."
|
||||
}
|
||||
}
|
||||
67254
server/in/data/data_formes.json
Normal file
67254
server/in/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
3412
server/in/data/data_items.json
Normal file
3412
server/in/data/data_items.json
Normal file
File diff suppressed because it is too large
Load Diff
13332
server/in/data/data_moves.json
Normal file
13332
server/in/data/data_moves.json
Normal file
File diff suppressed because it is too large
Load Diff
7855
server/in/data/data_species.json
Normal file
7855
server/in/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
1
server/in/data/delta_formes.json
Normal file
1
server/in/data/delta_formes.json
Normal file
@@ -0,0 +1 @@
|
||||
{ }
|
||||
4
server/in/data/index.coffee
Normal file
4
server/in/data/index.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
{@Moves, @MoveData, @MoveList} = require './moves'
|
||||
{@Ability} = require './abilities'
|
||||
{@Item, @ItemData} = require './items'
|
||||
{@SpeciesData, @FormeData} = require './pokemon'
|
||||
52
server/in/data/items.coffee
Normal file
52
server/in/data/items.coffee
Normal 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
141
server/in/data/moves.coffee
Normal 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)
|
||||
|
||||
2
server/in/data/pokemon.coffee
Normal file
2
server/in/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
@SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
29
server/in/move.coffee
Normal file
29
server/in/move.coffee
Normal 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
36
server/in/pokemon.coffee
Normal 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
216
server/in/priorities.coffee
Normal 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
3
server/in/queries.coffee
Normal 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
3
server/in/rng.coffee
Normal 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
3
server/in/team.coffee
Normal 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
29
server/in/util.coffee
Normal 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
432
server/index.coffee
Normal 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
30
server/logger.coffee
Normal 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
5
server/modify.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
{_} = require('underscore')
|
||||
|
||||
|
||||
formes = (req, res) ->
|
||||
console.log("1")
|
||||
31
server/player.coffee
Normal file
31
server/player.coffee
Normal 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
134
server/queue.coffee
Normal 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
239
server/ratings.coffee
Normal 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]})
|
||||
8146
server/rb/data/data_formes.json
Normal file
8146
server/rb/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
server/rb/data/data_species.json
Normal file
6615
server/rb/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
1
server/rb/data/index.coffee
Normal file
1
server/rb/data/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
{@FormeData, @SpeciesData} = require('./pokemon')
|
||||
165
server/rb/data/moves.yml
Normal file
165
server/rb/data/moves.yml
Normal 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}
|
||||
2
server/rb/data/pokemon.coffee
Normal file
2
server/rb/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
# @SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
27
server/redis.coffee
Normal file
27
server/redis.coffee
Normal 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
87
server/replays.coffee
Normal 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
65
server/rooms.coffee
Normal 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)
|
||||
31067
server/rs/data/data_formes.json
Normal file
31067
server/rs/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
server/rs/data/data_species.json
Normal file
6615
server/rs/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
1
server/rs/data/index.coffee
Normal file
1
server/rs/data/index.coffee
Normal file
@@ -0,0 +1 @@
|
||||
{@FormeData, @SpeciesData} = require('./pokemon')
|
||||
2
server/rs/data/pokemon.coffee
Normal file
2
server/rs/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
# @SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
31
server/schedule.coffee
Normal file
31
server/schedule.coffee
Normal 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
483
server/server.coffee
Normal 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
52
server/user.coffee
Normal 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
41
server/user_store.coffee
Normal 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()
|
||||
78
server/xy/attachment.coffee
Normal file
78
server/xy/attachment.coffee
Normal 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
39
server/xy/battle.coffee
Normal 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)
|
||||
3
server/xy/battle_controller.coffee
Normal file
3
server/xy/battle_controller.coffee
Normal 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))
|
||||
169
server/xy/data/abilities.coffee
Normal file
169
server/xy/data/abilities.coffee
Normal 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
|
||||
566
server/xy/data/data_abilities.json
Normal file
566
server/xy/data/data_abilities.json
Normal 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."
|
||||
}
|
||||
}
|
||||
62324
server/xy/data/data_formes.json
Normal file
62324
server/xy/data/data_formes.json
Normal file
File diff suppressed because it is too large
Load Diff
2971
server/xy/data/data_items.json
Normal file
2971
server/xy/data/data_items.json
Normal file
File diff suppressed because it is too large
Load Diff
13155
server/xy/data/data_moves.json
Normal file
13155
server/xy/data/data_moves.json
Normal file
File diff suppressed because it is too large
Load Diff
7343
server/xy/data/data_species.json
Normal file
7343
server/xy/data/data_species.json
Normal file
File diff suppressed because it is too large
Load Diff
4
server/xy/data/index.coffee
Normal file
4
server/xy/data/index.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
{@Moves, @MoveData, @MoveList} = require './moves'
|
||||
{@Ability} = require './abilities'
|
||||
{@Item, @ItemData} = require './items'
|
||||
{@SpeciesData, @FormeData} = require './pokemon'
|
||||
52
server/xy/data/items.coffee
Normal file
52
server/xy/data/items.coffee
Normal 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()
|
||||
97
server/xy/data/moves.coffee
Normal file
97
server/xy/data/moves.coffee
Normal 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)
|
||||
2
server/xy/data/pokemon.coffee
Normal file
2
server/xy/data/pokemon.coffee
Normal file
@@ -0,0 +1,2 @@
|
||||
@SpeciesData = require('./data_species.json')
|
||||
@FormeData = require('./data_formes.json')
|
||||
29
server/xy/move.coffee
Normal file
29
server/xy/move.coffee
Normal 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
36
server/xy/pokemon.coffee
Normal 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
213
server/xy/priorities.coffee
Normal 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
3
server/xy/queries.coffee
Normal 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
3
server/xy/rng.coffee
Normal 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
3
server/xy/team.coffee
Normal 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
29
server/xy/util.coffee
Normal 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 ] # ???
|
||||
]
|
||||
Reference in New Issue
Block a user