diff --git a/data/abilities.js b/data/abilities.js index 24a28fa8d4..368ee89cb6 100644 --- a/data/abilities.js +++ b/data/abilities.js @@ -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'); } }, diff --git a/data/items.js b/data/items.js index 9a09913d16..c5e4a54a38 100644 --- a/data/items.js +++ b/data/items.js @@ -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; } diff --git a/data/mods/gen1/moves.js b/data/mods/gen1/moves.js index b2737a80ed..bbb37f6965 100644 --- a/data/mods/gen1/moves.js +++ b/data/mods/gen1/moves.js @@ -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; diff --git a/data/mods/gen2/moves.js b/data/mods/gen2/moves.js index 08ac9a2a2c..88c04ca974 100644 --- a/data/mods/gen2/moves.js +++ b/data/mods/gen2/moves.js @@ -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++; } }, diff --git a/data/mods/gen2/scripts.js b/data/mods/gen2/scripts.js index 998d294985..f9f28a5748 100644 --- a/data/mods/gen2/scripts.js +++ b/data/mods/gen2/scripts.js @@ -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; diff --git a/data/mods/gen2/statuses.js b/data/mods/gen2/statuses.js index c00ec2267f..b6c1cc20fd 100644 --- a/data/mods/gen2/statuses.js +++ b/data/mods/gen2/statuses.js @@ -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}); } }, }, diff --git a/data/mods/gen3/moves.js b/data/mods/gen3/moves.js index 6adc414726..751a617b29 100644 --- a/data/mods/gen3/moves.js +++ b/data/mods/gen3/moves.js @@ -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++; } }, diff --git a/data/mods/gen4/items.js b/data/mods/gen4/items.js index 8e083fae9c..7650f96c18 100644 --- a/data/mods/gen4/items.js +++ b/data/mods/gen4/items.js @@ -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); } diff --git a/data/mods/gen4/moves.js b/data/mods/gen4/moves.js index a7b75b9d2e..224d42c9ab 100644 --- a/data/mods/gen4/moves.js +++ b/data/mods/gen4/moves.js @@ -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; diff --git a/data/mods/gen5/moves.js b/data/mods/gen5/moves.js index 1b2fd75ac7..2fdbaba128 100644 --- a/data/mods/gen5/moves.js +++ b/data/mods/gen5/moves.js @@ -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'); diff --git a/data/mods/gen6/moves.js b/data/mods/gen6/moves.js index 2c68f4e20a..ff66f99dee 100644 --- a/data/mods/gen6/moves.js +++ b/data/mods/gen6/moves.js @@ -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++; } }, diff --git a/data/mods/gen7/moves.js b/data/mods/gen7/moves.js index b89c3d57a7..077f99b0a7 100644 --- a/data/mods/gen7/moves.js +++ b/data/mods/gen7/moves.js @@ -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); diff --git a/data/mods/gennext/items.js b/data/mods/gennext/items.js index c342fd6441..15580f0f82 100644 --- a/data/mods/gennext/items.js +++ b/data/mods/gennext/items.js @@ -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; } diff --git a/data/mods/gennext/moves.js b/data/mods/gennext/moves.js index ac3d0a6c25..1870827fa4 100644 --- a/data/mods/gennext/moves.js +++ b/data/mods/gennext/moves.js @@ -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, diff --git a/data/mods/ssb/moves.js b/data/mods/ssb/moves.js index c58e29fbd9..9571f85737 100644 --- a/data/mods/ssb/moves.js +++ b/data/mods/ssb/moves.js @@ -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) { diff --git a/data/mods/ssb/scripts.js b/data/mods/ssb/scripts.js index 6d929129dd..c1d4763f49 100644 --- a/data/mods/ssb/scripts.js +++ b/data/mods/ssb/scripts.js @@ -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; diff --git a/data/moves.js b/data/moves.js index a5d78d2527..5a92036441 100644 --- a/data/moves.js +++ b/data/moves.js @@ -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'); diff --git a/data/scripts.js b/data/scripts.js index af68d58e75..5b6a952d98 100644 --- a/data/scripts.js +++ b/data/scripts.js @@ -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; diff --git a/sim/battle-queue.ts b/sim/battle-queue.ts new file mode 100644 index 0000000000..13b9fa3873 --- /dev/null +++ b/sim/battle-queue.ts @@ -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 { + 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; diff --git a/sim/battle.ts b/sim/battle.ts index 96378c3166..b6d20d29bc 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -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 = []; diff --git a/sim/global-types.ts b/sim/global-types.ts index 382b9ff31d..44ddf2dfa8 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -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; diff --git a/sim/side.ts b/sim/side.ts index e2d9fe98bd..7724024fef 100644 --- a/sim/side.ts +++ b/sim/side.ts @@ -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) diff --git a/sim/state.ts b/sim/state.ts index 72cca1b473..42cdea1057 100644 --- a/sim/state.ts +++ b/sim/state.ts @@ -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