Update Battle class API

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()`
This commit is contained in:
Guangcong Luo 2020-11-29 16:00:34 +00:00
parent 934eb7be2c
commit e1b6dd5346
9 changed files with 356 additions and 390 deletions

View File

@ -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;
@ -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');
@ -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',
@ -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);
},
@ -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();
@ -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++) {
@ -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) {
@ -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) {
@ -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 {
@ -325,7 +327,6 @@
// since those early-return.
app.topbar.updateTabbar();
},
controlsShown: false,
updateControlsForPlayer: function () {
this.callbackWaiting = true;
@ -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) {
@ -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;
@ -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');

View File

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

View File

@ -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) {
@ -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>');
@ -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);
@ -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;
@ -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();
};

View File

@ -99,11 +99,10 @@ var ReplayPanel = Panels.StaticPanel.extend({
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;
case 'speed':
@ -128,26 +127,24 @@ var ReplayPanel = Panels.StaticPanel.extend({
}
},
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 && m[1] !== 'smogtours') {
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();
}
},
muted: null,
updateContent: function() {
this.$el.css('overflow-x', 'hidden');
var $battle = this.$('.battle');
if (!$battle.length) return;
this.battle = new Battle($battle, this.$('.battle-log'));
//this.battle.preloadCallback = updateProgress;
// this.battle.errorCallback = this.errorCallback.bind(this);
this.battle.resumeButton = this.resume.bind(this);
this.battle.setQueue((this.$('script.log').text()||'').replace(/\\\//g,'/').split('\n'));
this.battle.reset();
$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>');
var replayid = this.$('input[name=replayid]').val() || '';
var log = (this.$('script.log').text() || '').replace(/\\\//g,'/');
var self = this;
this.battle = new Battle({
id: replayid,
$frame: $battle,
$logFrame: this.$('.battle-log'),
log: log.split('\n'),
isReplay: true,
paused: true
})
this.$('.urlbox').css('margin-right', 120).before('<a class="button replayDownloadButton" style="float:right;margin-top:7px;margin-right:7px" href="#"><i class="fa fa-download"></i> Download</a>');
@ -156,6 +153,27 @@ var ReplayPanel = Panels.StaticPanel.extend({
if (rc2) rc2.innerHTML = rc2.innerHTML;
if (window.HTMLAudioElement) this.$('.soundchooser, .startsoundchooser').show();
this.battle.subscribe(function (state) { self.update(state); });
this.update();
},
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>');
}
},
clickReplayDownloadButton: function (e) {
var filename = (this.battle.tier || 'Battle').replace(/[^A-Za-z0-9]/g, '');
@ -176,52 +194,35 @@ var ReplayPanel = Panels.StaticPanel.extend({
e.stopPropagation();
},
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').append('<div class="playbutton"><button data-action="start"><i class="fa fa-play"></i> Play</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() {
var turn = prompt('Turn?');
if (!turn) return;
turn = parseInt(turn);
this.battle.fastForwardTo(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.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>');
},
remove: function() {
this.battle.destroy();
Panels.StaticPanel.prototype.remove.call(this);
},
startMuted: function() {
this.changeSetting('sound', 'off');
this.start();
}
});
var App = Panels.App.extend({

View File

@ -244,10 +244,16 @@ class BattleScene {
pause() {
this.stopAnimation();
this.updateBgm();
if (this.battle.resumeButton) {
this.$frame.append('<div class="playbutton"><button data-action="resume"><i class="fa fa-play icon-play"></i> Resume</button></div>');
this.$frame.find('div.playbutton button').click(this.battle.resumeButton);
if (this.battle.turn > 0) {
this.$frame.append('<div class="playbutton"><button name="play"><i class="fa fa-play icon-play"></i> Resume</button></div>');
} else {
this.$frame.append('<div class="playbutton"><button name="play"><i class="fa fa-play"></i> Play</button><br /><br /><button name="play-muted" class="startsoundchooser" style="font-size:10pt;display:none">Play (music off)</button></div>');
this.$frame.find('div.playbutton button[name=play-muted]').click(() => {
this.battle.setMute(true);
this.battle.play();
});
}
this.$frame.find('div.playbutton button[name=play]').click(() => this.battle.play());
}
resume() {
this.$frame.find('div.playbutton').remove();
@ -939,7 +945,7 @@ class BattleScene {
}
}
resetTurn() {
if (!this.battle.turn) {
if (this.battle.turn <= 0) {
this.$turn.html('');
return;
}
@ -949,7 +955,7 @@ class BattleScene {
if (!this.animating) return;
const turn = this.battle.turn;
if (!turn) return;
if (turn <= 0) return;
const $prevTurn = this.$turn.children();
const $newTurn = $('<div class="turn has-tooltip" data-tooltip="field" data-ownheight="1">Turn ' + turn + '</div>');
$newTurn.css({
@ -1439,6 +1445,7 @@ class BattleScene {
/////////////////////////////////////////////////////////////////////
setFrameHTML(html: any) {
this.customControls = true;
this.$frame.html(html);
}
setControlsHTML(html: any) {
@ -1538,16 +1545,16 @@ class BattleScene {
}
updateBgm() {
/**
* - not playing in non-battle RoomGames (Playback.Uninitialized)
* - not playing at team preview in replays (Playback.Ready)
* - playing at team preview in games (Playback.Playing)
* - playing during the game (Playback.Playing)
* - not playing in non-battle RoomGames before `|start` (turn -1)
* - not playing at team preview in replays (paused)
* - playing at team preview in games (turn 0)
* - playing during the game (turn 1+)
* - not playing while paused
* - playing while waiting for players to choose moves (Playback.Finished)
* - playing while waiting for players to choose moves (atQueueEnd && !ended)
* - not playing after the game has ended
*/
const nowPlaying = this.battle.playbackState > Playback.Ready && (
this.battle.started && !this.battle.ended && !this.battle.paused
const nowPlaying = (
this.battle.turn >= 0 && !this.battle.ended && !this.battle.paused
);
if (nowPlaying) {
if (!this.bgm) this.rollBgm();

View File

@ -983,7 +983,7 @@ class BattleLog {
// replay panel
replayid = room.fragment;
}
battle.fastForwardTo(-1);
battle.seekTurn(Infinity);
let buf = '<!DOCTYPE html>\n';
buf += '<meta charset="utf-8" />\n';
buf += '<!-- version 1 -->\n';
@ -995,7 +995,7 @@ class BattleLog {
buf += '<input type="hidden" name="replayid" value="' + replayid + '" />\n';
buf += '<div class="battle"></div><div class="battle-log"></div><div class="replay-controls"></div><div class="replay-controls-2"></div>\n';
buf += `<h1 style="font-weight:normal;text-align:center"><strong>${BattleLog.escapeHTML(battle.tier)}</strong><br /><a href="http://${Config.routes.users}/${toID(battle.p1.name)}" class="subtle" target="_blank">${BattleLog.escapeHTML(battle.p1.name)}</a> vs. <a href="http://${Config.routes.users}/${toID(battle.p2.name)}" class="subtle" target="_blank">${BattleLog.escapeHTML(battle.p2.name)}</a></h1>\n`;
buf += '<script type="text/plain" class="battle-log-data">' + battle.activityQueue.join('\n').replace(/\//g, '\\/') + '</script>\n'; // lgtm [js/incomplete-sanitization]
buf += '<script type="text/plain" class="battle-log-data">' + battle.stepQueue.join('\n').replace(/\//g, '\\/') + '</script>\n'; // lgtm [js/incomplete-sanitization]
buf += '</div>\n';
buf += '<div class="battle-log battle-log-inline"><div class="inner">' + battle.scene.log.elem.innerHTML + '</div></div>\n';
buf += '</div>\n';

View File

@ -649,7 +649,6 @@ class Side {
this.rollTrainerSprites();
if (this.foe && this.avatar === this.foe.avatar) this.rollTrainerSprites();
}
if (this.battle.stagnateCallback) this.battle.stagnateCallback(this.battle);
}
addSideCondition(effect: Effect) {
let condition = effect.id;
@ -810,8 +809,6 @@ class Side {
}
this.battle.scene.animSummon(pokemon, slot);
if (this.battle.switchCallback) this.battle.switchCallback(this.battle, this);
}
dragIn(pokemon: Pokemon, slot = pokemon.slot) {
let oldpokemon = this.active[slot];
@ -828,8 +825,6 @@ class Side {
pokemon.slot = slot;
this.battle.scene.animDragIn(pokemon, slot);
if (this.battle.dragCallback) this.battle.dragCallback(this.battle, this);
}
replace(pokemon: Pokemon, slot = pokemon.slot) {
let oldpokemon = this.active[slot];
@ -857,8 +852,6 @@ class Side {
this.battle.scene.animUnsummon(oldpokemon, true);
}
this.battle.scene.animSummon(pokemon, slot, true);
// not sure if we want a different callback
if (this.battle.dragCallback) this.battle.dragCallback(this.battle, this);
}
switchOut(pokemon: Pokemon, slot = pokemon.slot) {
if (pokemon.lastMove !== 'batonpass' && pokemon.lastMove !== 'zbatonpass') {
@ -924,7 +917,6 @@ class Side {
pokemon.hp = 0;
this.battle.scene.animFaint(pokemon);
if (this.battle.faintCallback) this.battle.faintCallback(this.battle, this);
}
destroy() {
this.clearPokemon();
@ -933,39 +925,6 @@ class Side {
}
}
enum Playback {
/**
* Battle is at the end of the queue. `|start` is not in the queue.
* Battle is waiting for `.add()` or `.setQueue()` to add `|start` to
* the queue. Adding other queue entries will happen immediately,
* bringing the state back to Uninitialized.
*/
Uninitialized = 0,
/**
* Battle is at `|start` and hasn't been started yet.
* Battle is paused, waiting for `.play()`.
*/
Ready = 1,
/**
* `.play()` has been called. Battle should be animating
* normally.
*/
Playing = 2,
/**
* `.pause()` has been called. Battle is waiting for `.play()`.
*/
Paused = 3,
/**
* Battle is at the end of the queue. Battle is waiting for
* `.add()` for further battle progress.
*/
Finished = 4,
/**
* Battle is fast forwarding through the queue, with animations off.
*/
Seeking = 5,
}
interface PokemonDetails {
details: string;
name: string;
@ -1015,38 +974,45 @@ class Battle {
sidesSwitched = false;
// activity queue
activityQueue = [] as string[];
stepQueue: string[];
/** See battle.instantAdd */
preemptActivityQueue = [] as string[];
preemptStepQueue: string[] = [];
waitForAnimations: true | false | 'simult' = true;
activityStep = 0;
fastForward = 0;
fastForwardWillScroll = false;
/** the index of `stepQueue` currently being animated */
currentStep = 0;
/** null = not seeking, 0 = seek start, Infinity = seek end, otherwise: seek turn number */
seeking: number | null = null;
resultWaiting = false;
activeMoveIsSpread: string | null = null;
// callback
faintCallback: ((battle: Battle, side: Side) => void) | null = null;
switchCallback: ((battle: Battle, side: Side) => void) | null = null;
dragCallback: ((battle: Battle, side: Side) => void) | null = null;
turnCallback: ((battle: Battle) => void) | null = null;
startCallback: ((battle: Battle) => void) | null = null;
stagnateCallback: ((battle: Battle) => void) | null = null;
endCallback: ((battle: Battle) => void) | null = null;
customCallback: ((battle: Battle, cmd: string, args: string[], kwArgs: KWArgs) => void) | null = null;
errorCallback: ((battle: Battle) => void) | null = null;
subscription: ((state:
'playing' | 'paused' | 'turn' | 'atqueueend' | 'callback' | 'ended' | 'error'
) => void) | null;
mute = false;
messageFadeTime = 300;
messageShownTime = 1;
/** for tracking when to accelerate animations in long battles full of double switches */
turnsSinceMoved = 0;
turn = 0;
/**
* Has playback gotten to Team Preview or `|start` yet?
* (Affects whether BGM is playing)
* * `-1` = non-battle RoomGames, or hasn't hit Team Preview or `|start`
* * `0` = after Team Preview or `|start` but before `|turn|1`
*/
turn = -1;
/**
* Are we at the end of the queue and waiting for more input?
*
* In addition to at the end of a battle, this is also true if you're
* playing/watching a battle live, and waiting for a player to make a move.
*/
atQueueEnd = false;
/**
* Has the battle ever been played or fast-forwarded?
*
* This is not exactly `turn > 0` because if you start playing a replay,
* then pause before turn 1, `turn` will still be 0, but playback should
* be considered started (for the purposes of displaying "Play" vs "Resume")
*/
started = false;
/**
@ -1054,6 +1020,7 @@ class Battle {
* (Affects whether BGM is playing)
*/
ended = false;
isReplay = false;
usesUpkeep = false;
weather = '' as ID;
pseudoWeather = [] as WeatherState[];
@ -1068,7 +1035,7 @@ class Battle {
sides: [Side, Side] = [null!, null!];
lastMove = '';
gen = 7;
gen = 8;
dex: ModdedDex = Dex;
teamPreviewCount = 0;
speciesClause = false;
@ -1088,34 +1055,60 @@ class Battle {
// options
id = '';
/** used to forward some information to the room in the old client */
roomid = '';
hardcoreMode = false;
ignoreNicks = !!Dex.prefs('ignorenicks');
ignoreOpponent = !!Dex.prefs('ignoreopp');
ignoreSpects = !!Dex.prefs('ignorespects');
debug = false;
debug: boolean;
joinButtons = false;
/**
* The actual pause state. Will only be true if playback is actually
* paused, not just waiting for the opponent to make a move.
*/
paused = true;
playbackState = Playback.Uninitialized;
paused: boolean;
// external
resumeButton: JQuery.EventHandler<HTMLElement, null> | null = null;
constructor(options: {
$frame?: JQuery<HTMLElement>,
$logFrame?: JQuery<HTMLElement>,
id?: ID,
log?: string[],
paused?: boolean,
isReplay?: boolean,
debug?: boolean,
subscription?: Battle['subscription'],
} = {}) {
this.id = options.id || '';
constructor($frame: JQuery<HTMLElement>, $logFrame: JQuery<HTMLElement>, id = '') {
this.id = id;
if (!$frame && !$logFrame) {
if (options.$frame && options.$logFrame) {
this.scene = new BattleScene(this, options.$frame, options.$logFrame);
} else if (!options.$frame && !options.$logFrame) {
this.scene = new BattleSceneStub();
} else {
this.scene = new BattleScene(this, $frame, $logFrame);
throw new Error(`You must specify $frame and $logFrame simultaneously`);
}
this.init();
this.paused = !!options.paused;
this.started = !this.paused;
this.debug = !!options.debug;
this.stepQueue = options.log || [];
this.subscription = options.subscription || null;
this.p1 = new Side(this, 0);
this.p2 = new Side(this, 1);
this.sides = [this.p1, this.p2];
this.p2.foe = this.p1;
this.p1.foe = this.p2;
this.nearSide = this.mySide = this.p1;
this.farSide = this.p2;
this.resetStep();
}
subscribe(listener: Battle['subscription']) {
this.subscription = listener;
}
removePseudoWeather(weather: string) {
@ -1139,22 +1132,18 @@ class Battle {
}
return false;
}
init() {
this.p1 = new Side(this, 0);
this.p2 = new Side(this, 1);
this.sides = [this.p1, this.p2];
this.p2.foe = this.p1;
this.p1.foe = this.p2;
this.nearSide = this.mySide = this.p1;
this.farSide = this.p2;
this.gen = 7;
this.reset();
reset() {
this.paused = true;
this.scene.pause();
this.resetStep();
this.subscription?.('paused');
}
reset(dontResetSound?: boolean) {
resetStep() {
// battle state
this.turn = 0;
this.started = false;
this.turn = -1;
this.started = !this.paused;
this.ended = false;
this.atQueueEnd = false;
this.weather = '' as ID;
this.weatherTimeLeft = 0;
this.weatherMinTimeLeft = 0;
@ -1171,16 +1160,9 @@ class Battle {
// activity queue state
this.activeMoveIsSpread = null;
this.activityStep = 0;
this.fastForwardOff();
this.resultWaiting = false;
this.paused = true;
if (this.playbackState !== Playback.Seeking) {
this.playbackState = Playback.Uninitialized;
if (!dontResetSound) this.scene.resetBgm();
}
this.currentStep = 0;
this.resetTurnsSinceMoved();
this.nextActivity();
this.nextStep();
}
destroy() {
this.scene.destroy();
@ -1201,21 +1183,7 @@ class Battle {
}
resetToCurrentTurn() {
if (this.ended) {
this.reset(true);
this.fastForwardTo(-1);
} else {
let turn = this.turn;
let paused = this.paused;
this.reset(true);
this.paused = paused;
if (turn) this.fastForwardTo(turn);
if (!paused) {
this.play();
} else {
this.pause();
}
}
this.seekTurn(this.ended ? Infinity : this.turn, true);
}
switchSides() {
this.setSidesSwitched(!this.sidesSwitched);
@ -1242,15 +1210,16 @@ class Battle {
start() {
this.log(['start']);
this.resetTurnsSinceMoved();
if (this.startCallback) this.startCallback(this);
}
winner(winner?: string) {
this.log(['win', winner || '']);
this.ended = true;
this.subscription?.('ended');
}
prematureEnd() {
this.log(['message', 'This replay ends here.']);
this.ended = true;
this.subscription?.('ended');
}
endLastTurn() {
if (this.endLastTurnPending) {
@ -1263,28 +1232,25 @@ class Battle {
this.scene.updateSidebars();
this.scene.updateWeather(true);
}
setTurn(turnNum: string | number) {
turnNum = parseInt(turnNum as string, 10);
setTurn(turnNum: number) {
if (turnNum === this.turn + 1) {
this.endLastTurnPending = true;
}
if (this.turn && !this.usesUpkeep) this.updateTurnCounters(); // for compatibility with old replays
this.turn = turnNum;
this.started = true;
if (!this.fastForward) this.turnsSinceMoved++;
if (this.seeking === null) this.turnsSinceMoved++;
this.scene.incrementTurn();
if (this.fastForward) {
if (this.turnCallback) this.turnCallback(this);
if (this.fastForward > -1 && turnNum >= this.fastForward) {
this.fastForwardOff();
if (this.endCallback) this.endCallback(this);
if (this.seeking !== null) {
if (turnNum >= this.seeking) {
this.stopSeeking();
}
return;
} else {
this.subscription?.('turn');
}
if (this.turnCallback) this.turnCallback(this);
}
resetTurnsSinceMoved() {
this.turnsSinceMoved = 0;
@ -1300,7 +1266,7 @@ class Battle {
this.weatherTimeLeft--;
if (this.weatherMinTimeLeft !== 0) this.weatherMinTimeLeft--;
}
if (!this.fastForward) {
if (this.seeking === null) {
this.scene.upkeepWeather();
}
return;
@ -1389,7 +1355,7 @@ class Battle {
}
animateMove(pokemon: Pokemon, move: Move, target: Pokemon | null, kwArgs: KWArgs) {
this.activeMoveIsSpread = kwArgs.spread;
if (this.fastForward || kwArgs.still) return;
if (this.seeking !== null || kwArgs.still) return;
if (!target) target = pokemon.side.foe.active[0];
if (!target) target = pokemon.side.foe.missedPokemon;
@ -1514,7 +1480,7 @@ class Battle {
}
if (args[0] === 'detailschange' && nextArgs[0] === '-mega') {
if (this.scene.closeMessagebar()) {
this.activityStep--;
this.currentStep--;
return;
}
kwArgs.simult = '.';
@ -2820,11 +2786,10 @@ class Battle {
switch (effect.id) {
case 'gravity':
if (!this.fastForward) {
for (const side of this.sides) {
for (const active of side.active) {
if (active) this.scene.runOtherAnim('gravity' as ID, [active]);
}
if (this.seeking !== null) break;
for (const side of this.sides) {
for (const active of side.active) {
if (active) this.scene.runOtherAnim('gravity' as ID, [active]);
}
}
break;
@ -3082,20 +3047,12 @@ class Battle {
} as any;
}
add(command: string, fastForward?: boolean) {
if (command) this.activityQueue.push(command);
add(command?: string) {
if (command) this.stepQueue.push(command);
if (this.playbackState === Playback.Uninitialized) {
this.nextActivity();
} else if (this.playbackState === Playback.Finished) {
this.playbackState = this.paused ? Playback.Paused : Playback.Playing;
if (this.paused) return;
this.scene.updateBgm();
if (fastForward) {
this.fastForwardTo(-1);
} else {
this.nextActivity();
}
if (this.atQueueEnd && this.currentStep < this.stepQueue.length) {
this.atQueueEnd = false;
this.nextStep();
}
}
/**
@ -3108,7 +3065,7 @@ class Battle {
*/
instantAdd(command: string) {
this.run(command, true);
this.preemptActivityQueue.push(command);
this.preemptStepQueue.push(command);
this.add(command);
}
runMajor(args: Args, kwArgs: KWArgs, preempt?: boolean) {
@ -3126,7 +3083,7 @@ class Battle {
break;
}
case 'turn': {
this.setTurn(args[1]);
this.setTurn(parseInt(args[1], 10));
this.log(args);
break;
}
@ -3373,11 +3330,10 @@ class Battle {
break;
}
case 'callback': {
if (this.customCallback) this.customCallback(this, args[1], args.slice(1), kwArgs);
this.subscription?.('callback');
break;
}
case 'fieldhtml': {
this.playbackState = Playback.Seeking; // force seeking to prevent controls etc
this.scene.setFrameHTML(BattleLog.sanitizeHTML(args[1]));
break;
}
@ -3392,8 +3348,8 @@ class Battle {
}
run(str: string, preempt?: boolean) {
if (!preempt && this.preemptActivityQueue.length && str === this.preemptActivityQueue[0]) {
this.preemptActivityQueue.shift();
if (!preempt && this.preemptStepQueue.length && str === this.preemptStepQueue[0]) {
this.preemptStepQueue.shift();
this.scene.preemptCatchup();
return;
}
@ -3401,7 +3357,7 @@ class Battle {
const {args, kwArgs} = BattleTextParser.parseBattleLine(str);
if (this.scene.maybeCloseMessagebar(args, kwArgs)) {
this.activityStep--;
this.currentStep--;
this.activeMoveIsSpread = null;
return;
}
@ -3409,7 +3365,7 @@ class Battle {
// parse the next line if it's a minor: runMinor needs it parsed to determine when to merge minors
let nextArgs: Args = [''];
let nextKwargs: KWArgs = {};
const nextLine = this.activityQueue[this.activityStep + 1] || '';
const nextLine = this.stepQueue[this.currentStep + 1] || '';
if (nextLine.slice(0, 2) === '|-') {
({args: nextArgs, kwArgs: nextKwargs} = BattleTextParser.parseBattleLine(nextLine));
}
@ -3438,16 +3394,15 @@ class Battle {
this.log(['error', line]);
}
}
if (this.errorCallback) this.errorCallback(this);
this.subscription?.('error');
}
}
if (nextLine.startsWith('|start') || args[0] === 'teampreview') {
this.started = true;
if (this.playbackState === Playback.Uninitialized) {
this.playbackState = Playback.Ready;
if (this.turn === -1) {
this.turn = 0;
this.scene.updateBgm();
}
this.scene.updateBgm();
}
}
checkActive(poke: Pokemon) {
@ -3460,93 +3415,114 @@ class Battle {
pause() {
this.paused = true;
this.playbackState = Playback.Paused;
this.scene.pause();
this.subscription?.('paused');
}
/**
* Properties relevant to battle playback, for replay UI implementers:
* - `ended`: has the game ended in a win/loss?
* - `atQueueEnd`: is animation caught up to the end of the battle queue, waiting for more input?
* - `seeking`: are we trying to skip to a specific turn
* - `turn`: what turn are we currently on? `-1` if we haven't started yet, `0` at team preview
* - `paused`: are we playing at all?
*/
play() {
this.paused = false;
this.playbackState = Playback.Playing;
this.started = true;
this.scene.resume();
this.nextActivity();
this.nextStep();
this.subscription?.('playing');
}
skipTurn() {
this.fastForwardTo(this.turn + 1);
this.seekTurn(this.turn + 1);
}
fastForwardTo(time: string | number) {
if (this.fastForward) return;
time = Math.floor(Number(time));
if (isNaN(time)) return;
if (this.ended && time >= this.turn + 1) return;
seekTurn(turn: number, forceReset?: boolean) {
if (isNaN(turn)) return;
turn = Math.max(Math.floor(turn), 0);
if (time <= this.turn && time !== -1) {
let paused = this.paused;
this.reset(true);
if (paused) this.pause();
else this.paused = false;
this.fastForwardWillScroll = true;
}
if (!time) {
this.fastForwardOff();
this.nextActivity();
if (this.seeking !== null && this.seeking > turn && !forceReset) {
this.seeking = turn;
return;
}
this.scene.animationOff();
this.playbackState = Playback.Seeking;
this.fastForward = time;
this.nextActivity();
if (turn === 0) {
this.seeking = null;
this.resetStep();
this.scene.animationOn();
if (this.paused) this.subscription?.('paused');
return;
}
this.seeking = turn;
if (turn <= this.turn || forceReset) {
this.scene.animationOff();
this.resetStep();
} else if (this.atQueueEnd) {
this.scene.animationOn();
this.seeking = null;
} else {
this.scene.animationOff();
this.nextStep();
}
}
fastForwardOff() {
this.fastForward = 0;
stopSeeking() {
this.seeking = null;
this.scene.animationOn();
this.playbackState = this.paused ? Playback.Paused : Playback.Playing;
this.subscription?.(this.paused ? 'paused' : 'playing');
}
nextActivity() {
if (this.playbackState === Playback.Ready || this.playbackState === Playback.Paused) {
return;
}
shouldStep() {
if (this.atQueueEnd) return false;
if (this.seeking !== null) return true;
return !(this.paused && this.turn >= 0);
}
nextStep() {
if (!this.shouldStep()) return;
this.scene.startAnimations();
let animations = undefined;
while (!animations) {
do {
this.waitForAnimations = true;
if (this.activityStep >= this.activityQueue.length) {
this.fastForwardOff();
this.playbackState = Playback.Finished;
if (this.currentStep >= this.stepQueue.length) {
this.atQueueEnd = true;
if (!this.ended && this.isReplay) this.prematureEnd();
this.stopSeeking();
if (this.ended) {
this.scene.updateBgm();
}
if (this.endCallback) this.endCallback(this);
this.subscription?.('atqueueend');
return;
}
// @ts-ignore property modified in method
if (this.playbackState === Playback.Ready || this.playbackState === Playback.Paused) {
return;
}
this.run(this.activityQueue[this.activityStep]);
this.activityStep++;
this.run(this.stepQueue[this.currentStep]);
this.currentStep++;
if (this.waitForAnimations === true) {
animations = this.scene.finishAnimations();
} else if (this.waitForAnimations === 'simult') {
this.scene.timeOffset = 0;
}
}
} while (!animations && this.shouldStep());
// @ts-ignore property modified in method
if (this.playbackState === Playback.Ready || this.playbackState === Playback.Paused) {
if (this.paused && this.turn >= 0 && this.seeking === null) {
// initial Play button, team preview
this.scene.pause();
return;
}
if (!animations) return;
const interruptionCount = this.scene.interruptionCount;
animations.done(() => {
if (interruptionCount === this.scene.interruptionCount) {
this.nextActivity();
this.nextStep();
}
});
}
setQueue(queue: string[]) {
this.activityQueue = queue;
this.reset();
this.stepQueue = queue;
this.resetStep();
}
setMute(mute: boolean) {

View File

@ -123,7 +123,7 @@ class BattleRoom extends ChatRoom {
this.receiveLine([`error`, `/ffto - Invalid turn number: ${target}`]);
return true;
}
this.battle.fastForwardTo(turnNum);
this.battle.seekTurn(turnNum);
this.update(null);
return true;
} case 'switchsides': {
@ -257,18 +257,20 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
};
componentDidMount() {
const $elem = $(this.base!);
const battle = new Battle($elem.find('.battle'), $elem.find('.battle-log'));
const battle = new Battle({
$frame: $elem.find('.battle'),
$logFrame: $elem.find('.battle-log'),
});
this.props.room.battle = battle;
battle.endCallback = () => this.forceUpdate();
battle.play();
(battle.scene as BattleScene).tooltips.listen($elem.find('.battle-controls'));
super.componentDidMount();
battle.subscribe(() => this.forceUpdate());
}
receiveLine(args: Args) {
const room = this.props.room;
switch (args[0]) {
case 'initdone':
room.battle.fastForwardTo(-1);
room.battle.seekTurn(Infinity);
return;
case 'request':
this.receiveRequest(args[1] ? JSON.parse(args[1]) : null);
@ -316,7 +318,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
if (room.side) {
return this.renderPlayerControls();
}
const atEnd = room.battle.playbackState === Playback.Finished;
const atEnd = room.battle.atQueueEnd;
return <div class="controls">
<p>
{atEnd ?

View File

@ -12,33 +12,32 @@ require('../js/battle.js');
describe('Battle', () => {
it('should process a bunch of messages properly', () => {
let battle = new Battle();
battle.debug = true;
battle.setQueue([
"|init|battle",
"|title|FOO vs. BAR",
"|j|FOO",
"|j|BAR",
"|request|",
"|player|p1|FOO|169",
"|player|p2|BAR|265",
"|teamsize|p1|6",
"|teamsize|p2|6",
"|gametype|singles",
"|gen|7",
"|tier|[Gen 7] Random Battle",
"|rated|",
"|seed|",
"|rule|Sleep Clause Mod: Limit one foe put to sleep",
"|rule|HP Percentage Mod: HP is shown in percentages",
"|",
"|start",
"|switch|p1a: Leafeon|Leafeon, L83, F|100/100",
"|switch|p2a: Gliscor|Gliscor, L77, F|242/242",
"|turn|1",
]);
battle.fastForwardTo(-1);
let battle = new Battle({
debug: true,
log: [
"|init|battle",
"|title|FOO vs. BAR",
"|j|FOO",
"|j|BAR",
"|request|",
"|player|p1|FOO|169",
"|player|p2|BAR|265",
"|teamsize|p1|6",
"|teamsize|p2|6",
"|gametype|singles",
"|gen|7",
"|tier|[Gen 7] Random Battle",
"|rated|",
"|seed|",
"|rule|Sleep Clause Mod: Limit one foe put to sleep",
"|rule|HP Percentage Mod: HP is shown in percentages",
"|",
"|start",
"|switch|p1a: Leafeon|Leafeon, L83, F|100/100",
"|switch|p2a: Gliscor|Gliscor, L77, F|242/242",
"|turn|1",
],
});
let p1 = battle.sides[0];
let p2 = battle.sides[1];
@ -75,7 +74,6 @@ describe('Battle', () => {
]) {
battle.add(line);
}
battle.fastForwardTo(-1);
assert(!p2gliscor.isActive());
let p2kyurem = p2.pokemon[1];