[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 { StatusEffect } from "#enums/status-effect";
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";
@ -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)
*/
@ -3133,6 +3150,41 @@ export default class BattleScene extends SceneBase {
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> {
return new Promise(resolve => {
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
* 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;
}
/** 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
* @param weather {@linkcode WeatherType} new {@linkcode WeatherType} to set
@ -314,7 +319,7 @@ export class Arena {
return this.trySetWeatherOverride(Overrides.WEATHER_OVERRIDE);
}
if (this.weather?.weatherType === (weather || undefined)) {
if (!this.canSetWeather(weather)) {
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 {
if (this.terrain?.terrainType === (terrain || undefined)) {
if (!this.canSetTerrain(terrain)) {
return false;
}

View File

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

View File

@ -1,11 +1,7 @@
import { globalScene } from "#app/global-scene";
export class Phase {
start() {
if (globalScene.abilityBar.shown) {
globalScene.abilityBar.resetAutoHideTimer();
}
}
start() {}
end() {
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 { globalScene } from "#app/global-scene";
import { PokemonPhase } from "./pokemon-phase";
import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next";
/**
* 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.queueMessage(
i18next.t("abilityTriggers:postSummonTransform", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
targetName: target.name,
}),
);
promises.push(
user.loadAssets(false).then(() => {
user.playAnim();

View File

@ -1,36 +1,60 @@
import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#app/battle";
import { PokemonPhase } from "./pokemon-phase";
import { getPokemonNameWithAffix } from "#app/messages";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
export class ShowAbilityPhase extends PokemonPhase {
private passive: boolean;
private pokemonName: string;
private abilityName: string;
private pokemonOnField: boolean;
constructor(battlerIndex: BattlerIndex, passive = false) {
super(battlerIndex);
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() {
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();
if (pokemon) {
if (!pokemon.isPlayer()) {
/** If its an enemy pokemon, list it as last enemy to use ability or move */
globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2;
} else {
globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2;
}
globalScene.abilityBar.showAbility(pokemon, this.passive);
if (!pokemon.isPlayer()) {
/** If its an enemy pokemon, list it as last enemy to use ability or move */
globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2;
} else {
globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2;
}
globalScene.abilityBar.showAbility(this.pokemonName, this.abilityName, this.passive, this.player).then(() => {
if (pokemon?.battleData) {
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 type Pokemon from "../field/pokemon";
import { TextStyle, addTextObject } from "./text";
import i18next from "i18next";
const hiddenX = -118;
const shownX = 0;
const barWidth = 118;
const screenLeft = 0;
const baseY = -116;
export default class AbilityBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image;
private abilityBars: Phaser.GameObjects.Image[];
private abilityBarText: Phaser.GameObjects.Text;
private tween: Phaser.Tweens.Tween | null;
private autoHideTimer: NodeJS.Timeout | null;
public shown: boolean;
private player: boolean;
private screenRight: number; // hold screenRight in case size changes between show and hide
private shown: boolean;
constructor() {
super(globalScene, hiddenX, baseY);
super(globalScene, barWidth, baseY);
this.abilityBars = [];
this.player = true;
this.shown = false;
}
setup(): void {
this.bg = globalScene.add.image(0, 0, "ability_bar_left");
this.bg.setOrigin(0, 0);
this.add(this.bg);
for (const key of ["ability_bar_right", "ability_bar_left"]) {
const bar = globalScene.add.image(0, 0, key);
bar.setOrigin(0, 0);
bar.setVisible(false);
this.add(bar);
this.abilityBars.push(bar);
}
this.abilityBarText = addTextObject(15, 3, "", TextStyle.MESSAGE, {
fontSize: "72px",
@ -33,72 +35,80 @@ export default class AbilityBar extends Phaser.GameObjects.Container {
this.abilityBarText.setOrigin(0, 0);
this.abilityBarText.setWordWrapWidth(600, true);
this.add(this.abilityBarText);
this.bringToTop(this.abilityBarText);
this.setVisible(false);
this.shown = false;
this.setX(-barWidth); // start hidden (right edge of bar at x=0)
}
showAbility(pokemon: Pokemon, passive = false): void {
this.abilityBarText.setText(
`${i18next.t("fightUiHandler:abilityFlyInText", { pokemonName: getPokemonNameWithAffix(pokemon), passive: passive ? i18next.t("fightUiHandler:passive") : "", abilityName: !passive ? pokemon.getAbility().name : pokemon.getPassiveAbility().name })}`,
);
public override setVisible(value: boolean): this {
this.abilityBars[+this.player].setVisible(value);
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);
this.y = baseY + (globalScene.currentBattle.double ? 14 : 0);
this.tween = globalScene.tweens.add({
targets: this,
x: shownX,
duration: 500,
ease: "Sine.easeOut",
onComplete: () => {
this.tween = null;
this.resetAutoHideTimer();
},
});
let y = baseY;
if (this.player) {
y += globalScene.currentBattle.double ? 14 : 0;
} else {
y -= globalScene.currentBattle.double ? 28 : 14;
}
this.setVisible(true);
this.shown = true;
this.setY(y);
return this.startTween(
{
targets: this,
x: this.player ? screenLeft : this.screenRight - barWidth,
duration: 500,
ease: "Sine.easeOut",
hold: 1000,
},
text,
);
}
hide(): void {
if (!this.shown) {
return;
}
if (this.autoHideTimer) {
clearInterval(this.autoHideTimer);
}
if (this.tween) {
this.tween.stop();
}
this.tween = globalScene.tweens.add({
public async hide(): Promise<void> {
return this.startTween({
targets: this,
x: -91,
duration: 500,
x: this.player ? -barWidth : this.screenRight,
duration: 200,
ease: "Sine.easeIn",
onComplete: () => {
this.tween = null;
this.setVisible(false);
},
});
this.shown = false;
}
resetAutoHideTimer(): void {
if (this.autoHideTimer) {
clearInterval(this.autoHideTimer);
}
this.autoHideTimer = setTimeout(() => {
this.hide();
this.autoHideTimer = null;
}, 2500);
public isVisible(): boolean {
return this.shown;
}
}

View File

@ -68,7 +68,7 @@ describe("Moves - Secret Power", () => {
await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
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.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
@ -86,8 +86,8 @@ describe("Moves - Secret Power", () => {
await game.phaseInterceptor.to("BerryPhase", false);
expect(sereneGraceAttr.apply).toHaveBeenCalledOnce();
expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true);
expect(sereneGraceAttr.canApply).toHaveBeenCalledOnce();
expect(sereneGraceAttr.canApply).toHaveLastReturnedWith(true);
expect(rainbowEffect.apply).toHaveBeenCalledTimes(0);
});