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)