Source

dialog.js

/* global rpgcode, gui */
let dialog = new Dialog();

/**
 * The builtin dialog window system.
 *
 * @class
 * @constructor
 *
 * @returns {Dialog}
 */
function Dialog() {

}

/**
 * Shows the default dialog window based the supplied config.
 *
 * The dialog window can be set to appear at the "TOP", "CENTER", or "BOTTOM"
 * of the screen. It can also be supplied with an image for the blinking next
 * marker, a profile image of the speaker, and a typing sound that plays while
 * it is animating.
 *
 * Note: The current hardcoded next key is "E".
 *
 * @example
 * let config = {
 *  position: "CENTER",
 *  advancementKey: "E",
 *  nextMarkerImage: "next_marker.png",
 *  profileImage: rpgcode.getCharacter().graphics["PROFILE"],
 *  typingSound: "typing_loop.wav",
 *  text: "Hello, this text will be wrote to the window like a type-writer."
 * };
 * await dialog.show(config);
 *
 * @param {Object} config
 * @returns {undefined}
 */
Dialog.prototype.show = async function(config) {
   return new Promise((resolve, reject) => {
      dialog._loadAssets(config, async function() {
         this._setup(config);
         await this._animate();
         
         let lines = this._sortLines(config.text.trim());
         if (lines.length > 0) {
            let callback = function() {
               resolve();
            };
            this._reset(rpgcode.measureText(lines[0]).height);
            this._printLines(lines, callback);
            this._playTypingSound();
         } else {
             resolve();
         }
      }.bind(dialog));
   });
};

Dialog.prototype._end = function(callback) {
   rpgcode.unregisterKeyDown(this.advancementKey);
   rpgcode.unregisterMouseClick(false);
   this._blink = false;
   this._clearNextMarker();
   rpgcode.clearCanvas(this._nextMarkerCanvas);
   this.profileFrame.setVisible(false);
   this.frame.setVisible(false);
   callback();
};

Dialog.prototype._loadAssets = function(config, callback) {
   let assets = {
      "images": [config.nextMarkerImage, config.profileImage],
      "audio": {
         "dialog.typingSound": config.typingSound
      }
   };
   rpgcode.loadAssets(assets, function() {
      callback();
   }.bind(this));
};

Dialog.prototype._setup = function(config) {
   this._nextMarkerCanvas = "dialog.nextMarkerCanvas";
   this._nextMarkerVisible = false;
   this._currentLines = 1;
   this._blink = false;
   this._defaultX = 22;
   this._defaultY = 18;
   this._position = config.position;

   this.characterDelay = 25;
   this.markerBlinkDelay = 500;
   this.advancementKey = config.advancementKey;
   this._cursorX = this._defaultX;
   this._cursorY = this._defaultY;
   this.maxLines = 3;
   this.nextMarkerImage = config.nextMarkerImage;
   this.typingSound = "dialog.typingSound";
   this.skipMode = false
;

   this.padding = {
      x: 0,
      y: 0,
      line: 10
   };

   let width = 440;
   if (rpgcode.getViewport(false).width < 560) {
      width = rpgcode.getViewport(false).width - 120;
   }
   let height = 120;
   let profileWidth = height;
   let profileHeight = height;
   let x = Math.round((rpgcode.getViewport(false).width / 2) - (width / 2) + (profileWidth / 2));
   let y = 0;
   switch (config.position ? config.position : "BOTTOM") {
      case "TOP":
         y = 0;
         break;
      case "CENTER":
         y = Math.floor(rpgcode.getViewport(false).height / 2) - Math.floor(height / 2);
         break;
      case "BOTTOM":
      default:
         y = Math.floor(rpgcode.getViewport(false).height - height);
   }

   this.profileFrame = gui.createFrame({
      id: "Dialog.profileFrameCanvas",
      width: profileWidth,
      height: profileHeight,
      x: x - profileWidth,
      y: y
   });
   this.profileFrame.setImage(config.profileImage);
   this.profileFrame.setVisible(false);

   this.frame = gui.createFrame({
      id: "Dialog.frameCanvas",
      width: width,
      height: height,
      x: x,
      y: y
   });
   this.frame.setVisible(false);

   let image = rpgcode.getImage(this.nextMarkerImage);
   if (image && image.width > 0 && image.height > 0) {
      let widthTemp = image.width;
      let heightTemp = image.height;
      let xTemp = this.frame.x + (this.frame.width - (widthTemp + widthTemp / 4));
      let yTemp = this.frame.y + (this.frame.height - (heightTemp + heightTemp / 4));
      rpgcode.createCanvas(widthTemp, heightTemp, this._nextMarkerCanvas);
      rpgcode.setCanvasPosition(xTemp, yTemp, this._nextMarkerCanvas);
   }
};

Dialog.prototype._reset = function(lineHeight) {
   this._cursorX = this._defaultX + this.padding.x;
   this._cursorY = this._defaultY + this.padding.y + lineHeight;
   this._currentLines = 1;
   this._draw();
};

Dialog.prototype._sortLines = function(text) {
   rpgcode.setFont(gui.getFontSize(), gui.getFontFamily());
   let words = text.split(" ");
   let lines = [];
   let line = words[0];
   
   let newLine;
   for (let i = 1; i < words.length; i++) {
      if (/[\r\n]$/.test(line)) {
         lines.push(line);
         line = words[i].trim();
      }
      
      newLine = line.trim() + " " + words[i];
      if (rpgcode.measureText(newLine.trim()).width < this.frame.width - 45) {
         line = newLine;
      } else {
         lines.push(line);
         line = words[i];
      }
   }
   lines.push(line);

   return lines;
};

Dialog.prototype._animate = async function() {
   if (this._position === "BOTTOM") {
      const originalY = this.profileFrame.y;
      const newY = rpgcode.getViewport(false).height;
      this.profileFrame.setLocation(this.profileFrame.x, newY);
      this.frame.setLocation(this.frame.x, newY);
      this.profileFrame.setVisible(true);
      this.frame.setVisible(true);

      const change = 10; // pixels
      do {
         this.profileFrame.setLocation(this.profileFrame.x, this.profileFrame.y - change);
         this.frame.setLocation(this.frame.x, this.frame.y - change);
         await new Promise(r => requestAnimationFrame(r));
      } while(originalY < this.profileFrame.y);
   } else {
      this.profileFrame.setVisible(true);
      this.frame.setVisible(true);
   }
};

Dialog.prototype._printLines = function(lines, callback) {
   rpgcode.setFont(gui.getFontSize(), gui.getFontFamily());
   gui.prepareTextColor();

   if (dialog.advancementKey) {
      rpgcode.registerKeyDown(dialog.advancementKey, function () {
         dialog.skipMode = true;
      }, false);
   } else {
      rpgcode.registerMouseClick(function() {
         dialog.skipMode = true;
      }, false);
   }

   let line = lines.shift();
   if (/[\r\n]$/.test(line)) {
      let tempLine = line.split("\r\n")[0];
      if (line.replace(tempLine, "").trim()) {
         lines.unshift(line.replace(tempLine, "").trim());
      }
      dialog._currentLines = dialog.maxLines; // Force a fresh window
      line = tempLine.trim();
   }
   
   this._printCharacters(line.split(""), function() {
      if (lines.length) {
         dialog._currentLines++;
         if (dialog._currentLines > dialog.maxLines) {
            dialog.skipMode = false;
            dialog._stopTypingSound();
            dialog._blink = true;
            dialog._blinkNextMarker();

            // Decide whether or not to use keypress or mouse.
            if (dialog.advancementKey) {
               rpgcode.registerKeyDown(dialog.advancementKey, function() {
                  dialog._advance(lines, callback);
               }, false);
            } else {
               rpgcode.registerMouseClick(function() {
                  dialog._advance(lines, callback);
               }, false);
            }
         } else {
            dialog._cursorX = dialog._defaultX + dialog.padding.x;
            dialog._cursorY += rpgcode.measureText(line).height + dialog.padding.line;
            dialog._printLines(lines, callback);
         }
      } else {
         dialog._stopTypingSound();
         dialog._blink = true;
         dialog._blinkNextMarker();

         // Decide whether or not to use keypress or mouse.
         if (dialog.advancementKey) {
            rpgcode.registerKeyDown(dialog.advancementKey, function() {
               dialog._end(callback);
            }, false);
         } else {
            rpgcode.registerMouseClick(function() {
               dialog._end(callback);
            }, false);
         }
      }
   });
};

Dialog.prototype._printCharacters = function(characters, callback) {
   let character = characters.shift();
   rpgcode.drawText(dialog._cursorX, dialog._cursorY, character, this.frame.id);
   rpgcode.renderNow(this.frame.id);
   dialog._cursorX += rpgcode.measureText(character).width;

   if (characters.length) {
      if (dialog.skipMode) {
         dialog._printCharacters(characters, callback);
      } else {
         rpgcode.delay(dialog.characterDelay, function () {
            dialog._printCharacters(characters, callback);
         });
      }
   } else {
      callback();
   }
};

Dialog.prototype._blinkNextMarker = function() {
   if (this.nextMarkerImage && this._blink) {
      if (!this._nextMarkerVisible) {
         this._drawNextMarker();
      } else {
         this._clearNextMarker();
      }
      rpgcode.delay(this.markerBlinkDelay, this._blinkNextMarker.bind(this));
   }
};

Dialog.prototype._clearNextMarker = function() {
   if (this.nextMarkerImage) {
      rpgcode.clearCanvas(this._nextMarkerCanvas);
      this._nextMarkerVisible = false;
   }
};

Dialog.prototype._drawNextMarker = function() {
   let image = rpgcode.getImage(this.nextMarkerImage);
   if (image && image.width > 0 && image.height > 0) {
      let width = image.width;
      let height = image.height;
      rpgcode.setImage(this.nextMarkerImage, 0, 0, width, height, this._nextMarkerCanvas);
      rpgcode.renderNow(this._nextMarkerCanvas);
      this._nextMarkerVisible = true;
   }
};

Dialog.prototype._draw = function() {
   this.frame.draw();
};

Dialog.prototype._stopTypingSound = function() {
   if (this.typingSound) {
      rpgcode.stopSound(this.typingSound);
   }
};

Dialog.prototype._playTypingSound = function() {
   if (this.typingSound) {
      rpgcode.playSound(this.typingSound, true);
   }
};

Dialog.prototype._advance = function(lines, callback) {
   rpgcode.setFont(gui.getFontSize(), gui.getFontFamily());
   dialog._blink = false;
   dialog._clearNextMarker();
   rpgcode.unregisterKeyDown(dialog.advancementKey);
   dialog._reset(rpgcode.measureText(lines[0]).height);
   dialog._printLines(lines, callback);
   dialog._playTypingSound();
};