Add new BattleScene animation engine

battle.js is probably PS's oldest code. It's received minor touch-ups:
a refactor to prototypes early on, and then a refactor to TypeScript
and classes recently, but otherwise it's had basically zero maintenance
until now.

That's probably why this refactor took me over a week.

The biggest change is that the animation engine strewn around
Pokemon, Side, and Battle has been broken out into a new class named
BattleScene.

Pokemon, Side, and Battle now only track state; all animation is now
done in BattleScene and PokemonSprite.

The fates of major classes:

battle.ts:Pokemon - animation has been moved, mostly to PokemonSprite
battle.ts:Side - animation has been moved, to PokemonSprite/BattleScene
battle.ts:Battle - animation has been moved, mostly to BattleScene

Major changes:

- Many many variables have been renamed to be much clearer about what
  they mean. For instance, `animationDelay` is now `timeOffset`, and
  `activityDelay` is now `minWait`. A few bugs relating to me mixing
  up these two variables have also been fixed. jQuery variables named
  like `fooElem` have been renamed like `$foo`.

- The unnecessarily complicated queue1/queue2/activeQueue system,
  previously used for telling the animation engine to stop after Pause
  was pressed, has been replaced with a simple `interruptionCount`
  counter.

- The entire scene can now be reconstructed from scratch, which means
  that the `fastForward` system no longer needs to touch the DOM
  outside of the battle log. "Prev turn" and "Skip to turn" should be
  faster in 1000-turn battles now.

- The animation engine now supports displaying weather and terrain
  simultaneously.

- During a replay, Team Preview is shown for a second (instead of
  immediately skipping to the battle).

- Various aspects of the animation engine should be much less brittle
  now.

- Many bugs were fixed (and new ones were introduced).
This commit is contained in:
Guangcong Luo
2018-05-29 16:56:54 -05:00
parent e957f953c2
commit c590a0c370
9 changed files with 3200 additions and 3169 deletions

View File

@@ -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 += '<small>(Transformed into ' + pokemon.volatiles.formechange[2] + ')</small><br />';
text += '<small>(Transformed into ' + pokemon.volatiles.formechange[1] + ')</small><br />';
} else {
text += '<small>(Forme: ' + pokemon.volatiles.formechange[2] + ')</small><br />';
text += '<small>(Forme: ' + pokemon.volatiles.formechange[1] + ')</small><br />';
}
}
@@ -525,6 +525,9 @@ var BattleTooltips = (function () {
for (var i = 0; i < pokemon.moveTrack.length; i++) {
text += '&#8226; ' + this.getPPUseText(pokemon.moveTrack[i]) + '<br />';
}
if (pokemon.moveTrack.length > 4) {
text += '(More than 4 moves is usually a sign of Illusion Zoroark/Zorua.)';
}
text += '</p>';
}
text += '</div></div>';

View File

@@ -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('<div style="padding-top: 3px; padding-right: 3px; text-align: right"><button class="icon button" name="openBattleOptions" title="Options">Battle Options</button></div>');
this.$options = this.battle.scene.$options.html('<div style="padding-top: 3px; padding-right: 3px; text-align: right"><button class="icon button" name="openBattleOptions" title="Options">Battle Options</button></div>');
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('<p><em>Waiting for players...</em></p>');
this.$join = $('<div class="playbutton"><button name="joinBattle">Join Battle</button></div>');
this.$battle.append(this.$join);
} else {
@@ -795,7 +788,7 @@
updateWaitControls: function () {
var buf = '<div class="controls">';
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 += '<p><button class="button" name="setTimer" value="off">Stop timer</button> <small>&larr; Your opponent has disconnected. This will give them more time to reconnect.</small></p>';
} else {

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -222,12 +222,6 @@ interface Move extends Effect {
readonly type: string;
readonly category: string;
readonly isZ?: string;
// TODO: move to different interface
readonly anim: Function;
readonly residualAnim: Function;
readonly prepareAnim: Function;
readonly prepareMessage: Function;
}
interface Ability extends Effect {
readonly effectType: 'Ability';

View File

@@ -45,7 +45,7 @@ if (!String.prototype.endsWith) {
};
}
if (!Object.assign) {
Object.assign = function (thing, rest) {
Object.assign = function (thing: any, rest: any) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var k in source) {
@@ -172,6 +172,7 @@ interface SpriteData {
h: number;
y?: number;
url?: string;
rawHTML?: string;
pixelated?: boolean;
isBackSprite?: boolean;
cryurl?: string;
@@ -250,7 +251,7 @@ const Tools = {
// custom avatar served by the server
let protocol = (Config.server.port === 443) ? 'https' : 'http';
return protocol + '://' + Config.server.host + ':' + Config.server.port +
'/avatars/' + _.map(avatar.split('?', 2), encodeURIComponent).join('?');
'/avatars/' + encodeURIComponent(avatar).replace('%3F', '?');
}
// just pick a random avatar
let sprites = [1, 2, 101, 102, 169, 170];
@@ -744,11 +745,6 @@ const Tools = {
move.gen = 0;
}
}
if (window.BattleMoveAnims) {
if (!move.anim) move.anim = BattleOtherAnims.attack.anim;
Object.assign(move, BattleMoveAnims[move.id]);
}
}
return move;
},
@@ -978,7 +974,11 @@ const Tools = {
getSpriteData(pokemon: Pokemon | Template | string, siden: number, options: {gen?: number, shiny?: boolean, gender?: GenderName, afd?: boolean, noScale?: boolean} = {gen: 6}) {
if (!options.gen) options.gen = 6;
if (pokemon instanceof Pokemon) {
options.shiny = pokemon.shiny;
if (pokemon.volatiles.transform) {
options.shiny = pokemon.volatiles.transform[2];
} else {
options.shiny = pokemon.shiny;
}
options.gender = pokemon.gender;
pokemon = pokemon.getSpecies();
}
@@ -1603,7 +1603,7 @@ const Tools = {
buf += '<h1 style="font-weight:normal;text-align:center"><strong>' + Tools.escapeHTML(battle.tier) + '</strong><br /><a href="http://pokemonshowdown.com/users/' + toId(battle.p1.name) + '" class="subtle" target="_blank">' + Tools.escapeHTML(battle.p1.name) + '</a> vs. <a href="http://pokemonshowdown.com/users/' + toId(battle.p2.name) + '" class="subtle" target="_blank">' + Tools.escapeHTML(battle.p2.name) + '</a></h1>\n';
buf += '<script type="text/plain" class="battle-log-data">' + battle.activityQueue.join('\n').replace(/\//g, '\\/') + '</script>\n';
buf += '</div>\n';
buf += '<div class="battle-log battle-log-inline"><div class="inner">' + battle.logElem.html() + '</div></div>\n';
buf += '<div class="battle-log battle-log-inline"><div class="inner">' + battle.scene.$log.html() + '</div></div>\n';
buf += '</div>\n';
buf += '<script>\n';
buf += 'let daily = Math.floor(Date.now()/1000/60/60/24);document.write(\'<script src="https://play.pokemonshowdown.com/js/replay-embed.js?version\'+daily+\'"></\'+\'script>\');\n';

File diff suppressed because it is too large Load Diff

View File

@@ -91,17 +91,20 @@ License: GPLv2
margin-top: 0.5em;
}
.playbutton {
.playbutton, .playbutton2 {
position: absolute;
display: block;
top: 160px;
left: 0;
right: 0;
text-align: center;
top: 190px;
left: 100px;
z-index: 100;
}
.playbutton button {
font-size: 20pt;
.playbutton2 {
top: 130px;
left: 300px;
}
.playbutton button,
.playbutton2 button {
font-size: 16pt;
}
.background {
@@ -339,7 +342,7 @@ License: GPLv2
}
.turn {
position: absolute;
display: none;
display: block;
top: 10px;
left: 110px;
font-size: 13pt;

View File

@@ -87,6 +87,7 @@
<script src="https://play.pokemonshowdown.com/config/config.js"></script>
<script src="js/battledata.js"></script>
<script src="data/pokedex-mini.js" onerror="loadRemoteData(this.src)"></script>
<script src="data/pokedex-mini-bw.js" onerror="loadRemoteData(this.src)"></script>
<script src="data/typechart.js" onerror="loadRemoteData(this.src)"></script>
<script src="js/battle.js"></script>
<script src="js/lib/sockjs-0.3.4.min.js"></script>