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.

            
              <body><div id="app"></div></body>
            
          
!
            
              

/*Sizing*/
$game-width: 800px;
$game-height: 600px;
$title-height: 100px;
$characterSheet-height: 50px;
$narrator-font: 16px;
$legend-font: 12px;
$rooms-per-mapside: 3;
$tiles-per-roomside: 7;
$num-char-attr: 7; /*inventory counts as 2*/
/*Calculated Sizing*/
$map-height: $game-height - $title-height - $characterSheet-height;
$map-width: $map-height;
$narrator-height: $map-height;
$narrator-width: $game-width - $map-width;
$tile-width: $map-width / ($tiles-per-roomside * $rooms-per-mapside);
$tile-height: $map-height / ($tiles-per-roomside * $rooms-per-mapside);
$char-attr-width: ($game-width - 1)/$num-char-attr;


/*Coloring*/
$default-color: darkgrey;
$default-transparent: rgba($default-color, 0.85); 
$default-dark: darken($default-color, 20%);
$item-color: yellow;
$instruction-color: black;
$player-color: blue;
$enemy-color: lighten(red, 10%);
$boss-color: darken(red, 25%);
$harm-color: red;
$help-color: green;
$food-color: green;
$weapon-color: lighten(black, 10%);
$key-color: darken($item-color, 10%);
$door-color: #966F33;
$basic-color: black;
$fog-color: black;
$ground-color: $default-color;
$wall-color: darken(grey, 30%);
$empty-color: $default-color;

/*Mixins*/
@mixin entity {
  outline: 2px solid lighten($ground-color, 20%);
}



  
  
/*Classes*/

/*Container Settings*/
#app {  
  display: block;
  position: fixed;
  left: 50%;
  margin-left: -$game-width/2;
}

body {
  background:
    linear-gradient(135deg, $default-color 22px, #d9ecff 22px, #d9ecff 24px, transparent 24px, transparent 67px, #d9ecff 67px, #d9ecff 69px, transparent 69px),
    linear-gradient(225deg, $default-color 22px, #d9ecff 22px, #d9ecff 24px, transparent 24px, transparent 67px, #d9ecff 67px, #d9ecff 69px, transparent 69px)0 64px;
  background-color:$default-color;
  background-size: 64px 128px
}
  
.game {
  display: block;
  margin: 0 auto;
  width: $game-width;
  height: $game-height;
}


/*Map and Tile styling*/
.map {
  display: inline-block;
  width: $map-width;
  height: $map-height;
}

.tile {
  display: inline-block;
  vertical-align: bottom;
  width: $tile-width;
  height: $tile-height;
}

.tile-fog {
  background-color: $fog-color;
}

.tile-wall {
  background-color: $wall-color;
}

.tile-ground {
  background-color: $default-transparent;
}

.tile-friendly {
  background-color: $player-color;
  @include entity;
}

.tile-boss {
  background-color: $boss-color;
  @include entity;
}

.tile-enemy {
  background-color: $enemy-color;
  @include entity;
}

.tile-door {
  background-color: $door-color;
}

.tile-food {
  background-color: $food-color;
  @include entity;
}

.tile-weapon {
  background-color: $weapon-color;
  @include entity;
}

.tile-key {
  background-color: $key-color;
  @include entity;
}


/*Narrator and Message styling*/
.narrator {
  display: inline-block;
  height: $narrator-height;
  width: $narrator-width;
  overflow: auto;
  background: $default-transparent;
  vertical-align: bottom;
  line-height: 1.2;
}

.message {
  display: block;
  padding: 1px;
}

.text {
  font-size: $narrator-font;
}

.text-action {
  font-weight: bold;
  color: $instruction-color;
}

.text-player {
  font-weight: bold;
  color: $player-color;
}

.text-enemy {
  font-weight: bold;
  color: $enemy-color;
}

.text-item {
  font-weight: bold;
  background-color: $default-dark;
  padding: 0 5px;
  color: $item-color;
}

.text-harm {
  color: $harm-color;
}

.text-help {
  color: $help-color;
}

.text-instruction {
  font-weight: bold;
  color: $instruction-color;
}

.text-basic {
  color: $basic-color;
}

.text-empty {
  color: $empty-color;
}


/*title and containers*/
.header {
  width: 100%;
}

.title {
  display: block;
  margin: 10px auto;
  background-color: inherit;
  text-align: center;
  font-size: 40px;
  font-weight: bold;
  text-decoration: underline;
}

/*legend styling*/
.legend {
  width: 100%;
  background-color: $default-dark;
}

.legend-item {
  display: inline-block;
  padding: 5px 5px;
  background-color: inherit;
}

.legend-tile {
  display: inline-block;
  height: .70 * $tile-height;
  width: .70 * $tile-width;
  margin-right: 5px;
}

.legend-text {
  display: inline-block;
  color: $basic-color;
  padding: 1px;
  background-color: inherit;
}

/*Character Sheet styling*/
.character-sheet {
  width: 100%;
  margin: 10px 0;
  background-color: $default-dark;
}

.character-attr {
  display: inline-block;
  padding-left: 5px;
  padding-right: 5px;
  background-color: inherit;
}

.character-attr.attr-inv {
  background-color: inherit;
}

.attr-name {
  font-size: 12px;
  display: inline-block;
  vertical-align: baseline;
  margin-right: 2px;
  background-color: inherit;
}

.attr-value {
  font-size: 20px;
  font-weight: bold;
  display: inline-block;
  background-color: inherit;
}

.attr-value.attr-inv {
  font-size: $narrator-font;
  font-weight: bold;
  color: $item-color;
  background-color: inherit;
  outline: 1px solid black;
}

            
          
!
            
              /*****************************************************************
TABLE OF CONTENTS        

  Model Classes
    Entity
      Character
        Player
      Item
        Food/Key/Weapon

    Tile
      WallTile/GroundTile

    Message

  View Classes
    CharacterSheet, Map, Tile, Narrator, Message

  Collection Classes
    Map, Narrator, Game

  Startup Code

END CONTENTS
*****************************************************************/


/*****************************************************************
MODEL CLASSES                                                              
*****************************************************************/

//Model class is used to give .getProperties method to all prototypes
//Also has method to bind methods and set non-enumberable properties (useful for methods that don't matter in rendering)
class Model {
  constuctor (options) {
    
  }
  
  getProperties () {
    let props = {};
    Object.assign(props, this);
    for (let p in props) {
      if (typeof props[p] === "object") {
        if (props[p].hasOwnProperty("getProperties")) {
          props[p] = props[p].getProperties();
        } else if (Array.isArray(props[p])) {
          props[p] = props[p].slice(0);
        } else {
          props[p] = Object.assign({}, props[p]);
        }
      }  
    }
    return props;
  }
}


  /***************************************************************
  ENTITY 

  An entity is anything that exists on the map
  other than the map tiles, ie. players or items
  An entity:
    Has a name to be seen in the messages
    Has a location either:
      On the map
      In another Entities Inventory
  ***************************************************************/
  class Entity extends Model {
    constructor (options) {
      super(options);
      this.entityName = options.entityName;
      this.locationType = options.locationType || null; //tile or INV
      this.location = options.location || null;
    }
    
    move (location) {
      this.location = location;
    }
  }

    /*************************************************************
    CHARACTER

    A Character is an entity that:
      Has an inventory, LVL, HP, and ATK value
      Will drop items upon dying
      Will damage to other characters it touches
    *************************************************************/
    class Character extends Entity {
      constructor (options) {
        super(options);
        this.isAlive = true;
        this.isPlayer = options.isPlayer || false;
        
        this.INV = options.characterINV || [];
        this.LVL = options.characterLVL || 1;
        this.HP = options.characterHP || this.calcCharacterHP();

        //dig through inv for best weapon
        this.weapon = this.INV.reduce((best, next) => {
          if (next.itemType === "weapon" && next.itemLVL >= best.itemLVL) {
            return next;
          }
          return best;
        }, new Weapon({}));//by default makes a fist;
        
        //set this.characterDamage and subsequently this.ATK
        this.calcCharacterDMG();
        this.calcATK();
      }
      
      calcCharacterDMG () {
        this.characterDMG = this.LVL;
      }
      
      calcATK () {
        //sum player + weapon
        //enemyHP grows at exponential-ish rate, but playerATK grows at exponential-ish rate
        this.ATK = this.LVL * (this.weapon.weaponDMG + this.characterDMG);
      }
      
      calcCharacterHP () {
        //enemyHP grows at exponential-ish rate, but playerATK grows at exponential-ish rate
        return (this.isPlayer ? 100 : 1 + (this.LVL/4*this.LVL) * 10);  
      }
      
      //calculate how much damage character will swing for (some randomness)
      attackValue (enemyCharacter) {
        let ATK = this.ATK
        //roll between 1-8
        const roll = Math.ceil(Math.random() * 8);
        
        //averageHit = 4 + levelDiff
        //roll above min hit for bonus damage
        const averageHit = 4 + (this.LVL - enemyCharacter.LVL);
        
        ATK += ((roll - averageHit)/2); //was getting to varied of results, so decresing the range
        //critical hit (roll 8)
        //deal 50% more damage
        if (roll === 8) {ATK *= 1.5;}
        
        return ATK;
      }
      
      //decrement HP by value, if below 0 trigger death action
      loseHP (value, enemy) {
        this.HP -= value;
        if (this.HP <= 0) {
          this.die(enemy);
          if (this.isPlayer) {
            game.event("end", {win: false});
          }
        };
      }

      //consume foodItem and increase HP
      eat (foodItem) {
        this.HP += foodItem.foodHP;
        //trigger heal message
        game.event("message", {
          action: "heal",
          subject: this,
          additional: [foodItem.foodHP],
        });
      }

      //compare weaponItem to currently equiped weapon
      //equip the stronger one, destroy the weaker one
      equip (weaponItem) {
          this.weapon = weaponItem
          this.calcATK();
      }

      //food is consumed before touching INV
      //weapons go in slot 0
      //keys fo into slot 1
      //if item already in that slot, keep the most powerful
      addToINV (item) {
        if (item.itemType === "weapon") {
          let weapon = this.INV[0] || {itemLVL:0};
          if (item.itemLVL > weapon.itemLVL) {
            this.equip(item);
            this.INV[0] = item;              
          }  
        } else if (item.itemType === "key") {
          let key = this.INV[1] || {itemLVL:0};
          if (item.itemLVL > key.itemLVL) {
            this.INV[1] = item;              
          }       
        } else  {
          game.errorHandler(item);
        }
      }
      
      die (killedBy) {
        this.isAlive = false;
                
        //trigger death
        // remove self from map
        
        game.event("message", {
          action: "death",
          subject: this,
          object: killedBy
        });
        this.location.clear();
        
        //trigger drop event
        //add items to map
        if (this.INV[0] !== undefined) {
          game.event("message", {
            action: "drop",
            subject: this,
            object: this.INV[0]
          });  
          this.location.addEntity(this.INV[0])
        }        
      }
    }

      /***********************************************************
      BOSS

      A boss is an extra hard player:
        All the same methods of a charachter, but with boosted stats.
        Is rendered differently (main reason for the seperate class).
      
      ***********************************************************/
      class Boss extends Character {
        constructor (options) {
          options.characterLVL += 2;
          super(options);
        }
      }

      /***********************************************************
      PLAYER

      The Player is the Character controlled by the human player:
        Can move to other tiles from the one it was generated on
        Has XP prop that increases after killing other players
        Can go up in level after enough XP
        Interacts with doors, unlocks/goes to next level
        Can pickup items from the ground
          Manages INV to hold only most powerful weapon/key
          Consumes picked up food to increase HP
        Upon dying triggers game over event
      
      ***********************************************************/
      class Player extends Character {
        constructor (options) {
          options.isPlayer = true;
          super(options);
          this.XP = options.playerXP || 0;
          this.nextLevelXP = this.calcNextLevelXP();
        }
        
        //calculate the total amount of XP needed for the next level
        calcNextLevelXP () {
          const levelXPs = {
            1:0, 2:80, 3:110, 4:150,
          }
          
          return levelXPs[this.LVL +1] || Number.POSITIVE_INFINITY;
        }
        
        //increase XP, increase level if applicaple
        gainXP (value) {
          this.XP += value;
          if (this.XP >= this.nextLevelXP) {
            this.LVL++;
            this.XP -= this.nextLevelXP;
            this.calcATK();
            this.nextLevelXP = this.calcNextLevelXP();
          }
        }
        
        //swing weapon at enemy (and enemy swings back)
        swing (enemy) {
          let agressorATKValue = this.attackValue(enemy);
          let defenderATKValue = enemy.attackValue(this)          
          
          //agressor swings first
          //triger attack event
          game.event("message", {
            action: "attack",
            subject: this,
            object: enemy,
            additional: [agressorATKValue, this.weapon],
          });
          //make attack
          enemy.loseHP(agressorATKValue, this);
          
          if (enemy.isAlive) {
            //triger attack event
            game.event("message", {
              action: "attack",
              subject: enemy,
              object: this,
              additional: [defenderATKValue],
            });
            //make attack
            this.loseHP(defenderATKValue, enemy);
          } else {
            //.kill method just gives XP to player
            //.die method is run from .loseHP and death event is triggered from .die
            this.kill(enemy);
          }
        }

        //gainXP as needed based on enemy level
        kill (enemy) {
          const xp = Math.max(10 + 10*(enemy.LVL-this.LVL),0);
          this.gainXP(xp);
        }
        
        //route item to appropriate item handler
        pickup (item) {
          //trigger event for pick
          game.event("message", {
            action: "pickup",
            subject: this,
            object: item,
          });
          
          //router
          let type = item.itemType;
          if (type === "food") {
            this.eat(item)
          } else if (type === "weapon") {
            this.addToINV(item)
          } else if (type === "key") {
            this.addToINV(item)
          } else {
            game.errorHandler(item); 
          }
        }
        
        unlock (door) {
          return door.unlock(this.INV[1]);
        }
        
      }


    /*************************************************************
    ITEM

    An Item is an entity that can:
      be pickup and carried by players
    *************************************************************/
    class Item extends Entity {
      constructor (options) {
        super(options);
        this.itemLVL = options.itemLVL || 1;
      }
    }

      /***********************************************************
      FOOD
      
      Food are items that, when pickup up by player-characters,
      are consumed to increase HP
      As food increases in item level, it can increase in potency
      ***********************************************************/
      class Food extends Item {
        constructor (options) {
          super(options)
          this.itemType = "food";
          this.foodHP = options.foodHP || this.calcFoodHP(this.itemLVL);
          this.entityName = this.entityName || this.makeFoodName(this.itemLVL);
        }
        
        calcFoodHP (lvl) {
          return 25 + Math.ceil(10 * Math.random() * lvl);
        }
        
        makeFoodName (lvl) {
          return ({
            1: "Apple",
            2: "Bread",
            3: "Pork Chop",
            4: "Nutritious Clif Bar",
            5: "Lamb Curry", //Lamb curry is best curry. Always.
          })[lvl];
        }
      }

      /***********************************************************
      KEY
      
      Keys are items that, while in a  player-character's INV,
      can be consumed to unlock a door
      ***********************************************************/
      class Key extends Item {
        constructor (options) {
          super(options)
          this.itemType = "key";
          this.entityName = this.entityName || this.makeKeyName(this.itemLVL);
        }
        
        makeKeyName (lvl) {
          return ({
            1: "Copper Key",
            2: "Silver Key",
            3: "Golden Key",
            4: "Diamond Key",
          })[lvl];
        }
      }

      /***********************************************************
      WEAPON
      
      Weapons are items that, while in a player INV,
      increase that player's ATK value
      ***********************************************************/
      class Weapon extends Item {
        constructor (options) {
          super(options);
          this.itemType = "weapon";
          
          const weaponInfo = this.calcWeaponInfo();
          this.entityName = weaponInfo.name;
          this.weaponDMG = weaponInfo.dmg;
        }
        
        calcWeaponInfo() {
          const weapons = {
            1: {name: "Fist", dmg: 0},
            2: {name: "Dagger", dmg: 1},
            3: {name: "Short Sword", dmg: 3},
            4: {name: "Scimitar", dmg: 4},
            5: {name: "LightSaber", dmg: 7},
          };
          
          return (weapons[this.itemLVL] || game.errorHandler(this.itemLVL));
        }
        
      }

    /*************************************************************
    DOOR

    A doorway is an entity that can:
      be unlocked by a player holding the right key
      generate next level
    *************************************************************/  
    class Door extends Entity {
      constructor (options) {
        super(options);
        
        this.locked = true;
        this.keyNeeded = options.keyNeeded;
        this.roomNumber = options.roomNumber;
      }
      
      unlock (key) {
        if (key && key.itemLVL >= this.keyNeeded) {
          this.locked = false;
          
          game.event("nextLevel");
          return true;
          
          
        } else {
          
          game.event("message", {
            action: "instruction",
            subject: "A stronger key is needed"
          });
          
          return false;
        }
      }
    }


  /***************************************************************
  TILE

  Tiles are the hundreds of squares that makeup the map.
  Every location on the map has exactly 1 tile and vice-versa.
  
  A Tile:
    Knows its own location on the map
    Knows if it is "visible" or not
  ***************************************************************/
  class Tile extends Model {
    constructor (options) {
      super(options);
      this.visible = false; //set to false for production
      this.contains = null;
      this.address = Map.prototype.coordToStr(options.r, options.c);
    }
    
    discover () {
      this.visible = true;
    }
    
    unDiscover () {
      this.visible = false; //set to false for production
    }
    
    //beacuse the tiles are created in rooms and the rooms are only combined after
    //setAddress has to be called after construction
    setAddress (r,c) {
      this.address = Map.prototype.coordToStr(r, c);
    }
    
  }

    /*************************************************************
    WALL TILE

    A wall tile is the most basic title
    represents the impassible walls on the map
    *************************************************************/
    class WallTile extends Tile {
      constructor (options) {
        super(options);
        this.isWall = true;
      }
      
      // a simplified version of method by same name in ground
      // always returns false
      addEntity () {
        return false;
      }
    }

    /*************************************************************
    GROUND TILE

    A ground tile is either empty or contains an entitity
    Can convey different states for each entity for rendering purposes
    Can report what it contains if player-character tries to enter
    *************************************************************/
    class GroundTile extends Tile {
      constructor (options) {
        super(options);
        this.isWall = false;
        if (options.starterEntity) {
          this.addEntity(options.starterEntity);   
        }

      }
      
      // add entity to tile and return true OR
      // run conflict resolution code and return false
      addEntity (entity) {
        
        // add characters
        if (entity instanceof Character) {
          //contains nothing
          if (this.contains === null) {
            //add entity to tile and return true
            this.contains = entity;
            entity.move(this);
            return true;

          //contains characters
          } else if (this.contains instanceof Character) {
            //do no add entity to tile
            //run combat and return false
            entity.swing(this.contains);
            return false;
            
          //contains item
          } else if (this.contains instanceof Item) {
            //pick up item
            //add entity to tile and return true
            entity.pickup(this.contains);
            //move the character into this tile
            this.contains = entity;
            entity.move(this);
            return true;
            
          //contains door
          } else if (this.contains instanceof Door) {
            
            //try to unlock door with player
            return entity.unlock(this.contains);
            
          }
        
        // add items
        } else if (entity instanceof Item) {
          // contains characters
          if (this.contains instanceof Character) {
            //character on tile pick up added item
            this.contains.pickup.bind(this.contains)(entity);
            entity.move(this.contains);
            return true;
            
          // contains nothing
          } else if (this.contains === null) {
            //this tile now contains something
            this.contains = entity;
            entity.move(this);
            return true;
            
          // contains anything else
          } else {
            return false;
          }
        
        // add doors
        } else if (entity instanceof Door) {
          //contains nothing
          if (this.contains === null) {
            //this tile now contains something
            this.contains = entity;
            entity.move(this);
            return true;           
            
          //contains anything else
          } else {
            return false;
          }
        
        //empty invocation
        } else if (entity === undefined) {
          return false;
        }

        // error handling
        game.errorHandler({entity: entity, contains: this.contains});
          
      }
      
      //remove all entities from tile
      clear () {
        this.contains = null;
        return true;
      }
      
      //remove a specific entity if tile contains that entity
      removeEntity (entity) {
        if (this.contains === entity) {
          this.clear();
          return true;
        } else {
          return false;
        }
      }
      
    }


  /***************************************************************
  MESSAGE

  Message classes are used to display text in the narrator:
  Have unique ID for ordering
  Made up of "fragments" that contain a "text" and a "style"
  
  Text styles:
    basic, player, enemy, item, harm, help, action, instruction, empty
  
  
  Message types (AKA Actions):
    Attack:
      Subject: Agressor, Object: Defender, Additional: [,damage dealt], [,weapon]
    Death:
      Subject: Dead Character   
    Drop:
      Subject: Dropping Character, Object: Item Dropped
    Heal:
      Suject: Character Healed, Additional: [,HP healed]
    Instruction:
      Subject: Message of Instruction
    Pickup:
      Subject: Chracter Acquiring, Object: Item Picked-Up
  ***************************************************************/
  class Message extends Model {
    constructor (options) {
      super(options);
      this.fragments = this.makeFragments(options);
    }
    
    makeFragments (options) {
      return {
        "attack" : this.makeAttack,
        "death" : this.makeDeath,
        "drop" : this.makeDrop,
        "heal" : this.makeHeal,
        "instruction" : this.makeInstruction,
        "open" : this.makeOpen,
        "pickup" : this.makePickup,
      }[options.action](options);
    }
    
    makeAttack (options) {
      let damage = options.additional[0] !== undefined ? options.additional[0] : -1;
      let weapon = options.additional[1];
      return [{
        text: options.subject.entityName,
        style: options.subject instanceof Player ? "player" : "enemy"
      },{
        text: " attacks ",
        style: "action"
      },{
        text: options.object.entityName,
        style: options.object instanceof Player ? "player" : "enemy"
      },{
        text: damage >= 0 ? " for " : "",
        style: damage >= 0 ? "basic" : "empty"
      },{
        text: damage >= 0 ? damage + " damage" : "",
        style: damage >= 0 ? "harm" : "empty" 
      },{
        text: weapon ? " with " : "",
        style: weapon ? "basic" : "empty"
      },{
        text: weapon ? weapon.entityName : "",
        style: weapon ? "item" : "empty" 
      }];     
    }
    
    makeDeath (options) {
      return [{
        text: options.subject.entityName,
        style: options.subject instanceof Player ? "player" : "enemy"
      },{
        text: " was slain by ",
        style: "harm"
      }, {
        text: options.object.entityName,
        style: options.object instanceof Player ? "player" : "enemy"
      }];
    }
    
    makeDrop (options) {
      return [{
        text: options.subject.entityName,
        style: options.subject instanceof Player ? "player" : "enemy"
      },{
        text: " drops ",
        style: "action"
      }, {
        text: options.object.entityName,
        style: "item"
      }];
    }
    
    makeHeal (options) {
      let hp = options.additional[0] !== undefined ? options.additional[0] : -1;
      return [{
        text: options.subject.entityName,
        style: options.subject instanceof Player ? "player" : "enemy"
      },{
        text: " heals ",
        style: "action"
      },{
        text: hp >= 0 ? " for " : "",
        style: hp >= 0 ? "basic" : "empty"
      },{
        text: hp >= 0 ? hp + " HP" : "",
        style: hp >= 0 ? "help" : "empty" 
      }];
    }
    
    makeInstruction (options) {
      return [{
        text: options.subject,
        style: "instruction"
      }];
    }
    
    makePickup (options) {
      return [{
        text: options.subject.entityName,
        style: options.subject instanceof Player ? "player" : "enemy"
      },{
        text: " picks up ",
        style: "action"
      }, {
        text: options.object.entityName,
        style: "item"
      }];
    }
  }


/*****************************************************************
  VIEW CLASSES  
*****************************************************************/

  /***************************************************************
  LEGEND
  
  The Legend (which will include the game title above it) 
  will span the width of the game, and be the top most component
  
  needs to display:
    Game Title
    ...and colors for:
      enemy
      boss
      key
      food
      weapon
      stairwell
    
  ***************************************************************/
  class LegendView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    render () {
      return (<div className="header">
        <h1 className="title">Tower Triumph</h1>
        <div className="legend"><ul>
          <li className="legend-item">
            <span className="legend-tile tile-friendly"></span>
            <span className="legend-text">Player</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-enemy"></span>
            <span className="legend-text">Enemy</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-boss"></span>
            <span className="legend-text">Enemy-Boss</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-food"></span>
            <span className="legend-text">Food</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-weapon"></span>
            <span className="legend-text">Weapon</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-key"></span>
            <span className="legend-text">Key</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-wall"></span>
            <span className="legend-text">Wall</span>
          </li>
          <li className="legend-item">
            <span className="legend-tile tile-door"></span>
            <span className="legend-text">Stairwell</span>
          </li>
        </ul></div>
      </div>);
    }

  }

  /***************************************************************
  CHARACTER SHEET
  
  The Character sheet will also span the width of the game,
  with all player attributes in one narrow row
  
  needs to display:
    Player HP
    Player LVL
    PLAYER XP (and how much to next level)
    Player INV
  ***************************************************************/
  class CharacterSheetView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    render () {
      return (<div className="character-sheet">
        <div className="character-attr">
          <span className="attr-name">FLOOR:</span>
          <span className="attr-value">{this.props.floor}</span>
        </div>
        <div className="character-attr">
          <span className="attr-name">HEALTH:</span>
          <span className="attr-value">{this.props.player.HP}</span>
        </div>
        <div className="character-attr">
          <span className="attr-name">ATTACK:</span>
          <span className="attr-value">{this.props.player.ATK}</span>
        </div>
        <div className="character-attr">
          <span className="attr-name">LEVEL:</span>
          <span className="attr-value">{this.props.player.LVL}</span>
        </div>
        <div className="character-attr">
          <span className="attr-name">EXPERIENCE:</span>
          <span className="attr-value">{this.props.player.XP}/{this.props.player.nextLevelXP}</span>
        </div>
        <div className="character-attr attr-inv">
          <span className="attr-name">INVENTORY:</span>
          <span className="attr-value attr-inv">
            {this.props.player.INV.map((item, i) => {
              return (<span key={i} className="text text-item">{item.entityName}</span>);
            })}
          </span>
        </div>
      </div>);
    }
  }

  /***************************************************************
  TILE  
  Rendering:
    -Make a div with a tile class
    -Add classes for fog of war, ground, walls, or any entities contained
  ***************************************************************/
  class TileView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    makeClassName () {
      let className = "tile tile-"
      if (!this.props.visible) {
        return className + "fog";
      } else if (this.props.isWall) {
        return className + "wall";
      } else if (this.props.contains === null) {
        return className + "ground";
      } else if (this.props.contains instanceof Player) {
        return className + "friendly"
      } else if (this.props.contains instanceof Boss) {
        return className + "boss";
      } else if (this.props.contains instanceof Character) {
        return className + "enemy";
      } else if (this.props.contains instanceof Door) {
        return className + "door";
      } else if (this.props.contains instanceof Food) {
        return className + "food";
      } else if (this.props.contains instanceof Weapon) {
        return className + "weapon";
      } else if (this.props.contains instanceof Key) {
        return className + "key";
      } else {
        game.errorHandler(this.contains);
      }
    }
    
    render () {
      return (<div className={this.makeClassName()}></div>);
    }
  }

  /***************************************************************
  MAP
  
  The map will take up most of the height of the game
  The left aligned, taking up 60%-80% of the width of the game
  
  Rendering:
    -Make a div with map class
    -fill that div with Tile Views
  ***************************************************************/
  class MapView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    render () {
      return (<div className="map">
        {this.props.tiles.map((tile, i) => {
          return (<TileView
            key={i}
            visible={tile.visible}
            isWall={tile.isWall}
            contains={tile.contains}
          />); 
        })}
      </div>);
    }
  }

  /***************************************************************
  MESSAGE
  Rendering:
    -Make a span with a message class
    -Fill that span with spans for each text fragment
    -Give each span a text class + a specific class for its type of fragment
  ***************************************************************/
  class MessageView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    makeClassName (frag) {
      return "text " + "text-"+frag.style;
    }
    
    render () {
      return (<span className="message">
        {this.props.fragments.map((frag, i) => {
          return (<span key={i} className={this.makeClassName(frag)}>{frag.text}</span>);
        })}
        
      </span>);
    }
  }

  /***************************************************************
  NARRATOR 
  
  The Narrator will will line up vertically with the Map
  The right aligned, taking up the remaining space after the map
  
  Rendering:
    -Make a longform text input (to enable scrolling)
    -Fill that div with Message Views
  ***************************************************************/
  class NarratorView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    render () {
      return (<div className="narrator" ref={ (self)=>{this.self=self} }>
        {this.props.messages.map((message, i) => {
          return (<MessageView key={i} fragments={message.fragments} />); 
        })}
      </div>);
    }
    
    //scroll to bottom after adding messages;
    componentDidUpdate () {
      this.self.scrollTop = this.self.scrollHeight;
    }
  }

  /***************************************************************
  GAME 
  Rendering:
    -Make a div class game
    -containing all the other views
  ***************************************************************/  
  class GameView extends React.Component {
    constructor (props) {
      super(props);
    }
    
    render () {
      return (<div className="game">
        <LegendView />
        <CharacterSheetView player={this.props.player} floor={this.props.currentFloor} />
        <MapView tiles={this.props.map.tiles} />
        <NarratorView messages={this.props.narrator.messages} />
      </div>);
    }
  }


/*****************************************************************
COLLECTION CLASSES                                                              
*****************************************************************/

  /***************************************************************
  MAP
  
  contains all tiles
  relays info to tiles about when to be visible
  relays info to tiles about character actions
  
  each level instantiates a new map with 9 rooms (3x3 grid)
    1 rooms contains character
    3 rooms contain guards with food
    1 rooms contain guards with weapon
    1 rooms contain guards (boss) with key
    1 rooms contain door
    1 rooms empty (middle room + 1 other)
    
    All side rooms connect to middle room
    Corner rooms connect to exactly 1 of its sides rooms
    
    Each room is a 5x5 of floor tiles with entity on center tile
    Rooms have an additional border of wall tiles (making a 7X7 total)
    Doorways between rooms are 3 ground-tiles wide in EACH room (3x2 for total doorway)
    
  ***************************************************************/
  class Map extends Model {
    constructor (options) {
      super(options);
      this.LVL = options.LVL || 1;;
      this.roomSize = options.roomSize || 7; //including walls
      this.mapWidth = this.roomSize * 3;
      this.visRange = options.visRange || 4; //4 for production
      this.player = options.player; //reference to the player-character
      //set/determine start room for player
      if (this.player.location) {
        const coords = this.strToCoord(this.player.location.address);
        const r = coords.r; const c = coords.c;
        this.playerStartRoom = this.roomFromCoords(r,c);
      } else {
        this.playerStartRoom = options.playerStartRoom || 4; //0-8
      }
      
      this.tiles = this.makeTiles();
      this.setVis();
    }
    
    //returns all rooms of tiles combined into a single array
    makeTiles () {
      //make entities for map
      let mapEntities = [
        new Character({
          entityName: "Guard 2",
          characterINV: [new Food({itemLVL: this.LVL})],
          characterLVL: this.LVL,
        }),
        new Character({
          entityName: "Guard 3",
          characterINV: [new Food({itemLVL: this.LVL})],
          characterLVL: this.LVL,
        }),
        new Character({
          entityName: "Guard 4",
          characterINV: [new Food({itemLVL: this.LVL})],
          characterLVL: this.LVL,
        }),
        //adding 1 more guard with extra potent food has really helped with game balance
        new Character({
          entityName: "Guard 5",
          characterINV: [new Food({itemLVL: this.LVL+1})], 
          characterLVL: this.LVL,
        }),
        new Character({
          entityName: "Guard 1",
          characterINV: [new Weapon({itemLVL: this.LVL+1})],
          characterLVL: this.LVL,
        }),
        new Boss({
          entityName: "Guard Captian",
          characterINV: [new Key({itemLVL: this.LVL})],
          characterLVL: this.LVL,
        }),
        new Door({
          keyNeeded: this.LVL
        }),
        null
      ];
      
      //add player character
      //then add empty room to middle
      //then add player to correct room
      this.shuffle(mapEntities);
      //add empty middle room
      mapEntities.splice(this.playerStartRoom, 0, this.player);
      
      //determine open doors
      //all doors to mid open by default
      let corners = new Set();
      //add 1 doorway for each corner
      corners.add((["top-left", "left-top"])[Math.floor(Math.random()*2)])
      corners.add((["top-right", "right-top"])[Math.floor(Math.random()*2)])
      corners.add((["bot-left", "left-bot"])[Math.floor(Math.random()*2)])
      corners.add((["bot-right", "right-bot"])[Math.floor(Math.random()*2)])
      
      //create all rooms
      let rooms = mapEntities.map((entity, i) => {
        
        const openDoorways = {
          0: {botDoor: corners.has("left-top"), rightDoor: corners.has("top-left")},
          1: {leftDoor: corners.has("top-left"), botDoor: true, rightDoor: corners.has("top-right")},
          2: {leftDoor: corners.has("top-right"), botDoor: corners.has("right-top")},
          3: {topDoor: corners.has("left-top"), rightDoor: true, botDoor: corners.has("left-bot")},
          4: {topDoor: true, rightDoor: true, botDoor: true, leftDoor: true},
          5: {topDoor: corners.has("right-top"), leftDoor: true, botDoor: corners.has("right-bot")},
          6: {topDoor: corners.has("left-bot"), rightDoor: corners.has("bot-left")},
          7: {leftDoor: corners.has("bot-left"), topDoor: true, rightDoor: corners.has("bot-right")},
          8: {leftDoor: corners.has("bot-right"), topDoor: corners.has("right-bot")}
        };
        
        let options = openDoorways[i];
        options.entity = entity;
        
        return this.makeRoom(options);
      });
            
      //finally add rooms to tiles
      //this involves takeing apart the rows of each room and re-assebling
      let tiles = [];
      let roomSize = this.roomSize;
      
      for (let roomR = 0; roomR < 3; roomR++) {
        for (let mapR = 0; mapR < roomSize*roomSize; mapR+=roomSize) {
          for (let roomC = 0; roomC < 3; roomC++) {
            tiles = tiles.concat(rooms[roomR*3 + roomC].slice(mapR, mapR+roomSize));            
          } 
        }
      }
      
      //add address to tiles (now that rooms are connected)
      tiles.forEach((tile, i) => {
        let c = i % (3*roomSize);
        let r = (i-c) / (3*roomSize) ;
        tile.setAddress(r,c);
      });
      
      return tiles;
    }
    
    movePlayer (direction) {
      const oldTile = this.player.location
      const coords = this.strToCoord(oldTile.address);
      let r = coords.r; let c = coords.c;
      //determine destination tile
      if (direction === "up") {r--;} 
      else if (direction === "down") {r++;}
      else if (direction === "left") {c--;}
      else if (direction === "right") {c++;}
      
      //if it doesn't exists return false
      if (r < 0 || r >= this.mapWidth || c < 0 || c > this.mapWidth) {
        return false;

      // else try to add player to that tile
      } else {
        const newTile = this.getTile(r,c);
        
        //if successful
        if (newTile.addEntity(this.player)) {
        //clear oldTile and update visibility
          oldTile.clear();
          
          this.setVis();
          return true;
          
        } else {
          return false;
        }
      } 
    }
    
    //returns single array containing a room of tiles
    makeRoom (options) {      
      
      const centerEntity = options.entity;
      const roomSize = this.roomSize;
      const midPoint = Math.floor(roomSize/2);

      let doors = new Set();
      let coordToStr = this.coordToStr;
      
      //calculate idexes of doors in a flast array
      if (options.topDoor === true) {
        let r = 0; let c = midPoint;
        doors.add(coordToStr(r,c-1));
        doors.add(coordToStr(r,c));
        doors.add(coordToStr(r,c+1));
      }
      if (options.botDoor === true) {
        let r = roomSize-1; let c = midPoint;
        doors.add(coordToStr(r,c-1));
        doors.add(coordToStr(r,c));
        doors.add(coordToStr(r,c+1));
      }
      if (options.leftDoor === true) {
        let r = midPoint; let c = 0;
        doors.add(coordToStr(r-1,c));
        doors.add(coordToStr(r,c));
        doors.add(coordToStr(r+1,c));
      }
      if (options.rightDoor === true) {
        let r = midPoint; let c = roomSize-1;
        doors.add(coordToStr(r-1,c));
        doors.add(coordToStr(r,c));
        doors.add(coordToStr(r+1,c));   
      }
      
      //make room
      return this.makeSpiralArray(roomSize, roomSize, (r,c, i) => {
        //check if need to make doorways
        if ( doors.has( coordToStr(r,c) )) {
          return new GroundTile({});
        
          //add wall tiles to other outer edges
        } else if (i < (4 * roomSize)-4) {
          return new WallTile({});
        
          //add a ground tile with the centerentiy to last tile
        } else if (i === roomSize*roomSize -1) {
          return new GroundTile({starterEntity:centerEntity});

        //add ground tiles to everything else  
        } else {
          return new GroundTile({});
        }
      });
    }
    
    //make a 2D array and spiral traverse it
    makeSpiralArray(R, C, callback) { //R,C = max values
      let minR = 0;
      let minC = -1;
      let maxR = R;
      let maxC = C-1;
      let r = 0;
      let c = 0;
      //note (uppercase) R&C unchanged;
      
      let arr = new Array(R*C);
      //simpler to do callback on item 0 before traversal
      arr[0] = callback(r,c, 0);
      
      let dir = "right";
      for (let count = 1; count < R*C; count++) {
        
        if (dir === "right") {
          c++;
          if (c===maxC) {maxR -= 1; dir = "down";}
        } else if (dir === "down") {
          r++;
          if (r===maxR) {minC += 1; dir = "left";}
        } else if (dir === "left") {
          c--;
          if (c===minR) {minR += 1; dir = "up";}
        } else if (dir === "up") {
          r--;
          if (r===minR) {maxC -= 1; dir = "right";}
        }
        arr[ (r*C) + c ] = callback(r,c, count);
      }
      
      return arr;
    }
    
    setVis () {
      const playerR = this.strToCoord(this.player.location.address).r;
      const playerC = this.strToCoord(this.player.location.address).c;
      
      this.tiles.forEach((tile) => {
        
        const tileR = this.strToCoord(tile.address).r;
        const tileC = this.strToCoord(tile.address).c;
        const d = this.pythag(playerR, tileR, playerC, tileC);
        
        if (d < this.visRange) {
          tile.discover();
        } else {
          tile.unDiscover();
        }
      });
    }
    
    coordToStr (r,c) {
      return "" + r + "x" + c;
    }
    
    strToCoord (str) {
      const split = str.split("x");
      return {r: Number(split[0]), c: Number(split[1])};
    }
    
    getTile (r,c) {
      return this.tiles[r*this.mapWidth + c];
    }
    
    //return distance between 2 points
    pythag (x1, x2, y1, y2) {
      const dX = Math.abs(x1-x2);
      const dY = Math.abs(y1-y2);
      return Math.sqrt(dX*dX + dY*dY);
    }
    
    //takes coordinates, returns which of the 9 rooms those coords are in
    roomFromCoords (r,c) {
      const roomR = Math.floor(r/this.roomSize);
      const roomC = Math.floor(c/this.roomSize);
      return (roomR*3 + roomC);
    }
    
    //shuffle an array in place
    //DISCLAIMER: not an original shuffle function,
    //courtesy of http://stackoverflow.com/users/353278/jeff
    shuffle(a) {
      for (let i = a.length; i; i--) {
          let j = Math.floor(Math.random() * i);
          [a[i - 1], a[j]] = [a[j], a[i - 1]];
      }
    }
    
  }

  /***************************************************************
  NARRATOR
  
  Contains all messages
  Creates new messages
  Recieves event triggers between to make messages
  ***************************************************************/
  class Narrator extends Model {
    constructor (options) {
      super(options);
      this.messages = this.defaultMessage();
    }
    
    event (options) {
      this.messages.push(new Message(options));
    }
    
    defaultMessage () {
      let messages = [];
      let message1 = new Message({
        action: "instruction",
        subject: "Triumphantly fight up all (4) floors of the tower."
      });
      let message2 = new Message({
        action: "instruction",
        subject: "Use the arrow keys to move."
      });
      let message3 = new Message({
        action: "instruction",
        subject: "Defeat enemies for food, weapons, and keys to the next floor."
      });
      let message4 = new Message({
        action: "instruction",
        subject: "Good Luck!"
      });
      return messages.concat([message1, message2, message3, message4])
    }
    
    clearAll () {
      this.messages = [];
    }
    
  }

  /***************************************************************
  GAME
  
  Top level model
  contains all other models/views
  ***************************************************************/
  class Game extends Model {
    constructor (options) {
      super(options);
      this.player = new Player({entityName: "Player"});
      this.narrator = options.narrator || new Narrator({});
      this.map = new Map({player: this.player});
      this.isRunning = true;
      this.node = options.node;
      
      this.render();
      document.onkeydown = this.handleKeyPress.bind(this);
    }
    
    event (type, options) {
      let eventRouter = {
        message: this.narrator.event.bind(this.narrator),
        move: this.map.movePlayer.bind(this.map),
        nextLevel: this.nextLevel.bind(this),
        end: this.endGame.bind(this),
      };
      
      eventRouter[type](options);
      this.render();
    }
    
    nextLevel () {
      this.narrator.clearAll();
      let oldLevel = this.map.LVL;
      if (oldLevel === 4) { //end game
        game.event("end", {win: true});   
      } else {
        this.map = new Map({
          player: this.player,
          LVL: oldLevel + 1
        });
        this.narrator.event({
          action: "instruction",
          subject: "Welcome to Floor " + (oldLevel + 1)
        });        
      }

    }
    
    endGame (options) {
      this.narrator.clearAll();
      this.isRunning = false;
      if (options.win) {
        this.narrator.event({
          action: "instruction",
          subject: "You Win!"
        });
        this.narrator.event({
          action: "instruction",
          subject: "You saved the princess...or something like that."
        });
      } else {
        this.narrator.event({
          action: "instruction",
          subject: "You have been slain."
        });
      }
    }
    
    handleKeyPress (event) {
      if (this.isRunning) {
        if (event.key == 'ArrowUp' || event.key == 'ArrowDown' || event.key == 'ArrowLeft' || event.key == 'ArrowRight') {
          this.event("move", event.key.split("Arrow")[1].toLowerCase());
        }
      }
    }
    
    errorHandler (thing) {
      console.error("Unrecognized:");
      console.dir(thing);
      console.trace();
    }
    
    //note game and its children should be plain objects
    //use the Model prototype .getProperties
    render () {  
      let game = this.getProperties();
      ReactDOM.render(
        <GameView
          player={game.player}
          currentFloor={game.map.LVL}
          map={game.map}
          narrator={game.narrator}
        />,
        this.node
      );
    }
  }

/***************************************************************
STARTUP CODE  
***************************************************************/
let container = document.getElementById("app");
window.game = new Game({node: container});
            
          
!
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.

Console