diff --git a/.gitignore b/.gitignore index e055782c7..6bc408368 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ package-lock.json /js/battle-dex-data.js /js/battle-animations-moves.js /js/battle-animations.js +/js/battle-scene-stub.js + +.vscode diff --git a/package.json b/package.json index 4c166acfe..da7d07da0 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "image-size": "^0.6.2" }, "devDependencies": { + "@types/mocha": "^5.2.5", "@types/node": "^8.0.7", "eslint": "^4.11.0", + "mocha": "^5.2.0", "testcafe": "^0.18.4" }, "private": true diff --git a/src/battle-animations.ts b/src/battle-animations.ts index c004da393..7b3b5b922 100644 --- a/src/battle-animations.ts +++ b/src/battle-animations.ts @@ -31,6 +31,80 @@ This license DOES NOT extend to any other files in this repository. */ class BattleScene { + setFrameHTML(html: any) { + this.$frame.html(html); + } + setControlsHTML(html: any) { + let $controls = this.$frame.parent().children('.battle-controls'); + $controls.html(html); + } + + // Methods defined in PokemonSprite but need to be called through BattleScene + removeEffect(pokemon: Pokemon, id: ID, instant?: boolean) { + return pokemon.sprite.removeEffect(id, instant); + } + addEffect(pokemon: Pokemon, id: ID, instant?: boolean) { + return pokemon.sprite.addEffect(id, instant); + } + animSummon(pokemon: Pokemon, slot: number, instant?: boolean) { + return pokemon.sprite.animSummon(pokemon, slot, instant); + } + animUnsummon(pokemon: Pokemon, instant?: boolean) { + return pokemon.sprite.animUnsummon(pokemon, instant); + } + animDragIn(pokemon: Pokemon, slot: number) { + return pokemon.sprite.animDragIn(pokemon, slot); + } + animDragOut(pokemon: Pokemon) { + return pokemon.sprite.animDragOut(pokemon); + } + updateStatbar(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { + return pokemon.sprite.updateStatbar(pokemon, updatePrevhp, updateHp); + } + updateStatbarIfExists(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { + return pokemon.sprite.updateStatbarIfExists(pokemon, updatePrevhp, updateHp); + } + animTransform(pokemon: Pokemon, isCustomAnim?: boolean, isPermanent?: boolean) { + return pokemon.sprite.animTransform(pokemon, isCustomAnim, isPermanent); + } + clearEffects(pokemon: Pokemon) { + return pokemon.sprite.clearEffects(); + } + removeTransform(pokemon: Pokemon) { + return pokemon.sprite.removeTransform(); + } + animFaint(pokemon: Pokemon) { + return pokemon.sprite.animFaint(pokemon); + } + animReset(pokemon: Pokemon) { + return pokemon.sprite.animReset(); + } + anim(pokemon: Pokemon, end: ScenePos, transition?: string) { + return pokemon.sprite.anim(end, transition); + } + beforeMove(pokemon: Pokemon) { + return pokemon.sprite.beforeMove(); + } + afterMove(pokemon: Pokemon) { + return pokemon.sprite.afterMove(); + } + updateSpritesForSide(side: Side) { + if (side.missedPokemon && side.missedPokemon.sprite) { + side.missedPokemon.sprite.destroy(); + } + + side.missedPokemon = { + sprite: new PokemonSprite(null, { + x: side.leftof(-100), + y: side.y, + z: side.z, + opacity: 0, + }, this, side.n) + } as Pokemon; + + side.missedPokemon.sprite.isMissedPokemon = true; + } + battle: Battle; animating = true; acceleration = 1; @@ -2224,6 +2298,12 @@ class PokemonSprite extends Sprite { } } + updateStatbarIfExists(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { + if (this.$statbar) { + this.updateStatbar(pokemon, updatePrevhp, updateHp); + } + } + updateStatbar(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { if (!this.scene.animating) return; if (!pokemon.isActive()) { diff --git a/src/battle-dex.ts b/src/battle-dex.ts index f0834bcd8..b90a8f2a2 100644 --- a/src/battle-dex.ts +++ b/src/battle-dex.ts @@ -193,12 +193,12 @@ const Tools = { resourcePrefix: (() => { let prefix = ''; - if (document.location!.protocol !== 'http:') prefix = 'https:'; + if (!window.document || !document.location || document.location.protocol !== 'http:') prefix = 'https:'; return prefix + '//play.pokemonshowdown.com/'; })(), fxPrefix: (() => { - if (document.location!.protocol === 'file:') { + if (window.document && document.location && document.location.protocol === 'file:') { if (window.Replays) return 'https://play.pokemonshowdown.com/fx/'; return 'fx/'; } diff --git a/src/battle-scene-stub.ts b/src/battle-scene-stub.ts new file mode 100644 index 000000000..0f2f947bf --- /dev/null +++ b/src/battle-scene-stub.ts @@ -0,0 +1,68 @@ +class BattleSceneStub { + animating: boolean = false; + acceleration: number = NaN; + gen: number = NaN; + activeCount: number = NaN; + numericId: number = NaN; + timeOffset: number = NaN; + interruptionCount: number = NaN; + messagebarOpen: boolean = false; + + abilityActivateAnim(pokemon: Pokemon, result: string): void { } + addPokemonSprite(pokemon: Pokemon) { return null!; } + addSideCondition(siden: number, id: ID, instant?: boolean | undefined): void { } + animationOff(): void { } + animationOn(): void { } + closeMessagebar(): void { } + damageAnim(pokemon: Pokemon, damage: string | number): void { } + destroy(): void { } + finishAnimations(): JQuery.Promise, any, any> | undefined { return void(0); } + healAnim(pokemon: Pokemon, damage: string | number): void { } + hideJoinButtons(): void { } + incrementTurn(): void { } + log(html: string, preempt?: boolean | undefined): void { } + message(message: string, hiddenMessage?: string | undefined): void { } + pause(): void { } + preemptCatchup(): void { } + removeSideCondition(siden: number, id: ID): void { } + reset(): void { } + resultAnim(pokemon: Pokemon, result: string, type: "bad" | "good" | "neutral" | "par" | "psn" | "frz" | "slp" | "brn"): void { } + resume(): void { } + runMoveAnim(moveid: ID, participants: Pokemon[]): void { } + runOtherAnim(moveid: ID, participants: Pokemon[]): void { } + runPrepareAnim(moveid: ID, attacker: Pokemon, defender: Pokemon): void { } + runResidualAnim(moveid: ID, pokemon: Pokemon): void { } + runStatusAnim(moveid: ID, participants: Pokemon[]): void { } + soundStart(): void { } + soundStop(): void { } + startAnimations(): void { } + teamPreview(): void { } + teamPreviewEnd(): void { } + updateGen(): void { } + updateSidebar(side: Side): void { } + updateSidebars(): void { } + updateStatbars(): void { } + updateWeather(instant?: boolean | undefined): void { } + upkeepWeather(): void { } + wait(time: number): void { } + setFrameHTML(html: any): void { } + setControlsHTML(html: any): void { } + removeEffect(pokemon: Pokemon, id: ID, instant?: boolean) { } + addEffect(pokemon: Pokemon, id: ID, instant?: boolean) { } + animSummon(pokemon: Pokemon, slot: number, instant?: boolean) { } + animUnsummon(pokemon: Pokemon, instant?: boolean) { } + animDragIn(pokemon: Pokemon, slot: number) { } + animDragOut(pokemon: Pokemon) { } + updateStatbar(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { } + updateStatbarIfExists(pokemon: Pokemon, updatePrevhp?: boolean, updateHp?: boolean) { } + animTransform(pokemon: Pokemon, isCustomAnim?: boolean, isPermanent?: boolean) { } + clearEffects(pokemon: Pokemon) { } + removeTransform(pokemon: Pokemon) { } + animFaint(pokemon: Pokemon) { } + animReset(pokemon: Pokemon) { } + anim(pokemon: Pokemon, end: ScenePos, transition?: string) { } + beforeMove(pokemon: Pokemon) { } + afterMove(pokemon: Pokemon) { } + updateSpritesForSide(side: Side) { } + unlink(userid: string, showRevealButton = false) { } +} diff --git a/src/battle.ts b/src/battle.ts index dd3c87bec..3385d8551 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -245,26 +245,26 @@ class Pokemon { return this.ident.substr(0, 2) + slots[this.slot] + this.ident.substr(2); } removeVolatile(volatile: ID) { - this.sprite.removeEffect(volatile); + this.side.battle.scene.removeEffect(this, volatile); if (!this.hasVolatile(volatile)) return; delete this.volatiles[volatile]; } addVolatile(volatile: ID, ...args: any[]) { if (this.hasVolatile(volatile) && !args.length) return; this.volatiles[volatile] = [volatile, ...args] as EffectState; - this.sprite.addEffect(volatile); + this.side.battle.scene.addEffect(this, volatile); } hasVolatile(volatile: ID) { return !!this.volatiles[volatile]; } removeTurnstatus(volatile: ID) { - this.sprite.removeEffect(volatile); + this.side.battle.scene.removeEffect(this, volatile); if (!this.hasTurnstatus(volatile)) return; delete this.turnstatuses[volatile]; } addTurnstatus(volatile: ID) { volatile = toId(volatile); - this.sprite.addEffect(volatile); + this.side.battle.scene.addEffect(this, volatile); if (this.hasTurnstatus(volatile)) return; this.turnstatuses[volatile] = [volatile]; } @@ -278,7 +278,7 @@ class Pokemon { this.turnstatuses = {}; } removeMovestatus(volatile: ID) { - this.sprite.removeEffect(volatile); + this.side.battle.scene.removeEffect(this, volatile); if (!this.hasMovestatus(volatile)) return; delete this.movestatuses[volatile]; } @@ -286,7 +286,7 @@ class Pokemon { volatile = toId(volatile); if (this.hasMovestatus(volatile)) return; this.movestatuses[volatile] = [volatile]; - this.sprite.addEffect(volatile); + this.side.battle.scene.addEffect(this, volatile); } hasMovestatus(volatile: ID) { return !!this.movestatuses[volatile]; @@ -301,7 +301,7 @@ class Pokemon { this.volatiles = {}; this.clearTurnstatuses(); this.clearMovestatuses(); - this.sprite.clearEffects(); + this.side.battle.scene.clearEffects(this); } markMove(moveName: string, pp?: number, recursionSource?: string) { if (recursionSource === this.ident) return; @@ -475,7 +475,7 @@ class Pokemon { pokemon.boosts = {}; pokemon.volatiles = {}; - pokemon.sprite.removeTransform(); + pokemon.side.battle.scene.removeTransform(pokemon); pokemon.statusStage = 0; } copyTypesFrom(pokemon: Pokemon) { @@ -613,19 +613,7 @@ class Side { } updateSprites() { this.z = (this.n ? 200 : 0); - if (this.missedPokemon) { - this.missedPokemon.sprite.destroy(); - } - this.missedPokemon = { - sprite: new PokemonSprite(null, { - x: this.leftof(-100), - y: this.y, - z: this.z, - opacity: 0, - }, - this.battle.scene, this.n) - } as Pokemon; - this.missedPokemon.sprite.isMissedPokemon = true; + this.battle.scene.updateSpritesForSide(this); } setAvatar(spriteid: string) { this.spriteid = spriteid; @@ -801,7 +789,7 @@ class Side { this.battle.message('' + Tools.escapeHTML(pokemon.side.name) + ' sent out ' + pokemon.getFullName() + '!'); } - pokemon.sprite.animSummon(pokemon, slot); + this.battle.scene.animSummon(pokemon, slot); if (this.battle.switchCallback) this.battle.switchCallback(this.battle, this); } @@ -811,7 +799,7 @@ class Side { if (oldpokemon === pokemon) return; this.lastPokemon = oldpokemon; if (oldpokemon) { - oldpokemon.sprite.animDragOut(oldpokemon); + this.battle.scene.animDragOut(oldpokemon); oldpokemon.clearVolatile(); } pokemon.clearVolatile(); @@ -820,7 +808,7 @@ class Side { this.active[slot] = pokemon; pokemon.slot = slot; - pokemon.sprite.animDragIn(pokemon, slot); + this.battle.scene.animDragIn(pokemon, slot); if (this.battle.dragCallback) this.battle.dragCallback(this.battle, this); } @@ -846,9 +834,9 @@ class Side { pokemon.slot = slot; if (oldpokemon) { - oldpokemon.sprite.animUnsummon(oldpokemon, true); + this.battle.scene.animUnsummon(oldpokemon, true); } - pokemon.sprite.animSummon(pokemon, slot, true); + this.battle.scene.animSummon(pokemon, slot, true); // not sure if we want a different callback if (this.battle.dragCallback) this.battle.dragCallback(this.battle, this); } @@ -873,7 +861,7 @@ class Side { this.lastPokemon = pokemon; this.active[slot] = null; - pokemon.sprite.animUnsummon(pokemon); + this.battle.scene.animUnsummon(pokemon); } swapTo(pokemon: Pokemon, slot: number, kwargs: {[k: string]: string}) { if (pokemon.slot === slot) return; @@ -899,11 +887,11 @@ class Side { this.active[slot] = pokemon; this.active[oslot] = target; - pokemon.sprite.animUnsummon(pokemon, true); - if (target) target.sprite.animUnsummon(target, true); + this.battle.scene.animUnsummon(pokemon, true); + if (target) this.battle.scene.animUnsummon(target, true); - pokemon.sprite.animSummon(pokemon, slot, true); - if (target) target.sprite.animSummon(target, oslot, true); + this.battle.scene.animSummon(pokemon, slot, true); + if (target) this.battle.scene.animSummon(target, oslot, true); } swapWith(pokemon: Pokemon, target: Pokemon, kwargs: {[k: string]: string}) { // method provided for backwards compatibility only @@ -926,11 +914,11 @@ class Side { this.active[nslot] = pokemon; this.active[oslot] = target; - pokemon.sprite.animUnsummon(pokemon, true); - target.sprite.animUnsummon(target, true); + this.battle.scene.animUnsummon(pokemon, true); + this.battle.scene.animUnsummon(target, true); - pokemon.sprite.animSummon(pokemon, nslot, true); - target.sprite.animSummon(target, oslot, true); + this.battle.scene.animSummon(pokemon, nslot, true); + this.battle.scene.animSummon(target, oslot, true); } faint(pokemon: Pokemon, slot = pokemon.slot) { pokemon.clearVolatile(); @@ -946,7 +934,7 @@ class Side { pokemon.fainted = true; pokemon.hp = 0; - pokemon.sprite.animFaint(pokemon); + this.battle.scene.animFaint(pokemon); if (this.battle.faintCallback) this.battle.faintCallback(this.battle, this); } destroy() { @@ -966,7 +954,7 @@ enum Playback { } class Battle { - scene: BattleScene; + scene: BattleScene | BattleSceneStub; sidesSwitched = false; @@ -1043,7 +1031,12 @@ class Battle { constructor($frame: JQuery, $logFrame: JQuery, id = '') { this.id = id; - this.scene = new BattleScene(this, $frame, $logFrame); + + if (!$frame && !$logFrame) { + this.scene = new BattleSceneStub(); + } else { + this.scene = new BattleScene(this, $frame, $logFrame); + } this.init(); } @@ -1349,7 +1342,7 @@ class Battle { if (move.id === 'focuspunch') { pokemon.removeTurnstatus('focuspunch' as ID); } - pokemon.sprite.updateStatbar(pokemon); + this.scene.updateStatbar(pokemon); if (!target) { target = pokemon.side.foe.active[0]; } @@ -1522,7 +1515,7 @@ class Battle { } cantUseMove(pokemon: Pokemon, effect: Effect, move: Move, kwargs: {[k: string]: string}) { pokemon.clearMovestatuses(); - pokemon.sprite.updateStatbar(pokemon); + this.scene.updateStatbar(pokemon); if (effect.id in BattleStatusAnims) { this.scene.runStatusAnim(effect.id, [pokemon]); } @@ -1609,7 +1602,7 @@ class Battle { this.message('' + pokemon.getName() + (move.name ? ' can\'t use ' + move.name + '' : ' can\'t move') + '!'); break; } - pokemon.sprite.animReset(); + this.scene.animReset(pokemon); } runMinor(args?: string[], kwargs?: {[k: string]: string}, preempt?: boolean, nextArgs?: string[], nextKwargs?: {[k: string]: string}) { let actions = ''; @@ -2347,7 +2340,7 @@ class Battle { } case '-mustrecharge': { let poke = this.getPokemon(args[1])!; poke.addMovestatus('mustrecharge' as ID); - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } case '-status': { @@ -2403,7 +2396,7 @@ class Battle { actions += "" + poke.getName() + " was frozen solid!"; break; default: - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } break; @@ -2510,7 +2503,7 @@ class Battle { let poke = this.getPokemon(args[1])!; for (const target of poke.side.pokemon) { target.status = ''; - if (target.sprite.$statbar) target.sprite.updateStatbar(target); + this.scene.updateStatbarIfExists(target); } this.scene.resultAnim(poke, 'Team Cured', 'good'); @@ -2855,7 +2848,7 @@ class Battle { for (const trackedMove of tpoke.moveTrack) { poke.markMove(trackedMove[0], 0); } - poke.sprite.animTransform(poke); + this.scene.animTransform(poke); this.scene.resultAnim(poke, 'Transformed', 'good'); break; } case '-formechange': { @@ -2899,7 +2892,7 @@ class Battle { } } poke.addVolatile('formechange' as ID, template.species); // the formechange volatile reminds us to revert the sprite change on switch-out - poke.sprite.animTransform(poke, isCustomAnim); + this.scene.animTransform(poke, isCustomAnim); break; } case '-mega': { let poke = this.getPokemon(args[1])!; @@ -2942,7 +2935,7 @@ class Battle { poke.removeVolatile('typeadd' as ID); poke.addVolatile('typechange' as ID, types); if (kwargs.silent) { - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } if (fromeffect.id) { @@ -3001,7 +2994,7 @@ class Battle { } break; case 'leechseed': - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); actions += '' + poke.getName() + ' was seeded!'; break; case 'healblock': @@ -3143,7 +3136,7 @@ class Battle { actions += "" + poke.getName() + " fell straight down!"; poke.removeVolatile('magnetrise' as ID); poke.removeVolatile('telekinesis' as ID); - if (poke.lastMove === 'fly' || poke.lastMove === 'bounce') poke.sprite.animReset(); + if (poke.lastMove === 'fly' || poke.lastMove === 'bounce') this.scene.animReset(poke); break; case 'substitute': if (kwargs.damage) { @@ -3200,7 +3193,7 @@ class Battle { } } poke.addVolatile(effect.id); - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } case '-end': { let poke = this.getPokemon(args[1])!; @@ -3222,7 +3215,7 @@ class Battle { break; case 'skydrop': if (kwargs.interrupt) { - poke.sprite.anim({time: 100}); + this.scene.anim(poke, {time: 100}); } actions += "" + poke.getName() + " was freed from the Sky Drop!"; break; @@ -3333,7 +3326,7 @@ class Battle { } } } - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } case '-singleturn': { let poke = this.getPokemon(args[1])!; @@ -3415,7 +3408,7 @@ class Battle { actions += '' + poke.getName() + ' started heating up its beak!'; break; } - poke.sprite.updateStatbar(poke); + this.scene.updateStatbar(poke); break; } case '-singlemove': { let poke = this.getPokemon(args[1])!; @@ -3562,7 +3555,7 @@ class Battle { target.removeTurnstatus('quickguard' as ID); target.removeTurnstatus('craftyshield' as ID); target.removeTurnstatus('matblock' as ID); - target.sprite.updateStatbar(target); + this.scene.updateStatbar(target); } break; case 'spite': @@ -3575,7 +3568,7 @@ class Battle { actions += "" + poke.getName() + " couldn't stay airborne because of gravity!"; poke.removeVolatile('magnetrise' as ID); poke.removeVolatile('telekinesis' as ID); - poke.sprite.anim({time: 100}); + this.scene.anim(poke, {time: 100}); break; case 'magnitude': actions += "Magnitude " + Tools.escapeHTML(args[3]) + "!"; @@ -4051,10 +4044,10 @@ class Battle { let move = Tools.getMove(args[2]); if (this.checkActive(poke)) return; let poke2 = this.getPokemon(args[3]); - poke.sprite.beforeMove(); + this.scene.beforeMove(poke); kwargs.silent = '.'; this.useMove(poke, move, poke2, kwargs); - poke.sprite.afterMove(); + this.scene.afterMove(poke); break; } case '-hint': { @@ -4571,7 +4564,7 @@ class Battle { poke.details = args[2]; poke.searchid = args[1].substr(0, 2) + args[1].substr(3) + '|' + args[2]; - poke.sprite.animTransform(poke, true, true); + this.scene.animTransform(poke, true, true); if (toId(newSpecies) === 'greninjaash') { this.message('' + poke.getName() + ' became Ash-Greninja!'); } else if (toId(newSpecies) === 'mimikyubusted') { @@ -4628,9 +4621,9 @@ class Battle { let move = Tools.getMove(args[2]); if (this.checkActive(poke)) return; let poke2 = this.getPokemon(args[3]); - poke.sprite.beforeMove(); + this.scene.beforeMove(poke); this.useMove(poke, move, poke2, kwargs); - poke.sprite.afterMove(); + this.scene.afterMove(poke); break; } case 'cant': { this.endLastTurn(); @@ -4677,11 +4670,10 @@ class Battle { break; } case 'fieldhtml': { this.playbackState = Playback.Seeking; // force seeking to prevent controls etc - this.scene.$frame.html(Tools.sanitizeHTML(args[1])); + this.scene.setFrameHTML(Tools.sanitizeHTML(args[1])); break; } case 'controlshtml': { - let $controls = this.scene.$frame.parent().children('.battle-controls'); - $controls.html(Tools.sanitizeHTML(args[1])); + this.scene.setControlsHTML(Tools.sanitizeHTML(args[1])); break; } default: { this.scene.log('
Unknown command: ' + Tools.escapeHTML(args[0]) + '
'); diff --git a/test/battle-test.mocha.js b/test/battle-test.mocha.js new file mode 100644 index 000000000..7f482cca0 --- /dev/null +++ b/test/battle-test.mocha.js @@ -0,0 +1,94 @@ +const fs = require('fs'); +const assert = require('assert').strict; + +window = global; + +// Without making these modules, the best we can do is directly include them into this workspace. +eval('' + fs.readFileSync(`${__dirname}/../js/battle-scene-stub.js`)); +eval('' + fs.readFileSync(`${__dirname}/../js/battle-dex.js`)); +eval('' + fs.readFileSync(`${__dirname}/../js/battle-dex-data.js`)); +eval('' + fs.readFileSync(`${__dirname}/../js/battle.js`)); + +describe('Battle', function () { + + it('should instantiate without issue', function () { + var battle = new Battle(); + }); + + it('should process a bunch of messages properly', function () { + var battle = new Battle(); + battle.debug = true; + + battle.setQueue([ + "|init|battle", + "|title|FOO vs. BAR", + "|j|FOO", + "|j|BAR", + "|request|", + "|player|p1|FOO|169", + "|player|p2|BAR|265", + "|teamsize|p1|6", + "|teamsize|p2|6", + "|gametype|singles", + "|gen|7", + "|tier|[Gen 7] Random Battle", + "|rated|", + "|seed|", + "|rule|Sleep Clause Mod: Limit one foe put to sleep", + "|rule|HP Percentage Mod: HP is shown in percentages", + "|", + "|start", + "|switch|p1a: Leafeon|Leafeon, L83, F|100/100", + "|switch|p2a: Gliscor|Gliscor, L77, F|242/242", + "|turn|1", + ]); + battle.fastForwardTo(-1); + + var p1 = battle.sides[0]; + var p2 = battle.sides[1]; + + assert(p1.name === 'FOO'); + var p1leafeon = p1.pokemon[0]; + assert(p1leafeon.ident === 'p1: Leafeon'); + assert(p1leafeon.details === 'Leafeon, L83, F'); + assert(p1leafeon.hp === 100); + assert(p1leafeon.maxhp === 100); + assert(p1leafeon.isActive()); + assert.deepEqual(p1leafeon.moveTrack, []); + + assert(p2.name === 'BAR'); + var p2gliscor = p2.pokemon[0]; + assert(p2gliscor.ident === 'p2: Gliscor'); + assert(p2gliscor.details === 'Gliscor, L77, F'); + assert(p2gliscor.hp === 242); + assert(p2gliscor.maxhp === 242); + assert(p2gliscor.isActive()); + assert.deepEqual(p2gliscor.moveTrack, []); + + [ + "|", + "|switch|p2a: Kyurem|Kyurem-White, L73|303/303", + "|-ability|p2a: Kyurem|Turboblaze", + "|move|p1a: Leafeon|Knock Off|p2a: Kyurem", + "|-damage|p2a: Kyurem|226/303", + "|-enditem|p2a: Kyurem|Leftovers|[from] move: Knock Off|[of] p1a: Leafeon", + "|", + "|upkeep", + "|turn|2", + "|inactive|Time left: 150 sec this turn | 740 sec total", + ].forEach(msg => battle.add(msg)); + battle.fastForwardTo(-1); + + assert(!p2gliscor.isActive()); + var p2kyurem = p2.pokemon[1]; + assert(p2kyurem.ident === 'p2: Kyurem'); + assert(p2kyurem.details === 'Kyurem-White, L73'); + assert(p2kyurem.hp === 226); + assert(p2kyurem.maxhp === 303); + assert(p2kyurem.isActive()); + assert(p2kyurem.item === ''); + assert(p2kyurem.prevItem === 'Leftovers'); + + assert.deepEqual(p1leafeon.moveTrack, [['Knock Off', 1]]); + }); +});