Skip to content

Commit

Permalink
Update Battle class API (smogon#1686)
Browse files Browse the repository at this point in the history
The client Battle class API has been pretty old and crusty, so this
updates it to be saner.

The constructor now takes an options object. Any setting you'd want to
initialize with is now a constructor option, instead of needing to call
methods after the constructor.

(Deprecated settings `roomid` and `joinButtons` still need to be set
separately.)

The old callback system is removed. It's replaced with a subscription
system vaguely resembling `PSStreamModel`. Any callbacks only intended
to be used by the warstory generator are removed (anyone who wants to
write their own warstory generator should extend `BattleSceneStub`
instead).

Battles no longer start paused. You can still start them paused by
passing `paused: true` as an option.

Playback state tracking had a bunch of rearrangement:

- `playbackState` no longer exists; state should be directly read from
  `paused`, `atQueueEnd`, `turn`, and `seeking`.

- `turn` is now initialized to `-1`. `-1` now means "we haven't reached
  `|teampreview|` or `|start|` yet". Reaching those sets turn to `0`.

- "Fast forwarding" and "seeking" are now consistently named "seeking".
  - `seeking` tracks seek state; changes from `fastForward`:
    - `null` means not seeking (replaces `0`)
    - `0` means seeking the start (replaces `0.5`)
    - `Infinity` means seeking the end (replaces `-1`)
  - `fastForward` deprecated and replaced with `seeking`
  - `fastForwardTo()` deprecated and replaced with `seekTurn()`

- `resultWaiting` is removed (it's unused)

- The "activity queue" has been renamed the "step queue", which means
  some renamed properties:
  - `activityQueue` to `stepQueue`
  - `activityStep` to `currentStep`
  - `nextActivity()` to `nextStep()`

- new property: `atQueueEnd` to track if animation has caught up to the
  end of the step queue (replaces checking `playbackState`)

- new property/option: `isReplay` - will automatically set `ended` when
  reaching the end of a replay (stopping music and showing a message),
  if the replay was saved before the end of the battle

- both replay players (`replay.pokemonshowdown.com` and downloaded
  files) have been rewritten to use an observer system, instead of the
  previous manual updating

- `reset(true)` has been renamed `resetStep()`
  • Loading branch information
Zarel authored Jan 23, 2021
1 parent 0005f1f commit 3e4b832
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 394 deletions.
71 changes: 35 additions & 36 deletions js/client-battle.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
maxWidth: 1180,
initialize: function (data) {
this.me = {};
this.choice = undefined;
/** are move/switch/team-preview controls currently being shown? */
this.controlsShown = false;

this.battlePaused = false;
this.autoTimerActivated = false;
Expand All @@ -23,12 +26,19 @@
this.$foeHint = this.$el.find('.foehint');

BattleSound.setMute(Dex.prefs('mute'));
this.battle = new Battle(this.$battle, this.$chatFrame, this.id);
this.battle = new Battle({
id: this.id,
$frame: this.$battle,
$logFrame: this.$chatFrame
});
this.battle.roomid = this.id;
this.battle.joinButtons = true;
this.tooltips = this.battle.scene.tooltips;
this.tooltips.listen(this.$controls);

this.battle.roomid = this.id;
this.battle.joinButtons = true;
var self = this;
this.battle.subscribe(function () { self.updateControls(); });

this.users = {};
this.userCount = {users: 0};
this.$userList = this.$('.userlist');
Expand All @@ -41,14 +51,6 @@
this.$chat = this.$chatFrame.find('.inner');

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(); };
this.battle.endCallback = function () { self.updateControls(); };
this.battle.startCallback = function () { self.updateControls(); };
this.battle.stagnateCallback = function () { self.updateControls(); };

this.battle.play();
},
events: {
'click .replayDownloadButton': 'clickReplayDownloadButton',
Expand Down Expand Up @@ -108,9 +110,9 @@
},
focus: function (e) {
this.tooltips.hideTooltip();
if (this.battle.playbackState === 3 && !this.battlePaused) {
if (this.battle.paused && !this.battlePaused) {
if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
this.battle.play();
if (Dex.prefs('noanim')) this.battle.fastForwardTo(-1);
}
ConsoleRoom.prototype.focus.call(this, e);
},
Expand All @@ -125,10 +127,9 @@
log.shift();
app.roomTitleChanged(this);
}
if (this.battle.activityQueue.length) return;
this.battle.activityQueue = log;
this.battle.fastForwardTo(-1);
this.battle.play();
if (this.battle.stepQueue.length) return;
this.battle.stepQueue = log;
this.battle.seekTurn(Infinity, true);
if (this.battle.ended) this.battleEnded = true;
this.updateLayout();
this.updateControls();
Expand Down Expand Up @@ -190,12 +191,12 @@
var args = logLine.substr(10).split('|');
var pokemon = isNaN(Number(args[1])) ? this.battle.getPokemon(args[1]) : this.battle.nearSide.active[args[1]];
var requestData = this.request.active[pokemon ? pokemon.slot : 0];
delete this.choice;
this.choice = undefined;
switch (args[0]) {
case 'trapped':
requestData.trapped = true;
var pokeName = pokemon.side.n === 0 ? BattleLog.escapeHTML(pokemon.name) : "The opposing " + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.speciesForme : BattleLog.escapeHTML(pokemon.name));
this.battle.activityQueue.push('|message|' + pokeName + ' is trapped and cannot switch!');
this.battle.stepQueue.push('|message|' + pokeName + ' is trapped and cannot switch!');
break;
case 'cant':
for (var i = 0; i < requestData.moves.length; i++) {
Expand All @@ -204,20 +205,21 @@
}
}
args.splice(1, 1, pokemon.getIdent());
this.battle.activityQueue.push('|' + args.join('|'));
this.battle.stepQueue.push('|' + args.join('|'));
break;
}
} else if (logLine.substr(0, 7) === '|title|') { // eslint-disable-line no-empty
} else if (logLine.substr(0, 5) === '|win|' || logLine === '|tie') {
this.battleEnded = true;
this.battle.activityQueue.push(logLine);
this.battle.stepQueue.push(logLine);
} else if (logLine.substr(0, 6) === '|chat|' || logLine.substr(0, 3) === '|c|' || logLine.substr(0, 4) === '|c:|' || logLine.substr(0, 9) === '|chatmsg|' || logLine.substr(0, 10) === '|inactive|') {
this.battle.instantAdd(logLine);
} else {
this.battle.activityQueue.push(logLine);
this.battle.stepQueue.push(logLine);
}
}
this.battle.add('', Dex.prefs('noanim'));
this.battle.add();
if (Dex.prefs('noanim')) this.battle.seekTurn(Infinity);
this.updateControls();
},
toggleMessages: function (user) {
Expand Down Expand Up @@ -248,18 +250,18 @@
* Battle stuff
*********************************************************/

updateControls: function (force) {
updateControls: function () {
if (this.battle.scene.customControls) return;
var controlsShown = this.controlsShown;
this.controlsShown = false;

if (this.battle.playbackState === 5) {
if (this.battle.seeking !== null) {

// battle is seeking
this.$controls.html('');
return;

} else if (this.battle.playbackState === 2 || this.battle.playbackState === 3) {
} else if (!this.battle.atQueueEnd) {

// battle is playing or paused
if (!this.side || this.battleEnded) {
Expand Down Expand Up @@ -296,7 +298,7 @@

// player
this.controlsShown = true;
if (force || !controlsShown || this.choice === undefined || this.choice && this.choice.waiting) {
if (!controlsShown || this.choice === undefined || this.choice && this.choice.waiting) {
// don't update controls (and, therefore, side) if `this.choice === null`: causes damage miscalculations
this.updateControlsForPlayer();
} else {
Expand Down Expand Up @@ -325,7 +327,6 @@
// since those early-return.
app.topbar.updateTabbar();
},
controlsShown: false,
updateControlsForPlayer: function () {
this.callbackWaiting = true;

Expand Down Expand Up @@ -393,6 +394,7 @@
if (this.battle.mySide.pokemon && !this.battle.mySide.pokemon.length) {
// too early, we can't determine `this.choice.count` yet
// TODO: send teamPreviewCount in the request object
this.controlsShown = false;
return;
}
if (!this.choice) {
Expand Down Expand Up @@ -977,18 +979,15 @@
request.requestType = 'wait';
}

var choice = null;
if (choiceText) {
choice = {waiting: true};
}
this.choice = choice;
this.choice = choiceText ? {waiting: true} : null;
this.finalDecision = this.finalDecisionMove = this.finalDecisionSwitch = false;
this.request = request;
if (request.side) {
this.updateSideLocation(request.side);
}
this.notifyRequest();
this.updateControls(true);
this.controlsShown = false;
this.updateControls();
},
notifyRequest: function () {
var oName = this.battle.farSide.name;
Expand Down Expand Up @@ -1089,11 +1088,11 @@
},
rewindTurn: function () {
if (this.battle.turn) {
this.battle.fastForwardTo(this.battle.turn - 1);
this.battle.seekTurn(this.battle.turn - 1);
}
},
goToEnd: function () {
this.battle.fastForwardTo(-1);
this.battle.seekTurn(Infinity);
},
register: function (userid) {
var registered = app.user.get('registered');
Expand Down
11 changes: 1 addition & 10 deletions js/client-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,16 +1041,7 @@
for (var roomid in app.rooms) {
var battle = app.rooms[roomid] && app.rooms[roomid].battle;
if (!battle) continue;
var turn = battle.turn;
var oldState = battle.playbackState;
if (oldState === 4) turn = -1;
battle.reset(true);
battle.fastForwardTo(turn);
if (oldState !== 3) {
battle.play();
} else {
battle.pause();
}
battle.resetToCurrentTurn();
}
return false;

Expand Down
92 changes: 42 additions & 50 deletions js/replay-embed.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ requireScript('https://play.pokemonshowdown.com/js/battle-tooltips.js?a7');
requireScript('https://play.pokemonshowdown.com/js/battle.js?a7');

var Replays = {
init: function (log) {
battle: null,
muted: false,
init: function () {
this.$el = $('.wrapper');
if (!this.$el.length) {
$('body').append('<div class="wrapper replay-wrapper" style="max-width:1180px;margin:0 auto"><div class="battle"></div><div class="battle-log"></div><div class="replay-controls"></div><div class="replay-controls-2"></div>');
this.$el = $('.wrapper');
}

var id = $('input[name=replayid]').val() || '';
var log = ($('script.battle-log-data').text() || '').replace(/\\\//g, '/');

var self = this;
this.$el.on('click', '.chooser button', function (e) {
Expand All @@ -53,18 +57,14 @@ var Replays = {
if (action) self[action]();
});

this.battle = new Battle(this.$('.battle'), this.$('.battle-log'), id);
//this.battle.preloadCallback = updateProgress;
this.battle.errorCallback = this.errorCallback.bind(this);
this.battle.resumeButton = this.resume.bind(this);

this.setlog(log);
},
setlog: function (log) {
this.battle.setQueue(log.split('\n'));

this.battle.reset();
this.$('.battle').append('<div class="playbutton"><button data-action="start"><i class="fa fa-play"></i> Play</button><br /><br /><button data-action="startMuted" class="startsoundchooser" style="font-size:10pt;display:none">Play (music off)</button></div>');
this.battle = new Battle({
id: id,
$frame: this.$('.battle'),
$logFrame: this.$('.battle-log'),
log: log.split('\n'),
isReplay: true,
paused: true,
});

this.$('.replay-controls-2').html('<div class="chooser leftchooser speedchooser"> <em>Speed:</em> <div><button class="sel" value="fast">Fast</button><button value="normal">Normal</button><button value="slow">Slow</button><button value="reallyslow">Really Slow</button></div> </div> <div class="chooser colorchooser"> <em>Color&nbsp;scheme:</em> <div><button class="sel" value="light">Light</button><button value="dark">Dark</button></div> </div> <div class="chooser soundchooser" style="display:none"> <em>Music:</em> <div><button class="sel" value="on">On</button><button value="off">Off</button></div> </div>');

Expand All @@ -74,7 +74,8 @@ var Replays = {
if (rc2) rc2.innerHTML = rc2.innerHTML;

if (window.HTMLAudioElement) $('.soundchooser, .startsoundchooser').show();
this.reset();
this.update();
this.battle.subscribe(function (state) { self.update(state); });
},
"$": function (sel) {
return this.$el.find(sel);
Expand Down Expand Up @@ -115,11 +116,9 @@ var Replays = {
break;

case 'sound':
var muteTable = {
on: false, // this is kind of backwards: sound[on] === muted[false]
off: true
};
this.battle.setMute(muteTable[value]);
// remember this is reversed: sound[off] === muted[true]
this.muted = (value === 'off');
this.battle.setMute(this.muted);
this.$('.startsoundchooser').remove();
break;

Expand All @@ -134,59 +133,52 @@ var Replays = {
break;
}
},
battle: null,
errorCallback: function () {
var replayid = this.$('input[name=replayid]').val();
var m = /^([a-z0-9]+)-[a-z0-9]+-[0-9]+$/.exec(replayid);
if (m) {
this.battle.log('<hr /><div class="chat">This replay was uploaded from a third-party server (<code>' + BattleLog.escapeHTML(m[1]) + '</code>). It contains errors and cannot be viewed.</div><div class="chat">Replays uploaded from third-party servers can contain errors if the server is running custom code, or the server operator has otherwise incorrectly configured their server.</div>', true);
this.battle.pause();
update: function (state) {
if (state === 'error') {
var m = /^([a-z0-9]+)-[a-z0-9]+-[0-9]+$/.exec(this.battle.id);
if (m) {
this.battle.log('<hr /><div class="chat">This replay was uploaded from a third-party server (<code>' + BattleLog.escapeHTML(m[1]) + '</code>). It contains errors.</div><div class="chat">Replays uploaded from third-party servers can contain errors if the server is running custom code, or the server operator has otherwise incorrectly configured their server.</div>', true);
}
return;
}

if (BattleSound.muted && !this.muted) this.changeSetting('sound', 'off');

if (this.battle.paused) {
var resetDisabled = !this.battle.started ? ' disabled' : '';
this.$('.replay-controls').html('<button data-action="play"><i class="fa fa-play"></i> Play</button><button data-action="reset"' + resetDisabled + '><i class="fa fa-undo"></i> Reset</button> <button data-action="rewind"><i class="fa fa-step-backward"></i> Last turn</button><button data-action="ff"><i class="fa fa-step-forward"></i> Next turn</button> <button data-action="ffto"><i class="fa fa-fast-forward"></i> Go to turn...</button> <button data-action="switchSides"><i class="fa fa-random"></i> Switch sides</button>');
} else {
this.$('.replay-controls').html('<button data-action="pause"><i class="fa fa-pause"></i> Pause</button><button data-action="reset"><i class="fa fa-undo"></i> Reset</button> <button data-action="rewind"><i class="fa fa-step-backward"></i> Last turn</button><button data-action="ff"><i class="fa fa-step-forward"></i> Next turn</button> <button data-action="ffto"><i class="fa fa-fast-forward"></i> Go to turn...</button> <button data-action="switchSides"><i class="fa fa-random"></i> Switch sides</button>');
}
},
pause: function () {
this.$('.replay-controls').html('<button data-action="play"><i class="fa fa-play"></i> Play</button><button data-action="reset"><i class="fa fa-undo"></i> Reset</button> <button data-action="rewind"><i class="fa fa-step-backward"></i> Last turn</button><button data-action="ff"><i class="fa fa-step-forward"></i> Next turn</button> <button data-action="ffto"><i class="fa fa-fast-forward"></i> Go to turn...</button> <button data-action="switchSides"><i class="fa fa-random"></i> Switch sides</button>');
this.battle.pause();
},
play: function () {
this.$('.battle .playbutton').remove();
this.$('.replay-controls').html('<button data-action="pause"><i class="fa fa-pause"></i> Pause</button><button data-action="reset"><i class="fa fa-undo"></i> Reset</button> <button data-action="rewind"><i class="fa fa-step-backward"></i> Last turn</button><button data-action="ff"><i class="fa fa-step-forward"></i> Next turn</button> <button data-action="ffto"><i class="fa fa-fast-forward"></i> Go to turn...</button> <button data-action="switchSides"><i class="fa fa-random"></i> Switch sides</button>');
this.battle.play();
},
resume: function () {
this.play();
},
reset: function () {
this.battle.reset();
this.battle.fastForwardTo(0);
this.$('.battle').append('<div class="playbutton"><button data-action="start"><i class="fa fa-play"></i> Play</button><br /><br /><button data-action="startMuted" class="startsoundchooser" style="font-size:10pt;display:none">Play (music off)</button></div>');
// this.$('.battle-log').html('');
this.$('.replay-controls').html('<button data-action="start"><i class="fa fa-play"></i> Play</button><button data-action="reset" disabled="disabled"><i class="fa fa-undo"></i> Reset</button>');
},
ff: function () {
this.battle.skipTurn();
},
rewind: function () {
if (this.battle.turn) {
this.battle.fastForwardTo(this.battle.turn - 1);
}
this.battle.seekTurn(this.battle.turn - 1);
},
ffto: function () {
this.battle.fastForwardTo(prompt('Turn?'));
var turn = prompt('Turn?');
if (!turn.trim()) return;
if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turn = Infinity;
turn = Number(turn);
if (isNaN(turn) || turn < 0) alert("Invalid turn");
this.battle.seekTurn(turn);
},
switchSides: function () {
this.battle.switchSides();
},
start: function () {
this.battle.reset();
this.battle.play();
this.$('.replay-controls').html('<button data-action="pause"><i class="fa fa-pause"></i> Pause</button><button data-action="reset"><i class="fa fa-undo"></i> Reset</button> <button data-action="rewind"><i class="fa fa-step-backward"></i> Last turn</button><button data-action="ff"><i class="fa fa-step-forward"></i> Next turn</button> <button data-action="ffto"><i class="fa fa-fast-forward"></i> Go to turn...</button> <button data-action="switchSides"><i class="fa fa-random"></i> Switch sides</button>');
},
startMuted: function () {
this.changeSetting('sound', 'off');
this.start();
}
};

window.onload = function () {
Replays.init((this.$('script.battle-log-data').text() || '').replace(/\\\//g, '/'));
Replays.init();
};
Loading

0 comments on commit 3e4b832

Please sign in to comment.