Repository: Despair-Games/poketernity
Pull request #1079: [Refactor] Rewrite Battle Animation config interfaces
Author: login: innerthunder
Labels: name: Refactor
description: For refactoring code
name: Art / Graphics
description: For issues/PRs related to graphics
Created: 2025-04-27T09:55:28Z | Merged: 2025-05-03T23:12:43Z

## What are the changes the user will see?

N/A

## Why am I making these changes?

This expands on the schemas introduced in #920 and #973 for seamless type-safe validation.
Part of #948 

## What are the changes from a developer perspective?

- `anim-config.ts`: Added new interfaces `AnimConfig`, `AnimProp`, `AnimKeyFrame<T>`, and `AnimTimedEvent`. The old `AnimConfig` and `AnimTimedEvent` have been renamed to `LegacyAnimConfig` and `LegacyAnimTimedEvent`, respectfully, and both have been marked as deprecated.
- `anim-config-schema.ts`: All schemas are now of the utility type `JSONSchemaType<T>`, where `T` is their corresponding interface.

> [!NOTE]
> Ajv for TS has a known limitation where `JSONSchemaType<T>` cannot resolve correctly when T is a generic type (https://github.com/ajv-validator/ajv/issues/2202). As a result, the generic `getKeyFrameSetSchema` function had to be broken up into three separate functions (for `number`, `boolean`, and `number[]` value types).

## Screenshots/Videos

In `test\animations\validate-battle-anims.test.ts`, we can see the validator acts as a type guard for the `AnimConfig` interface:

![image](https://github.com/user-attachments/assets/adc1ca5b-5f7b-4531-bb6c-1a33f762bceb)

## How to test the changes?

Code review only...

## Checklist

- [ ] ⚠️ If this is a PR for `main` (such as a hotfix), has the game version been updated (`npm run update-version:patch` / `npm run update version:minor`?
- [x] Otherwise: **I'm using `beta` as my base branch**
- [x] There is no overlap with another PR?
- [x] The PR is self-contained and cannot be split into smaller PRs?
- [x] Have I provided a clear explanation of the changes?
- [x] Have I tested the changes manually?
<!-- We have heavily optimized our test suite, so please actually run the tests :) -->
- [x] Are all unit tests still passing? (`npm run test:silent`)
  - [n/a] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes?
- [x] Have I provided screenshots/videos of the changes (if applicable)?
  - [n/a] Have I made sure that any UI change works for both UI themes (dark and light)?


Diff:
diff --git a/src/data/animations/anim-config-schema.ts b/src/data/animations/anim-config-schema.ts
index 7b3d3ed651..c5d52f15db 100644
--- a/src/data/animations/anim-config-schema.ts
+++ b/src/data/animations/anim-config-schema.ts
@@ -1,74 +1,126 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
+import { AnimTimedEventType } from "#enums/anim-timed-event-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+/** JSON Schema properties applicable to keyframes for any animation property. */
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
+
+/**
+ * JSON Schema property for the easing specification available for most
+ * number-based animation properties.
+ */
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+/**
+ * Generates a JSON Schema for an array of keyframes with boolean values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
+ * {
+ *   type: "boolean",
+ * }
+ * ```
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
+ */
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      /** @todo Remove `easeOption` from this */
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
-  };
-
-  const easeOption = {
-    /**
-     * The easing function used to interpolate intermediate values during the tween.
-     * @see {@linkcode easeFunctions}
-     */
-    ease: { enum: easeFunctions },
-  };
-  const ease = easeable ? easeOption : {};
+    minItems: 1,
+  } as const;
+}
 
+/**
+ * Generates a JSON Schema for an array of keyframes with number array values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
+ * {
+ *   type: "array",
+ *   items: { type: "number" },
+ *   minItems: 1,
+ * }
+ * ```
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
+ */
+function getNumberArrayKeyFrameSetSchema(
+  valueSpec: JSONSchemaType<number[]>,
+): JSONSchemaType<AnimKeyFrame<number[]>[]> {
   return {
     type: "array",
     items: {
       type: "object",
-      properties: { value: valueSpec, ...keyframeOptions, ...ease },
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
       required: ["value"],
     },
-    nullable: true,
   };
 }
 
 /**
- * Schema for a single property of an animated object.
- * This specifies the property's {@linkcode AnimFrameTargets | focus}
- * and contains the {@linkcode keyFrameSchema | keyframes} applied
- * to the focus.
+ * Schema containing the properties of an asset in an animation.
+ * Each individual property is defined with a {@linkcode getKeyFrameSetSchema | set of keyframes}.
  */
-const animPropSchema: Schema = {
+const animPropSchema: JSONSchemaType<AnimProp> = {
   type: "object",
   properties: {
     /**
@@ -78,14 +130,14 @@ const animPropSchema: Schema = {
      * the origin point is away from the source. If `u = 0`, then the origin point is
      * the source; if `u = 1`, then the origin point is the target.
      */
-    u: getKeyFrameSetSchema({
+    u: getNumberKeyFrameSetSchema({
       type: "number",
       minimum: 0,
       maximum: 1,
     }),
 
     /** The horizontal coordinate relative to the keyframe's origin point. */
-    x: getKeyFrameSetSchema({
+    x: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
@@ -93,28 +145,37 @@ const animPropSchema: Schema = {
      * The vertical coordinate relative to the keyframe's origin point.
      * An increase in `y` will move the sprite downward.
      */
-    y: getKeyFrameSetSchema({
+    y: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /** Horizontal scale factor (%) */
-    scaleX: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleX: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** Vertical scale factor (%) */
-    scaleY: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleY: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** The alpha value for the animated sprite, in the range [0, 255] */
-    alpha: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-      maximum: 255,
-    }),
+    alpha: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+        maximum: 255,
+      }),
+      nullable: true,
+    },
 
     /**
      * The rotation angle of the sprite in degrees.
@@ -122,48 +183,63 @@ const animPropSchema: Schema = {
      * 90 is down, and -90 is up. The value of this should be in the interval
      * [-180, 180].
      */
-    angle: getKeyFrameSetSchema({
-      type: "number",
-      minimum: -180,
-      maximum: 180,
-    }),
+    angle: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: -180,
+        maximum: 180,
+      }),
+      nullable: true,
+    },
 
     /** If `true`, flips the sprite horizontally */
-    mirror: getKeyFrameSetSchema({ type: "boolean" }, false),
+    mirror: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /** If `false`, hides the sprite */
-    visible: getKeyFrameSetSchema({ type: "boolean" }, false),
+    visible: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /**
      * The blend mode to specify how the sprite is rendered on the canvas
      * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
-     * @todo
-     * - Is this still required?
-     * - Should it be a keyframe property?
      */
-    blendType: getKeyFrameSetSchema({ enum: Object.values(AnimBlendType) }, false),
+    blendType: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: getEnumValues(AnimBlendType),
+      }),
+      nullable: true,
+    },
 
     /**
      * If this keyframe is for a graphic, specifies the tile index used
      * for the graphic during the tween. This is only relevant for VFX
      * properties.
      */
-    graphicFrame: getKeyFrameSetSchema({ type: "integer" }, false),
+    graphicFrame: {
+      ...getNumberKeyFrameSetSchema({ type: "integer" }),
+      nullable: true,
+    },
 
-    /** A tone to pipeline over the animated sprite (RGBA) */
-    tone: getKeyFrameSetSchema(
-      {
+    /** A tone to pipeline over the animated sprite (RGBA, normalized) */
+    tone: {
+      ...getNumberArrayKeyFrameSetSchema({
         type: "array",
         items: {
           type: "number",
           minimum: 0,
-          maximum: 255,
+          maximum: 1,
         },
         minItems: 3,
         maxItems: 4,
-      },
-      false,
-    ),
+      }),
+      nullable: true,
+    },
 
     /**
      * The z-depth of the animated sprite during the tween
@@ -172,26 +248,36 @@ const animPropSchema: Schema = {
      * - 3 is on top of both fields
      * - 5 is on top of player sprite
      */
-    priority: getKeyFrameSetSchema({ enum: [0, 1, 3, 5] }, false),
+    priority: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: [0, 1, 3, 5],
+      }),
+      nullable: true,
+    },
   },
-};
+  required: ["u", "x", "y"],
+  additionalProperties: false,
+  nullable: true,
+} as const;
 
 /**
  * Schema for a timed event to play during an animation.
  * This may include a {@linkcode AnimTimedSoundEvent | sound effect}
  * or an {@linkcode AnimTimedAddBgEvent | update to the background image}
  */
-const animTimedEventSchema: Schema = {
+const animTimedEventSchema: JSONSchemaType<AnimTimedEvent> = {
   type: "object",
   properties: {
     // Required fields
 
     /** The type of event to execute. */
     eventType: {
-      enum: ["AnimTimedSoundEvent", "AnimTimedAddBgEvent", "AnimTimedUpdateBgEvent"],
+      type: "string",
+      enum: Object.values(AnimTimedEventType),
     },
 
-    /** The delay from the start of the animation to the given event (ms) */
+    /** The delay from the start of the animation to the given event (in frames) */
     time: {
       type: "number",
       minimum: 0,
@@ -208,6 +294,8 @@ const animTimedEventSchema: Schema = {
     volume: {
       type: "number",
       default: 100,
+      minimum: 0,
+      nullable: true,
     },
 
     /**
@@ -217,6 +305,8 @@ const animTimedEventSchema: Schema = {
     pitch: {
       type: "number",
       default: 100,
+      minimum: 0,
+      nullable: true,
     },
 
     // Background Image Event fields
@@ -225,32 +315,36 @@ const animTimedEventSchema: Schema = {
     bgX: {
       type: "number",
       default: 0,
+      nullable: true,
     },
 
     /** The y-coordinate of the background image */
     bgY: {
       type: "number",
       default: 0,
+      nullable: true,
     },
 
-    /** The amount of time the image is displayed (ms) */
+    /** The duration the image is displayed (in frames) */
     duration: {
       type: "number",
-      /** @todo Should this default be kept? */
-      default: 0,
+      nullable: true,
     },
 
     /** Scale factor (%) for the background image */
     scale: {
       type: "number",
       default: 100,
+      nullable: true,
     },
   },
-  required: ["time", "resourceName"],
-  if: { properties: { eventType: { enum: ["AnimTimedSoundEvent"] } } },
+  nullable: true,
+  additionalProperties: false,
+  required: ["eventType", "time", "resourceName"],
+  if: { properties: { eventType: { const: "AnimTimedSoundEvent" } } },
   then: { required: ["volume", "pitch"] },
   else: { required: ["bgX", "bgY", "duration", "scale"] },
-};
+} as const;
 
 /**
  * Schema for the config of a battle animation.
@@ -259,7 +353,7 @@ const animTimedEventSchema: Schema = {
  * the given set of {@linkcode keyFrameSchema | keyframes}.
  * @see {@linkcode AnimConfig}
  */
-export const animConfigSchema: Schema = {
+export const animConfigSchema: JSONSchemaType<AnimConfig> = {
   type: "object",
   properties: {
     /**
@@ -267,33 +361,34 @@ export const animConfigSchema: Schema = {
      * associated with the anim (if applicable).
      * This is required for all {@linkcode MoveAnim | MoveAnims}.
      */
-    id: { enum: Object.values(MoveId) },
+    moveId: {
+      type: "integer",
+      enum: getEnumValues(MoveId),
+      nullable: true,
+    },
 
-    /**
-     * The name of the tileset used for the animation.
-     * @todo Should this be required?
-     */
-    graphic: { type: "string" },
+    /** The name of the tileset used for the animation. */
+    graphic: { type: "string", nullable: true },
 
     /**
      * Contains all properties applied to the sprite of the
      * "source" object (e.g. the {@linkcode Pokemon} using a move)
      * @see {@linkcode animPropSchema}
      */
-    sourceProperties: animPropSchema,
+    sourceProperties: { ...animPropSchema, nullable: true },
 
     /**
      * Contains all properties applied to the sprite of the
      * "target" object (e.g. the {@linkcode Pokemon} attacked by a move)
      * @see {@linkcode animPropSchema}
      */
-    targetProperties: animPropSchema,
+    targetProperties: { ...animPropSchema, nullable: true },
 
     /**
      * Contains all properties applied to the animation's VFX
      * @see {@linkcode animPropSchema}
      */
-    vfxProperties: animPropSchema,
+    vfxProperties: { ...animPropSchema, nullable: true },
 
     /**
      * Contains all timed events played during the animation.
@@ -303,7 +398,9 @@ export const animConfigSchema: Schema = {
       type: "array",
       items: animTimedEventSchema,
       minItems: 1,
+      nullable: true,
     },
   },
-  required: ["props"],
-};
+  additionalProperties: false,
+  required: [],
+} as const;
diff --git a/src/data/animations/anim-config.ts b/src/data/animations/anim-config.ts
index c397b4928b..8c2409b651 100644
--- a/src/data/animations/anim-config.ts
+++ b/src/data/animations/anim-config.ts
@@ -1,22 +1,199 @@
+// -- start tsdoc imports --
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Pokemon } from "#app/field/pokemon";
+/* eslint-enable @typescript-eslint/no-unused-vars */
+// -- end tsdoc imports --
+
+import type { easeFunctions } from "#app/data/animations/ease-functions";
 import { globalScene } from "#app/global-scene";
 import { getFrameMs, isNil } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { AnimFocus } from "#enums/anim-focus";
 import { AnimFrameTarget } from "#enums/anim-frame-target";
+import type { AnimTimedEventType } from "#enums/anim-timed-event-type";
+import type { MoveId } from "#enums/move-id";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
 
-export class AnimConfig {
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly moveId?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (normalized RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   * @todo define the allowed priority values as an enum
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
+   */
+  readonly eventType: AnimTimedEventType;
+
+  /** The start time for the event */
+  readonly time: number;
+
+  /** The name of the file containing assets used in the event */
+  readonly resourceName: string;
+
+  // Timed Sound Event fields
+
+  /**
+   * The volume of the played sound effect (%).
+   * @default 100
+   */
+  readonly volume?: number;
+
+  /**
+   * The pitch of the played sound effect (%).
+   * @default 100
+   */
+  readonly pitch?: number;
+
+  // Background Image Event fields
+
+  /** The x-coordinate of the background image */
+  readonly bgX?: number;
+
+  /** The y-coordinate of the background image */
+  readonly bgY?: number;
+
+  /** The amount of time the image is displayed (in frames) */
+  readonly duration?: number;
+
+  /**
+   * Scale factor (%) for the background image.
+   * @default 100
+   */
+  readonly scale?: number;
+}
+
+/** @deprecated to be replaced by {@linkcode AnimConfig} */
+export class LegacyAnimConfig {
   public id: number;
   public graphic: string;
   public frames: AnimFrame[][];
-  public frameTimedEvents: Map<number, AnimTimedEvent[]>;
+  public frameTimedEvents: Map<number, LegacyAnimTimedEvent[]>;
   public position: number;
   public hue: number;
 
   constructor(source?: any) {
-    this.frameTimedEvents = new Map<number, AnimTimedEvent[]>();
+    this.frameTimedEvents = new Map<number, LegacyAnimTimedEvent[]>();
 
     if (source) {
       this.id = source.id;
@@ -31,9 +208,9 @@ export class AnimConfig {
 
       const frameTimedEvents = source.frameTimedEvents;
       for (const fte of Object.keys(frameTimedEvents)) {
-        const timedEvents: AnimTimedEvent[] = [];
+        const timedEvents: LegacyAnimTimedEvent[] = [];
         for (const te of frameTimedEvents[fte]) {
-          let timedEvent: AnimTimedEvent | undefined;
+          let timedEvent: LegacyAnimTimedEvent | undefined;
           switch (te.eventType) {
             case "AnimTimedSoundEvent":
               timedEvent = new AnimTimedSoundEvent(te.frameIndex, te.resourceName, te);
@@ -298,7 +475,9 @@ class ImportedAnimFrame extends AnimFrame {
     this.graphicFrame = source.graphicFrame;
   }
 }
-export abstract class AnimTimedEvent {
+
+/** @deprecated to be replaced by {@linkcode AnimTimedEvent} */
+export abstract class LegacyAnimTimedEvent {
   public frameIndex: number;
   public resourceName: string;
 
@@ -312,7 +491,7 @@ export abstract class AnimTimedEvent {
   abstract getEventType(): string;
 }
 
-export class AnimTimedSoundEvent extends AnimTimedEvent {
+export class AnimTimedSoundEvent extends LegacyAnimTimedEvent {
   public volume: number = 100;
   public pitch: number = 100;
 
@@ -346,7 +525,7 @@ export class AnimTimedSoundEvent extends AnimTimedEvent {
   }
 }
 
-abstract class AnimTimedBgEvent extends AnimTimedEvent {
+abstract class AnimTimedBgEvent extends LegacyAnimTimedEvent {
   public bgX: number = 0;
   public bgY: number = 0;
   public opacity: number = 0;
diff --git a/src/data/animations/battle-anims.ts b/src/data/animations/battle-anims.ts
index 45f477f831..bcbe9a76df 100644
--- a/src/data/animations/battle-anims.ts
+++ b/src/data/animations/battle-anims.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig, AnimFrame } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig, AnimFrame } from "#app/data/animations/anim-config";
 import type { SubstituteTag } from "#app/data/battler-tags/substitute-tag";
 import type { Pokemon } from "#app/field/pokemon";
 import { globalScene } from "#app/global-scene";
@@ -68,7 +68,7 @@ export abstract class BattleAnim {
     this.playRegardlessOfIssues = playRegardlessOfIssues;
   }
 
-  abstract getAnim(): AnimConfig | null;
+  abstract getAnim(): LegacyAnimConfig | null;
 
   abstract isOppAnim(): boolean;
 
diff --git a/src/data/animations/charge-anims.ts b/src/data/animations/charge-anims.ts
index f5e21cb8bd..9f2366fe83 100644
--- a/src/data/animations/charge-anims.ts
+++ b/src/data/animations/charge-anims.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import type { ChargeAnim } from "#enums/charge-anim";
 
-export const chargeAnims = new Map<ChargeAnim, AnimConfig | [AnimConfig, AnimConfig] | null>();
+export const chargeAnims = new Map<ChargeAnim, LegacyAnimConfig | [LegacyAnimConfig, LegacyAnimConfig] | null>();
diff --git a/src/data/animations/common-anims.ts b/src/data/animations/common-anims.ts
index 825ee5400a..f5ca1e72c7 100644
--- a/src/data/animations/common-anims.ts
+++ b/src/data/animations/common-anims.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import type { CommonAnim } from "#enums/common-anim";
 
-export const commonAnims = new Map<CommonAnim, AnimConfig>();
+export const commonAnims = new Map<CommonAnim, LegacyAnimConfig>();
diff --git a/src/data/animations/common-battle-anim.ts b/src/data/animations/common-battle-anim.ts
index 1c8954fdba..b9f8ac5119 100644
--- a/src/data/animations/common-battle-anim.ts
+++ b/src/data/animations/common-battle-anim.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { BattleAnim } from "./battle-anims";
 import { commonAnims } from "#app/data/animations/common-anims";
 import type { Pokemon } from "#app/field/pokemon";
@@ -21,7 +21,7 @@ export class CommonBattleAnim extends BattleAnim {
     this.commonAnim = commonAnim;
   }
 
-  getAnim(): AnimConfig | null {
+  getAnim(): LegacyAnimConfig | null {
     return this.commonAnim ? (commonAnims.get(this.commonAnim) ?? null) : null;
   }
 
diff --git a/src/data/animations/ease-functions.ts b/src/data/animations/ease-functions.ts
index 1927bdf458..206dd2d7e0 100644
--- a/src/data/animations/ease-functions.ts
+++ b/src/data/animations/ease-functions.ts
@@ -33,4 +33,4 @@ export const easeFunctions = [
   "Bounce.easeInOut",
   "Elastic",
   "Stepped",
-];
+] as const;
diff --git a/src/data/animations/encounter-anims.ts b/src/data/animations/encounter-anims.ts
index 9c9ed3c567..79a32668f4 100644
--- a/src/data/animations/encounter-anims.ts
+++ b/src/data/animations/encounter-anims.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import type { EncounterAnim } from "#enums/encounter-anims";
 
-export const encounterAnims = new Map<EncounterAnim, AnimConfig>();
+export const encounterAnims = new Map<EncounterAnim, LegacyAnimConfig>();
diff --git a/src/data/animations/encounter-battle-anim.ts b/src/data/animations/encounter-battle-anim.ts
index ce61b544db..618139a1d9 100644
--- a/src/data/animations/encounter-battle-anim.ts
+++ b/src/data/animations/encounter-battle-anim.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import type { Pokemon } from "#app/field/pokemon";
 import type { EncounterAnim } from "#enums/encounter-anims";
 import { BattleAnim } from "./battle-anims";
@@ -22,7 +22,7 @@ export class EncounterBattleAnim extends BattleAnim {
     this.oppAnim = oppAnim ?? false;
   }
 
-  getAnim(): AnimConfig | null {
+  getAnim(): LegacyAnimConfig | null {
     return encounterAnims.get(this.encounterAnim) ?? null;
   }
 
diff --git a/src/data/animations/move-anim.ts b/src/data/animations/move-anim.ts
index e8e015b417..db432d59b8 100644
--- a/src/data/animations/move-anim.ts
+++ b/src/data/animations/move-anim.ts
@@ -1,5 +1,5 @@
 import { allMoves } from "#app/data/data-lists";
-import { AnimConfig } from "#app/data/animations/anim-config";
+import { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { BattleAnim } from "./battle-anims";
 import { moveAnims } from "#app/data/animations/move-anims";
 import type { Pokemon } from "#app/field/pokemon";
@@ -22,10 +22,10 @@ export class MoveAnim extends BattleAnim {
     this.moveId = move;
   }
 
-  getAnim(): AnimConfig {
+  getAnim(): LegacyAnimConfig {
     const anim = moveAnims.get(this.moveId);
     const animSource = this.user?.isPlayer() ? 0 : 1;
-    return anim instanceof AnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
+    return anim instanceof LegacyAnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
   }
 
   isOppAnim(): boolean {
diff --git a/src/data/animations/move-anims.ts b/src/data/animations/move-anims.ts
index 619493a667..61c817786d 100644
--- a/src/data/animations/move-anims.ts
+++ b/src/data/animations/move-anims.ts
@@ -1,4 +1,4 @@
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import type { MoveId } from "#enums/move-id";
 
-export const moveAnims = new Map<MoveId, AnimConfig | [AnimConfig, AnimConfig] | null>();
+export const moveAnims = new Map<MoveId, LegacyAnimConfig | [LegacyAnimConfig, LegacyAnimConfig] | null>();
diff --git a/src/data/animations/move-charge-anim.ts b/src/data/animations/move-charge-anim.ts
index d7a4d1a784..127430851d 100644
--- a/src/data/animations/move-charge-anim.ts
+++ b/src/data/animations/move-charge-anim.ts
@@ -1,4 +1,4 @@
-import { AnimConfig } from "#app/data/animations/anim-config";
+import { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { MoveAnim } from "./move-anim";
 import { chargeAnims } from "#app/data/animations/charge-anims";
 import type { Pokemon } from "#app/field/pokemon";
@@ -22,9 +22,9 @@ export class MoveChargeAnim extends MoveAnim {
     return !this.user?.isPlayer() && Array.isArray(chargeAnims.get(this.chargeAnim));
   }
 
-  override getAnim(): AnimConfig {
-    return chargeAnims.get(this.chargeAnim) instanceof AnimConfig
-      ? (chargeAnims.get(this.chargeAnim) as AnimConfig)
-      : (chargeAnims.get(this.chargeAnim)?.[this.user?.isPlayer() ? 0 : 1] as AnimConfig);
+  override getAnim(): LegacyAnimConfig {
+    return chargeAnims.get(this.chargeAnim) instanceof LegacyAnimConfig
+      ? (chargeAnims.get(this.chargeAnim) as LegacyAnimConfig)
+      : (chargeAnims.get(this.chargeAnim)?.[this.user?.isPlayer() ? 0 : 1] as LegacyAnimConfig);
   }
 }
diff --git a/src/data/init/init-anims.ts b/src/data/init/init-anims.ts
index 60bf9b5242..3e535eca84 100644
--- a/src/data/init/init-anims.ts
+++ b/src/data/init/init-anims.ts
@@ -1,7 +1,7 @@
 import {
-  AnimConfig,
+  LegacyAnimConfig,
   AnimFrame,
-  type AnimTimedEvent,
+  type LegacyAnimTimedEvent,
   AnimTimedSoundEvent,
   AnimTimedAddBgEvent,
   AnimTimedUpdateBgEvent,
@@ -55,18 +55,20 @@ export async function populateAnims() {
     if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) {
       continue;
     }
-    const anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig();
-    if (anim instanceof AnimConfig) {
-      (anim as AnimConfig).id = moveNameToId[animName];
+    const anim = commonAnimId || chargeAnimId ? new LegacyAnimConfig() : new LegacyAnimConfig();
+    if (anim instanceof LegacyAnimConfig) {
+      (anim as LegacyAnimConfig).id = moveNameToId[animName];
     }
     if (commonAnimId) {
       commonAnims.set(commonAnimId, anim);
     } else if (chargeAnimId) {
-      chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as AnimConfig, anim]);
+      chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as LegacyAnimConfig, anim]);
     } else {
       moveAnims.set(
         moveNameToId[animName],
-        !isOppMove ? (anim as AnimConfig) : [moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig],
+        !isOppMove
+          ? (anim as LegacyAnimConfig)
+          : [moveAnims.get(moveNameToId[animName]) as LegacyAnimConfig, anim as LegacyAnimConfig],
       );
     }
     for (let f = 0; f < fields.length; f++) {
@@ -132,7 +134,7 @@ export async function populateAnims() {
             const frameIndex = parseInt(/frame: (\d+)/.exec(timingData)![1]); // TODO: is the bang correct?
             let resourceName = /name: "(.*?)"/.exec(timingData)![1].replace("''", ""); // TODO: is the bang correct?
             const timingType = parseInt(/timingType: (\d)/.exec(timingData)![1]); // TODO: is the bang correct?
-            let timedEvent: AnimTimedEvent | undefined;
+            let timedEvent: LegacyAnimTimedEvent | undefined;
             switch (timingType) {
               case 0:
                 if (resourceName && resourceName.indexOf(".") === -1) {
diff --git a/src/data/init/init-common-anims.ts b/src/data/init/init-common-anims.ts
index d70dda0089..df945643e5 100644
--- a/src/data/init/init-common-anims.ts
+++ b/src/data/init/init-common-anims.ts
@@ -1,5 +1,5 @@
 import { commonAnims } from "../animations/common-anims";
-import { AnimConfig } from "../animations/anim-config";
+import { LegacyAnimConfig } from "../animations/anim-config";
 import { globalScene } from "#app/global-scene";
 import { getEnumKeys, getEnumValues } from "#app/utils/common-utils";
 import { CommonAnim } from "#enums/common-anim";
@@ -8,14 +8,14 @@ export function initCommonAnims(): Promise<void> {
   return new Promise((resolve) => {
     const commonAnimNames = getEnumKeys(CommonAnim);
     const commonAnimIds = getEnumValues(CommonAnim);
-    const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
+    const commonAnimFetches: Promise<Map<CommonAnim, LegacyAnimConfig>>[] = [];
     for (let ca = 0; ca < commonAnimIds.length; ca++) {
       const commonAnimId = commonAnimIds[ca];
       commonAnimFetches.push(
         globalScene
           .cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, "-")}.json`)
           .then((response) => response.json())
-          .then((cas) => commonAnims.set(commonAnimId, new AnimConfig(cas))),
+          .then((cas) => commonAnims.set(commonAnimId, new LegacyAnimConfig(cas))),
       );
     }
     Promise.allSettled(commonAnimFetches).then(() => resolve());
diff --git a/src/data/init/init-encounter-anims.ts b/src/data/init/init-encounter-anims.ts
index b5e31283a5..c90afa7eee 100644
--- a/src/data/init/init-encounter-anims.ts
+++ b/src/data/init/init-encounter-anims.ts
@@ -1,4 +1,4 @@
-import { AnimConfig } from "#app/data/animations/anim-config";
+import { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { encounterAnims } from "#app/data/animations/encounter-anims";
 import { globalScene } from "#app/global-scene";
 import { getEnumKeys, isNil } from "#app/utils/common-utils";
@@ -11,7 +11,7 @@ import { EncounterAnim } from "#enums/encounter-anims";
 export async function initEncounterAnims(encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> {
   const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim];
   const encounterAnimNames = getEnumKeys(EncounterAnim);
-  const encounterAnimFetches: Promise<Map<EncounterAnim, AnimConfig>>[] = [];
+  const encounterAnimFetches: Promise<Map<EncounterAnim, LegacyAnimConfig>>[] = [];
   for (const anim of anims) {
     if (encounterAnims.has(anim) && !isNil(encounterAnims.get(anim))) {
       continue;
@@ -20,7 +20,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
       globalScene
         .cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
         .then((response) => response.json())
-        .then((cas) => encounterAnims.set(anim, new AnimConfig(cas))),
+        .then((cas) => encounterAnims.set(anim, new LegacyAnimConfig(cas))),
     );
   }
   await Promise.allSettled(encounterAnimFetches);
diff --git a/src/data/init/init-move-anim.ts b/src/data/init/init-move-anim.ts
index b53066cf7e..989216dba4 100644
--- a/src/data/init/init-move-anim.ts
+++ b/src/data/init/init-move-anim.ts
@@ -1,7 +1,7 @@
 import { allMoves } from "#app/data/data-lists";
 import { chargeAnims } from "#app/data/animations/charge-anims";
 import { moveAnims } from "#app/data/animations/move-anims";
-import { AnimConfig } from "#app/data/animations/anim-config";
+import { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { initMoveChargeAnim } from "#app/data/init/init-move-charge-anim";
 import type { ChargingMove } from "#app/data/moves/move";
 import { BeakBlastHeaderAttr } from "#app/data/moves/move-attrs/beak-blast-header-attr";
@@ -107,12 +107,12 @@ function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) {
 }
 
 function populateMoveAnim(move: MoveId, animSource: any): void {
-  const moveAnim = new AnimConfig(animSource);
+  const moveAnim = new LegacyAnimConfig(animSource);
   if (moveAnims.get(move) === null) {
     moveAnims.set(move, moveAnim);
     return;
   }
-  moveAnims.set(move, [moveAnims.get(move) as AnimConfig, moveAnim]);
+  moveAnims.set(move, [moveAnims.get(move) as LegacyAnimConfig, moveAnim]);
 }
 
 //#endregion
diff --git a/src/data/init/init-move-charge-anim.ts b/src/data/init/init-move-charge-anim.ts
index 04e6fa9c13..b2a731bf90 100644
--- a/src/data/init/init-move-charge-anim.ts
+++ b/src/data/init/init-move-charge-anim.ts
@@ -1,5 +1,5 @@
 import { chargeAnims } from "../animations/charge-anims";
-import { AnimConfig } from "../animations/anim-config";
+import { LegacyAnimConfig } from "../animations/anim-config";
 import { globalScene } from "#app/global-scene";
 import { ChargeAnim } from "#enums/charge-anim";
 
@@ -40,12 +40,12 @@ export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise<void> {
 //#region Helpers
 
 function populateMoveChargeAnim(chargeAnim: ChargeAnim, animSource: any) {
-  const moveChargeAnim = new AnimConfig(animSource);
+  const moveChargeAnim = new LegacyAnimConfig(animSource);
   if (chargeAnims.get(chargeAnim) === null) {
     chargeAnims.set(chargeAnim, moveChargeAnim);
     return;
   }
-  chargeAnims.set(chargeAnim, [chargeAnims.get(chargeAnim) as AnimConfig, moveChargeAnim]);
+  chargeAnims.set(chargeAnim, [chargeAnims.get(chargeAnim) as LegacyAnimConfig, moveChargeAnim]);
 }
 
 //#endregion
diff --git a/src/enums/anim-timed-event-type.ts b/src/enums/anim-timed-event-type.ts
new file mode 100644
index 0000000000..de7ab05a65
--- /dev/null
+++ b/src/enums/anim-timed-event-type.ts
@@ -0,0 +1,8 @@
+export enum AnimTimedEventType {
+  /** Plays a sound effect */
+  SOUND = "AnimTimedSoundEvent",
+  /** Adds a background sprite to display */
+  ADD_BG = "AnimTimedAddBgEvent",
+  /** Updates a displayed background sprite */
+  UPDATE_BG = "AnimTimedUpdateBgEvent",
+}
diff --git a/src/utils/anim-utils.ts b/src/utils/anim-utils.ts
index b788f11a84..6f256822fb 100644
--- a/src/utils/anim-utils.ts
+++ b/src/utils/anim-utils.ts
@@ -5,14 +5,14 @@ import type { initEncounterAnims } from "#app/data/init/init-encounter-anims";
 /* eslint-enable @typescript-eslint/no-unused-vars */
 // -- end tsdoc imports --
 
-import type { AnimConfig } from "#app/data/animations/anim-config";
+import type { LegacyAnimConfig } from "#app/data/animations/anim-config";
 import { commonAnims } from "#app/data/animations/common-anims";
 import { encounterAnims } from "#app/data/animations/encounter-anims";
 import { globalScene } from "#app/global-scene";
 import { ImagesFolder } from "#enums/images-folders";
 import Phaser from "phaser";
 
-export function loadAnimAssets(anims: AnimConfig[], startLoad?: boolean): Promise<void> {
+export function loadAnimAssets(anims: LegacyAnimConfig[], startLoad?: boolean): Promise<void> {
   return new Promise((resolve) => {
     const backgrounds = new Set<string>();
     const sounds = new Set<string>();
diff --git a/test/animations/anim-config-schema.test.ts b/test/animations/anim-config-schema.test.ts
new file mode 100644
index 0000000000..8ea2b29db0
--- /dev/null
+++ b/test/animations/anim-config-schema.test.ts
@@ -0,0 +1,265 @@
+import { animConfigSchema } from "#app/data/animations/anim-config-schema";
+import { AnimBlendType } from "#enums/anim-blend-type";
+import { MoveId } from "#enums/move-id";
+import Ajv from "ajv";
+import { describe, expect, it } from "vitest";
+
+describe("Animations - Schema", () => {
+  const ajv = new Ajv({
+    allErrors: true,
+    allowUnionTypes: true,
+  });
+
+  const validate = ajv.compile(animConfigSchema);
+
+  it("should accept configs with a fully defined source property", async () => {
+    const validConfig = {
+      moveId: MoveId.TACKLE,
+      graphic: "003-Attack01.png",
+      sourceProperties: {
+        u: [
+          {
+            value: 0,
+            duration: 0,
+            delay: 0,
+            ease: "Linear",
+          },
+          {
+            value: 1,
+            duration: 10,
+            delay: 0,
+            ease: "Sine.easeInOut",
+          },
+        ],
+
+        x: [
+          {
+            value: 10,
+            duration: 0,
+            delay: 0,
+            ease: "Linear",
+          },
+        ],
+
+        y: [
+          {
+            value: 20,
+            duration: 0,
+            delay: 10,
+            ease: "Linear",
+          },
+        ],
+
+        scaleX: [
+          {
+            value: 100,
+            duration: 10,
+            delay: 0,
+            ease: "Linear",
+          },
+        ],
+
+        scaleY: [
+          {
+            value: 1,
+            duration: 0,
+            delay: 0,
+            ease: "Stepped",
+          },
+        ],
+
+        alpha: [
+          {
+            value: 255,
+            duration: 0,
+            delay: 0,
+            ease: "Circ.easeIn",
+          },
+        ],
+
+        angle: [
+          {
+            value: 90,
+            duration: 0,
+            delay: 0,
+            ease: "Linear",
+          },
+        ],
+
+        mirror: [
+          {
+            value: true,
+            duration: 0,
+            delay: 10,
+            // Normally ease is ignored for boolean and enum properties,
+            // but this is just included for completeness.
+            ease: "Linear",
+          },
+        ],
+
+        visible: [
+          {
+            value: true,
+            duration: 0,
+            delay: 0,
+            ease: "Stepped",
+          },
+        ],
+
+        blendType: [
+          {
+            value: AnimBlendType.NORMAL,
+            duration: 0,
+            delay: 0,
+            ease: "Stepped",
+          },
+        ],
+
+        graphicFrame: [
+          {
+            value: 1,
+            duration: 0,
+            delay: 10,
+            // This is also normally ignored
+            ease: "Linear",
+          },
+        ],
+
+        tone: [
+          {
+            value: [1, 0, 0, 1],
+            duration: 100,
+            delay: 0,
+            ease: "Sine.easeIn",
+          },
+        ],
+
+        priority: [
+          {
+            value: 1,
+            duration: 0,
+            delay: 100,
+            // This is also normally ignored
+            ease: "Stepped",
+          },
+        ],
+      },
+      /**
+       * `targetProperties` and `vfxProperties` are omitted, but the config
+       * should still be valid since all prop sets are optional.
+       * Similarly, `timedEvents` should not be required.
+       */
+    };
+
+    const result = validate(validConfig);
+    expect(result, `${validate.errors?.map((err) => err.message)}`).toBeTruthy();
+  });
+
+  it("should accept configs with partially defined properties", async () => {
+    const validPartialConfig = {
+      moveId: MoveId.TACKLE,
+      graphic: "003-Attack01.png",
+
+      sourceProperties: {
+        u: [
+          {
+            value: 0.5,
+            duration: 100,
+            // delay, ease omitted
+          },
+        ],
+
+        x: [
+          {
+            value: 10,
+            duration: 100,
+            ease: "Sine.easeInOut",
+          },
+          {
+            value: 20,
+            duration: 50,
+            delay: 10,
+            ease: "Linear",
+          },
+        ],
+
+        y: [
+          {
+            value: 1,
+            duration: 1,
+            delay: 1,
+          },
+        ],
+
+        scaleX: [
+          {
+            value: 50,
+            delay: 50,
+          },
+        ],
+      },
+
+      timedEvents: [
+        {
+          eventType: "AnimTimedSoundEvent",
+          time: 10,
+          resourceName: "PRSFX- Tackle.wav",
+          volume: 50,
+          pitch: 100,
+        },
+      ],
+    };
+
+    const result = validate(validPartialConfig);
+    expect(result, `${validate.errors?.map((err) => err.message)}`).toBeTruthy();
+  });
+
+  it("should not accept configs with unrecognized fields", async () => {
+    const invalidConfig = {
+      moveId: MoveId.TACKLE,
+      graphic: "003-Attack01.png",
+
+      // This is intentionally misspelled
+      sorceProperties: {
+        u: [
+          {
+            value: 1,
+            duration: 0,
+          },
+        ],
+
+        x: [{ value: 0 }],
+
+        y: [{ value: 1 }],
+      },
+    };
+
+    const result = validate(invalidConfig);
+    expect(result).toBeFalsy();
+  });
+
+  it("should not accept configs with invalid property values", async () => {
+    const invalidConfig = {
+      moveId: MoveId.TACKLE,
+      graphic: "003-Attack01.png",
+
+      sourceProperties: {
+        u: [
+          {
+            // U-value must be in the range [0, 1]
+            value: 2,
+            duration: 100,
+            ease: "Linear",
+          },
+        ],
+
+        x: [{ value: 0 }],
+
+        y: [{ value: 0 }],
+      },
+    };
+
+    const result = validate(invalidConfig);
+    expect(result).toBeFalsy();
+  });
+});


Reviews:
user: login: Tempo-anon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:56:43Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:36:41Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:38:18Z
user: login: Tempo-anon
body: Looks ok to me but should probably have the approval of someone more experienced with animation
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T02:24:18Z
user: login: flx-sta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T17:17:49Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T20:23:58Z
user: login: flx-sta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T20:26:25Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T20:32:49Z
user: login: flx-sta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T20:39:11Z
user: login: pom-eranian
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T21:10:57Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T21:57:38Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T21:59:55Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:01:17Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:02:53Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:04:39Z
user: login: flx-sta
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T22:08:39Z
user: login: flx-sta
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T00:50:09Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:52:23Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:56:16Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:56:21Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:57:18Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:59:12Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:02:00Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:02:30Z
user: login: DayKev
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:06:16Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:07:58Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:47:51Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:50:06Z
user: login: DayKev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:20:48Z
user: login: flx-sta
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:44:20Z
user: login: flx-sta
body: Any Ideas for my [previous comment](https://github.com/Despair-Games/poketernity/pull/1079#issuecomment-2845296231) @innerthunder ?

> We should have an exported version of the actual json-schema in use. _Possibly have scripts that can be run to export the schema._ So we can leverage the `$schema` key inside a json file to tell the IDE (e.g. vscode) which schema to validate against providing a better "experience" to the person _writing_ that file

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:45:31Z
user: login: DayKev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T04:33:29Z
user: login: pom-eranian
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T18:21:33Z
user: login: innerthunder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T18:56:35Z
user: login: pom-eranian
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T19:01:35Z
user: login: pom-eranian
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T19:56:12Z
user: login: pom-eranian
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T19:59:03Z

Review comments:
path: public/locales
user: login: Tempo-anon
body: Not sure this locale change belongs in this pr but w/e
created_at: 2025-04-28T16:53:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,101 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
path: src/data/animations/anim-config-schema.ts
user: login: Tempo-anon
body: just wondering if this has to be defined as `integer`
created_at: 2025-04-28T16:54:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,101 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
+
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+    readonly: true,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
path: src/data/animations/anim-config-schema.ts
user: login: Tempo-anon
body: tsdoc
created_at: 2025-04-28T16:55:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,101 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
+
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+    readonly: true,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      /** @todo Remove `easeOption` from this */
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
-  };
-
-  const easeOption = {
-    /**
-     * The easing function used to interpolate intermediate values during the tween.
-     * @see {@linkcode easeFunctions}
-     */
-    ease: { enum: easeFunctions },
-  };
-  const ease = easeable ? easeOption : {};
+    minItems: 1,
+    readonly: true,
+  } as const;
+}
 
+function getNumberArrayKeyFrameSetSchema(
path: src/data/animations/anim-config-schema.ts
user: login: Tempo-anon
body: tsdoc
created_at: 2025-04-28T16:55:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,101 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
path: src/data/animations/anim-config-schema.ts
user: login: innerthunder
body: To be clear, this is still interpreted as a `number` in TS terms (not Phaser's `integer` alias bs); it just enforces a non-negative integer duration. Decimal values can technically work, but this is necessary for the frame-based abstraction.
created_at: 2025-04-28T17:36:41Z
author_association: CONTRIBUTOR
path: public/locales
user: login: innerthunder
body: Just the result of `npm run update-locales:remote`
created_at: 2025-04-28T17:38:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: DayKev
body: Btw, what's the reason this is `0 | 1 | 3 | 5`? Why not `0 | 1 | 2 | 3` or something?
created_at: 2025-05-01T05:08:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,125 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+/** JSON Schema properties applicable to keyframes for any animation property. */
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * JSON Schema property for the easing specification available for most
+ * number-based animation properties.
+ */
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
+
+/**
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+/**
+ * Generates a JSON Schema for an array of keyframes with boolean values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
+ * {
+ *   type: "boolean",
+ * }
+ * ```
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
+ */
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      /** @todo Remove `easeOption` from this */
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: Why not remove it now?
created_at: 2025-05-01T15:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,92 +129,116 @@ const animPropSchema: Schema = {
      * the origin point is away from the source. If `u = 0`, then the origin point is
      * the source; if `u = 1`, then the origin point is the target.
      */
-    u: getKeyFrameSetSchema({
+    u: getNumberKeyFrameSetSchema({
       type: "number",
       minimum: 0,
       maximum: 1,
     }),
 
     /** The horizontal coordinate relative to the keyframe's origin point. */
-    x: getKeyFrameSetSchema({
+    x: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /**
      * The vertical coordinate relative to the keyframe's origin point.
      * An increase in `y` will move the sprite downward.
      */
-    y: getKeyFrameSetSchema({
+    y: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /** Horizontal scale factor (%) */
-    scaleX: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleX: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** Vertical scale factor (%) */
-    scaleY: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleY: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** The alpha value for the animated sprite, in the range [0, 255] */
-    alpha: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-      maximum: 255,
-    }),
+    alpha: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+        maximum: 255,
+      }),
+      nullable: true,
+    },
 
     /**
      * The rotation angle of the sprite in degrees.
      * Phaser uses a right-hand clockwise rotation system, where 0 is right,
      * 90 is down, and -90 is up. The value of this should be in the interval
      * [-180, 180].
      */
-    angle: getKeyFrameSetSchema({
-      type: "number",
-      minimum: -180,
-      maximum: 180,
-    }),
+    angle: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: -180,
+        maximum: 180,
+      }),
+      nullable: true,
+    },
 
     /** If `true`, flips the sprite horizontally */
-    mirror: getKeyFrameSetSchema({ type: "boolean" }, false),
+    mirror: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /** If `false`, hides the sprite */
-    visible: getKeyFrameSetSchema({ type: "boolean" }, false),
+    visible: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /**
      * The blend mode to specify how the sprite is rendered on the canvas
      * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
-     * @todo
-     * - Is this still required?
-     * - Should it be a keyframe property?
      */
-    blendType: getKeyFrameSetSchema({ enum: Object.values(AnimBlendType) }, false),
+    blendType: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: getEnumValues(AnimBlendType),
+      }),
+      nullable: true,
+    },
 
     /**
      * If this keyframe is for a graphic, specifies the tile index used
      * for the graphic during the tween. This is only relevant for VFX
      * properties.
      */
-    graphicFrame: getKeyFrameSetSchema({ type: "integer" }, false),
+    graphicFrame: {
+      ...getNumberKeyFrameSetSchema({ type: "integer" }),
+      nullable: true,
+    },
 
     /** A tone to pipeline over the animated sprite (RGBA) */
-    tone: getKeyFrameSetSchema(
-      {
+    tone: {
+      ...getNumberArrayKeyFrameSetSchema({
         type: "array",
         items: {
-          type: "number",
+          type: "integer",
           minimum: 0,
           maximum: 255,
         },
         minItems: 3,
         maxItems: 4,
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: Q: What's the 4th item then?
created_at: 2025-05-01T15:48:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,26 +247,36 @@ const animPropSchema: Schema = {
      * - 3 is on top of both fields
      * - 5 is on top of player sprite
      */
-    priority: getKeyFrameSetSchema({ enum: [0, 1, 3, 5] }, false),
+    priority: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: [0, 1, 3, 5],
+      }),
+      nullable: true,
+    },
   },
-};
+  required: ["u", "x", "y"],
+  additionalProperties: false,
+  nullable: true,
+} as const;
 
 /**
  * Schema for a timed event to play during an animation.
  * This may include a {@linkcode AnimTimedSoundEvent | sound effect}
  * or an {@linkcode AnimTimedAddBgEvent | update to the background image}
  */
-const animTimedEventSchema: Schema = {
+const animTimedEventSchema: JSONSchemaType<AnimTimedEvent> = {
   type: "object",
   properties: {
     // Required fields
 
     /** The type of event to execute. */
     eventType: {
+      type: "string",
       enum: ["AnimTimedSoundEvent", "AnimTimedAddBgEvent", "AnimTimedUpdateBgEvent"],
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: Should possibly be saved to an actual, exported, enum/const array.
That one should also be used in `anim-config.ts` then
created_at: 2025-05-01T15:57:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -208,6 +293,7 @@ const animTimedEventSchema: Schema = {
     volume: {
       type: "number",
       default: 100,
+      nullable: true,
     },
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: Q: no min/max?
created_at: 2025-05-01T15:57:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -217,6 +303,7 @@ const animTimedEventSchema: Schema = {
     pitch: {
       type: "number",
       default: 100,
+      nullable: true,
     },
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: Q: no min/max?
created_at: 2025-05-01T15:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,32 +312,36 @@ const animTimedEventSchema: Schema = {
     bgX: {
       type: "number",
       default: 0,
+      nullable: true,
     },
 
     /** The y-coordinate of the background image */
     bgY: {
       type: "number",
       default: 0,
+      nullable: true,
     },
 
-    /** The amount of time the image is displayed (ms) */
+    /** The amount of time the image is displayed (in frames) */
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: NIT:
```suggestion
    /** The duration the image is displayed (in frames) */
```
Using `time` and `(in frames)` is kind of contradicting.
created_at: 2025-05-01T15:58:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: Q: Does this **have** to be called `id`? It's quite generic for the fact that it's only for MoveAnimations.
created_at: 2025-05-01T17:06:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: These tuples should probably be put into their own type.
created_at: 2025-05-01T17:10:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
+   */
+  readonly eventType: string;
+
+  /** The start time for the event */
+  readonly time: number;
+
+  /** The name of the file containing assets used in the event */
+  readonly resourceName: string;
+
+  // Timed Sound Event fields
+
+  /**
+   * The volume of the played sound effect (%).
+   * @default 100
+   */
+  readonly volume?: number;
+
+  /**
+   * The pitch of the played sound effect (%).
+   * @default 100
+   */
+  readonly pitch?: number;
+
+  // Background Image Event fields
+
+  /** The x-coordinate of the background image */
+  readonly bgX?: number;
+
+  /** The y-coordinate of the background image */
+  readonly bgY?: number;
+
+  /** The amount of time the image is displayed (in frames) */
+  readonly duration?: number;
+
+  /**
+   * Scale factor (%) for the background image.
+   * @default 100
+   */
+  readonly scale?: number;
+}
 
-export class AnimConfig {
+/** @deprecated */
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: NIT: `@deprecated` should always include an info of what to use instead (if given).
<sub> A lot of larger projects don't do it and I hate having to do the research myself. </sub>
created_at: 2025-05-01T17:11:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -298,7 +473,7 @@ class ImportedAnimFrame extends AnimFrame {
     this.graphicFrame = source.graphicFrame;
   }
 }
-export abstract class AnimTimedEvent {
+export abstract class LegacyAnimTimedEvent {
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: Q: Should this also be deprecated?
created_at: 2025-05-01T17:12:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,7 +487,7 @@ export abstract class AnimTimedEvent {
   abstract getEventType(): string;
 }
 
-export class AnimTimedSoundEvent extends AnimTimedEvent {
+export class AnimTimedSoundEvent extends LegacyAnimTimedEvent {
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: Q: Should this also be deprecated?
created_at: 2025-05-01T17:12:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -346,7 +521,7 @@ export class AnimTimedSoundEvent extends AnimTimedEvent {
   }
 }
 
-abstract class AnimTimedBgEvent extends AnimTimedEvent {
+abstract class AnimTimedBgEvent extends LegacyAnimTimedEvent {
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: Q: Should this also be `Legacy*`?
created_at: 2025-05-01T17:13:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,10 +22,10 @@ export class MoveAnim extends BattleAnim {
     this.moveId = move;
   }
 
-  getAnim(): AnimConfig {
+  getAnim(): LegacyAnimConfig {
     const anim = moveAnims.get(this.moveId);
     const animSource = this.user?.isPlayer() ? 0 : 1;
-    return anim instanceof AnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
+    return anim instanceof LegacyAnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
path: src/data/animations/move-anim.ts
user: login: flx-sta
body: Still no solution for that *bang*? :( 
created_at: 2025-05-01T17:14:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,7 @@
 import {
-  AnimConfig,
+  LegacyAnimConfig,
   AnimFrame,
-  type AnimTimedEvent,
+  type LegacyAnimTimedEvent,
   AnimTimedSoundEvent,
   AnimTimedAddBgEvent,
   AnimTimedUpdateBgEvent,
path: src/data/init/init-anims.ts
user: login: flx-sta
body: @DayKev so what's the deal now with mixing non-type and type imports?
created_at: 2025-05-01T17:15:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,14 +8,14 @@ export function initCommonAnims(): Promise<void> {
   return new Promise((resolve) => {
     const commonAnimNames = getEnumKeys(CommonAnim);
     const commonAnimIds = getEnumValues(CommonAnim);
-    const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
+    const commonAnimFetches: Promise<Map<CommonAnim, LegacyAnimConfig>>[] = [];
     for (let ca = 0; ca < commonAnimIds.length; ca++) {
       const commonAnimId = commonAnimIds[ca];
       commonAnimFetches.push(
         globalScene
           .cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, "-")}.json`)
           .then((response) => response.json())
-          .then((cas) => commonAnims.set(commonAnimId, new AnimConfig(cas))),
+          .then((cas) => commonAnims.set(commonAnimId, new LegacyAnimConfig(cas))),
path: src/data/init/init-common-anims.ts
user: login: flx-sta
body: Q: What is `cas` supposed to stand for?
created_at: 2025-05-01T17:17:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,7 +20,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
       globalScene
         .cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
         .then((response) => response.json())
-        .then((cas) => encounterAnims.set(anim, new AnimConfig(cas))),
+        .then((cas) => encounterAnims.set(anim, new LegacyAnimConfig(cas))),
path: src/data/init/init-encounter-anims.ts
user: login: flx-sta
body: Q: What is `cas` supposed to stand for?
created_at: 2025-05-01T17:17:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,92 +129,116 @@ const animPropSchema: Schema = {
      * the origin point is away from the source. If `u = 0`, then the origin point is
      * the source; if `u = 1`, then the origin point is the target.
      */
-    u: getKeyFrameSetSchema({
+    u: getNumberKeyFrameSetSchema({
       type: "number",
       minimum: 0,
       maximum: 1,
     }),
 
     /** The horizontal coordinate relative to the keyframe's origin point. */
-    x: getKeyFrameSetSchema({
+    x: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /**
      * The vertical coordinate relative to the keyframe's origin point.
      * An increase in `y` will move the sprite downward.
      */
-    y: getKeyFrameSetSchema({
+    y: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /** Horizontal scale factor (%) */
-    scaleX: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleX: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** Vertical scale factor (%) */
-    scaleY: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleY: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** The alpha value for the animated sprite, in the range [0, 255] */
-    alpha: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-      maximum: 255,
-    }),
+    alpha: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+        maximum: 255,
+      }),
+      nullable: true,
+    },
 
     /**
      * The rotation angle of the sprite in degrees.
      * Phaser uses a right-hand clockwise rotation system, where 0 is right,
      * 90 is down, and -90 is up. The value of this should be in the interval
      * [-180, 180].
      */
-    angle: getKeyFrameSetSchema({
-      type: "number",
-      minimum: -180,
-      maximum: 180,
-    }),
+    angle: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: -180,
+        maximum: 180,
+      }),
+      nullable: true,
+    },
 
     /** If `true`, flips the sprite horizontally */
-    mirror: getKeyFrameSetSchema({ type: "boolean" }, false),
+    mirror: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /** If `false`, hides the sprite */
-    visible: getKeyFrameSetSchema({ type: "boolean" }, false),
+    visible: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /**
      * The blend mode to specify how the sprite is rendered on the canvas
      * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
-     * @todo
-     * - Is this still required?
-     * - Should it be a keyframe property?
      */
-    blendType: getKeyFrameSetSchema({ enum: Object.values(AnimBlendType) }, false),
+    blendType: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: getEnumValues(AnimBlendType),
+      }),
+      nullable: true,
+    },
 
     /**
      * If this keyframe is for a graphic, specifies the tile index used
      * for the graphic during the tween. This is only relevant for VFX
      * properties.
      */
-    graphicFrame: getKeyFrameSetSchema({ type: "integer" }, false),
+    graphicFrame: {
+      ...getNumberKeyFrameSetSchema({ type: "integer" }),
+      nullable: true,
+    },
 
     /** A tone to pipeline over the animated sprite (RGBA) */
-    tone: getKeyFrameSetSchema(
-      {
+    tone: {
+      ...getNumberArrayKeyFrameSetSchema({
         type: "array",
         items: {
-          type: "number",
+          type: "integer",
           minimum: 0,
           maximum: 255,
         },
         minItems: 3,
         maxItems: 4,
path: src/data/animations/anim-config-schema.ts
user: login: innerthunder
body: This is for an RGB[A] tone, the alpha (A) value is optional.
created_at: 2025-05-01T20:23:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,92 +129,116 @@ const animPropSchema: Schema = {
      * the origin point is away from the source. If `u = 0`, then the origin point is
      * the source; if `u = 1`, then the origin point is the target.
      */
-    u: getKeyFrameSetSchema({
+    u: getNumberKeyFrameSetSchema({
       type: "number",
       minimum: 0,
       maximum: 1,
     }),
 
     /** The horizontal coordinate relative to the keyframe's origin point. */
-    x: getKeyFrameSetSchema({
+    x: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /**
      * The vertical coordinate relative to the keyframe's origin point.
      * An increase in `y` will move the sprite downward.
      */
-    y: getKeyFrameSetSchema({
+    y: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /** Horizontal scale factor (%) */
-    scaleX: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleX: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** Vertical scale factor (%) */
-    scaleY: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleY: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** The alpha value for the animated sprite, in the range [0, 255] */
-    alpha: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-      maximum: 255,
-    }),
+    alpha: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+        maximum: 255,
+      }),
+      nullable: true,
+    },
 
     /**
      * The rotation angle of the sprite in degrees.
      * Phaser uses a right-hand clockwise rotation system, where 0 is right,
      * 90 is down, and -90 is up. The value of this should be in the interval
      * [-180, 180].
      */
-    angle: getKeyFrameSetSchema({
-      type: "number",
-      minimum: -180,
-      maximum: 180,
-    }),
+    angle: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: -180,
+        maximum: 180,
+      }),
+      nullable: true,
+    },
 
     /** If `true`, flips the sprite horizontally */
-    mirror: getKeyFrameSetSchema({ type: "boolean" }, false),
+    mirror: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /** If `false`, hides the sprite */
-    visible: getKeyFrameSetSchema({ type: "boolean" }, false),
+    visible: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /**
      * The blend mode to specify how the sprite is rendered on the canvas
      * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
-     * @todo
-     * - Is this still required?
-     * - Should it be a keyframe property?
      */
-    blendType: getKeyFrameSetSchema({ enum: Object.values(AnimBlendType) }, false),
+    blendType: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: getEnumValues(AnimBlendType),
+      }),
+      nullable: true,
+    },
 
     /**
      * If this keyframe is for a graphic, specifies the tile index used
      * for the graphic during the tween. This is only relevant for VFX
      * properties.
      */
-    graphicFrame: getKeyFrameSetSchema({ type: "integer" }, false),
+    graphicFrame: {
+      ...getNumberKeyFrameSetSchema({ type: "integer" }),
+      nullable: true,
+    },
 
     /** A tone to pipeline over the animated sprite (RGBA) */
-    tone: getKeyFrameSetSchema(
-      {
+    tone: {
+      ...getNumberArrayKeyFrameSetSchema({
         type: "array",
         items: {
-          type: "number",
+          type: "integer",
           minimum: 0,
           maximum: 255,
         },
         minItems: 3,
         maxItems: 4,
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: But wouldn't 0 - 255 be wrong then?
Alpha is usually 0.0 - 1.0
created_at: 2025-05-01T20:26:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,125 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+/** JSON Schema properties applicable to keyframes for any animation property. */
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * JSON Schema property for the easing specification available for most
+ * number-based animation properties.
+ */
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
+
+/**
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+/**
+ * Generates a JSON Schema for an array of keyframes with boolean values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
+ * {
+ *   type: "boolean",
+ * }
+ * ```
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
+ */
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      /** @todo Remove `easeOption` from this */
path: src/data/animations/anim-config-schema.ts
user: login: innerthunder
body: I had trouble with it when I tried earlier. I tried modifying the keyframe interface as below and couldn't get it to resolve correctly in the schema. Removing `...easeOption` from the schema also produces a type error.
```ts
export interface AnimKeyFrame<ValueType> {
  //...

  /**
   * The easing function used to interpolate intermediate values.
   * This is ignored if the keyframe's `ValueType` is `boolean`.
   */
  readonly ease?: ValueType extends boolean ? never : (typeof easeFunctions)[number];
}
```

created_at: 2025-05-01T20:32:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,74 +1,125 @@
-// -- start tsdoc imports --
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import type { AnimConfig, AnimTimedAddBgEvent, AnimTimedSoundEvent } from "#app/data/animations/anim-config";
-import type { MoveAnim } from "#app/data/animations/move-anim";
-/* eslint-enable @typescript-eslint/no-unused-vars */
-// -- end tsdoc imports --
-
+import type { AnimConfig, AnimKeyFrame, AnimProp, AnimTimedEvent } from "#app/data/animations/anim-config";
 import { easeFunctions } from "#app/data/animations/ease-functions";
+import { getEnumValues } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { MoveId } from "#enums/move-id";
-import type { Schema } from "ajv";
+import type { JSONSchemaType } from "ajv";
+
+/** JSON Schema properties applicable to keyframes for any animation property. */
+const keyframeOptions = {
+  /** The duration (in frames) of the tween played for this keyframe */
+  duration: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+
+  /**
+   * The time (in frames) between when the previous keyframe's tween
+   * ends and this keyframe's tween begins
+   */
+  delay: {
+    type: "integer",
+    minimum: 0,
+    nullable: true,
+  },
+} as const;
 
 /**
- * Constructs a {@linkcode Schema} for an array of keyframes
- * with the provided specification for `value`
- * @param valueSpec - An object containing the JSON Schema specification for a
- * `value` property, e.g.
- * ```ts
+ * JSON Schema property for the easing specification available for most
+ * number-based animation properties.
+ */
+const easeOption = {
+  /**
+   * The easing function used to interpolate intermediate values during the tween.
+   * @see {@linkcode easeFunctions}
+   */
+  ease: {
+    type: "string",
+    enum: easeFunctions,
+    nullable: true,
+  },
+} as const;
+
+/**
+ * Generates a JSON Schema for an array of keyframes with number values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
  * {
  *   type: "number",
- *   minimum: 0
+ *   minimum: 0,
  * }
  * ```
- * @param easeable - Does this property have intermediate values that are interpolated
- * with an {@linkcode easeFunctions | ease function}? (default `true`)
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
  */
-function getKeyFrameSetSchema(valueSpec: Schema, easeable: boolean = true): Schema {
-  const keyframeOptions = {
-    /** The duration (in frames) of the tween played for this keyframe */
-    duration: {
-      type: "integer",
-      minimum: 0,
+function getNumberKeyFrameSetSchema(valueSpec: JSONSchemaType<number>): JSONSchemaType<AnimKeyFrame<number>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      properties: { value: valueSpec, ...keyframeOptions, ...easeOption },
+      additionalProperties: false,
+      required: ["value"],
     },
+    minItems: 1,
+  } as const;
+}
 
-    /**
-     * The time (in frames) between when the previous keyframe's tween
-     * ends and this keyframe's tween begins
-     */
-    delay: {
-      type: "integer",
-      minimum: 0,
+/**
+ * Generates a JSON Schema for an array of keyframes with boolean values.
+ * @param valueSpec The JSON Schema specification for the "value" property, e.g.
+ * ```
+ * {
+ *   type: "boolean",
+ * }
+ * ```
+ * @returns the {@linkcode JSONSchemaType | Schema} object for the keyframe set
+ */
+function getBooleanKeyFrameSetSchema(valueSpec: JSONSchemaType<boolean>): JSONSchemaType<AnimKeyFrame<boolean>[]> {
+  return {
+    type: "array",
+    items: {
+      type: "object",
+      /** @todo Remove `easeOption` from this */
path: src/data/animations/anim-config-schema.ts
user: login: flx-sta
body: I see
created_at: 2025-05-01T20:39:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,92 +129,116 @@ const animPropSchema: Schema = {
      * the origin point is away from the source. If `u = 0`, then the origin point is
      * the source; if `u = 1`, then the origin point is the target.
      */
-    u: getKeyFrameSetSchema({
+    u: getNumberKeyFrameSetSchema({
       type: "number",
       minimum: 0,
       maximum: 1,
     }),
 
     /** The horizontal coordinate relative to the keyframe's origin point. */
-    x: getKeyFrameSetSchema({
+    x: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /**
      * The vertical coordinate relative to the keyframe's origin point.
      * An increase in `y` will move the sprite downward.
      */
-    y: getKeyFrameSetSchema({
+    y: getNumberKeyFrameSetSchema({
       type: "number",
     }),
 
     /** Horizontal scale factor (%) */
-    scaleX: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleX: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** Vertical scale factor (%) */
-    scaleY: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-    }),
+    scaleY: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+      }),
+      nullable: true,
+    },
 
     /** The alpha value for the animated sprite, in the range [0, 255] */
-    alpha: getKeyFrameSetSchema({
-      type: "number",
-      minimum: 0,
-      maximum: 255,
-    }),
+    alpha: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: 0,
+        maximum: 255,
+      }),
+      nullable: true,
+    },
 
     /**
      * The rotation angle of the sprite in degrees.
      * Phaser uses a right-hand clockwise rotation system, where 0 is right,
      * 90 is down, and -90 is up. The value of this should be in the interval
      * [-180, 180].
      */
-    angle: getKeyFrameSetSchema({
-      type: "number",
-      minimum: -180,
-      maximum: 180,
-    }),
+    angle: {
+      ...getNumberKeyFrameSetSchema({
+        type: "number",
+        minimum: -180,
+        maximum: 180,
+      }),
+      nullable: true,
+    },
 
     /** If `true`, flips the sprite horizontally */
-    mirror: getKeyFrameSetSchema({ type: "boolean" }, false),
+    mirror: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /** If `false`, hides the sprite */
-    visible: getKeyFrameSetSchema({ type: "boolean" }, false),
+    visible: {
+      ...getBooleanKeyFrameSetSchema({ type: "boolean" }),
+      nullable: true,
+    },
 
     /**
      * The blend mode to specify how the sprite is rendered on the canvas
      * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
-     * @todo
-     * - Is this still required?
-     * - Should it be a keyframe property?
      */
-    blendType: getKeyFrameSetSchema({ enum: Object.values(AnimBlendType) }, false),
+    blendType: {
+      ...getNumberKeyFrameSetSchema({
+        type: "integer",
+        enum: getEnumValues(AnimBlendType),
+      }),
+      nullable: true,
+    },
 
     /**
      * If this keyframe is for a graphic, specifies the tile index used
      * for the graphic during the tween. This is only relevant for VFX
      * properties.
      */
-    graphicFrame: getKeyFrameSetSchema({ type: "integer" }, false),
+    graphicFrame: {
+      ...getNumberKeyFrameSetSchema({ type: "integer" }),
+      nullable: true,
+    },
 
     /** A tone to pipeline over the animated sprite (RGBA) */
-    tone: getKeyFrameSetSchema(
-      {
+    tone: {
+      ...getNumberArrayKeyFrameSetSchema({
         type: "array",
         items: {
-          type: "number",
+          type: "integer",
           minimum: 0,
           maximum: 255,
         },
         minItems: 3,
         maxItems: 4,
path: src/data/animations/anim-config-schema.ts
user: login: pom-eranian
body: RGBA could be [1.0, 1.0, 1.0, 1.0] or [255, 255, 255, 255], but please don't mix types.
created_at: 2025-05-01T21:10:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,10 +22,10 @@ export class MoveAnim extends BattleAnim {
     this.moveId = move;
   }
 
-  getAnim(): AnimConfig {
+  getAnim(): LegacyAnimConfig {
     const anim = moveAnims.get(this.moveId);
     const animSource = this.user?.isPlayer() ? 0 : 1;
-    return anim instanceof AnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
+    return anim instanceof LegacyAnimConfig ? anim : anim?.[animSource]!; // TODO: resolve bang
path: src/data/animations/move-anim.ts
user: login: innerthunder
body: Once the anim is properly validated against the config interface, the bang shouldn't be necessary 😄 
created_at: 2025-05-01T21:57:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: This is a leftover from the old config layout and can definitely be changed
created_at: 2025-05-01T21:59:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: Added a `todo` for now. These values are used all over the animation code and I'd rather not bloat this PR with those changes.
created_at: 2025-05-01T22:01:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -312,7 +487,7 @@ export abstract class AnimTimedEvent {
   abstract getEventType(): string;
 }
 
-export class AnimTimedSoundEvent extends AnimTimedEvent {
+export class AnimTimedSoundEvent extends LegacyAnimTimedEvent {
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: Not deprecated, but this and the other subclasses will be rewritten to implement the new `AnimTimedEvent` interface instead of extending `LegacyAnimTimedEvent`
created_at: 2025-05-01T22:02:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,14 +8,14 @@ export function initCommonAnims(): Promise<void> {
   return new Promise((resolve) => {
     const commonAnimNames = getEnumKeys(CommonAnim);
     const commonAnimIds = getEnumValues(CommonAnim);
-    const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
+    const commonAnimFetches: Promise<Map<CommonAnim, LegacyAnimConfig>>[] = [];
     for (let ca = 0; ca < commonAnimIds.length; ca++) {
       const commonAnimId = commonAnimIds[ca];
       commonAnimFetches.push(
         globalScene
           .cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, "-")}.json`)
           .then((response) => response.json())
-          .then((cas) => commonAnims.set(commonAnimId, new AnimConfig(cas))),
+          .then((cas) => commonAnims.set(commonAnimId, new LegacyAnimConfig(cas))),
path: src/data/init/init-common-anims.ts
user: login: innerthunder
body: No idea 😆. The only change here was from renaming `AnimConfig`
created_at: 2025-05-01T22:04:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -346,7 +521,7 @@ export class AnimTimedSoundEvent extends AnimTimedEvent {
   }
 }
 
-abstract class AnimTimedBgEvent extends AnimTimedEvent {
+abstract class AnimTimedBgEvent extends LegacyAnimTimedEvent {
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: See above comment
created_at: 2025-05-02T07:52:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,7 @@
 import {
-  AnimConfig,
+  LegacyAnimConfig,
   AnimFrame,
-  type AnimTimedEvent,
+  type LegacyAnimTimedEvent,
   AnimTimedSoundEvent,
   AnimTimedAddBgEvent,
   AnimTimedUpdateBgEvent,
path: src/data/init/init-anims.ts
user: login: DayKev
body: No issues. If ESLint doesn't complain it's fine. 👍 
created_at: 2025-05-02T07:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,14 +8,14 @@ export function initCommonAnims(): Promise<void> {
   return new Promise((resolve) => {
     const commonAnimNames = getEnumKeys(CommonAnim);
     const commonAnimIds = getEnumValues(CommonAnim);
-    const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
+    const commonAnimFetches: Promise<Map<CommonAnim, LegacyAnimConfig>>[] = [];
     for (let ca = 0; ca < commonAnimIds.length; ca++) {
       const commonAnimId = commonAnimIds[ca];
       commonAnimFetches.push(
         globalScene
           .cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, "-")}.json`)
           .then((response) => response.json())
-          .then((cas) => commonAnims.set(commonAnimId, new AnimConfig(cas))),
+          .then((cas) => commonAnims.set(commonAnimId, new LegacyAnimConfig(cas))),
path: src/data/init/init-common-anims.ts
user: login: DayKev
body: "Common animation [s]" probably... so the question is what does the `s` stand for.
created_at: 2025-05-02T07:57:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: 2 and 4 place the graphics in the same "layer" as the Player and Enemy Pokemon.
created_at: 2025-05-02T07:59:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
path: src/data/animations/anim-config.ts
user: login: DayKev
body: Probably could add this then:
```
@todo Rename to `moveId`
```
created_at: 2025-05-02T08:02:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
path: src/data/animations/anim-config.ts
user: login: DayKev
body: That is, add it to all the `id: MoveId` fields, unless it can be changed now.
created_at: 2025-05-02T08:02:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,199 @@
+// -- start tsdoc imports --
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Pokemon } from "#app/field/pokemon";
+/* eslint-enable @typescript-eslint/no-unused-vars */
+// -- end tsdoc imports --
+
+import type { easeFunctions } from "#app/data/animations/ease-functions";
 import { globalScene } from "#app/global-scene";
 import { getFrameMs, isNil } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { AnimFocus } from "#enums/anim-focus";
 import { AnimFrameTarget } from "#enums/anim-frame-target";
+import type { AnimTimedEventType } from "#enums/anim-timed-event-type";
+import type { MoveId } from "#enums/move-id";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
 
-export class AnimConfig {
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (normalized RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   * @todo define the allowed priority values as an enum
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
path: src/data/animations/anim-config.ts
user: login: DayKev
body: ```suggestion
   * - `AnimTimedSoundEvent`: for SFX
   * - `AnimTimedAddBgEvent`: for initializing a background image
   * - `AnimTimedUpdateBgEvent`: for updating the background image
```
created_at: 2025-05-02T08:04:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: DayKev
body: Is that documented anywhere? Is that a Phaser thing or an "us" thing?
created_at: 2025-05-02T08:07:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: It's an "us" thing... I don't think there's a place where all of the layering is documented.
created_at: 2025-05-02T08:47:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,199 @@
+// -- start tsdoc imports --
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Pokemon } from "#app/field/pokemon";
+/* eslint-enable @typescript-eslint/no-unused-vars */
+// -- end tsdoc imports --
+
+import type { easeFunctions } from "#app/data/animations/ease-functions";
 import { globalScene } from "#app/global-scene";
 import { getFrameMs, isNil } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { AnimFocus } from "#enums/anim-focus";
 import { AnimFrameTarget } from "#enums/anim-frame-target";
+import type { AnimTimedEventType } from "#enums/anim-timed-event-type";
+import type { MoveId } from "#enums/move-id";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
 
-export class AnimConfig {
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (normalized RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   * @todo define the allowed priority values as an enum
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: I think the quotes make it more clear that the accepted values are the event name *strings*, not an object or class reference
created_at: 2025-05-02T08:50:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,199 @@
+// -- start tsdoc imports --
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Pokemon } from "#app/field/pokemon";
+/* eslint-enable @typescript-eslint/no-unused-vars */
+// -- end tsdoc imports --
+
+import type { easeFunctions } from "#app/data/animations/ease-functions";
 import { globalScene } from "#app/global-scene";
 import { getFrameMs, isNil } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { AnimFocus } from "#enums/anim-focus";
 import { AnimFrameTarget } from "#enums/anim-frame-target";
+import type { AnimTimedEventType } from "#enums/anim-timed-event-type";
+import type { MoveId } from "#enums/move-id";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
 
-export class AnimConfig {
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (normalized RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   * @todo define the allowed priority values as an enum
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
path: src/data/animations/anim-config.ts
user: login: DayKev
body: Ah, sure.
created_at: 2025-05-02T09:20:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,22 +1,199 @@
+// -- start tsdoc imports --
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Pokemon } from "#app/field/pokemon";
+/* eslint-enable @typescript-eslint/no-unused-vars */
+// -- end tsdoc imports --
+
+import type { easeFunctions } from "#app/data/animations/ease-functions";
 import { globalScene } from "#app/global-scene";
 import { getFrameMs, isNil } from "#app/utils/common-utils";
 import { AnimBlendType } from "#enums/anim-blend-type";
 import { AnimFocus } from "#enums/anim-focus";
 import { AnimFrameTarget } from "#enums/anim-frame-target";
+import type { AnimTimedEventType } from "#enums/anim-timed-event-type";
+import type { MoveId } from "#enums/move-id";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
 
-export class AnimConfig {
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (normalized RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   * @todo define the allowed priority values as an enum
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
+}
+
+export interface AnimKeyFrame<ValueType> {
+  /** The end value for the keyframe */
+  readonly value: ValueType;
+  /**
+   * The duration (in frames) of the tween played for this keyframe.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly duration?: number;
+  /**
+   * The time (in frames) between the start of this keyframe and the
+   * end of the last keyframe (or t=0)
+   */
+  readonly delay?: number;
+  /**
+   * The easing function used to interpolate intermediate values.
+   * This is ignored if the keyframe's `ValueType` is `boolean`.
+   */
+  readonly ease?: (typeof easeFunctions)[number];
+}
+
+export interface AnimTimedEvent {
+  /**
+   * The type of timed event. Currently supported events include:
+   * - "AnimTimedSoundEvent": for SFX
+   * - "AnimTimedAddBgEvent": for initializing a background image
+   * - "AnimTimedUpdateBgEvent": for updating the background image
path: src/data/animations/anim-config.ts
user: login: flx-sta
body: @innerthunder @DayKev You could also combine both, not too uncommon to make it clear that it's strings
e.g.
`"AnimTimedSoundEvent"`
created_at: 2025-05-02T15:44:19Z
author_association: CONTRIBUTOR
path: src/data/animations/anim-config.ts
user: login: pom-eranian
body: `moveAnim.bgSprite.setScale(1.25);` (line 611)
Please allow to set a default value for bgSprite scale. Keyable is NiceToHave.
created_at: 2025-05-03T18:21:32Z
author_association: CONTRIBUTOR
path: src/data/animations/anim-config.ts
user: login: innerthunder
body: The `scale` value in `animTimedEventSchema` has the `default: 100` keyword (i.e. it should default to 100% scale).

<sub>Disclaimer: the `default` keyword is just for documentation purposes; it doesn't fill in values on validation. However, when the implementation is rewritten, it should match that specification.
created_at: 2025-05-03T18:56:35Z
author_association: CONTRIBUTOR
path: src/data/animations/anim-config.ts
user: login: pom-eranian
body: oh ok got it. I skimmed over it and assumed it was for sound volume
created_at: 2025-05-03T19:01:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,17 +12,186 @@ import { AnimFrameTarget } from "#enums/anim-frame-target";
 import type Phaser from "phaser";
 import type { BattleAnim } from "./battle-anims";
 import type { MoveAnim } from "./move-anim";
+import type { MoveId } from "#enums/move-id";
+import type { easeFunctions } from "#app/data/animations/ease-functions";
+
+export interface AnimConfig {
+  /**
+   * If this is for a {@linkcode MoveAnim}, this specifies the {@linkcode MoveId}
+   * associated with the animation.
+   */
+  readonly id?: MoveId;
+
+  /** The file name for VFX assets used in this animation */
+  readonly graphic?: string;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "source" object (e.g. the {@linkcode Pokemon} using the move)
+   */
+  readonly sourceProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the sprite
+   * of the "target" object (e.g. the {@linkcode Pokemon} targeted by a move)
+   */
+  readonly targetProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimProp | properties} applied to the
+   * animation's VFX element
+   */
+  readonly vfxProperties?: AnimProp;
+
+  /**
+   * Contains all {@linkcode AnimTimedEvent | timed events} played
+   * during the animation
+   */
+  readonly timedEvents?: AnimTimedEvent[];
+}
+
+export interface AnimProp {
+  /**
+   * For battle animations, the origin point for keyframes is defined
+   * along the line connecting the start point ("source") and end point ("target").
+   * The `u`-value is the fraction of the distance between the start and end point
+   * the origin point is away from the source. If `u = 0`, then the origin point is
+   * the source; if `u = 1`, then the origin point is the target.
+   */
+  readonly u: AnimKeyFrame<number>[];
+
+  /** The horizontal coordinate relative to the keyframe's origin point. */
+  readonly x: AnimKeyFrame<number>[];
+
+  /**
+   * The vertical coordinate relative to the keyframe's origin point.
+   * An increase in `y` will move the sprite downward.
+   */
+  readonly y: AnimKeyFrame<number>[];
+
+  /** Horizontal scale factor (%) */
+  readonly scaleX?: AnimKeyFrame<number>[];
+
+  /** Vertical scale factor (%) */
+  readonly scaleY?: AnimKeyFrame<number>[];
+
+  /** The alpha value for the sprite, in the range [0, 255] */
+  readonly alpha?: AnimKeyFrame<number>[];
+
+  /**
+   * The rotation angle of the sprite in degrees.
+   * Phaser uses a right-hand clockwise rotation system, where 0 is right,
+   * 90 is down, and -90 is up. The value of this should be in the interval
+   * [-180, 180].
+   */
+  readonly angle?: AnimKeyFrame<number>[];
+
+  /** When `true`, the sprite is flipped horizontally */
+  readonly mirror?: AnimKeyFrame<boolean>[];
+
+  /** When `false`, the sprite is hidden */
+  readonly visible?: AnimKeyFrame<boolean>[];
+
+  /**
+   * The blend mode to specify how the sprite is rendered on the canvas
+   * @see {@link https://docs.phaser.io/api-documentation/constant/blendmodes}
+   */
+  readonly blendType?: AnimKeyFrame<AnimBlendType>[];
+
+  /** If this is a VFX property, specifies the graphic's tile index */
+  readonly graphicFrame?: AnimKeyFrame<number>[];
+
+  /** A tone to pipeline over the animated sprite (RGBA, A is optional) */
+  readonly tone?: AnimKeyFrame<number[]>[];
+
+  /**
+   * The z-depth of the animated sprite during the tween
+   * - 0 is behind all other sprites (except BG)
+   * - 1 is on top of player field
+   * - 3 is on top of both fields
+   * - 5 is on top of player sprite
+   */
+  readonly priority?: AnimKeyFrame<0 | 1 | 3 | 5>[];
path: src/data/animations/anim-config.ts
user: login: pom-eranian
body: Also, rather than `priority` it could be called `depth` since that is much clearer.
created_at: 2025-05-03T19:56:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: flx-sta
created_at: 2025-05-01T17:19:18Z
body: We should have an exported version of the actual json-schema in use. *Possibly have scripts that can be run to export the schema.*
So we can leverage the `$schema` key inside a json file to tell the IDE (e.g. vscode) which schema to validate against providing a better "experience" to the person *writing* that file
author_association: CONTRIBUTOR
user: login: innerthunder
created_at: 2025-05-02T19:47:43Z
body: We'd have to write a script for it; it's not something that Ajv supports directly from what I've read. Probably work for another PR,  but it may be as simple as writing
```ts
JSON.stringify({ ...animConfigSchema, ...metadata }) // metadata contains $schema, $id, ...
```
to a JSON Schema file. A boilerplate script for animation JSONs might also help, but I'd need input from @pom-eranian; iirc they want to be able to convert animation data from a csv or some other software's output 😅 
author_association: CONTRIBUTOR
user: login: flx-sta
created_at: 2025-05-02T20:26:36Z
body: > We'd have to write a script for it; it's not something that Ajv supports directly from what I've read. Probably work for another PR, but it may be as simple as writing
> 
> ```ts
> JSON.stringify({ ...animConfigSchema, ...metadata }) // metadata contains $schema, $id, ...
> ```
> 
> to a JSON Schema file. A boilerplate script for animation JSONs might also help, but I'd need input from @pom-eranian; iirc they want to be able to convert animation data from a csv or some other software's output 😅

The thing is, I've tried creating a script but it's not that easy, as it seems it needs to load the whole project context, and can't be run as simple as `create-test-boilerplate`.
author_association: CONTRIBUTOR
user: login: innerthunder
created_at: 2025-05-02T20:40:51Z
body: > > We'd have to write a script for it; it's not something that Ajv supports directly from what I've read. Probably work for another PR, but it may be as simple as writing
> > ```ts
> > JSON.stringify({ ...animConfigSchema, ...metadata }) // metadata contains $schema, $id, ...
> > ```
> > 
> > to a JSON Schema file. A boilerplate script for animation JSONs might also help, but I'd need input from @pom-eranian; iirc they want to be able to convert animation data from a csv or some other software's output 😅
> 
> The thing is, I've tried creating a script but it's not that easy, as it seems it needs to load the whole project context, and can't be run as simple as `create-test-boilerplate`.

Maybe it could be run within the test suite instead?
author_association: CONTRIBUTOR
user: login: flx-sta
created_at: 2025-05-02T21:11:14Z
body: @innerthunder That's worth a shot
When I tested around there were so many things in tsconfig & package.json I had to adjust that I gave up.
I was however able to export it once:

<details><summary>Show anim-config.schema.json</summary>
<p>

```json
{
  "type": "object",
  "properties": {
    "moveId": {
      "type": "integer",
      "enum": [
        0,
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
        46,
        47,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
        60,
        61,
        62,
        63,
        64,
        65,
        66,
        67,
        68,
        69,
        70,
        71,
        72,
        73,
        74,
        75,
        76,
        77,
        78,
        79,
        80,
        81,
        82,
        83,
        84,
        85,
        86,
        87,
        88,
        89,
        90,
        91,
        92,
        93,
        94,
        95,
        96,
        97,
        98,
        99,
        100,
        101,
        102,
        103,
        104,
        105,
        106,
        107,
        108,
        109,
        110,
        111,
        112,
        113,
        114,
        115,
        116,
        117,
        118,
        119,
        120,
        121,
        122,
        123,
        124,
        125,
        126,
        127,
        128,
        129,
        130,
        131,
        132,
        133,
        134,
        135,
        136,
        137,
        138,
        139,
        140,
        141,
        142,
        143,
        144,
        145,
        146,
        147,
        148,
        149,
        150,
        151,
        152,
        153,
        154,
        155,
        156,
        157,
        158,
        159,
        160,
        161,
        162,
        163,
        164,
        165,
        166,
        167,
        168,
        169,
        170,
        171,
        172,
        173,
        174,
        175,
        176,
        177,
        178,
        179,
        180,
        181,
        182,
        183,
        184,
        185,
        186,
        187,
        188,
        189,
        190,
        191,
        192,
        193,
        194,
        195,
        196,
        197,
        198,
        199,
        200,
        201,
        202,
        203,
        204,
        205,
        206,
        207,
        208,
        209,
        210,
        211,
        212,
        213,
        214,
        215,
        216,
        217,
        218,
        219,
        220,
        221,
        222,
        223,
        224,
        225,
        226,
        227,
        228,
        229,
        230,
        231,
        232,
        233,
        234,
        235,
        236,
        237,
        238,
        239,
        240,
        241,
        242,
        243,
        244,
        245,
        246,
        247,
        248,
        249,
        250,
        251,
        252,
        253,
        254,
        255,
        256,
        257,
        258,
        259,
        260,
        261,
        262,
        263,
        264,
        265,
        266,
        267,
        268,
        269,
        270,
        271,
        272,
        273,
        274,
        275,
        276,
        277,
        278,
        279,
        280,
        281,
        282,
        283,
        284,
        285,
        286,
        287,
        288,
        289,
        290,
        291,
        292,
        293,
        294,
        295,
        296,
        297,
        298,
        299,
        300,
        301,
        302,
        303,
        304,
        305,
        306,
        307,
        308,
        309,
        310,
        311,
        312,
        313,
        314,
        315,
        316,
        317,
        318,
        319,
        320,
        321,
        322,
        323,
        324,
        325,
        326,
        327,
        328,
        329,
        330,
        331,
        332,
        333,
        334,
        335,
        336,
        337,
        338,
        339,
        340,
        341,
        342,
        343,
        344,
        345,
        346,
        347,
        348,
        349,
        350,
        351,
        352,
        353,
        354,
        355,
        356,
        357,
        358,
        359,
        360,
        361,
        362,
        363,
        364,
        365,
        366,
        367,
        368,
        369,
        370,
        371,
        372,
        373,
        374,
        375,
        376,
        377,
        378,
        379,
        380,
        381,
        382,
        383,
        384,
        385,
        386,
        387,
        388,
        389,
        390,
        391,
        392,
        393,
        394,
        395,
        396,
        397,
        398,
        399,
        400,
        401,
        402,
        403,
        404,
        405,
        406,
        407,
        408,
        409,
        410,
        411,
        412,
        413,
        414,
        415,
        416,
        417,
        418,
        419,
        420,
        421,
        422,
        423,
        424,
        425,
        426,
        427,
        428,
        429,
        430,
        431,
        432,
        433,
        434,
        435,
        436,
        437,
        438,
        439,
        440,
        441,
        442,
        443,
        444,
        445,
        446,
        447,
        448,
        449,
        450,
        451,
        452,
        453,
        454,
        455,
        456,
        457,
        458,
        459,
        460,
        461,
        462,
        463,
        464,
        465,
        466,
        467,
        468,
        469,
        470,
        471,
        472,
        473,
        474,
        475,
        476,
        477,
        478,
        479,
        480,
        481,
        482,
        483,
        484,
        485,
        486,
        487,
        488,
        489,
        490,
        491,
        492,
        493,
        494,
        495,
        496,
        497,
        498,
        499,
        500,
        501,
        502,
        503,
        504,
        505,
        506,
        507,
        508,
        509,
        510,
        511,
        512,
        513,
        514,
        515,
        516,
        517,
        518,
        519,
        520,
        521,
        522,
        523,
        524,
        525,
        526,
        527,
        528,
        529,
        530,
        531,
        532,
        533,
        534,
        535,
        536,
        537,
        538,
        539,
        540,
        541,
        542,
        543,
        544,
        545,
        546,
        547,
        548,
        549,
        550,
        551,
        552,
        553,
        554,
        555,
        556,
        557,
        558,
        559,
        560,
        561,
        562,
        563,
        564,
        565,
        566,
        567,
        568,
        569,
        570,
        571,
        572,
        573,
        574,
        575,
        576,
        577,
        578,
        579,
        580,
        581,
        582,
        583,
        584,
        585,
        586,
        587,
        588,
        589,
        590,
        591,
        592,
        593,
        594,
        595,
        596,
        597,
        598,
        599,
        600,
        601,
        602,
        603,
        604,
        605,
        606,
        607,
        608,
        609,
        610,
        611,
        612,
        613,
        614,
        615,
        616,
        617,
        618,
        619,
        620,
        621,
        622,
        623,
        624,
        625,
        626,
        627,
        628,
        629,
        630,
        631,
        632,
        633,
        634,
        635,
        636,
        637,
        638,
        639,
        640,
        641,
        642,
        643,
        644,
        645,
        646,
        647,
        648,
        649,
        650,
        651,
        652,
        653,
        654,
        655,
        656,
        657,
        658,
        659,
        660,
        661,
        662,
        663,
        664,
        665,
        666,
        667,
        668,
        669,
        670,
        671,
        672,
        673,
        674,
        675,
        676,
        677,
        678,
        679,
        680,
        681,
        682,
        683,
        684,
        685,
        686,
        687,
        688,
        689,
        690,
        691,
        692,
        693,
        694,
        695,
        696,
        697,
        698,
        699,
        700,
        701,
        702,
        703,
        704,
        705,
        706,
        707,
        708,
        709,
        710,
        711,
        712,
        713,
        714,
        715,
        716,
        717,
        718,
        719,
        720,
        721,
        722,
        723,
        724,
        725,
        726,
        727,
        728,
        729,
        730,
        731,
        732,
        733,
        734,
        735,
        736,
        737,
        738,
        739,
        740,
        741,
        742,
        743,
        744,
        745,
        746,
        747,
        748,
        749,
        750,
        751,
        752,
        753,
        754,
        755,
        756,
        757,
        758,
        759,
        760,
        761,
        762,
        763,
        764,
        765,
        766,
        767,
        768,
        769,
        770,
        771,
        772,
        773,
        774,
        775,
        776,
        777,
        778,
        779,
        780,
        781,
        782,
        783,
        784,
        785,
        786,
        787,
        788,
        789,
        790,
        791,
        792,
        793,
        794,
        795,
        796,
        797,
        798,
        799,
        800,
        801,
        802,
        803,
        804,
        805,
        806,
        807,
        808,
        809,
        810,
        811,
        812,
        813,
        814,
        815,
        816,
        817,
        818,
        819,
        820,
        821,
        822,
        823,
        824,
        825,
        826,
        827,
        828,
        829,
        830,
        831,
        832,
        833,
        834,
        835,
        836,
        837,
        838,
        839,
        840,
        841,
        842,
        843,
        844,
        845,
        846,
        847,
        848,
        849,
        850,
        8001,
        8002,
        8003,
        8004,
        8005,
        8006,
        8007,
        8008,
        8009,
        8010,
        8011,
        8012,
        8013,
        8014,
        8015,
        8016,
        8017,
        8018,
        8019,
        8020,
        8021,
        8022,
        8023,
        8024,
        8025,
        8026,
        8027,
        8028,
        8029,
        8030,
        8031,
        8032,
        8033,
        851,
        852,
        853,
        854,
        855,
        856,
        857,
        858,
        859,
        860,
        861,
        862,
        863,
        864,
        865,
        866,
        867,
        868,
        869,
        870,
        871,
        872,
        873,
        874,
        875,
        876,
        877,
        878,
        879,
        880,
        881,
        882,
        883,
        884,
        885,
        886,
        887,
        888,
        889,
        890,
        891,
        892,
        893,
        894,
        895,
        896,
        897,
        898,
        899,
        900,
        901,
        902,
        903,
        904,
        905,
        906,
        907,
        908,
        909,
        910,
        911,
        912,
        913,
        914,
        915,
        916,
        917,
        918,
        919
      ],
      "nullable": true
    },
    "graphic": {
      "type": "string",
      "nullable": true
    },
    "sourceProperties": {
      "type": "object",
      "properties": {
        "u": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "x": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "y": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "scaleX": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "scaleY": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "alpha": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 255
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "angle": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "mirror": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "visible": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "blendType": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  2
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "graphicFrame": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "tone": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "minItems": 3,
                "maxItems": 4
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "nullable": true
        },
        "priority": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  3,
                  5
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        }
      },
      "required": [
        "u",
        "x",
        "y"
      ],
      "additionalProperties": false,
      "nullable": true
    },
    "targetProperties": {
      "type": "object",
      "properties": {
        "u": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "x": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "y": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "scaleX": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "scaleY": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "alpha": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 255
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "angle": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "mirror": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "visible": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "blendType": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  2
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "graphicFrame": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "tone": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "minItems": 3,
                "maxItems": 4
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "nullable": true
        },
        "priority": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  3,
                  5
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        }
      },
      "required": [
        "u",
        "x",
        "y"
      ],
      "additionalProperties": false,
      "nullable": true
    },
    "vfxProperties": {
      "type": "object",
      "properties": {
        "u": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "x": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "y": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1
        },
        "scaleX": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "scaleY": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "alpha": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": 0,
                "maximum": 255
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "angle": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "mirror": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "visible": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "blendType": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  2
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "graphicFrame": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer"
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        },
        "tone": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                },
                "minItems": 3,
                "maxItems": 4
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "nullable": true
        },
        "priority": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  3,
                  5
                ]
              },
              "duration": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "delay": {
                "type": "integer",
                "minimum": 0,
                "nullable": true
              },
              "ease": {
                "type": "string",
                "enum": [
                  "Linear",
                  "Quad.easeIn",
                  "Cubic.easeIn",
                  "Quart.easeIn",
                  "Quint.easeIn",
                  "Sine.easeIn",
                  "Expo.easeIn",
                  "Circ.easeIn",
                  "Back.easeIn",
                  "Bounce.easeIn",
                  "Quad.easeOut",
                  "Cubic.easeOut",
                  "Quart.easeOut",
                  "Quint.easeOut",
                  "Sine.easeOut",
                  "Expo.easeOut",
                  "Circ.easeOut",
                  "Back.easeOut",
                  "Bounce.easeOut",
                  "Quad.easeInOut",
                  "Cubic.easeInOut",
                  "Quart.easeInOut",
                  "Quint.easeInOut",
                  "Sine.easeInOut",
                  "Expo.easeInOut",
                  "Circ.easeInOut",
                  "Back.easeInOut",
                  "Bounce.easeInOut",
                  "Elastic",
                  "Stepped"
                ],
                "nullable": true
              }
            },
            "additionalProperties": false,
            "required": [
              "value"
            ]
          },
          "minItems": 1,
          "nullable": true
        }
      },
      "required": [
        "u",
        "x",
        "y"
      ],
      "additionalProperties": false,
      "nullable": true
    },
    "timedEvents": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "eventType": {
            "type": "string",
            "enum": [
              "AnimTimedSoundEvent",
              "AnimTimedAddBgEvent",
              "AnimTimedUpdateBgEvent"
            ]
          },
          "time": {
            "type": "number",
            "minimum": 0
          },
          "resourceName": {
            "type": "string"
          },
          "volume": {
            "type": "number",
            "default": 100,
            "minimum": 0,
            "nullable": true
          },
          "pitch": {
            "type": "number",
            "default": 100,
            "minimum": 0,
            "nullable": true
          },
          "bgX": {
            "type": "number",
            "default": 0,
            "nullable": true
          },
          "bgY": {
            "type": "number",
            "default": 0,
            "nullable": true
          },
          "duration": {
            "type": "number",
            "nullable": true
          },
          "scale": {
            "type": "number",
            "default": 100,
            "nullable": true
          }
        },
        "nullable": true,
        "additionalProperties": false,
        "required": [
          "eventType",
          "time",
          "resourceName"
        ],
        "if": {
          "properties": {
            "eventType": {
              "const": "AnimTimedSoundEvent"
            }
          }
        },
        "then": {
          "required": [
            "volume",
            "pitch"
          ]
        },
        "else": {
          "required": [
            "bgX",
            "bgY",
            "duration",
            "scale"
          ]
        }
      },
      "minItems": 1,
      "nullable": true
    }
  },
  "additionalProperties": false,
  "required": []
}
```

</p>
</details> 
author_association: CONTRIBUTOR
user: login: pom-eranian
created_at: 2025-05-03T20:13:31Z
body: > @innerthunder That's worth a shot When I tested around there were so many things in tsconfig & package.json I had to adjust that I gave up. I was however able to export it once:
> Show anim-config.schema.json

The moveID is killing me.... Besides, I am not sure if they even need to be specified in the json file, given that anims are usually called by file name anyway. Or if there was another method to load the correct file, I doubt a moveID would need to be specified within the file itself.
author_association: CONTRIBUTOR