clean up battle animation logic

This commit is contained in:
ImperialSympathizer 2024-07-17 22:58:34 -04:00
parent 09a3167bac
commit 06684e06a2
8 changed files with 151 additions and 800 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import {Constructor, isNullOrUndefined} from "#app/utils";
import * as Utils from "./utils";
import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball";
import { initCommonAnims, initEncounterAnims, initMoveAnim, loadCommonAnimAssets, loadEncounterAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { Phase } from "./phase";
import { initGameSpeed } from "./system/game-speed";
import { Arena, ArenaBase } from "./field/arena";
@ -555,7 +555,6 @@ export default class BattleScene extends SceneBase {
Promise.all([
Promise.all(loadPokemonAssets),
initCommonAnims(this).then(() => loadCommonAnimAssets(this, true)),
initEncounterAnims(this).then(() => loadEncounterAnimAssets(this, true)),
Promise.all([ Moves.TACKLE, Moves.TAIL_WHIP, Moves.FOCUS_ENERGY, Moves.STRUGGLE ].map(m => initMoveAnim(this, m))).then(() => loadMoveAnimAssets(this, defaultMoves, true)),
this.initStarterColors()
]).then(() => {

View File

@ -6,6 +6,7 @@ import * as Utils from "../utils";
import { BattlerIndex } from "../battle";
import { Element } from "json-stable-stringify";
import { Moves } from "#enums/moves";
import { isNullOrUndefined } from "../utils";
//import fs from 'vite-plugin-fs/browser';
export enum AnimFrameTarget {
@ -307,7 +308,7 @@ abstract class AnimTimedEvent {
this.resourceName = resourceName;
}
abstract execute(scene: BattleScene, battleAnim: BattleAnim): integer;
abstract execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer;
abstract getEventType(): string;
}
@ -325,7 +326,7 @@ class AnimTimedSoundEvent extends AnimTimedEvent {
}
}
execute(scene: BattleScene, battleAnim: BattleAnim): integer {
execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer {
const soundConfig = { rate: (this.pitch * 0.01), volume: (this.volume * 0.01) };
if (this.resourceName) {
try {
@ -387,7 +388,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent {
super(frameIndex, resourceName, source);
}
execute(scene: BattleScene, moveAnim: MoveAnim): integer {
execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer {
const tweenProps = {};
if (this.bgX !== undefined) {
tweenProps["x"] = (this.bgX * 0.5) - 320;
@ -417,7 +418,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
super(frameIndex, resourceName, source);
}
execute(scene: BattleScene, moveAnim: MoveAnim): integer {
execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer {
if (moveAnim.bgSprite) {
moveAnim.bgSprite.destroy();
}
@ -429,7 +430,9 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent {
moveAnim.bgSprite.setAlpha(this.opacity / 255);
scene.field.add(moveAnim.bgSprite);
const fieldPokemon = scene.getEnemyPokemon() || scene.getPlayerPokemon();
if (fieldPokemon?.isOnField()) {
if (!isNullOrUndefined(priority)) {
scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority);
} else if (fieldPokemon?.isOnField()) {
scene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon);
}
@ -517,14 +520,18 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
});
}
export function initEncounterAnims(scene: BattleScene): Promise<void> {
export function initEncounterAnims(scene: BattleScene, anims: EncounterAnim | EncounterAnim[]): Promise<void> {
anims = anims instanceof Array ? anims : [anims];
return new Promise(resolve => {
const encounterAnimNames = Utils.getEnumKeys(EncounterAnim);
const encounterAnimIds = Utils.getEnumValues(EncounterAnim);
const encounterAnimFetches = [];
for (let ea = 0; ea < encounterAnimIds.length; ea++) {
const encounterAnimId = encounterAnimIds[ea];
encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[ea].toLowerCase().replace(/\_/g, "-")}.json`)
for (const anim of anims) {
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
continue;
}
const encounterAnimId = encounterAnimIds[anim];
encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
.then(response => response.json())
.then(cas => encounterAnims.set(encounterAnimId, new AnimConfig(cas))));
}
@ -1005,18 +1012,13 @@ export abstract class BattleAnim {
});
}
private getGraphicFrameDataWithoutTarget(scene: BattleScene, frames: AnimFrame[], targetInitialX: number, targetInitialY: number): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
private getGraphicFrameDataWithoutTarget(frames: AnimFrame[], targetInitialX: number, targetInitialY: number): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
const ret: Map<integer, Map<AnimFrameTarget, GraphicFrameData>> = new Map([
[AnimFrameTarget.GRAPHIC, new Map<AnimFrameTarget, GraphicFrameData>() ],
[AnimFrameTarget.USER, new Map<AnimFrameTarget, GraphicFrameData>() ],
[AnimFrameTarget.TARGET, new Map<AnimFrameTarget, GraphicFrameData>() ]
]);
const userInitialX = 0;
const userInitialY = 0;
const userHalfHeight = 30;
const targetHalfHeight = 30;
let g = 0;
let u = 0;
let t = 0;
@ -1024,27 +1026,10 @@ export abstract class BattleAnim {
for (const frame of frames) {
let x = frame.x;
let y = frame.y;
let scaleX = (frame.zoomX / 100) * (!frame.mirror ? 1 : -1);
const scaleX = (frame.zoomX / 100) * (!frame.mirror ? 1 : -1);
const scaleY = (frame.zoomY / 100);
switch (frame.focus) {
case AnimFocus.TARGET:
x += targetInitialX - targetFocusX;
y += (targetInitialY - targetHalfHeight) - targetFocusY;
break;
case AnimFocus.USER:
x += userInitialX - userFocusX;
y += (userInitialY - userHalfHeight) - userFocusY;
break;
case AnimFocus.USER_TARGET:
const point = transformPoint(this.srcLine[0], this.srcLine[1], this.srcLine[2], this.srcLine[3],
this.dstLine[0], this.dstLine[1] - userHalfHeight, this.dstLine[2], this.dstLine[3] - targetHalfHeight, x, y);
x = point[0];
y = point[1];
if (frame.target === AnimFrameTarget.GRAPHIC && isReversed(this.srcLine[0], this.srcLine[2], this.dstLine[0], this.dstLine[2])) {
scaleX = scaleX * -1;
}
break;
}
x += targetInitialX;
y += targetInitialY;
const angle = -frame.angle;
const key = frame.target === AnimFrameTarget.GRAPHIC ? g++ : frame.target === AnimFrameTarget.USER ? u++ : t++;
ret.get(frame.target).set(key, { x: x, y: y, scaleX: scaleX, scaleY: scaleY, angle: angle });
@ -1053,7 +1038,20 @@ export abstract class BattleAnim {
return ret;
}
playWithoutTargets(scene: BattleScene, targetInitialX: number, targetInitialY: number, frameTimeMult: number, callback?: Function) {
/**
*
* @param scene
* @param targetInitialX
* @param targetInitialY
* @param frameTimeMult
* @param frameTimedEventPriority
* - 0 is behind all other sprites (except BG)
* - 1 on top of player field
* - 3 is on top of both fields
* - 5 is on top of player sprite
* @param callback
*/
playWithoutTargets(scene: BattleScene, targetInitialX: number, targetInitialY: number, frameTimeMult: number, frameTimedEventPriority?: 0 | 1 | 3 | 5, callback?: Function) {
const spriteCache: SpriteCache = {
[AnimFrameTarget.GRAPHIC]: [],
[AnimFrameTarget.USER]: [],
@ -1087,12 +1085,17 @@ export abstract class BattleAnim {
let r = anim.frames.length;
let f = 0;
const fieldSprites = scene.field.getAll();
const playerFieldSprite = fieldSprites[1];
const enemyFieldSprite = fieldSprites[3];
const trainerSprite = fieldSprites[5];
scene.tweens.addCounter({
duration: Utils.getFrameMs(3) * frameTimeMult,
repeat: anim.frames.length,
onRepeat: () => {
const spriteFrames = anim.frames[f];
const frameData = this.getGraphicFrameDataWithoutTarget(scene, anim.frames[f], targetInitialX, targetInitialY);
const frameData = this.getGraphicFrameDataWithoutTarget(anim.frames[f], targetInitialX, targetInitialY);
const u = 0;
const t = 0;
let g = 0;
@ -1116,13 +1119,29 @@ export abstract class BattleAnim {
spritePriorities[graphicIndex] = frame.priority;
const setSpritePriority = (priority: integer) => {
if (priority < 0) {
// Move to top of scene
// Move to top of scene
scene.field.moveTo(moveSprite, scene.field.getAll().length - 1);
} else if (priority < scene.field.getAll().length) {
// Indexes of field:
// 0 is scene background
// 1 is enemy field
scene.field.moveTo(moveSprite, priority);
} else if (priority === 1) {
// Move above player field
if (playerFieldSprite) {
scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, playerFieldSprite);
} else {
setSpritePriority(-1);
}
} else if (priority === 3) {
// Move above player enemy field
if (enemyFieldSprite) {
scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, enemyFieldSprite);
} else {
setSpritePriority(-1);
}
} else if (priority === 5) {
// Move above player trainer sprite
if (trainerSprite) {
scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, trainerSprite);
} else {
setSpritePriority(-1);
}
} else {
setSpritePriority(-1);
}
@ -1143,7 +1162,7 @@ export abstract class BattleAnim {
}
if (anim.frameTimedEvents.has(f)) {
for (const event of anim.frameTimedEvents.get(f)) {
r = Math.max((anim.frames.length - f) + event.execute(scene, this), r);
r = Math.max((anim.frames.length - f) + event.execute(scene, this, frameTimedEventPriority), r);
}
}
const targets = Utils.getEnumValues(AnimFrameTarget);

View File

@ -1,5 +1,5 @@
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes, } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene";
@ -12,7 +12,7 @@ import { Type } from "#app/data/type";
import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon";
import { Moves } from "#enums/moves";
import { EncounterAnim, EncounterBattleAnim, initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import { EncounterAnim, EncounterBattleAnim } from "#app/data/battle-anims";
import { WeatherType } from "#app/data/weather";
import { randSeedInt } from "#app/utils";
@ -27,6 +27,7 @@ export const FieryFalloutEncounter: IMysteryEncounter =
.withSceneWaveRangeRequirement(40, 180) // waves 10 to 180
.withCatchAllowed(true)
.withIntroSpriteConfigs([]) // Set in onInit()
.withAnimations(EncounterAnim.MAGMA_BG, EncounterAnim.MAGMA_SPOUT)
.withIntroDialogue([
{
text: `${namespace}_intro_message`,
@ -60,21 +61,20 @@ export const FieryFalloutEncounter: IMysteryEncounter =
scene.arena.trySetWeather(WeatherType.SUNNY, true);
// Load animations/sfx for Volcarona moves
Promise.all([initMoveAnim(scene, Moves.QUIVER_DANCE), initMoveAnim(scene, Moves.FIRE_SPIN)])
.then(() => loadMoveAnimAssets(scene, [Moves.QUIVER_DANCE, Moves.FIRE_SPIN]));
initCustomMovesForEncounter(scene, [Moves.FIRE_SPIN, Moves.QUIVER_DANCE]);
return true;
})
.withOnVisualsStart((scene: BattleScene) => {
// Play animations
const background = new EncounterBattleAnim(EncounterAnim.MAGMA_BG, scene.getPlayerPokemon(), scene.getPlayerPokemon());
background.playWithoutTargets(scene, 200, 70, 2);
background.playWithoutTargets(scene, 200, 70, 2, 3);
const animation = new EncounterBattleAnim(EncounterAnim.MAGMA_SPOUT, scene.getPlayerPokemon(), scene.getPlayerPokemon());
animation.playWithoutTargets(scene, 200, 70, 2);
animation.playWithoutTargets(scene, 100, 100, 2);
const increment = 600;
for (let i = 3; i < 6; i++) {
scene.time.delayedCall((increment) * (i - 2), () => {
animation.playWithoutTargets(scene, 100 + randSeedInt(12) * 20, 110 - randSeedInt(10) * 15, 2);
animation.playWithoutTargets(scene, randSeedInt(12) * 15, 150 - randSeedInt(10) * 15, 2);
});
}

View File

@ -19,6 +19,7 @@ import {
WaveRangeRequirement
} from "./mystery-encounter-requirements";
import { BattlerIndex } from "#app/battle";
import { EncounterAnim } from "#app/data/battle-anims";
export enum MysteryEncounterVariant {
DEFAULT,
@ -57,6 +58,7 @@ export default interface IMysteryEncounter {
* Optional params
*/
encounterTier?: MysteryEncounterTier;
encounterAnimations?: EncounterAnim[];
hideBattleIntroMessage?: boolean;
hideIntroVisuals?: boolean;
catchAllowed?: boolean;
@ -193,13 +195,6 @@ export default class IMysteryEncounter implements IMysteryEncounter {
const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary
const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
// console.log("-------" + MysteryEncounterType[this.encounterType] + " Encounter Check -------");
// console.log(this);
// console.log( "sceneCheck: " + sceneReq);
// console.log( "primaryCheck: " + priReqs);
// console.log( "secondaryCheck: " + secReqs);
// console.log(MysteryEncounterTier[this.encounterTier]);
return sceneReq && secReqs && priReqs;
}
@ -377,6 +372,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
dialogue?: MysteryEncounterDialogue;
encounterTier?: MysteryEncounterTier;
encounterAnimations?: EncounterAnim[];
requirements?: EncounterSceneRequirement[] = [];
primaryPokemonRequirements?: EncounterPokemonRequirement[] = [];
secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = [];
@ -471,6 +467,16 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return Object.assign(this, { encounterTier: encounterTier });
}
/**
* Defines any EncounterAnim animations that are intended to be used during the encounter
* @param encounterAnimations
* @returns
*/
withAnimations(...encounterAnimations: EncounterAnim[]): this & Required<Pick<IMysteryEncounter, "encounterAnimations">> {
encounterAnimations = encounterAnimations instanceof Array ? encounterAnimations : [encounterAnimations];
return Object.assign(this, { encounterAnimations: encounterAnimations });
}
/**
* Sets the maximum number of times that an encounter can spawn in a given Classic run
* @param maxAllowedEncounters

View File

@ -26,6 +26,8 @@ import * as Overrides from "#app/overrides";
import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { Gender } from "#app/data/gender";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
import { Moves } from "#enums/moves";
export class EnemyPokemonConfig {
species: PokemonSpecies;
@ -230,6 +232,20 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
}
}
/**
* Load special move animations/sfx for hard-coded encounter-specific moves that a pokemon uses at the start of an encounter
* See: [startOfBattleEffects](IMysteryEncounter.startOfBattleEffects) for more details
*
* This promise does not need to be awaited on if called in an encounter onInit (will just load lazily)
* @param scene
* @param moves
*/
export function initCustomMovesForEncounter(scene: BattleScene, moves: Moves | Moves[]) {
moves = moves instanceof Array ? moves : [moves];
return Promise.all(moves.map(move => initMoveAnim(scene, move)))
.then(() => loadMoveAnimAssets(scene, moves));
}
/**
* Will update player money, and animate change (sound optional)
* @param scene - Battle Scene

View File

@ -9,7 +9,7 @@ import { Stat } from "./data/pokemon-stat";
import { BerryModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, IvScannerModifier, LapsingPersistentModifier, LapsingPokemonHeldItemModifier, MapModifier, Modifier, MoneyInterestModifier, MoneyMultiplierModifier, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier } from "./modifier/modifier";
import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball";
import { CommonAnim, CommonBattleAnim, initMoveAnim, loadMoveAnimAssets, MoveAnim } from "./data/battle-anims";
import { CommonAnim, CommonBattleAnim, initEncounterAnims, initMoveAnim, loadEncounterAnimAssets, loadMoveAnimAssets, MoveAnim } from "./data/battle-anims";
import { getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText, StatusEffect } from "./data/status-effect";
import { SummaryUiMode } from "./ui/summary-ui-handler";
import EvolutionSceneHandler from "./ui/evolution-scene-handler";
@ -832,6 +832,11 @@ export class EncounterPhase extends BattlePhase {
mysteryEncounter.populateDialogueTokensFromRequirements(this.scene);
}, this.scene.currentBattle.waveIndex);
// Add any special encounter animations to load
if (mysteryEncounter.encounterAnimations && mysteryEncounter.encounterAnimations.length > 0) {
loadEnemyAssets.push(initEncounterAnims(this.scene, mysteryEncounter.encounterAnimations).then(() => loadEncounterAnimAssets(this.scene, true)));
}
// Add intro visuals for mystery encounter
mysteryEncounter.initIntroVisuals(this.scene);
this.scene.field.add(mysteryEncounter.introVisuals);

View File

@ -89,8 +89,8 @@ describe("Mystery Encounter Phases", () => {
expect(dialogueSpy).toHaveBeenCalledTimes(1);
expect(messageSpy).toHaveBeenCalledTimes(2);
expect(dialogueSpy).toHaveBeenCalledWith("What's this?", "???", null, expect.any(Function));
expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 750, true);
expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 750, true);
expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 300, true);
expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 300, true);
});
});