[Refactor] Align ability display with mainline (#5267)

* Stop ShowAbilityPhase from ending until the bar has popped out

* Remove ability bar hiding from messagePhase

* Remove abilityBar reference from base Phase class

* Add HideAbilityPhase to hide ability bar after effects

* Add willSucceed to ability attrs

* Update AbAttrs and PostInitAbAttrs

* Update PreDefendAbAttrs

* Update postDefend, postMoveUsed, StatStage, postSetStatus, and PostDamage

* Update preAttack and fieldStat

* Partially implement postAttack

* Finish PostAttack

* Update PostSummon

* Update PreSwitchOut

* Update preStatStageChange

* Update PostStatStageChange, PreSetStatus, PreApplyBattlerTag

* Update postTurn and preWeatherEffect

* Update postWeatherChange

* Update postWeatherChange

* Update PostTerrainChange

* Update CheckTrapped and PostBattle

* Update postFaint

* Update PostItemLost

* Bug fixes from test cases

* Fix intimidate display

* Stop trace from displaying itself

* Rename to canApply

* Fix ability displays using getTriggerMessage

* Ensure abilities which are mistakenly shown are still hidden

* Fix ability bar showing the wrong ability with imposter

* Add canApply for imposter

* Update abilities using promises and `trySet...` functions

* Committing overrides changes is bad

* Document apply and canApply

* Update PreLeaveFieldAbAttr

* Remove boolean return type apply functions

* Remove redundant  assignment

* Remove ability display from abilities that shouldn't have it

* Move queueAbilityDisplay to battlescene

* Remove unused shown variable

* Minor changes

* Fix using id instead of battlerindex in queueAbilityDisplay

* Fix PostBattleInitFormChangeAbAttr displaying

* Prevent crashes in case an ability for a pokemon not on the field is shown

* Stop more abilities from displaying

* Move enemy ability bar to the right side

* Automatically reload bar if shown while already out, fix specific abilities

* Remove duplicate call to clearPhaseQueueSplice

* Remove ShowAbilityPhase import from ability.ts

* Update PostDefendTypeChangeAbAttr to use PokemonType

* Update PostSummonAddArenaTagAbAttr

* Minor changes
This commit is contained in:
Dean 2025-03-15 19:51:02 -07:00 committed by GitHub
parent 7aa5649aa8
commit 1d7f916240
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1729 additions and 1575 deletions

View File

@ -168,6 +168,8 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { initGlobalScene } from "#app/global-scene"; import { initGlobalScene } from "#app/global-scene";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -2904,6 +2906,21 @@ export default class BattleScene extends SceneBase {
} }
} }
/**
* Queues an ability bar flyout phase
* @param pokemon The pokemon who has the ability
* @param passive Whether the ability is a passive
* @param show Whether to show or hide the bar
*/
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase(
show
? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive)
: new HideAbilityPhase(pokemon.getBattlerIndex(), passive),
);
this.clearPhaseQueueSplice();
}
/** /**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/ */
@ -3133,6 +3150,41 @@ export default class BattleScene extends SceneBase {
return false; return false;
} }
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
const mod = itemModifier.clone() as PokemonHeldItemModifier;
const source = mod.pokemonId ? mod.getPokemon() : null;
const cancelled = new Utils.BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs(BlockItemTheftAbAttr, source, cancelled);
}
if (cancelled.value) {
return false;
}
const matchingModifier = this.findModifier(
m => m instanceof PokemonHeldItemModifier && m.matchType(mod) && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier;
if (matchingModifier) {
const maxStackCount = matchingModifier.getMaxStackCount();
if (matchingModifier.stackCount >= maxStackCount) {
return false;
}
const countTaken = Math.min(transferQuantity, mod.stackCount, maxStackCount - matchingModifier.stackCount);
mod.stackCount -= countTaken;
} else {
const countTaken = Math.min(transferQuantity, mod.stackCount);
mod.stackCount -= countTaken;
}
const removeOld = mod.stackCount === 0;
return !removeOld || !source || this.hasModifier(itemModifier, !source.isPlayer());
}
removePartyMemberModifiers(partyMemberIndex: number): Promise<void> { removePartyMemberModifiers(partyMemberIndex: number): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const pokemonId = this.getPlayerParty()[partyMemberIndex].id; const pokemonId = this.getPlayerParty()[partyMemberIndex].id;
@ -3290,6 +3342,11 @@ export default class BattleScene extends SceneBase {
}); });
} }
hasModifier(modifier: PersistentModifier, enemy = false): boolean {
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
return modifiers.indexOf(modifier) > -1;
}
/** /**
* Removes a currently owned item. If the item is stacked, the entire item stack * Removes a currently owned item. If the item is stacked, the entire item stack
* gets removed. This function does NOT apply in-battle effects, such as Unburden. * gets removed. This function does NOT apply in-battle effects, such as Unburden.

File diff suppressed because it is too large Load Diff

View File

@ -303,6 +303,11 @@ export class Arena {
return true; return true;
} }
/** Returns weather or not the weather can be changed to {@linkcode weather} */
canSetWeather(weather: WeatherType): boolean {
return !(this.weather?.weatherType === (weather || undefined));
}
/** /**
* Attempts to set a new weather to the battle * Attempts to set a new weather to the battle
* @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set * @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set
@ -314,7 +319,7 @@ export class Arena {
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE); return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
} }
if (this.weather?.weatherType === (weather || undefined)) { if (!this.canSetWeather(weather)) {
return false; return false;
} }
@ -388,8 +393,13 @@ export class Arena {
}); });
} }
/** Returns whether or not the terrain can be set to {@linkcode terrain} */
canSetTerrain(terrain: TerrainType): boolean {
return !(this.terrain?.terrainType === (terrain || undefined));
}
trySetTerrain(terrain: TerrainType, hasPokemonSource: boolean, ignoreAnim = false): boolean { trySetTerrain(terrain: TerrainType, hasPokemonSource: boolean, ignoreAnim = false): boolean {
if (this.terrain?.terrainType === (terrain || undefined)) { if (!this.canSetTerrain(terrain)) {
return false; return false;
} }

View File

@ -79,6 +79,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("icon_owned", "ui"); this.loadImage("icon_owned", "ui");
this.loadImage("icon_egg_move", "ui"); this.loadImage("icon_egg_move", "ui");
this.loadImage("ability_bar_left", "ui"); this.loadImage("ability_bar_left", "ui");
this.loadImage("ability_bar_right", "ui");
this.loadImage("bgm_bar", "ui"); this.loadImage("bgm_bar", "ui");
this.loadImage("party_exp_bar", "ui"); this.loadImage("party_exp_bar", "ui");
this.loadImage("achv_bar", "ui"); this.loadImage("achv_bar", "ui");

View File

@ -1,11 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
export class Phase { export class Phase {
start() { start() {}
if (globalScene.abilityBar.shown) {
globalScene.abilityBar.resetAutoHideTimer();
}
}
end() { end() {
globalScene.shiftPhase(); globalScene.shiftPhase();

View File

@ -0,0 +1,27 @@
import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#app/battle";
import { PokemonPhase } from "./pokemon-phase";
export class HideAbilityPhase extends PokemonPhase {
private passive: boolean;
constructor(battlerIndex: BattlerIndex, passive = false) {
super(battlerIndex);
this.passive = passive;
}
start() {
super.start();
const pokemon = this.getPokemon();
if (pokemon) {
globalScene.abilityBar.hide().then(() => {
this.end();
});
} else {
this.end();
}
}
}

View File

@ -61,12 +61,4 @@ export class MessagePhase extends Phase {
); );
} }
} }
end() {
if (globalScene.abilityBar.shown) {
globalScene.abilityBar.hide();
}
super.end();
}
} }

View File

@ -5,6 +5,8 @@ import { EFFECTIVE_STATS, BATTLE_STATS } from "#enums/stat";
import { PokemonMove } from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next";
/** /**
* Transforms a Pokemon into another Pokemon on the field. * Transforms a Pokemon into another Pokemon on the field.
@ -62,6 +64,13 @@ export class PokemonTransformPhase extends PokemonPhase {
globalScene.playSound("battle_anims/PRSFX- Transform"); globalScene.playSound("battle_anims/PRSFX- Transform");
} }
globalScene.queueMessage(
i18next.t("abilityTriggers:postSummonTransform", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
targetName: target.name,
}),
);
promises.push( promises.push(
user.loadAssets(false).then(() => { user.loadAssets(false).then(() => {
user.playAnim(); user.playAnim();

View File

@ -1,22 +1,47 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#app/battle"; import type { BattlerIndex } from "#app/battle";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { getPokemonNameWithAffix } from "#app/messages";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
export class ShowAbilityPhase extends PokemonPhase { export class ShowAbilityPhase extends PokemonPhase {
private passive: boolean; private passive: boolean;
private pokemonName: string;
private abilityName: string;
private pokemonOnField: boolean;
constructor(battlerIndex: BattlerIndex, passive = false) { constructor(battlerIndex: BattlerIndex, passive = false) {
super(battlerIndex); super(battlerIndex);
this.passive = passive; this.passive = passive;
const pokemon = this.getPokemon();
if (pokemon) {
// Set these now as the pokemon object may change before the queued phase is run
this.pokemonName = getPokemonNameWithAffix(pokemon);
this.abilityName = (passive ? this.getPokemon().getPassiveAbility() : this.getPokemon().getAbility()).name;
this.pokemonOnField = true;
} else {
this.pokemonOnField = false;
}
} }
start() { start() {
super.start(); super.start();
if (!this.pokemonOnField || !this.getPokemon()) {
return this.end();
}
// If the bar is already out, hide it before showing the new one
if (globalScene.abilityBar.isVisible()) {
globalScene.unshiftPhase(new HideAbilityPhase(this.battlerIndex, this.passive));
globalScene.unshiftPhase(new ShowAbilityPhase(this.battlerIndex, this.passive));
return this.end();
}
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon) {
if (!pokemon.isPlayer()) { if (!pokemon.isPlayer()) {
/** If its an enemy pokemon, list it as last enemy to use ability or move */ /** If its an enemy pokemon, list it as last enemy to use ability or move */
globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2; globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2;
@ -24,13 +49,12 @@ export class ShowAbilityPhase extends PokemonPhase {
globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2; globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2;
} }
globalScene.abilityBar.showAbility(pokemon, this.passive); globalScene.abilityBar.showAbility(this.pokemonName, this.abilityName, this.passive, this.player).then(() => {
if (pokemon?.battleData) { if (pokemon?.battleData) {
pokemon.battleData.abilityRevealed = true; pokemon.battleData.abilityRevealed = true;
} }
}
this.end(); this.end();
});
} }
} }

View File

@ -1,31 +1,33 @@
import { getPokemonNameWithAffix } from "#app/messages";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type Pokemon from "../field/pokemon";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "./text";
import i18next from "i18next"; import i18next from "i18next";
const hiddenX = -118; const barWidth = 118;
const shownX = 0; const screenLeft = 0;
const baseY = -116; const baseY = -116;
export default class AbilityBar extends Phaser.GameObjects.Container { export default class AbilityBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image; private abilityBars: Phaser.GameObjects.Image[];
private abilityBarText: Phaser.GameObjects.Text; private abilityBarText: Phaser.GameObjects.Text;
private player: boolean;
private tween: Phaser.Tweens.Tween | null; private screenRight: number; // hold screenRight in case size changes between show and hide
private autoHideTimer: NodeJS.Timeout | null; private shown: boolean;
public shown: boolean;
constructor() { constructor() {
super(globalScene, hiddenX, baseY); super(globalScene, barWidth, baseY);
this.abilityBars = [];
this.player = true;
this.shown = false;
} }
setup(): void { setup(): void {
this.bg = globalScene.add.image(0, 0, "ability_bar_left"); for (const key of ["ability_bar_right", "ability_bar_left"]) {
this.bg.setOrigin(0, 0); const bar = globalScene.add.image(0, 0, key);
bar.setOrigin(0, 0);
this.add(this.bg); bar.setVisible(false);
this.add(bar);
this.abilityBars.push(bar);
}
this.abilityBarText = addTextObject(15, 3, "", TextStyle.MESSAGE, { this.abilityBarText = addTextObject(15, 3, "", TextStyle.MESSAGE, {
fontSize: "72px", fontSize: "72px",
@ -33,72 +35,80 @@ export default class AbilityBar extends Phaser.GameObjects.Container {
this.abilityBarText.setOrigin(0, 0); this.abilityBarText.setOrigin(0, 0);
this.abilityBarText.setWordWrapWidth(600, true); this.abilityBarText.setWordWrapWidth(600, true);
this.add(this.abilityBarText); this.add(this.abilityBarText);
this.bringToTop(this.abilityBarText);
this.setVisible(false); this.setVisible(false);
this.shown = false; this.setX(-barWidth); // start hidden (right edge of bar at x=0)
} }
showAbility(pokemon: Pokemon, passive = false): void { public override setVisible(value: boolean): this {
this.abilityBarText.setText( this.abilityBars[+this.player].setVisible(value);
`${i18next.t("fightUiHandler:abilityFlyInText", { pokemonName: getPokemonNameWithAffix(pokemon), passive: passive ? i18next.t("fightUiHandler:passive") : "", abilityName: !passive ? pokemon.getAbility().name : pokemon.getPassiveAbility().name })}`, this.shown = value;
); return this;
if (this.shown) {
return;
} }
public async startTween(config: any, text?: string): Promise<void> {
this.setVisible(true);
if (text) {
this.abilityBarText.setText(text);
}
return new Promise(resolve => {
globalScene.tweens.add({
...config,
onComplete: () => {
if (config.onComplete) {
config.onComplete();
}
resolve();
},
});
});
}
public async showAbility(pokemonName: string, abilityName: string, passive = false, player = true): Promise<void> {
const text = `${i18next.t("fightUiHandler:abilityFlyInText", { pokemonName: pokemonName, passive: passive ? i18next.t("fightUiHandler:passive") : "", abilityName: abilityName })}`;
this.screenRight = globalScene.scaledCanvas.width;
if (player !== this.player) {
// Move the bar if it has changed from the player to enemy side (or vice versa)
this.setX(player ? -barWidth : this.screenRight);
this.player = player;
}
globalScene.fieldUI.bringToTop(this); globalScene.fieldUI.bringToTop(this);
this.y = baseY + (globalScene.currentBattle.double ? 14 : 0); let y = baseY;
this.tween = globalScene.tweens.add({ if (this.player) {
y += globalScene.currentBattle.double ? 14 : 0;
} else {
y -= globalScene.currentBattle.double ? 28 : 14;
}
this.setY(y);
return this.startTween(
{
targets: this, targets: this,
x: shownX, x: this.player ? screenLeft : this.screenRight - barWidth,
duration: 500, duration: 500,
ease: "Sine.easeOut", ease: "Sine.easeOut",
onComplete: () => { hold: 1000,
this.tween = null;
this.resetAutoHideTimer();
}, },
}); text,
);
this.setVisible(true);
this.shown = true;
} }
hide(): void { public async hide(): Promise<void> {
if (!this.shown) { return this.startTween({
return;
}
if (this.autoHideTimer) {
clearInterval(this.autoHideTimer);
}
if (this.tween) {
this.tween.stop();
}
this.tween = globalScene.tweens.add({
targets: this, targets: this,
x: -91, x: this.player ? -barWidth : this.screenRight,
duration: 500, duration: 200,
ease: "Sine.easeIn", ease: "Sine.easeIn",
onComplete: () => { onComplete: () => {
this.tween = null;
this.setVisible(false); this.setVisible(false);
}, },
}); });
this.shown = false;
} }
resetAutoHideTimer(): void { public isVisible(): boolean {
if (this.autoHideTimer) { return this.shown;
clearInterval(this.autoHideTimer);
}
this.autoHideTimer = setTimeout(() => {
this.hide();
this.autoHideTimer = null;
}, 2500);
} }
} }

View File

@ -68,7 +68,7 @@ describe("Moves - Secret Power", () => {
await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]); await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0];
vi.spyOn(sereneGraceAttr, "apply"); vi.spyOn(sereneGraceAttr, "canApply");
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
@ -86,8 +86,8 @@ describe("Moves - Secret Power", () => {
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(sereneGraceAttr.apply).toHaveBeenCalledOnce(); expect(sereneGraceAttr.canApply).toHaveBeenCalledOnce();
expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true); expect(sereneGraceAttr.canApply).toHaveLastReturnedWith(true);
expect(rainbowEffect.apply).toHaveBeenCalledTimes(0); expect(rainbowEffect.apply).toHaveBeenCalledTimes(0);
}); });