Source: LevelTransition.js

/**
 * @license GPL-3.0-only
 *
 * @author Mark Mayes / mm-dev
 *
 *
 * @module LevelTransition
 *
 * @description
 * ## Manage the transition between levels
 */

import { LEVEL_TRANSITION_PROPERTY_TYPE } from "./PD/ENUM.js";
import * as GAME from "./PD/GAME.js";
import * as TIMINGS from "./PD/TIMINGS.js";

import { Controller } from "./Controller.js";
import { Display } from "./Display.js";
import { Game } from "./Game.js";
import { InternalTimer } from "./InternalTimer.js";
import { ObstacleManager } from "./ObstacleManager.js";
import { Player } from "./Player.js";

import {
  __,
  addFadeStepToRGB,
  getFadeStepBetweenRGBColorsArray,
  hexOpacityToRGBA,
  rgbToHex,
  rgbToRGB_ar,
} from "./utils.js";

class LevelTransition {}

LevelTransition.init = function () {
  LevelTransition.propertiesInfo = [
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.COLOR,
      readFromName: "bgColor",
      writeToClass: Display,
      writeToName: "bgColor",
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.COLOR,
      readFromName: "textColor",
      writeToClass: Display,
      writeToName: "textColor",
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.COLOR,
      readFromName: "textColorHighlight",
      writeToClass: Display,
      writeToName: "textColorHighlight",
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.COLOR,
      readFromName: "textColorShadow",
      writeToClass: Display,
      writeToName: "shadowColor",
      defaultValue: GAME.TEXT_DEFAULT_SHADOW_COLOR,
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.COLOR,
      readFromName: "player.color",
      writeToClass: Player,
      writeToName: "color",
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.NUMBER,
      readFromName: "player.radius",
      //readFromClass: Player,
      writeToClass: Player,
      writeToName: "radius",
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.NUMBER,
      readFromName: "controllerSpeedDamp",
      writeToClass: Controller,
      writeToName: "speedDamp",
      defaultValue: GAME.CONTROLLER_SPEED_DAMP,
    },
    {
      type: LEVEL_TRANSITION_PROPERTY_TYPE.NUMBER,
      readFromName: "controllerSlipperiness",
      writeToClass: Controller,
      writeToName: "slipperiness",
      defaultValue: GAME.CONTROLLER_SLIPPERINESS,
    },
  ];
};

/**
 * @function startLevelOutro
 * @static
 *
 * @description
 * ##### Start level outro sequence
 */
LevelTransition.start = function () {
  LevelTransition.blendablePropertiesValues = {};

  LevelTransition.nextLevelData =
    Game.levelData[Object.keys(Game.levelData)[Game.getNextLevelIndex()]];

  LevelTransition.totalFrames = InternalTimer.secondsToFrames(
    TIMINGS.LEVEL_OUTROEND_MS / 1000
  );

  ObstacleManager.explodeAllAvoids();

  Game.levelOutroBgObstaclesToRemovePerFrame = Math.ceil(
    Game.backgroundTotal / LevelTransition.totalFrames
  );

  LevelTransition.setupFades();
};

/**
 * @function nextFrame
 * @static
 *
 * @description
 * ##### Perform the next step of the level outro sequence
 * - Move from old level colours to the next level colours
 * - Move from old level numeric values to the next level numeric values
 */
LevelTransition.nextFrame = function () {
  var i,
    type,
    propertyName,
    writeToPropertyName,
    writeToClass,
    details,
    newValue;

  for (i = 0; i < LevelTransition.propertiesInfo.length; i++) {
    type = LevelTransition.propertiesInfo[i].type;

    propertyName = LevelTransition.propertiesInfo[i].readFromName;
    writeToPropertyName = LevelTransition.propertiesInfo[i].writeToName;
    writeToClass = LevelTransition.propertiesInfo[i].writeToClass;

    details = LevelTransition.blendablePropertiesValues[propertyName];

    if (type === LEVEL_TRANSITION_PROPERTY_TYPE.COLOR) {
      if (LevelTransition.totalFrames > 0) {
        details.currentRgba = addFadeStepToRGB(
          details.currentRgba,
          details.stepRgb_ar
        );
      } else {
        details.currentRgba = details.aimRgba;
      }

      // RGBA -> RGB Array (spread) -> Hex
      newValue = rgbToHex(...rgbToRGB_ar(details.currentRgba));
    } else if (type === LEVEL_TRANSITION_PROPERTY_TYPE.NUMBER) {
      if (LevelTransition.totalFrames > 0) {
        details.currentValue += details.stepValue;
      } else {
        details.currentValue = details.aimValue;
      }

      newValue = details.currentValue;
    }

    writeToClass[writeToPropertyName] = newValue;
  }

  if (LevelTransition.totalFrames > 0) {
    ObstacleManager.levelOutroRemoveNextBackgroundObstacles(
      Game.levelOutroBgObstaclesToRemovePerFrame
    );

    // Continue sequence
    LevelTransition.totalFrames--;
  }

  Display.updateColors();
};

/**
 * @function getNestedPropertyValue
 * @static
 *
 * @description
 * ##### Dynamically retrieve a nested property from an object
 *
 * Given an object, convert string-based dot notation into square bracket notation to access the property, so eg if the object is:
 *
 * ```
 * object1 = {
 *   child: {
 *     grandchild: {
 *       type: "flower"
 *     }
 *   }
 * }
 *
 * getNestedPropertyValue(object1, "child.grandchild.type")
 * // output: "flower"
 * ```
 *
 * @param {object} _object - The parent object which is to be searched
 * @param {string} _nestedProperty - The (potentially) nested property as a dot-separated string
 *
 * @returns {object} The discovered value
 */
LevelTransition.getNestedPropertyValue = function (_object, _nestedProperty) {
  var nestedPropertyValue = _object,
    propertyName_ar = _nestedProperty.split(".");

  __("propertyName_ar: " + JSON.stringify(propertyName_ar));

  // ["player", "radius"];
  while (propertyName_ar.length) {
    nestedPropertyValue = nestedPropertyValue[propertyName_ar[0]];
    propertyName_ar.splice(0, 1);
    __("nestedPropertyValue: " + nestedPropertyValue);
  }

  return nestedPropertyValue;
};

/**
 * @function setupFades
 * @static 
 *
 * @description
 * ##### Loop through each of the properties we want to transition and work out how to achieve the change over `LevelTransition.totalFrames` number of frames
 */
LevelTransition.setupFades = function () {
  var i, type, propertyName;

  for (i = 0; i < LevelTransition.propertiesInfo.length; i++) {
    type = LevelTransition.propertiesInfo[i].type;
    propertyName = LevelTransition.propertiesInfo[i].readFromName;

    if (type === LEVEL_TRANSITION_PROPERTY_TYPE.COLOR) {
      LevelTransition.blendablePropertiesValues[propertyName] =
        LevelTransition.getColorBlendDetails(LevelTransition.propertiesInfo[i]);
    } else if (type === LEVEL_TRANSITION_PROPERTY_TYPE.NUMBER) {
      LevelTransition.blendablePropertiesValues[propertyName] =
        LevelTransition.getNumberBlendDetails(
          LevelTransition.propertiesInfo[i]
        );
    }
  }
};


/**
 * @function getColorBlendDetails
 * @static
 *
 * @description
 * ##### Get info about how to transition a colour
 * - Most colour data in the game is stored as hex values
 * - Convert those to RGB(/A) values, so that each channel can have its transition steps calculated
 *
 * @param {object} _propertyInfo - Info about this property from `LevelTransition.propertiesInfo`
 *
 * @returns {object} Describing the current value, the aim, and steps to get there
 */
LevelTransition.getColorBlendDetails = function (_propertyInfo) {
  var currentHex, currentRgba, aimHex, aimRgba, stepRgb_ar;

  currentHex = LevelTransition.getNestedPropertyValue(
    _propertyInfo.readFromClass || Game.curLevelData,
    _propertyInfo.readFromName
  );
  if (currentHex === undefined) {
    currentHex = _propertyInfo.defaultValue;
  }
  currentRgba = hexOpacityToRGBA(currentHex, 1);

  aimHex = LevelTransition.getNestedPropertyValue(
    LevelTransition.nextLevelData,
    _propertyInfo.readFromName
  );
  if (aimHex === undefined) {
    aimHex = _propertyInfo.defaultValue;
  }
  aimRgba = hexOpacityToRGBA(aimHex, 1);

  stepRgb_ar = getFadeStepBetweenRGBColorsArray(
    currentRgba,
    aimRgba,
    LevelTransition.totalFrames
  );

  return {
    currentRgba: currentRgba,
    aimRgba: aimRgba,
    stepRgb_ar: stepRgb_ar,
  };
};

/**
 * @function getNumberBlendDetails
 * @static
 *
 * @description
 * ##### Get info about how to transition a number
 *
 * @param {object} _propertyInfo - Info about this property from `LevelTransition.propertiesInfo`
 *
 * @returns {object} Describing the current value, the aim, and steps to get there
 */
LevelTransition.getNumberBlendDetails = function (_propertyInfo) {
  var currentValue, aimValue, stepValue;

  currentValue = LevelTransition.getNestedPropertyValue(
    _propertyInfo.readFromClass || Game.curLevelData,
    _propertyInfo.readFromName
  );
  if (currentValue === undefined) {
    currentValue = _propertyInfo.defaultValue;
  }

  aimValue = LevelTransition.getNestedPropertyValue(
    LevelTransition.nextLevelData,
    _propertyInfo.readFromName
  );
  if (aimValue === undefined) {
    aimValue = _propertyInfo.defaultValue;
  }

  stepValue = (aimValue - currentValue) / LevelTransition.totalFrames;

  return {
    currentValue: currentValue,
    aimValue: aimValue,
    stepValue: stepValue,
  };
};

export { LevelTransition };