diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index a179f3a3e9b..a42779563f2 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,7 +1,20 @@ import { globalScene } from "#app/global-scene"; -import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; +import { + AttackMove, + BeakBlastHeaderAttr, + DelayedAttackAttr, + MoveFlags, + SelfStatusMove, + allMoves, +} from "./move"; import type Pokemon from "../field/pokemon"; -import * as Utils from "../utils"; +import { + type nil, + getFrameMs, + getEnumKeys, + getEnumValues, + animationFileName, +} from "../utils"; import type { BattlerIndex } from "../battle"; import type { Element } from "json-stable-stringify"; import { Moves } from "#enums/moves"; @@ -401,7 +414,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { if (Object.keys(tweenProps).length) { globalScene.tweens.add(Object.assign({ targets: moveAnim.bgSprite, - duration: Utils.getFrameMs(this.duration * 3) + duration: getFrameMs(this.duration * 3) }, tweenProps)); } return this.duration * 2; @@ -437,7 +450,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { globalScene.tweens.add({ targets: moveAnim.bgSprite, - duration: Utils.getFrameMs(this.duration * 3) + duration: getFrameMs(this.duration * 3) }); return this.duration * 2; @@ -455,8 +468,8 @@ export const encounterAnims = new Map(); export function initCommonAnims(): Promise { return new Promise(resolve => { - const commonAnimNames = Utils.getEnumKeys(CommonAnim); - const commonAnimIds = Utils.getEnumValues(CommonAnim); + const commonAnimNames = getEnumKeys(CommonAnim); + const commonAnimIds = getEnumValues(CommonAnim); const commonAnimFetches: Promise>[] = []; for (let ca = 0; ca < commonAnimIds.length; ca++) { const commonAnimId = commonAnimIds[ca]; @@ -493,7 +506,7 @@ export function initMoveAnim(move: Moves): Promise { const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; const fetchAnimAndResolve = (move: Moves) => { - globalScene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) + globalScene.cachedFetch(`./battle-anims/${animationFileName(move)}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { @@ -550,7 +563,7 @@ function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) { * @remarks use {@linkcode useDefaultAnim} to use a default animation */ function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { - const moveName = Utils.animationFileName(move); + const moveName = animationFileName(move); console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); } @@ -560,7 +573,7 @@ function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { */ export async function initEncounterAnims(encounterAnim: EncounterAnim | EncounterAnim[]): Promise { const anims = Array.isArray(encounterAnim) ? encounterAnim : [ encounterAnim ]; - const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); + const encounterAnimNames = getEnumKeys(EncounterAnim); const encounterAnimFetches: Promise>[] = []; for (const anim of anims) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { @@ -922,7 +935,7 @@ export abstract class BattleAnim { let f = 0; globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(3), + duration: getFrameMs(3), repeat: anim?.frames.length ?? 0, onRepeat: () => { if (!f) { @@ -994,47 +1007,39 @@ export abstract class BattleAnim { const moveSprite = sprites[graphicIndex]; if (spritePriorities[graphicIndex] !== frame.priority) { spritePriorities[graphicIndex] = frame.priority; + /** Move the position that the moveSprite is rendered in based on the priority. + * @param priority The priority level to draw the sprite. + * - 0: Draw the sprite in front of the pokemon on the field. + * - 1: Draw the sprite in front of the user pokemon. + * - 2: Draw the sprite in front of its `bgSprite` (if it has one), or its + * `AnimFocus` (if that is user/target), otherwise behind everything. + * - 3: Draw the sprite behind its `AnimFocus` (if that is user/target), otherwise in front of everything. + */ const setSpritePriority = (priority: number) => { - switch (priority) { - case 0: - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false)!); // TODO: is this bang correct? - break; - case 1: - globalScene.field.moveTo(moveSprite, globalScene.field.getAll().length - 1); - break; - case 2: - switch (frame.focus) { - case AnimFocus.USER: - if (this.bgSprite) { - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.bgSprite); - } else { - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.user!); // TODO: is this bang correct? - } - break; - case AnimFocus.TARGET: - globalScene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.target!); // TODO: is this bang correct? - break; - default: - setSpritePriority(1); - break; - } - break; - case 3: - switch (frame.focus) { - case AnimFocus.USER: - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.user!); // TODO: is this bang correct? - break; - case AnimFocus.TARGET: - globalScene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.target!); // TODO: is this bang correct? - break; - default: - setSpritePriority(1); - break; - } - break; - default: - setSpritePriority(1); + /** The sprite we are moving the moveSprite in relation to */ + let targetSprite: Phaser.GameObjects.GameObject | nil; + /** The method that is being used to move the sprite.*/ + let moveFunc: ((sprite: Phaser.GameObjects.GameObject, target: Phaser.GameObjects.GameObject) => void) | + ((sprite: Phaser.GameObjects.GameObject) => void) = globalScene.field.bringToTop; + + if (priority === 0) { // Place the sprite in front of the pokemon on the field. + targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p); + console.log(typeof targetSprite); + moveFunc = globalScene.field.moveBelow; + } else if (priority === 2 && this.bgSprite) { + moveFunc = globalScene.field.moveAbove; + targetSprite = this.bgSprite; + } else if (priority === 2 || priority === 3) { + moveFunc = priority === 2 ? globalScene.field.moveBelow : globalScene.field.moveAbove; + if (frame.focus === AnimFocus.USER) { + targetSprite = this.user; + } else if (frame.focus === AnimFocus.TARGET) { + targetSprite = this.target; + } } + // If target sprite is not undefined and exists in the field container, then move the sprite using the moveFunc. + // Otherwise, default to just bringing it to the top. + targetSprite && globalScene.field.exists(targetSprite) ? moveFunc.bind(globalScene.field)(moveSprite as Phaser.GameObjects.GameObject, targetSprite) : globalScene.field.bringToTop(moveSprite as Phaser.GameObjects.GameObject); }; setSpritePriority(frame.priority); } @@ -1052,11 +1057,13 @@ export abstract class BattleAnim { } } if (anim?.frameTimedEvents.has(f)) { - for (const event of anim.frameTimedEvents.get(f)!) { // TODO: is this bang correct? - r = Math.max((anim.frames.length - f) + event.execute(this), r); + const base = anim.frames.length - f; + // Bang is correct due to `has` check above, which cannot return true for an undefined / null `f` + for (const event of anim.frameTimedEvents.get(f)!) { + r = Math.max(base + event.execute(this), r); } } - const targets = Utils.getEnumValues(AnimFrameTarget); + const targets = getEnumValues(AnimFrameTarget); for (const i of targets) { const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; if (count < spriteCache[i].length) { @@ -1084,7 +1091,7 @@ export abstract class BattleAnim { } if (r) { globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(r), + duration: getFrameMs(r), onComplete: () => cleanUpAndComplete() }); } else { @@ -1166,7 +1173,7 @@ export abstract class BattleAnim { let existingFieldSprites = globalScene.field.getAll().slice(0); globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(3) * frameTimeMult, + duration: getFrameMs(3) * frameTimeMult, repeat: anim!.frames.length, onRepeat: () => { existingFieldSprites = globalScene.field.getAll().slice(0); @@ -1215,11 +1222,12 @@ export abstract class BattleAnim { } } if (anim?.frameTimedEvents.get(frameCount)) { + const base = anim.frames.length - frameCount; for (const event of anim.frameTimedEvents.get(frameCount)!) { - totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(this, frameTimedEventPriority), totalFrames); + totalFrames = Math.max(base + event.execute(this, frameTimedEventPriority), totalFrames); } } - const targets = Utils.getEnumValues(AnimFrameTarget); + const targets = getEnumValues(AnimFrameTarget); for (const i of targets) { const count = graphicFrameCount; if (count < spriteCache[i].length) { @@ -1244,7 +1252,7 @@ export abstract class BattleAnim { } if (totalFrames) { globalScene.tweens.addCounter({ - duration: Utils.getFrameMs(totalFrames), + duration: getFrameMs(totalFrames), onComplete: () => cleanUpAndComplete() }); } else { @@ -1342,15 +1350,15 @@ export class EncounterBattleAnim extends BattleAnim { } export async function populateAnims() { - const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); + const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, "")); - const commonAnimIds = Utils.getEnumValues(CommonAnim) as CommonAnim[]; - const chargeAnimNames = Utils.getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); + const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[]; + const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/\_/g, " ")); - const chargeAnimIds = Utils.getEnumValues(ChargeAnim) as ChargeAnim[]; + const chargeAnimIds = getEnumValues(ChargeAnim) as ChargeAnim[]; const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; const moveNameToId = {}; - for (const move of Utils.getEnumValues(Moves).slice(1)) { + for (const move of getEnumValues(Moves).slice(1)) { const moveName = Moves[move].toUpperCase().replace(/\_/g, ""); moveNameToId[moveName] = move; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0da390c5ab4..dad8f5d6787 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1407,10 +1407,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks if the {@linkcode Pokemon} has is the specified {@linkcode Species} or is fused with it. * @param species the pokemon {@linkcode Species} to check + * @param formKey If provided, requires the species to be in that form * @returns `true` if the pokemon is the species or is fused with it, `false` otherwise */ - hasSpecies(species: Species): boolean { - return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; + hasSpecies(species: Species, formKey?: string): boolean { + if (Utils.isNullOrUndefined(formKey)) { + return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; + } + + return (this.species.speciesId === species && this.getFormKey() === formKey) || (this.fusionSpecies?.speciesId === species && this.getFusionFormKey() === formKey); } abstract isBoss(): boolean; @@ -3390,6 +3395,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return maxForms.includes(this.getFormKey()) || (!!this.getFusionFormKey() && maxForms.includes(this.getFusionFormKey()!)); } + isMega(): boolean { + const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, SpeciesFormKey.MEGA_Y, SpeciesFormKey.PRIMAL ] as string[]; + return megaForms.includes(this.getFormKey()) || (!!this.getFusionFormKey() && megaForms.includes(this.getFusionFormKey()!)); + } + canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index f23cc78c9f7..20cffbbe30a 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -10,6 +10,7 @@ import { globalScene } from "#app/global-scene"; import { TerastallizeAccessModifier } from "#app/modifier/modifier"; import { Type } from "#app/enums/type"; import { getTypeRgb } from "#app/data/type"; +import { Species } from "#enums/species"; export enum Command { FIGHT = 0, @@ -180,9 +181,11 @@ export default class CommandUiHandler extends UiHandler { canTera(): boolean { const hasTeraMod = !!globalScene.getModifiers(TerastallizeAccessModifier).length; + const activePokemon = globalScene.getField()[this.fieldIndex]; + const isBlockedForm = activePokemon.isMega() || activePokemon.isMax() || activePokemon.hasSpecies(Species.NECROZMA, "ultra"); const currentTeras = globalScene.arena.playerTerasUsed; const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 ? 1 : 0; - return hasTeraMod && (currentTeras + plannedTera) < 1; + return hasTeraMod && !isBlockedForm && (currentTeras + plannedTera) < 1; } toggleTeraButton() {