Ctrl K
ComunidadGuías…

src/animations.ts

Rama beta · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { audioManager } from "#app/global-audio-manager";
import { globalScene } from "#app/global-scene";
import { settings } from "#app/global-settings-manager";
import type { BattleAnim } from "#data/battle-anims";
import { PokeballType } from "#enums/pokeball";
import type { Variant } from "#sprites/variant";
import { type BooleanHolder, getFrameMs, randGauss, randInt } from "#utils/common";

/**
 * Class for handling general animations such as particle effects.
 * For battle animations, see {@linkcode BattleAnim}.
 */
// TODO: Can this be made into an interface/POJO?
// TODO: Rename to not conflict with built-in `animation` class
// TODO: Clean up a lot of the animation code to be more maintainable and add better docs
export class Animation {
  /**
   * Animates particles that "spiral" upwards at start of transform animation
   * @param transformationBaseBg - The background image to animate.
   * @param transformationContainer - The phaser container
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  public doSpiralUpward(
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset = 0,
    yOffset = 0,
  ): void {
    let cycle = 0;

    globalScene.tweens.addCounter({
      repeat: 8,
      duration: getFrameMs(8),
      onRepeat: () => {
        for (let i = 0; i < 4; i++) {
          this.doSpiralUpwardParticle(
            16 * cycle + i * 64,
            transformationBaseBg,
            transformationContainer,
            xOffset,
            yOffset,
          );
        }
        cycle++;
      },
    });
  }

  /**
   * Helper function for {@linkcode doSpiralUpward}, handles a single particle
   * @param trigIndex - Starting offset for particle
   * @param transformationBaseBg - The background image
   * @param transformationContainer - The phaser container
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  private doSpiralUpwardParticle(
    trigIndex: number,
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset: number,
    yOffset: number,
  ): void {
    const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
    const particle = globalScene.add.image(initialX, 0, "evo_sparkle");
    transformationContainer.add(particle);

    let f = 0;
    let amp = 48;

    const particleTimer = globalScene.tweens.addCounter({
      repeat: -1,
      duration: getFrameMs(1),
      onRepeat: () => {
        updateParticle();
      },
    });

    const updateParticle = () => {
      if (!f || particle.y > 8) {
        particle.setPosition(initialX, 88 - (f * f) / 80 + yOffset);
        particle.y += this.sin(trigIndex, amp) / 4;
        particle.x += this.cos(trigIndex, amp);
        particle.setScale(1 - f / 80);
        trigIndex += 4;
        if (f % 2 === 1) {
          amp--;
        }
        f++;
      } else {
        particle.destroy();
        particleTimer.remove();
      }
    };

    updateParticle();
  }

  /**
   * Animates particles that arc downwards after the upwards spiral for evolution/form change animations.
   * @param transformationBaseBg - The background image to animate
   * @param transformationContainer - The {@linkcode Phaser.GameObjects.Container | Container}
   * which will own the particles
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  public doArcDownward(
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset = 0,
    yOffset = 0,
  ): Phaser.Tweens.Tween {
    return globalScene.tweens.addCounter({
      repeat: 6,
      duration: getFrameMs(1),
      onRepeat: () => {
        for (let i = 0; i < 9; i++) {
          this.doArcDownParticle(i * 16, transformationBaseBg, transformationContainer, xOffset, yOffset);
        }
      },
    });
  }

  /**
   * Helper function for {@linkcode doArcDownward}, handles a single particle
   * @param trigIndex - Starting offset for particle
   * @param transformationBaseBg - The background image
   * @param transformationContainer - The phaser container
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  private doArcDownParticle(
    trigIndex: number,
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset: number,
    yOffset: number,
  ): void {
    const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
    const particle = globalScene.add.image(initialX, 0, "evo_sparkle");
    particle.setScale(0.5);
    transformationContainer.add(particle);

    let f = 0;
    let amp = 8;

    // NB: While this would recurse infinitely, `addParticle` manually ends `particleTimer` once the particles descend enough.
    const particleTimer = globalScene.tweens.addCounter({
      repeat: -1,
      duration: getFrameMs(1),
      onRepeat: () => {
        updateParticle();
      },
    });

    const updateParticle = () => {
      if (!f || particle.y < 88) {
        particle.setPosition(initialX, 8 + (f * f) / 5 + yOffset);
        particle.y += this.sin(trigIndex, amp) / 4;
        particle.x += this.cos(trigIndex, amp);
        amp = 8 + this.sin(f * 4, 40);
        f++;
      } else {
        particle.destroy();
        particleTimer.remove();
      }
    };

    updateParticle();
  }

  /**
   * Animate the transformation between 2 Pokemon forms.
   * @param currentCycle - Variable to track how many cycles have been run and also how long the delay is
   * @param finalCycle - Number representing how many times to recursively cycle the animation
   * @param pokemonTintSprite - The tinted sprite of the Pokemon
   * @param pokemonNewFormTintSprite - The tinted sprite of the Pokemon's new form
   * @param cancelled - If its value is set to `true` by external code during the animation, then cancel the animation.
   */
  public doCycle(
    currentCycle: number,
    finalCycle: number,
    pokemonTintSprite: Phaser.GameObjects.Sprite,
    pokemonNewFormTintSprite: Phaser.GameObjects.Sprite,
    cancelled?: BooleanHolder,
  ): Promise<void> {
    const isFinalCycle = currentCycle === finalCycle;
    const duration = 500 / currentCycle;

    return new Promise(resolve => {
      globalScene.tweens.add({
        targets: pokemonTintSprite,
        scale: 0.25,
        ease: "Cubic.easeInOut",
        duration,
        yoyo: !isFinalCycle,
      });
      globalScene.tweens.add({
        targets: pokemonNewFormTintSprite,
        scale: 1,
        ease: "Cubic.easeInOut",
        duration,
        yoyo: !isFinalCycle,
        onComplete: () => {
          if (cancelled?.value) {
            return resolve();
          }
          if (isFinalCycle) {
            pokemonTintSprite.setVisible(false);
            return resolve();
          }
          // TODO: Explain or refactor away the recursion
          this.doCycle(currentCycle + 0.5, finalCycle, pokemonTintSprite, pokemonNewFormTintSprite, cancelled).then(
            resolve,
          );
        },
      });
    });
  }

  /**
   * Animates particles in a circle pattern
   * @param transformationBaseBg - The background image
   * @param transformationContainer - The phaser container
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  public doCircleInward(
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset = 0,
    yOffset = 0,
  ): void {
    // Add 2 batches of particles - 1 set immediately, another after 32 frames have passed
    globalScene.tweens.addCounter({
      duration: getFrameMs(32),
      onStart: () => {
        for (let i = 0; i < 16; i++) {
          this.doCircleInwardParticle(i * 16, 4, transformationBaseBg, transformationContainer, xOffset, yOffset);
        }
      },
      onComplete: () => {
        for (let i = 0; i < 16; i++) {
          this.doCircleInwardParticle(i * 16, 8, transformationBaseBg, transformationContainer, xOffset, yOffset);
        }
      },
    });
  }

  /**
   * Helper function for {@linkcode doCircleInward}, handles a single particle
   * @param trigIndex - Starting offset for particle
   * @param speed - How much the amplitude slows down by each cycle
   * @param transformationBaseBg - The background image
   * @param transformationContainer - The phaser container
   * @param xOffset - The x offset
   * @param yOffset - The y offset
   */
  private doCircleInwardParticle(
    trigIndex: number,
    speed: number,
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
    xOffset: number,
    yOffset: number,
  ): void {
    const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
    const initialY = transformationBaseBg.displayHeight / 2 + yOffset;
    const particle = globalScene.add.image(initialX, initialY, "evo_sparkle");
    transformationContainer.add(particle);

    let amp = 120;

    const particleTimer = globalScene.tweens.addCounter({
      repeat: -1,
      duration: getFrameMs(1),
      onRepeat: () => {
        updateParticle();
      },
    });

    const updateParticle = () => {
      if (amp > 8) {
        particle.setPosition(initialX, initialY);
        particle.y += this.sin(trigIndex, amp);
        particle.x += this.cos(trigIndex, amp);
        amp -= speed;
        trigIndex += 4;
      } else {
        particle.destroy();
        particleTimer.remove();
      }
    };

    updateParticle();
  }

  public doSpray(
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
  ): void {
    let f = 0;

    globalScene.tweens.addCounter({
      repeat: 48,
      duration: getFrameMs(1),
      onRepeat: () => {
        if (!f) {
          for (let i = 0; i < 8; i++) {
            this.doSprayParticle(i, transformationBaseBg, transformationContainer);
          }
        } else if (f < 50) {
          this.doSprayParticle(randInt(8), transformationBaseBg, transformationContainer);
        }
        f++;
      },
    });
  }

  private doSprayParticle(
    trigIndex: number,
    transformationBaseBg: Phaser.GameObjects.Image,
    transformationContainer: Phaser.GameObjects.Container,
  ): void {
    const initialX = transformationBaseBg.displayWidth / 2;
    const initialY = transformationBaseBg.displayHeight / 2;
    const particle = globalScene.add.image(initialX, initialY, "evo_sparkle");
    transformationContainer.add(particle);

    let f = 0;
    let yOffset = 0;
    const speed = 3 - randInt(8);
    const amp = 48 + randInt(64);

    const particleTimer = globalScene.tweens.addCounter({
      repeat: -1,
      duration: getFrameMs(1),
      onRepeat: () => {
        updateParticle();
      },
    });

    const updateParticle = () => {
      if (!(f & 3)) {
        yOffset++;
      }
      if (trigIndex < 128) {
        particle.setPosition(initialX + (speed * f) / 3, initialY + yOffset);
        particle.y += -this.sin(trigIndex, amp);
        if (f > 108) {
          particle.setScale(1 - (f - 108) / 20);
        }
        trigIndex++;
        f++;
      } else {
        particle.destroy();
        particleTimer.remove();
      }
    };

    updateParticle();
  }

  public addPokeballOpenParticles(x: number, y: number, pokeballType: PokeballType): void {
    switch (pokeballType) {
      case PokeballType.POKEBALL:
        this.doDefaultPbOpenParticles(x, y, 48);
        break;
      case PokeballType.GREAT_BALL:
        this.doDefaultPbOpenParticles(x, y, 96);
        break;
      case PokeballType.ULTRA_BALL:
        this.doUbOpenParticles(x, y);
        break;
      case PokeballType.MASTER_BALL:
        this.doMbOpenParticles(x, y);
        break;
    }
  }

  private doDefaultPbOpenParticles(x: number, y: number, radius: number): void {
    const pbOpenParticlesFrameNames = globalScene.anims.generateFrameNames("pb_particles", {
      start: 0,
      end: 3,
      suffix: ".png",
    });
    if (!globalScene.anims.exists("pb_open_particle")) {
      globalScene.anims.create({
        key: "pb_open_particle",
        frames: pbOpenParticlesFrameNames,
        frameRate: 16,
        repeat: -1,
      });
    }

    const addParticle = (index: number) => {
      const particle = globalScene.add.sprite(x, y, "pb_open_particle");
      globalScene.field.add(particle);
      const angle = index * 45;
      const [xCoord, yCoord] = [radius * Math.cos((angle * Math.PI) / 180), radius * Math.sin((angle * Math.PI) / 180)];
      globalScene.tweens.add({
        targets: particle,
        x: x + xCoord,
        y: y + yCoord,
        duration: 575,
      });
      particle.play({
        key: "pb_open_particle",
        startFrame: (index + 3) % 4,
        frameRate: Math.floor(16 * settings.general.gameSpeed),
      });
      globalScene.tweens.add({
        targets: particle,
        delay: 500,
        duration: 75,
        alpha: 0,
        ease: "Sine.easeIn",
        onComplete: () => particle.destroy(),
      });
    };

    let particleCount = 0;
    globalScene.time.addEvent({
      delay: 20,
      repeat: 16,
      callback: () => addParticle(++particleCount),
    });
  }

  private doMbOpenParticles(x: number, y: number): void {
    const particles: Phaser.GameObjects.Image[] = [];
    for (let i = 0; i < 2; i++) {
      for (let j = 0; j < 8; j++) {
        particles.push(this.doFanOutParticle(j * 32, x, y, i ? 1 : 2, j ? 2 : 1, 8, 4));
      }

      globalScene.tweens.add({
        targets: particles,
        delay: 750,
        duration: 250,
        alpha: 0,
        ease: "Sine.easeIn",
        onComplete: () => {
          for (const particle of particles) {
            particle.destroy();
          }
        },
      });
    }
  }

  private doUbOpenParticles(x: number, y: number): void {
    const particles: Phaser.GameObjects.Image[] = [];
    for (let i = 0; i < 10; i++) {
      particles.push(this.doFanOutParticle(i * 25, x, y, 1, 1, 5, 8));
    }

    globalScene.tweens.add({
      targets: particles,
      delay: 750,
      duration: 250,
      alpha: 0,
      ease: "Sine.easeIn",
      onComplete: () => {
        for (const particle of particles) {
          particle.destroy();
        }
      },
    });
  }

  private doFanOutParticle(
    trigIndex: number,
    x: number,
    y: number,
    xSpeed: number,
    ySpeed: number,
    angle: number,
    frameIndex: number,
  ): Phaser.GameObjects.Image {
    let f = 0;

    const particle = globalScene.add.image(x, y, "pb_particles", `${frameIndex}.png`);
    globalScene.field.add(particle);

    const updateParticle = () => {
      if (!particle.scene) {
        return particleTimer.remove();
      }
      particle.x = x + this.sin(trigIndex, f * xSpeed);
      particle.y = y + this.cos(trigIndex, f * ySpeed);
      trigIndex += angle;
      f++;
    };

    const particleTimer = globalScene.tweens.addCounter({
      repeat: -1,
      duration: getFrameMs(1),
      onRepeat: () => {
        updateParticle();
      },
    });

    return particle;
  }

  public addPokeballCaptureStars(pokeball: Phaser.GameObjects.Sprite): void {
    const addParticle = (): void => {
      const particle = globalScene.add.sprite(pokeball.x, pokeball.y, "pb_particles", "4.png");
      particle.setOrigin(pokeball.originX, pokeball.originY);
      particle.setAlpha(0.5);
      globalScene.field.add(particle);

      globalScene.tweens.add({
        targets: particle,
        y: pokeball.y - 10,
        ease: "Sine.easeOut",
        duration: 250,
        onComplete: () => {
          globalScene.tweens.add({
            targets: particle,
            y: pokeball.y,
            alpha: 0,
            ease: "Sine.easeIn",
            duration: 250,
          });
        },
      });

      const dist = randGauss(25);
      globalScene.tweens.add({
        targets: particle,
        x: pokeball.x + dist,
        duration: 500,
      });

      globalScene.tweens.add({
        targets: particle,
        alpha: 0,
        delay: 425,
        duration: 75,
        onComplete: () => particle.destroy(),
      });
    };

    const numStars = 3;
    for (let i = 0; i < numStars; i++) {
      addParticle();
    }
  }

  /**
   * Play the shiny sparkle animation and sound effect for the given sprite
   * First ensures that the animation has been properly initialized
   * @param sparkleSprite the Sprite to play the animation on
   * @param variant which shiny {@linkcode variant} to play the animation for
   */
  public doShinySparkleAnim(sparkleSprite: Phaser.GameObjects.Sprite, variant: Variant): void {
    const keySuffix = variant ? `_${variant + 1}` : "";
    const spriteKey = `shiny${keySuffix}`;
    const animationKey = `sparkle${keySuffix}`;

    // Make sure the animation exists, and create it if not
    if (!globalScene.anims.exists(animationKey)) {
      const frameNames = globalScene.anims.generateFrameNames(spriteKey, { suffix: ".png", end: 34 });
      globalScene.anims.create({
        key: `sparkle${keySuffix}`,
        frames: frameNames,
        frameRate: 32,
        showOnStart: true,
        hideOnComplete: true,
      });
    }

    // Play the animation
    sparkleSprite.play(animationKey);
    audioManager.playSound("se/sparkle");
  }

  public cos(index: number, amplitude: number): number {
    return amplitude * Math.cos(index * (Math.PI / 128));
  }

  public sin(index: number, amplitude: number): number {
    return amplitude * Math.sin(index * (Math.PI / 128));
  }
}