Source: engine.js

/** Main engine file
 * @module Engine
 * @author Onien
 */

import Effects from "./effects.js";
import Images from "./image.js";
import Sound from "./sound.js";
import Text from "./text.js";
import Media from "./media.js";
import Choises from "./choises.js";
import SceneObject from "./object.js"; 

/** Main Engine class. See {@tutorial scenario-example}
 * @class Engine
 */

export default class Engine {
  /** @member {Object} handlers
   * @desc there are 4 handlers: handlers.Text, handlers.BackImage, handlers.Effects, handlers.Media
   */

  /** @member {Object} config
   * @desc config
   */

  /** @member {HTMLElement} scene
   * @desc scene element. there will be placed all sprites
   */

  /** @member {HTMLElement} textElement */

  /** @member {HTMLElement} dialogElement */

  /** @member {HTMLElement} choisesElement */

  /** @member {Object[]} registered
   * @desc there will be all dialogs
   */

  /** @member {number} readingIndex
   * @desc current dialog
   */

  /** @member {Object.<string, Sound>} sounds
   * @desc there will be all sounds
   */

  /** @member {boolean} typing
   * @desc true, if Text is typing text
   */

  /** @member {number} typingInterval
   * @desc interval (from setInterval) of typing
   */

  /** @member {Map} sprites
   * @desc there will be all sprites
   */

  /** @member {Map} varibles
   * @desc contains all user-created varibles
   */

  /** @member {Map<string, SceneObject>} objects
   * @desc contains all scene objects
   */

  /** @member {string} branchReading
   * @desc name of branch currently reading
   */

  /** @function constructor
   * @param {HTMLElement} backgroundImageElement - background image element
   * @param {HTMLElement} textElement - text element
   * @param {HTMLElement} effect - effect element
   * @param {HTMLElement} mediaElement - media parent element
   * @param {HTMLElement} choisesElement - choises parent element
   * @param {Object} config
   */
  constructor(
    backgroundImageElement,
    textElement,
    effect,
    mediaElement,
    choisesElement,
    config
  ) {
    this.handlers = {
      Text: new Text(textElement, config.text),
      BackImage: new Images(backgroundImageElement),
      Effects: new Effects(effect),
      Media: new Media(mediaElement),
    };
    this.config = config;

    this.scene = document.querySelector(".scene");
    this.textElement = textElement;
    this.dialogElement = textElement.querySelector(".dialog");
    this.choisesElement = choisesElement;

    this.registered = {};
    this.readingIndex = 0;
    this.branchReading = "";

    this.sounds = {};

    this.typing = false;
    this.typingInterval;
    this.sprites = new Map();
    this.varibles = new Map();
    this.objects = new Map();
  }

  /** @method setTitle
   * @desc sets html > head > title innerHTML. Title of the page
   * @param {string} text - new title
   */
  setTitle(text) {
    document.querySelector("title").innerText = text;
  }

  /** @method importScenario
   * @desc imports scenario from file. Uses "import" fucntion
   * @param {string} file - which file will be loaded
   * @param {Function} loaded - when file is loaded
   */
  importScenario(file, loaded) {
    import(file).then((scenario) => {
      this.registered = scenario.scenario;
      loaded();
    });
  }

  /** @method registerDialog
   * @desc adds dialog to Engine#registered
   * @param {Object} dialog
   */
  registerDialog(dialog, name) {
    this.registered[name].push(dialog);
  }

  /** @method onTypingEnd
   * @desc this method calls when all text is typed or if next set to intstant mode <br>
   * if next isnt undefined and its object (additional setting), it will read and handle settings <br>
   * else, it will just call Engine#next and increment Engine@readingIndex;
   */

  onTypingEnd() {
    this.typing = false;
    let dialog = this.registered[this.branchReading][this.readingIndex];

    if (dialog.next != undefined && typeof(dialog.next) == "object") {
      if (dialog.next.increment != undefined) {
        this.readingIndex += dialog.next.increment;
      }
      return this.next();
    }
    else if (dialog.next != undefined && typeof(dialog.next) == "boolean" && dialog.next) {
      this.readingIndex++;
      return this.next();
    }
  }

  /** @method next
   * @desc processes dialog from Engine#registered under index (index)
   * @param {number} [index=Engine#readingIndex]
   * @param {Object} [?dialog=null]
   * @returns {Object} processed dialog
   */

  next(index = this.index, dialog = null) {
    if (dialog == null) 
      if (this.index == this.registered[this.branch].length) return; // if end reached

    this.typing = true;
    
    this.textElement.style.display = "block";

    if (dialog == null)
      dialog = this.registered[this.branch][this.index]; // this dialog

    if (!dialog.other?.dontStopText)
      this.typingInterval?.disable();

    setTimeout(
      () => {
        if (dialog.next != undefined && typeof(dialog.next) == "object") {
          if (dialog.next.instantly)
            this.onTypingEnd();
        }

        if (dialog.text == undefined) {
          if (dialog.texts != undefined) {
            this.dialogElement.innerHTML = "";

            dialog.texts.formatting.forEach((format, idx) => {
              this.dialogElement.appendChild(format);
              format.classList.add(
                `format${parseInt(format.innerText.slice(1, -1)) - 1}`
              );
              format.innerHTML = "";
            });
            this.handlers.Text.typeMultiply(
              dialog.texts.strings,
              dialog.texts.formatting,
              () => this.onTypingEnd()
            );
          }
        } else {
          if (dialog.text.trim().startsWith("[w]")) {
            // if text continue at this dialog
            this.handlers.Text.type(
              dialog.text.replace("[w]", ""),
              this.dialogElement.innerHTML,
              () => this.onTypingEnd(), this.handlers.Text.textEl, this.config.text.speed, Object.fromEntries(this.varibles)
            );
          } else
            this.typingInterval = this.handlers.Text.type(dialog.text, "", () =>
              this.onTypingEnd(), this.handlers.Text.textEl, this.config.text.speed, Object.fromEntries(this.varibles)
            ); // type text
        }


        if (dialog.name != undefined) {
          // if name changed
          this.handlers.Text.setName(dialog.name); // change name
        }
        if (dialog.background != undefined) {
          // if background changed
          this.handlers.BackImage.setAsBackground(
            dialog.background.src,
            dialog.background.transition
          ); // change background
        }
        if (dialog.varibles != undefined) {
          Object.keys(dialog.varibles).forEach((varible) => {
            let actions = dialog.varibles[varible];

            if (!this.varibles.has(varible)) { // creating then
              this.varibles.set(varible, 0);
            }

            Object.keys(actions).forEach(action => {
              let value = actions[action];

              if (action == "set") {
                this.varibles.set(varible, value);
              }
              else if (action == "increment") {
                this.varibles.set(varible, this.varibles.get(varible) + value);
              }
              else if (action == "decrement") {
                this.varibles.set(varible, this.varibles.get(varible) - value);
              }
              else if (action == "multiply") {
                this.varibles.set(varible, this.varibles.get(varible) * value);
              }
              else if (action == "divide") {
                this.varibles.set(varible, this.varibles.get(varible) / value);
              }
              // string functions
              else if (action == "conc") {
                this.varibles.set(varible, this.varibles.get(varible) + value);
              }
              else if (action == "remove") {
                this.varibles.set(varible, this.varibles.get(varible).replace(value, ""));
              }
            });
          });
        }
        if (dialog.sprites != undefined) {
          if (
            dialog.other != undefined &&
            dialog.other.clearSprites != undefined &&
            dialog.other.clearSprites == true
          )
            this.scene.innerHTML = ""; // clear all

          Object.keys(dialog.sprites).forEach((spriteName) => {
            const sprite = dialog.sprites[spriteName]; // getting sprite

            const img = new Image(); // getting size
            img.src = sprite.poses[sprite.pose];

            img.onload = () => {
              let HTMLElement = document.createElement("div"); // adding sprite div
              HTMLElement.classList.add("image", "sprite"); // class setup
              HTMLElement.setAttribute("sprite", spriteName); // name
              this.scene.appendChild(HTMLElement); // adding

              let image = new Images(HTMLElement); // loading sprite
              image.setAsSprite(sprite.poses[sprite.pose]);

              // -_-_ Setting styles -_-_-

              HTMLElement.style.top = sprite.top;
              HTMLElement.style.left = sprite.left;
              HTMLElement.style.width = img.width + "px";
              HTMLElement.style.height = img.height + "px";
              if (sprite.styles != undefined)
                Object.assign(HTMLElement.style, sprite.styles);
              if (sprite.transition != undefined) {
                HTMLElement.style.transition = `all ${sprite.transition.time}s ${sprite.transition.easing}`;
                setTimeout(
                  () =>
                  Object.assign(HTMLElement.style, sprite.transition.styles),
                  15
                );
              }
              this.sprites.set(spriteName, {
                sprite: sprite,
                element: HTMLElement
              });
            };

          });
        }
        if (dialog.events != undefined) {
          if (dialog.events.sprites != undefined) {
            Object.keys(dialog.events.sprites).forEach((spriteName) => {
              // exec sprite events
              const event = dialog.events.sprites[spriteName];
              const sprite = this.sprites.get(spriteName);
              let element = sprite.element;

              if (event.changePose != undefined) {
                let image = new Images(element);
                image.setAsSprite(sprite.sprite.poses[event.changePose]);
              }

              if (event.effect == true || event.effect == undefined) {
                this.handlers.Effects.anyEffect(element, event, () => {
                  if (event.remove) {
                    element.remove();
                  }
                });
              }
            });
          }

          if (dialog.events.effects != undefined) {
            dialog.events.effects.forEach((effect) => {
              // exec effects
              this.handlers.Effects.handleEffect(
                effect.name,
                effect.time,
                effect.easing,
                effect.timeout,
                effect.styles
              );
            });
          }
          if (dialog.events.media != undefined) {
            Object.keys(dialog.events.media).forEach((mediaEvent) => {
              const event = dialog.events.media[mediaEvent];
              let element = document.querySelector(
                `.media[mediaName=${mediaEvent}]`
              ); // getting media element
              if (event.pause && element) {
                element.querySelector("video").pause();
              }
              if (event.play && element) {
                element.querySelector("video").play();
              }
              if (event.time && event.styles) {
                this.handlers.Effects.anyEffect(element, event, () => {
                  // and applying effect
                  if (event.remove) {
                    // if remove after effect ending
                    element.remove(); // removing
                  }
                });
              }
            });
          }

          if (dialog.events.objects != undefined) {
            Object.keys(dialog.events.objects).forEach((objectName) => {
              if (!this.objects.has(objectName)) {
                throw new Error("Object " + objectName + " not found!");
              }
              let object = this.objects.get(objectName);
              const event = dialog.events.objects[objectName];

              if (event.html) object.setHtml(event.html, Object.fromEntries(this.varibles));
              if (event.add) object.add();

              this.handlers.Effects.anyEffect(object.element, event, () => {
                if (event.remove) { 
                  object.element.remove();
                  this.object.delete(objectName);
                }
              });
            });
          }
        }
        if (dialog.sounds != undefined) {
          // if sounds changed
          Object.keys(dialog.sounds).forEach((soundName) => {
            let soundObj = dialog.sounds[soundName]; // getting sound options
            if (!this.sounds[soundName]) {
              // adding sound to sound array if there is no sound
              this.sounds[soundName] = new Sound(soundObj.src);
            }
            let thisSound = this.sounds[soundName]; // gettung sound from engine
            thisSound.play(); // starting sound
            thisSound.loop = soundObj.loop; // if loop

            if (soundObj.ended && soundObj.ended.do) {
              thisSound.ended = () => {
                this.next(0, soundObj.ended.do);
              }
            }

            if (soundObj.transition) {
              // if transition
              return thisSound.transition(
                soundObj.transition.from,
                soundObj.transition.to,
                soundObj.transition.step,
                soundObj.transition.time,
                () => {
                  if (soundObj.stop) {
                    thisSound.stop();
                  }
                }
              );
            }
            if (soundObj.stop) return thisSound.stop();
            thisSound.volume = soundObj.volume;
          });
        }
        if (dialog.media != undefined) {
          Object.keys(dialog.media).forEach((media) => {
            let values = dialog.media[media];
            this.handlers.Media.addMedia({ ...values, name: media, onend: (options, element) => {
              if (!values.ended) return;

              if (values.ended.removeMedia) element.parentNode.remove();
              if (values.ended.do) this.next(0, values.ended.do);
            } });
          });
        }
        if (dialog.choises != undefined) { // processing choises
          if (dialog.other != undefined ) {
            if (dialog.other.stopTypingAtChoises != undefined && dialog.other.stopTypingAtChoises) {
              this.typingInterval.disable();
              this.typing = false;
            }
            if (dialog.other.hideUIAtChoises != undefined && dialog.other.hideUIAtChoises) {
              this.textElement.style.display = "none";
            }
          }
          let choise = new Choises(dialog.choises);
          let builded = choise.build();

          this.choisesElement.appendChild(builded);

          this.choisesElement.hidden = false;

          choise.onSelect = (choiseObj, element) => {
            if (choiseObj.hideChoisesElement == undefined || choiseObj.hideChoisesElement) {
              this.choisesElement.hidden = true;
              this.choisesElement.querySelector(".choisesParent").remove();
            }

            if (choiseObj.do.process != undefined) {
              if (choiseObj.do.removeChoise != undefined && choiseObj.do.removeChoise) {
                element.remove();
              }
              this.next(0, choiseObj.do.process);
            }
          }

          choise.onMouseEnter = (choiseObj, element) => {
            if (choiseObj.do.mouseenter != undefined) {
              if (choiseObj.do.mouseenter.removeChoise != undefined && choiseObj.do.mouseenter.removeChoise) {
                element.remove();
              }
              this.next(0, choiseObj.do.mouseenter);
            }
          }
        }
        if (dialog.branch != undefined) {
          if (dialog.branch.set != undefined) {
            this.branch = dialog.branch.set;
            if (dialog.branch.cursor != undefined) {
              this.index = dialog.branch.cursor;
            }
            this.next();
          }
        }

        if (dialog.objects != undefined) {
          Object.keys(dialog.objects).forEach(objectName => {
            const event = dialog.objects[objectName];
            let object = new SceneObject(this.scene, event);
            if (event.add) object.add();
            if (event.html) object.setHtml(event.html, Object.fromEntries(this.varibles));

            this.objects.set(objectName, object);
          });
        }
        document.querySelector(".count").innerText = this.readingIndex + "; " + this.branchReading;
      },
      dialog.timeout == undefined ? 1 : dialog.timeout
    );

    return dialog;
  }

  set branch(value) {
    if (this.registered[value] != undefined) {
      this.branchReading = value;
      this.readingIndex = 0;
    }
    else throw new Error("Cannot set branch " + value + ". Branch not found");
  }

  get branch() {
    return this.branchReading;
  }

  set index(value) {
    if (this.index == this.registered[this.branch].length) throw new Error("Cannot set index " + value + ". End reached in branch " + this.branch); // if end reached

    this.readingIndex = value;
  }

  get index() {
    return this.readingIndex;
  }
}