BattleSim/server/bw/pokemon.coffee

717 lines
22 KiB
CoffeeScript

{_} = 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 || @getMaxLevel()
@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
getMaxLevel: ->
level = 100
level
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
try
@ppHash[move.name] = @maxPPHash[move.name] = pp || (move.pp * 8/5)
catch error
console.log(move)
setType: (newType) ->
@types = newType
# 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)
if @hasAbility("Ethereal Shroud")
ghosteffect = util.typeEffectiveness(type, ["Ghost"])
if ghosteffect < 1
multiplier *= ghosteffect
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}