diff --git a/js/client-battle-tooltips.js b/js/client-battle-tooltips.js index bcab1c6ad..56f7c510a 100644 --- a/js/client-battle-tooltips.js +++ b/js/client-battle-tooltips.js @@ -386,9 +386,9 @@ var BattleTooltips = (function () { var template = Tools.getTemplate(pokemon.getSpecies ? pokemon.getSpecies() : pokemon.species); if (pokemon.volatiles && pokemon.volatiles.formechange) { if (pokemon.volatiles.transform) { - text += '(Transformed into ' + pokemon.volatiles.formechange[2] + ')
'; + text += '(Transformed into ' + pokemon.volatiles.formechange[1] + ')
'; } else { - text += '(Forme: ' + pokemon.volatiles.formechange[2] + ')
'; + text += '(Forme: ' + pokemon.volatiles.formechange[1] + ')
'; } } @@ -525,6 +525,9 @@ var BattleTooltips = (function () { for (var i = 0; i < pokemon.moveTrack.length; i++) { text += '• ' + this.getPPUseText(pokemon.moveTrack[i]) + '
'; } + if (pokemon.moveTrack.length > 4) { + text += '(More than 4 moves is usually a sign of Illusion Zoroark/Zorua.)'; + } text += '

'; } text += ''; diff --git a/js/client-battle.js b/js/client-battle.js index 49d9d0daf..97b6b4d39 100644 --- a/js/client-battle.js +++ b/js/client-battle.js @@ -17,7 +17,6 @@ this.$controls = this.$el.find('.battle-controls'); this.$chatFrame = this.$el.find('.battle-log'); this.$chatAdd = this.$el.find('.battle-log-add'); - this.$join = null; this.$foeHint = this.$el.find('.foehint'); BattleSound.setMute(Tools.prefs('mute')); @@ -25,6 +24,7 @@ this.tooltips = new BattleTooltips(this.battle, this); this.battle.roomid = this.id; + this.battle.joinButtons = true; this.users = {}; this.userCount = {users: 0}; this.$userList = this.$('.userlist'); @@ -36,7 +36,7 @@ this.$chat = this.$chatFrame.find('.inner'); - this.$options = this.battle.optionsElem.html('
'); + this.$options = this.battle.scene.$options.html('
'); var self = this; this.battle.customCallback = function () { self.updateControls(); }; @@ -243,11 +243,6 @@ *********************************************************/ updateControls: function (force) { - if (this.$join) { - this.$join.remove(); - this.$join = null; - } - var controlsShown = this.controlsShown; this.controlsShown = false; @@ -297,12 +292,10 @@ this.updateTimer(); } - } else if (!this.battle.mySide.initialized || !this.battle.yourSide.initialized) { + } else if (!this.battle.mySide.name || !this.battle.yourSide.name) { // empty battle this.$controls.html('

Waiting for players...

'); - this.$join = $('
'); - this.$battle.append(this.$join); } else { @@ -795,7 +788,7 @@ updateWaitControls: function () { var buf = '
'; buf += this.getPlayerChoicesHTML(); - if (!this.battle.mySide.initialized || !this.battle.yourSide.initialized || !this.request) { + if (!this.battle.mySide.name || !this.battle.yourSide.name || !this.request) { if (this.battle.kickingInactive) { buf += '

← Your opponent has disconnected. This will give them more time to reconnect.

'; } else { diff --git a/src/battle-animations-moves.ts b/src/battle-animations-moves.ts index 5e2213fd0..ac6dd64db 100644 --- a/src/battle-animations-moves.ts +++ b/src/battle-animations-moves.ts @@ -3989,7 +3989,7 @@ const BattleMoveAnims: AnimTable = { attacker.anim({opacity: 0, time: 50}, 'linear'); attacker.anim({opacity: 1, time: 50}, 'linear'); attacker.anim({opacity: 0, time: 50}, 'linear'); - battle.activityWait(200); + battle.wait(200); }, prepareMessage(pokemon) { return pokemon.getName() + ' vanished instantly!'; @@ -4826,7 +4826,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(1150); + battle.wait(1150); } }, peck: { @@ -5527,7 +5527,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(500); + battle.wait(500); } }, heavyslam: { @@ -5594,7 +5594,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(500); + battle.wait(500); } }, steamroller: { @@ -5868,7 +5868,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('rightslash', { x: defender.x, @@ -5952,7 +5952,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('rightslash', { x: defender.x, @@ -6099,7 +6099,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('foot', { x: defender.x - 10, @@ -6337,7 +6337,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('foot', { x: defender.x - 10, @@ -6569,7 +6569,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('fist', { x: defender.x, @@ -6803,7 +6803,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(800); + battle.wait(800); battle.showEffect('fist', { x: defender.x - 10, @@ -7224,7 +7224,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(1150); + battle.wait(1150); } }, meteormash: { @@ -7434,7 +7434,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(350); + battle.wait(350); } }, focuspunch: { @@ -8506,7 +8506,7 @@ const BattleMoveAnims: AnimTable = { scale: 1, time: 300 }); - battle.activityWait(700); + battle.wait(700); } }, icefang: { @@ -10084,7 +10084,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(550); + battle.wait(550); } }, fusionbolt: { @@ -10217,7 +10217,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 200 }, 'swing'); - battle.activityWait(550); + battle.wait(550); } }, zenheadbutt: { @@ -10318,7 +10318,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(350); + battle.wait(350); } }, covet: { @@ -10368,7 +10368,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(350); + battle.wait(350); battle.showEffect('heart', { x: attacker.x - 10, @@ -10404,7 +10404,7 @@ const BattleMoveAnims: AnimTable = { }, earthquake: { anim(battle, [attacker, ...defenders]) { - battle.bgElem.animate({ + battle.$bg.animate({ top: -90, bottom: 0, }, 75).animate({ @@ -10594,8 +10594,8 @@ const BattleMoveAnims: AnimTable = { }, bulldoze: { anim(battle, [attacker, ...defenders]) { - battle.bgElem.delay(275); - battle.bgElem.animate({ + battle.$bg.delay(275); + battle.$bg.animate({ top: -90, bottom: 0, }, 75).animate({ @@ -10732,7 +10732,7 @@ const BattleMoveAnims: AnimTable = { y: defender.y, time: 100 }); - battle.activityWait(325); + battle.wait(325); } } }, @@ -10758,12 +10758,12 @@ const BattleMoveAnims: AnimTable = { x: defender.x, time: 100 }); - battle.activityWait(425); + battle.wait(425); } }, earthpower: { anim(battle, [attacker, defender]) { - battle.bgElem.animate({ + battle.$bg.animate({ top: -90, bottom: 0, }, 75).animate({ @@ -12429,7 +12429,7 @@ const BattleMoveAnims: AnimTable = { scale: 1, time: 150 }); - battle.activityWait(700); + battle.wait(700); } }, meanlook: { @@ -12468,14 +12468,14 @@ const BattleMoveAnims: AnimTable = { opacity: 0.1, time: 600 }, 'accel', 'fade'); - battle.activityWait(700); + battle.wait(700); } }, fairylock: { anim(battle, [attacker, defender]) { battle.backgroundEffect('#FF99FF', 250, 0.3); battle.backgroundEffect('#AA44BB', 250, 0.2, 400); - battle.activityWait(700); + battle.wait(700); } }, rockblast: { @@ -15348,7 +15348,7 @@ const BattleMoveAnims: AnimTable = { attacker.anim({ time: 300 }, 'linear'); - battle.activityWait(800); + battle.wait(800); } }, iciclecrash: { @@ -16559,6 +16559,8 @@ const BattleMoveAnims: AnimTable = { }, 'accel'); }, residualAnim(battle, [attacker]) { + battle.backgroundEffect("url('https://play.pokemonshowdown.com/fx/bg-space.jpg')", 600, 0.4); + battle.showEffect('wisp', { x: attacker.x, y: attacker.y + 130, @@ -16571,6 +16573,8 @@ const BattleMoveAnims: AnimTable = { z: attacker.z, opacity: 1 }, 'decel', 'explode'); + + battle.timeOffset += 500; } }, healingwish: { @@ -20559,7 +20563,7 @@ const BattleMoveAnims: AnimTable = { y: defender.y - 80, opacity: 0, time: 300 - }, 'accel', 'fade'); + }, 'accel'); defender.anim({ y: defender.y, opacity: 0, @@ -23392,7 +23396,7 @@ const BattleMoveAnims: AnimTable = { time: 1200 }, 'linear'); } - battle.activityWait(500); + battle.wait(500); const defender = defenders[1] || defenders[0]; attacker.anim({ @@ -24042,7 +24046,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(1450); + battle.wait(1450); } }, smartstrike: { @@ -24872,7 +24876,7 @@ const BattleMoveAnims: AnimTable = { time: 900 }, 'accel', 'explode'); battle.showEffect('lightning', { - x: defender - 20, + x: defender.x - 20, y: defender.y, z: defender.behind(-75), yscale: 0, @@ -26633,7 +26637,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(1450); + battle.wait(1450); } }, aciddownpour: { @@ -26694,7 +26698,7 @@ const BattleMoveAnims: AnimTable = { y: defender.y - 40, opacity: 0, time: 200 - }, 'accel', 'fade'); + }, 'accel'); defender.delay(400); defender.anim({ y: defender.y, @@ -27724,7 +27728,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 350 }, 'swing'); - battle.activityWait(550); + battle.wait(550); } }, twinkletackle: { @@ -27894,7 +27898,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 400 }); - battle.activityWait(800); + battle.wait(800); } }, pulverizingpancake: { @@ -28002,7 +28006,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 400 }, 'swing'); - battle.activityWait(1450); + battle.wait(1450); } }, stokedsparksurfer: { @@ -28358,49 +28362,46 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 400 }, 'swing'); - battle.activityWait(1450); + battle.wait(1450); } }, sinisterarrowraid: { - anim(battle, [attacker, ...defenders]) { - for (const defender of defenders) { - defender.delay(1050); - defender.anim({ - z: defender.behind(20), - time: 300 - }, 'swing'); - defender.anim({ - time: 200 - }, 'swing'); - defender.anim({ - x: defender.x - 5, - time: 75 - }, 'swing'); - defender.anim({ - x: defender.x + 5, - time: 75 - }, 'swing'); - defender.anim({ - x: defender.x - 5, - time: 75 - }, 'swing'); - defender.anim({ - x: defender.x + 5, - time: 75 - }, 'swing'); - defender.anim({ - x: defender.x - 5, - time: 75 - }, 'swing'); - defender.anim({ - x: defender.x + 5, - time: 75 - }, 'swing'); - defender.anim({ - time: 100 - }, 'accel'); - } - const defender = defenders[1] || defenders[0]; + anim(battle, [attacker, defender]) { + defender.delay(1050); + defender.anim({ + z: defender.behind(20), + time: 300 + }, 'swing'); + defender.anim({ + time: 200 + }, 'swing'); + defender.anim({ + x: defender.x - 5, + time: 75 + }, 'swing'); + defender.anim({ + x: defender.x + 5, + time: 75 + }, 'swing'); + defender.anim({ + x: defender.x - 5, + time: 75 + }, 'swing'); + defender.anim({ + x: defender.x + 5, + time: 75 + }, 'swing'); + defender.anim({ + x: defender.x - 5, + time: 75 + }, 'swing'); + defender.anim({ + x: defender.x + 5, + time: 75 + }, 'swing'); + defender.anim({ + time: 100 + }, 'accel'); attacker.anim({ y: attacker.y + 120, opacity: 0, @@ -28427,6 +28428,7 @@ const BattleMoveAnims: AnimTable = { opacity: 1, time: 350 }, 'accel'); + battle.showEffect('wisp', { x: defender.x, y: defender.y, @@ -29003,7 +29005,7 @@ const BattleMoveAnims: AnimTable = { opacity: 1, time: 200 }, { - y: defender + 100, + y: defender.x + 100, opacity: 0, time: 1200 }, 'accel'); @@ -29428,7 +29430,7 @@ const BattleMoveAnims: AnimTable = { defender.anim({ time: 300 }, 'swing'); - battle.activityWait(1750); + battle.wait(1750); battle.showEffect('impact', { x: defender.x - 25, @@ -30278,7 +30280,7 @@ const BattleMoveAnims: AnimTable = { z: defender.behind(-10), opacity: 1, time: 300 - }, 'linear', 'fade'); + }, 'linear'); attacker.anim({ x: defender.leftof(-10), y: defender.y, diff --git a/src/battle-animations.ts b/src/battle-animations.ts index d4f69dd42..7c3507074 100644 --- a/src/battle-animations.ts +++ b/src/battle-animations.ts @@ -30,10 +30,2469 @@ This license DOES NOT extend to any other files in this repository. */ +class BattleScene { + battle: Battle; + animating = true; + acceleration = 1; + + /** Note: Not the actual generation of the battle, but the gen of the sprites/background */ + gen = 7; + /** 1 = singles, 2 = doubles, 3 = triples */ + activeCount = 1; + + numericId = 0; + $frame: JQuery; + $battle: JQuery = null!; + $logFrame: JQuery; + $options: JQuery = null!; + $logPreempt: JQuery = null!; + $log: JQuery = null!; + $terrain: JQuery = null!; + $weather: JQuery = null!; + $bgEffect: JQuery = null!; + $bg: JQuery = null!; + $sprite: JQuery = null!; + $sprites: [JQuery, JQuery] = [null!, null!]; + $spritesFront: [JQuery, JQuery] = [null!, null!]; + $stat: JQuery = null!; + $fx: JQuery = null!; + $leftbar: JQuery = null!; + $rightbar: JQuery = null!; + $turn: JQuery = null!; + $messagebar: JQuery = null!; + $delay: JQuery = null!; + $hiddenMessage: JQuery = null!; + + sideConditions = [{}, {}] as [{[id: string]: Sprite[]}, {[id: string]: Sprite[]}]; + + preloadDone = 0; + preloadNeeded = 0; + bgm: string | null = null; + backdropImage: string = ''; + preloadCache = {} as {[url: string]: HTMLImageElement}; + + autoScrollOnResume = false; + messagebarOpen = false; + interruptionCount = 1; + curWeather = ''; + curTerrain = ''; + + // Animation state + //////////////////////////////////// + + timeOffset = 0; + pokemonTimeOffset = 0; + minDelay = 0; + /** jQuery objects that need to finish animating */ + activeAnimations = $(); + + constructor(battle: Battle, $frame: JQuery, $logFrame: JQuery) { + this.battle = battle; + $frame.addClass('battle'); + this.$frame = $frame; + this.$logFrame = $logFrame; + + let numericId = 0; + if (battle.id) { + numericId = parseInt(battle.id.slice(battle.id.lastIndexOf('-') + 1)); + } + if (!numericId) { + numericId = Math.floor(Math.random() * 1000000); + } + this.numericId = numericId; + + this.preloadEffects(); + // reset() is called during battle initialization, so it doesn't need to be called here + } + + reset() { + this.updateGen(); + + // Log frame + ///////////// + + if (this.$options) { + this.$log.empty(); + this.$logPreempt.empty(); + } else { + this.$logFrame.empty(); + this.$options = $('
'); + this.$log = $('
'); + this.$logPreempt = $('
'); + this.$logFrame.append(this.$options); + this.$logFrame.append(this.$log); + this.$logFrame.append(this.$logPreempt); + } + + // Battle frame + /////////////// + + this.$frame.empty(); + this.$battle = $('
'); + this.$frame.append(this.$battle); + + this.$bg = $('
'); + this.$terrain = $('
'); + this.$weather = $('
'); + this.$bgEffect = $('
'); + this.$sprite = $('
'); + + this.$sprites = [$('
'), $('
')]; + this.$spritesFront = [$('
'), $('
')]; + + this.$sprite.append(this.$sprites[1]); + this.$sprite.append(this.$spritesFront[1]); + this.$sprite.append(this.$spritesFront[0]); + this.$sprite.append(this.$sprites[0]); + + this.$stat = $('
'); + this.$fx = $('
'); + this.$leftbar = $(''); + this.$rightbar = $(''); + this.$turn = $('
'); + this.$messagebar = $('
'); + this.$delay = $('
'); + this.$hiddenMessage = $(''); + + this.$battle.append(this.$bg); + this.$battle.append(this.$terrain); + this.$battle.append(this.$weather); + this.$battle.append(this.$bgEffect); + this.$battle.append(this.$sprite); + this.$battle.append(this.$stat); + this.$battle.append(this.$fx); + this.$battle.append(this.$leftbar); + this.$battle.append(this.$rightbar); + this.$battle.append(this.$turn); + this.$battle.append(this.$messagebar); + this.$battle.append(this.$delay); + this.$battle.append(this.$hiddenMessage); + + if (this.battle.ignoreNicks) { + this.$log.addClass('hidenicks'); + this.$messagebar.addClass('hidenicks'); + this.$hiddenMessage.addClass('hidenicks'); + } + + if (!this.animating) { + this.$battle.append('
seeking...
'); + } + + this.messagebarOpen = false; + this.timeOffset = 0; + this.pokemonTimeOffset = 0; + } + + animationOff() { + this.$battle.append('
seeking...
'); + this.stopAnimation(); + + this.animating = false; + this.autoScrollOnResume = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!); + this.$messagebar.empty().css({ + opacity: 0, + height: 0 + }); + } + stopAnimation() { + this.interruptionCount++; + this.$battle.find(':animated').finish(); + this.$fx.empty(); + } + animationOn() { + this.animating = true; + this.$battle.find('.seeking').remove(); + this.updateSidebars(); + for (const side of this.battle.sides) { + for (const pokemon of side.pokemon) { + pokemon.sprite.reset(pokemon); + } + } + this.updateWeather(true); + this.resetTurn(); + if (this.autoScrollOnResume) { + this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!); + } + this.resetSideConditions(); + } + pause() { + this.stopAnimation(); + this.soundPause(); + if (this.battle.resumeButton) { + this.$frame.append('
'); + this.$frame.find('div.playbutton button').click(this.battle.resumeButton); + } + } + resume() { + this.$frame.find('div.playbutton').remove(); + this.soundStart(); + } + wait(time: number) { + if (!this.animating) return; + this.timeOffset += time; + } + + // Sprite handling + ///////////////////////////////////////////////////////////////////// + + addSprite(sprite: PokemonSprite) { + if (sprite.$el) this.$sprites[sprite.siden].append(sprite.$el); + } + showEffect(effect: string | SpriteData, start: ScenePos, end: ScenePos, transition: string, after?: string) { + if (typeof effect === 'string') effect = BattleEffects[effect] as SpriteData; + if (!start.time) start.time = 0; + if (!end.time) end.time = start.time + 500; + start.time += this.timeOffset; + end.time += this.timeOffset; + if (!end.scale && end.scale !== 0 && start.scale) end.scale = start.scale; + if (!end.xscale && end.xscale !== 0 && start.xscale) end.xscale = start.xscale; + if (!end.yscale && end.yscale !== 0 && start.yscale) end.yscale = start.yscale; + end = {...start, ...end}; + + let startpos = this.pos(start, effect); + let endpos = this.posT(end, effect, transition, start); + + let $effect = $(''); + this.$fx.append($effect); + $effect = this.$fx.children().last(); + + if (start.time) { + $effect.css({...startpos, opacity: 0}); + $effect.delay(start.time).animate({ + opacity: startpos.opacity, + }, 1); + } else { + $effect.css(startpos); + } + $effect.animate(endpos, end.time! - start.time); + if (after === 'fade') { + $effect.animate({ + opacity: 0 + }, 100); + } + if (after === 'explode') { + if (end.scale) end.scale *= 3; + if (end.xscale) end.xscale *= 3; + if (end.yscale) end.yscale *= 3; + end.opacity = 0; + let endendpos = this.pos(end, effect); + $effect.animate(endendpos, 200); + } + this.waitFor($effect); + } + backgroundEffect(bg: string, duration: number, opacity = 1, delay = 0) { + let $effect = $('
'); + $effect.css({ + background: bg, + display: 'block', + opacity: 0 + }); + this.$bgEffect.append($effect); + $effect.delay(delay).animate({ + opacity: opacity + }, 250).delay(duration - 250); + $effect.animate({ + opacity: 0 + }, 250); + } + + /** + * Converts a PS location (x, y, z, scale, xscale, yscale, opacity) + * to a jQuery position (top, left, width, height, opacity) suitable + * for passing into `jQuery#css` or `jQuery#animate`. + * The display property is passed through if it exists. + */ + pos(loc: ScenePos, obj: SpriteData) { + let left, top, scale, width, height; + + loc = { + x: 0, + y: 0, + z: 0, + scale: 1, + opacity: 1, + ...loc, + }; + if (!loc.xscale && loc.xscale !== 0) loc.xscale = loc.scale; + if (!loc.yscale && loc.yscale !== 0) loc.yscale = loc.scale; + + left = 210; + top = 245; + scale = 1; + scale = 1.5 - 0.5 * ((loc.z!) / 200); + if (scale < .1) scale = .1; + + left += (410 - 190) * ((loc.z!) / 200); + top += (135 - 245) * ((loc.z!) / 200); + left += Math.floor(loc.x! * scale); + top -= Math.floor(loc.y! * scale /* - loc.x * scale / 4 */); + width = Math.floor(obj.w * scale * loc.xscale!); + height = Math.floor(obj.h * scale * loc.yscale!); + let hoffset = Math.floor((obj.h - (obj.y || 0) * 2) * scale * loc.yscale!); + left -= Math.floor(width / 2); + top -= Math.floor(hoffset / 2); + + let pos = { + left: left, + top: top, + width: width, + height: height, + opacity: loc.opacity + } as JQuery.PlainObject; + if (loc.display) pos.display = loc.display; + return pos; + } + /** + * Converts a PS location to a jQuery transition map (see `pos`) + * suitable for passing into `jQuery#animate`. + * oldLoc is required for ballistic (jumping) animations. + */ + posT(loc: ScenePos, obj: SpriteData, transition?: string, oldLoc?: ScenePos) { + const pos = this.pos(loc, obj); + const oldPos = (oldLoc ? this.pos(oldLoc, obj) : null); + let transitionMap = { + left: 'linear', + top: 'linear', + width: 'linear', + height: 'linear', + opacity: 'linear' + }; + if (transition === 'ballistic') { + transitionMap.top = (pos.top < oldPos!.top ? 'ballisticUp' : 'ballisticDown'); + } + if (transition === 'ballisticUnder') { + transitionMap.top = (pos.top < oldPos!.top ? 'ballisticDown' : 'ballisticUp'); + } + if (transition === 'ballistic2') { + transitionMap.top = (pos.top < oldPos!.top ? 'quadUp' : 'quadDown'); + } + if (transition === 'ballistic2Under') { + transitionMap.top = (pos.top < oldPos!.top ? 'quadDown' : 'quadUp'); + } + if (transition === 'swing') { + transitionMap.left = 'swing'; + transitionMap.top = 'swing'; + transitionMap.width = 'swing'; + transitionMap.height = 'swing'; + } + if (transition === 'accel') { + transitionMap.left = 'quadDown'; + transitionMap.top = 'quadDown'; + transitionMap.width = 'quadDown'; + transitionMap.height = 'quadDown'; + } + if (transition === 'decel') { + transitionMap.left = 'quadUp'; + transitionMap.top = 'quadUp'; + transitionMap.width = 'quadUp'; + transitionMap.height = 'quadUp'; + } + return { + left: [pos.left, transitionMap.left], + top: [pos.top, transitionMap.top], + width: [pos.width, transitionMap.width], + height: [pos.height, transitionMap.height], + opacity: [pos.opacity, transitionMap.opacity] + } as JQuery.PlainObject; + } + + waitFor(elem: JQuery) { + this.activeAnimations = this.activeAnimations.add(elem); + } + + startAnimations() { + this.$fx.empty(); + this.activeAnimations = $(); + this.timeOffset = 0; + this.minDelay = 0; + } + + finishAnimations() { + if (this.minDelay || (this.timeOffset && !this.activeAnimations.length)) { + this.$delay.delay(Math.max(this.minDelay, this.timeOffset)); + this.activeAnimations = this.activeAnimations.add(this.$delay); + } + if (!this.activeAnimations.length) return undefined; + return this.activeAnimations.promise(); + } + + // Messagebar and log + ///////////////////////////////////////////////////////////////////// + + log(html: string, preempt?: boolean) { + let willScroll = false; + if (this.animating) willScroll = (this.$logFrame.scrollTop()! + 60 >= this.$log.height()! + this.$logPreempt.height()! - this.$options.height()! - this.$logFrame.height()!); + if (preempt) { + this.$logPreempt.append(html); + } else { + this.$log.append(html); + } + if (willScroll) { + this.$logFrame.scrollTop(this.$log.height()! + this.$logPreempt.height()!); + } + } + preemptCatchup() { + this.$log.append(this.$logPreempt.children().first()); + } + message(message: string, hiddenMessage?: string) { + if (!this.messagebarOpen) { + this.log('
'); + if (this.animating) { + this.$messagebar.empty(); + this.$messagebar.css({ + display: 'block', + opacity: 0, + height: 'auto' + }); + this.$messagebar.animate({ + opacity: 1 + }, this.battle.messageFadeTime / this.acceleration); + } + } + if (this.battle.hardcoreMode && message.slice(0, 8) === '(') { + hiddenMessage = message + hiddenMessage; + message = ''; + } + if (message && this.animating) { + this.$hiddenMessage.append('

'); + let $message = this.$hiddenMessage.children().last(); + $message.html(message); + $message.css({ + display: 'block', + opacity: 0 + }); + $message.animate({ + height: 'hide' + }, 1, () => { + $message.appendTo(this.$messagebar); + $message.animate({ + height: 'show', + 'padding-bottom': 4, + opacity: 1 + }, this.battle.messageFadeTime / this.acceleration); + }); + this.waitFor($message); + } + this.messagebarOpen = true; + this.log('
' + message + (hiddenMessage ? hiddenMessage : '') + '
'); + } + closeMessagebar() { + if (this.messagebarOpen) { + this.messagebarOpen = false; + if (this.animating) { + this.$messagebar.delay(this.battle.messageShownTime / this.acceleration).animate({ + opacity: 0 + }, this.battle.messageFadeTime / this.acceleration); + this.waitFor(this.$messagebar); + } + } + } + + // General updating + ///////////////////////////////////////////////////////////////////// + + runMoveAnim(moveid: ID, participants: Pokemon[]) { + if (!this.animating) return; + BattleMoveAnims[moveid].anim(this, participants.map(p => p.sprite)); + } + + runOtherAnim(moveid: ID, participants: Pokemon[]) { + if (!this.animating) return; + BattleOtherAnims[moveid].anim(this, participants.map(p => p.sprite)); + } + + runStatusAnim(moveid: ID, participants: Pokemon[]) { + if (!this.animating) return; + BattleStatusAnims[moveid].anim(this, participants.map(p => p.sprite)); + } + + runResidualAnim(moveid: ID, pokemon: Pokemon) { + if (!this.animating) return; + BattleMoveAnims[moveid].residualAnim!(this, [pokemon.sprite]); + } + + runPrepareAnim(moveid: ID, attacker: Pokemon, defender: Pokemon) { + if (!this.animating) return; + const moveAnim = BattleMoveAnims[moveid]; + if (!moveAnim.prepareAnim) return; + moveAnim.prepareAnim(this, [attacker.sprite, defender.sprite]); + this.message('' + moveAnim.prepareMessage!(attacker, defender) + ''); + } + + updateGen() { + let gen = this.battle.gen; + if (Tools.prefs('nopastgens')) gen = 6; + if (Tools.prefs('bwgfx') && gen > 5) gen = 5; + this.gen = gen; + this.activeCount = this.battle.mySide && this.battle.mySide.active.length || 1; + + if (gen <= 1) this.backdropImage = 'fx/bg-gen1.png?'; + else if (gen <= 2) this.backdropImage = 'fx/bg-gen2.png?'; + else if (gen <= 3) this.backdropImage = 'fx/' + BattleBackdropsThree[this.numericId % BattleBackdropsThree.length] + '?'; + else if (gen <= 4) this.backdropImage = 'fx/' + BattleBackdropsFour[this.numericId % BattleBackdropsFour.length]; + else if (gen <= 5) this.backdropImage = 'fx/' + BattleBackdropsFive[this.numericId % BattleBackdropsFive.length]; + else this.backdropImage = 'sprites/gen6bgs/' + BattleBackdrops[this.numericId % BattleBackdrops.length]; + + if (this.$bg) { + this.$bg.css('background-image', 'url(' + Tools.resourcePrefix + '' + this.backdropImage + ')'); + } + } + + updateSidebar(side: Side) { + if (!this.animating) return; + let pokemonhtml = ''; + let noShow = this.battle.hardcoreMode && this.battle.gen < 7; + let pokemonCount = Math.max(side.pokemon.length, 6); + for (let i = 0; i < pokemonCount; i++) { + let poke = side.pokemon[i]; + if (i >= side.totalPokemon) { + pokemonhtml += ''; + } else if (noShow && poke && poke.fainted) { + pokemonhtml += ''; + } else if (noShow && poke && poke.status) { + pokemonhtml += ''; + } else if (noShow) { + pokemonhtml += ''; + } else if (!poke) { + pokemonhtml += ''; + } else if (!poke.ident && this.battle.teamPreviewCount && this.battle.teamPreviewCount < side.pokemon.length) { + pokemonhtml += ''; + } else { + pokemonhtml += ''; + } + if (i % 3 === 2) pokemonhtml += '
'; + } + pokemonhtml = '
' + pokemonhtml + '
'; + const $sidebar = (side.n ? this.$rightbar : this.$leftbar); + if (side.name) { + $sidebar.html('
' + Tools.escapeHTML(side.name) + '
' + pokemonhtml + '
'); + $sidebar.find('.trainer').css('opacity', 1); + } else { + $sidebar.find('.trainer').css('opacity', 0.4); + } + } + updateSidebars() { + for (const side of this.battle.sides) this.updateSidebar(side); + } + updateStatbars() { + for (const side of this.battle.sides) { + for (const active of side.active) if (active) { + active.sprite.updateStatbar(active); + } + } + } + + teamPreview(start?: boolean) { + for (let siden = 0; siden < 2; siden++) { + let side = this.battle.sides[siden]; + let textBuf = ''; + let buf = ''; + let buf2 = ''; + this.$sprites[siden].empty(); + if (!start) { + side.updateSprites(); + continue; + } + for (let i = 0; i < side.pokemon.length; i++) { + let pokemon = side.pokemon[i]; + + let spriteData = Tools.getSpriteData(pokemon, siden, { + gen: this.gen, + noScale: true + }); + let y = 0; + let x = 0; + if (siden) { + y = 48 + 50 + 3 * (i + 6 - side.pokemon.length); + x = 48 + 180 + 50 * (i + 6 - side.pokemon.length); + } else { + y = 48 + 200 + 3 * i; + x = 48 + 100 + 50 * i; + } + if (textBuf) textBuf += ' / '; + textBuf += pokemon.species; + let url = spriteData.url; + // if (this.paused) url.replace('/xyani', '/xy').replace('.gif', '.png'); + buf += ''; + buf2 += '
'; + if (pokemon.gender === 'F') { + buf2 += 'F '; + } else if (pokemon.gender === 'M') { + buf2 += 'M '; + } + if (pokemon.level !== 100) { + buf2 += 'L' + pokemon.level + ''; + } + if (pokemon.item) { + buf2 += ' F'; + } + buf2 += '
'; + } + side.totalPokemon = side.pokemon.length; + if (textBuf) { + this.log('
' + Tools.escapeHTML(side.name) + '\'s team: ' + Tools.escapeHTML(textBuf) + '
'); + } + this.$sprites[siden].html(buf + buf2); + } + if (start) { + this.wait(1000); + this.updateSidebars(); + } + } + + showJoinButtons() { + if (!this.battle.joinButtons) return; + if (this.battle.ended || this.battle.rated) return; + if (!this.battle.p1.name) { + this.$battle.append('
'); + } + if (!this.battle.p2.name) { + this.$battle.append('
'); + } + } + hideJoinButtons() { + if (!this.battle.joinButtons) return; + this.$battle.find('.playbutton, .playbutton2').remove(); + } + + pseudoWeatherLeft(pWeather: WeatherState) { + let buf = '
' + Tools.getMove(pWeather[0]).name; + if (!pWeather[1] && pWeather[2]) { + pWeather[1] = pWeather[2]; + pWeather[2] = 0; + } + if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf; + if (pWeather[2]) { + return buf + ' (' + pWeather[1] + ' or ' + pWeather[2] + ' turns)'; + } + if (pWeather[1]) { + return buf + ' (' + pWeather[1] + ' turn' + (pWeather[1] == 1 ? '' : 's') + ')'; + } + return buf; // weather not found + } + sideConditionLeft(cond: [string, number, number, number], siden: number) { + if (!cond[2] && !cond[3]) return ''; + let buf = '
' + (siden ? "Foe's " : "") + Tools.getMove(cond[0]).name; + if (!cond[2] && cond[3]) { + cond[2] = cond[3]; + cond[3] = 0; + } + if (this.battle.gen < 7 && this.battle.hardcoreMode) return buf; + if (!cond[3]) { + return buf + ' (' + cond[2] + ' turn' + (cond[2] == 1 ? '' : 's') + ')'; + } + return buf + ' (' + cond[2] + ' or ' + cond[3] + ' turns)'; + } + weatherLeft() { + if (this.battle.gen < 7 && this.battle.hardcoreMode) return ''; + if (this.battle.weatherMinTimeLeft != 0) { + return ' (' + this.battle.weatherMinTimeLeft + ' or ' + this.battle.weatherTimeLeft + ' turns)'; + } + if (this.battle.weatherTimeLeft != 0) { + return ' (' + this.battle.weatherTimeLeft + ' turn' + (this.battle.weatherTimeLeft == 1 ? '' : 's') + ')'; + } + return ''; + } + upkeepWeather() { + const isIntense = (this.curWeather === 'desolateland' || this.curWeather === 'primordialsea' || this.curWeather === 'deltastream'); + this.$weather.animate({ + opacity: 1.0 + }, 300).animate({ + opacity: isIntense ? 0.9 : 0.5 + }, 300); + } + updateWeather(instant?: boolean) { + if (!this.animating) return; + let isIntense = false; + let weatherNameTable = { + sunnyday: 'Sun', + desolateland: 'Intense Sun', + raindance: 'Rain', + primordialsea: 'Heavy Rain', + sandstorm: 'Sandstorm', + hail: 'Hail', + deltastream: 'Strong Winds' + } as {[id: string]: string}; + let weather = this.battle.weather; + let terrain = '' as ID; + for (const pseudoWeatherData of this.battle.pseudoWeather) { + let pwid = toId(pseudoWeatherData[0]); + switch (pwid) { + case 'electricterrain': + case 'grassyterrain': + case 'mistyterrain': + case 'psychicterrain': + terrain = pwid; + break; + default: + if (!terrain) terrain = 'pseudo' as ID; + break; + } + } + if (weather === 'desolateland' || weather === 'primordialsea' || weather === 'deltastream') { + isIntense = true; + } + + let weatherhtml = ''; + if (weather && weather in weatherNameTable) { + weatherhtml += '
' + weatherNameTable[weather] + this.weatherLeft(); + } + for (const pseudoWeather of this.battle.pseudoWeather) { + weatherhtml += this.pseudoWeatherLeft(pseudoWeather); + } + for (const side of this.battle.sides) { + for (const id in side.sideConditions) { + weatherhtml += this.sideConditionLeft(side.sideConditions[id], side.n); + } + } + + if (instant) { + if (this.curWeather === weather && this.curTerrain === terrain) return; + this.$terrain.attr('class', terrain ? 'weather ' + terrain + 'weather' : 'weather'); + this.curTerrain = terrain; + this.$weather.attr('class', weather ? 'weather ' + weather + 'weather' : 'weather'); + this.curWeather = weather; + this.$weather.html('' + weatherhtml + ''); + return; + } + + if (weather !== this.curWeather) { + this.$weather.animate({ + opacity: 0, + }, this.curWeather ? 300 : 100, () => { + this.$weather.html('' + weatherhtml + ''); + this.$weather.attr('class', weather ? 'weather ' + weather + 'weather' : 'weather'); + this.$weather.animate({opacity: isIntense ? 0.9 : 0.5}, 300); + }); + this.curWeather = weather; + } else { + this.$weather.html('' + weatherhtml + ''); + } + + if (terrain !== this.curTerrain) { + this.$terrain.animate({ + top: 360, + }, this.curTerrain ? 400 : 1, () => { + this.$terrain.attr('class', terrain ? 'weather ' + terrain + 'weather' : 'weather'); + this.$weather.animate({top: 0}, 400); + }); + this.curTerrain = terrain; + } + } + resetTurn() { + if (!this.battle.turn) { + this.$turn.html(''); + return; + } + this.$turn.html('
Turn ' + this.battle.turn + '
'); + } + incrementTurn() { + if (!this.animating) return; + + const turn = this.battle.turn; + if (!turn) return; + const $prevTurn = this.$turn.children(); + const $newTurn = $('
Turn ' + turn + '
'); + $newTurn.css({ + opacity: 0, + left: 160, + }); + this.$turn.append($newTurn); + $newTurn.animate({ + opacity: 1, + left: 110, + }, 500).animate({ + opacity: .4, + }, 1500); + $prevTurn.animate({ + opacity: 0, + left: 60, + }, 500, function () { + $prevTurn.remove(); + }); + if (this.battle.turnsSinceMoved > 2) { + this.acceleration = (this.battle.messageFadeTime < 150 ? 2 : 1) * Math.min(this.battle.turnsSinceMoved - 1, 3); + } else { + this.acceleration = (this.battle.messageFadeTime < 150 ? 2 : 1); + } + this.wait(500 / this.acceleration); + } + + addPokemonSprite(pokemon: Pokemon) { + const siden = pokemon.side.n; + const sprite = new PokemonSprite(Tools.getSpriteData(pokemon, siden, { + gen: this.gen, + }), { + x: pokemon.side.x, + y: pokemon.side.y, + z: pokemon.side.z, + opacity: 0, + }, this, siden); + if (sprite.$el) this.$sprites[siden].append(sprite.$el); + return sprite; + } + + addSideCondition(siden: number, id: ID, instant?: boolean) { + if (!this.animating) return; + const side = this.battle.sides[siden]; + switch (id) { + case 'auroraveil': + const auroraveil = new Sprite(BattleEffects.auroraveil, { + display: 'block', + x: side.x, + y: side.y, + z: side.behind(-14), + xscale: 1, + yscale: 0, + opacity: 0.1, + }, this); + this.$spritesFront[siden].append(auroraveil.$el!); + this.sideConditions[siden][id] = [auroraveil]; + auroraveil.anim({ + opacity: 0.7, + time: instant ? 0 : 400, + }).anim({ + opacity: 0.3, + time: instant ? 0 : 300, + }); + break; + case 'reflect': + const reflect = new Sprite(BattleEffects.reflect, { + display: 'block', + x: side.x, + y: side.y, + z: side.behind(-17), + xscale: 1, + yscale: 0, + opacity: 0.1, + }, this); + this.$spritesFront[siden].append(reflect.$el!); + this.sideConditions[siden][id] = [reflect]; + reflect.anim({ + opacity: 0.7, + time: instant ? 0 : 400, + }).anim({ + opacity: 0.3, + time: instant ? 0 : 300, + }); + break; + case 'safeguard': + const safeguard = new Sprite(BattleEffects.safeguard, { + display: 'block', + x: side.x, + y: side.y, + z: side.behind(-20), + xscale: 1, + yscale: 0, + opacity: 0.1, + }, this); + this.$spritesFront[siden].append(safeguard.$el!); + this.sideConditions[siden][id] = [safeguard]; + safeguard.anim({ + opacity: 0.7, + time: instant ? 0 : 400, + }).anim({ + opacity: 0.3, + time: instant ? 0 : 300, + }); + break; + case 'lightscreen': + const lightscreen = new Sprite(BattleEffects.lightscreen, { + display: 'block', + x: side.x, + y: side.y, + z: side.behind(-23), + xscale: 1, + yscale: 0, + opacity: 0.1, + }, this); + this.$spritesFront[siden].append(lightscreen.$el!); + this.sideConditions[siden][id] = [lightscreen]; + lightscreen.anim({ + opacity: 0.7, + time: instant ? 0 : 400, + }).anim({ + opacity: 0.3, + time: instant ? 0 : 300, + }); + break; + case 'mist': + const mist = new Sprite(BattleEffects.mist, { + display: 'block', + x: side.x, + y: side.y, + z: side.behind(-27), + xscale: 1, + yscale: 0, + opacity: 0.1, + }, this); + this.$spritesFront[siden].append(mist.$el!); + this.sideConditions[siden][id] = [mist]; + mist.anim({ + opacity: 0.7, + time: instant ? 0 : 400, + }).anim({ + opacity: 0.3, + time: instant ? 0 : 300, + }); + break; + case 'stealthrock': + const rock1 = new Sprite(BattleEffects.rock1, { + display: 'block', + x: side.leftof(-40), + y: side.y - 10, + z: side.z, + opacity: 0.5, + scale: 0.2, + }, this); + + const rock2 = new Sprite(BattleEffects.rock2, { + display: 'block', + x: side.leftof(-20), + y: side.y - 40, + z: side.z, + opacity: 0.5, + scale: 0.2, + }, this); + + const rock3 = new Sprite(BattleEffects.rock1, { + display: 'block', + x: side.leftof(30), + y: side.y - 20, + z: side.z, + opacity: 0.5, + scale: 0.2, + }, this); + + const rock4 = new Sprite(BattleEffects.rock2, { + display: 'block', + x: side.leftof(10), + y: side.y - 30, + z: side.z, + opacity: 0.5, + scale: 0.2, + }, this); + + this.$spritesFront[siden].append(rock1.$el!); + this.$spritesFront[siden].append(rock2.$el!); + this.$spritesFront[siden].append(rock3.$el!); + this.$spritesFront[siden].append(rock4.$el!); + this.sideConditions[siden][id] = [rock1, rock2, rock3, rock4]; + break; + case 'spikes': + let spikeArray = this.sideConditions[siden]['spikes']; + if (!spikeArray) { + spikeArray = []; + this.sideConditions[siden]['spikes'] = spikeArray; + } + let levels = this.battle.sides[siden].sideConditions['spikes'][1]; + if (spikeArray.length < 1 && levels >= 1) { + const spike1 = new Sprite(BattleEffects.caltrop, { + display: 'block', + x: side.x - 25, + y: side.y - 40, + z: side.z, + scale: 0.3, + }, this); + this.$spritesFront[siden].append(spike1.$el!); + spikeArray.push(spike1); + } + if (spikeArray.length < 2 && levels >= 2) { + const spike2 = new Sprite(BattleEffects.caltrop, { + display: 'block', + x: side.x + 30, + y: side.y - 45, + z: side.z, + scale: .3 + }, this); + this.$spritesFront[siden].append(spike2.$el!); + spikeArray.push(spike2); + } + if (spikeArray.length < 3 && levels >= 3) { + const spike3 = new Sprite(BattleEffects.caltrop, { + display: 'block', + x: side.x + 50, + y: side.y - 40, + z: side.z, + scale: .3 + }, this); + this.$spritesFront[siden].append(spike3.$el!); + spikeArray.push(spike3); + } + break; + case 'toxicspikes': + let tspikeArray = this.sideConditions[siden]['toxicspikes']; + if (!tspikeArray) { + tspikeArray = []; + this.sideConditions[siden]['toxicspikes'] = tspikeArray; + } + let tspikeLevels = this.battle.sides[siden].sideConditions['toxicspikes'][1]; + if (tspikeArray.length < 1 && tspikeLevels >= 1) { + const tspike1 = new Sprite(BattleEffects.poisoncaltrop, { + display: 'block', + x: side.x + 5, + y: side.y - 40, + z: side.z, + scale: 0.3, + }, this); + this.$spritesFront[siden].append(tspike1.$el!); + tspikeArray.push(tspike1); + } + if (tspikeArray.length < 2 && tspikeLevels >= 2) { + const tspike2 = new Sprite(BattleEffects.poisoncaltrop, { + display: 'block', + x: side.x - 15, + y: side.y - 35, + z: side.z, + scale: .3 + }, this); + this.$spritesFront[siden].append(tspike2.$el!); + tspikeArray.push(tspike2); + } + break; + case 'stickyweb': + const web = new Sprite(BattleEffects.web, { + display: 'block', + x: side.x + 15, + y: side.y - 35, + z: side.z, + opacity: 0.4, + scale: 0.7, + }, this); + this.$spritesFront[siden].append(web.$el!); + this.sideConditions[siden][id] = [web]; + break; + } + } + removeSideCondition(siden: number, id: ID) { + if (!this.animating) return; + if (this.sideConditions[siden][id]) { + for (const sprite of this.sideConditions[siden][id]) sprite.destroy(); + delete this.sideConditions[siden][id]; + } + } + resetSideConditions() { + for (let siden = 0; siden < this.sideConditions.length; siden++) { + for (const id in this.sideConditions[siden]) { + this.removeSideCondition(siden, id as ID); + } + for (const id in this.battle.sides[siden].sideConditions) { + this.addSideCondition(siden, id as ID); + } + } + } + + resultAnim(pokemon: Pokemon, result: string, type: 'bad' | 'good' | 'neutral' | StatusName) { + if (!this.animating) return; + let $effect = $('
' + result + '
'); + this.$fx.append($effect); + $effect.delay(this.timeOffset).css({ + display: 'block', + opacity: 0, + top: pokemon.sprite.top - 5, + left: pokemon.sprite.left - 75 + }).animate({ + opacity: 1 + }, 1); + $effect.animate({ + opacity: 0, + top: pokemon.sprite.top - 65 + }, 1000, 'swing'); + this.wait(this.acceleration < 2 ? 350 : 250); + pokemon.sprite.updateStatbar(pokemon); + if (this.acceleration < 3) this.waitFor($effect); + } + abilityActivateAnim(pokemon: Pokemon, result: string) { + if (!this.animating) return; + this.$fx.append('
' + result + '
'); + let $effect = this.$fx.children().last(); + $effect.delay(this.timeOffset).css({ + display: 'block', + opacity: 0, + top: pokemon.sprite.top + 15, + left: pokemon.sprite.left - 75 + }).animate({ + opacity: 1 + }, 1); + $effect.delay(800).animate({ + opacity: 0 + }, 400, 'swing'); + this.wait(100); + pokemon.sprite.updateStatbar(pokemon); + if (this.acceleration < 3) this.waitFor($effect); + } + damageAnim(pokemon: Pokemon, damage: number | string) { + if (!this.animating) return; + if (!pokemon.sprite.$statbar) return; + pokemon.sprite.updateHPText(pokemon); + + let $hp = pokemon.sprite.$statbar.find('div.hp'); + let w = pokemon.hpWidth(150); + let hpcolor = pokemon.getHPColor(); + let callback; + if (hpcolor === 'y') callback = function () { + $hp.addClass('hp-yellow'); + }; + if (hpcolor === 'r') callback = function () { + $hp.addClass('hp-yellow hp-red'); + }; + + this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Damage' : '−' + damage, 'bad'); + + $hp.animate({ + width: w, + 'border-right-width': w ? 1 : 0 + }, 350, callback); + } + healAnim(pokemon: Pokemon, damage: number | string) { + if (!this.animating) return; + if (!pokemon.sprite.$statbar) return; + pokemon.sprite.updateHPText(pokemon); + + let $hp = pokemon.sprite.$statbar.find('div.hp'); + let w = pokemon.hpWidth(150); + let hpcolor = pokemon.getHPColor(); + let callback; + if (hpcolor === 'g') callback = function () { + $hp.removeClass('hp-yellow hp-red'); + }; + if (hpcolor === 'y') callback = function () { + $hp.removeClass('hp-red'); + }; + + this.resultAnim(pokemon, this.battle.hardcoreMode ? 'Heal' : '+' + damage, 'good'); + + $hp.animate({ + width: w, + 'border-right-width': w ? 1 : 0 + }, 350, callback); + } + + // Misc + ///////////////////////////////////////////////////////////////////// + + preloadImage(url: string) { + let token = url.replace(/\.(gif|png)$/, '').replace(/\//g, '-'); + if (this.preloadCache[token]) { + return; + } + this.preloadNeeded++; + this.preloadCache[token] = new Image(); + this.preloadCache[token].onload = () => { + this.preloadDone++; + }; + this.preloadCache[token].src = url; + } + preloadEffects() { + for (let i in BattleEffects) { + if (i === 'alpha' || i === 'omega') continue; + const url = BattleEffects[i].url; + if (url) this.preloadImage(url); + } + this.preloadImage(Tools.fxPrefix + 'weather-raindance.jpg'); // rain is used often enough to precache + this.preloadImage(Tools.resourcePrefix + 'sprites/xyani/substitute.gif'); + this.preloadImage(Tools.resourcePrefix + 'sprites/xyani-back/substitute.gif'); + //this.preloadImage(Tools.fxPrefix + 'bg.jpg'); + } + preloadBgm() { + let bgmNum = this.numericId % 13; + + if (window.forceBgm || window.forceBgm === 0) bgmNum = window.forceBgm; + window.bgmNum = bgmNum; + let ext = window.nodewebkit ? '.ogg' : '.mp3'; + switch (bgmNum) { + case -1: + BattleSound.loadBgm('audio/bw2-homika-dogars' + ext, 1661, 68131); + this.bgm = 'audio/bw2-homika-dogars' + ext; + break; + case 0: + BattleSound.loadBgm('audio/hgss-kanto-trainer' + ext, 13003, 94656); + this.bgm = 'audio/hgss-kanto-trainer' + ext; + break; + case 1: + BattleSound.loadBgm('audio/bw-subway-trainer' + ext, 15503, 110984); + this.bgm = 'audio/bw-subway-trainer' + ext; + break; + case 2: + BattleSound.loadBgm('audio/bw-trainer' + ext, 14629, 110109); + this.bgm = 'audio/bw-trainer' + ext; + break; + case 3: + BattleSound.loadBgm('audio/bw-rival' + ext, 19180, 57373); + this.bgm = 'audio/bw-rival' + ext; + break; + case 4: + BattleSound.loadBgm('audio/dpp-trainer' + ext, 13440, 96959); + this.bgm = 'audio/dpp-trainer' + ext; + break; + case 5: + BattleSound.loadBgm('audio/hgss-johto-trainer' + ext, 23731, 125086); + this.bgm = 'audio/hgss-johto-trainer' + ext; + break; + case 6: + BattleSound.loadBgm('audio/dpp-rival' + ext, 13888, 66352); + this.bgm = 'audio/dpp-rival' + ext; + break; + case 7: + BattleSound.loadBgm('audio/bw2-kanto-gym-leader' + ext, 14626, 58986); + this.bgm = 'audio/bw2-kanto-gym-leader' + ext; + break; + case 8: + BattleSound.loadBgm('audio/bw2-rival' + ext, 7152, 68708); + this.bgm = 'audio/bw2-rival' + ext; + break; + case 9: + BattleSound.loadBgm('audio/xy-trainer' + ext, 7802, 82469); + this.bgm = 'audio/xy-trainer' + ext; + break; + case 10: + BattleSound.loadBgm('audio/xy-rival' + ext, 7802, 58634); + this.bgm = 'audio/xy-rival' + ext; + break; + case 11: + BattleSound.loadBgm('audio/oras-trainer' + ext, 13579, 91548); + this.bgm = 'audio/oras-trainer' + ext; + break; + case 12: + BattleSound.loadBgm('audio/sm-trainer' + ext, 8323, 89230); + this.bgm = 'audio/sm-trainer' + ext; + break; + case 13: + BattleSound.loadBgm('audio/sm-rival' + ext, 11389, 62158); + this.bgm = 'audio/sm-rival' + ext; + break; + default: + BattleSound.loadBgm('audio/oras-rival' + ext, 14303, 69149); + this.bgm = 'audio/oras-rival' + ext; + break; + } + } + soundStart() { + if (!this.bgm) this.preloadBgm(); + BattleSound.playBgm(this.bgm!); + } + soundStop() { + BattleSound.stopBgm(); + } + soundPause() { + BattleSound.pauseBgm(); + } + destroy() { + if (this.$logFrame) this.$logFrame.empty(); + if (this.$frame) this.$frame.empty(); + this.soundStop(); + this.battle = null!; + } +} + +interface ScenePos { + x?: number, + y?: number, + z?: number, + scale?: number, + xscale?: number, + yscale?: number, + opacity?: number, + time?: number, + display?: string, +} +interface InitScenePos { + x: number, + y: number, + z: number, + scale?: number, + xscale?: number, + yscale?: number, + opacity?: number, + time?: number, + display?: string, +} + +class Sprite { + scene: BattleScene; + $el: JQuery = null!; + sp: SpriteData; + x: number; + y: number; + z: number; + constructor(spriteData: SpriteData | null, pos: InitScenePos, scene: BattleScene) { + this.scene = scene; + let sp = null; + if (spriteData) { + sp = spriteData; + let rawHTML = sp.rawHTML || ''; + this.$el = $(rawHTML); + } else { + sp = { + w: 0, + h: 0, + url: '', + }; + } + this.sp = sp; + + this.x = pos.x; + this.y = pos.y; + this.z = pos.z; + if (pos.opacity !== 0 && spriteData) this.$el!.css(scene.pos(pos, sp)); + + if (!spriteData) { + this.delay = function () { return this; }; + this.anim = function () { return this; }; + } + } + + destroy() { + if (this.$el) this.$el.remove(); + this.$el = null!; + this.scene = null!; + } + delay(time: number) { + this.$el!.delay(time); + return this; + } + anim(end: ScenePos, transition?: string) { + end = { + x: this.x, + y: this.y, + z: this.z, + scale: 1, + opacity: 1, + time: 500, + ...end, + }; + if (end.time === 0) { + this.$el!.css(this.scene.pos(end, this.sp)); + return this; + } + this.$el!.animate(this.scene.posT(end, this.sp, transition, this), end.time!); + return this; + } +} + +class PokemonSprite extends Sprite { + siden: number; + forme = ''; + cryurl: string | undefined = undefined; + + subsp: SpriteData | null = null; + $sub: JQuery | null = null; + isSubActive = false; + + $statbar: JQuery | null = null; + isBackSprite: boolean; + isMissedPokemon = false; + /** + * If the pokemon is transformed, sprite.sp will be the transformed + * SpriteData and sprite.oldsp will hold the original form's SpriteData + */ + oldsp: SpriteData | null = null; + + statbarLeft = 0; + statbarTop = 0; + left = 0; + top = 0; + + effects = {} as {[id: string]: Sprite[]}; + + constructor(spriteData: SpriteData | null, pos: InitScenePos, scene: BattleScene, siden: number) { + super(spriteData, pos, scene); + this.siden = siden; + + this.isBackSprite = !this.siden; + } + destroy() { + if (this.$el) this.$el.remove(); + this.$el = null!; + if (this.$statbar) this.$statbar.remove(); + this.$statbar = null; + if (this.$sub) this.$sub.remove(); + this.$sub = null; + this.scene = null!; + } + + delay(time: number) { + this.$el.delay(time); + if (this.$sub) this.$sub.delay(time); + return this; + } + anim(end: ScenePos, transition?: string) { + end = { + x: this.x, + y: this.y, + z: this.z, + scale: 1, + opacity: 1, + time: 500, + ...end, + }; + const $el = (this.isSubActive ? this.$sub : this.$el)!; + $el.animate(this.scene.posT(end, this.sp, transition, this), end.time!); + return this; + } + + behindx(offset: number) { + return this.x + (this.isBackSprite ? -1 : 1) * offset; + } + behindy(offset: number) { + return this.y + (this.isBackSprite ? 1 : -1) * offset; + } + leftof(offset: number) { + return this.x + (this.isBackSprite ? -1 : 1) * offset; + } + behind(offset: number) { + return this.z + (this.isBackSprite ? -1 : 1) * offset; + } + + removeTransform() { + if (!this.scene.animating) return; + if (!this.oldsp) return; + let sp = this.oldsp; + this.cryurl = sp.cryurl; + this.sp = sp; + this.oldsp = null; + + const $el = this.isSubActive ? this.$sub! : this.$el; + $el.attr('src', sp.url!); + $el.css(this.scene.pos({ + x: this.x, + y: this.y, + z: (this.isSubActive ? this.behind(30) : this.z), + opacity: (this.$sub ? .3 : 1), + }, sp)); + } + animSub(instant?: boolean) { + if (!this.scene.animating) return; + if (this.$sub) return; + const subsp = Tools.getSpriteData('substitute', this.siden, { + gen: this.scene.gen + }); + this.subsp = subsp; + this.$sub = $(''); + this.scene.$spritesFront[this.siden].append(this.$sub); + this.isSubActive = true; + if (instant) { + this.animReset(); + return; + } + this.$el.animate(this.scene.pos({ + x: this.x, + y: this.y, + z: this.behind(30), + opacity: 0.3, + }, this.sp), 500); + this.$sub.css(this.scene.pos({ + x: this.x, + y: this.y + 50, + z: this.z, + opacity: 0 + }, subsp)); + this.$sub.animate(this.scene.pos({ + x: this.x, + y: this.y, + z: this.z + }, subsp), 500); + this.scene.waitFor(this.$sub); + } + animSubFade(instant?: boolean) { + if (!this.$sub || !this.scene.animating) return; + if (instant) { + this.$sub.remove(); + this.$sub = null; + this.isSubActive = false; + this.animReset(); + return; + } + if (this.scene.timeOffset) { + this.$el.delay(this.scene.timeOffset); + this.$sub.delay(this.scene.timeOffset); + } + this.$sub.animate(this.scene.pos({ + x: this.x, + y: this.y - 50, + z: this.z, + opacity: 0 + }, this.subsp!), 500); + + this.$sub = null; + this.isSubActive = false; + this.anim({time: 500}); + if (this.scene.animating) this.scene.waitFor(this.$el); + } + beforeMove() { + if (!this.scene.animating) return false; + if (!this.isSubActive) return false; + this.isSubActive = false; + this.anim({time: 300}); + this.$sub!.animate(this.scene.pos({ + x: this.leftof(-50), + y: this.y, + z: this.z, + opacity: 0.5 + }, this.subsp!), 300); + for (const side of this.scene.battle.sides) { + for (const active of side.active) { + if (active && active.sprite !== this) { + active.sprite.delay(300); + } + } + } + this.scene.wait(300); + this.scene.waitFor(this.$el); + + return true; + } + afterMove() { + if (!this.scene.animating) return false; + if (!this.$sub || this.isSubActive) return false; + this.isSubActive = true; + this.$sub.delay(300); + this.$el.add(this.$sub).promise().done(() => { + if (!this.$sub || !this.$el) return; + this.$el.animate(this.scene.pos({ + x: this.x, + y: this.y, + z: this.behind(30), + opacity: 0.3, + }, this.sp), 300); + this.anim({time: 300}); + }); + return false; + } + removeSub() { + if (!this.$sub) return; + if (!this.scene.animating) { + this.$sub.remove(); + } else { + const $sub = this.$sub; + $sub.animate({ + opacity: 0 + }, () => { + $sub.remove(); + }); + } + this.$sub = null; + } + reset(pokemon: Pokemon) { + this.clearEffects(); + + if (pokemon.volatiles.formechange) { + this.oldsp = this.sp; + this.sp = Tools.getSpriteData(pokemon, this.isBackSprite ? 0 : 1, { + gen: this.scene.gen, + }); + } + + // I can rant for ages about how jQuery sucks, necessitating this function + // The short version is: after calling elem.finish() on an animating + // element, there appear to be a grand total of zero ways to hide it + // afterwards. I've tried `elem.css('display', 'none')`, `elem.hide()`, + // `elem.hide(1)`, `elem.hide(1000)`, `elem.css('opacity', 0)`, + // `elem.animate({opacity: 0}, 1000)`. + // They literally all do nothing, and the element retains + // a style attribute containing `display: inline-block` and `opacity: 1` + // Only forcibly removing the element from the DOM actually makes it + // disappear, so that's what we do. + if (this.$el) { + this.$el.stop(true, false); + const $newEl = $(''); + this.$el.replaceWith($newEl); + this.$el = $newEl; + } + + if (!pokemon.isActive()) { + if (this.$statbar) { + this.$statbar.remove(); + this.$statbar = null; + } + return; + } + + this.recalculatePos(pokemon.slot); + this.resetStatbar(pokemon); + this.$el.css(this.scene.pos({ + display: 'block', + x: this.x, + y: this.y, + z: this.z, + }, this.sp)); + + for (const id in pokemon.volatiles) this.addEffect(id as ID, true); + for (const id in pokemon.turnstatuses) this.addEffect(id as ID, true); + for (const id in pokemon.movestatuses) this.addEffect(id as ID, true); + } + animReset() { + if (!this.scene.animating) return; + if (this.$sub) { + this.isSubActive = true; + this.$el.stop(true, false); + this.$sub.stop(true, false); + this.$el.css(this.scene.pos({ + x: this.x, + y: this.y, + z: this.behind(30), + opacity: .3 + }, this.sp)); + this.$sub.css(this.scene.pos({ + x: this.x, + y: this.y, + z: this.z + }, this.subsp!)); + } else { + this.$el.stop(true, false); + this.$el.css(this.scene.pos({ + x: this.x, + y: this.y, + z: this.z + }, this.sp)); + } + } + recalculatePos(slot: number) { + let moreActive = this.scene.activeCount - 1; + let statbarOffset = 0; + if (this.scene.gen <= 4 && moreActive) { + this.x = (slot - 0.52) * (this.isBackSprite ? -1 : 1) * -55; + this.y = (this.isBackSprite ? -1 : 1) + 1; + if (!this.isBackSprite) statbarOffset = 30 * slot; + if (this.isBackSprite) statbarOffset = -28 * slot; + } else { + switch (moreActive) { + case 0: + this.x = 0; + break; + case 1: + if (this.sp.pixelated) { + this.x = (slot * -100 + 18) * (this.isBackSprite ? -1 : 1); + } else { + this.x = (slot * -75 + 18) * (this.isBackSprite ? -1 : 1); + } + break; + case 2: + this.x = (slot * -70 + 20) * (this.isBackSprite ? -1 : 1); + break; + } + this.y = (slot * 10) * (this.isBackSprite ? -1 : 1); + if (!this.isBackSprite) statbarOffset = 17 * slot; + if (!this.isBackSprite && !moreActive && this.sp.pixelated) statbarOffset = 15; + if (this.isBackSprite) statbarOffset = -7 * slot; + if (!this.isBackSprite && moreActive == 2) statbarOffset = 14 * slot - 10; + } + if (this.scene.gen <= 2) { + statbarOffset += this.isBackSprite ? 1 : 20; + } else if (this.scene.gen <= 3) { + statbarOffset += this.isBackSprite ? 5 : 30; + } else { + statbarOffset += this.isBackSprite ? 20 : 30; + } + + let pos = this.scene.pos({ + x: this.x, + y: this.y, + z: this.z + }, { + w: 0, + h: 96 + }); + pos.top += 40; + + this.left = pos.left; + this.top = pos.top; + this.statbarLeft = pos.left - 80; + this.statbarTop = pos.top - 73 - statbarOffset; + + if (moreActive) { + // make sure element is in the right z-order + if (!slot && this.isBackSprite || slot && !this.isBackSprite) { + this.$el.prependTo(this.$el.parent()); + } else { + this.$el.appendTo(this.$el.parent()); + } + } + } + animSummon(pokemon: Pokemon, slot: number, instant?: boolean) { + if (!this.scene.animating) return; + this.recalculatePos(slot); + + // 'z-index': (this.isBackSprite ? 1+slot : 4-slot), + if (instant) { + this.$el.css('display', 'block'); + this.animReset(); + this.resetStatbar(pokemon); + if (pokemon.hasVolatile('substitute' as ID)) this.animSub(true); + return; + } + if (this.cryurl) { + BattleSound.playEffect(this.cryurl); + } + this.$el.css(this.scene.pos({ + display: 'block', + x: this.x, + y: this.y - 10, + z: this.z, + scale: 0, + opacity: 0, + }, this.sp)); + this.scene.showEffect('pokeball', { + opacity: 0, + x: this.x, + y: this.y + 30, + z: this.behind(50), + scale: .7 + }, { + opacity: 1, + x: this.x, + y: this.y - 10, + z: this.z, + time: 300 / this.scene.acceleration + }, 'ballistic2', 'fade'); + if (this.scene.gen <= 4) { + this.delay(this.scene.timeOffset + 300 / this.scene.acceleration).anim({ + x: this.x, + y: this.y, + z: this.z, + time: 400 / this.scene.acceleration, + }); + } else { + this.delay(this.scene.timeOffset + 300 / this.scene.acceleration).anim({ + x: this.x, + y: this.y + 30, + z: this.z, + time: 400 / this.scene.acceleration, + }).anim({ + x: this.x, + y: this.y, + z: this.z, + time: 300 / this.scene.acceleration, + }, 'accel'); + } + if (this.sp.shiny && this.scene.acceleration < 2) BattleOtherAnims.shiny.anim(this.scene, [this]); + this.scene.waitFor(this.$el); + + if (pokemon.hasVolatile('substitute' as ID)) this.animSub(); + + this.resetStatbar(pokemon, true); + this.scene.updateSidebar(pokemon.side); + this.$statbar!.css({ + display: 'block', + left: this.statbarLeft, + top: this.statbarTop + 20, + opacity: 0, + }); + this.$statbar!.delay(300 / this.scene.acceleration).animate({ + top: this.statbarTop, + opacity: 1, + }, 400 / this.scene.acceleration); + + this.dogarsCheck(pokemon); + } + animDragIn(pokemon: Pokemon, slot: number) { + if (!this.scene.animating) return; + this.recalculatePos(slot); + + // 'z-index': (this.isBackSprite ? 1+slot : 4-slot), + this.$el.css(this.scene.pos({ + display: 'block', + x: this.leftof(-50), + y: this.y, + z: this.z, + opacity: 0 + }, this.sp)); + this.delay(300).anim({ + x: this.x, + y: this.y, + z: this.z, + time: 400, + }, 'decel'); + if (!!this.scene.animating && this.sp.shiny) BattleOtherAnims.shiny.anim(this.scene, [this]); + this.scene.waitFor(this.$el); + this.scene.timeOffset = 700; + + this.resetStatbar(pokemon, true); + this.scene.updateSidebar(pokemon.side); + this.$statbar!.css({ + display: 'block', + left: this.statbarLeft + (this.siden ? -50 : 50), + top: this.statbarTop, + opacity: 0, + }); + this.$statbar!.delay(300).animate({ + left: this.statbarLeft, + opacity: 1, + }, 400); + + this.dogarsCheck(pokemon); + } + animDragOut(pokemon: Pokemon) { + if (!this.scene.animating) return this.animUnsummon(pokemon, true); + this.removeSub(); + this.anim({ + x: this.leftof(50), + y: this.y, + z: this.z, + opacity: 0, + time: 400, + }, 'accel'); + + this.updateStatbar(pokemon, true); + let $statbar = this.$statbar; + if ($statbar) { + this.$statbar = null; + $statbar.animate({ + left: this.statbarTop + 30, + opacity: 0 + }, 300 / this.scene.acceleration, () => { + $statbar!.remove(); + }); + } + } + animUnsummon(pokemon: Pokemon, instant?: boolean) { + this.removeSub(); + if (!this.scene.animating || instant) { + this.$el.hide(); + if (this.$statbar) { + this.$statbar.remove(); + this.$statbar = null; + } + return; + } + if (this.scene.gen <= 4) { + this.anim({ + x: this.x, + y: this.y - 25, + z: this.z, + scale: 0, + opacity: 0, + time: 400 / this.scene.acceleration, + }); + } else { + this.anim({ + x: this.x, + y: this.y - 40, + z: this.z, + scale: 0, + opacity: 0, + time: 400 / this.scene.acceleration, + }); + } + this.scene.showEffect('pokeball', { + opacity: 1, + x: this.x, + y: this.y - 40, + z: this.z, + scale: .7, + time: 300 / this.scene.acceleration, + }, { + opacity: 0, + x: this.x, + y: this.y, + z: this.behind(50), + time: 700 / this.scene.acceleration, + }, 'ballistic2'); + if (this.scene.acceleration < 3) this.scene.wait(600 / this.scene.acceleration); + + this.updateStatbar(pokemon, true); + let $statbar = this.$statbar; + if ($statbar) { + this.$statbar = null; + $statbar.animate({ + left: this.statbarLeft + (this.siden ? 50 : -50), + opacity: 0 + }, 300 / this.scene.acceleration, () => { + $statbar!.remove(); + }); + } + } + animFaint(pokemon: Pokemon) { + this.removeSub(); + if (!this.scene.animating) { + this.$el.remove(); + this.$el = null!; + if (this.$statbar) { + this.$statbar.remove(); + this.$statbar = null; + } + return; + } + this.updateStatbar(pokemon, false, true); + this.scene.updateSidebar(pokemon.side); + if (this.cryurl) { + BattleSound.playEffect(this.cryurl); + } + this.anim({ + y: this.y - 80, + opacity: 0, + }, 'accel'); + this.scene.waitFor(this.$el); + this.$el.promise().done(() => { + this.$el.remove(); + this.$el = null!; + }); + + let $statbar = this.$statbar; + if ($statbar) { + this.$statbar = null; + $statbar.animate({ + opacity: 0, + }, 300, () => { + $statbar!.remove(); + }); + } + } + animTransform(pokemon: Pokemon, isCustomAnim?: boolean, isPermanent?: boolean) { + if (!this.scene.animating && !isPermanent) return; + let sp = Tools.getSpriteData(pokemon, this.isBackSprite ? 0 : 1, { + gen: this.scene.gen, + }); + let oldsp = this.sp; + if (isPermanent) { + this.oldsp = null; + } else if (!this.oldsp) { + this.oldsp = oldsp; + } + this.sp = sp; + this.cryurl = sp.cryurl; + + if (!this.scene.animating) return; + let speciesid = toId(pokemon.getSpecies()); + let doCry = false; + const scene = this.scene; + if (isCustomAnim) { + if (speciesid === 'kyogreprimal') { + BattleOtherAnims.primalalpha.anim(scene, [this]); + doCry = true; + } else if (speciesid === 'groudonprimal') { + BattleOtherAnims.primalomega.anim(scene, [this]); + doCry = true; + } else if (speciesid === 'necrozmaultra') { + BattleOtherAnims.ultraburst.anim(scene, [this]); + doCry = true; + } else if (speciesid === 'zygardecomplete') { + BattleOtherAnims.powerconstruct.anim(scene, [this]); + } else if (speciesid === 'wishiwashischool' || speciesid === 'greninjaash') { + BattleOtherAnims.schoolingin.anim(scene, [this]); + } else if (speciesid === 'wishiwashi') { + BattleOtherAnims.schoolingout.anim(scene, [this]); + } else if (speciesid === 'mimikyubusted' || speciesid === 'mimikyubustedtotem') { + // standard animation + } else { + BattleOtherAnims.megaevo.anim(scene, [this]); + doCry = true; + } + } + // Constructing here gives us 300ms extra time to preload the new sprite + let $newEl = $(''); + $newEl.css(this.scene.pos({ + x: this.x, + y: this.y, + z: this.z, + yscale: 0, + xscale: 0, + opacity: 0, + }, sp)); + this.$el.animate(this.scene.pos({ + x: this.x, + y: this.y, + z: this.z, + yscale: 0, + xscale: 0, + opacity: 0.3, + }, oldsp), 300, () => { + if (this.cryurl && doCry) { + BattleSound.playEffect(this.cryurl); + } + this.$el.replaceWith($newEl); + this.$el = $newEl; + this.$el.animate(scene.pos({ + x: this.x, + y: this.y, + z: this.z, + opacity: 1 + }, sp), 300); + }); + this.scene.wait(500); + + if (isPermanent) { + this.scene.updateSidebar(pokemon.side); + this.resetStatbar(pokemon); + } else { + this.updateStatbar(pokemon); + } + } + + pokeEffect(id: ID) { + if (id === 'protect' || id === 'magiccoat') { + this.effects[id][0].anim({ + scale: 1.2, + opacity: 1, + time: 100, + }).anim({ + opacity: .4, + time: 300, + }); + } + } + addEffect(id: ID, instant?: boolean) { + if (id in this.effects) { + this.pokeEffect(id); + return; + } + if (id === 'substitute') { + this.animSub(instant); + } else if (id === 'leechseed') { + const pos1 = { + display: 'block', + x: this.x - 30, + y: this.y - 40, + z: this.z, + scale: .2, + opacity: .6, + }; + const pos2 = { + display: 'block', + x: this.x + 40, + y: this.y - 35, + z: this.z, + scale: .2, + opacity: .6, + }; + const pos3 = { + display: 'block', + x: this.x + 20, + y: this.y - 25, + z: this.z, + scale: .2, + opacity: .6, + }; + + const leechseed1 = new Sprite(BattleEffects.energyball, pos1, this.scene); + const leechseed2 = new Sprite(BattleEffects.energyball, pos2, this.scene); + const leechseed3 = new Sprite(BattleEffects.energyball, pos3, this.scene); + this.scene.$spritesFront[this.siden].append(leechseed1.$el!); + this.scene.$spritesFront[this.siden].append(leechseed2.$el!); + this.scene.$spritesFront[this.siden].append(leechseed3.$el!); + this.effects['leechseed'] = [leechseed1, leechseed2, leechseed3]; + } else if (id === 'protect' || id === 'magiccoat') { + const protect = new Sprite(BattleEffects.protect, { + display: 'block', + x: this.x, + y: this.y, + z: this.behind(-15), + xscale: 1, + yscale: 0, + opacity: .1, + }, this.scene); + this.scene.$spritesFront[this.siden].append(protect.$el!); + this.effects[id] = [protect]; + protect.anim({ + opacity: .9, + time: instant ? 0 : 400, + }).anim({ + opacity: .4, + time: instant ? 0 : 300, + }); + } + } + + removeEffect(id: ID, instant?: boolean) { + if (id === 'formechange') this.removeTransform(); + if (id === 'substitute') this.animSubFade(); + if (this.effects[id]) { + for (const sprite of this.effects[id]) sprite.destroy(); + delete this.effects[id]; + } + } + clearEffects() { + for (const id in this.effects) this.removeEffect(id as ID, true); + this.animSubFade(true); + this.removeTransform(); + } + + dogarsCheck(pokemon: Pokemon) { + if (pokemon.side.n === 1) return; + + if (pokemon.species === 'Koffing' && pokemon.name.match(/dogars/i)) { + if (window.forceBgm !== -1) { + window.originalBgm = window.bgmNum; + window.forceBgm = -1; + this.scene.preloadBgm(); + this.scene.soundStart(); + } + } else if (window.forceBgm === -1) { + window.forceBgm = null; + if (window.originalBgm || window.originalBgm === 0) { + window.forceBgm = window.originalBgm; + } + this.scene.preloadBgm(); + this.scene.soundStart(); + } + } + + // Statbar + ///////////////////////////////////////////////////////////////////// + + getStatbarHTML(pokemon: Pokemon) { + let buf = '