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