123

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.

            
                <div id="container"></div>
            
          
!
            
              body {
  background-image: url(https://c2064.in-berlin.de/fcc/img/tiles/floors/minerocks.png);
  font-family: "Anonymous Pro", "Courier New", sans-serif;
}

.dungeon-wrapper {
  position: relative;
  width: 1600px;
  margin: 0 auto;
  overflow: hidden;
  margin-bottom: 86px;
}

table.dungeon {
  width: 100%;
  
  tr {
  }
  td.tile {
    padding: 0;
    
    img {
      width: 32px;
    }
  }
}

.player {  
  .light-circle {
    border: 1650px solid rgba(0,0,0,1.0);
    border-radius: 100%;
    padding-top: 142px;
    height: 3600px;
    width: 3600px;

    .image-wrapper {
      position: relative;
      width: 32px;
      height: 32px;
      margin-left: auto;
      margin-right: auto;
      
      .fighting-anim {
        position: absolute;
        top: -40px;
        left: -45px;
      }
    }
  }
}

.score-wrapper {
  width: 1600px;
  height: 80px;
  position: fixed;
  bottom: 3px;
  
  .score {
    width: 770px;
    height: 100%;
    margin: 0 auto;
    border: 2px solid grey;
    border-radius: 20px;
    background-color: rgba(100,100,100,0.7);
    overflow: hidden;
    
    .meter {
      position: relative;
      width: 200px;
      height: 35px;
      border: 2px solid #555;
      border-radius: 10px;
      float: right;
      margin: 3px 20px;
      overflow: hidden;
      z-index: 100;
      
      .text {
        width: 100%;
        text-align: center;
        line-height: 10px;
        
        .name {
          color: rgba(0,0,0,0.5);
          font-size: 20px;          
          line-height: 20px;
        }
        
        br {
          line-height: 0px;
        }
        
        .value {
          color: rgba(0,0,0,1);
          font-size: 14px;    
          line-height: 14px;
          
        }
      }
      
      .bar {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
      }
      
      .bar.health {
        background-color: rgba(0,150,0,0.4);
      }
      .bar.strength {
        background-color: rgba(150,0,0,0.4);
      }
      .bar.stamina {
        background-color: rgba(0,0,150,0.4);
      }
    }
    
    .items {
      float: left;
      margin-left: 40px;
      
      img {
        width: 24px;
      }
      
      img.selected {
        border: 1px solid rgba(255,255,255,0.4);
      }
      
      .name {
        font-size: 20px;
      }
    }
    
    .gameover {
      margin-left: 50px;
      font-size: 30px;
      font-weight: bold;
      
      .cry {
        color: darkred;
      }
      
      button {
        margin-right: 40px;
        font-size: 22px;
        padding: 2px 8px 2px 8px;
        border-radius: 10px;
        float: right;
      }
    }
  }
  
  .score.monster {
    width: 0px;
    float: right;
  }  
}

.score-wrapper.in-fight {
  .player {
    float: left;  
  }
  
  .score {
    width: 770px;
  }
}

.highscore {
  position: absolute;
  bottom: 55px;
  left: 1240px;
  
  td {
    padding: 0 10px;
  }
}
            
          
!
            
              //////////////////////////////////////////////////////////////////
// ROGUE REACTION 2
// Rogue like Game programmed with React/Redux
// Author: Michael Schmidt 2017
//////////////////////////////////////////////////////////////////

// React /////////////////////////////////////////////////////////
const Component = React.Component;

// Redux /////////////////////////////////////////////////////////
const Provider = ReactRedux.Provider;
const connect = ReactRedux.connect;
const createStore = Redux.createStore;
const combineReducers = Redux.combineReducers;
const bindActionCreators = Redux.bindActionCreators;

// Constants //////////////////////////////////////////////////////
const IMG_URL = "http://c2064.in-berlin.de/fcc/img/tiles/",
      BOARD_WIDTH = 50,
      KEY_LEFT = 37,
      KEY_UP = 38,
      KEY_RIGHT = 39,
      KEY_DOWN = 40,
      
// Game constants //////////////////////////////////////////////////////      
      HEALTH_MAX = 100,
      STAMINA_MAX = 666,
      STRENGTH_MAX = 666,
      STRENGTH_TO_STAMINA = 0.07, // Increase of stamina after monster defeat
      STAMINA_TO_STRENGTH = 0.10, // Increase of strength after monster defeat

// Player Definition //////////////////////////////////////////////////////
      PLAYER = {
        strength: 1,
        health: 100,
        stamina: 1,
        weapons: [],
        armories: [],
        treasures: [],
        bossDead: false,
        img: "player/base/kenku_winged_f.png",
        imgDead: "player/base/kenku_winged_f_dead.png",
        imgFighting: "fighting.svg"   // Animation
      },

// Weapons ////////////////////////////////////////////////////////////////      
      WEAPONS = [ // Board token: "S"
        { name: "Knife",        img: "UNUSED/weapons/ancient_sword.png",    strength:   4, speciality: [2,5,1,1,1,1,1,1,1] },
        { name: "Hammer",       img: "UNUSED/weapons/lucern_hammer.png",    strength:   8, speciality: [2,2,1,2,1,1,1,1,1] },
        { name: "Crossbow",     img: "UNUSED/weapons/hand_crossbow.png",    strength:  10, speciality: [1,1,1,3,2,1,1,1,1] },
        { name: "Axe",          img: "UNUSED/weapons/axe.png",              strength:  15, speciality: [1,2,1,1,1,1,1,1,1] },
        { name: "Sword",        img: "UNUSED/weapons/two_handed_sword.png", strength:  25, speciality: [1,1,2,1,1,2,1,1,1] },
        { name: "Mace",         img: "UNUSED/weapons/mace3.png",            strength:  30, speciality: [1,1,1,1,2,1,1,1,1] },
        { name: "War Hammer",   img: "UNUSED/weapons/war_hammer.png",       strength:  50, speciality: [1,1,1,1,1,1,2,1,1] },
        { name: "Elven Sword",  img: "UNUSED/weapons/elven_broadsword.png", strength:  80, speciality: [1,1,1,1,1,1,1,2,1] },
        { name: "Golden Sword", img: "UNUSED/weapons/golden_sword.png",     strength: 111, speciality: [1,1,2,1,1,2,1,1,3] }
      ],

// Armories ////////////////////////////////////////////////////////////////      
      ARMORIES = [ // "A"
        { name: "Leather Armor", img: "UNUSED/armour/studded_leather_armor.png", stamina: 4 },
        { name: "Elven Armor", img: "item/armour/elven_leather_armor.png", stamina: 6 },
        { name: "Orcish Armor", img: "item/armour/orcish_leather_armor.png", stamina: 10 },
        { name: "Plate Mail", img: "item/armour/plate_mail2.png", stamina: 20 },
        { name: "Ring Mail", img: "item/armour/ring_mail2.png", stamina: 50 },
        { name: "Scale Mail", img: "item/armour/scale_mail2.png", stamina: 80 },
        { name: "Robe Armor", img: "item/armour/robe3.png", stamina: 222 },
      ],

// Potions ////////////////////////////////////////////////////////////////      
      POTIONS = [ // "P"
        { name: "Murkey", img: "UNUSED/potions/potion_murky.png", health: 35, stamina: 1 },
        { name: "Golden Potion", img: "UNUSED/potions/potion_golden.png", health: 50, stamina: 1 },
        { name: "Cloudy Potion", img: "UNUSED/potions/potion_cloudy.png", health: 60, stamina: 2 },
        { name: "Effervescent", img: "UNUSED/potions/potion_effervescent.png", health: 70, stamina: 2 },
        { name: "Bubbly Potion", img: "UNUSED/potions/potion_bubbly.png", health: 70, stamina: 3},
        { name: "Blue Sky", img: "UNUSED/potions/potion_sky_blue.png", health: 80, stamina: 3 },
        { name: "Purple", img: "UNUSED/potions/potion_purple_red.png", health: 90, stamina: 5 },
        { name: "Puce", img: "UNUSED/potions/potion_puce.png", health: 100, stamina: 10 },
        { name: "Fizzy", img: "UNUSED/potions/potion_fizzy.png", health: 100, stamina: 30 },
      ],

// Keys ////////////////////////////////////////////////////////////////      
      KEYS = [ // "K"
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 1 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 2 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 3 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 4 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 5 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 6 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 7 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 8 },
        { name: "Golden Key", img: "UNUSED/other/key.png", door: 9 },
      ],

// Treasures ////////////////////////////////////////////////////////////////      
      TREASURES = [ // "T"
        { name: "Golden Ring",    img: "item/ring/gold.png" },
        { name: "Magic Ring",     img: "item/ring/artefact/urand_mage.png" },
        { name: "Scroll",         img: "item/scroll/scroll.png" },
        { name: "Staff",          img: "item/staff/staff05.png" },
        { name: "Amulet",         img: "item/amulet/face1_gold.png" },
        { name: "Celtic amulet",  img: "item/amulet/celtic_red.png" },
        { name: "Stone amulet",   img: "item/amulet/stone3_blue.png" },
        { name: "Mail",           img: "item/armour/artefact/urand_dyrovepreva.png" },
        { name: "Book",           img: "item/book/book_of_the_dead.png" },
        { name: "Magic Weapon",   img: "item/weapon/artefact/urand_flaming_death.png" },
        { name: "Golden Staff",   img: "item/staff/staff07.png" },
        { name: "The Artefact",   img: "item/armour/artefact/urand_pondering.png" },
      ],

// Monsters ////////////////////////////////////////////////////////////////      
      MONSTERS = [ // "M"
        { name: "Kobold" ,      img: "dc-mon/big_kobold.png",           strength: 4, stamina: 4, health: 100 },
        { name: "Stone Giant" , img: "dc-mon/stone_giant.png",          strength: 10, stamina: 8, health: 100 },
        { name: "Elf"  ,        img: "dc-mon/deep_elf_conjurer.png",    strength: 20, stamina: 20, health: 100 },
        { name: "Centaur" ,     img: "dc-mon/centaur_warrior.png",      strength: 50, stamina: 60, health: 100 },
        { name: "Hell Wing"  ,  img: "dc-mon/demons/hellwing.png",      strength: 50, stamina: 100, health: 100 },
        { name: "Elf Priest" ,  img: "dc-mon/deep_elf_high_priest.png", strength: 130, stamina: 130, health: 100 },
        { name: "Frost Giant" , img: "dc-mon/frost_giant.png",          strength: 180, stamina: 240, health: 100 },
        { name: "Daeva"  ,      img: "dc-mon/daeva.png",                strength: 300, stamina: 280, health: 100 },
        { name: "Balrug" ,      img: "dc-mon/demons/balrug.png",        strength: 1500, stamina: 3000, health: 100 },
      ],      

// Dungeon definitions //////////////////////////////////////////////////////////////////////
// Theoretically more than one is possible, but this feature is not used in the game so far.
      DUNGEONS = [{
        wallImgs: [ // Consequtive used
          "dc-dngn/wall/brick_gray0.png",
          "dc-dngn/wall/brick_gray1.png",
          "dc-dngn/wall/brick_gray2.png",
          "dc-dngn/wall/brick_gray3.png",
        ],
        doorImgs: [ // Randomly chosen
          "dc-dngn/dngn_closed_door.png",
          "dc-dngn/dngn_closed_door.png",
          "dc-dngn/dngn_closed_door.png",
          "dc-dngn/dngn_closed_door.png",
        ],
// Dungeon map //////////////////////////////////////////////////////////////////////
// W- : Wall
// E  : Player entrance      
// Mx : Monster / M9 is boss
// Kx : Key
// Dx : Door
// Ax : Armory
// Px : Potion
// Sx : Weapon
// Tx : Treasure
// Hint: Size changes (rows/cols) are not tested, but should work
        dmap: [
            "W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-",
            "W-W-  TB                                                D9                  P8                    W-",
            "W-W-  W-W-W-W-W-W-W-W-P9W-W-W-P9W-W-W-W-W-W-W-W-W-W-W-W-W-  W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-  W-",
            "W-W-  W-W-W-W-K4TA    W-      W-      W-      W-      W-W-  W-  M2M3M4M5M6M7M8S9M8M8  P7  P7  W-  W-",
            "W-W-M9W-W-W-W-W-W-W-  W-  W-  W-  W-      W-  P7  W-    W-  W-  M1W-M3M4M5M6M7M8M8M8  W-W-W-K9W-  W-",
            "W-W-W-W-W-W-      W-M6W-  W-W-W-  W-W-W-W-W-  W-  W-W-  W-  W-P7  W-M2M3M4M5M6M7M8M8  W-W-W-W-W-  W-",
            "W-W-W-W-W-W-  W-  W-  W-  W-      W-      W-  W-  W-    W-  W-P6  W-M1M2M3M4M5M6M7M8  W-          W-",
            "W-W-W-W-W-W-  W-  W-  W-P7W-W-W-  W-W-W-  W-  W-  W-  W-W-D8W-W-  W-W-W-W-W-W-W-W-W-W-W-  W-W-W-W-W-",
            "W-M6M6    W-  W-M6W-  W-          P7  W-  W-  W-W-W-T0W-  S8  W-                          W-K5S4P4W-",
            "W-M6M6W-  W-  W-  W-  W-  W-W-W-W-W-  W-  W-      W-W-W-  A7  W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-P5  P4W-",
            "W-M6M6W-      W-    M6W-          W-  P7  W-T9W-      W-K8    W-T5W-      M3M4M5A5M6M5          P5W-",
            "W-    W-W-W-W-W-W-W-W-W-W-W-W-W-  W-  W-W-W-W-W-W-W-  W-W-M8W-W-K2W-  W-W-W-W-W-W-W-W-W-D2W-W-P4P4W-",
            "W-    W-                          W-    W-T8      W-  W-  M8  W-M4W-  W-S1D7  K7            W-W-W-W-",
            "W-    W-  W-W-W-W-W-  W-  W-  W-  W-W-P7W-W-  W-  W-  W-  M8  W-  W-  W-W-W-                W-P3P3W-",
            "W-    W-  W-P4W-      W-M7W-  W-          W-  W-  W-  W-      W-  W-  W-T1W-M2  M2          W-P2P2W-",
            "W-P4  W-  W-T6W-  W-W-W-M8W-  W-W-W-W-W-W-W-  W-  P7  W-      W-  W-  W-K3M3  M2            W-P1P1W-",
            "W-P4  W-  W-  W-P3W-K6P6M8W-W-W-W-            W-W-W-W-W-M7W-W-W-  W-  W-W-W-M2              W-P1P1W-",
            "W-P4  W-  W-M4W-M4W-P6P5    M5      W-W-W-W-W-W-  M5      M7M5D4  W-M2W-                      P1P1W-",
            "W-P4  W-  W-M4W-M4W-W-W-W-W-W-W-W-W-W-        W-  M4  M6M6M7  W-  W-  W-W-W-W-W-W-W-W-W-    W-W-W-W-",
            "W-P4  W-  W-M4W-M4W-M2M2M2W-        W-    W-M2W-        M7M6  W-  W-  W-  M1    A1P1P1      W-  K1W-",
            "W-P5  W-  W-M4W-M4W-M2W-  W-M3W-W-A3W-    W-  W-  M3M5    M5  W-  W-  W-                    W-    W-",
            "W-P6P5W-S6W-      W-M2W-S3W-P2M3W-T3W-    W-M2W-    M4  M4M6  W-  W-  W-W-                  W-W-  W-",
            "W-P7P6D6T7W-W-  W-W-M2W-T4W-W-P2W-P5W-    W-  W-W-W-W-W-W-W-W-W-      P3W-                    M1  W-",
            "W-W-W-W-W-W-    W-W-M2W-P5W-  M3W-P6W-    W-            M2            T2W-                      M1W-",
            "W-P5P5P5M6S5  W-W-W-  W-W-W-  W-W-W-W-M4M4W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-                        W-",
            "W-D5W-W-W-W-W-W-                      P3P3          M2            W-S2A2W-                M1      W-",
            "W-                                          M3      M2      M3    D3P1  D1            M1        P2W-",
            "W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-W-  E   W-W-W-W-W-W-W-W-W-"
          ],
      }];

///////////////////////////////////////////////////////////////////////////////////////
// React classes 

// GameBoard /////////////////////////////////////////
class GameBoardClass extends Component {
  constructor(props) {
    super(props);
    
    this.state = { };
  }
  
  // drawDungeon ///////////////////////////////////////////////////////////////////
  // Draws DOM for dungeon
  drawDungeon() {
    let board = this.props.board.dungeons[this.props.board.currentDungeon];
    
    if( !board ) return <div>No board yet ...</div>;
    
    // Transform 2-dim array board.dmap into a DOM table
    return board.dmap.map((row, i) => {
      return (
        <tr>
          {row.map((tile, j) => {
            if( tile.info ) {
              // Square with an object ///////////////////
              var element = <img 
                  src={IMG_URL+tile.info.img}
                  className={"img img-responsive tile-"+(i+"-"+j)}
                />;
            } else {
              // Empty square ////////////////////////////
              var element = <div className={"tile-"+(i+"-"+j)}></div>;
            }
            return (
              <td className="tile">
                {element}
              </td>
            )
          })}
        </tr>
      )
    })
  }
  
  render() {
    return (
      <div>
        <table className="dungeon">
          {this.drawDungeon()}
        </table>
        <HighScore />
      </div>
    )
  }
}

const GameBoard = connect(
  ({board}) => { return {board}; }
)(GameBoardClass);

// Player /////////////////////////////////////////
class PlayerClass extends Component {
  constructor(props) {
    super(props);
    
    this.state = { 
      interval: setInterval(() => this.fighting(), 500),
      scrolling: false
    };    
  }
  
  componentDidMount() {
    // Set player position and scroll screen down at the beginning
    // Todo: Replace jQuery with sth more reactish
    let $this = $(ReactDOM.findDOMNode(this)),
        pos = this.getPosition();
    
    $this.css( pos );
    $("html, body").animate({ scrollTop: $(document).height() }, 1000);
    
    // ToDo: find timing problem with player position: This is a workaround!
    this.props.movePlayer(KEY_UP);
    setTimeout(() => this.props.movePlayer(KEY_DOWN), 100);

  }

  // Event handler for key presses (how this without DOM commands?)
  componentWillMount() {
      document.addEventListener("keydown", this.onKeyPressed.bind(this));
  }
  componentWillUnmount() {
      document.removeEventListener("keydown", this.onKeyPressed.bind(this));
  }      
  
  // Handle key presses ////////////////////////////////////
  onKeyPressed(e) {
    e.preventDefault();
    
    let player = this.props.board.player;
    
    if( player.health > 0 ) this.props.movePlayer(e.keyCode);
  }
  
  // Determine position of (very large) player object
  // The player object consists of the player image and a large circle
  // covering the dungeon, so that only the player area can be seen of it
  getPosition() {
    let $this = $(ReactDOM.findDOMNode(this)),
        pos = this.props.board.player.pos,
        tile = $(".tile-"+(pos.row+"-"+pos.col)),
        position = tile && tile.position() || 0;

    return {
      "top": position.top || 0,
      "left": position.left || 0,
      "circleTop": position && (position.top-1825+16)+"px" || 0,
      "circleLeft": position && (position.left-1800+16)+"px" || 0    
    };
  }
  
  // Let the board scroll to the player position if needed
  scrollBoard(top, left) {
    let distanceTop = top - $("body").scrollTop(),
        distanceLeft = left - $("body").scrollLeft(),
        state = this.state;

    // Vertical scrolling ////////////////////////////////////////////////////
    if( !state.scrolling && (distanceTop < 200 || distanceTop > 400) ) {
      state.scrolling = true;
      $("html, body").animate({ 
        scrollTop: top-300,
      }, 500, () => state.scrolling = false );      
    }

    // Horizontal scrolling ////////////////////////////////////////////////////
    if( !state.scrolling && (distanceLeft < 200 || distanceLeft > 400) ) {
      state.scrolling = true;
      $("html, body").animate({ 
        scrollLeft: left-300,
      }, 500, () => state.scrolling = false );      
    }
  }

  // Fighting function (called every 0.5 sec) ////////////////////////////////////////
  fighting() {
    let player = this.props.board.player,
        monster = this.props.board.dungeons[this.props.board.currentDungeon].monster;
    
    if( !monster ) return; // no monster no fight
    
    // Let them fight!
    let monsterDamage = Math.round( (player.strength + (player.additionalStrength || 0)) / monster.info.stamina * Math.random() * 10 ),
        playerDamage = Math.round( monster.info.strength / (player.stamina + (player.additionalStamina || 0)) * Math.random() * 10 );
    this.props.modifyHealth( -playerDamage , -monsterDamage );
  }
    
  render() {
    let {img, imgDead, imgFighting, health} = this.props.board.player,
        {top, left, circleTop, circleLeft} = this.getPosition(),
        fighting = !!this.props.board.dungeons[this.props.board.currentDungeon].monster;

    let fightingAnim = fighting? <div className="fighting-anim"><img src={IMG_URL+imgFighting} className="img img-resonsive" /></div> : "";
    
    this.scrollBoard(top, left);
    
    return (
      <div 
        className="player"
        style={{ position: "absolute", top: circleTop, left: circleLeft }}
        onKeyDown={(e) => this.onKeyPressed(e)}  // This is not working! componentWillMount() and componentWillUnmount() is needed
      >
        <div className="light-circle">
          <div className="image-wrapper">
            {fightingAnim}
            <img src={IMG_URL+ (health>0? img:imgDead)} />
          </div>
        </div>
      </div>
    )
  }
}

const Player = connect(
  ({board}) => { return {board}; },
  (dispatch) => bindActionCreators({movePlayer, modifyHealth}, dispatch)
)(PlayerClass);

// Score //////////////////////////////////////////////////////////////////////
class ScoreClass extends Component {
  constructor(props) {
    super(props);
    
    this.state = { };
  }
  
  render() {
    let {
      img, 
      stamina,
      additionalStamina = 0,
      health,
      strength,
      additionalStrength = 0,
      weapons,
      armories,
      treasures,
      bossDead = 0,
    } = this.props.board.player,
      monster = this.props.board.dungeons[this.props.board.currentDungeon].monster;
    
    if( health > 0 && !bossDead ) {
      var items = (
        <div>
          <ScoreItems type="weapons" label="W" items={weapons} selected={this.props.board.player.selectedWeapon} />
          <ScoreItems type="armories" label="A" items={armories} />
          <ScoreItems type="treasures" label="T" items={treasures} />
        </div>)
    } else {
      let score = Math.floor( 
          ((stamina + additionalStamina) / STAMINA_MAX * 20 +
          (strength + additionalStrength) / STRENGTH_MAX * 20 + 
          treasures.length / TREASURES.length * 10 +
          (bossDead? 50:0)) * 10
        ) / 10;
    
      var items = (
        <div className="gameover">
          <span className="cry">{bossDead?"YOU WON!!!":"GAME OVER!"}</span>
          <span className="final-score"> Your Score: {score}%</span>
          <button 
            className="btn btn-primary"
            onClick={() => this.props.newGame()}
          >
            Start over ...
          </button>
      </div>);
      
      // Set Highscore
      this.props.setHighScore(score);
    }
    
    return (
      <div className={"score-wrapper "+(monster? "in-fight":"")}>
        <div className="score player">
          <img src={IMG_URL+img} className="img img-responsive" />
          <Meter name="Health" type="health" value={health} min="0" max={HEALTH_MAX} /> 
          <Meter name="Strength" type="strength" value={strength+additionalStrength} min="0" max={STRENGTH_MAX} /> 
          <Meter name="Stamina" type="stamina" value={stamina+additionalStamina} min="0" max={STAMINA_MAX} />
          {items}
        </div>
        <div className="score monster">
          <img src={monster && IMG_URL+monster.info.img || undefined} className="img img-responsive" />
          <Meter name="Health" type="health" value={monster && monster.info.health || undefined} min="0" max={HEALTH_MAX} /> 
          <Meter name="Strength" type="strength" value={monster && monster.info.strength || undefined} min="0" max={STRENGTH_MAX} /> 
          <Meter name="Stamina" type="stamina" value={monster && monster.info.stamina || undefined} min="0" max={STAMINA_MAX} />
        </div>        
      </div>
    )
  }
}

const Score = connect(
  ({board}) => { return {board}; },
  (dispatch) => bindActionCreators({newGame, setHighScore}, dispatch)
)(ScoreClass);

// Meter ///////////////////////////////////////////////////////////////////////////
const Meter = ({name, type, value, min, max}) => {
  let width = ((value-min)/(max-min)*100)+"%";
  
  return (
    <div className="meter">
      <div className="text">
        <span className="name">{name}</span>
        <br />
        <span className="value">{value}</span>
      </div>
      <div className={"bar "+type} style={{ width: width }}></div>
    </div>
  );
}

// ScoreItems //////////////////////////////////////////////
const ScoreItems = ({type, label, items, selected}) => {
  //console.log("ScoreItems/selected: "+selected+", items[0]: "+JSON.stringify(items[0]));
  return (
    <div className={type + " items"}>
      <span className="name">{label}: </span>
      { items.map((weapon, i) => 
        <img 
          src={IMG_URL+weapon.info.img} 
          className={"img img-responsive "+(selected===weapon.info.name?"selected":"")} 
         />
       )}
    </div>
  );
}

// Highscore Class /////////////////////////////////////////
class HighScoreClass extends Component {
  constructor(props) {
    super(props);
    
    this.state = { };
  }
  
  render() {
    return (
      <table className="highscore">
        {this.props.highscore.table_.map((score, i) => <tr><td>{(i+1)+":"}</td><td>{score.score+"%"}</td><td>{score.date}</td></tr>)}
      </table>
    )
  }
}

const HighScore = connect(
  ({highscore}) => { return {highscore}; }
)(HighScoreClass);


// Redux Reducers /////////////////////////////////////////////////////////
const GameBoardReducer = function(state, action) {
  
  if( state === undefined ) {
    let dungeons = fillDungeons();
    let state = { 
      currentDungeon: 0,
      player: { ...dungeons[0].player },
      dungeons: dungeons
    }
    
    return state;
  }
 
  switch( action.type ) {
    
    case MOVE_PLAYER_ACTION:
      var player = checkAndMovePlayer(state, action.payload);
      return { ...state, player: player };
    
    case MODIFY_HEALTH_ACTION:
      var monster = modifyMonsterHealth( 
            state.dungeons[state.currentDungeon], 
            state.dungeons[state.currentDungeon].monster, 
            action.payload.monsterHealth
          ), 
          player = modifyPlayerHealth(
            state.dungeons[state.currentDungeon],
            state.player,
            monster? null : state.dungeons[state.currentDungeon].monster.info,
            action.payload.playerHealth
          );
      
      if( state.dungeons[state.currentDungeon].monster ) state.dungeons[state.currentDungeon].monster = monster;
      return { ...state, player };
    
    case NEW_GAME_ACTION:
      let dungeons = fillDungeons();
      let state = { 
        currentDungeon: 0,
        player: { ...dungeons[0].player, weapons: [], armories: [], treasures: [], bossDead: false },
        dungeons: dungeons
      }
      return state;      
  }
  
  return state;
}

const checkAndMovePlayer = function(state, move) {
  let player = { ...state.player },
      newPos = { ...player.pos },
      board = state.dungeons[state.currentDungeon];

  switch( move ) {
    case KEY_UP: newPos.row--; break;    
    case KEY_DOWN: newPos.row++; break;    
    case KEY_LEFT: newPos.col--; break;    
    case KEY_RIGHT: newPos.col++; break;    
  }

  let tile = board.dmap[newPos.row][newPos.col];
  if( typeof tile === "object") {
    player = check[tile.type](board, player, newPos) || state.player;
  } else {
    setPlayerPos( board, player, newPos );
  }
  
  if( tile.type !== "monster" ) {
    player = checkSurroundingMonsters( board, player, newPos);
  }
  
  return player;
}

const setPlayerPos = function(board, player, pos) {
  player.pos = pos;
  board.monster = null; // End fight with monster (if there was one)
}

const modifyPlayerHealth = function( board, player, info, health ) {
  // Modify players health
  player.health = Math.min(Math.max(player.health + health, 0), HEALTH_MAX);

  // If monster is dead, add a fourth of the stamina of it to players strength
  if( info ) {
    if( info.name === "Balrug" ) {
      player.bossDead = true; 
    } else {
      player.strength = Math.floor(Math.min(player.strength + info.stamina*STAMINA_TO_STRENGTH, STRENGTH_MAX)*10)/10;
      player.stamina = Math.floor(Math.min(player.stamina + info.strength*STRENGTH_TO_STAMINA, STAMINA_MAX)*10)/10;      
    }
  }

  // if player is dead, for the monster the fight ends also
  if( player.health === 0 ) board.monster = null;
 
  return player;
}

const modifyMonsterHealth = function( board, monster, health ) {
  // Modify players health
  monster.info.health = Math.min(Math.max(monster.info.health + health, 0), HEALTH_MAX);

  // If monster is dead remove it and return null
  if( monster.info.health === 0 ) {
      board.dmap[monster.pos.row][monster.pos.col] = -1;
      return null;
  } else {
    return monster;
  }
}

////////////////////////////////////////////////////////////////////////
// Check Wall (nothing to check, just not let him through)
const checkWall = function(board, player, newPos) {
  return null;
}

////////////////////////////////////////////////////////////////////////
// Check monster
const checkMonster = function(board, player, newPos) {
  let monster = board.dmap[newPos.row][newPos.col],
      index = parseInt(monster.index)-1,
      additionalStrength = 0,
      weapon = player.weapons.reduce((best, weapon) => {
        if( best && best.info.speciality[index] * best.info.strength > weapon.info.speciality[index] * weapon.info.strength ) {
          additionalStrength = best.info.speciality[index] * best.info.strength;
          return best;
        } else {
          additionalStrength = weapon.info.speciality[index] * weapon.info.strength;
          return weapon;
        }
      },null);
  
  board.monster = monster;
  player.selectedWeapon = weapon && weapon.info.name;
  player.additionalStrength = additionalStrength;
  
  return player;
}

////////////////////////////////////////////////////////////////////////
// Check key
const checkKey = function(board, player, newPos) {
  var key = board.dmap[newPos.row][newPos.col],
      door = board.doors.filter(function(door) {return door.index === key.index})[0];

  if( !door ) {
    console.error("Door "+key.index+" is missing.");
    return null;    
  }
  
  // Remove door and key from map
  board.dmap[door.pos.row][door.pos.col] = -1;
  board.dmap[newPos.row][newPos.col] = -1;
  
  // Set player to new position
  setPlayerPos( board, player, newPos );
  
  return player;
}

////////////////////////////////////////////////////////////////////////
// Check armory
const checkArmory = function(board, player, newPos) {
  var armory = board.dmap[newPos.row][newPos.col];

  // Collect armory
  if( !player.additionalStamina || player.additionalStamina < armory.info.stamina ) {
    player.additionalStamina = armory.info.stamina;
    player.armories[0] = armory; // Only one armory at a time
  }
  
  // Remove armory from dungeon
  board.dmap[newPos.row][newPos.col] = -1;

  // Set player to new position
  setPlayerPos( board, player, newPos );

  return player;
}

////////////////////////////////////////////////////////////////////////
// Check potion
const checkPotion = function(board, player, newPos) {
  var potion = board.dmap[newPos.row][newPos.col];
  
  // Heal the player
  player.health = Math.min(player.health + potion.info.health, HEALTH_MAX);
  player.stamina = Math.min(player.stamina + potion.info.stamina, STAMINA_MAX);

  // Remove potion
  board.dmap[newPos.row][newPos.col] = -1;

  // Set player to new position
  setPlayerPos( board, player, newPos );
  
  return player;
}

////////////////////////////////////////////////////////////////////////
// Check weapon
const checkWeapon = function(board, player, newPos) {
  var weapon = board.dmap[newPos.row][newPos.col];

  // Collect armory
  player.weapons.push(weapon);
  
  // Remove armory
  board.dmap[newPos.row][newPos.col] = -1;

  // Set player to new position
  setPlayerPos( board, player, newPos );

  return player;
}

////////////////////////////////////////////////////////////////////////
// Check treasure
const checkTreasure = function(board, player, newPos) {
  var treasure = board.dmap[newPos.row][newPos.col];

  // Collect armory
  player.treasures.push(treasure);
  
  // Remove armory
  board.dmap[newPos.row][newPos.col] = -1;

  // Set player to new position
  setPlayerPos( board, player, newPos );

  return player;
}

const check = {
  "wall": checkWall,
  "door": checkWall,
  "monster": checkMonster,
  "key": checkKey,
  "armory": checkArmory,
  "potion": checkPotion,
  "weapon": checkWeapon,
  "treasure": checkTreasure
}

////////////////////////////////////////////////////////////////////////
// Check if the player has a monster around him and take a hit from it if so
const checkSurroundingMonsters = function( board, player, newPos ) {
  [[-1,0], [1,0], [0,-1], [0,1]].map((offset) => {
    let obj = board.dmap[newPos.row+offset[0]] && board.dmap[newPos.row+offset[0]][newPos.col+offset[1]] || null;
                         
    if( obj && obj !== -1 && obj.type === "monster" ) {
      let playerDamage = Math.round( obj.info.strength / player.stamina * Math.random() * 10 );
      player = modifyPlayerHealth( board, player, null, -playerDamage);               
    }
  });
  
  return player;
}

const HighScoreReducer = function(state, action) {
  
  if( state === undefined ) {
    let state = { 
      table_: localStorage.rogueReactionHighScore && JSON.parse( localStorage.rogueReactionHighScore ) || [0,0,0,0,0,0,0].map(() => ({ "score": "0", "date": "" }))
    }
    
    return state;
  }
  
  switch( action.type ) {
    case SET_HIGHSCORE_ACTION:
      let date = new Date().toISOString().slice(0, 10),
          table_ = [...state.table_, { score: action.payload, date: date }].sort((a,b) => { return a.score < b.score; });
      table_.splice(-1,1);
      
      localStorage.rogueReactionHighScore = JSON.stringify(table_);
        
      return { ...state, table_ };
  }      

  return state;
}


const rootReducer = combineReducers({
  board:      GameBoardReducer,
  highscore:  HighScoreReducer  
});

////////////////////////////////////////////////////////////////////////
// Read dungeon ascii maps and fill it into a 2d array
function fillDungeons() {
  let dungeons = DUNGEONS.map((dungeon) => {
    
    let player = { ...PLAYER },
        walls = [],
        doors = [],
        monsters = [],
        keys = [],
        armories = [],
        potions = [],
        weapons = [],
        treasures = [];
    
    let dmap = dungeon.dmap.map((row, i) => {
      console.assert( row.length === BOARD_WIDTH * 2, "Rows of dungeons must have a width of "+BOARD_WIDTH +". Row " + i + " is "+row.length+"." )
      return row.match(/.{1,2}/g).map( (pair, j) => {
        switch( pair[0] ) {

          // A wall
          case "W":
            console.assert(pair[1]==="-", "Missing - at "+i+"/"+j);
            return {
              type: "wall",
              info: {
                img: dungeon.wallImgs[(i+j)%(dungeon.wallImgs.length)]
              }
            }
          
          // A monster
          case "M":
            var item = {
              type: "monster",
              index: pair[1],
              info: { ...MONSTERS[pair[1]-1] },
              pos: { row: i, col: j }
            }
            monsters.push(item);
            return item;
          
          // A Door
          case "D":
            var item = {
              type: "door",
              index: pair[1],
              info: {
                img: dungeon.doorImgs[(i+j)%(dungeon.doorImgs.length)]
              },
              pos: { row: i, col: j }
            };
            doors.push(item);
            return item;
          
          // A Key
          case "K":
            var item = {
              type: "key",
              index: pair[1],
              info: KEYS[pair[1]-1],
              pos: { row: i, col: j }
            }
             keys.push(item);
            return item;
          
          // An armour
         case "A":
            var item = {
              type: "armory",
              index: pair[1],
              info: ARMORIES[pair[1]-1],
              pos: { row: i, col: j }
            }
             armories.push(item);
            return item;
          
          // A potion
         case "P":
            var item = {
              type: "potion",
              index: pair[1],
              info: POTIONS[pair[1]-1],
              pos: { row: i, col: j }
            }
            potions.push(item);
            return item;
          
          // A Waepon
          case "S":
            var item = {
              type: "weapon",
              index: pair[1],
              info: WEAPONS[pair[1]-1],
              pos: { row: i, col: j }
            }
            weapons.push(item);
            return item;
          
          // A Treasure
          case "T":
            let index = [null,"1","2","3","4","5","6","7","8","9","0","A","B"].indexOf( pair[1] );
            var item = {
              type: "treasure",
              index: index,
              info: TREASURES[index-1],
              pos: { row: i, col: j }
            }
            treasures.push(item);
            return item;
          
          // Player entrance
          case "E":
            player.pos = {
              row: i,
              col: j
            }
            return -1;
          
          default:
            console.assert(pair[1]===" ", "Unknown char at "+i+"/"+j);
            return -1;
        }
      })
    });
    
    return {
      dmap: dmap,
      player: player,
      monster: null,
      wallImgs:  dungeon.wallImgs,
      doorImgs:  dungeon.doorImgs,
      doors: doors,
      walls: walls,
      monsters: monsters,
      keys: keys,
      armories: armories,
      potions: potions,
      weapons: weapons,
      treasures: treasures
    }
  });
  
  return dungeons;
}

// Redux Actions ////////////////////////////////////////////////////////////
const MOVE_PLAYER_ACTION = "MOVE_PLAYER_ACTION",
      MODIFY_HEALTH_ACTION = "MODIFY_HEALTH_ACTION",
      SET_HIGHSCORE_ACTION = "SET_HIGHSCORE_ACTION",
      NEW_GAME_ACTION = "NEW_GAME_ACTION";

function movePlayer(direction) {
  return {
    type: MOVE_PLAYER_ACTION,
    payload: direction
  }
}

function modifyHealth(playerHealth, monsterHealth) {
  return {
    type: MODIFY_HEALTH_ACTION,
    payload: { playerHealth, monsterHealth }
  }
}

function newGame(direction) {
  return {
    type: NEW_GAME_ACTION
  }
}

function setHighScore(score) {
  return {
    type: SET_HIGHSCORE_ACTION,
    payload: score
  }
}


// App ///////////////////////////////////////////////////////////////////////
class App extends Component {
  constructor(props) {
    super(props);
    
    this.state = {
    }
  }

  render() {
    return (
      <div class>
        <div className="board">
          <div className="dungeon-wrapper">
            <GameBoard />
            <Player />
            <Score />
          </div>
        </div>
      </div>
    )
  }
}

ReactDOM.render(<Provider store={createStore(rootReducer)}><App /></Provider>, document.getElementById("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