mirror of
https://gitlab.com/Deukhoofd/BattleSim.git
synced 2025-10-28 02:00:04 +00:00
Lots of stuff
This commit is contained in:
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 ] # ???
|
||||
]
|
||||
Reference in New Issue
Block a user