Reference Source Test

WTF-Adventure/client/renderer/renderer.js

/* global document */
import _ from 'underscore';
import $ from 'jquery';
import log from '../lib/log';
import Camera from './camera';
import Tile from './tile';
import Character from '../entity/character/character';
import Item from '../entity/objects/item';
import Detect from '../utils/detect';
import { isIntersecting } from '../utils/util';

const getX = (index, width) => {
  if (index === 0) {
    return 0;
  }

  return index % width === 0
    ? width - 1
    : (index % width) - 1;
};

export default class Renderer {
  constructor(backgroundCanvas, entitiesCanvas, foregroundCanvas, textCanvas, cursorCanvas, game) {
    this.backgroundCanvas = backgroundCanvas;
    this.entitiesCanvas = entitiesCanvas;
    this.foregroundCanvas = foregroundCanvas;
    this.textCanvas = textCanvas;
    this.cursorCanvas = cursorCanvas;

    this.context = entitiesCanvas.getContext('2d');
    this.backContext = backgroundCanvas.getContext('2d');
    this.foreContext = foregroundCanvas.getContext('2d');
    this.textContext = textCanvas.getContext('2d');
    this.cursorContext = cursorCanvas.getContext('2d');

    this.context.imageSmoothingEnabled = false;
    this.backContext.imageSmoothingEnabled = false;
    this.foreContext.imageSmoothingEnabled = false;
    this.textContext.imageSmoothingEnabled = true;
    this.cursorContext.imageSmoothingEnabled = false;

    this.contexts = [this.backContext, this.foreContext, this.context];
    this.canvases = [
      this.backgroundCanvas,
      this.entitiesCanvas,
      this.foregroundCanvas,
      this.textCanvas,
      this.cursorCanvas,
    ];

    this.game = game;
    this.camera = null;
    this.entities = null;
    this.input = null;

    this.checkDevice();

    this.scale = 1;
    this.tileSize = 16;
    this.fontSize = 10;

    this.screenWidth = 0;
    this.screenHeight = 0;

    this.time = new Date();

    this.fps = 0;
    this.frameCount = 0;
    this.renderedFrame = [0, 0];
    this.lastTarget = [0, 0];

    this.animatedTiles = [];

    this.resizeTimeout = null;
    this.autoCentre = false;

    this.drawTarget = false;
    this.selectedCellVisible = false;

    this.stopRendering = false;
    this.animateTiles = true;
    this.debugging = false;
    this.brightness = 100;
    this.drawNames = true;
    this.drawLevels = false;
    this.forceRendering = false;
    this.textCanvas = $('#textCanvas');

    this.loadRenderer();
  }

  stop() {
    log.debug('Renderer - stop()');

    this.camera = null;
    this.input = null;
    this.stopRendering = true;

    this.forEachContext((context) => {
      context.fillStyle = '#12100D'; // eslint-disable-line
      context.fillRect(0, 0, context.canvas.width, context.canvas.height);
    });
  }

  loadRenderer() {
    log.debug('Renderer - loadRenderer()');

    this.scale = this.getScale();
    this.drawingScale = this.getDrawingScale();

    this.forEachContext((context) => {
      context.imageSmoothingEnabled = false; // eslint-disable-line
      context.webkitImageSmoothingEnabled = false; // eslint-disable-line
      context.mozImageSmoothingEnabled = false; // eslint-disable-line
      context.msImageSmoothingEnabled = false; // eslint-disable-line
      context.oImageSmoothingEnabled = false; // eslint-disable-line
    });
  }

  loadSizes() {
    // log.debug('Renderer - loadSizes()');

    if (!this.camera) {
      return;
    }

    this.screenWidth = this.camera.gridWidth * this.tileSize;
    this.screenHeight = this.camera.gridHeight * this.tileSize;

    const width = $('#container').width() - this.camera.gridWidth;
    const height = $('#container').height() - this.camera.gridHeight;

    this.forEachCanvas((canvas) => {
      canvas.width = width; // eslint-disable-line
      canvas.height = height; // eslint-disable-line
    });
  }

  loadCamera() {
    // log.debug('Renderer - loadCamera()');

    const { storage } = this.game;
    this.camera = new Camera(this);

    this.loadSizes();

    if (
      storage.data.new
      && (this.firefox
        || parseFloat(Detect.androidVersion()) < 6.0
        || parseFloat(Detect.iOSVersion() < 9.0)
        || Detect.isIpad())
    ) {
      this.camera.centered = false;

      storage.data.settings.centerCamera = false;
      storage.save();
    }
  }

  resize() {
    // log.debug('Renderer - resize()');

    this.stopRendering = true;

    this.clearAll();
    this.checkDevice();

    if (!this.resizeTimeout) {
      this.resizeTimeout = setTimeout(() => {
        this.scale = this.getScale();
        this.drawingScale = this.getDrawingScale();

        if (this.camera) {
          this.camera.update();
        }

        this.updateAnimatedTiles();

        this.loadSizes();

        if (this.entities) {
          this.entities.update();
        }

        if (this.map) {
          this.map.updateTileset();
        }

        if (this.camera) {
          this.camera.centreOn(this.game.player);
        }

        if (this.game.interface) {
          this.game.interface.resize();
        }

        this.renderedFrame[0] = -1;
        this.stopRendering = false;
        this.resizeTimeout = null;
      }, 500);
    }
  }

  render() {
    // log.debug('Renderer - render()');

    if (this.stopRendering) {
      return;
    }

    this.clearScreen(this.context);
    this.clearText();
    this.saveAll();

    /**
     * Rendering related draws
     */
    this.draw();
    this.drawAnimatedTiles();

    // the annoying square under the cursor
    // this.drawTargetCell();

    this.drawSelectedCell();
    this.drawEntities();
    this.drawInfos();
    this.drawDebugging();
    this.restoreAll();
    this.drawCursor();
  }

  /**
   * Context Drawing
   */

  draw() {
    // log.debug('Renderer - draw()');

    if (this.hasRenderedFrame()) {
      // log.debug('has rendered rate', this.hasRenderedFrame());
      return;
    }

    this.clearDrawing();
    this.updateDrawingView();

    this.forEachVisibleTile((id, index) => {
      const isHighTile = this.map.isHighTile(id);
      const context = isHighTile ? this.foreContext : this.backContext;

      if (!this.map.isAnimatedTile(id) || !this.animateTiles) {
        this.drawTile(
          context,
          id,
          this.tileset,
          this.tileset.width / this.tileSize,
          this.map.width,
          index,
        );
      }
    });

    this.saveFrame();
  }

  drawAnimatedTiles() {
    // log.debug('Renderer - drawAnimatedTiles()');

    this.setCameraView(this.context);

    if (!this.animateTiles) {
      return;
    }

    this.forEachAnimatedTile((tile) => {
      this.drawTile(
        this.context,
        tile.id,
        this.tileset,
        this.tileset.width / this.tileSize,
        this.map.width,
        tile.index,
      );
      tile.loaded = true; // eslint-disable-line
    });
  }

  drawInfos() {
    // log.debug('Renderer - drawInfos()');

    if (this.game.info.getCount() === 0) {
      return;
    }

    this.game.info.forEachInfo((info) => {
      const factor = this.mobile ? 2 : 1;

      this.textContext.save();
      this.textContext.font = '24px sans serif';
      this.setCameraView(this.textContext);
      this.textContext.globalAlpha = info.opacity;
      this.drawText(
        `${info.text}`,
        Math.floor((info.x + 8) * factor),
        Math.floor(info.y * factor),
        true,
        info.fill,
        info.stroke,
      );
      this.textContext.restore();
    });
  }

  drawDebugging() {
    // log.debug('Renderer - drawDebugging()');

    if (!this.debugging) {
      return;
    }

    this.drawFPS();

    if (!this.mobile) {
      this.drawPosition();
      this.drawPathing();
    }
  }

  drawEntities() {
    // log.debug('Renderer - drawEntities()');

    this.forEachVisibleEntity((entity) => {
      if (entity.spriteLoaded) {
        // log.debug('drawEntities', entity);
        this.drawEntity(entity);
      }
    });
  }

  drawEntity(entity) {
    // log.debug('Renderer - drawEntity()', entity);

    const { sprite } = entity;
    const animation = entity.currentAnimation;
    const data = entity.renderingData;

    if (!sprite || !animation || !entity.isVisible()) {
      return;
    }

    const frame = animation.currentFrame;
    const x = frame.x * this.drawingScale;
    const y = frame.y * this.drawingScale;
    const dx = entity.x * this.drawingScale;
    const dy = entity.y * this.drawingScale;
    const flipX = dx + this.tileSize * this.drawingScale;
    const flipY = dy + data.height;

    this.context.save();

    if (data.scale !== this.scale || data.sprite !== sprite) {
      data.scale = this.scale;
      data.sprite = sprite;
      data.width = sprite.width * this.drawingScale;
      data.height = sprite.height * this.drawingScale;
      data.ox = sprite.offsetX * this.drawingScale;
      data.oy = sprite.offsetY * this.drawingScale;

      if (entity.angled) {
        data.angle = (entity.angle * Math.PI) / 180;
      }

      if (entity.hasShadow()) {
        data.shadowWidth = this.shadowSprite.width * this.drawingScale;
        data.shadowHeight = this.shadowSprite.height * this.drawingScale;
        data.shadowOffsetY = entity.shadowOffsetY * this.drawingScale;
      }
    }

    if (entity.fading) {
      this.context.globalAlpha = entity.fadingAlpha;
    }

    if (entity.spriteFlipX) {
      this.context.translate(flipX, dy);
      this.context.scale(-1, 1);
    } else if (entity.spriteFlipY) {
      this.context.translate(dx, flipY);
      this.context.scale(1, -1);
    } else this.context.translate(dx, dy);

    if (entity.angled) {
      this.context.rotate(data.angle);
    }

    if (entity.hasShadow()) {
      this.context.drawImage(
        this.shadowSprite.image,
        0,
        0,
        data.shadowWidth,
        data.shadowHeight,
        0,
        data.shadowOffsetY,
        data.shadowWidth,
        data.shadowHeight,
      );
    }

    this.drawEntityBack(entity);

    this.context.drawImage(
      sprite.image,
      x,
      y,
      data.width,
      data.height,
      data.ox,
      data.oy,
      data.width,
      data.height,
    );

    if (
      entity instanceof Character
      && !entity.dead
      && !entity.teleporting
      && entity.hasWeapon()
    ) {
      const weaponSprite = this.entities.getSprite(entity.weapon.getName());

      if (weaponSprite) {
        if (!weaponSprite.loaded) {
          weaponSprite.loadSprite();
        }

        const weaponAnimationData = weaponSprite.animationData[animation.name];

        const index = frame.index < weaponAnimationData.length
          ? frame.index
          : frame.index % weaponAnimationData.length;


        const weaponX = weaponSprite.width * index * this.drawingScale;
        const weaponY = weaponSprite.height * animation.row * this.drawingScale;
        const weaponWidth = weaponSprite.width * this.drawingScale;
        const weaponHeight = weaponSprite.height * this.drawingScale;

        this.context.drawImage(
          weaponSprite.image,
          weaponX,
          weaponY,
          weaponWidth,
          weaponHeight,
          weaponSprite.offsetX * this.drawingScale,
          weaponSprite.offsetY * this.drawingScale,
          weaponWidth,
          weaponHeight,
        );
      }
    }

    if (entity instanceof Item) {
      const {
        sparksAnimation,
      } = this.entities.sprites;
      const sparksFrame = sparksAnimation.currentFrame;

      if (data.scale !== this.scale) {
        data.sparksX = this.sparksSprite.width * sparksFrame.index * this.drawingScale;
        data.sparksY = this.sparksSprite.height * sparksAnimation.row * this.drawingScale;

        data.sparksWidth = this.sparksSprite.width * this.drawingScale;
        data.sparksHeight = this.sparksSprite.height * this.drawingScale;
      }

      this.context.drawImage(
        this.sparksSprite.image,
        data.sparksX,
        data.sparksY,
        data.sparksWidth,
        data.sparksHeight,
        0,
        0,
        data.sparksWidth,
        data.sparksHeight,
      );
    }

    this.drawEntityFore(entity);

    this.context.restore();

    this.drawHealth(entity);
    this.drawName(entity);
  }

  /**
   * Function used to draw special effects prior
   * to rendering the entity.
   */
  drawEntityBack(entity) {
    // const self = this;
    // @TODO
  }

  drawEntityFore(entity) {
    // log.debug('Renderer - drawEntityFore()');

    /**
     * Function used to draw special effects after
     * having rendererd the entity
     */

    if (
      entity.terror
      || entity.stunned
      || entity.critical
      || entity.explosion
    ) {
      const sprite = this.entities.getSprite(entity.getActiveEffect());

      if (!sprite.loaded) {
        sprite.loadSprite();
      }

      if (sprite) {
        const animation = entity.getEffectAnimation();
        const { index } = animation.currentFrame;
        const x = sprite.width * index * this.drawingScale;
        const y = sprite.height * animation.row * this.drawingScale;
        const width = sprite.width * this.drawingScale;
        const height = sprite.height * this.drawingScale;
        const offsetX = sprite.offsetX * this.drawingScale;
        const offsetY = sprite.offsetY * this.drawingScale;

        this.context.drawImage(
          sprite.image,
          x,
          y,
          width,
          height,
          offsetX,
          offsetY,
          width,
          height,
        );
      }
    }
  }

  drawHealth(entity) {
    // log.debug('Renderer - entity()');

    if (!entity.hitPoints || entity.hitPoints < 0 || !entity.healthBarVisible) {
      return;
    }

    const barLength = 16;
    const healthX = entity.x * this.drawingScale - barLength / 2 + 8;
    const healthY = (entity.y - 9) * this.drawingScale;

    const healthWidth = Math.round(
      (entity.hitPoints / entity.maxHitPoints)
      * barLength
      * this.drawingScale,
    );

    const healthHeight = 2 * this.drawingScale;

    this.context.save();
    this.context.strokeStyle = '#00000';
    this.context.lineWidth = 1;
    this.context.strokeRect(
      healthX,
      healthY,
      barLength * this.drawingScale,
      healthHeight,
    );
    this.context.fillStyle = '#FD0000';
    this.context.fillRect(healthX, healthY, healthWidth, healthHeight);
    this.context.restore();
  }

  drawName(entity) {
    // log.debug('Renderer - drawName()');

    if (entity.hidden || (!this.drawNames && !this.drawLevels)) {
      return;
    }

    let colour = entity.wanted ? 'red' : 'white';
    const factor = this.mobile ? 2 : 1;

    if (entity.rights > 1) {
      colour = '#ba1414';
    } else if (entity.rights > 0) {
      colour = '#a59a9a';
    }

    if (entity.id === this.game.player.id) {
      colour = '#fcda5c';
    }

    this.textContext.save();
    this.setCameraView(this.textContext);
    this.textContext.font = '14px sans serif';

    if (!entity.hasCounter) {
      if (this.drawNames && entity !== 'player') {
        this.drawText(
          entity.username,
          (entity.x + 8) * factor,
          (entity.y - (this.drawLevels ? 20 : 10)) * factor,
          true,
          colour,
        );
      }

      if (
        this.drawLevels
        && (entity.type === 'mob' || entity.type === 'player')
      ) {
        this.drawText(
          `Level ${entity.level}`,
          (entity.x + 8) * factor,
          (entity.y - 10) * factor,
          true,
          colour,
        );
      }

      if (entity.type === 'item' && entity.count > 1) {
        this.drawText(
          entity.count,
          (entity.x + 8) * factor,
          (entity.y - 10) * factor,
          true,
          colour,
        );
      }
    } else {
      if (this.game.time - entity.countdownTime > 1000) {
        entity.countdownTime = this.game.time; // eslint-disable-line
        entity.counter -= 1; // eslint-disable-line
      }

      if (entity.counter <= 0) {
        entity.hasCounter = false; // eslint-disable-line
      }

      this.drawText(
        entity.counter,
        (entity.x + 8) * factor,
        (entity.y - 10) * factor,
        true,
        colour,
      );
    }

    this.textContext.restore();
  }

  drawCursor() {
    // log.debug('Renderer - drawCursor()');

    if (this.tablet || this.mobile) {
      return;
    }

    const { cursor } = this.input;

    this.clearScreen(this.cursorContext);
    this.cursorContext.save();

    if (cursor && this.scale > 1) {
      if (!cursor.loaded) {
        cursor.loadSprite();
      }

      if (cursor.loaded) {
        this.cursorContext.drawImage(
          cursor.image,
          0,
          0,
          14 * this.drawingScale,
          14 * this.drawingScale,
          this.input.mouse.x,
          this.input.mouse.y,
          14 * this.drawingScale,
          14 * this.drawingScale,
        );
      }
    }

    this.cursorContext.restore();
  }

  drawFPS() {
    // log.debug('Renderer - drawFPS()');

    const currentTime = new Date();
    const timeDiff = currentTime - this.time;

    if (timeDiff >= 1000) {
      this.realFPS = this.frameCount;
      this.frameCount = 0;
      this.time = currentTime;
      this.fps = this.realFPS;
    }

    this.frameCount += 1;

    this.drawText(`FPS: ${this.realFPS}`, 10, 11, false, 'white');
  }

  drawPosition() {
    // log.debug('Renderer - drawPosition()');

    const { player } = this.game;

    this.drawText(
      `x: ${player.gridX} y: ${player.gridY}`,
      10,
      31,
      false,
      'white',
    );
  }

  drawPathing() {
    // log.debug('Renderer - drawPathing()');

    const { pathingGrid } = this.entities.grids;

    if (!pathingGrid) {
      return;
    }

    this.camera.forEachVisiblePosition((x, y) => {
      if (x < 0 || y < 0) {
        return;
      }

      if (pathingGrid[y][x] !== 0) {
        this.drawCellHighlight(x, y, 'rgba(50, 50, 255, 0.5)');
      }
    });
  }

  drawSelectedCell() {
    // log.debug('Renderer - drawSelectedCell()');

    if (!this.input.selectedCellVisible) {
      return;
    }

    const posX = this.input.selectedX;
    const posY = this.input.selectedY;

    // only draw the highlight cell if they are not adjacent
    // from character's current position
    if (!this.game.player.isPositionAdjacent(posX, posY)) {
      this.drawCellHighlight(posX, posY, this.input.mobileTargetColour);
    }
  }

  /**
   * Primitive drawing functions
   */

  drawTile(context, tileId, tileset, setWidth, gridWidth, cellId) {
    // log.debug('Renderer - draw()', context, tileId, tileset, setWidth, gridWidth, cellId);

    if (tileId === -1) {
      return;
    }

    this.drawScaledImage(
      context,
      tileset,
      getX(tileId + 1, setWidth / this.drawingScale) * this.tileSize,
      Math.floor(tileId / (setWidth / this.drawingScale)) * this.tileSize,
      this.tileSize,
      this.tileSize,
      getX(cellId + 1, gridWidth) * this.tileSize,
      Math.floor(cellId / gridWidth) * this.tileSize,
    );
  }

  clearTile(context, gridWidth, cellId) {
    // log.debug('Renderer - clearTile()', context, gridWidth, cellId);

    const x = getX(cellId + 1, gridWidth) * this.tileSize * this.drawingScale;
    const y = Math.floor(cellId / gridWidth) * this.tileSize * this.drawingScale;
    const w = this.tileSize * this.scale;

    context.clearRect(x, y, w, w);
  }

  drawText(text, x, y, centered, colour, strokeColour) {
    // log.debug('Renderer - drawText()', text, x, y, centered, colour, strokeColour);

    let strokeSize = 1;
    const context = this.textContext;

    if (this.scale > 2) {
      strokeSize = 3;
    }

    if (text && x && y) {
      context.save();

      if (centered) {
        context.textAlign = 'center';
      }

      context.strokeStyle = strokeColour || '#373737';
      context.lineWidth = strokeSize;
      context.strokeText(text, x * this.scale, y * this.scale);
      context.fillStyle = colour || 'white';
      context.fillText(text, x * this.scale, y * this.scale);

      context.restore();
    }
  }

  drawScaledImage(context, image, x, y, width, height, dx, dy) {
    // log.debug('Renderer - drawScaledImage()', context, image, x, y, width, height, dx, dy);

    if (!context) {
      return;
    }

    context.drawImage(
      image,
      x * this.drawingScale,
      y * this.drawingScale,
      width * this.drawingScale,
      height * this.drawingScale,
      dx * this.drawingScale,
      dy * this.drawingScale,
      width * this.drawingScale,
      height * this.drawingScale,
    );
  }

  updateAnimatedTiles() {
    // log.debug('Renderer - updateAnimatedTiles()');

    if (!this.animateTiles) {
      return;
    }

    const newTiles = [];

    this.forEachVisibleTile((id, index) => {
      /**
       * We don't want to reinitialize animated tiles that already exist
       * and are within the visible camera proportions. This way we can parse
       * it every time the tile moves slightly.
       */

      if (!this.map.isAnimatedTile(id)) {
        return;
      }

      /**
       * Push the pre-existing tiles.
       */

      const tileIndex = this.animatedTiles.indexOf(id);

      if (tileIndex > -1) {
        newTiles.push(this.animatedTiles[tileIndex]);
        return;
      }

      const tile = new Tile(
        id,
        index,
        this.map.getTileAnimationLength(id),
        this.map.getTileAnimationDelay(id),
      );

      const position = this.map.indexToGridPosition(tile.index);

      tile.setPosition(position);
      newTiles.push(tile);
    }, 2);

    this.animatedTiles = newTiles;
  }

  checkDirty(rectOne, source, x, y) {
    // log.debug('Renderer - checkDirty()', rectOne, source, x, y);

    this.entities.forEachEntityAround(x, y, 2, (entityTwo) => {
      if (source && source.id && entityTwo.id === source.id) return;

      if (!entityTwo.isDirty && isIntersecting(rectOne, this.getEntityBounds(entityTwo))) {
        entityTwo.loadDirty();
      }
    });

    if (source && !source.hasOwnProperty('index')) { // eslint-disable-line
      this.forEachAnimatedTile((tile) => {
        if (!tile.isDirty && isIntersecting(rectOne, this.getTileBounds(tile))) {
          tile.dirty = true; // eslint-disable-line
        }
      });
    }

    if (!this.drawTarget && this.input.selectedCellVisible) {
      const targetRect = this.getTargetBounds();

      if (isIntersecting(rectOne, targetRect)) {
        this.drawTarget = true;
        this.targetRect = targetRect;
      }
    }
  }

  drawCellRect(x, y, colour) {
    // log.debug('Renderer - drawCellRect()', x, y, colour);

    const multiplier = this.tileSize * this.drawingScale;

    this.context.save();
    this.context.lineWidth = 2 * this.drawingScale;
    this.context.translate(x + 2, y + 2);
    this.context.strokeStyle = colour;
    this.context.strokeRect(0, 0, multiplier - 4, multiplier - 4);
    this.context.restore();
  }

  drawCellHighlight(x, y, colour) {
    // log.debug('Renderer - drawCellHighlight()', x, y, colour);

    this.drawCellRect(
      x * this.drawingScale * this.tileSize,
      y * this.drawingScale * this.tileSize,
      colour,
    );
  }

  drawTargetCell() {
    // log.debug('Renderer - drawTargetCell()');

    if (
      this.mobile
      || this.tablet
      || !this.input.targetVisible
      || !this.input
      || !this.camera
    ) {
      return;
    }

    const location = this.input.getCoords();

    if (
      !(
        location.x === this.input.selectedX
        && location.y === this.input.selectedY
      )
    ) {
      this.drawCellHighlight(location.x, location.y, this.input.targetColour);
    }
  }

  /**
   * Primordial Rendering functions
   */

  forEachVisibleIndex(callback, offset) {
    // log.debug('Renderer - forEachVisibleIndex()', callback, offset);

    this.camera.forEachVisiblePosition((x, y) => {
      if (!this.map.isOutOfBounds(x, y)) callback(this.map.gridPositionToIndex(x, y) - 1);
    }, offset);
  }

  forEachVisibleTile(callback, offset) {
    // log.debug('Renderer - forEachVisibleTile()', callback, offset);

    if (!this.map || !this.map.mapLoaded) {
      return;
    }

    this.forEachVisibleIndex((index) => {
      if (_.isArray(this.map.data[index])) {
        _.each(this.map.data[index], (id) => {
          callback(id - 1, index);
        });
      } else if (!isNaN(this.map.data[index] - 1)) { // eslint-disable-line
        callback(this.map.data[index] - 1, index);
      }
    }, offset);
  }

  forEachAnimatedTile(callback) {
    // log.debug('Renderer - forEachAnimatedTile()', callback);

    _.each(this.animatedTiles, (tile) => {
      callback(tile);
    });
  }

  forEachVisibleEntity(callback) {
    // log.debug('Renderer - forEachVisibleEntity()', callback);

    if (!this.entities || !this.camera) {
      return;
    }

    const { grids } = this.entities;

    this.camera.forEachVisiblePosition((x, y) => {
      if (!this.map.isOutOfBounds(x, y) && grids.renderingGrid[y][x]) {
        _.each(grids.renderingGrid[y][x], (entity) => {
          callback(entity);
        });
      }
    });
  }

  isVisiblePosition(x, y) {
    // log.debug('Renderer - isVisiblePosition()', x, y);

    return (
      y >= this.camera.gridY
      && y < this.camera.gridY + this.camera.gridHeight
      && x >= this.camera.gridX
      && x < this.camera.gridX + this.camera.gridWidth
    );
  }

  getScale() {
    // log.debug('Renderer - getScale()');

    return this.game.getScaleFactor();
  }

  getDrawingScale() {
    // log.debug('Renderer - getDrawingScale()');

    let scale = this.getScale();

    if (this.mobile) {
      scale = 2;
    }

    return scale;
  }

  getUpscale() {
    // log.debug('Renderer - getUpscale()');

    let scale = this.getScale();

    if (scale > 2) {
      scale = 2;
    }

    return scale;
  }

  clearContext() {
    // log.debug('Renderer - clearContext()');

    this.context.clearRect(
      0,
      0,
      this.screenWidth * this.scale,
      this.screenHeight * this.scale,
    );
  }

  clearText() {
    // log.debug('Renderer - clearText()');

    this.textContext.clearRect(
      0,
      0,
      this.textCanvas.width * this.scale,
      this.textCanvas.height * this.scale,
    );
  }

  restore() {
    // log.debug('Renderer - restore()');

    this.forEachContext((context) => {
      context.restore();
    });
  }

  clearAll() {
    // log.debug('Renderer - clearAll()');

    this.forEachContext((context) => {
      context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    });
  }

  clearDrawing() {
    // log.debug('Renderer - clearDrawing()');

    this.forEachDrawingContext((context) => {
      context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    });
  }

  saveAll() {
    // log.debug('Renderer - saveAll()');

    this.forEachContext((context) => {
      context.save();
    });
  }

  restoreAll() {
    // log.debug('Renderer - restoreAll()');

    this.forEachContext((context) => {
      context.restore();
    });
  }

  focus() {
    // log.debug('Renderer - focus()');

    this.forEachContext((context) => {
      context.focus();
    });
  }

  /**
   * Rendering Functions
   */

  updateView() {
    // log.debug('Renderer - updateView()');

    this.forEachContext((context) => {
      this.setCameraView(context);
    });
  }

  updateDrawingView() {
    // log.debug('Renderer - updateDrawingView()');

    this.forEachDrawingContext((context) => {
      this.setCameraView(context);
    });
  }

  setCameraView(context) {
    // log.debug('Renderer - setCameraView()');

    if (!this.camera || this.stopRendering) {
      return;
    }

    context.translate(
      -this.camera.x * this.drawingScale,
      -this.camera.y * this.drawingScale,
    );
  }

  clearScreen(context) {
    // log.debug('Renderer - clearScreen()', context);

    context.clearRect(
      0,
      0,
      this.context.canvas.width,
      this.context.canvas.height,
    );
  }

  hasRenderedFrame() {
    // log.debug('Renderer - hasRenderedFrame()');

    if (this.forceRendering) {
      return false;
    }

    if (!this.camera || this.stopRendering || !this.input) {
      return true;
    }

    return (
      this.renderedFrame[0] === this.camera.x
      && this.renderedFrame[1] === this.camera.y
    );
  }

  saveFrame() {
    // log.debug('Renderer - saveFrame()');

    if (!this.hasRenderedFrame()) {
      this.renderedFrame[0] = this.camera.x;
      this.renderedFrame[1] = this.camera.y;
    }
  }

  adjustBrightness(level) {
    // log.debug('Renderer - adjustBrightness()', level);

    if (level < 0 || level > 100) {
      return;
    }

    this.textCanvas.css(
      'background',
      `rgba(0, 0, 0, ${0.5 - level / 200})`,
    );
  }

  loadStaticSprites() {
    log.debug('Renderer - loadStaticSprites()');

    this.shadowSprite = this.entities.getSprite('shadow16');

    // if (!this.shadowSprite.loaded) {
    //   this.shadowSprite.loadSprite();
    // }

    this.sparksSprite = this.entities.getSprite('sparks');

    // if (!this.sparksSprite.loaded) {
    //   this.sparksSprite.loadSprite();
    // }
  }

  /**
   * Miscellaneous functions
   */

  forEachContext(callback) {
    // log.debug('Renderer - forEachContext()', callback);

    _.each(this.contexts, (context) => {
      callback(context);
    });
  }

  forEachDrawingContext(callback) {
    // log.debug('Renderer - forEachDrawingContext()', callback);

    _.each(this.contexts, (context) => {
      if (context.canvas.id !== 'entities') {
        callback(context);
      }
    });
  }

  forEachCanvas(callback) {
    // log.debug('Renderer - forEachCanvas()', callback);

    _.each(this.canvases, (canvas) => {
      callback(canvas);
    });
  }

  checkDevice() {
    // log.debug('Renderer - checkDevice()');

    this.mobile = this.game.client.isMobile();
    this.tablet = this.game.client.isTablet();
    this.firefox = Detect.isFirefox();
  }

  verifyCentration() {
    // log.debug('Renderer - verifyCentration()');

    this.forceRendering = (this.mobile || this.tablet) && this.camera.centered;
  }

  isPortableDevice() {
    // log.debug('Renderer - isPortableDevice()');

    return this.mobile || this.tablet;
  }

  /**
   * Setters
   */

  setTileset(tileset) {
    log.debug('Renderer - setTileset()', tileset);

    this.tileset = tileset;
  }

  setMap(map) {
    // log.debug('Renderer - setMap()', map);

    this.map = map;
  }

  setEntities(entities) {
    // log.debug('Renderer - entities()', entities);

    this.entities = entities;
  }

  setInput(input) {
    // log.debug('Renderer - setInput()', input);

    this.input = input;
  }

  /**
   * Getters
   */

  getTileBounds(tile) {
    // log.debug('Renderer - getTileBounds()', tile);

    const bounds = {};
    const cellId = tile.index;

    bounds.x = (getX(cellId + 1, this.map.width) * this.tileSize
        - this.camera.x)
      * this.drawingScale;
    bounds.y = (Math.floor(cellId / this.map.width) * this.tileSize - this.camera.y)
      * this.drawingScale;
    bounds.width = this.tileSize * this.drawingScale;
    bounds.height = this.tileSize * this.drawingScale;
    bounds.left = bounds.x;
    bounds.right = bounds.x + bounds.width;
    bounds.top = bounds.y;
    bounds.bottom = bounds.y + bounds.height;

    return bounds;
  }

  getEntityBounds(entity) {
    // log.debug('Renderer - getEntityBounds()', entity);

    const bounds = {};
    const { sprite } = entity;

    // TODO - Ensure that the sprite over there has the correct bounds

    if (!sprite) {
      log.error(`Sprite malformation for: ${entity.name}`);
    } else {
      bounds.x = (entity.x + sprite.offsetX - this.camera.x) * this.drawingScale;
      bounds.y = (entity.y + sprite.offsetY - this.camera.y) * this.drawingScale;
      bounds.width = sprite.width * this.drawingScale;
      bounds.height = sprite.height * this.drawingScale;
      bounds.left = bounds.x;
      bounds.right = bounds.x + bounds.width;
      bounds.top = bounds.y;
      bounds.bottom = bounds.y + bounds.height;
    }

    return bounds;
  }

  getTargetBounds(x, y) {
    // log.debug('Renderer - getTargetBounds()', x, y);

    const bounds = {};
    const tx = x || this.input.selectedX;
    const ty = y || this.input.selectedY;

    bounds.x = (tx * this.tileSize - this.camera.x) * this.drawingScale;
    bounds.y = (ty * this.tileSize - this.camera.y) * this.drawingScale;
    bounds.width = this.tileSize * this.drawingScale;
    bounds.height = this.tileSize * this.drawingScale;
    bounds.left = bounds.x;
    bounds.right = bounds.x + bounds.width;
    bounds.top = bounds.y;
    bounds.bottom = bounds.y + bounds.height;

    return bounds;
  }

  getTileset() {
    // log.debug('Renderer - getTileset()');

    return this.tileset;
  }
}