mini-memory.js

import { Localization } from './lib/i18n';
import { Game } from './lib/game';
import { Fallback } from './lib/fallback';
import { default as style } from './style.css';

import { debounce } from './lib/tools/debounce';

import { default as tmplToolbar } from './templates/toolbar.html';

import { Settings } from './lib/layers/settings';
import { NextLevel } from './lib/layers/nextlevel';
import { FullScreen } from './lib/fullscreen';
import { ImageServer } from './lib/image-server';

// Tools
import { Limits } from './lib/tools/limit';
import { newTile } from './lib/tools/tile';
import { oddMiddle } from './lib/tools/middle';
import { default as getManifestJson } from './lib/tools/manifest';

const observedAttributes = ['matrix', 'lang', 'view', 'settings', 'challenge'];
/**
 * @classdesc Main container of the game. Will be used as
 * {`CustomElements.define`} constructor.
 * @author Emre Sakarya <emre.sakarya@softberry.de>
 * @extends HTMLElement
 * @example customElements.define('mini-memory', MiniMemory);
 * // Create MiniMemory DOMElement like this :
 * document.createElement('mini-memory');
 * // or like this:
 * <mini-memory></mini-memory>
 */
export class MiniMemory extends HTMLElement {
  /**
   * of the game. Will be used as ``{CustomElements.define}`` constructor.
   */
  constructor() {
    super();
    const self = this;
    this.i18n = new Localization();
    this.fullScreen = new FullScreen();
    this.imageServer = new ImageServer(this);
    this.manifest;
    this.rendered = false;
    this.images = [];
    this.tiles = [];

    window.addEventListener(
      'resize',
      debounce(() => {
        self.reset();
        self.render();
      }, 500)
    );

    /**
     * @summary HTMLDivElement information bar on top of the screen.
     */
    this.toolbar = document.createElement('div');

    /**
     * @summary Controls and keep track of game settings {@link Settings}.
     */
    this.settings = new Settings(this);
    /**
     * @summary handle of all layers such as loading, toolbar, setting etc.
     * which will be defined when they are created.
     */
    this.layers = {
      loading: null,
    };

    /**
     * @summary Image of the closed card drawn to be conatiner canvas
     */
    this.cardBack = document.createElement('img');
  }
  /**
   * @summary Resets all parameters to enable a clean restart/render.
   */
  reset() {
    this.rendered = false;
    this.images = [];
    this.tiles = [];
    this.layers = {
      loading: {},
      nextLevel: new NextLevel(this),
    };
  }

  /**
   * @summary Callback on custom element included in DOM.
   */
  connectedCallback() {
    if (!this.rendered) {
      this.render();
    }
  }

  /**
   * @summary Necessary attributes to be watched.
   */
  static get observedAttributes() {
    return observedAttributes;
  }

  /**
   * @summary Callback function on removing of custom element from DOM.
   */
  disconnectedCallback() {}
  /**
   * @summary Callback function on adding of custom element to another DOM.
   */
  adoptedCallback() {}
  /**
   * callback function on attribute change of custom element.
   * @param {string} name Name of the attribute.
   * @param {string} oldVal Latest value before change.
   * @param {string} newVal The current value to be set.
   */
  attributeChangedCallback(name, oldVal, newVal) {
    const self = this;
    /**
     * Resets all parameters and renders game again.
     */
    function __fullReset() {
      self.reset();
      self.rendered = false;
      self.render();
    }

    switch (name) {
      case 'lang': {
        this.i18n = new Localization(newVal);
        this.i18n.update(this.shadowRoot);
        break;
      }
      case 'view': {
        if (newVal === 'fullscreen') {
          this.fullScreen.enter();
        } else {
          this.fullScreen.exit();
        }
        break;
      }
      case 'matrix': {
        __fullReset();
        break;
      }
      default: {
        // __fullReset();
      }
    }
  }
  /**
   * @summary check if element has manadatory fields defined or not.
   * @return {boolean} Returns true if minimum requirements
   * are correctly defined.
   */
  checkDefaultAttributes() {
    const attr = this.getAttribute('matrix');
    const limits = new Limits(2, 10, attr);
    if (!limits.defined()) {
      this.shadowRoot.innerHTML += `
      ${this.i18n.message('MATRIX_DIMENSIONS_ERROR')}
      ${this.i18n.sample('MATRIX_DIMENSIONS_ERROR')}
      `;
      return false;
    }

    if (limits.exceeded(attr.split('x'))) {
      this.shadowRoot.innerHTML += `
      ${this.i18n.message('MATRIX_DIMENSIONS_ERROR')}
      ${this.i18n.sample('MATRIX_DIMENSIONS_ERROR')}
      `;
      return false;
    }

    return true;
  }

  /**
   * @summary Prepare cards layout using given matrix attribute.
   */
  prepareMatrix() {
    const matrix = this.getAttribute('matrix').split('x');
    matrix[0] = parseInt(matrix[0]);
    matrix[1] = parseInt(matrix[1]);

    const oddMid = oddMiddle(matrix);
    const cssRow = `width:${100 / matrix[0]}%;`;
    const cssCol = `height:${100 / matrix[1]}%;`;
    const squares = matrix[0] * matrix[1];
    const imageCount = squares % 2 === 0 ? squares / 2 : (squares - 1) / 2;

    const self = this;
    console.log(JSON.stringify(style));
    self.shadowRoot.innerHTML += `
    <style>:host .tile {${cssRow + cssCol}}</style>`;
    let outer = 0;
    let inner = 0;
    let index = 0;
    const tilesContainer = document.createElement('div');
    const parentContainer =
      self.getAttribute('view') === 'fullscreen'
        ? {
            w: document.documentElement.offsetWidth,
            h: document.documentElement.offsetHeight,
          }
        : { w: self.parentNode.offsetWidth, h: self.parentNode.offsetHeight };

    const width = parseInt(parentContainer.w / matrix[0]);
    const height = parseInt(parentContainer.h / matrix[1]);

    tilesContainer.id = 'tiles-container';
    self.shadowRoot.appendChild(tilesContainer);
    for (outer = 0; outer < matrix[0]; outer++) {
      for (inner = 0; inner < matrix[1]; inner++) {
        index = inner + matrix[1] * outer;
        const tile = newTile(index);
        tilesContainer.appendChild(tile.tile);
        if (index !== oddMid) {
          tile.canvas.classList.add('wait');
          self.tiles.push(tile);
        }
      }
    }
    self.game = new Game(
      this.tiles.map((t) => t.canvas),
      this.cardBack
    );

    self.game.events.addEventListener('ready', (e) => {
      self.shadowRoot.querySelector('#loading').classList.add('done');
      self.game.counter.reset(self.shadowRoot.querySelector('#counter'));
    });

    self.game.events.addEventListener('win', (e) => {
      const attr = self.myAttributes();
      const nextLevelMatrix = self.game.nextLevel(attr);
      self.settings.scores.currentPlayer.addScore(
        attr.matrix,
        self.game.counter
      );

      if (attr.challenge === 'off') {
        self.reset();
        self.render();
      } else {
        self.setAttribute('matrix', nextLevelMatrix);
      }
    });

    getManifestJson()
      .then((manifest) => {
        self.manifest = manifest;

        return self.imageServer
          .getCardImages({ width, height, imageCount })
          .catch((e) => {
            new Fallback(self, { width, height, imageCount });
          });
      })
      .catch((e) => {
        new Fallback(self, { width, height, imageCount });
      });
  }

  /**
   * Gets current attributes according to observedAttributes.
   * @return {object} attr Obeserved values object.
   */
  myAttributes() {
    const attr = {};

    observedAttributes.forEach((oa) => {
      attr[oa] = this.getAttribute(oa);
    });

    return attr;
  }
  /**
   * @summary Renders custom element with initial attributes.
   */
  render() {
    const self = this;
    if (self.rendered) {
      return;
    }
    self.i18n = new Localization(self.getAttribute('lang'));
    self.rendered = true;

    if (!self.shadowRoot) {
      self.attachShadow({ mode: 'open' });
    }

    self.shadowRoot.innerHTML = `<style>${style}</style>`;
    if (!self.checkDefaultAttributes()) {
      self.i18n.update(self.shadowRoot);
      return;
    }

    self.shadowRoot.innerHTML = `
    <style>${style}</style>
      ${tmplToolbar}
      ${self.settings.html}
      ${self.layers.nextLevel.html}
          <div id="loading">
              <div class="lastScore">${
                self.settings.scores.currentPlayer.lastGame
              }
              </div>
            <div class="goto next level">${self.i18n.message('LOADING')}</div>
          </div>`;

    self.prepareMatrix();
    self.i18n.update(self.shadowRoot);

    self.settings.updateChildren(self.shadowRoot);
    self.layers.toolbar = {
      counter: this.shadowRoot.querySelector('#counter'),
      player: this.shadowRoot.querySelector('#player'),
      menu: this.shadowRoot.querySelector('#menu'),
    };

    self.settings.reset(self.myAttributes());

    self.layers.toolbar.menu.addEventListener('click', () => {
      self.settings.reset(self.myAttributes());
      self.settings.show();
    });

    self.layers.nextLevel.init();
    self.layers.nextLevel.show();
  }
}