Source: gfx/BackgroundMap.js

const Node = require('./core/Node');
const Sprite = require('./core/sprites/Sprite');
const Texture = require('./core/textures/Texture');
const { textureFromData } = require('./utils');
const { filmstrip } = require('./utils');

const TILESETS = {};
const POOL = [];

/**
 * Tilemap node
 */
class BackgroundMap extends Node {
  /**
   * @constructor
   * @param  {Number} tilesize  Size of a single tile(in pixel)
   * @param  {Array} data       Map ata
   * @param  {Texture} tileset  Tileset texture
   */
  constructor(tilesize, data, tileset) {
    super();

    if (!Number.isFinite(tilesize) || tilesize <= 0) {
      console.log('Invalid tilesize!');
      return;
    }
    if (!Array.isArray(data) || (data.length === 0) || !Array.isArray(data[0])) {
      console.log('Invalid data format!');
      return;
    }
    if (!tileset || !(tileset instanceof Texture)) {
      console.log('Invalid tileset!');
      return;
    }

    this.tilesize = tilesize;
    this.data = data;
    this.tileset = tileset;
    this.tilesetTextures = null;
    this.tileSprites = null;

    this._width = data[0].length;
    this._height = data.length;

    this.parseTileset();
    this.drawTiles();
  }

  /**
   * Width of this map (in tile)
   * @readonly
   */
  get width() {
    return this._width;
  }
  /**
   * Height of this map (in tile)
   * @readonly
   */
  get height() {
    return this._height;
  }

  /**
   * Get the tile with its row and column
   * @param  {Number} r Row
   * @param  {Number} q Column
   * @return {Number}   Tile index
   */
  getTile(r, q) {
    return (q >= 0 && q < this._width && r >= 0 && r < this._height) ? this.data[r][q] : 0;
  }
  /**
   * Get the tile at a specific position
   * @param  {Number} x X position
   * @param  {Number} y Y position
   * @return {Number}   Tile index
   */
  getTileAt(x, y) {
    const q = Math.floor(x / this.tilesize);
    const r = Math.floor(y / this.tilesize);

    return (q >= 0 && q < this._width && r >= 0 && r < this._height) ? this.data[r][q] : 0;
  }

  /**
   * Set the tile at (row, column)
   * @param {Number} r    Row
   * @param {Number} q    Column
   * @param {Number} tile Tile index to set
   */
  setTile(r, q, tile) {
    if (q >= 0 && q < this._width && r >= 0 && r < this._height) {
      this.data[r][q] = tile;

      if (tile > 0) {
        this.tileSprites[r][q].visible = true;
        this.tileSprites[r][q].texture = this.tilesetTextures[tile - 1];
      }
      else {
        this.tileSprites[r][q].visible = false;
      }
    }
  }
  /**
   * Set the tile at a specific position
   * @param {Number} x    X position
   * @param {Number} y    Y position
   * @param {Number} tile Tile index
   */
  setTileAt(x, y, tile) {
    const q = Math.floor(x / this.tilesize);
    const r = Math.floor(y / this.tilesize);
    if (q >= 0 && q < this._width && r >= 0 && r < this._height) {
      this.data[r][q] = tile;

      if (tile > 0) {
        this.tileSprites[r][q].visible = true;
        this.tileSprites[r][q].texture = this.tilesetTextures[tile - 1];
      }
      else {
        this.tileSprites[r][q].visible = false;
      }
    }
  }

  /**
   * Parse the tileset of this map
   * @private
   */
  parseTileset() {
    let tileList;

    let uid = this.tileset.baseTexture.uid;
    if (TILESETS.hasOwnProperty(uid) && Array.isArray(TILESETS[uid])) {
      tileList = TILESETS[uid];
    }
    else {
      tileList = filmstrip(this.tileset, this.tilesize, this.tilesize);
      TILESETS[uid] = tileList;
    }

    this.tilesetTextures = tileList;
  }

  /**
   * Draw tiles of this map
   * @private
   */
  drawTiles() {
    // Draw nothing if tileset is invalid
    if (!this.tileset || !this.tilesetTextures || this.tilesetTextures.length === 0) {
      return;
    }

    // Create sprites to draw the map
    this.createTileSprites();

    // Update texture of each tile
    let q, r, tile, idx;
    for (r = 0; r < this._height; r++) {
      for (q = 0; q < this._width; q++) {
        idx = this.data[r][q] - 1;
        tile = this.tileSprites[r][q];

        if (idx < 0) {
          tile.visible = false;
        }
        else {
          tile.visible = true;
          tile.texture = this.tilesetTextures[idx];
        }
      }
    }
  }

  /**
   * Create sprites for drawing
   * @private
   */
  createTileSprites() {
    this.tileSprites = new Array(this._height);

    // Insert tiles to fit map size
    let q, r, tile, row;
    for (r = 0; r < this._height; r++) {
      row = new Array(this._width);

      for (q = 0; q < this._width; q++) {
        tile = POOL.pop();
        if (!tile) {
          tile = new Sprite();
        }
        tile.position.set(q * this.tilesize, r * this.tilesize);
        this.addChild(tile);

        row[q] = tile;
      }

      this.tileSprites[r] = row;
    }
  }
}

/**
 * BackgroundMap factory
 * @param  {Number} tilesize Size of a single tile
 * @param  {Array}  data     Map data
 * @param  {Texture} tileset Tileset texture
 * @return {BackgroundMap}   BackgroundMap instance
 */
module.exports = function(tilesize = 8, data = [[]], tileset = null) {
  return new BackgroundMap(tilesize, data, textureFromData(tileset));
};