css Audio - Active file-generic CSS - Active Generic - Active HTML - Active JS - Active SVG - Active Text - Active file-generic Video - Active header Love html icon-new-collection icon-person icon-team numbered-list123 pop-out spinner split-screen star tv

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              // Variables
$dashboard: hsla(37, 18%, 20%, 1);
$primary: hsla(34, 77%, 56%, 1);
$secondary: hsla(36, 100%, 65%, 1);
$background: hsla(37, 100%, 96%, 1);
$text: hsla(0, 0, 0, 1);
$text-dark: #323232;
$inverted: hsla(360, 100%, 100%, 1);

// Google Font
@import url("https://fonts.googleapis.com/css?family=Condiment|Playfair+Display:400,900");

@import url("https://fonts.googleapis.com/css?family=Heebo:100,400,700,900");

* {
  font-family: "Heebo", serif;
  font-size: 16px;
}

body {
  background: $background;
  width: 100%;
}

.game {
  margin: 0;
  padding: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;

  .dash {
    width: 100%;
    background: $dashboard;
    color: $inverted;
    margin-bottom: 10px;
    padding: 10px 0 10px;

    @media screen and (min-width: 600px) {
      padding: 40px 0 30px;
    }

    .dash__container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      width: 100%;
      max-width: 1200px;
      margin: auto;
      padding: 0 10px;
      box-sizing: border-box;

      @media screen and (min-width: 600px) {
        flex-direction: row;
        justify-content: space-between;
      }

      .dash__title {
        font-size: 1.2rem;
        font-family: "Condiment", cursive;
        padding: 0;
        margin: 0 0 10px 0;

        @media screen and (min-width: 600px) {
          font-size: 2rem;
          margin: 0 0 0 10px;
        }

        @media screen and (min-width: 1200px) {
          margin: 0 0 0 0;
        }
      }

      .dash__status {
        margin: 0;
        padding: 0;
        list-style: none;
        display: flex;
        justify-content: flex-start;

        li {
          font-size: 0.9rem;
          margin: 0 10px;
          
          strong {
            font-size: .9rem;
          }

          @media screen and (min-width: 1200px) {
            text-align: right;

            &:last-child {
              margin: 0 0 0 10px;
            }
          }
        }
      }
    }
  }

  .panel {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: flex-start;
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 10px;
    box-sizing: border-box;

    .panel__message {
      width: 100%;
      margin: 0 10px 0 0;
      padding: 10px;
      
      p {
        color: #171819;
        font-size: 0.75rem;
        padding: 0;
        margin: 0;
        width: 100%;

        @media screen and (min-width: 800px) {
          font-size: 1.1rem;
          text-align: center;
        }
      }
    }

    .panel__button {
      border: none;
      cursor: pointer;
      font: inherit;
      outline: inherit;
      padding: 9px 15px;
      font-weight: 700;
      color: $text-dark;
    }

    .panel__button-dark {
      background: #000;
      color: #fff;
    }

    .panel__button-light {
      background: #fff;
      color: #000;
    }
  }

  .display {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;

    @media screen and (min-width: 1200px) {
      margin: 10px 0;
    }

    canvas {
      padding: 10px;
      box-sizing: border-box;
      width: 100%;
      max-width: 1200px;
      margin: auto;
    }

    .display-flash {
      box-shadow: inset 0 0 10px 10px hsl(1, 75%, 60%);
    }
  }
}

            
          
!
            
              /* Globals obj containing all global var/func */
const GLOBALS = {
  unitSize: 20,
  health: 100,
  attack: 5,
  expToNext: 100,
  weapon: "Worn Katana",
  color: "rgb(199,102,201)",
  mapSize: 500,
  names: [
    "Masamune",
    "Muramasa",
    "Dojigiri",
    "Onimaru",
    "Mikazuki-munechika",
    "Odenta",
    "Jyuzumaru",
    "Kusanagi-no-tsurugi",
    "Tsugi Twig",
    "Kamata Kitchen Knife"
  ],

  /* @desc get an x and y position from map, checking for overlap in collMap */
  getPos: (map, collMap) => {
    let coords = {};
    let xPos = null;
    let yPos = null;
    while (!xPos) {
      let x = Math.floor(Math.random() * map.length);
      let y = Math.floor(Math.random() * map.length);
      if (map[x][y] != 1) {
        continue;
      }
      if (collMap[x][y] == 0) {
        coords.xPos = xPos = x;
        coords.yPos = yPos = y;
      }
    }
    return coords;
  }
};

/* 
 * @desc this class will be used to generate a single player and a set number of npc's.
 * @params
 * attack: num
 * expToNext: [optional] num
 * health: num
 * level: num
 * weapon: [optional] obj
 * xPos: num
 * yPos: num
 * player: bool
 * color: string
 */
class Humanoid {
  constructor(
    health,
    attack,
    level,
    xPos,
    yPos,
    player,
    color,
    expToNext = 9999,
    weapon = null
  ) {
    this._attack = attack;
    this._color = color;
    this._exp = 0;
    this._expToNext = expToNext;
    this._health = health;
    this._level = level;
    this._player = player;
    this._weapon = weapon;
    this._xPos = xPos;
    this._yPos = yPos;
  }

  get attack() {
    return this._attack;
  }

  get color() {
    return this._color;
  }

  get exp() {
    return this._exp;
  }

  get expToNext() {
    return this._expToNext - this._exp;
  }

  get health() {
    return this._health;
  }

  get level() {
    return this._level;
  }

  get weapon() {
    if (!this._weapon) {
      return;
    }
    return this._weapon;
  }

  get xPos() {
    return this._xPos;
  }

  get yPos() {
    return this._yPos;
  }

  set attack(attack) {
    this._attack = attack;
  }

  set exp(exp) {
    this._exp = exp;
  }

  set expToNext(expToNext) {
    this._expToNext = expToNext;
  }

  set health(health) {
    this._health = health;
  }

  set level(level) {
    this._level = level;
  }

  set weapon(weapon) {
    this._weapon = weapon;
  }

  set xPos(pos) {
    this._xPos = pos;
  }

  set yPos(pos) {
    this._yPos = pos;
  }
}

/* 
 * @desc this class will be used to generate health and weapon items.
 * @params
 * name: string
 * type: string
 * xPos: num
 * yPos: num
 * health: num
 * attack: num
 */
class Item {
  constructor(name, type, xPos = null, yPos = null) {
    this._attack =
      type == "weapon" ? Math.floor(Math.random() * 15 + 5) + 10 : null;
    this._health =
      type == "weapon"
        ? Math.floor(Math.random() * 10)
        : Math.floor(Math.random() * 30) + 10;
    this._name = name;
    this._type = type;
    this._xPos = xPos;
    this._yPos = yPos;
  }

  get name() {
    return this._name;
  }

  get health() {
    return this._health;
  }

  get attack() {
    return this._attack;
  }

  get xPos() {
    return this._xPos;
  }

  get yPos() {
    return this._yPos;
  }

  get type() {
    return this._type;
  }
  
  set health(hp) {
    this._health = hp;
  }
}

/* React begins here! */

/* @desc functional component to display the games dashboard. 
 * Display health, attack, level, exp to next level, and weapon to user.
 */
function Dashboard(props) {
  const npcs = props.npcsArr;
  const boss = props.active;
  let text;
  let count;
  if (boss) {
    text = `Mitsuhide`;
    count = 1;
  } else {
    text = `Enemies`;
    count = npcs.length;
  }
  return (
    <div className="dash">
      <div className="dash__container">
        <h1 className="dash__title">Hideyoshi's Revenge</h1>
        <ul className="dash__status">
          <li>
            Weapon<br />
            <strong>{props.player.weapon.name}</strong>
          </li>
          <li>
            Health<br />
            <strong>{props.player.health + props.player.weapon.health}</strong>
          </li>
          <li>
            Attack<br />
            <strong>{props.player.attack + props.player.weapon.attack}</strong>
          </li>
          <li>
            Exp.<br />
            <strong>{props.player.expToNext}</strong>
          </li>
          <li>
            Level<br />
            <strong>{props.player.level}</strong>
          </li>
          <li>
            { text }<br />
            <strong>{ count }</strong>
          </li>
        </ul>
      </div>
    </div>
  );
}

function Panel(props) {
  return (
    <div className="panel">
      <Message message={props.message} />
      <Button onClick={props.onClick} darkness={props.darkness} />
    </div>
  );
}

function Message(props) {
  return (
    <div className="panel__message">
      <p>{props.message}</p>
    </div>
  );
}

function Button(props) {
  return (
    <button
      className={
        props.darkness
          ? "panel__button panel__button-light"
          : "panel__button panel__button-dark"
      }
      onClick={props.onClick}
    >
      Darkness
    </button>
  );
}

/* @desc functional component to display the games Canvas */
function Display(props) {
  return (
    <div className="display">
      <GameCanvas
        map={props.map}
        collMap={props.collMap}
        player={props.player}
        npcs={props.npcs}
        darkness={props.darkness}
        combat={props.combat}
        boss={props.boss}
      />
    </div>
  );
}

/* @desc canvas component */
class GameCanvas extends React.Component {
  constructor(props) {
    super(props);
    this.calcBound = this.calcBound.bind(this);
    this.clearCanvas = this.clearCanvas.bind(this);
    this.drawCanvas = this.drawCanvas.bind(this);
    this.getBounds = this.getBounds.bind(this);
    this.getColor = this.getColor.bind(this);
    this.getDist = this.getDist.bind(this);
  }

  /*
   * Set width and height of canvas
   * Draw portion of map based on width, height, and player coords
   */
  componentDidMount() {
    this.canvas.height = Math.floor(window.innerHeight - 200);
    this.canvas.width =
      window.innerWidth < 1200 ? Math.floor(window.innerWidth - 20) : 1200;
    const heightInUnits = Math.floor(this.canvas.height / GLOBALS.unitSize);
    const widthInUnits = Math.floor(this.canvas.width / GLOBALS.unitSize);
    const player = this.props.player;
    const boundaries = this.getBounds(player, heightInUnits, widthInUnits);
    const map = this.props.map;
    const collMap = this.props.collMap;
    const npcs = this.props.npcs;
    const boss = this.props.boss;
    const ctx = this.canvas.getContext("2d");
    this.drawCanvas(ctx, map, collMap, boundaries, npcs, boss, player);
  }

  /* Draw portion of map based on width, height, and player coords */
  componentDidUpdate() {
    const heightInUnits = Math.floor(this.canvas.height / GLOBALS.unitSize);
    const widthInUnits = Math.floor(this.canvas.width / GLOBALS.unitSize);
    const player = this.props.player;
    const boundaries = this.getBounds(player, heightInUnits, widthInUnits);
    const map = this.props.map;
    const collMap = this.props.collMap;
    const npcs = this.props.npcs;
    const boss = this.props.boss;
    const ctx = this.canvas.getContext("2d");
    this.clearCanvas(ctx);
    this.drawCanvas(ctx, map, collMap, boundaries, npcs, boss, player);
  }

  /* @desc iterate through game and collision map
   * draw square for each individual unit
   * map:
   * 1 | 0
   * collMap:
   * player = 1
   * npc = 2
   * weapon = 3
   * health = 4
   */
  drawCanvas(ctx, map, collMap, boundaries, npcs, boss, player) {
    let { height, width } = boundaries;
    for (let row = width[0]; row <= width[1]; row++) {
      for (let col = height[0]; col <= height[1]; col++) {
        ctx.save();
        if (this.props.darkness && this.getDist(player, row, col) > 6) {
          ctx.fillStyle = "black";
        } else {
          ctx.fillStyle =
            map[row][col] == 1 ? "#c99766" : "hsla(37, 100%, 96%, 1)"; //brown or light brown
          switch (collMap[row][col]) {
            case 4:
              ctx.fillStyle = "#66c96c"; //green hsl(123.6,47.8%,59.4%);
              break;
            case 3:
              ctx.fillStyle = "#6667c9"; //blue hsl(239.4,47.8%,59.4%);
              break;
            case 2:
              ctx.fillStyle = boss ? this.getColor([boss], row, col) : this.getColor(npcs, row, col);
              break;
            case 1:
              ctx.fillStyle = "#c766c9"; //purple hsl(298.8,47.8%,59.4%);
              break;
            default:
          }
        }

        ctx.fillRect(
          (row - width[0]) * GLOBALS.unitSize,
          (col - height[0]) * GLOBALS.unitSize,
          GLOBALS.unitSize,
          GLOBALS.unitSize
        );
        ctx.restore();
      }
    }
  }

  /* @desc completely clear canvas */
  clearCanvas(ctx) {
    ctx.save();
    ctx.fillStyle = "#c99766";
    ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
    ctx.restore();
  }

  /* 
   * @desc calculate start and end points for given width or height in units
   * based on players current position 
   * @return array containing start and end points [start, end]
   */

  calcBound(length, pos) {
    let halfLength = length / 2;
    let arr = [];
    if (halfLength > pos) {
      arr = [0, length];
    } else if (halfLength + pos >= GLOBALS.mapSize - 1) {
      arr = [GLOBALS.mapSize - (length + 1), GLOBALS.mapSize - 1];
    } else {
      arr = [
        Math.floor(pos - halfLength),
        Math.floor(pos - halfLength) + length
      ];
    }
    return arr;
  }

  /* 
   * @desc create boundary object to choose which portion of map to draw 
   * @return boundaries object with start/end points for height and width
   */

  getBounds(player, height, width) {
    let boundaries = {};
    boundaries.height = this.calcBound(height, player.yPos);
    boundaries.width = this.calcBound(width, player.xPos);
    return boundaries;
  }

  getColor(arr, xPos, yPos) {
    for (let item of arr) {
      if (item.xPos == xPos && item.yPos == yPos) {
        return item.color;
      }
    }
    return `hsl(0, 0, 0)`;
  }

  getDist(player, xPos, yPos) {
    let x = (player.xPos - xPos) ** 2;
    let y = (player.yPos - yPos) ** 2;
    return Math.sqrt(x + y);
  }

  render() {
    return (
      <canvas
        ref={canvas => (this.canvas = canvas)}
        className={this.props.combat ? "display-flash" : ""}
      />
    );
  }
}

/* @desc primary App Component */
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      map: [],
      collMap: [],
      player: {},
      npcs: [],
      items: [],
      combat: false,
      active: false,
      alive: true,
      darkness: true,
      complete: false,
      message: "",
      touch: {}
    };
    this.checkCollision = this.checkCollision.bind(this);
    this.gainExp = this.gainExp.bind(this);
    this.generateHumanoid = this.generateHumanoid.bind(this);
    this.generateItem = this.generateItem.bind(this);
    this.generateMap = this.generateMap.bind(this);
    this.generateTileMap = this.generateTileMap.bind(this);
    this.generateRoomCoords = this.generateRoomCoords.bind(this);
    this.generateDoor = this.generateDoor.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
    this.initiateCombat = this.initiateCombat.bind(this);
    this.initGame = this.initGame.bind(this);
    this.populateCollisionMap = this.populateCollisionMap.bind(this);
    this.resetGame = this.resetGame.bind(this);
    this.setMessage = this.setMessage.bind(this);
    this.updateMap = this.updateMap.bind(this);
    this.updatePos = this.updatePos.bind(this);
    this.handleTouchStart = this.handleTouchStart.bind(this);
    this.handleTouchEnd = this.handleTouchEnd.bind(this);
    this.handleTouchMove = this.handleTouchMove.bind(this);
    
    // workaround to access node elements directly and apply event listeners
    this.touchDisplay = null;
    this.setTouchDisplayRef = element => {
      this.touchDisplay = element;
    };
    this.addTouchDisplayListener = () => {
      if (this.touchDisplay) {
        const node = this.touchDisplay;
        node.addEventListener("touchstart", this.handleTouchStart, false);
        node.addEventListener("touchmove", this.handleTouchMove, {
          passive: false
        });
        node.addEventListener("touchend", this.handleTouchEnd, false);
      }
    };
  }

  componentWillMount() {
    this.resetGame();
  }

  componentDidMount() {
    // workaround to access node elements directly and apply event listeners
    this.addTouchDisplayListener();
  }

  initGame() {
    const units = GLOBALS.mapSize;
    const starterWeapon = this.generateItem(GLOBALS.weapon, true, false);
    let newGame = {};
    newGame.map = this.generateMap(0, units);
    this.generateTileMap(newGame.map);
    newGame.collisionMap = this.generateMap(0, units);
    newGame.player = this.generateHumanoid(
      GLOBALS.health,
      GLOBALS.attack,
      1,
      newGame.map,
      newGame.collisionMap,
      GLOBALS.color,
      true,
      GLOBALS.expToNext,
      starterWeapon
    );
    newGame.npcArr = [];
    newGame.itemArr = [];
    return newGame;
  }

  populateCollisionMap(
    map,
    collisionMap,
    player,
    npcArr,
    itemArr,
    starterWeapon
  ) {
    for (let i = 1; i < 11; i++) {
      let npc = this.generateHumanoid(
        i * 30,
        i * 5,
        i,
        map,
        collisionMap,
        `hsl(1, 75%, ${80 - (5 * (i - 1))}%)`
      );
      let weapon = this.generateItem(
        GLOBALS.names[i - 1],
        true,
        true,
        map,
        collisionMap
      );
      let health = this.generateItem("Onigiri", false, true, map, collisionMap);
      npcArr.push(npc);
      itemArr.push(weapon, health);
      collisionMap[player.xPos][player.yPos] = 1;
      collisionMap[npc.xPos][npc.yPos] = 2;
      collisionMap[weapon.xPos][weapon.yPos] = 3;
      collisionMap[health.xPos][health.yPos] = 4;
    }
  }

  resetGame() {
    const { map, collisionMap, player, npcArr, itemArr } = this.initGame();
    this.populateCollisionMap(map, collisionMap, player, npcArr, itemArr);
    this.setState({
      map: map,
      collMap: collisionMap,
      player: player,
      npcs: npcArr,
      items: itemArr,
      alive: true
    });
    this.setMessage(
      "Nobunaga has been betrayed by General Mitsuhide. Find and defeat all enemies until he appears. Click anywhere to begin.",
      15000
    );
  }

  generateHumanoid(
    health,
    attack,
    level,
    map,
    collMap,
    color,
    player = false,
    expToNext = null,
    startWeapon = null
  ) {
    const { xPos, yPos } = GLOBALS.getPos(map, collMap);
    let humanoid = player
      ? new Humanoid(
          health,
          attack,
          level,
          xPos,
          yPos,
          player,
          color,
          expToNext,
          startWeapon
        )
      : new Humanoid(health, attack, level, xPos, yPos, player, color);
    return humanoid;
  }

  generateItem(
    name,
    weapon = true,
    discoverable = true,
    map = null,
    collMap = null
  ) {
    if (!discoverable) {
      return weapon ? new Item(name, "weapon") : new Item(name, "health");
    }
    const { xPos, yPos } = GLOBALS.getPos(map, collMap);
    let item = weapon
      ? new Item(name, "weapon", xPos, yPos)
      : new Item(name, "health", xPos, yPos);
    return item;
  }

  generateMap(value, units) {
    let map = [];
    for (let i = 0; i < units; i++) {
      map[i] = new Array(units).fill(value);
    }
    return map;
  }

  /* 
   * desc add tilemap to empty map array 
   * 1. create list of coord/length objects
   * 2. input rooms into map array
   * 3. connect rooms vertically, then horizontally
   */
  generateTileMap(map) {
    let vacancy = true;
    let rooms = [];
    let coords = {
      x: Math.floor(Math.random() * 10 + 10),
      y: Math.floor(Math.random() * 10 + 10)
    };
    while (vacancy) {
      const length = Math.floor(Math.random() * 10 + 30);
      const newCoords = this.generateRoomCoords(map, coords, length);
      vacancy = newCoords ? true : false;
      if (vacancy) {
        const room = { coords: coords, length: length };
        rooms.push(room);
        coords = newCoords.coords;
      }
    }
    // input rooms to map
    for (let rv = 1; rv < rooms.length; rv++) {
      const room = rooms[rv];
      const neighbor = rooms[rv - 1];
      if (room && neighbor) {
        const coords = neighbor.coords;
        for (let x = coords.x; x < coords.x + neighbor.length; x++) {
          for (let y = coords.y; y < coords.y + neighbor.length; y++) {
            if (x < map.length && y < map.length) {
              map[x][y] = 1;
            }
          }
        }
        // generate doors vertically as rooms are input
        this.generateDoor(map, room, neighbor);
      }
    }
    // generate doors horizontally
    for (let rh = rooms.length - 1; rh > 0; rh--) {
      const room = rooms[rh];
      if (room) {
        let door = false;
        let n = rh - 1;
        while (!door) {
          door = n >= -1 ? false : true;
          let neighbor = rooms[n];
          let diffY = neighbor
            ? Math.abs(room.coords.y - neighbor.coords.y)
            : null;
          let diffX = neighbor
            ? Math.abs(room.coords.x - (neighbor.coords.x + neighbor.length))
            : null;
          if (
            diffX &&
            diffY &&
            diffX < room.coords.x - neighbor.coords.x &&
            diffY < room.length / 2
          ) {
            this.generateDoor(map, room, neighbor);
            door = true;
          }
          n--;
        }
      }
    }
  }

  /*
   * Set room in 2d map array starting from position x,y.
   * place rooms from left to right, top to bottom.
   * return starting position for next room.
   */
  generateRoomCoords(map, { x, y }, length) {
    let outerBound = GLOBALS.mapSize - 40;
    if (x + length >= outerBound && y + length >= outerBound) {
      return false;
    }
    let next = {};
    if (y + length + 2 < outerBound) {
      next.coords = {
        x: Math.floor(Math.random() * 2 + x),
        y: y + length + 2
      };
    } else {
      next.coords = {
        x: x + length + 2,
        y: Math.floor(Math.random() * 10 + 10)
      };
    }
    return next;
  }

  /* 
   * Takes map and two room objects
   * create doorway between rooms
   * modifies map
   */
  generateDoor(map, room, neighbor) {
    if (!room || !neighbor) {
      return null;
    }
    const rX = room.coords.x;
    const rY = room.coords.y;
    const nX = neighbor.coords.x;
    const nY = neighbor.coords.y;
    let start;
    let end;
    let point;
    // Vertical Doors
    if (rY - (nY + neighbor.length) < 4 && Math.abs(rX - nX) < 8) {
      if (rY < GLOBALS.mapSize - 40) {
        start = rX > nX ? rX : nX;
        end =
          rX + room.length < nX + neighbor.length
            ? rX + room.length
            : nX + neighbor.length;
        point = Math.floor(Math.random() * ((end - 1) - (start + 1)) + (start + 1));
        for (let y = nY + neighbor.length; y <= rY; y++) {
          if (point < map.length && y < map.length && !map[point][y]) {
            map[point][y] = 1;
          }
        }
      }
      // Horizontal Doors
    } else {
      if (rX < GLOBALS.mapSize) {
        start = rY > nY ? rY : nY;
        end =
          rY + room.length < nY + neighbor.length
            ? rY + room.length
            : nY + neighbor.length;
        point = Math.floor(Math.random() * (end - 1 - (start + 1)) + (start + 1));
        for (let x = nX + neighbor.length; x <= rX; x++) {
          if (point < map.length && x < map.length && !map[x][point]) {
            map[x][point] = 1;
          }
        } 
      }
    }
  }

  /*
   * Methods for combat and movement
   */

  /* 
   * @desc loop through item and npc arrays
   * to check for collisions
   * @return true if there is no collision, or
   * if collision is with item. Otherwise false
   */
  checkCollision(collMap, player, itemsArr, npcsArr, xPos, yPos) {
    if (this.state.active) {
      let boss = this.state.boss;
      if (boss.xPos == xPos && boss.yPos == yPos) {
        this.initiateCombat(player, boss);
        let result = "collision:engage";
        if (boss.health < 1) {
          collMap[boss.xPos][boss.yPos] = 0;
          boss = null;
          result = "game complete";
        }
        if (player.health + player.weapon.health < 1) {
          result = "collision:ko";
        }
        this.setState({
          boss: boss,
        });
        return result;
      }
    }

    for (let item of itemsArr) {
      if (item.xPos == xPos && item.yPos == yPos) {
        let text = `You found `;
        text += item.name;
        if (item.type == "weapon") {
          text+= ` (+${item.health} Health, +${item.attack ? item.attack : 0} Attack)`;
        } else {
          text += ` (+${item.health} Health)`;
        }
        this.setMessage(text, 10000);
        item.type == "weapon"
          ? (player.weapon = item)
          : (player.health += item.health);
        collMap[item.xPos][item.yPos] = 0;
        let idx = itemsArr.indexOf(item);
        itemsArr.splice(idx, 1);
        this.setState({
          items: itemsArr
        });
        return "collision:item";
      }
    }

    for (let npc of npcsArr) {
      if (npc.xPos == xPos && npc.yPos == yPos) {
        this.initiateCombat(player, npc);
        let result = "collision:engage";
        if (npc.health < 1) {
          this.gainExp(player, npc.level * 50);
          collMap[npc.xPos][npc.yPos] = 0;
          let idx = npcsArr.indexOf(npc);
          npcsArr.splice(idx, 1);
          result = "collision:victory";
        }
        if (player.health + player.weapon.health < 1) {
          result = "collision:ko";
        }
        this.setState({
          npcs: npcsArr
        });
        
        if (!this.state.active && npcsArr.length < 1) {
          setTimeout(() => {
            this.setMessage(
              "Mitsuhide has appeared somewhere in the castle. Find and defeat him!",
              15000
            );
          }, 3000);
          const boss = this.generateHumanoid(
              500,
              75,
              99,
              this.state.map,
              this.state.collMap,
              `#000000`
            );
            collMap[boss.xPos][boss.yPos] = 2;
            this.setState({
              active: true,
              boss: boss
            });
          }
        return result;
      }
    }
    return "no collision";
  }

  initiateCombat(player, npc) {
    this.setState(
      {combat: true},
      () => {
        setTimeout(() => {
          this.setState({
            combat: false
          });
        }, 100);
      }
    );
    
    npc.health -= player.attack + player.weapon.attack;

    const hp = (player.health + player.weapon.health) - npc.attack;
    if (hp > 1) {
      player.health -= npc.attack;
    } else {
      player.health = 0;
      player.weapon.health = 0;
    }
  }

  /* @ desc add exp and check if player levels up */
  gainExp(player, exp) {
    player.exp += exp;
    if (player.expToNext <= 0) {
      player.level += 1;
      player.attack += 10;
      player.health += 100;
      player.exp = 0;
      player.expToNext = 100 * player.level;
    }
  }

  updateMap(map, collMap, prevX, prevY, nextX, nextY) {
    if (!map[nextX][nextY] || !this.state.alive) {
      return false;
    }
    collMap[prevX][prevY] = 0;
    collMap[nextX][nextY] = 1;
    this.setState({
      collMap: collMap
    });
    return true;
  }

  /* @desc change x and y coords of player */
  updatePos(player, xPos, yPos, collMap) {
    let items = this.state.items;
    let npcs = this.state.npcs;
    let map = this.state.map;
    if (this.state.alive) {
      switch (this.checkCollision(collMap, player, items, npcs, xPos, yPos)) {
        case "collision:ko":
          this.setState({
            alive: false,
            player: player
          });
          this.setMessage(`You've been defeated.`, 3000, this.resetGame);
          break;
        case "collision:engage":
          //TODO: Add red flash
          break;
        case "collision:victory":
          const remaining = this.state.npcs.length;
          this.setMessage(
            `You've defeated one of Hideyoshi's samurai. ${
              remaining
            } remaining!`,
            5000
          );
          this.setState({
            collMap: collMap,
          });
          break;
        case "game complete":
          this.setState({
            alive: false,
            collMap: collMap,
          });
          this.setMessage(
            `Congratulations! You've defeated Akechi Mitsuhide and avenged Nobunaga!`,
            15000,
            this.resetGame
          );
        default:
          if (
            this.updateMap(map, collMap, player.xPos, player.yPos, xPos, yPos)
          ) {
            player.xPos = xPos;
            player.yPos = yPos;
            this.setState({
              player: player
            });
          }
        }
      }
  }

  setMessage(message, time, callback = null) {
    this.setState({
      message: message
    });
    setTimeout(() => {
      this.setState({
        message: ""
      });
      if (callback) {
        callback();
      }
    }, time);
  }

  handleClick() {
    let darkness = this.state.darkness;
    this.setState({
      darkness: !darkness
    });
  }

  handleKeyDown(event) {
    let player = this.state.player;
    let { xPos, yPos } = player;
    switch (event.key) {
      case "ArrowDown":
        player.yPos == this.state.map.length
          ? (yPos = player.yPos)
          : (yPos = player.yPos + 1);
        break;
      case "ArrowUp":
        player.yPos == 0 ? (yPos = 0) : (yPos = player.yPos - 1);
        break;
      case "ArrowLeft":
        player.xPos == 0 ? (xPos = 0) : (xPos = player.xPos - 1);
        break;
      case "ArrowRight":
        player.xPos == this.state.map.length - 1
          ? (xPos = player.xPos)
          : (xPos = player.xPos + 1);
        break;
      default:
        return null;
    }
    let collMap = this.state.collMap;
    this.updatePos(player, xPos, yPos, collMap);
  }

  handleTouchStart(event) {
    const touch = this.state.touch;
    touch.startX = event.touches[0].screenX;
    touch.startY = event.touches[0].screenY;
    touch.time = new Date().getTime();
    this.setState({
      touch: touch
    });
  }

  handleTouchMove(event) {
    event.preventDefault();
  }

  handleTouchEnd(event) {
    const touch = this.state.touch;
    const distX = event.changedTouches[0].screenX - touch.startX;
    const distY = event.changedTouches[0].screenY - touch.startY;
    const time = new Date().getTime() - touch.time;
    let player = this.state.player;
    let { xPos, yPos } = player;
    if (time > 50) {
      if (Math.abs(distX) > Math.abs(distY)) {
        if (distX < 0) {
          player.xPos == 0 ? (xPos = 0) : (xPos = player.xPos - 1);
        } else {
          player.xPos == this.state.map.length - 1
            ? (xPos = player.xPos)
            : (xPos = player.xPos + 1);
        }
      }
      if (Math.abs(distX) < Math.abs(distY)) {
        if (distY < 0) {
          player.yPos == 0 ? (yPos = 0) : (yPos = player.yPos - 1);
        } else {
          player.yPos == this.state.map.length
            ? (yPos = player.yPos)
            : (yPos = player.yPos + 1);
        }
      }
    }
    let collMap = this.state.collMap;
    this.updatePos(player, xPos, yPos, collMap);
    this.setState({
      touch: {}
    });
  }

  /* Render the app and its various components */
  render() {
    return (
      <div
        className="game"
        onKeyDown={this.handleKeyDown}
        ref={this.setTouchDisplayRef}
        autoFocus
        tabIndex="0"
      >
        <Dashboard 
          player={this.state.player} 
          npcsArr={this.state.npcs} 
          active={this.state.active}
          />
        
        <Display
          map={this.state.map}
          collMap={this.state.collMap}
          player={this.state.player}
          npcs={this.state.npcs}
          darkness={this.state.darkness}
          combat={this.state.combat}
          boss={this.state.boss}
        />
        
        <Panel
          onClick={this.handleClick}
          darkness={this.state.darkness}
          message={this.state.message}
        />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector("#root"));

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.
Loading ..................

Console