From 31e23b39d3ac0b2bd51e9ca4dcd09ce55ddd099b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Bastos=20Dias?=
<80102738+andrebastosdias@users.noreply.github.com>
Date: Thu, 19 Mar 2026 20:38:47 +0000
Subject: [PATCH] Track Gen 3 Pressure (#2607)
---
.../src/battle-tooltips.ts | 17 +++--
play.pokemonshowdown.com/src/battle.ts | 75 ++++++++++++++-----
2 files changed, 69 insertions(+), 23 deletions(-)
diff --git a/play.pokemonshowdown.com/src/battle-tooltips.ts b/play.pokemonshowdown.com/src/battle-tooltips.ts
index 3d9d51320..c1a7a4cee 100644
--- a/play.pokemonshowdown.com/src/battle-tooltips.ts
+++ b/play.pokemonshowdown.com/src/battle-tooltips.ts
@@ -8,7 +8,7 @@
* @license MIT
*/
-import { Pokemon, type Battle, type ServerPokemon } from "./battle";
+import { Pokemon, type Battle, type PPState, type ServerPokemon } from "./battle";
import { Dex, type ModdedDex, toID, type ID } from "./battle-dex";
import type { BattleScene } from "./battle-animations";
import { BattleLog } from "./battle-log";
@@ -993,8 +993,8 @@ export class BattleTooltips {
}).length > 4) {
text += `(More than 4 moves is usually a sign of Illusion Zoroark/Zorua.) `;
}
- if (this.battle.gen === 3) {
- text += `(Pressure is not visible in Gen 3, so in certain situations, more PP may have been lost than shown here.) `;
+ if (this.battle.gen === 3 && clientPokemon.moveTrack.some(([_, pp]) => typeof pp !== 'number')) {
+ text += `(Pressure is not visible in Gen 3, so in certain situations, the exact amount of PP used may be unknown.) `;
}
if (this.pokemonHasClones(clientPokemon)) {
text += `(Your opponent has two indistinguishable Pokémon, making it impossible for you to tell which one has which moves/ability/item.) `;
@@ -1472,7 +1472,7 @@ export class BattleTooltips {
return buf;
}
- getPPUseText(moveTrackRow: [string, number], showKnown?: boolean) {
+ getPPUseText(moveTrackRow: [string, PPState], showKnown?: boolean) {
let [moveName, ppUsed] = moveTrackRow;
let move;
let maxpp;
@@ -1490,7 +1490,11 @@ export class BattleTooltips {
return `${bullet} ${move.name} (0/${maxpp})`;
}
if (ppUsed || moveName.startsWith('*')) {
- return `${bullet} ${move.name} (${maxpp - ppUsed}/${maxpp})`;
+ if (typeof ppUsed === 'number') {
+ return `${bullet} ${move.name} (${maxpp - ppUsed}/${maxpp})`;
+ } else {
+ return `${bullet} ${move.name} (${maxpp - ppUsed[0]}/${maxpp} to ${maxpp - ppUsed[1]}/${maxpp})`;
+ }
}
return `${bullet} ${move.name} ${showKnown ? ' (revealed)' : ''}`;
}
@@ -1988,7 +1992,8 @@ export class BattleTooltips {
value.set(20 + 20 * boostCount);
}
if (move.id === 'trumpcard') {
- const ppLeft = 5 - this.ppUsed(move, pokemon);
+ const pp = this.ppUsed(move, pokemon);
+ const ppLeft = 5 - (typeof pp === 'number' ? pp : pp[1]);
let basePower = 40;
if (ppLeft === 1) basePower = 200;
else if (ppLeft === 2) basePower = 80;
diff --git a/play.pokemonshowdown.com/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts
index 27f2eb072..f80495c83 100644
--- a/play.pokemonshowdown.com/src/battle.ts
+++ b/play.pokemonshowdown.com/src/battle.ts
@@ -40,6 +40,7 @@ declare const app: { user: AnyObject, rooms: AnyObject, ignore?: AnyObject } | u
export type EffectState = any[] & { 0: ID };
export type WeatherState = [name: string, minTimeLeft: number, maxTimeLeft: number];
export type HPColor = 'r' | 'y' | 'g';
+export type PPState = number | [number, number];
export class Pokemon implements PokemonDetails, PokemonHealth {
name = '';
@@ -106,7 +107,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
lastMove = '';
/** [[moveName, ppUsed]] */
- moveTrack: [string, number][] = [];
+ moveTrack: [string, PPState][] = [];
statusData = { sleepTurns: 0, toxicTurns: 0 };
timesAttacked = 0;
@@ -340,7 +341,37 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
this.clearMovestatuses();
this.side.battle.scene.clearEffects(this);
}
- rememberMove(moveName: string, pp = 1, recursionSource?: string) {
+ private mergePP(entry: [string, PPState], pp: PPState): PPState {
+ let ppUsed = entry[1];
+ if (typeof ppUsed === 'number') {
+ if (typeof pp === 'number') {
+ ppUsed += pp;
+ } else {
+ ppUsed = [ppUsed + pp[0], ppUsed + pp[1]];
+ }
+ } else {
+ if (typeof pp === 'number') {
+ ppUsed[0] += pp;
+ ppUsed[1] += pp;
+ } else {
+ ppUsed[0] += pp[0];
+ ppUsed[1] += pp[1];
+ }
+ }
+ if (typeof ppUsed === 'number') {
+ if (ppUsed < 0) ppUsed = 0;
+ } else {
+ if (ppUsed[0] < 0) ppUsed[0] = 0;
+ if (ppUsed[1] < 0) ppUsed[1] = 0;
+ const move = this.side.battle.dex.moves.get(entry[0]);
+ const maxpp = (move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5);
+ if (ppUsed[0] > maxpp) ppUsed[0] = maxpp;
+ if (ppUsed[0] < ppUsed[1]) ppUsed[0] = ppUsed[1];
+ if (ppUsed[0] === ppUsed[1]) ppUsed = ppUsed[0];
+ }
+ return ppUsed;
+ }
+ rememberMove(moveName: string, pp: PPState = 1, recursionSource?: string) {
if (recursionSource === this.ident) return;
moveName = Dex.moves.get(moveName).name;
if (moveName.startsWith('*')) return;
@@ -353,8 +384,7 @@ export class Pokemon implements PokemonDetails, PokemonHealth {
}
for (const entry of this.moveTrack) {
if (moveName === entry[0]) {
- entry[1] += pp;
- if (entry[1] < 0) entry[1] = 0;
+ entry[1] = this.mergePP(entry, pp);
return;
}
}
@@ -1537,8 +1567,8 @@ export class Battle {
}
}
}
- let pp = 1;
- if (this.abilityActive('Pressure') && move.id !== 'stickyweb') {
+ let pp: PPState = callerMoveForPressure ? 0 : 1; // 1 pp was already deducted from using the move itself
+ if ((this.abilityActive('Pressure') || this.gen === 3) && move.id !== 'stickyweb') {
const foeTargets = [];
const moveTarget = move.pressureTarget;
@@ -1559,18 +1589,9 @@ export class Battle {
} else if (target && target.side !== pokemon.side) {
foeTargets.push(target);
}
-
- for (const foe of foeTargets) {
- if (foe && !foe.fainted && foe.effectiveAbility() === 'Pressure') {
- pp += 1;
- }
- }
- }
- if (!callerMoveForPressure) {
- pokemon.rememberMove(moveName, pp);
- } else {
- pokemon.rememberMove(callerMoveForPressure.name, pp - 1); // 1 pp was already deducted from using the move itself
+ pp = this.getPressurePP(pp, foeTargets.filter(foe => foe && !foe.fainted) as Pokemon[]);
}
+ pokemon.rememberMove(callerMoveForPressure ? callerMoveForPressure.name : moveName, pp);
}
pokemon.lastMove = move.id;
this.lastMove = move.id;
@@ -1578,6 +1599,26 @@ export class Battle {
pokemon.side.wisher = pokemon;
}
}
+ private getPressurePP(pp: PPState, foes: Pokemon[]) {
+ for (const foe of foes) {
+ const abilities = Object.values(this.dex.species.get(foe.speciesForme).abilities);
+ const canHavePressure = this.gen === 3 && abilities.includes('Pressure');
+ if (foe.effectiveAbility() === 'Pressure' || (canHavePressure && abilities.length === 1)) {
+ if (typeof pp === 'number') {
+ pp += 1;
+ } else {
+ pp[0] += 1;
+ pp[1] += 1;
+ }
+ } else if (canHavePressure) {
+ if (typeof pp === 'number') {
+ pp = [pp, pp];
+ }
+ pp[0] += 1;
+ }
+ }
+ return pp;
+ }
animateMove(pokemon: Pokemon, move: Dex.Move, target: Pokemon | null, kwArgs: KWArgs) {
this.activeMoveIsSpread = kwArgs.spread;
if (this.seeking !== null || kwArgs.still) return;