Refactor queue to new BattleQueue class

Previously, battle queue stuff was just strewn around `battle.ts`.
This gives it a new home: `battle-queue.ts`.

This was intended to make `battle.ts` slightly more tractable, although
the difference is so small that maybe I shouldn't bother. Oh, well,
every little bit helps.
This commit is contained in:
Guangcong Luo 2020-02-11 14:44:03 -08:00
parent b91779476f
commit 9659511ff1
23 changed files with 496 additions and 420 deletions

View File

@ -107,7 +107,7 @@ let BattleAbilities = {
let boosted = true;
for (const target of this.getAllActive()) {
if (target === pokemon) continue;
if (this.willMove(target)) {
if (this.queue.willMove(target)) {
boosted = false;
break;
}
@ -439,7 +439,7 @@ let BattleAbilities = {
if (target.side.active.length === 2 && target.position === 1) {
// Curse Glitch
const action = this.willMove(target);
const action = this.queue.willMove(target);
if (action && action.move.id === 'curse') {
action.targetLoc = -1;
}
@ -2389,7 +2389,7 @@ let BattleAbilities = {
continue;
}
// pokemon isn't switching this turn
if (curPoke !== pokemon && !this.willSwitch(curPoke)) {
if (curPoke !== pokemon && !this.queue.willSwitch(curPoke)) {
// this.add('-message', "" + curPoke + " skipped: not switching");
continue;
}
@ -4300,7 +4300,7 @@ let BattleAbilities = {
shortDesc: "This Pokemon skips every other turn instead of using a move.",
onStart(pokemon) {
pokemon.removeVolatile('truant');
if (pokemon.activeTurns && (pokemon.moveThisTurnResult !== undefined || !this.willMove(pokemon))) {
if (pokemon.activeTurns && (pokemon.moveThisTurnResult !== undefined || !this.queue.willMove(pokemon))) {
pokemon.addVolatile('truant');
}
},

View File

@ -576,7 +576,7 @@ let BattleItems = {
spritenum: 41,
onSwitchIn(pokemon) {
if (pokemon.isActive && pokemon.baseTemplate.species === 'Kyogre') {
this.insertQueue({pokemon: pokemon, choice: 'runPrimal'});
this.queue.insertChoice({choice: 'runPrimal', pokemon: pokemon});
}
},
onPrimal(pokemon) {
@ -5007,7 +5007,7 @@ let BattleItems = {
spritenum: 390,
onSwitchIn(pokemon) {
if (pokemon.isActive && pokemon.baseTemplate.species === 'Groudon') {
this.insertQueue({pokemon: pokemon, choice: 'runPrimal'});
this.queue.insertChoice({choice: 'runPrimal', pokemon: pokemon});
}
},
onPrimal(pokemon) {
@ -7759,7 +7759,7 @@ let BattleItems = {
basePower: 10,
},
onSourceModifyAccuracy(accuracy, target) {
if (typeof accuracy === 'number' && !this.willMove(target)) {
if (typeof accuracy === 'number' && !this.queue.willMove(target)) {
this.debug('Zoom Lens boosting accuracy');
return accuracy * 1.2;
}

View File

@ -255,7 +255,7 @@ let BattleMovedex = {
// If both are true, counter will deal twice the last damage dealt in battle, no matter what was the move.
// That means that, if opponent switches, counter will use last counter damage * 2.
let lastUsedMove = target.side.lastMove && this.dex.getMove(target.side.lastMove.id);
if (lastUsedMove && lastUsedMove.basePower > 0 && ['Normal', 'Fighting'].includes(lastUsedMove.type) && this.lastDamage > 0 && !this.willMove(target)) {
if (lastUsedMove && lastUsedMove.basePower > 0 && ['Normal', 'Fighting'].includes(lastUsedMove.type) && this.lastDamage > 0 && !this.queue.willMove(target)) {
return 2 * this.lastDamage;
}
this.debug("Gen 1 Counter failed due to conditions not met");
@ -304,7 +304,7 @@ let BattleMovedex = {
return duration;
},
onStart(pokemon) {
if (!this.willMove(pokemon)) {
if (!this.queue.willMove(pokemon)) {
this.effectData.duration++;
}
let moves = pokemon.moves;

View File

@ -252,7 +252,7 @@ let BattleMovedex = {
}
this.effectData.move = target.lastMove.id;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
if (!this.queue.willMove(target)) {
this.effectData.duration++;
}
},

View File

@ -112,7 +112,7 @@ let BattleScripts = {
if (pokemon.moveThisTurn) {
// THIS IS PURELY A SANITY CHECK
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
// USE this.cancelMove INSTEAD
// USE this.queue.cancelMove INSTEAD
this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn);
this.clearActiveMove(true);
return;

View File

@ -201,7 +201,7 @@ let BattleStatuses = {
let move = this.dex.getMove(this.effectData.move);
if (move.id) {
this.debug('Forcing into ' + move.id);
this.changeAction(pokemon, {move: move.id});
this.queue.changeAction(pokemon, {choice: 'move', moveid: move.id});
}
},
},

View File

@ -256,7 +256,7 @@ let BattleMovedex = {
},
noCopy: true,
onStart(pokemon) {
if (!this.willMove(pokemon)) {
if (!this.queue.willMove(pokemon)) {
this.effectData.duration++;
}
if (!pokemon.lastMove) {
@ -365,7 +365,7 @@ let BattleMovedex = {
}
this.effectData.move = target.lastMove.id;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
if (!this.queue.willMove(target)) {
this.effectData.duration++;
}
},

View File

@ -50,9 +50,9 @@ let BattleItems = {
onModifyPriority() {},
onBeforeTurn(pokemon) {
if (pokemon.hp <= pokemon.maxhp / 4 || (pokemon.hp <= pokemon.maxhp / 2 && pokemon.ability === 'gluttony')) {
let action = this.willMove(pokemon);
let action = this.queue.willMove(pokemon);
if (!action) return;
this.insertQueue({
this.queue.insertChoice({
choice: 'event',
event: 'Custap',
priority: action.priority + 0.1,
@ -63,10 +63,10 @@ let BattleItems = {
}
},
onCustap(pokemon) {
let action = this.willMove(pokemon);
let action = this.queue.willMove(pokemon);
this.debug('custap action: ' + action);
if (action && pokemon.eatItem()) {
this.cancelAction(pokemon);
this.queue.cancelAction(pokemon);
this.add('-message', "Custap Berry activated.");
this.runAction(action);
}

View File

@ -382,7 +382,7 @@ let BattleMovedex = {
},
noCopy: true,
onStart(pokemon) {
if (!this.willMove(pokemon)) {
if (!this.queue.willMove(pokemon)) {
this.effectData.duration++;
}
if (!pokemon.lastMove) {
@ -527,7 +527,7 @@ let BattleMovedex = {
}
this.effectData.move = target.lastMove.id;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
if (!this.queue.willMove(target)) {
this.effectData.duration++;
}
},
@ -1182,7 +1182,7 @@ let BattleMovedex = {
inherit: true,
desc: "Power doubles if the user moves after the target this turn. Switching in counts as an action.",
basePowerCallback(pokemon, target) {
if (this.willMove(target)) {
if (this.queue.willMove(target)) {
return 50;
}
return 100;
@ -1543,7 +1543,7 @@ let BattleMovedex = {
inherit: true,
desc: "Fails if the target did not select a physical or special attack for use this turn, or if the target moves before the user.",
onTry(source, target) {
let action = this.willMove(target);
let action = this.queue.willMove(target);
if (!action || action.choice !== 'move' || action.move.category === 'Status' || target.volatiles.mustrecharge) {
this.add('-fail', source);
return null;

View File

@ -767,7 +767,7 @@ let BattleMovedex = {
desc: "The user and its party members are protected from attacks with original priority greater than 0 made by other Pokemon, including allies, during this turn. This attack has a 1/X chance of being successful, where X starts at 1 and doubles each time this move is successfully used. X resets to 1 if this attack fails or if the user's last used move is not Detect, Endure, Protect, Quick Guard, or Wide Guard. If X is 256 or more, this move has a 1/(2^32) chance of being successful. Fails if the user moves last this turn or if this move is already in effect for the user's side.",
stallingMove: true,
onTryHitSide(side, source) {
return this.willAct() && this.runEvent('StallMove', source);
return this.queue.willAct() && this.runEvent('StallMove', source);
},
onHitSide(side, source) {
source.addVolatile('stall');
@ -1230,7 +1230,7 @@ let BattleMovedex = {
desc: "The user and its party members are protected from damaging attacks made by other Pokemon, including allies, during this turn that target all adjacent foes or all adjacent Pokemon. This attack has a 1/X chance of being successful, where X starts at 1 and doubles each time this move is successfully used. X resets to 1 if this attack fails or if the user's last used move is not Detect, Endure, Protect, Quick Guard, or Wide Guard. If X is 256 or more, this move has a 1/(2^32) chance of being successful. Fails if the user moves last this turn or if this move is already in effect for the user's side.",
stallingMove: true,
onTryHitSide(side, source) {
return this.willAct() && this.runEvent('StallMove', source);
return this.queue.willAct() && this.runEvent('StallMove', source);
},
onHitSide(side, source) {
source.addVolatile('stall');

View File

@ -85,7 +85,7 @@ let BattleMovedex = {
}
this.effectData.move = target.lastMove.id;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
if (!this.queue.willMove(target)) {
this.effectData.duration++;
}
},

View File

@ -817,11 +817,11 @@ let BattleMovedex = {
inherit: true,
onHit(target) {
if (target.side.active.length < 2) return false; // fails in singles
let action = this.willMove(target);
let action = this.queue.willMove(target);
if (!action) return false;
action.priority = -7.1;
this.cancelMove(target);
this.queue.cancelMove(target);
for (let i = this.queue.length - 1; i >= 0; i--) {
if (this.queue[i].choice === 'residual') {
this.queue.splice(i, 0, action);

View File

@ -34,7 +34,7 @@ let BattleItems = {
"zoomlens": {
inherit: true,
onSourceModifyAccuracy(accuracy, target) {
if (typeof accuracy === 'number' && !this.willMove(target)) {
if (typeof accuracy === 'number' && !this.queue.willMove(target)) {
this.debug('Zoom Lens boosting accuracy');
return accuracy * 1.6;
}

View File

@ -625,7 +625,7 @@ let BattleMovedex = {
bide: {
inherit: true,
onTryHit(pokemon) {
return this.willAct() && this.runEvent('StallMove', pokemon);
return this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
effect: {
duration: 2,

View File

@ -324,7 +324,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'backoffgrrr',
onTryHit(target, source, move) {
return !!this.willAct() && this.runEvent('StallMove', target);
return !!this.queue.willAct() && this.runEvent('StallMove', target);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -447,7 +447,7 @@ let BattleMovedex = {
this.add('-anim', target, 'Dark Void', target);
},
onHit(target, source, move) {
let wouldMove = this.cancelMove(target);
let wouldMove = this.queue.cancelMove(target);
// Generate a new team
let team = this.teamGenerator.getTeam({name: target.side.name, inBattle: true});
let set = team.shift();
@ -2495,7 +2495,7 @@ let BattleMovedex = {
this.attrLastMove('[still]');
},
onTryHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onPrepareHit(target, source) {
this.add('-anim', source, "King's Shield", source);
@ -2839,7 +2839,7 @@ let BattleMovedex = {
onPrepareHit(target, source) {
this.add('-anim', source, "Protect", source);
this.add('-anim', source, "Quiver Dance", source);
let result = !!this.willAct() && this.runEvent('StallMove', source);
let result = !!this.queue.willAct() && this.runEvent('StallMove', source);
return result;
},
onHit(target, source) {
@ -3493,7 +3493,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'lilypadshield',
onTryHit(target, source, move) {
return !!this.willAct() && this.runEvent('StallMove', target);
return !!this.queue.willAct() && this.runEvent('StallMove', target);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -3675,7 +3675,7 @@ let BattleMovedex = {
},
onHit(target, source) {
source.addVolatile('rage', source);
if (this.willAct() && this.runEvent('StallMove', source)) {
if (this.queue.willAct() && this.runEvent('StallMove', source)) {
this.debug('Rageeeee endure');
source.addVolatile('endure', source);
source.addVolatile('stall');
@ -4281,7 +4281,7 @@ let BattleMovedex = {
this.add('-message', `${source.active[0].name}'s replacement is going to switch out next turn!`);
},
onBeforeTurn(pokemon) {
this.insertQueue({choice: 'event', event: 'SSBRotate', pokemon: pokemon, priority: -69});
this.queue.insertChoice({choice: 'event', event: 'SSBRotate', pokemon: pokemon, priority: -69});
},
// @ts-ignore unsupported custom event
onSSBRotate(/** @type {Pokemon} */ pokemon) {

View File

@ -24,7 +24,7 @@ let BattleScripts = {
/* if (pokemon.moveThisTurn) {
// THIS IS PURELY A SANITY CHECK
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
// USE this.cancelMove INSTEAD
// USE this.queue.cancelMove INSTEAD
this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn);
this.clearActiveMove(true);
return;

View File

@ -280,9 +280,9 @@ let BattleMovedex = {
flags: {authentic: 1, mystery: 1},
onHit(target) {
if (target.side.active.length < 2) return false; // fails in singles
let action = this.willMove(target);
let action = this.queue.willMove(target);
if (action) {
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
this.add('-activate', target, 'move: After You');
} else {
return false;
@ -1041,7 +1041,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'banefulbunker',
onTryHit(target, source, move) {
return !!this.willAct() && this.runEvent('StallMove', target);
return !!this.queue.willAct() && this.runEvent('StallMove', target);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -1615,7 +1615,7 @@ let BattleMovedex = {
accuracy: 100,
basePower: 85,
basePowerCallback(pokemon, target, move) {
if (target.newlySwitched || this.willMove(target)) {
if (target.newlySwitched || this.queue.willMove(target)) {
this.debug('Bolt Beak damage boost');
return move.basePower * 2;
}
@ -2842,12 +2842,12 @@ let BattleMovedex = {
flags: {protect: 1, mirror: 1},
onHit(target) {
if (['battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange', 'zenmode'].includes(target.ability)) return;
if (target.newlySwitched || this.willMove(target)) return;
if (target.newlySwitched || this.queue.willMove(target)) return;
target.addVolatile('gastroacid');
},
onAfterSubDamage(damage, target) {
if (['battlebond', 'comatose', 'disguise', 'multitype', 'powerconstruct', 'rkssystem', 'schooling', 'shieldsdown', 'stancechange', 'zenmode'].includes(target.ability)) return;
if (target.newlySwitched || this.willMove(target)) return;
if (target.newlySwitched || this.queue.willMove(target)) return;
target.addVolatile('gastroacid');
},
secondary: null,
@ -3105,7 +3105,7 @@ let BattleMovedex = {
flags: {},
sideCondition: 'craftyshield',
onTryHitSide(side, source) {
return !!this.willAct();
return !!this.queue.willAct();
},
effect: {
duration: 1,
@ -3558,7 +3558,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'protect',
onPrepareHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -3682,7 +3682,7 @@ let BattleMovedex = {
noCopy: true, // doesn't get copied by Baton Pass
onStart(pokemon, source, effect) {
// The target hasn't taken its turn, or Cursed Body activated and the move was not used through Dancer or Instruct
if (this.willMove(pokemon) || (pokemon === this.activePokemon && this.activeMove && !this.activeMove.isExternal)) {
if (this.queue.willMove(pokemon) || (pokemon === this.activePokemon && this.activeMove && !this.activeMove.isExternal)) {
this.effectData.duration--;
}
if (!pokemon.lastMove) {
@ -4594,7 +4594,7 @@ let BattleMovedex = {
flags: {protect: 1, mirror: 1, mystery: 1},
volatileStatus: 'electrify',
onTryHit(target) {
if (!this.willMove(target) && target.activeTurns) return false;
if (!this.queue.willMove(target) && target.activeTurns) return false;
},
effect: {
duration: 1,
@ -4753,7 +4753,7 @@ let BattleMovedex = {
}
this.effectData.move = move.id;
this.add('-start', target, 'Encore');
if (!this.willMove(target)) {
if (!this.queue.willMove(target)) {
this.effectData.duration++;
}
},
@ -4828,7 +4828,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'endure',
onTryHit(pokemon) {
return this.willAct() && this.runEvent('StallMove', pokemon);
return this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -5407,7 +5407,7 @@ let BattleMovedex = {
// @ts-ignore
if (action.pokemon.side === source.side && ['grasspledge', 'waterpledge'].includes(action.move.id)) {
// @ts-ignore
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
this.add('-waiting', source, action.pokemon);
return null;
}
@ -5522,7 +5522,7 @@ let BattleMovedex = {
accuracy: 100,
basePower: 85,
basePowerCallback(pokemon, target, move) {
if (target.newlySwitched || this.willMove(target)) {
if (target.newlySwitched || this.queue.willMove(target)) {
this.debug('Fishious Rend damage boost');
return move.basePower * 2;
}
@ -7633,7 +7633,7 @@ let BattleMovedex = {
// @ts-ignore
if (action.pokemon.side === source.side && ['waterpledge', 'firepledge'].includes(action.move.id)) {
// @ts-ignore
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
this.add('-waiting', source, action.pokemon);
return null;
}
@ -7808,12 +7808,12 @@ let BattleMovedex = {
let applies = false;
if (pokemon.removeVolatile('bounce') || pokemon.removeVolatile('fly')) {
applies = true;
this.cancelMove(pokemon);
this.queue.cancelMove(pokemon);
pokemon.removeVolatile('twoturnmove');
}
if (pokemon.volatiles['skydrop']) {
applies = true;
this.cancelMove(pokemon);
this.queue.cancelMove(pokemon);
if (pokemon.volatiles['skydrop'].source) {
this.add('-end', pokemon.volatiles['twoturnmove'].source, 'Sky Drop', '[interrupt]');
@ -8634,7 +8634,7 @@ let BattleMovedex = {
flags: {authentic: 1},
volatileStatus: 'helpinghand',
onTryHit(target) {
if (!target.newlySwitched && !this.willMove(target)) return false;
if (!target.newlySwitched && !this.queue.willMove(target)) return false;
},
effect: {
duration: 1,
@ -10057,7 +10057,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'kingsshield',
onTryHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -11369,7 +11369,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'maxguard',
onPrepareHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -11767,7 +11767,7 @@ let BattleMovedex = {
priority: 0,
flags: {protect: 1, authentic: 1},
onTryHit(target, pokemon) {
const action = this.willMove(target);
const action = this.queue.willMove(target);
if (!action) return false;
const noMeFirst = [
@ -13134,7 +13134,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'obstruct',
onTryHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -13487,7 +13487,7 @@ let BattleMovedex = {
accuracy: 100,
basePower: 50,
basePowerCallback(pokemon, target, move) {
if (target.newlySwitched || this.willMove(target)) {
if (target.newlySwitched || this.queue.willMove(target)) {
this.debug('Payback NOT boosted');
return move.basePower;
}
@ -14309,7 +14309,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'protect',
onPrepareHit(pokemon) {
return !!this.willAct() && this.runEvent('StallMove', pokemon);
return !!this.queue.willAct() && this.runEvent('StallMove', pokemon);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -14746,7 +14746,7 @@ let BattleMovedex = {
this.debug('Pursuit start');
let alreadyAdded = false;
for (const source of this.effectData.sources) {
if (!this.cancelMove(source) || !source.hp) continue;
if (!this.queue.cancelMove(source) || !source.hp) continue;
if (!alreadyAdded) {
this.add('-activate', pokemon, 'move: Pursuit');
alreadyAdded = true;
@ -14805,7 +14805,7 @@ let BattleMovedex = {
flags: {protect: 1, mirror: 1},
onHit(target) {
if (target.side.active.length < 2) return false; // fails in singles
let action = this.willMove(target);
let action = this.queue.willMove(target);
if (!action) return false;
action.order = 201;
@ -14849,7 +14849,7 @@ let BattleMovedex = {
flags: {snatch: 1},
sideCondition: 'quickguard',
onTryHitSide(side, source) {
return !!this.willAct();
return !!this.queue.willAct();
},
onHitSide(side, source) {
source.addVolatile('stall');
@ -15864,7 +15864,7 @@ let BattleMovedex = {
// @ts-ignore
if (action.move.id === 'round') {
// @ts-ignore
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
return;
}
}
@ -16613,9 +16613,9 @@ let BattleMovedex = {
onHit(pokemon, source, move) {
if (pokemon.side !== source.side && move.category === 'Physical') {
pokemon.volatiles['shelltrap'].gotHit = true;
let action = this.willMove(pokemon);
let action = this.queue.willMove(pokemon);
if (action) {
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
}
}
},
@ -17304,7 +17304,7 @@ let BattleMovedex = {
if (pokemon.hasItem('ironball') || pokemon.volatiles['ingrain'] || this.field.getPseudoWeather('gravity')) applies = false;
if (pokemon.removeVolatile('fly') || pokemon.removeVolatile('bounce')) {
applies = true;
this.cancelMove(pokemon);
this.queue.cancelMove(pokemon);
pokemon.removeVolatile('twoturnmove');
}
if (pokemon.volatiles['magnetrise']) {
@ -17320,7 +17320,7 @@ let BattleMovedex = {
},
onRestart(pokemon) {
if (pokemon.removeVolatile('fly') || pokemon.removeVolatile('bounce')) {
this.cancelMove(pokemon);
this.queue.cancelMove(pokemon);
this.add('-start', pokemon, 'Smack Down');
}
},
@ -17926,7 +17926,7 @@ let BattleMovedex = {
stallingMove: true,
volatileStatus: 'spikyshield',
onTryHit(target, source, move) {
return !!this.willAct() && this.runEvent('StallMove', target);
return !!this.queue.willAct() && this.runEvent('StallMove', target);
},
onHit(pokemon) {
pokemon.addVolatile('stall');
@ -18821,7 +18821,7 @@ let BattleMovedex = {
priority: 1,
flags: {contact: 1, protect: 1, mirror: 1},
onTry(source, target) {
let action = this.willMove(target);
let action = this.queue.willMove(target);
if (!action || action.choice !== 'move' || (action.move.category === 'Status' && action.move.id !== 'mefirst') || target.volatiles.mustrecharge) {
this.add('-fail', source);
this.attrLastMove('[still]');
@ -19388,7 +19388,7 @@ let BattleMovedex = {
effect: {
duration: 3,
onStart(target) {
if (target.activeTurns && !this.willMove(target)) {
if (target.activeTurns && !this.queue.willMove(target)) {
this.effectData.duration++;
}
this.add('-start', target, 'move: Taunt');
@ -20194,7 +20194,7 @@ let BattleMovedex = {
if (target.side.active.length === 2 && target.position === 1) {
// Curse Glitch
const action = this.willMove(target);
const action = this.queue.willMove(target);
if (action && action.move.id === 'curse') {
action.targetLoc = -1;
}
@ -20753,7 +20753,7 @@ let BattleMovedex = {
// @ts-ignore
if (action.pokemon.side === source.side && ['firepledge', 'grasspledge'].includes(action.move.id)) {
// @ts-ignore
this.prioritizeAction(action);
this.queue.prioritizeAction(action);
this.add('-waiting', source, action.pokemon);
return null;
}
@ -21009,7 +21009,7 @@ let BattleMovedex = {
flags: {snatch: 1},
sideCondition: 'wideguard',
onTryHitSide(side, source) {
return !!this.willAct();
return !!this.queue.willAct();
},
onHitSide(side, source) {
source.addVolatile('stall');

View File

@ -42,7 +42,7 @@ let BattleScripts = {
/* if (pokemon.moveThisTurn) {
// THIS IS PURELY A SANITY CHECK
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
// USE this.cancelMove INSTEAD
// USE this.queue.cancelMove INSTEAD
this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn);
this.clearActiveMove(true);
return;

367
sim/battle-queue.ts Normal file
View File

@ -0,0 +1,367 @@
/**
* Simulator Battle Action Queue
* Pokemon Showdown - http://pokemonshowdown.com/
*
* The action queue is the core of the battle simulation. A rough overview of
* the core battle loop:
*
* - chosen moves/switches are added to the action queue
* - the action queue is sorted in speed/priority order
* - we go through the action queue
* - repeat
*
* @license MIT
*/
import {Battle} from './battle';
/** A move action */
export interface MoveAction {
/** action type */
choice: 'move' | 'beforeTurnMove';
order: 3 | 5 | 200 | 201 | 199;
/** priority of the action (lower first) */
priority: number;
/** fractional priority of the action (lower first) */
fractionalPriority: number;
/** speed of pokemon using move (higher first if priority tie) */
speed: number;
/** the pokemon doing the move */
pokemon: Pokemon;
/** location of the target, relative to pokemon's side */
targetLoc: number;
/** original target pokemon, for target-tracking moves */
originalTarget: Pokemon;
/** a move to use (move action only) */
moveid: ID;
/** a move to use (move action only) */
move: Move;
/** true if megaing or ultra bursting */
mega: boolean | 'done';
/** if zmoving, the name of the zmove */
zmove?: string;
/** if dynamaxed, the name of the max move */
maxMove?: string;
/** effect that called the move (eg Instruct) if any */
sourceEffect?: Effect | null;
}
/** A switch action */
export interface SwitchAction {
/** action type */
choice: 'switch' | 'instaswitch';
order: 3 | 103;
/** priority of the action (lower first) */
priority: number;
/** speed of pokemon switching (higher first if priority tie) */
speed: number;
/** the pokemon doing the switch */
pokemon: Pokemon;
/** pokemon to switch to */
target: Pokemon;
/** effect that called the switch (eg U */
sourceEffect: Effect | null;
}
/** A Team Preview choice action */
export interface TeamAction {
/** action type */
choice: 'team';
/** priority of the action (lower first) */
priority: number;
/** unused for this action type */
speed: 1;
/** the pokemon switching */
pokemon: Pokemon;
/** new index */
index: number;
}
/** A generic action not done by a pokemon */
export interface FieldAction {
/** action type */
choice: 'start' | 'residual' | 'pass' | 'beforeTurn';
/** priority of the action (lower first) */
priority: number;
/** unused for this action type */
speed: 1;
/** unused for this action type */
pokemon: null;
}
/** A generic action done by a single pokemon */
export interface PokemonAction {
/** action type */
choice: 'megaEvo' | 'shift' | 'runPrimal' | 'runSwitch' | 'event' | 'runUnnerve' | 'runDynamax';
/** priority of the action (lower first) */
priority: number;
/** speed of pokemon doing action (higher first if priority tie) */
speed: number;
/** the pokemon doing action */
pokemon: Pokemon;
}
export type Action = MoveAction | SwitchAction | TeamAction | FieldAction | PokemonAction;
/**
* An ActionChoice is like an Action and has the same structure, but it doesn't need to be fully filled out.
*
* Any Action or ChosenAction qualifies as an ActionChoice.
*
* The `[k: string]: any` part is required so TypeScript won't warn about unnecessary properties.
*/
export interface ActionChoice {
choice: string;
[k: string]: any;
}
/**
* Kind of like a priority queue, although not sorted mid-turn in Gen 1-7.
*
* Sort order is documented in `BattleQueue.comparePriority`.
*/
export class BattleQueue extends Array<Action> {
battle: Battle;
constructor(battle: Battle) {
super();
this.battle = battle;
}
/**
* Takes an ActionChoice, and fills it out into a full Action object.
*
* Returns an array of Actions because some ActionChoices (like mega moves)
* resolve to two Actions (mega evolution + use move)
*/
resolveAction(action: ActionChoice, midTurn: boolean = false): Action[] {
if (!action) throw new Error(`Action not passed to resolveAction`);
if (action.choice === 'pass') return [];
const actions = [action];
if (!action.side && action.pokemon) action.side = action.pokemon.side;
if (!action.move && action.moveid) action.move = this.battle.dex.getActiveMove(action.moveid);
if (!action.order) {
const orders: {[choice: string]: number} = {
team: 1,
start: 2,
instaswitch: 3,
beforeTurn: 4,
beforeTurnMove: 5,
runUnnerve: 100,
runSwitch: 101,
runPrimal: 102,
switch: 103,
megaEvo: 104,
runDynamax: 105,
shift: 106,
// default is 200 (for moves)
residual: 300,
};
if (action.choice in orders) {
action.order = orders[action.choice];
} else {
action.order = 200;
if (!['move', 'event'].includes(action.choice)) {
throw new Error(`Unexpected orderless action ${action.choice}`);
}
}
}
if (!midTurn) {
if (action.choice === 'move') {
if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) {
actions.unshift(...this.resolveAction({
choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc,
}));
}
if (action.mega) {
// TODO: Check that the Pokémon is not affected by Sky Drop.
// (This is currently being done in `runMegaEvo`).
actions.unshift(...this.resolveAction({
choice: 'megaEvo',
pokemon: action.pokemon,
}));
}
if (action.maxMove && !action.pokemon.volatiles['dynamax']) {
actions.unshift(...this.resolveAction({
choice: 'runDynamax',
pokemon: action.pokemon,
}));
}
action.fractionalPriority = this.battle.runEvent('FractionalPriority', action.pokemon, null, action.move, 0);
} else if (['switch', 'instaswitch'].includes(action.choice)) {
if (typeof action.pokemon.switchFlag === 'string') {
action.sourceEffect = this.battle.dex.getMove(action.pokemon.switchFlag as ID) as any;
}
action.pokemon.switchFlag = false;
}
}
const deferPriority = this.battle.gen === 7 && action.mega && action.mega !== 'done';
if (action.move) {
let target = null;
action.move = this.battle.dex.getActiveMove(action.move);
if (!action.targetLoc) {
target = this.battle.getRandomTarget(action.pokemon, action.move);
// TODO: what actually happens here?
if (target) action.targetLoc = this.battle.getTargetLoc(target, action.pokemon);
}
action.originalTarget = this.battle.getAtLoc(action.pokemon, action.targetLoc);
}
if (!deferPriority) this.battle.getActionSpeed(action);
return actions as any;
}
/**
* Makes the passed action happen next (skipping speed order).
*/
prioritizeAction(action: MoveAction | SwitchAction, source?: Pokemon, sourceEffect?: Effect) {
for (const [i, curAction] of this.entries()) {
if (curAction === action) {
this.splice(i, 1);
break;
}
}
action.sourceEffect = sourceEffect;
action.order = 3;
this.unshift(action);
}
/**
* Changes a pokemon's action, and inserts its new action
* in priority order.
*
* You'd normally want the OverrideAction event (which doesn't
* change priority order).
*/
changeAction(pokemon: Pokemon, action: ActionChoice) {
this.cancelAction(pokemon);
if (!action.pokemon) action.pokemon = pokemon;
this.insertChoice(action);
}
addChoice(choices: ActionChoice | ActionChoice[]) {
if (!Array.isArray(choices)) choices = [choices];
for (const choice of choices) {
this.push(...this.resolveAction(choice));
}
}
willAct() {
for (const action of this) {
if (['move', 'switch', 'instaswitch', 'shift'].includes(action.choice)) {
return action;
}
}
return null;
}
willMove(pokemon: Pokemon) {
if (pokemon.fainted) return false;
for (const action of this) {
if (action.choice === 'move' && action.pokemon === pokemon) {
return action;
}
}
return null;
}
cancelAction(pokemon: Pokemon) {
const oldLength = this.length;
for (let i = 0; i < this.length; i++) {
if (this[i].pokemon === pokemon) {
this.splice(i, 1);
i--;
}
}
return this.length !== oldLength;
}
cancelMove(pokemon: Pokemon) {
for (const [i, action] of this.entries()) {
if (action.choice === 'move' && action.pokemon === pokemon) {
this.splice(i, 1);
return true;
}
}
return false;
}
willSwitch(pokemon: Pokemon) {
for (const action of this) {
if (['switch', 'instaswitch'].includes(action.choice) && action.pokemon === pokemon) {
return action;
}
}
return false;
}
/**
* Inserts the passed action into the action queue when it normally
* would have happened (sorting by priority/speed), without
* re-sorting the existing actions.
*/
insertChoice(choices: ActionChoice | ActionChoice[], midTurn: boolean = false) {
if (Array.isArray(choices)) {
for (const choice of choices) {
this.insertChoice(choice);
}
return;
}
const choice = choices;
if (choice.pokemon) {
choice.pokemon.updateSpeed();
}
const actions = this.resolveAction(choice, midTurn);
for (const [i, curAction] of this.entries()) {
if (BattleQueue.comparePriority(actions[0], curAction) < 0) {
this.splice(i, 0, ...actions);
return;
}
}
this.push(...actions);
}
clear() {
this.splice(0);
}
debug() {
return this.map(action =>
// @ts-ignore
`${action.order || ''}:${action.priority || ''}:${action.speed || ''}:${action.subOrder || ''} - ${action.choice}${action.pokemon ? ' ' + action.pokemon : ''}${action.move ? ' ' + action.move : ''}`
).join('\n') + '\n';
}
sort(): this;
sort(DO_NOT_USE_COMPARATORS?: never) {
if (DO_NOT_USE_COMPARATORS) throw new Error(`Battle queues can't be sorted with a custom comparator`);
// this.log.push('SORT ' + this.debugQueue());
this.battle.speedSort(this);
return this;
}
/**
* The default sort order for actions, but also event listeners.
*
* 1. Order, low to high (default last)
* 2. Priority, high to low (default 0)
* 3. Speed, high to low (default 0)
* 4. SubOrder, low to high (default 0)
* 5. AbilityOrder, switch-in order for abilities
*/
static comparePriority(a: AnyObject, b: AnyObject) {
return -((b.order || 4294967296) - (a.order || 4294967296)) ||
((b.priority || 0) - (a.priority || 0)) ||
((b.speed || 0) - (a.speed || 0)) ||
-((b.subOrder || 0) - (a.subOrder || 0)) ||
((a.thing && b.thing) ? -(b.thing.abilityOrder - a.thing.abilityOrder) : 0) ||
0;
}
}
export default BattleQueue;

View File

@ -12,6 +12,7 @@ import {Pokemon} from './pokemon';
import {PRNG, PRNGSeed} from './prng';
import {Side} from './side';
import {State} from './state';
import {BattleQueue, Action} from './battle-queue';
/** A Pokemon that has fainted. */
interface FaintedPokemon {
@ -88,7 +89,7 @@ export class Battle {
reportPercentages: boolean;
supportCancel: boolean;
queue: Actions.Action[];
queue: BattleQueue;
readonly faintQueue: FaintedPokemon[];
readonly log: string[];
@ -165,7 +166,7 @@ export class Battle {
this.reportPercentages = false;
this.supportCancel = false;
this.queue = [];
this.queue = new BattleQueue(this);
this.faintQueue = [];
this.log = [];
@ -1088,6 +1089,14 @@ export class Battle {
}
}
clearRequest() {
this.requestState = '';
for (const side of this.sides) {
side.activeRequest = null;
side.clearChoice();
}
}
getMaxTeamSize() {
const teamLengthData = this.format.teamLength;
return (teamLengthData && teamLengthData.battle) || 6;
@ -1231,7 +1240,7 @@ export class Battle {
const oldActive = side.active[pos];
if (oldActive) {
// if a pokemon is forced out by Whirlwind/etc or Eject Button/Pack, it no longer takes its turn
this.cancelAction(oldActive);
this.queue.cancelAction(oldActive);
let newMove = null;
if (this.gen === 4 && sourceEffect) {
@ -1262,8 +1271,8 @@ export class Battle {
}
this.add('switch', pokemon, pokemon.getDetails);
if (sourceEffect) this.log[this.log.length - 1] += `|[from]${sourceEffect.fullname}`;
this.insertQueue({pokemon, choice: 'runUnnerve'});
this.insertQueue({pokemon, choice: 'runSwitch'});
this.queue.insertChoice({choice: 'runUnnerve', pokemon});
this.queue.insertChoice({choice: 'runSwitch', pokemon});
}
canSwitch(side: Side) {
@ -1311,7 +1320,7 @@ export class Battle {
pokemon.position = pos;
side.pokemon[pokemon.position] = pokemon;
side.pokemon[oldActive.position] = oldActive;
this.cancelMove(oldActive);
this.queue.cancelMove(oldActive);
oldActive.clearVolatile();
}
side.active[pos] = pokemon;
@ -1332,7 +1341,7 @@ export class Battle {
this.singleEvent('Start', pokemon.getItem(), pokemon.itemData, pokemon);
}
} else {
this.insertQueue({pokemon, choice: 'runSwitch'});
this.queue.insertChoice({choice: 'runSwitch', pokemon});
}
return true;
}
@ -1365,6 +1374,7 @@ export class Battle {
nextTurn() {
this.turn++;
this.lastMoveThisTurn = null;
const trappedBySide: boolean[] = [];
const stalenessBySide: ('internal' | 'external' | undefined)[] = [];
for (const side of this.sides) {
@ -1546,12 +1556,12 @@ export class Battle {
}
start() {
// deserialized should use restart instead
// Deserialized games should use restart()
if (this.deserialized) return;
// need all players to start
if (!this.sides.every(side => !!side)) return;
if (!this.sides.every(side => !!side)) throw new Error(`Missing sides: ${this.sides}`);
if (this.started) return;
if (this.started) throw new Error(`Battle already started`);
this.started = true;
this.sides[1].foe = this.sides[0];
@ -1588,7 +1598,7 @@ export class Battle {
this.residualEvent('TeamPreview');
this.addToQueue({choice: 'start'});
this.queue.addChoice({choice: 'start'});
this.midTurn = true;
if (!this.requestState) this.go();
}
@ -1763,7 +1773,7 @@ export class Battle {
this.faintMessages(true);
if (this.gen <= 2) {
target.faint();
if (this.gen <= 1) this.queue = [];
if (this.gen <= 1) this.queue.clear();
}
}
}
@ -2325,16 +2335,16 @@ export class Battle {
if (this.gen <= 1) {
// in gen 1, fainting skips the rest of the turn
// residuals don't exist in gen 1
this.queue = [];
this.queue.clear();
} else if (this.gen <= 3 && this.gameType === 'singles') {
// in gen 3 or earlier, fainting in singles skips to residuals
for (const pokemon of this.getAllActive()) {
if (this.gen <= 2) {
// in gen 2, fainting skips moves only
this.cancelMove(pokemon);
this.queue.cancelMove(pokemon);
} else {
// in gen 3, fainting skips all moves and switches
this.cancelAction(pokemon);
this.queue.cancelAction(pokemon);
}
}
}
@ -2370,92 +2380,6 @@ export class Battle {
return false;
}
/**
* Takes an object describing an action, and fills it out into a full
* Action object.
*/
resolveAction(action: AnyObject, midTurn: boolean = false): Actions.Action {
if (!action) throw new Error(`Action not passed to resolveAction`);
if (!action.side && action.pokemon) action.side = action.pokemon.side;
if (!action.move && action.moveid) action.move = this.dex.getActiveMove(action.moveid);
if (!action.choice && action.move) action.choice = 'move';
if (!action.order) {
const orders: {[choice: string]: number} = {
team: 1,
start: 2,
instaswitch: 3,
beforeTurn: 4,
beforeTurnMove: 5,
runUnnerve: 100,
runSwitch: 101,
runPrimal: 102,
switch: 103,
megaEvo: 104,
runDynamax: 105,
shift: 106,
// default is 200 (for moves)
residual: 300,
};
if (action.choice in orders) {
action.order = orders[action.choice];
} else {
action.order = 200;
if (!['move', 'event'].includes(action.choice)) {
throw new Error(`Unexpected orderless action ${action.choice}`);
}
}
}
if (!midTurn) {
if (action.choice === 'move') {
if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) {
this.addToQueue({
choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc,
});
}
if (action.mega) {
// TODO: Check that the Pokémon is not affected by Sky Drop.
// (This is currently being done in `runMegaEvo`).
this.addToQueue({
choice: 'megaEvo',
pokemon: action.pokemon,
});
}
if (action.maxMove && !action.pokemon.volatiles['dynamax']) {
this.addToQueue({
choice: 'runDynamax',
pokemon: action.pokemon,
});
}
action.fractionalPriority = this.runEvent('FractionalPriority', action.pokemon, null, action.move, 0);
} else if (['switch', 'instaswitch'].includes(action.choice)) {
if (typeof action.pokemon.switchFlag === 'string') {
action.sourceEffect = this.dex.getMove(action.pokemon.switchFlag as ID) as any;
}
action.pokemon.switchFlag = false;
}
}
const deferPriority = this.gen === 7 && action.mega && action.mega !== 'done';
if (action.move) {
let target = null;
action.move = this.dex.getActiveMove(action.move);
if (!action.targetLoc) {
target = this.getRandomTarget(action.pokemon, action.move);
// TODO: what actually happens here?
if (target) action.targetLoc = this.getTargetLoc(target, action.pokemon);
}
action.originalTarget = this.getAtLoc(action.pokemon, action.targetLoc);
}
if (!deferPriority) this.getActionSpeed(action);
return action as any;
}
getActionSpeed(action: AnyObject) {
if (action.choice === 'move') {
let move = action.move;
@ -2493,122 +2417,7 @@ export class Battle {
}
}
/**
* Adds the action last in the queue. Mostly used before sortQueue.
*/
addToQueue(action: AnyObject | AnyObject[]) {
if (Array.isArray(action)) {
for (const curAction of action) {
this.addToQueue(curAction);
}
return;
}
if (action.choice === 'pass') return;
this.queue.push(this.resolveAction(action));
}
sortQueue() {
// this.log.push('SORT ' + this.debugQueue());
this.speedSort(this.queue);
}
debugQueue() {
return this.queue.map(action =>
// @ts-ignore
`${action.order || ''}:${action.priority || ''}:${action.speed || ''}:${action.subOrder || ''} - ${action.choice}${action.pokemon ? ' ' + action.pokemon : ''}${action.move ? ' ' + action.move : ''}`
).join('\n') + '\n';
}
/**
* Inserts the passed action into the action queue when it normally
* would have happened (sorting by priority/speed), without
* re-sorting the existing actions.
*/
insertQueue(chosenAction: AnyObject | AnyObject[], midTurn: boolean = false) {
if (Array.isArray(chosenAction)) {
for (const subAction of chosenAction) {
this.insertQueue(subAction);
}
return;
}
if (chosenAction.pokemon) {
chosenAction.pokemon.updateSpeed();
}
const action = this.resolveAction(chosenAction, midTurn);
for (const [i, curAction] of this.queue.entries()) {
if (this.comparePriority(action, curAction) < 0) {
this.queue.splice(i, 0, action);
return;
}
}
this.queue.push(action);
}
/**
* Makes the passed action happen next (skipping speed order).
*/
prioritizeAction(action: Actions.MoveAction | Actions.SwitchAction, source?: Pokemon, sourceEffect?: Effect) {
if (this.event && !sourceEffect) sourceEffect = this.effect;
for (const [i, curAction] of this.queue.entries()) {
if (curAction === action) {
this.queue.splice(i, 1);
break;
}
}
action.sourceEffect = sourceEffect;
action.order = 3;
this.queue.unshift(action);
}
willAct() {
for (const action of this.queue) {
if (['move', 'switch', 'instaswitch', 'shift'].includes(action.choice)) {
return action;
}
}
return null;
}
willMove(pokemon: Pokemon) {
if (pokemon.fainted) return false;
for (const action of this.queue) {
if (action.choice === 'move' && action.pokemon === pokemon) {
return action;
}
}
return null;
}
cancelAction(pokemon: Pokemon) {
const oldLength = this.queue.length;
this.queue = this.queue.filter(action =>
action.pokemon !== pokemon
);
return this.queue.length !== oldLength;
}
cancelMove(pokemon: Pokemon) {
for (const [i, action] of this.queue.entries()) {
if (action.choice === 'move' && action.pokemon === pokemon) {
this.queue.splice(i, 1);
return true;
}
}
return false;
}
willSwitch(pokemon: Pokemon) {
for (const action of this.queue) {
if (['switch', 'instaswitch'].includes(action.choice) && action.pokemon === pokemon) {
return action;
}
}
return false;
}
runAction(action: Actions.Action) {
runAction(action: Action) {
const pokemonOriginalHP = action.pokemon?.hp;
// returns whether or not we ended in a callback
switch (action.choice) {
@ -2800,13 +2609,13 @@ export class Battle {
} else if (action.choice === 'megaEvo' && this.gen === 7) {
this.eachEvent('Update');
// In Gen 7, the action order is recalculated for a Pokémon that mega evolves.
const moveIndex = this.queue.findIndex(queuedAction =>
queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move'
);
if (moveIndex >= 0) {
const moveAction = this.queue.splice(moveIndex, 1)[0] as Actions.MoveAction;
moveAction.mega = 'done';
this.insertQueue(moveAction, true);
for (const [i, queuedAction] of this.queue.entries()) {
if (queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move') {
this.queue.splice(i, 1);
queuedAction.mega = 'done';
this.queue.insertChoice(queuedAction, true);
break;
}
}
return false;
} else if (this.queue.length && this.queue[0].choice === 'instaswitch') {
@ -2850,7 +2659,7 @@ export class Battle {
for (const queueAction of this.queue) {
if (queueAction.pokemon) this.getActionSpeed(queueAction);
}
this.sortQueue();
this.queue.sort();
}
return false;
@ -2861,34 +2670,20 @@ export class Battle {
if (this.requestState) this.requestState = '';
if (!this.midTurn) {
this.queue.push(this.resolveAction({choice: 'residual'}));
this.queue.unshift(this.resolveAction({choice: 'beforeTurn'}));
this.queue.insertChoice({choice: 'beforeTurn'});
this.queue.addChoice({choice: 'residual'});
this.midTurn = true;
}
while (this.queue.length) {
const action = this.queue[0];
this.queue.shift();
const action = this.queue.shift()!;
this.runAction(action);
if (this.requestState || this.ended) return;
}
this.nextTurn();
this.midTurn = false;
this.queue = [];
}
/**
* Changes a pokemon's action, and inserts its new action
* in priority order.
*
* You'd normally want the OverrideAction event (which doesn't
* change priority order).
*/
changeAction(pokemon: Pokemon, action: AnyObject) {
this.cancelAction(pokemon);
if (!action.pokemon) action.pokemon = pokemon;
this.insertQueue(action);
this.queue.clear();
}
/**
@ -2927,8 +2722,8 @@ export class Battle {
commitDecisions() {
this.updateSpeed();
const oldQueue = this.queue;
this.queue = [];
const oldQueue = this.queue.slice();
this.queue.clear();
if (!this.allChoicesDone()) throw new Error("Not all choices done");
for (const side of this.sides) {
@ -2936,10 +2731,11 @@ export class Battle {
if (choice) this.inputLog.push(`>${side.id} ${choice}`);
}
for (const side of this.sides) {
this.addToQueue(side.choice.actions);
this.queue.addChoice(side.choice.actions);
}
this.clearRequest();
this.sortQueue();
this.queue.sort();
this.queue.push(...oldQueue);
this.requestState = '';
@ -3137,7 +2933,7 @@ export class Battle {
if (!didSomething) return;
this.inputLog.push(`>player ${slot} ` + JSON.stringify(options));
this.add('player', side.id, side.name, side.avatar, options.rating || '');
this.start();
if (this.sides.every(battleSide => !!battleSide)) this.start();
}
/** @deprecated */
@ -3408,7 +3204,8 @@ export class Battle {
delete action.pokemon;
}
this.queue = [];
this.queue.battle = null!;
this.queue = null!;
// in case the garbage collector really sucks, at least deallocate the log
// @ts-ignore - readonly
this.log = [];

View File

@ -1,4 +1,5 @@
type Battle = import('./battle').Battle
type Action = import('./battle-queue').Action
type Field = import('./field').Field
type ModdedDex = import('./dex').ModdedDex
type Pokemon = import('./pokemon').Pokemon
@ -1118,8 +1119,7 @@ interface BattleScriptsData {
hitStepTypeImmunity?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]
isAdjacent?: (this: Battle, pokemon1: Pokemon, pokemon2: Pokemon) => boolean
moveHit?: (this: Battle, target: Pokemon | null, pokemon: Pokemon, move: ActiveMove, moveData?: ActiveMove, isSecondary?: boolean, isSelf?: boolean) => number | undefined | false
resolveAction?: (this: Battle, action: AnyObject, midTurn?: boolean) => Actions.Action
runAction?: (this: Battle, action: Actions.Action) => void
runAction?: (this: Battle, action: Action) => void
runMegaEvo?: (this: Battle, pokemon: Pokemon) => boolean
runMove?: (this: Battle, moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, sourceEffect?: Effect | null, zMove?: string, externalMove?: boolean, maxMove?: string, originalTarget?: Pokemon) => void
runMoveEffects?: (this: Battle, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean) => SpreadMoveDamage
@ -1213,96 +1213,6 @@ interface PlayerOptions {
seed?: PRNGSeed;
}
namespace Actions {
/** A move action */
export interface MoveAction {
/** action type */
choice: 'move' | 'beforeTurnMove';
order: 3 | 5 | 200 | 201 | 199;
/** priority of the action (lower first) */
priority: number;
/** fractional priority of the action (lower first) */
fractionalPriority: number;
/** speed of pokemon using move (higher first if priority tie) */
speed: number;
/** the pokemon doing the move */
pokemon: Pokemon;
/** location of the target, relative to pokemon's side */
targetLoc: number;
/** original target pokemon, for target-tracking moves */
originalTarget: Pokemon;
/** a move to use (move action only) */
moveid: ID
/** a move to use (move action only) */
move: Move;
/** true if megaing or ultra bursting */
mega: boolean | 'done';
/** if zmoving, the name of the zmove */
zmove?: string;
/** if dynamaxed, the name of the max move */
maxMove?: string;
/** effect that called the move (eg Instruct) if any */
sourceEffect?: Effect | null;
}
/** A switch action */
export interface SwitchAction {
/** action type */
choice: 'switch' | 'instaswitch';
order: 3 | 103;
/** priority of the action (lower first) */
priority: number;
/** speed of pokemon switching (higher first if priority tie) */
speed: number;
/** the pokemon doing the switch */
pokemon: Pokemon;
/** pokemon to switch to */
target: Pokemon;
/** effect that called the switch (eg U */
sourceEffect: Effect | null;
}
/** A Team Preview choice action */
export interface TeamAction {
/** action type */
choice: 'team';
/** priority of the action (lower first) */
priority: number;
/** unused for this action type */
speed: 1;
/** the pokemon switching */
pokemon: Pokemon;
/** new index */
index: number;
}
/** A generic action not done by a pokemon */
export interface FieldAction {
/** action type */
choice: 'start' | 'residual' | 'pass' | 'beforeTurn';
/** priority of the action (lower first) */
priority: number;
/** unused for this action type */
speed: 1;
/** unused for this action type */
pokemon: null;
}
/** A generic action done by a single pokemon */
export interface PokemonAction {
/** action type */
choice: 'megaEvo' | 'shift' | 'runPrimal' | 'runSwitch' | 'event' | 'runUnnerve' | 'runDynamax';
/** priority of the action (lower first) */
priority: number;
/** speed of pokemon doing action (higher first if priority tie) */
speed: number;
/** the pokemon doing action */
pokemon: Pokemon;
}
export type Action = MoveAction | SwitchAction | TeamAction | FieldAction | PokemonAction;
}
namespace RandomTeamsTypes {
export interface TeamDetails {
megaStone?: number;

View File

@ -9,7 +9,7 @@ import {Pokemon} from './pokemon';
import {State} from './state';
/** A single action that can be chosen. */
interface ChosenAction {
export interface ChosenAction {
choice: 'move' | 'switch' | 'instaswitch' | 'team' | 'shift' | 'pass'; // action type
pokemon?: Pokemon; // the pokemon doing the action
targetLoc?: number; // relative location of the target to pokemon (move action only)

View File

@ -32,12 +32,11 @@ type Referable = Battle | Field | Side | Pokemon | PureEffect | Ability | Item |
// keys which we skip during default (de)serialization and (the keys which)
// need special treatment from these sets are then handled manually.
// Battle inherits from Dex, but all of Dex's fields are redundant - we can
// just recreate the Dex from the format.
const BATTLE = new Set([
'dex', 'gen', 'ruleTable', 'id', 'log', 'inherit', 'format',
'zMoveTable', 'teamGenerator', 'NOT_FAIL', 'FAIL', 'SILENT_FAIL',
'field', 'sides', 'prng', 'hints', 'deserialized', 'maxMoveTable',
'queue',
]);
const FIELD = new Set(['id', 'battle']);
const SIDE = new Set(['battle', 'team', 'pokemon', 'choice', 'activeRequest']);
@ -69,6 +68,7 @@ export const State = new class {
// We treat log specially because we only set it back on Battle after everything
// else has been deserialized to avoid anything accidentally `add`-ing to it.
state.log = battle.log;
state.queue = this.serializeWithRefs([...battle.queue], battle);
state.formatid = battle.format.id;
return state;
}
@ -145,6 +145,8 @@ export const State = new class {
}
}
battle.prng = new PRNG(state.prng);
const queue = this.deserializeWithRefs(state.queue, battle);
battle.queue.push(...queue);
// @ts-ignore - readonly
battle.hints = new Set(state.hints);
// @ts-ignore - readonly