mirror of
https://gitlab.com/Deukhoofd/BattleSim.git
synced 2025-10-27 18:00:03 +00:00
Lots of stuff
This commit is contained in:
2778
test/bw/abilities.coffee
Normal file
2778
test/bw/abilities.coffee
Normal file
File diff suppressed because it is too large
Load Diff
68
test/bw/attachments.coffee
Normal file
68
test/bw/attachments.coffee
Normal file
@@ -0,0 +1,68 @@
|
||||
require '../helpers'
|
||||
|
||||
{Attachments, Attachment, BaseAttachment} = require '../../server/bw/attachment'
|
||||
should = require 'should'
|
||||
|
||||
describe "An Attachment list", ->
|
||||
class TestAttachment extends BaseAttachment
|
||||
name: "TestAttachment"
|
||||
maxLayers: 2
|
||||
|
||||
class OtherAttachment extends BaseAttachment
|
||||
name: "OtherAttachment"
|
||||
|
||||
beforeEach ->
|
||||
@attachments = new Attachments()
|
||||
|
||||
it "will not add attachments past the maximum stack", ->
|
||||
should.exist @attachments.push(TestAttachment)
|
||||
should.exist @attachments.push(TestAttachment)
|
||||
should.not.exist @attachments.push(TestAttachment)
|
||||
|
||||
describe '#unattach', ->
|
||||
it "removes the current attachment", ->
|
||||
@attachments.push(TestAttachment)
|
||||
@attachments.unattach(TestAttachment)
|
||||
@attachments.attachments.should.have.length(0)
|
||||
|
||||
it "does not remove other attachments if none is found", ->
|
||||
@attachments.push(TestAttachment)
|
||||
@attachments.unattach(OtherAttachment)
|
||||
@attachments.attachments.should.have.length(1)
|
||||
@attachments.attachments[0].should.be.instanceOf(TestAttachment)
|
||||
|
||||
describe '#unattachAll', ->
|
||||
it 'is never passed an undefined attachment', ->
|
||||
stub = @sandbox.stub().returns(true)
|
||||
@attachments.push(TestAttachment)
|
||||
@attachments.push(OtherAttachment)
|
||||
(=> @attachments.unattachAll(stub)).should.not.throw()
|
||||
stub.calledWithMatch(undefined).should.be.false
|
||||
stub.calledWithMatch(null).should.be.false
|
||||
@attachments.attachments.should.be.empty
|
||||
|
||||
describe '#getPassable', ->
|
||||
beforeEach ->
|
||||
@attachments.attachments.push(new Attachment.Embargo())
|
||||
@attachments.attachments.push(new Attachment.Yawn())
|
||||
@attachments.attachments.push(new Attachment.Ingrain())
|
||||
@attachments.attachments.push(new Attachment.AquaRing())
|
||||
@attachments.attachments.push(new Attachment.Disable())
|
||||
@attachments.attachments.push(new Attachment.Torment())
|
||||
@attachments.attachments.push(new Attachment.Substitute())
|
||||
@attachments.attachments.push(new Attachment.Curse())
|
||||
@attachments.attachments.push(new Attachment.LeechSeed())
|
||||
@attachments.attachments.push(new Attachment.MagnetRise())
|
||||
@attachments.attachments.push(new Attachment.Confusion())
|
||||
|
||||
it "returns an array of passable attachments already attached", ->
|
||||
attachments = @attachments.getPassable()
|
||||
attachments.should.not.containEql(Attachment.Disable)
|
||||
attachments.should.not.containEql(Attachment.Torment)
|
||||
attachments.should.not.containEql(Attachment.Yawn)
|
||||
attachments.should.containEql(Attachment.Ingrain)
|
||||
attachments.should.containEql(Attachment.AquaRing)
|
||||
attachments.should.containEql(Attachment.Embargo)
|
||||
attachments.should.containEql(Attachment.Substitute)
|
||||
attachments.should.containEql(Attachment.Curse)
|
||||
attachments.should.containEql(Attachment.LeechSeed)
|
||||
477
test/bw/battle.coffee
Normal file
477
test/bw/battle.coffee
Normal file
@@ -0,0 +1,477 @@
|
||||
require '../helpers'
|
||||
|
||||
sinon = require('sinon')
|
||||
|
||||
{Attachment} = require('../../server/bw/attachment')
|
||||
{Battle} = require('../../server/bw/battle')
|
||||
{Weather} = require('../../shared/weather')
|
||||
{Conditions} = require '../../shared/conditions'
|
||||
{Factory} = require('../factory')
|
||||
{BattleServer} = require('../../server/server')
|
||||
{Protocol} = require '../../shared/protocol'
|
||||
shared = require('../shared')
|
||||
should = require 'should'
|
||||
{_} = require 'underscore'
|
||||
ratings = require('../../server/ratings')
|
||||
|
||||
describe 'Battle', ->
|
||||
beforeEach ->
|
||||
@server = new BattleServer()
|
||||
shared.create.call this,
|
||||
format: 'xy1000'
|
||||
team1: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
|
||||
it 'starts at turn 1', ->
|
||||
@battle.turn.should.equal 1
|
||||
|
||||
describe '#hasWeather(weatherName)', ->
|
||||
it 'returns true if the current battle weather is weatherName', ->
|
||||
@battle.weather = "Sunny"
|
||||
@battle.hasWeather("Sunny").should.be.true
|
||||
|
||||
it 'returns false on non-None in presence of a weather-cancel ability', ->
|
||||
@battle.weather = "Sunny"
|
||||
@sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
|
||||
@battle.hasWeather("Sunny").should.be.false
|
||||
|
||||
it 'returns true on None in presence of a weather-cancel ability', ->
|
||||
@battle.weather = "Sunny"
|
||||
@sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
|
||||
@battle.hasWeather("None").should.be.true
|
||||
|
||||
describe '#recordMove', ->
|
||||
it "records a player's move", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1)
|
||||
should.exist(action)
|
||||
action.should.have.property("move")
|
||||
action.move.should.equal @battle.getMove('Tackle')
|
||||
|
||||
it "does not record a move if player has already made an action", ->
|
||||
length = @battle.pokemonActions.length
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.pokemonActions.length.should.equal(1 + length)
|
||||
|
||||
describe '#undoCompletedRequest', ->
|
||||
it "fails if the player didn't make any action", ->
|
||||
@battle.undoCompletedRequest(@id1).should.be.false
|
||||
|
||||
it "fails on the second turn as well if the player didn't make any action", ->
|
||||
@controller.makeMove(@id1, "Mach Punch")
|
||||
@controller.makeMove(@id2, "Mach Punch")
|
||||
@battle.turn.should.equal 2
|
||||
@battle.undoCompletedRequest(@id1).should.be.false
|
||||
|
||||
it "succeeds if the player selected an action already", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.pokemonActions.should.not.be.empty
|
||||
@battle.undoCompletedRequest(@id1).should.be.true
|
||||
@battle.pokemonActions.should.be.empty
|
||||
|
||||
it "can cancel an action multiple times", ->
|
||||
for i in [0..5]
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.pokemonActions.should.not.be.empty
|
||||
@battle.undoCompletedRequest(@id1).should.be.true
|
||||
@battle.pokemonActions.should.be.empty
|
||||
|
||||
describe '#recordSwitch', ->
|
||||
it "records a player's switch", ->
|
||||
@battle.recordSwitch(@id1, 1)
|
||||
action = _(@battle.pokemonActions).find((a) => a.pokemon == @p1)
|
||||
should.exist(action)
|
||||
action.should.have.property("to")
|
||||
action.to.should.equal 1
|
||||
|
||||
it "does not record a switch if player has already made an action", ->
|
||||
length = @battle.pokemonActions.length
|
||||
@battle.recordSwitch(@id1, 1)
|
||||
@battle.recordSwitch(@id1, 1)
|
||||
@battle.pokemonActions.length.should.equal(1 + length)
|
||||
|
||||
describe '#performSwitch', ->
|
||||
it "swaps pokemon positions of a player's team", ->
|
||||
[poke1, poke2] = @team1.pokemon
|
||||
@battle.performSwitch(@p1, 1)
|
||||
@team1.pokemon.slice(0, 2).should.eql [poke2, poke1]
|
||||
|
||||
it "calls the pokemon's switchOut() method", ->
|
||||
pokemon = @p1
|
||||
mock = @sandbox.mock(pokemon)
|
||||
mock.expects('switchOut').once()
|
||||
@battle.performSwitch(@p1, 1)
|
||||
mock.verify()
|
||||
|
||||
describe "#setWeather", ->
|
||||
it "can last a set number of turns", ->
|
||||
@battle.setWeather(Weather.SUN, 5)
|
||||
for i in [0...5]
|
||||
@battle.endTurn()
|
||||
@battle.weather.should.equal Weather.NONE
|
||||
|
||||
describe "weather", ->
|
||||
it "damages pokemon who are not of a certain type", ->
|
||||
@battle.setWeather(Weather.SAND)
|
||||
@battle.endTurn()
|
||||
maxHP = @p1.stat('hp')
|
||||
(maxHP - @p1.currentHP).should.equal Math.floor(maxHP / 16)
|
||||
(maxHP - @p2.currentHP).should.equal Math.floor(maxHP / 16)
|
||||
|
||||
@battle.setWeather(Weather.HAIL)
|
||||
@battle.endTurn()
|
||||
maxHP = @p1.stat('hp')
|
||||
(maxHP - @p1.currentHP).should.equal 2*Math.floor(maxHP / 16)
|
||||
(maxHP - @p2.currentHP).should.equal 2*Math.floor(maxHP / 16)
|
||||
|
||||
describe "move PP", ->
|
||||
it "goes down after a pokemon uses a move", ->
|
||||
pokemon = @p1
|
||||
move = @p1.moves[0]
|
||||
@battle.performMove(@p1, move)
|
||||
@p1.pp(move).should.equal(@p1.maxPP(move) - 1)
|
||||
|
||||
describe "#performMove", ->
|
||||
it "records this move as the battle's last move", ->
|
||||
move = @p1.moves[0]
|
||||
@battle.performMove(@p1, move)
|
||||
|
||||
should.exist @battle.lastMove
|
||||
@battle.lastMove.should.equal move
|
||||
|
||||
describe "#bump", ->
|
||||
it "bumps a pokemon to the front of its priority bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Splash'))
|
||||
@battle.determineTurnOrder()
|
||||
|
||||
# Get last pokemon to move and bump it up
|
||||
{pokemon} = @battle.pokemonActions[@battle.pokemonActions.length - 1]
|
||||
@battle.bump(pokemon)
|
||||
@battle.pokemonActions[0].pokemon.should.eql pokemon
|
||||
|
||||
it "bumps a pokemon to the front of a specific priority bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Mach Punch'))
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
@battle.bump(@p1, @battle.getMove('Mach Punch').priority)
|
||||
queue[0].pokemon.should.eql @p1
|
||||
|
||||
it "still works even if there's nothing in that bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Mach Punch'))
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
queue.should.have.length(2)
|
||||
@battle.bump(@p1)
|
||||
queue.should.have.length(2)
|
||||
queue[0].pokemon.should.eql @p2
|
||||
queue[1].pokemon.should.eql @p1
|
||||
|
||||
it "is a no-op if no actions", ->
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
queue.should.have.length(0)
|
||||
@battle.bump(@p1)
|
||||
queue.should.have.length(0)
|
||||
|
||||
describe "#delay", ->
|
||||
it "delays a pokemon to the end of its priority bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Splash'))
|
||||
@battle.determineTurnOrder()
|
||||
|
||||
# Get first pokemon to move and delay it
|
||||
{pokemon} = @battle.pokemonActions[0]
|
||||
@battle.delay(pokemon)
|
||||
@battle.pokemonActions[1].pokemon.should.eql pokemon
|
||||
|
||||
it "delays a pokemon to the end of a specific priority bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Mach Punch'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Tackle'))
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
@battle.delay(@p1, @battle.getMove('Tackle').priority)
|
||||
queue[1].pokemon.should.eql @p1
|
||||
|
||||
it "still works even if there's nothing in that bracket", ->
|
||||
@battle.recordMove(@id1, @battle.getMove('Tackle'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Mach Punch'))
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
queue.should.have.length(2)
|
||||
@battle.delay(@p1)
|
||||
queue.should.have.length(2)
|
||||
queue[0].pokemon.should.eql @p2
|
||||
queue[1].pokemon.should.eql @p1
|
||||
|
||||
it "is a no-op if no actions", ->
|
||||
queue = @battle.determineTurnOrder()
|
||||
|
||||
queue.should.have.length(0)
|
||||
@battle.delay(@p1)
|
||||
queue.should.have.length(0)
|
||||
|
||||
describe "#weatherUpkeep", ->
|
||||
it "does not damage Pokemon if a weather-cancel ability is on the field", ->
|
||||
@battle.setWeather(Weather.HAIL)
|
||||
@sandbox.stub(@battle, 'hasWeatherCancelAbilityOnField', -> true)
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.not.be.lessThan @p1.stat('hp')
|
||||
@p2.currentHP.should.not.be.lessThan @p2.stat('hp')
|
||||
|
||||
it "does not damage a Pokemon who is immune to a weather", ->
|
||||
@battle.setWeather(Weather.HAIL)
|
||||
@sandbox.stub(@p2, 'isWeatherDamageImmune', -> true)
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.be.lessThan @p1.stat('hp')
|
||||
@p2.currentHP.should.not.be.lessThan @p2.stat('hp')
|
||||
|
||||
describe "#add", ->
|
||||
it "adds the spectator to an internal array", ->
|
||||
connection = @stubSpark()
|
||||
spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
|
||||
length = @battle.sparks.length
|
||||
@battle.add(connection)
|
||||
@battle.sparks.should.have.length(length + 1)
|
||||
@battle.sparks.should.containEql(connection)
|
||||
|
||||
it "gives the spectator battle information", ->
|
||||
connection = @stubSpark()
|
||||
spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
|
||||
spy = @sandbox.spy(connection, 'send')
|
||||
@battle.add(connection)
|
||||
{id, numActive, log} = @battle
|
||||
spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true
|
||||
|
||||
it "receives the correct set of initial teams", ->
|
||||
connection = @stubSpark()
|
||||
spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
|
||||
spy = @sandbox.spy(connection, 'send')
|
||||
teams = @battle.getTeams().map((team) -> team.toJSON(hidden: true))
|
||||
@team1.switch(@p1, 1)
|
||||
|
||||
@battle.add(connection)
|
||||
{id, numActive, log} = @battle
|
||||
spy.calledWithMatch("spectateBattle", id, sinon.match.string, numActive, null, @battle.playerNames, log).should.be.true
|
||||
|
||||
it "does not add a spectator twice", ->
|
||||
connection = @stubSpark()
|
||||
spectator = @server.findOrCreateUser(id: 1, name: "derp", connection)
|
||||
length = @battle.sparks.length
|
||||
@battle.add(connection)
|
||||
@battle.add(connection)
|
||||
@battle.sparks.should.have.length(length + 1)
|
||||
|
||||
describe "#getWinner", ->
|
||||
it "returns player 1 if player 2's team has all fainted", ->
|
||||
pokemon.faint() for pokemon in @team2.pokemon
|
||||
@battle.getWinner().should.equal(@id1)
|
||||
|
||||
it "returns player 2 if player 1's team has all fainted", ->
|
||||
pokemon.faint() for pokemon in @team1.pokemon
|
||||
@battle.getWinner().should.equal(@id2)
|
||||
|
||||
it "declares the pokemon dying of recoil the winner", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Talonflame', moves: ["Brave Bird"])]
|
||||
team2: [Factory('Magikarp')]
|
||||
|
||||
@p1.currentHP = @p2.currentHP = 1
|
||||
spy = @sandbox.spy(@battle, 'getWinner')
|
||||
@controller.makeMove(@id1, "Brave Bird")
|
||||
@controller.makeMove(@id2, "Splash")
|
||||
spy.returned(@id1).should.be.true
|
||||
|
||||
describe "#endBattle", ->
|
||||
it "cannot end multiple times", ->
|
||||
spy = @sandbox.spy(@battle, 'emit')
|
||||
spy.withArgs("end")
|
||||
@battle.endBattle()
|
||||
@battle.endBattle()
|
||||
spy.withArgs("end").calledOnce.should.be.true
|
||||
|
||||
it "marks the battle as over", ->
|
||||
@battle.endBattle()
|
||||
@battle.isOver().should.be.true
|
||||
|
||||
it "updates the winner and losers' ratings", (done) ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
conditions: [ Conditions.RATED_BATTLE ]
|
||||
@battle.on "ratingsUpdated", =>
|
||||
ratings.getPlayer @id1, (err, rating1) =>
|
||||
ratings.getPlayer @id2, (err, rating2) =>
|
||||
rating1.rating.should.be.greaterThan(ratings.DEFAULT_RATING)
|
||||
rating2.rating.should.be.lessThan(ratings.DEFAULT_RATING)
|
||||
done()
|
||||
|
||||
@p2.currentHP = 1
|
||||
mock = @sandbox.mock(@controller)
|
||||
@controller.makeMove(@id1, 'Mach Punch')
|
||||
@controller.makeMove(@id2, 'Psychic')
|
||||
|
||||
it "doesn't update ratings if an unrated battle", (done) ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
@battle.on 'end', =>
|
||||
ratings.getPlayer @id1, (err, rating1) =>
|
||||
ratings.getPlayer @id2, (err, rating2) =>
|
||||
rating1.rating.should.equal(ratings.DEFAULT_RATING)
|
||||
rating2.rating.should.equal(ratings.DEFAULT_RATING)
|
||||
done()
|
||||
|
||||
@p2.currentHP = 1
|
||||
mock = @sandbox.mock(@controller)
|
||||
@controller.makeMove(@id1, 'Mach Punch')
|
||||
@controller.makeMove(@id2, 'Psychic')
|
||||
|
||||
describe "#forfeit", ->
|
||||
it "prematurely ends the battle", ->
|
||||
spy = @sandbox.spy(@battle, 'tell')
|
||||
spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
|
||||
@battle.forfeit(@id1)
|
||||
spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
|
||||
.called.should.be.true
|
||||
|
||||
it "does not forfeit if the player given is invalid", ->
|
||||
mock = @sandbox.mock(@battle).expects('tell').never()
|
||||
@battle.forfeit('this definitely should not work')
|
||||
mock.verify()
|
||||
|
||||
it "cannot forfeit multiple times", ->
|
||||
spy = @sandbox.spy(@battle, 'tell')
|
||||
spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
|
||||
@battle.forfeit(@id1)
|
||||
@battle.forfeit(@id1)
|
||||
spy.withArgs(Protocol.FORFEIT_BATTLE, @battle.getPlayerIndex(@id1))
|
||||
.calledOnce.should.be.true
|
||||
|
||||
it "marks the battle as over", ->
|
||||
@battle.forfeit(@id1)
|
||||
@battle.isOver().should.be.true
|
||||
|
||||
it "doesn't update the winner and losers' ratings if not a rated battle", (done) ->
|
||||
@battle.on 'end', =>
|
||||
ratings.getPlayer @id1, (err, rating1) =>
|
||||
ratings.getPlayer @id2, (err, rating2) =>
|
||||
rating1.rating.should.equal(ratings.DEFAULT_RATING)
|
||||
rating2.rating.should.equal(ratings.DEFAULT_RATING)
|
||||
done()
|
||||
@battle.forfeit(@id2)
|
||||
|
||||
describe "#hasStarted", ->
|
||||
it "returns false if the battle has not started", ->
|
||||
battle = new Battle('id', [])
|
||||
battle.hasStarted().should.be.false
|
||||
|
||||
it "returns true if the battle has started", ->
|
||||
battle = new Battle('id', [])
|
||||
battle.begin()
|
||||
battle.hasStarted().should.be.true
|
||||
|
||||
describe "#getAllAttachments", ->
|
||||
it "returns a list of attachments for all pokemon, teams, and battles", ->
|
||||
@battle.attach(Attachment.TrickRoom)
|
||||
@team2.attach(Attachment.Reflect)
|
||||
@p1.attach(Attachment.Ingrain)
|
||||
attachments = @battle.getAllAttachments()
|
||||
should.exist(attachments)
|
||||
attachments = attachments.map((a) -> a.constructor)
|
||||
attachments.length.should.be.greaterThan(2)
|
||||
attachments.should.containEql(Attachment.TrickRoom)
|
||||
attachments.should.containEql(Attachment.Reflect)
|
||||
attachments.should.containEql(Attachment.Ingrain)
|
||||
|
||||
describe "#query", ->
|
||||
it "queries all attachments attached to a specific event", ->
|
||||
@battle.attach(Attachment.TrickRoom)
|
||||
@team2.attach(Attachment.Reflect)
|
||||
@p1.attach(Attachment.Ingrain)
|
||||
mocks = []
|
||||
mocks.push @sandbox.mock(Attachment.TrickRoom.prototype)
|
||||
mocks.push @sandbox.mock(Attachment.Reflect.prototype)
|
||||
mocks.push @sandbox.mock(Attachment.Ingrain.prototype)
|
||||
mock.expects("endTurn").once() for mock in mocks
|
||||
attachments = @battle.query("endTurn")
|
||||
mock.verify() for mock in mocks
|
||||
|
||||
describe "#getOpponents", ->
|
||||
it "returns all opponents of a particular pokemon as an array", ->
|
||||
@battle.getOpponents(@p1).should.be.an.instanceOf(Array)
|
||||
@battle.getOpponents(@p1).should.have.length(1)
|
||||
|
||||
it "does not include fainted opponents", ->
|
||||
@p2.faint()
|
||||
@battle.getOpponents(@p1).should.have.length(0)
|
||||
|
||||
describe "#sendRequestTo", ->
|
||||
it "sends all requests to a certain player", ->
|
||||
mock = @sandbox.mock(@battle).expects('tellPlayer').once()
|
||||
mock.withArgs(@id1, Protocol.REQUEST_ACTIONS)
|
||||
@battle.sendRequestTo(@id1)
|
||||
mock.verify()
|
||||
|
||||
describe "expiration", ->
|
||||
beforeEach ->
|
||||
shared.create.call(this)
|
||||
|
||||
it "ends ongoing battles after a specific amount", ->
|
||||
time = Battle::ONGOING_BATTLE_TTL
|
||||
@clock.tick(time)
|
||||
@battle.isOver().should.be.true
|
||||
|
||||
it "sends BATTLE_EXPIRED after a specific amount after battle end", ->
|
||||
time = Battle::ENDED_BATTLE_TTL
|
||||
delta = 5000
|
||||
spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED)
|
||||
@clock.tick(delta)
|
||||
@battle.forfeit(@id1)
|
||||
@battle.isOver().should.be.true
|
||||
spy.withArgs(Protocol.BATTLE_EXPIRED).called.should.be.false
|
||||
|
||||
@battle.tell.restore()
|
||||
spy = @sandbox.spy(@battle, 'tell').withArgs(Protocol.BATTLE_EXPIRED)
|
||||
@clock.tick(time)
|
||||
@battle.isOver().should.be.true
|
||||
spy.withArgs(Protocol.BATTLE_EXPIRED).calledOnce.should.be.true
|
||||
|
||||
it "doesn't call 'expire' if expiring twice", ->
|
||||
firstExpire = 24 * 60 * 60 * 1000
|
||||
secondExpire = 48 * 60 * 60 * 1000
|
||||
spy = @sandbox.spy(@battle, 'expire')
|
||||
@clock.tick(firstExpire)
|
||||
@battle.isOver().should.be.true
|
||||
@clock.tick(secondExpire)
|
||||
spy.calledOnce.should.be.true
|
||||
|
||||
it "doesn't call 'end' twice", ->
|
||||
longExpire = 48 * 60 * 60 * 1000
|
||||
spy = @sandbox.spy()
|
||||
@battle.on('end', spy)
|
||||
@battle.forfeit(@id1)
|
||||
@battle.isOver().should.be.true
|
||||
|
||||
@clock.tick(longExpire)
|
||||
spy.calledOnce.should.be.true
|
||||
|
||||
describe "Rated battles", ->
|
||||
beforeEach ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
conditions: [ Conditions.RATED_BATTLE ]
|
||||
|
||||
it "updates the winner and losers' ratings", (done) ->
|
||||
@battle.on "ratingsUpdated", =>
|
||||
ratings.getPlayer @id1, (err, rating1) =>
|
||||
ratings.getPlayer @id2, (err, rating2) =>
|
||||
defaultPlayer = ratings.algorithm.createPlayer()
|
||||
rating1.rating.should.be.greaterThan(defaultPlayer.rating)
|
||||
rating2.rating.should.be.lessThan(defaultPlayer.rating)
|
||||
done()
|
||||
@battle.forfeit(@id2)
|
||||
139
test/bw/battle_controller.coffee
Normal file
139
test/bw/battle_controller.coffee
Normal file
@@ -0,0 +1,139 @@
|
||||
require '../helpers'
|
||||
|
||||
{Factory} = require '../factory'
|
||||
should = require 'should'
|
||||
shared = require '../shared'
|
||||
|
||||
describe 'BattleController', ->
|
||||
it "automatically ends the turn if all players move", ->
|
||||
shared.create.call(this)
|
||||
mock = @sandbox.mock(@controller)
|
||||
mock.expects('continueTurn').once()
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Tackle')
|
||||
mock.verify()
|
||||
|
||||
it "automatically ends the turn if all players switch", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
mock = @sandbox.mock(@controller)
|
||||
mock.expects('continueTurn').once()
|
||||
@controller.makeSwitch(@id1, 1)
|
||||
@controller.makeSwitch(@id2, 1)
|
||||
mock.verify()
|
||||
|
||||
describe "switch validations", ->
|
||||
it "rejects switches under 0", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').never()
|
||||
@controller.makeSwitch(@id1, -1)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches for pokemon who are already out", ->
|
||||
shared.create.call this,
|
||||
numActive: 2
|
||||
team1: (Factory("Magikarp") for x in [0..2])
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').never()
|
||||
@controller.makeSwitch(@id1, 0)
|
||||
@controller.makeSwitch(@id1, 1)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches over the max team party index", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').never()
|
||||
@controller.makeSwitch(@id1, 3)
|
||||
mock.verify()
|
||||
|
||||
it "accepts switches between active pokemon and max team party index", ->
|
||||
shared.create.call this,
|
||||
numActive: 2
|
||||
team1: (Factory("Magikarp") for x in [0..2])
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').once()
|
||||
@controller.makeSwitch(@id1, 2)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches that are not part of the request action", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
@p1.blockSwitch()
|
||||
@p1.resetBlocks = ->
|
||||
@battle.removeRequest(@id1)
|
||||
@battle.beginTurn()
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').never()
|
||||
@controller.makeSwitch(@id1, 2)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches if the battle has not started yet", ->
|
||||
shared.build(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordSwitch').never()
|
||||
@controller.makeSwitch(@id1, 2)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches not for a specific turn", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').never()
|
||||
@controller.makeSwitch(@id1, 2)
|
||||
mock.verify()
|
||||
|
||||
it "rejects switches for a player who doesn't exist", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [0..2]))
|
||||
|
||||
@controller.makeSwitch("fake dude", 2).should.be.false
|
||||
|
||||
describe "move validations", ->
|
||||
it "rejects moves not part of the pokemon's valid moves", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').never()
|
||||
@controller.makeMove(@id1, "EXTERMINATE")
|
||||
mock.verify()
|
||||
|
||||
it "accepts Struggle", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
@p1.blockMove(move) for move in @p1.moves
|
||||
@p1.resetBlocks = ->
|
||||
@battle.removeRequest(@id1)
|
||||
@battle.beginTurn()
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').once()
|
||||
@controller.makeMove(@id1, "Struggle")
|
||||
mock.verify()
|
||||
|
||||
it "rejects moves that cannot be selected", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
move = @p1.moves[0]
|
||||
@p1.blockMove(move)
|
||||
@p1.resetBlocks = ->
|
||||
@battle.removeRequest(@id1)
|
||||
@battle.beginTurn()
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').never()
|
||||
@controller.makeMove(@id1, move.name)
|
||||
mock.verify()
|
||||
|
||||
it "rejects moves if the battle has not started yet", ->
|
||||
shared.build this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').never()
|
||||
@controller.makeMove(@id1, @p1.moves[0].name)
|
||||
mock.verify()
|
||||
|
||||
it "rejects moves not for a specific turn", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
|
||||
mock = @sandbox.mock(@battle).expects('recordMove').never()
|
||||
@controller.makeMove(@id1, @p1.moves[0].name, null, @battle.turn - 1)
|
||||
mock.verify()
|
||||
|
||||
it "rejects moves for a player who doesn't exist", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp", moves: ["Tackle", "Splash"]) ]
|
||||
|
||||
@controller.makeMove("fake dude", @p1.moves[0].name).should.be.false
|
||||
567
test/bw/battle_engine.coffee
Normal file
567
test/bw/battle_engine.coffee
Normal file
@@ -0,0 +1,567 @@
|
||||
require '../helpers'
|
||||
|
||||
{Battle} = require('../../server/bw/battle')
|
||||
{Pokemon} = require('../../server/bw/pokemon')
|
||||
{Status, Attachment} = require('../../server/bw/attachment')
|
||||
{Conditions} = require '../../shared/conditions'
|
||||
{Factory} = require '../factory'
|
||||
should = require 'should'
|
||||
shared = require '../shared'
|
||||
{Protocol} = require '../../shared/protocol'
|
||||
|
||||
describe 'Mechanics', ->
|
||||
describe 'an attack missing', ->
|
||||
it 'deals no damage', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Celebi')]
|
||||
team2: [Factory('Magikarp')]
|
||||
shared.biasRNG.call(this, 'randInt', 'miss', 100)
|
||||
move = @battle.getMove('Leaf Storm')
|
||||
originalHP = @p2.currentHP
|
||||
@battle.performMove(@p1, @battle.getMove('Leaf Storm'))
|
||||
@p2.currentHP.should.equal(originalHP)
|
||||
|
||||
it 'triggers effects dependent on the move missing', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonlee')]
|
||||
team2: [Factory('Magikarp')]
|
||||
shared.biasRNG.call(this, 'randInt', 'miss', 100)
|
||||
hiJumpKick = @battle.getMove('Hi Jump Kick')
|
||||
mock = @sandbox.mock(hiJumpKick).expects('afterMiss').once()
|
||||
@battle.performMove(@p1, hiJumpKick)
|
||||
mock.verify()
|
||||
|
||||
it 'does not trigger effects dependent on the move hitting', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Celebi')]
|
||||
team2: [Factory('Gyarados')]
|
||||
shared.biasRNG.call(this, 'randInt', 'miss', 100)
|
||||
hiJumpKick = @battle.getMove('Hi Jump Kick')
|
||||
mock = @sandbox.mock(hiJumpKick).expects('afterSuccessfulHit').never()
|
||||
@battle.performMove(@p1, hiJumpKick)
|
||||
mock.verify()
|
||||
|
||||
describe 'fainting', ->
|
||||
it 'forces a new pokemon to be picked', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Mew'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
spy = @sandbox.spy(@battle, 'tellPlayer')
|
||||
@p2.currentHP = 1
|
||||
@controller.makeMove(@id1, 'Psychic')
|
||||
@controller.makeMove(@id2, 'Mach Punch')
|
||||
spy.calledWith(@id2, Protocol.REQUEST_ACTIONS).should.be.true
|
||||
|
||||
it 'does not increment the turn count', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Mew'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
turn = @battle.turn
|
||||
@p2.currentHP = 1
|
||||
@controller.makeMove(@id1, 'Psychic')
|
||||
@controller.makeMove(@id2, 'Mach Punch')
|
||||
@battle.turn.should.not.equal turn + 1
|
||||
|
||||
it 'removes the fainted pokemon from the action priority queue', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Mew'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
@p1.currentHP = 1
|
||||
@p2.currentHP = 1
|
||||
@controller.makeMove(@id1, 'Psychic')
|
||||
@controller.makeMove(@id2, 'Mach Punch')
|
||||
@p1.currentHP.should.be.below 1
|
||||
@p2.currentHP.should.equal 1
|
||||
action = @battle.getAction(@p1)
|
||||
should.not.exist(action)
|
||||
|
||||
it 'lets the player switch in a new pokemon', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Mew'), Factory('Heracross')]
|
||||
team2: [Factory('Hitmonchan'), Factory('Heracross')]
|
||||
@p2.currentHP = 1
|
||||
@controller.makeMove(@id1, 'Psychic')
|
||||
@controller.makeMove(@id2, 'Mach Punch')
|
||||
@controller.makeSwitch(@id2, 1)
|
||||
@team2.first().species.should.equal 'Heracross'
|
||||
|
||||
it "occurs when a pokemon faints from passive damage", ->
|
||||
shared.create.call(this)
|
||||
@p2.currentHP = 1
|
||||
@battle.performMove(@p1, @battle.getMove("Leech Seed"))
|
||||
spy = @sandbox.spy(@p2, 'faint')
|
||||
@battle.endTurn()
|
||||
spy.calledOnce.should.be.true
|
||||
|
||||
it "occurs when a pokemon faints normally", ->
|
||||
shared.create.call(this)
|
||||
@p2.currentHP = 1
|
||||
spy = @sandbox.spy(@p2, 'faint')
|
||||
@battle.performMove(@p1, @battle.getMove("Tackle"))
|
||||
spy.calledOnce.should.be.true
|
||||
|
||||
describe 'secondary effect attacks', ->
|
||||
it 'can inflict effect on successful hit', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Porygon-Z')]
|
||||
team2: [Factory('Porygon-Z')]
|
||||
shared.biasRNG.call(this, "next", 'secondary effect', 0) # 100% chance
|
||||
@battle.performMove(@p1, @battle.getMove('Flamethrower'))
|
||||
@p2.has(Status.Burn).should.be.true
|
||||
|
||||
describe 'the fang attacks', ->
|
||||
it 'can inflict two effects at the same time', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Gyarados')]
|
||||
team2: [Factory('Gyarados')]
|
||||
shared.biasRNG.call(this, "randInt", "secondary effect", 0) # 100% chance
|
||||
shared.biasRNG.call(this, "randInt", "flinch", 0)
|
||||
@battle.performMove(@p1, @battle.getMove("Ice Fang"))
|
||||
|
||||
@p2.has(Attachment.Flinch).should.be.true
|
||||
@p2.has(Status.Freeze).should.be.true
|
||||
|
||||
describe 'a pokemon with technician', ->
|
||||
it "doesn't increase damage if the move has bp > 60", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmontop')]
|
||||
team2: [Factory('Mew')]
|
||||
icePunch = @battle.getMove('Ice Punch')
|
||||
icePunch.modifyBasePower(@battle, @p1, @p2).should.equal(0x1000)
|
||||
|
||||
it "increases damage if the move has bp <= 60", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmontop')]
|
||||
team2: [Factory('Shaymin')]
|
||||
bulletPunch = @battle.getMove('Bullet Punch')
|
||||
bulletPunch.modifyBasePower(@battle, @p1, @p2).should.equal(0x1800)
|
||||
|
||||
describe 'STAB', ->
|
||||
it "gets applied if the move and user share a type", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Heracross')]
|
||||
team2: [Factory('Regirock')]
|
||||
megahorn = @battle.getMove("Megahorn")
|
||||
megahorn.stabModifier(@battle, @p1, @p2).should.equal(0x1800)
|
||||
|
||||
it "doesn't get applied if the move and user are of different types", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
icePunch = @battle.getMove("Ice Punch")
|
||||
icePunch.stabModifier(@battle, @p1, @p2).should.equal(0x1000)
|
||||
|
||||
describe 'turn order', ->
|
||||
it 'randomly decides winner if pokemon have the same speed and priority', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Mew')]
|
||||
team2: [Factory('Mew')]
|
||||
spy = @sandbox.spy(@battle, 'determineTurnOrder')
|
||||
shared.biasRNG.call(this, "next", "turn order", .6)
|
||||
@battle.recordMove(@id1, @battle.getMove('Psychic'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Psychic'))
|
||||
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
|
||||
|
||||
@battle.pokemonActions = []
|
||||
|
||||
shared.biasRNG.call(this, "next", "turn order", .4)
|
||||
@battle.recordMove(@id1, @battle.getMove('Psychic'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Psychic'))
|
||||
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p1, @p2 ]
|
||||
|
||||
it 'decides winner by highest priority move', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Hitmonchan')]
|
||||
spy = @sandbox.spy(@battle, 'determineTurnOrder')
|
||||
@battle.recordMove(@id1, @battle.getMove('Mach Punch'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Psychic'))
|
||||
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p1, @p2 ]
|
||||
|
||||
@battle.pokemonActions = []
|
||||
|
||||
@battle.recordMove(@id1, @battle.getMove('Psychic'))
|
||||
@battle.recordMove(@id2, @battle.getMove('Mach Punch'))
|
||||
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
|
||||
|
||||
it 'decides winner by speed if priority is equal', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Hitmonchan', evs: { speed: 4 })]
|
||||
@battle.recordMove(@id1, @battle.getMove('ThunderPunch'))
|
||||
@battle.recordMove(@id2, @battle.getMove('ThunderPunch'))
|
||||
@battle.determineTurnOrder().map((o) -> o.pokemon).should.eql [ @p2, @p1 ]
|
||||
|
||||
describe 'fainting all the opposing pokemon', ->
|
||||
it "doesn't request any more actions from players", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
@p2.currentHP = 1
|
||||
@controller.makeMove(@id1, 'Mach Punch')
|
||||
@controller.makeMove(@id2, 'Psychic')
|
||||
@battle.requests.should.not.have.property @id1.id
|
||||
@battle.requests.should.not.have.property @id2.id
|
||||
|
||||
it 'ends the battle', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Hitmonchan')]
|
||||
team2: [Factory('Mew')]
|
||||
@p2.currentHP = 1
|
||||
mock = @sandbox.mock(@battle)
|
||||
mock.expects('endBattle').once()
|
||||
@controller.makeMove(@id1, 'Mach Punch')
|
||||
@controller.makeMove(@id2, 'Psychic')
|
||||
mock.verify()
|
||||
|
||||
describe 'a pokemon with a type immunity', ->
|
||||
it 'cannot be damaged by a move of that type', ->
|
||||
shared.create.call this,
|
||||
team1: [Factory('Camerupt')]
|
||||
team2: [Factory('Gyarados')]
|
||||
@controller.makeMove(@id1, 'Earthquake')
|
||||
@controller.makeMove(@id2, 'Dragon Dance')
|
||||
|
||||
@p2.currentHP.should.equal @p2.stat('hp')
|
||||
|
||||
describe 'a confused pokemon', ->
|
||||
it "has a 50% chance of hurting itself", ->
|
||||
shared.create.call(this)
|
||||
|
||||
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
|
||||
@p1.attach(Attachment.Confusion, {@battle})
|
||||
shared.biasRNG.call(this, "next", 'confusion', 0) # always hits
|
||||
|
||||
mock = @sandbox.mock(@battle.getMove('Tackle'))
|
||||
mock.expects('execute').never()
|
||||
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
mock.verify()
|
||||
|
||||
@p1.currentHP.should.be.lessThan @p1.stat('hp')
|
||||
@p2.currentHP.should.equal @p2.stat('hp')
|
||||
|
||||
it "deals a minimum of 1 damage", ->
|
||||
shared.create.call(this, team1: [Factory("Shuckle", level: 1)])
|
||||
|
||||
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
|
||||
@p1.attach(Attachment.Confusion, {@battle})
|
||||
shared.biasRNG.call(this, "next", 'confusion', 0) # always hits
|
||||
@sandbox.stub(@battle.confusionMove, 'calculateDamage', -> 0)
|
||||
|
||||
@battle.performMove(@p1, @battle.getMove('Tackle'))
|
||||
|
||||
@p1.currentHP.should.equal(@p1.stat('hp') - 1)
|
||||
|
||||
it "snaps out of confusion after a predetermined number of turns", ->
|
||||
shared.create.call(this)
|
||||
|
||||
shared.biasRNG.call(this, "randInt", 'confusion turns', 1) # always 1 turn
|
||||
@p1.attach(Attachment.Confusion)
|
||||
|
||||
@controller.makeMove(@id1, 'Splash')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
@controller.makeMove(@id1, 'Splash')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
@p1.has(Attachment.Confusion).should.be.false
|
||||
|
||||
it "will not crit the confusion recoil", ->
|
||||
shared.create.call(this)
|
||||
|
||||
@p1.attach(Attachment.Confusion)
|
||||
shared.biasRNG.call(this, "next", 'confusion', 0) # always recoils
|
||||
shared.biasRNG.call(this, 'next', 'ch', 0) # always crits
|
||||
|
||||
spy = @sandbox.spy(@battle.confusionMove, 'isCriticalHit')
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Tackle')
|
||||
|
||||
spy.returned(false).should.be.true
|
||||
|
||||
it "will not error for not having unusual move properties", ->
|
||||
shared.create.call(this, team1: [Factory("Magikarp", ability: "Iron Fist")])
|
||||
|
||||
@p1.attach(Attachment.Confusion)
|
||||
shared.biasRNG.call(this, "next", 'confusion', 0) # always recoils
|
||||
|
||||
(=>
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Tackle')
|
||||
).should.not.throw()
|
||||
|
||||
describe 'a frozen pokemon', ->
|
||||
it "will not execute moves", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Freeze)
|
||||
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
|
||||
|
||||
mock = @sandbox.mock(@battle.getMove('Tackle'))
|
||||
mock.expects('execute').never()
|
||||
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
mock.verify()
|
||||
|
||||
it "has a 20% chance of unfreezing", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Freeze)
|
||||
shared.biasRNG.call(this, "next", 'unfreeze chance', 0) # always unfreezes
|
||||
|
||||
@controller.makeMove(@id1, 'Splash')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
@p1.has(Status.Freeze).should.be.false
|
||||
|
||||
it "unfreezes if hit by a fire move", ->
|
||||
shared.create.call(this)
|
||||
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
|
||||
@p1.attach(Status.Freeze)
|
||||
|
||||
@battle.performMove(@p2, @battle.getMove('Flamethrower'))
|
||||
@p1.has(Status.Freeze).should.be.false
|
||||
|
||||
it "does not unfreeze if hit by a non-damaging move", ->
|
||||
shared.create.call(this)
|
||||
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
|
||||
@p1.attach(Status.Freeze)
|
||||
|
||||
@battle.performMove(@p2, @battle.getMove('Will-O-Wisp'))
|
||||
@p1.has(Status.Freeze).should.be.true
|
||||
|
||||
for moveName in ["Sacred Fire", "Flare Blitz", "Flame Wheel", "Fusion Flare", "Scald"]
|
||||
it "automatically unfreezes if using #{moveName}", ->
|
||||
shared.create.call(this)
|
||||
|
||||
@p1.attach(Status.Freeze)
|
||||
shared.biasRNG.call(this, "next", 'unfreeze chance', 1) # always stays frozen
|
||||
|
||||
@battle.performMove(@p1, @battle.getMove(moveName))
|
||||
@p1.has(Status.Freeze).should.be.false
|
||||
|
||||
describe "a paralyzed pokemon", ->
|
||||
it "has a 25% chance of being fully paralyzed", ->
|
||||
shared.create.call(this)
|
||||
|
||||
@p1.attach(Status.Paralyze)
|
||||
shared.biasRNG.call(this, "next", 'paralyze chance', 0) # always stays frozen
|
||||
|
||||
mock = @sandbox.mock(@battle.getMove('Tackle'))
|
||||
mock.expects('execute').never()
|
||||
|
||||
@controller.makeMove(@id1, 'Tackle')
|
||||
@controller.makeMove(@id2, 'Splash')
|
||||
|
||||
mock.verify()
|
||||
|
||||
it "has its speed quartered", ->
|
||||
shared.create.call(this)
|
||||
|
||||
speed = @p1.stat('speed')
|
||||
@p1.attach(Status.Paralyze)
|
||||
|
||||
@p1.stat('speed').should.equal Math.floor(speed / 4)
|
||||
|
||||
describe "a burned pokemon", ->
|
||||
it "loses 1/8 of its HP each turn", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Burn)
|
||||
hp = @p1.currentHP
|
||||
eighth = Math.floor(hp / 8)
|
||||
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(eighth)
|
||||
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(2 * eighth)
|
||||
|
||||
it "loses 1 minimum HP", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja")])
|
||||
@p1.attach(Status.Burn)
|
||||
@p1.currentHP.should.equal(1)
|
||||
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.equal(0)
|
||||
|
||||
describe "a sleeping pokemon", ->
|
||||
it "sleeps for 1-3 turns", ->
|
||||
shared.create.call(this)
|
||||
shared.biasRNG.call(this, "randInt", 'sleep turns', 3)
|
||||
@p1.attach(Status.Sleep)
|
||||
tackle = @battle.getMove('Tackle')
|
||||
|
||||
for i in [0...3]
|
||||
@battle.performMove(@p1, tackle)
|
||||
@p1.has(Status.Sleep).should.be.true
|
||||
|
||||
@battle.performMove(@p1, tackle)
|
||||
@p1.has(Status.Sleep).should.be.false
|
||||
|
||||
it "cannot move while asleep", ->
|
||||
shared.create.call(this)
|
||||
shared.biasRNG.call(this, "randInt", 'sleep turns', 3)
|
||||
@p1.attach(Status.Sleep)
|
||||
tackle = @battle.getMove('Tackle')
|
||||
|
||||
mock = @sandbox.mock(tackle).expects('execute').never()
|
||||
for i in [0...3]
|
||||
@battle.performMove(@p1, tackle)
|
||||
mock.verify()
|
||||
tackle.execute.restore()
|
||||
|
||||
mock = @sandbox.mock(tackle).expects('execute').once()
|
||||
@battle.performMove(@p1, tackle)
|
||||
mock.verify()
|
||||
|
||||
it "resets its counter when switching out", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp"), Factory("Magikarp") ]
|
||||
shared.biasRNG.call(this, "randInt", 'sleep turns', 1)
|
||||
@p1.attach(Status.Sleep)
|
||||
tackle = @battle.getMove('Tackle')
|
||||
|
||||
@battle.performMove(@p1, tackle)
|
||||
@battle.performSwitch(@team1.first(), 1)
|
||||
@battle.performSwitch(@team1.first(), 1)
|
||||
@battle.performMove(@team1.first(), tackle)
|
||||
@p1.has(Status.Sleep).should.be.true
|
||||
|
||||
describe "a poisoned pokemon", ->
|
||||
it "loses 1/8 of its HP each turn", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Poison)
|
||||
hp = @p1.currentHP
|
||||
eighth = Math.floor(hp / 8)
|
||||
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(eighth)
|
||||
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(2 * eighth)
|
||||
|
||||
it "loses 1 minimum HP", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja")])
|
||||
@p1.attach(Status.Poison)
|
||||
@p1.currentHP.should.equal(1)
|
||||
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.equal(0)
|
||||
|
||||
describe "a badly poisoned pokemon", ->
|
||||
it "loses 1/16 of its HP the first turn", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Toxic)
|
||||
hp = @p1.currentHP
|
||||
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(hp >> 4)
|
||||
|
||||
it "loses 1/16 of its max HP, rounded down, times x where x is the number of turns up to 15", ->
|
||||
shared.create.call(this)
|
||||
@p1.attach(Status.Toxic)
|
||||
hp = @p1.currentHP
|
||||
fraction = (hp >> 4)
|
||||
|
||||
for i in [1..16]
|
||||
@battle.endTurn()
|
||||
hpFraction = Math.min(fraction * i, fraction * 15)
|
||||
(hp - @p1.currentHP).should.equal(hpFraction)
|
||||
@p1.currentHP = hp
|
||||
|
||||
it "still increases the counter with poison heal", ->
|
||||
shared.create.call(this, team1: [Factory("Magikarp", ability: "Poison Heal")])
|
||||
@p1.attach(Status.Toxic)
|
||||
hp = @p1.currentHP
|
||||
fraction = (hp >> 4)
|
||||
turns = 3
|
||||
|
||||
for i in [1...turns]
|
||||
@battle.endTurn()
|
||||
|
||||
@p1.copyAbility(null)
|
||||
@battle.endTurn()
|
||||
hpFraction = Math.min(fraction * turns, fraction * 15)
|
||||
(hp - @p1.currentHP).should.equal(hpFraction)
|
||||
|
||||
it "loses 1 minimum HP", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja")])
|
||||
@p1.attach(Status.Toxic)
|
||||
@p1.currentHP.should.equal(1)
|
||||
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.equal(0)
|
||||
|
||||
it "resets its counter when switching out", ->
|
||||
shared.create.call this,
|
||||
team1: [ Factory("Magikarp"), Factory("Magikarp") ]
|
||||
@p1.attach(Status.Toxic)
|
||||
hp = @p1.currentHP
|
||||
|
||||
@battle.endTurn()
|
||||
@battle.performSwitch(@team1.first(), 1)
|
||||
@p1.currentHP = hp
|
||||
@battle.performSwitch(@team1.first(), 1)
|
||||
@battle.endTurn()
|
||||
(hp - @p1.currentHP).should.equal(hp >> 4)
|
||||
|
||||
describe "Pokemon#turnsActive", ->
|
||||
it "is 1 on start of battle", ->
|
||||
shared.create.call(this)
|
||||
@p1.turnsActive.should.equal 1
|
||||
|
||||
it "is set to 0 when switching", ->
|
||||
shared.create.call(this, team1: (Factory("Magikarp") for x in [1..2]))
|
||||
@p1.turnsActive = 4
|
||||
@team1.switch(@p1, 0)
|
||||
@team1.first().turnsActive.should.equal 0
|
||||
|
||||
it "increases by 1 when a turn ends", ->
|
||||
shared.create.call(this)
|
||||
@p1.turnsActive.should.equal 1
|
||||
|
||||
@battle.endTurn()
|
||||
@p1.turnsActive.should.equal 2
|
||||
|
||||
describe "A move with 0 PP", ->
|
||||
it "will not execute", ->
|
||||
shared.create.call(this)
|
||||
move = @p1.moves[0]
|
||||
@p1.setPP(move, 0)
|
||||
|
||||
@sandbox.mock(move).expects('execute').never()
|
||||
@sandbox.mock(@p1).expects('beforeMove').never()
|
||||
@battle.performMove(@p1, move)
|
||||
|
||||
describe "A pokemon with no available moves", ->
|
||||
it "can struggle", ->
|
||||
shared.create.call(this)
|
||||
@battle.removeRequest(@id1)
|
||||
|
||||
# Next turn, @p1 will have no available moves.
|
||||
for move in @p1.moves
|
||||
@p1.blockMove(move)
|
||||
@p1.resetBlocks = ->
|
||||
|
||||
@p1.validMoves().should.be.empty
|
||||
@battle.beginTurn()
|
||||
request = @battle.requestFor(@p1)
|
||||
should.exist(request)
|
||||
request.should.have.property('moves')
|
||||
request.moves.should.eql(["Struggle"])
|
||||
|
||||
describe "Ability activation at the beginning of a battle", ->
|
||||
it "considers Abilities that are always active", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory("Magikarp", ability: "Clear Body")]
|
||||
team2: [Factory("Magikarp", ability: "Intimidate")]
|
||||
@p1.stages.should.containEql(attack: 0)
|
||||
|
||||
xit "works the same way regardless of the entry order", ->
|
||||
shared.create.call this,
|
||||
team1: [Factory("Magikarp", ability: "Intimidate")]
|
||||
team2: [Factory("Magikarp", ability: "Clear Body")]
|
||||
@p2.stages.should.containEql(attack: 0)
|
||||
163
test/bw/battles.coffee
Normal file
163
test/bw/battles.coffee
Normal file
@@ -0,0 +1,163 @@
|
||||
require '../helpers'
|
||||
|
||||
shared = require '../shared'
|
||||
{Attachment} = require '../../server/bw/attachment'
|
||||
|
||||
defaultTeam = [
|
||||
{
|
||||
species: "Pikachu"
|
||||
moves: ["Substitute", "Thunderbolt", "Hidden Power", "Grass Knot"]
|
||||
item: "Light Ball"
|
||||
ability: "Lightningrod"
|
||||
gender: "F"
|
||||
}
|
||||
{
|
||||
species: "Hitmonchan"
|
||||
moves: ["Close Combat", "Mach Punch", "Ice Punch", "ThunderPunch"]
|
||||
item: "Life Orb"
|
||||
ability: "Iron Fist"
|
||||
gender: "M"
|
||||
}
|
||||
{
|
||||
species: "Charizard"
|
||||
item: "Choice Specs"
|
||||
moves: ["Fire Blast", "Air Slash", "Hidden Power", "Focus Blast"]
|
||||
ability: "Blaze"
|
||||
}
|
||||
{
|
||||
species: "Dragonite"
|
||||
item: "Leftovers"
|
||||
moves: ["Dragon Dance", "Outrage", "Fire Punch", "ExtremeSpeed"]
|
||||
ability: "Multiscale"
|
||||
}
|
||||
{
|
||||
species: "Politoed"
|
||||
item: "Leftovers"
|
||||
moves: ["Scald", "Ice Beam", "Protect", "Toxic"]
|
||||
ability: "Drizzle"
|
||||
}
|
||||
{
|
||||
species: "Haunter"
|
||||
item: "Leftovers"
|
||||
moves: ["Substitute", "Disable", "Shadow Ball", "Focus Blast"]
|
||||
ability: "Levitate"
|
||||
}
|
||||
]
|
||||
|
||||
describe "BW: Integration:", ->
|
||||
describe "a battle", ->
|
||||
it "executes fainted replacements at the proper time", ->
|
||||
shared.create.call(this, team1: defaultTeam, team2: defaultTeam)
|
||||
spy = @sandbox.spy(@battle, 'performReplacements')
|
||||
|
||||
@battle.turn.should.equal(1)
|
||||
@sandbox.stub(@battle.getMove("Thunderbolt"), "baseDamage", -> 9999)
|
||||
@controller.makeMove(@id1, "Thunderbolt")
|
||||
@controller.makeSwitch(@id2, 4)
|
||||
|
||||
@battle.turn.should.equal(1)
|
||||
@controller.makeSwitch(@id2, 1)
|
||||
@battle.turn.should.equal(2)
|
||||
|
||||
(=>
|
||||
@controller.makeMove(@id1, "Thunderbolt")
|
||||
@controller.makeSwitch(@id2, 2)
|
||||
).should.not.throw()
|
||||
spy.calledOnce.should.be.true
|
||||
|
||||
it "can execute two fainted replacements in a row", ->
|
||||
shared.create.call(this, team1: defaultTeam, team2: defaultTeam)
|
||||
spy = @sandbox.spy(@battle, 'performReplacements')
|
||||
|
||||
@battle.turn.should.equal(1)
|
||||
|
||||
# Artificially set up conditions
|
||||
spy1 = @sandbox.spy(@team2.at(1), "faint")
|
||||
spy2 = @sandbox.spy(@team2.at(2), "faint")
|
||||
@team2.at(1).currentHP = 1
|
||||
@team2.at(2).currentHP = 1
|
||||
@team2.attach(Attachment.StealthRock)
|
||||
|
||||
# Now attempt the first replacement.
|
||||
@controller.makeSwitch(@id2, 1)
|
||||
@controller.makeMove(@id1, "Thunderbolt")
|
||||
|
||||
# Nothing happens except the Pokemon faints.
|
||||
@battle.turn.should.equal(1)
|
||||
@team2.first().species.should.equal("Hitmonchan")
|
||||
@team2.first().isFainted().should.be.true
|
||||
@battle.areAllRequestsCompleted().should.be.false
|
||||
|
||||
# Do the second replacement.
|
||||
@controller.makeSwitch(@id2, 2)
|
||||
|
||||
# Nothing happens except the Pokemon faints.
|
||||
@battle.turn.should.equal(1)
|
||||
@team2.first().species.should.equal("Charizard")
|
||||
@team2.first().isFainted().should.be.true
|
||||
@battle.areAllRequestsCompleted().should.be.false
|
||||
|
||||
# Each pokemon should have called their faint method
|
||||
spy1.called.should.be.true
|
||||
spy2.called.should.be.true
|
||||
|
||||
it "doesn't trigger weather if a switch-in faints immediately", ->
|
||||
shared.create.call(this, team1: defaultTeam, team2: defaultTeam)
|
||||
spy = @sandbox.spy(@battle, 'performReplacements')
|
||||
|
||||
@battle.turn.should.equal(1)
|
||||
|
||||
# There should be no weather at the start of the battle.
|
||||
@battle.hasWeather().should.be.false
|
||||
|
||||
# Artificially set up conditions
|
||||
@team2.at(4).currentHP = 1
|
||||
@team2.attach(Attachment.StealthRock)
|
||||
|
||||
# Now attempt the switch.
|
||||
@controller.makeSwitch(@id2, 4)
|
||||
@controller.makeMove(@id1, "Substitute")
|
||||
|
||||
# Nothing happens except the Pokemon faints.
|
||||
@battle.turn.should.equal(1)
|
||||
@team2.first().species.should.equal("Politoed")
|
||||
@team2.first().isFainted().should.be.true
|
||||
@battle.areAllRequestsCompleted().should.be.false
|
||||
|
||||
# There should be no weather.
|
||||
@battle.hasWeather().should.be.false
|
||||
|
||||
it "doesn't trigger weather if a replacement faints immediately", ->
|
||||
shared.create.call(this, team1: defaultTeam, team2: defaultTeam)
|
||||
|
||||
@battle.turn.should.equal(1)
|
||||
|
||||
# There should be no weather at the start of the battle.
|
||||
@battle.hasWeather().should.be.false
|
||||
|
||||
# Artificially set up conditions
|
||||
@team2.at(1).currentHP = 1
|
||||
@team2.at(4).currentHP = 1
|
||||
@team2.attach(Attachment.StealthRock)
|
||||
|
||||
# Now attempt the switches.
|
||||
@controller.makeSwitch(@id2, 1)
|
||||
@controller.makeMove(@id1, "Substitute")
|
||||
|
||||
# Nothing happens except the Pokemon faints.
|
||||
@battle.turn.should.equal(1)
|
||||
@team2.first().species.should.equal("Hitmonchan")
|
||||
@team2.first().isFainted().should.be.true
|
||||
@battle.areAllRequestsCompleted().should.be.false
|
||||
|
||||
# Attempt the replacement
|
||||
@controller.makeSwitch(@id2, 4)
|
||||
|
||||
# Nothing happens except the Pokemon faints, again.
|
||||
@battle.turn.should.equal(1)
|
||||
@team2.first().species.should.equal("Politoed")
|
||||
@team2.first().isFainted().should.be.true
|
||||
@battle.areAllRequestsCompleted().should.be.false
|
||||
|
||||
# There should be no weather.
|
||||
@battle.hasWeather().should.be.false
|
||||
1509
test/bw/items.coffee
Normal file
1509
test/bw/items.coffee
Normal file
File diff suppressed because it is too large
Load Diff
7723
test/bw/moves.coffee
Normal file
7723
test/bw/moves.coffee
Normal file
File diff suppressed because it is too large
Load Diff
492
test/bw/pokemon.coffee
Normal file
492
test/bw/pokemon.coffee
Normal file
@@ -0,0 +1,492 @@
|
||||
require '../helpers'
|
||||
|
||||
{Battle} = require('../../server/bw/battle')
|
||||
{Weather} = require('../../shared/weather')
|
||||
{Pokemon} = require('../../server/bw/pokemon')
|
||||
{Status, Attachment, BaseAttachment, VolatileAttachment} = require('../../server/bw/attachment')
|
||||
{Moves, SpeciesData} = require('../../server/bw/data')
|
||||
{Protocol} = require '../../shared/protocol'
|
||||
{Factory} = require '../factory'
|
||||
should = require 'should'
|
||||
shared = require '../shared'
|
||||
|
||||
describe 'Pokemon', ->
|
||||
it 'should have a species of Missingno by default', ->
|
||||
new Pokemon().species.should.equal 'Missingno'
|
||||
|
||||
it 'can change the default species', ->
|
||||
new Pokemon(species: 'Pikachu').species.should.equal 'Pikachu'
|
||||
|
||||
it 'should have a level of 100 by default', ->
|
||||
new Pokemon().level.should.equal 100
|
||||
|
||||
it 'can change the default level', ->
|
||||
new Pokemon(level: 5).level.should.equal 5
|
||||
|
||||
it 'gets its current hp populated from its max hp', ->
|
||||
new Pokemon().currentHP.should.equal 341
|
||||
|
||||
it 'has pp for each move', ->
|
||||
pokemon = new Pokemon(moves: ["Tackle", "Splash"])
|
||||
pokemon.pp(Moves['Tackle']).should.equal 35 * 8/5
|
||||
pokemon.pp(Moves['Splash']).should.equal 40 * 8/5
|
||||
|
||||
describe '#iv', ->
|
||||
it 'has default iv of 31', ->
|
||||
new Pokemon().iv('hp').should.equal 31
|
||||
|
||||
it 'retrieves iv successfully', ->
|
||||
new Pokemon(ivs: {'hp': 25}).iv('hp').should.equal 25
|
||||
|
||||
it "doesn't default to 31 if iv is 0", ->
|
||||
new Pokemon(ivs: {'hp': 0}).iv('hp').should.equal 0
|
||||
|
||||
describe '#ev', ->
|
||||
it 'has default ev of 0', ->
|
||||
new Pokemon().ev('hp').should.equal 0
|
||||
|
||||
it 'retrieves ev successfully', ->
|
||||
new Pokemon(evs: {hp: 25}).ev('hp').should.equal 25
|
||||
|
||||
describe '#stat', ->
|
||||
it 'calculates hp correctly', ->
|
||||
pokemon = new Pokemon(level: 100, evs: { hp: 255 })
|
||||
pokemon.stat('hp').should.equal 404
|
||||
pokemon = new Pokemon(level: 50, evs: { hp: 255 })
|
||||
pokemon.stat('hp').should.equal 207
|
||||
# todo: test other numbers later
|
||||
|
||||
it 'calculates 1 base HP correctly', ->
|
||||
pokemon = new Pokemon(level: 100, evs: { hp: 255 })
|
||||
pokemon.baseStats.hp = 1
|
||||
pokemon.stat('hp').should.equal 1
|
||||
|
||||
it 'calculates other stats correctly', ->
|
||||
pokemon = new Pokemon(level: 100, evs: { attack: 255 })
|
||||
pokemon.stat('attack').should.equal 299
|
||||
pokemon = new Pokemon(level: 50, evs: { attack: 255 })
|
||||
pokemon.stat('attack').should.equal 152
|
||||
# todo: test other numbers later
|
||||
|
||||
it "calculates a stat with a nature boost correctly", ->
|
||||
pokemon = new Pokemon(nature: 'Adamant')
|
||||
pokemon.stat('attack').should.equal 259
|
||||
|
||||
it "calculates a stat with a nature decrease correctly", ->
|
||||
pokemon = new Pokemon(nature: 'Bold')
|
||||
pokemon.stat('attack').should.equal 212
|
||||
|
||||
describe 'stat boosts', ->
|
||||
it 'increase the stat by (n+2)/2 if positive', ->
|
||||
pokemon = new Pokemon()
|
||||
speed = pokemon.stat('speed')
|
||||
pokemon.stages.speed = 3
|
||||
pokemon.stat('speed').should.equal Math.floor(2.5 * speed)
|
||||
|
||||
it 'decrease the stat by 2/(n+2) if negative', ->
|
||||
pokemon = new Pokemon()
|
||||
speed = pokemon.stat('speed')
|
||||
pokemon.stages.speed = -3
|
||||
pokemon.stat('speed').should.equal Math.floor(speed / 2.5)
|
||||
|
||||
describe '#natureBoost', ->
|
||||
it "returns 1 by default for non-existent natures", ->
|
||||
new Pokemon(nature: 'Super').natureBoost('attack').should.equal 1
|
||||
|
||||
it "returns 1.1 for natures that boost a certain stat", ->
|
||||
new Pokemon(nature: 'Adamant').natureBoost('attack').should.equal 1.1
|
||||
|
||||
it "returns 1.0 for natures do not affect a certain stat", ->
|
||||
new Pokemon(nature: 'Adamant').natureBoost('speed').should.equal 1
|
||||
|
||||
it "returns 0.9 for natures that decrease a certain stat", ->
|
||||
new Pokemon(nature: 'Timid').natureBoost('attack').should.equal 0.9
|
||||
|
||||
describe '#hasType', ->
|
||||
it 'returns false if the pokemon does not have that type', ->
|
||||
new Pokemon().hasType('Grass').should.be.false
|
||||
|
||||
it 'returns true if the pokemon has that type', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = ['Dark', 'Grass']
|
||||
pokemon.hasType('Grass').should.be.true
|
||||
|
||||
describe '#switchOut', ->
|
||||
it 'resets stat boosts', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.boost(specialAttack: 2)
|
||||
pokemon.switchOut()
|
||||
pokemon.stages['specialAttack'].should.equal 0
|
||||
|
||||
it 'removes blocked moves', ->
|
||||
pokemon = new Pokemon(moves: ['Earthquake'])
|
||||
pokemon.blockMove(Moves['Earthquake'])
|
||||
pokemon.switchOut()
|
||||
pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false
|
||||
|
||||
it 'removes volatile attachments', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(VolatileAttachment)
|
||||
pokemon.switchOut()
|
||||
pokemon.has(VolatileAttachment).should.be.false
|
||||
|
||||
describe '#endTurn', ->
|
||||
it 'removes blocked moves', ->
|
||||
pokemon = new Pokemon(moves: ['Earthquake'])
|
||||
pokemon.blockMove(Moves['Earthquake'])
|
||||
pokemon.switchOut()
|
||||
pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false
|
||||
|
||||
describe '#attach', ->
|
||||
it 'adds an attachment to a list of attachments', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(BaseAttachment)
|
||||
pokemon.has(BaseAttachment).should.be.true
|
||||
|
||||
describe '#unattach', ->
|
||||
it 'removes an attachment from the list of attachments', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(BaseAttachment)
|
||||
pokemon.unattach(BaseAttachment)
|
||||
pokemon.has(BaseAttachment).should.be.false
|
||||
|
||||
describe '#unattachAll', ->
|
||||
it 'removes every attachment', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(BaseAttachment)
|
||||
pokemon.attach(VolatileAttachment)
|
||||
pokemon.unattachAll()
|
||||
pokemon.has(BaseAttachment).should.be.false
|
||||
pokemon.has(VolatileAttachment).should.be.false
|
||||
|
||||
describe '#blockMove', ->
|
||||
it 'adds a move to a list of blocked moves', ->
|
||||
pokemon = new Pokemon(moves: ['Earthquake'])
|
||||
pokemon.blockMove(Moves['Earthquake'])
|
||||
pokemon.blockedMoves.should.containEql Moves['Earthquake']
|
||||
|
||||
describe '#isMoveBlocked', ->
|
||||
it 'returns true if the move is blocked', ->
|
||||
pokemon = new Pokemon(moves: ['Earthquake'])
|
||||
pokemon.blockMove(Moves['Earthquake'])
|
||||
pokemon.isMoveBlocked(Moves['Earthquake']).should.be.true
|
||||
|
||||
it 'returns false if the move is not blocked', ->
|
||||
pokemon = new Pokemon(moves: ['Earthquake'])
|
||||
pokemon.isMoveBlocked(Moves['Earthquake']).should.be.false
|
||||
|
||||
describe '#validMoves', ->
|
||||
it 'returns moves without blocked moves', ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
pokemon.blockMove(Moves['Earthquake'])
|
||||
pokemon.validMoves().should.eql([ Moves['Splash'] ])
|
||||
|
||||
it 'excludes moves with 0 PP', ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = pokemon.moves[0]
|
||||
otherMoves = pokemon.moves.filter((m) -> m != move)
|
||||
pokemon.setPP(move, 0)
|
||||
pokemon.validMoves().should.eql(otherMoves)
|
||||
|
||||
describe '#reducePP', ->
|
||||
it 'reduces PP of a move by 1', ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = Moves['Splash']
|
||||
pokemon.reducePP(move)
|
||||
pokemon.pp(move).should.equal pokemon.maxPP(move) - 1
|
||||
|
||||
it 'does not go below 0', ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = Moves['Splash']
|
||||
for x in [0..pokemon.maxPP(move)]
|
||||
pokemon.reducePP(move)
|
||||
pokemon.pp(move).should.equal 0
|
||||
|
||||
describe '#setPP', ->
|
||||
it "sets the PP of a move", ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = Moves['Splash']
|
||||
pokemon.setPP(move, 1)
|
||||
pokemon.pp(move).should.equal 1
|
||||
|
||||
it "cannot go below 0", ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = Moves['Splash']
|
||||
pokemon.setPP(move, -1)
|
||||
pokemon.pp(move).should.equal 0
|
||||
|
||||
it "cannot go above the max PP possible", ->
|
||||
pokemon = new Pokemon(moves: ['Splash', 'Earthquake'])
|
||||
move = Moves['Splash']
|
||||
pokemon.setPP(move, pokemon.maxPP(move) + 1)
|
||||
pokemon.pp(move).should.equal pokemon.maxPP(move)
|
||||
|
||||
describe '#positiveBoostCount', ->
|
||||
it "returns the number of boosts higher than 0", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.boost(attack: 4, defense: -3, speed: 1)
|
||||
pokemon.positiveBoostCount().should.equal 5
|
||||
|
||||
describe '#isItemBlocked', ->
|
||||
it 'returns true if the Pokemon has no item', ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.isItemBlocked().should.be.true
|
||||
|
||||
describe "attaching statuses", ->
|
||||
it "returns null if attaching too many statuses", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(Status.Freeze)
|
||||
should.not.exist pokemon.attach(Status.Paralyze)
|
||||
|
||||
it "does not override statuses", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(Status.Freeze)
|
||||
pokemon.attach(Status.Paralyze)
|
||||
pokemon.has(Status.Freeze).should.be.true
|
||||
pokemon.has(Status.Paralyze).should.be.false
|
||||
|
||||
it "sets the status of the pokemon", ->
|
||||
for species, status of Status
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(status)
|
||||
pokemon.status.should.equal(status)
|
||||
|
||||
it "sets the corresponding attachment on the pokemon", ->
|
||||
for species, status of Status
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(status)
|
||||
pokemon.has(status).should.be.true
|
||||
|
||||
it "doesn't poison Poison types", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = ["Poison"]
|
||||
pokemon.attach(Status.Poison)
|
||||
pokemon.attach(Status.Toxic)
|
||||
pokemon.has(Status.Poison).should.be.false
|
||||
pokemon.has(Status.Toxic).should.be.false
|
||||
|
||||
it "doesn't poison Steel types", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = ["Steel"]
|
||||
pokemon.attach(Status.Poison)
|
||||
pokemon.attach(Status.Toxic)
|
||||
pokemon.has(Status.Poison).should.be.false
|
||||
pokemon.has(Status.Toxic).should.be.false
|
||||
|
||||
it "doesn't burn Fire types", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = ["Fire"]
|
||||
pokemon.attach(Status.Burn)
|
||||
pokemon.has(Status.Burn).should.be.false
|
||||
|
||||
it "doesn't freeze Ice types", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = ["Ice"]
|
||||
pokemon.attach(Status.Freeze)
|
||||
pokemon.has(Status.Freeze).should.be.false
|
||||
|
||||
it "doesn't freeze under Sun", ->
|
||||
battle = new Battle('id', {"a": [], "b": []})
|
||||
battle.setWeather(Weather.SUN)
|
||||
pokemon = new Pokemon(battle: battle)
|
||||
pokemon.attach(Status.Freeze)
|
||||
pokemon.has(Status.Freeze).should.be.false
|
||||
|
||||
describe "#cureStatus", ->
|
||||
it "removes all statuses if no argument is passed", ->
|
||||
pokemon = new Pokemon()
|
||||
for species, status of Status
|
||||
pokemon.attach(status)
|
||||
pokemon.cureStatus()
|
||||
pokemon.hasStatus().should.be.false
|
||||
|
||||
it "removes only a certain status if an argument is passed", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.attach(Status.Freeze)
|
||||
pokemon.cureStatus(Status.Paralyze)
|
||||
pokemon.has(Status.Freeze).should.be.true
|
||||
|
||||
describe '#hasTakeableItem', ->
|
||||
it "returns false if the pokemon has no item", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
it "returns true if the item can be taken", ->
|
||||
pokemon = new Pokemon(item: "Leftovers")
|
||||
pokemon.hasTakeableItem().should.be.true
|
||||
|
||||
it "returns false if the pokemon has a mail", ->
|
||||
pokemon = new Pokemon(item: "Air Mail")
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
it "returns false if the pokemon has a key item", ->
|
||||
pokemon = new Pokemon(item: "Acro Bike")
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
it "returns false if the pokemon has Multitype and a plate", ->
|
||||
pokemon = new Pokemon(ability: "Multitype", item: "Draco Plate")
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
it "returns false if the pokemon is Giratina-O", ->
|
||||
pokemon = new Pokemon(species: "Giratina", forme: "origin", item: "Griseous Orb")
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
it "returns false if the pokemon is Genesect with a Drive item", ->
|
||||
pokemon = new Pokemon(species: "Genesect", item: "Burn Drive")
|
||||
pokemon.hasTakeableItem().should.be.false
|
||||
|
||||
describe "#isWeatherDamageImmune", ->
|
||||
it "returns true if it's hailing and the Pokemon is Ice type", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = [ "Ice" ]
|
||||
pokemon.isWeatherDamageImmune(Weather.HAIL).should.be.true
|
||||
|
||||
it "returns true if it's sandstorming and the Pokemon is Rock type", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = [ "Rock" ]
|
||||
pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true
|
||||
|
||||
it "returns true if it's sandstorming and the Pokemon is Steel type", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = [ "Steel" ]
|
||||
pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true
|
||||
|
||||
it "returns true if it's sandstorming and the Pokemon is Ground type", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = [ "Ground" ]
|
||||
pokemon.isWeatherDamageImmune(Weather.SAND).should.be.true
|
||||
|
||||
it "returns false otherwise", ->
|
||||
pokemon = new Pokemon()
|
||||
pokemon.types = [ "Grass" ]
|
||||
pokemon.isWeatherDamageImmune(Weather.SAND).should.be.false
|
||||
pokemon.isWeatherDamageImmune(Weather.HAIL).should.be.false
|
||||
|
||||
describe "#useItem", ->
|
||||
it "records the item in lastItem", ->
|
||||
pokemon = new Pokemon(item: "Leftovers")
|
||||
pokemon.activate()
|
||||
pokemon.useItem()
|
||||
should.exist pokemon.lastItem
|
||||
pokemon.lastItem.displayName.should.equal("Leftovers")
|
||||
|
||||
it "removes the item", ->
|
||||
pokemon = new Pokemon(item: "Leftovers")
|
||||
pokemon.activate()
|
||||
pokemon.useItem()
|
||||
pokemon.hasItem().should.be.false
|
||||
|
||||
describe "#removeItem", ->
|
||||
it "removes the item", ->
|
||||
pokemon = new Pokemon(item: "Leftovers")
|
||||
pokemon.activate()
|
||||
pokemon.removeItem()
|
||||
pokemon.hasItem().should.be.false
|
||||
|
||||
it "does not remove prior records of an item", ->
|
||||
pokemon = new Pokemon(item: "Flying Gem")
|
||||
fake = new Pokemon(item: "Leftovers")
|
||||
pokemon.activate()
|
||||
fake.activate()
|
||||
pokemon.useItem()
|
||||
pokemon.setItem(fake.getItem())
|
||||
pokemon.removeItem()
|
||||
should.exist pokemon.lastItem
|
||||
|
||||
describe '#changeForme', ->
|
||||
it "changes the Pokemon's forme from one to another", ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
pokemon.forme.should.equal('default')
|
||||
pokemon.changeForme('sky')
|
||||
pokemon.forme.should.equal('sky')
|
||||
|
||||
it 'changes base stats if applicable', ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
pokemon.baseStats.should.eql(
|
||||
attack: 100, defense: 100, hp: 100,
|
||||
specialAttack: 100, specialDefense: 100, speed: 100)
|
||||
pokemon.changeForme('sky')
|
||||
pokemon.baseStats.should.eql(
|
||||
attack: 103, defense: 75, hp: 100,
|
||||
specialAttack: 120, specialDefense: 75, speed: 127)
|
||||
|
||||
it 'changes weight if applicable', ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
pokemon.weight.should.equal(21)
|
||||
pokemon.changeForme('sky')
|
||||
pokemon.weight.should.equal(52)
|
||||
|
||||
it 'changes type if applicable', ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
pokemon.types.should.eql([ 'Grass' ])
|
||||
pokemon.changeForme('sky')
|
||||
pokemon.types.should.eql([ 'Grass', 'Flying' ])
|
||||
|
||||
it 'does nothing if the new forme is an invalid forme', ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
pokemon.forme.should.equal('default')
|
||||
pokemon.changeForme('batman')
|
||||
pokemon.forme.should.equal('default')
|
||||
|
||||
describe "#toJSON", ->
|
||||
it "can hide information", ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
json = pokemon.toJSON(hidden: true)
|
||||
json.should.not.have.property("hp")
|
||||
json.should.not.have.property("maxHP")
|
||||
json.should.not.have.property("moves")
|
||||
json.should.not.have.property("moveTypes")
|
||||
json.should.not.have.property("pp")
|
||||
json.should.not.have.property("maxPP")
|
||||
json.should.not.have.property("ivs")
|
||||
|
||||
it "shows all information otherwise", ->
|
||||
pokemon = new Pokemon(species: 'Shaymin')
|
||||
json = pokemon.toJSON()
|
||||
json.should.have.property("hp")
|
||||
json.should.have.property("maxHP")
|
||||
json.should.have.property("moves")
|
||||
json.should.have.property("moveTypes")
|
||||
json.should.have.property("pp")
|
||||
json.should.have.property("maxPP")
|
||||
json.should.have.property("ivs")
|
||||
|
||||
describe "Shedinja holding Focus Sash", ->
|
||||
it "faints from burn damage", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
|
||||
@p1.attach(Status.Burn)
|
||||
@p1.currentHP.should.equal(1)
|
||||
|
||||
@battle.endTurn()
|
||||
@p1.currentHP.should.equal(0)
|
||||
|
||||
it "faints from switching into hazards", ->
|
||||
shared.create.call this,
|
||||
team2: [Factory("Magikarp"), Factory("Shedinja", item: "Focus Sash")]
|
||||
|
||||
@battle.performMove(@p1, @battle.getMove("Stealth Rock"))
|
||||
@battle.performSwitch(@team2.first(), 1)
|
||||
|
||||
pokemon = @team2.first()
|
||||
(pokemon.stat('hp') - pokemon.currentHP).should.equal(1)
|
||||
|
||||
it "faints from Perish Song", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
|
||||
|
||||
@battle.performMove(@p2, @battle.getMove('Perish Song'))
|
||||
@battle.endTurn() for x in [0..3]
|
||||
@p1.isFainted().should.be.true
|
||||
|
||||
it "faints from Destiny Bond", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
|
||||
|
||||
@p2.currentHP = 1
|
||||
@battle.performMove(@p2, @battle.getMove("Destiny Bond"))
|
||||
@battle.performMove(@p1, @battle.getMove("Tackle"))
|
||||
@p1.isFainted().should.be.true
|
||||
|
||||
it "faints from using Final Gambit", ->
|
||||
shared.create.call(this, team1: [Factory("Shedinja", item: "Focus Sash")])
|
||||
finalGambit = @battle.getMove("Final Gambit")
|
||||
@battle.performMove(@p1, finalGambit)
|
||||
@p1.isFainted().should.be.true
|
||||
35
test/bw/priorities.coffee
Normal file
35
test/bw/priorities.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
require '../helpers'
|
||||
|
||||
{Ability} = require('../../server/bw/data/abilities')
|
||||
{Item} = require('../../server/bw/data/items')
|
||||
{Attachment, Status} = require('../../server/bw/attachment')
|
||||
Priorities = require('../../server/bw/priorities')
|
||||
Query = require('../../server/bw/queries')
|
||||
shared = require('../shared')
|
||||
|
||||
describe "BW Priorities:", ->
|
||||
ensureAttachments = (arrayOfAttachments, eventName) ->
|
||||
attachments = (a for a in arrayOfAttachments when a.prototype[eventName]? && a not in Priorities[eventName])
|
||||
attachments = attachments.map((a) -> a.displayName || a::name)
|
||||
if attachments.length > 0
|
||||
throw new Error("#{attachments.join(', ')} must specify their #{eventName} priority.")
|
||||
|
||||
it "ensures all relevant attachments have their specified event names", ->
|
||||
for eventName of Priorities
|
||||
ensureAttachments((klass for name, klass of Attachment), eventName)
|
||||
ensureAttachments((klass for name, klass of Item), eventName)
|
||||
ensureAttachments((klass for name, klass of Ability), eventName)
|
||||
|
||||
describe "Queries", ->
|
||||
it "execute priorities in order", ->
|
||||
shared.create.call(this)
|
||||
@battle.attach(Attachment.TrickRoom)
|
||||
@team2.attach(Attachment.Reflect)
|
||||
@p1.attach(Attachment.Ingrain)
|
||||
spy1 = @sandbox.spy(Attachment.TrickRoom.prototype, 'endTurn')
|
||||
spy2 = @sandbox.spy(Attachment.Reflect.prototype, 'endTurn')
|
||||
spy3 = @sandbox.spy(Attachment.Ingrain.prototype, 'endTurn')
|
||||
|
||||
Query("endTurn", @battle.getAllAttachments())
|
||||
spy3.calledBefore(spy2).should.be.true
|
||||
spy2.calledBefore(spy1).should.be.true
|
||||
Reference in New Issue
Block a user