Pen Settings

HTML

CSS

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

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

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

Behavior

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.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id='root'>
</div>
              
            
!

CSS

              
                @import url("https://fonts.googleapis.com/css?family=VT323");

$color-bg: #000000;
$color-bg2: #555555;
$color-bg3: #aaaaaa;
$color-link: #5555ff;

$color-white: #ffffff;
$color-brightblue: #5555ff;
$color-blue: #0000aa;
$color-brightred: #ff5555;
$color-red: #aa0000;
$color-brightgreen: #55ff55;
$color-green: #00aa00;
$color-yellow: #ffff55;
$color-brown: #aa5500;

$color-easyenemy: $color-brightred;
$color-mediumenemy: $color-red;
$color-bossenemy: #550000;

$font-heading: 32px "VT323", monospace;
$font-body: 18px "VT323", monospace;

$footer-height: 60px;
$footer-padding: 12px;

body,
html,
#root {
	background-color: $color-bg;
	color: $color-white;
	font: $font-body;
	margin: 0;
	width: 100vw;
}

#combatLog {
	font-size: 16px;
	ul {
		li {
			list-style: none;
			&:before {
				color: $color-bg2;
				content: "> ";
			}
		}
		margin: 0;
		padding: 0;
	}
}

#grid {
	display: flex;
	flex-direction: column;
	flex-wrap: nowrap;
	overflow-x: hidden;
	overflow-y: hidden;
	.mapRow {
		display: flex;
		flex-direction: row;
		flex-wrap: nowrap;
		.mapCell {
			font-size: 10px;
			text-align: center;
			display: inline-block;
			height: 10px;
			width: 10px;
		}
		.notvisible {
			visibility: hidden;
		}
		.boss {
			background-color: $color-bossenemy;
		}
		.door {
			background-color: $color-brown;
		}
		.easy {
			background-color: $color-easyenemy;
		}
		.floor {
			background-color: $color-brown;
		}
		.fog {
			background-color: $color-bg;
		}
		.healing {
			background-color: $color-brightgreen;
		}
		.item {
			background-color: $color-yellow;
		}
		.medium {
			background-color: $color-mediumenemy;
		}
		.player {
			background-color: $color-white;
		}
		.stairsDown, .stairsUp {
			background-color: $color-yellow;
			color: $color-brown;
		}
		.stairsDown:after {
			content: "▼"
		}
		.stairsUp:after {
			content: "▲"
		}
		.wall, .corner {
			background-color: $color-bg2;
		}
	}
}

#playerInfo {
	font-size: 16px;
	.playerInfoBar {
		align-items: center;
		display: flex;
		.bar {
			align-items: center;
			border: 2px solid $color-bg2;
			display: flex;
			height: 20px;
			margin: 0 auto;
			padding: 0;
			position: relative;
			width: 200px;
			.remaining,
			.missing {
				height: 100%;
				margin: 0;
				z-index: 5;
			}
			.remaining {
				left: 0;
				top: 0;
				bottom: 0;
			}
			.missing {
				top: 0;
				right: 0;
				bottom: 0;
			}
			.infoText {
				background-color: transparent;
				position: absolute;
				text-align: center;
				width: 100%;
				z-index: 10;
			}
		}
		.hp {
			.remaining {
				background-color: $color-red;
			}
			.missing {
				background-color: $color-bg;
			}
		}
		.xp {
			.remaining {
				background-color: $color-brightblue;
			}
			.missing {
				background-color: $color-bg;
			}
		}
	}
	.playerLevel {
		font-size: 20px;
		margin-bottom: 10px;
	}
	.levelInfo {
		font-size: 16px;
	}
	.playerName {
		font-size: 24px;
	}
	p {
		margin: 5px;
	}
}

#main {
	align-items: stretch;
	display: flex;
	height: 100%;
	margin: 0;
}

#center {
	align-items: center;
	border: 2px solid $color-bg3;
	display: flex;
	flex-grow: 1;
	justify-content: space-around;
	margin: 8px 0;
	overflow-x: hidden;
	overflow-y: hidden;
}
#center,
.panel {
	border-radius: 2px;
	padding: 8px;
	overflow-y: auto;
}
.panel {
	border: 2px solid $color-bg2;
	margin: 8px;
	width: 300px;
}

#wrapper {
	display: flex;
	flex-direction: column;
	height: 100vh;
	.footer {
		flex-grow: 0;
		.author {
			a:hover {
				text-decoration: underline;
			}
			.button-group {
				display: flex;
				.button {
					color: #ffffff;
					padding: 8px 4px 4px 0;
				}
			}
		}
		.heading {
			font: $font-heading;
		}
		display: flex;
		height: $footer-height;
		justify-content: space-between;
		padding: $footer-padding;
	}
	#main {
		flex-grow: 1;
	}
	a {
		color: $color-link;
		text-decoration: none;
	}
}

              
            
!

JS

              
                /*-------TODO-------
Implement simple one-slot inventory for weapons
Spawn weapons in a new map
Allow user to choose to pick up a new weapon or ignore it
Add more monster families
Change current hp/attack system into stat system (str dex con)
Implement dodge/chance to hit to combat
Generate equipment and inventory
Update UI - inventory screen, stat screen, enemy screen
When leveled up, allow points to be allocated to stat increases
--------------------*/

console.olog = function(o) {
	console.log(JSON.parse(JSON.stringify(o)));
};

const my = {
	site: "https://iamlewis.com",
	github: "https://github.com/Lewis65",
	freecodecamp: "https://freecodecamp.com/lewis65"
};

//Map gen settings
const viewSize = 7,
	maxViewSize = 12,
	mapSize = 30,
			
	roomWidthMin = 4,
	roomWidthMax = 10,
	roomHeightMin = 4,
	roomHeightMax = 10,
	minRooms = 1,
	maxRooms = 5,
	maxAttempts = 5;

//Monster gen and scaling settings
const monsterDensity = 0.05, //Range 0-1, suggest 0.01-0.2
	monsterDensityVariation = 0.5, //Range 0-1 - how much the monster/floor tile density can vary
	monsterScaling = 1.15, //Based around 1 - the amount monsters scale up per lvl
			
	godMode = false, //for testing or otherwise cheating!
			
	baseHp = 50, //Base hp multiplies by level and varies by hpVariation
	hpPerLevel = 1.17,
	hpVariation = 0.2,
			
	baseAttack = 10, 
	attackPerLevel = 1.13,
	attackVariation = 0.05,
			
	baseArmor = 0, //Gets added to monster armor
	armorVariation = 0.2,
	armorCap = 0.8, //Max damage reduction
	armorForInvincibility = 1000, //The equivalent of 100% damage reduction
			
	damageVariation = 0.4; //The amount that damage for a given hit varies from the attack stat

//Levels and scaling
const lvlRange = {easy: [-2, 1], medium: [-1, 2], boss: [0, 3]}, //Range of monsters' level scaling relative to player's
			xpModifier = {easy: 0.9, medium: 1.1, boss: 1.6}, //Modifier for xp gain based on monster tier
			xpRequiredPerLevel = 1.2, //xp required for next level increase per previous level
			xpVariation = 0.1, //Variation of xp received for given victory
			hpToXp = 0.6; //Ratio of xp gained per hp of monster slain

//Healing item and equipment spawn variables
const healingItemDensity = 0.025, //How many healing items per open tile
			healingItemRarity = 0.2; //How likely it is to spawn a rare item rather than a common item

//Healing items and their properties
const healingItems = {
	common: [
		{
			name: "a donut",
			heals: 0.1},
		{
			name: "a slice of pizza",
			heals: 0.15
		},
		{
			name: "some candy",
			heals: 0.05
		},
		{
			name: "precious cake",
			heals: 0.2
		}
	],
	rare: [
		{
			name: "a potion",
			heals: 0.4
		},
		{
			name: "a super potion",
			heals: 0.8
		}
	]
}

//All items (weapons/equipment) and their iLvl-weighted potential properties
const items = {
	slots: [],
	bonuses: [],
	
}

//All possible 'themes' of monster and their modifiers
const monsters = [
	{name: "Spider",
	easy: [
		{
			name: "Spider Hatchling",
			hp: 0.3,
			attack: 0.4,
			armor: 0
		},
		{
			name: "Young Spiderling",
			hp: 0.4,
			attack: 0.5,
			armor: 0.1
		}
	],
	medium: [
		{
			name: "Spider Whelp",
			hp: 0.5,
			attack: 0.4,
			armor: 0.2
		},
		{
			name: "Arachnid Soldier",
			hp: 0.7,
			attack: 0.4,
			armor: 0.3
		}
	],
	boss: [
		{
			name: "Spider Matriarch",
			hp: 0.85,
			attack: 0.8,
			armor: 0.3
		},
		{
			name: "Mutated Arachnoid",
			hp: 0.6,
			attack: 1.4,
			armor: 0.1
		}
	]}
];

class App extends React.Component {
	constructor(props) {
		super(props);

		this.state = {
			combatLog: [
				"Welcome to React Roguelike.", 
				"This combat log will show you events and messages as you play.", 
				"Use WASD to move and fight monsters by walking into them.", 
				"Good luck!"
			],
			player: {
				id: 0,
				x: null,
				y: null,
				name: "John",
				level: 1,
				type: "player",
				gear: {
					head: undefined,
					chest: undefined,
					legs: undefined,
					feet: undefined,
					hands: undefined,
					mainhand: undefined,
					offhand: undefined
				},
				inv: new Array(16),
				hp: 100,
				hpmax: 100,
				attack: baseAttack,
				armor: baseArmor,
				xp: 0,
				xpmax: 100
			},
			depth: 1,
			levels: [],
			map: this.generateBlankMap(mapSize),
			monsterCount: undefined,
			npcs: [],
			visibleMap: [[{state: "rock"}]]
		};
	}
	
	//==========HANDLERS==========
	handleKeyDown(event){
		if(this.state.player.hp == 0){
			return;
		}
		let nextMap;
		if(event.key == "w"){
			nextMap = this.moveEntity(this.state.player, "n");
		} else
		if(event.key == "a"){
			nextMap = this.moveEntity(this.state.player, "w");
		} else
		if(event.key == "s"){
			nextMap = this.moveEntity(this.state.player, "s");
		} else
		if(event.key == "d"){
			nextMap = this.moveEntity(this.state.player, "e");
		}
	}
	
	
	//==========LOGIC==========
	combat(a, b, opportunityAttack = false){
		let attacker, defender;
		
		//Decide attack order
		if(opportunityAttack){
			attacker = a;
			defender = b;
		} else {
			if(Math.random() > 0.5){
				attacker = a;
				defender = b;
			} else {
				attacker = a;
				defender = b;
			}
		}

		console.log("combat(): attacker and defender", attacker, defender);
		let attackerDamage = attacker.attack
		attackerDamage += attackerDamage*((Math.random()+Math.random())-1)*damageVariation;
		attackerDamage *= 1-(defender.armor/armorForInvincibility);
		console.log("The attacker, " + attacker.name + ", hits the " + defender.name + " with " + attackerDamage + " points of damage.");
		attackerDamage = Math.round(attackerDamage);
		if(attackerDamage < 0){attackerDamage = 0;}
		defender.hp -= attackerDamage;
		
		if(opportunityAttack){
			this.log("As you move away from the " + attacker.name + ", you are hit for " + attackerDamage + ".");
		} else {
			this.log(attacker.name + " hits " + defender.name + " for " + attackerDamage + " damage!")
		}
		
		if(defender.hp > 0 && !opportunityAttack) {
			//If defender didn't die, and is not fleeing, strike back
			let defenderDamage = defender.attack
			defenderDamage += defenderDamage*((Math.random()+Math.random())-1)*damageVariation;
			defenderDamage *= 1-(attacker.armor/armorForInvincibility);
			console.log("The defender, " + defender.name + ", hits the " + attacker.name + " with " + defenderDamage + " points of damage.");
			defenderDamage = Math.round(defenderDamage);
			if(defenderDamage < 0){defenderDamage = 0;}
			attacker.hp -= defenderDamage;
			this.log(defender.name + " hits " + attacker.name + " for " + defenderDamage + " damage!")
		
			if(attacker.hp < 0){
				//If attacker gets negative hp
				attacker.hp = 0;
			}
		}
		if (defender.hp < 0){
			//If defender dies
			defender.hp = 0;
		}
		let result = {};
		if(attacker.id == this.state.player.id){
			result.player = attacker;
			result.monster = defender;
		} else if (defender.id == this.state.player.id){
			result.player = defender;
			result.monster = attacker;
		} else {
			console.log("ERR: combat() could not find player's ID. attacker:",
								 attacker,
								 "defender:",
								 defender);
			return;
		}
		
		if(godMode){
			result.player.hp = result.player.hpmax;
		}
		
		//If something dies
		if(result.monster.hp == 0){
			this.log(result.monster.name + " was slain!");
		}
		if(result.player.hp == 0){
			this.log("You fall to your knees, gasping in agony. Mercifully, " + result.monster.name + " finishes you off.");
			this.log("You are slain.");
		}
		
		return result;
	}
	
	gainXp(player, monster){
		let xp = monster.hpMax * hpToXp * xpModifier[monster.type];
		xp += xp*(Math.random()+Math.random()-1)*xpVariation;
		xp = Math.round(xp);
		this.log("You gain " + xp + "xp.")
		player.xp += xp;
		if(player.xp >= player.xpmax){
			player.xp = player.xp - player.xpmax;
			player.level++;
			player.hpmax = Math.round(player.hpmax * hpPerLevel);
			player.hp = player.hpmax;
			player.attack = Math.round(player.attack * attackPerLevel);
			player.xpmax = Math.round(player.xpmax * xpRequiredPerLevel);
			this.log("You have reached level " + player.level + "!");
		}
		return player;
	}
	
	//Generate an empty map of solid rock
	generateBlankMap(size) {
		let map = [];
		for (var i = 0; i < size; i++) {
			let row = [];
			for (let j = 0; j < size; j++) {
				row.push({
					state: "rock",
					x: j,
					y: i
				});
			}
			map.push(row);
		}
		return map;
	}

	generateMap() {
		
		let player = this.state.player;
		let npcs = [];
		
		//generate a blank map
		let map = this.generateBlankMap(mapSize);
		
		//fill the blank map with rooms and walls
		map = this.generateRooms(map);
		
		//populate player
		let spawnPlayer = this.spawn("player", player, map, npcs);
		player = spawnPlayer.player;
		map = spawnPlayer.map;
		
		//populate monsters
		let spawnMonsters = this.spawnMonsters(map);
		map = spawnMonsters.map;
		npcs = npcs.concat(spawnMonsters.npcs);
		
		//populate items
		map = this.spawnHealingItems(map);
		
		//update the visible portion of the map
		let visible = this.updateVisibleMap(map, player.x, player.y);
		return{
			player: player,
			map: map,
			monsterCount: spawnMonsters.monsterCount,
			npcs: npcs,
			visibleMap: visible
		};
	}

	//Generate one room
	generateRoom() {
		let width = this.random(roomWidthMin, roomWidthMax);
		let height = this.random(roomHeightMin, roomHeightMax);
		let room = [];
		for (let y = 0; y < height + 2; y++) {
			let row = [];
			for (let x = 0; x < width + 2; x++) {
				if (y == 0 || y == height + 1 || x == 0 || x == width + 1) {
					if (
						(y == 0 && x == 0) ||
						(y == 0 && x == width + 1) ||
						(y == height + 1 && x == 0) ||
						(y == height + 1 && x == width + 1)
					) {
						//Push a corner for the wall corners
						row.push({ state: "corner" });
					} else {
						//Push a wall if it's on the edges
						row.push({ state: "wall" });
					}
				} else {
					//Else push a floor
					row.push({ state: "floor" });
				}
			}
			room.push(row);
		}
		//console.log("Generating room " + width + " wide and " + height + " high, not including walls:");
		//console.olog(room);
		return room;
	}

	//Generate all rooms
	generateRooms(blankMap) {
		//randomize roomCount
		let roomCount = this.random(minRooms, maxRooms);
		let roomsPlaced = 0;
		console.log("The roomCount will be " + roomCount);

		let map = blankMap;
		console.log("Setting map to blank map...");
		this.setState({ map: map });

		//generate rooms until a room count or number of attempts reached
		//generate room, find possible placement or retry, place room, set as new map
		//for every room
		console.log("Starting room generation...");
		for (let i = 0; i < roomCount; i++) {
			//generate a new room
			let room = this.generateRoom();
			//set roomPlaced to false and try to place while it's false
			let roomPlaced = false;
			let placeAttempts = 0;
			let nextMap = map;
			while (roomPlaced == false && placeAttempts < maxAttempts) {
				//run placeroom and return bool
				//console.log("Attempting to place room... (attempt " + placeAttempts + ")")
				let placeRoom = this.placeRoom(room, i, map);
				roomPlaced = placeRoom.success;
				nextMap = placeRoom.nextMap;
				//console.olog(roomPlaced);
				placeAttempts++;
			}
			if (roomPlaced) {
				//console.log("Room placed successfully. nextMap:");
				//console.olog(nextMap);
				roomsPlaced++;
				map = nextMap;
			} else if (placeAttempts == maxAttempts) {
				//console.log("Failed to place room: Max attempts reached.");
			} else {
				//console.log("Room was not placed.");
			}
			//console.log("Finished iteration " + i + " of for loop in generateRooms. Here is map:");
			//console.olog(map);
		}

		console.log("generateRooms complete.");
		console.olog(map);
		console.log("Placed " + roomsPlaced + " rooms of goal of " + roomCount);
		console.log("Cleaning doors...");

		//Clean up failed doors
		let doors = this.scanMapFor("door", map);
		//console.log("Here's all the doors:", doors);
		for (let i = 0; i < doors.length; i++) {
			//If a door does not connect 2+ floor tiles, revert it to a wall
			let adjacent = this.getAdjacentCells(map, doors[i].x, doors[i].y);
			//console.log("Here are the neighbors of the door at x:"+doors[i].x+" y:"+doors[i].y);
			//console.olog(adjacent);
			let floors = 0;
			for (let j = 0; j < adjacent.length; j++) {
				if (adjacent[j].state == "floor") {
					floors++;
				}
			}
			if (floors < 2) {
				map[doors[i].y][doors[i].x].state = "wall";
			}
		}

		return map;
	}

	//return the 4 adjacent cells
	getAdjacentCells(map, x, y) {
		let adjacent = [];
		if (y < map.length - 1) {
			let cell = map[y + 1][x];
			cell.dir = "s";
			adjacent.push(cell);
		}
		if (x < map[0].length - 1) {
			let cell = map[y][x + 1];
			cell.dir = "e";
			adjacent.push(cell);
		}
		if (y > 0) {
			let cell = map[y - 1][x];
			cell.dir = "n";
			adjacent.push(cell);
		}
		if (x > 0) {
			let cell = map[y][x - 1];
			cell.dir = "w";
			adjacent.push(cell);
		}
		return adjacent;
	}
	
	goToLevel(depth){
		
		if(depth < 1){
			console.log("ERR: goToLevel(): Attempted to find level for depth " + depth);
			return;
		}
		
		let combatLog = this.state.combatLog;
		let levels = this.state.levels;
		let nextState;
		//Store the player's current state
		let currentPlayer = this.state.player;
		//Store the current level information
		let currentLevel = {
			depth: this.state.depth,
			map: this.state.map,
			npcs: this.state.npcs,
			stairs: {
				up: {
					x: undefined,
					y: undefined
				},
				down: {
					x: undefined,
					y: undefined
				}
			}
		};
		//Pick the player off of the current level's map
		currentLevel.map[currentPlayer.y][currentPlayer.x].entity = undefined;
		
		if(depth > this.state.levels.length){
			//Going to a new depth
			nextState = this.generateMap();
			console.log("Didn't find previously created level for depth " + depth + ".")
			combatLog.push("You climb down the stairs, pushing further into the depths.")
			console.log("Generated nextLevel:");
			console.olog(nextState);
			nextState.map[nextState.player.y][nextState.player.x].state = "stairsUp";
		} else {
			//Previously generated level
			nextState = this.state.levels[depth-1];
			console.log("Retrieved level for nextState:");
			console.olog(nextState);
			console.log("Found previously created level for depth " + depth + ".");
			combatLog.push("You return to floor " + depth + ".");
			
			//Place the player in the next level at the appropriate staircase
			nextState.player = currentPlayer;
			console.log("stairs for nextState");
			console.olog(nextState.stairs);
			if(depth > this.state.depth){
				//Going down to a previously visited level
				console.log("going down");
				nextState.player.y = nextState.stairs.up.y;
				nextState.player.x = nextState.stairs.up.x;
			} else if (depth < this.state.depth){
				//Going up to a previously visited level
				console.log("going up");
				nextState.player.y = nextState.stairs.down.y;
				nextState.player.x = nextState.stairs.down.x;
			} else {
				console.log("ERR: goToLevel(): Attempted to go to same depth as current depth")
			}
			nextState.map[nextState.player.y][nextState.player.x].entity = nextState.player;
		}

		console.log("Updating visiblemap");
		console.olog(nextState);
		nextState.visibleMap = this.updateVisibleMap(nextState.map, nextState.player.x, nextState.player.y);
		nextState.levels = this.state.levels;
		nextState.depth = depth;
		nextState.monsterCount = nextState.npcs.length;
		
		//Save the current level along with stair info in state.levels
		console.log("currentLevel from which stairs are saved")
		console.olog(currentLevel);
		let stairsUp = this.scanMapFor("stairsUp", currentLevel.map);
		if(stairsUp){currentLevel.stairs.up = stairsUp[0]}
		let stairsDown = this.scanMapFor("stairsDown", currentLevel.map);
		if(stairsDown){currentLevel.stairs.down = stairsDown[0]}
		
		console.log("Storing currentLevel:");
		console.olog(currentLevel);
		
		levels[currentLevel.depth - 1] = currentLevel;
		nextState.levels = levels;
		
		//Update state to display nextLevel stuff
		console.log("goToLevel(): Set nextState to:");
		console.log(nextState);
		
		this.setState(nextState);
	}
	
	log(message){
		let log = this.state.combatLog;
		log.push(message);
		this.setState({combatLog: log});
	}
	
	moveEntity(entity, d = null){
		let x = entity.x;
		let y = entity.y;
		let dirs = ["n", "e", "s", "w"];
		if(d !== null && dirs.indexOf(d) == -1){
			console.log("ERR: moveEntity(): Called with invalid d:" + d);
			return;
		}
		
		let dir = d || dirs[this.random(0,3)];

		let targetCoord = {
			x: x,
			y: y
		}
		switch(dir){
			case "n":
				targetCoord.y--;
				break;
			case "s":
				targetCoord.y++;
				break;
			case "e":
				targetCoord.x++;
				break;
			case "w":
				targetCoord.x--;
				break;
			default:
				console.log("ERR: moveEntity(): Error in switch statement. dir:" + dir);
		}
		let targetCell = this.state.map[targetCoord.y][targetCoord.x];
		
		if(!(this.state.map[y] && this.state.map[y][x])){
			console.log("ERR: moveEntity(): The coordinates provided to moveEntity do not exist on the map in state. y:"+y+" x:"+x);
			return;
		} else if (!this.state.map[y][x].entity){
			console.log("ERR: moveEntity(): No entity exists at y:"+y+" x:"+x);
			return;
		}
		let entityMoving = this.state.map[y][x].entity;
		console.log("Attempting to move the entity " + entityMoving + " to x:" + targetCoord.x + " y:" + targetCoord.y + ". Target cell is:", targetCell);
		
		if(targetCell.state === "stairsUp"){
			console.log("Walked to stairs up at x:" + targetCoord.x + " y:" + targetCoord.y)
			this.goToLevel(this.state.depth-1);
			return;
		}
		if(targetCell.state === "stairsDown"){
			console.log("Walked to stairs down at x:" + targetCoord.x + " y:" + targetCoord.y)
			this.goToLevel(this.state.depth+1);
			return;
		}
		
		let nextMap = this.state.map;
		let player = this.state.player;
		let nextNpcs = this.state.npcs;
		let monsterCount = this.state.monsterCount;
		
		if(targetCell.state !== "floor" && targetCell.state !== "door"){
			//If it is a non-traversable cell
			console.log("Can't move there.");
			return;
		}
		if(targetCell.entity){
			//If there's another entity there
			if(targetCell.entity.type == "player" || entityMoving.type == "player"){
				//and one of them is the player
				let combat = this.combat(targetCell.entity, entityMoving);
				
				//Then update the post-combat stats for player and monster in state
				for(let m = 0; m < nextNpcs.length; m++){
					if(nextNpcs[m].id == combat.monster.id){
						if(combat.monster.hp == 0){
							//If the monster died
							combat.player = this.gainXp(combat.player, combat.monster);
							nextNpcs.splice(m, 1);
							nextMap[combat.monster.y][combat.monster.x].entity = undefined;
							monsterCount--;
							if(combat.monster.type == "boss"){
								this.log("A way down appears.");
								nextMap[combat.monster.y][combat.monster.x].state = "stairsDown";
							}
						} else {
							//If it's still alive
							nextNpcs[m] = combat.monster;
							nextMap[combat.monster.y][combat.monster.x].entity = combat.monster;
						}
						break;
					}
				}
				console.log("Combat with " + combat.monster.name + " with id " + combat.monster.id + ". combat:");
				console.log(combat);
				console.log("nextNpcs after combat()", nextNpcs);
				player = combat.player;
				console.log("And the player:", player);
			}
		} else {
			//There's no entity there
			if(entityMoving.type == "player"){
				let adjacentCells = this.getAdjacentCells(nextMap, player.x, player.y);
				for(let i = 0; i < adjacentCells.length; i++){
					if(player.hp == 0){
						//Break if already killed (used in multiple attacks of opportunity)
						break;
					}
					if(adjacentCells[i].entity !== undefined){
						let opportunityAttack = this.combat(adjacentCells[i].entity, player, true);
						player = opportunityAttack.player;
					}
				}
				if(player.hp > 0){
					//Move if they didn't die to an opportunity attack
					player.x = targetCoord.x;
					player.y = targetCoord.y;
				} else {
					return;
				}
			} else {
				let id = entityMoving.id;
				let index = nextNpcs.indexOf(entityMoving);
				nextNpcs[index].y = targetCoord.y;
				nextNpcs[index].x = targetCoord.x;
			}
			
			if(entityMoving.type == "player" && nextMap[targetCoord.y][targetCoord.x].item){
				//If the player is moving to a tile with an item
				let item = nextMap[targetCoord.y][targetCoord.x].item;
				
				if(item.type == "healing"){
					nextMap[targetCoord.y][targetCoord.x].item = undefined;
					let healsFor = Math.round(player.hpmax * item.heals);
					console.log("The " + item.name + " heals for " + healsFor + ". player.hp:" + player.hp + "player.hpmax:" + player.hpmax);
					if(healsFor > (player.hpmax - player.hp)){
						healsFor = (player.hpmax - player.hp);
					}
					player.hp += healsFor;
					this.log("You pick up " + item.name + ". It heals " + healsFor + "hp.");
				}
				
				//PUT ELSE FOR EQUIPMENT ITEMS HERE
			}
			
			nextMap[targetCoord.y][targetCoord.x].entity = entityMoving;
			nextMap[y][x].entity = undefined;
		}
		
		let nextVisibleMap = this.updateVisibleMap(nextMap, player.x, player.y);
		this.setState({
			map: nextMap,
			player: player,
			monsterCount: monsterCount,
			npcs: nextNpcs,
			visibleMap: nextVisibleMap
		})
		
	}

	//attempt to place a room
	placeRoom(r, i, map) {
		//length of first row
		//let roomWidth = room[0].length;
		//number of rows
		//let roomHeight = room.length;
		let nextMap = map;

		//Set variables for the start coords of room (top-left corner)
		//let startingX, startingY;

		let room = {
			//leftmost and rightmost value for possible floor placement, does not include walls
			minX: null,
			maxX: null,
			//topmost and bottommost value for possible floor placement, does not include walls
			minY: null,
			maxY: null,
			//coords at which the topleft corner of the room will be placed, including walls
			startingX: null,
			startingY: null,
			//width and height, including walls
			width: r[0].length,
			height: r.length,
			//number of the room
			number: i,
			//actual cells of the room
			map: r,
			//if the room is placeable
			isPlaceable: false
		};

		//find coords for a door and set the room coords
		if (room.number == 0) {
			//First room to be placed
			room.startingX = this.random(0, mapSize - room.width);
			room.startingY = this.random(0, mapSize - room.height);
			//console.log("Placing first room.", "Room is " + room.width + " wide and " + room.height + " high including walls:");
			//console.olog(room.map);
			//console.log("Placing at x:" + room.startingX + " y:" + room.startingY);
			room.isPlaceable = true;
		} else {
			//Non-first rooms
			//find all non-corner walls and choose a random one for a door
			let door = this.placeDoor(map);
			//console.log("Door placed: ", door)
			if (door.success) {
				nextMap[door.y][door.x].state = "door";
				//after placing a door but before adding the room on
				//find direction to go out the room through the door
				//search 4 adjacent tiles and find 1 rock - if no rock find a new door position
				let adjacent = this.getAdjacentCells(nextMap, door.x, door.y);
				let neighborCoords = [
					{ x: door.x, y: door.y - 1, dir: "n" },
					{ x: door.x - 1, y: door.y, dir: "w" },
					{ x: door.x, y: door.y + 1, dir: "s" },
					{ x: door.x + 1, y: door.y, dir: "e" }
				];
				//check each y and x exists in case the room is on the edge of the map
				let dir = null;
				for (let i = 0; i < neighborCoords.length; i++) {
					//if the cell is within the map
					if (
						nextMap[neighborCoords[i].y] !== undefined &&
						nextMap[neighborCoords[i].x] !== undefined
					) {
						//and if it's an empty tile
						if (nextMap[neighborCoords[i].y][neighborCoords[i].x].state == "rock") {
							//save a string to show the direction out of the room
							dir = neighborCoords[i].dir;
						}
					}
				}

				//check bounds of space for the room in that dir
				//console.log("Dir is: ", dir)
				if (!dir) {
					//first, if no way out of this door to empty rock tile, return fail
					//console.log("ERR: Did not find empty tile adjacent to door:", door);
					//console.log("Reverting to map:", map);
					return { success: false, nextMap: map };
				} else {
					//console.log("Setting initial min/max for x/y - this will not include walls");
					//console.log("room.height:" + room.height + " room.width:" + room.width + ", including walls");
					if (dir == "w" || dir == "e") {
						//Primary x, secondary y
						//The furthest the floor can potentially reach given room size and door connectivity.
						room.yMin = door.y - (room.height - 2) + 1; //
						room.yMax = door.y + (room.height - 2) - 1; //
						if (dir == "w") {
							room.xMin = door.x - (room.width - 2); //
							room.xMax = door.x - 1; //
						} else if (dir == "e") {
							room.xMin = door.x + 1; //
							room.xMax = door.x + (room.width - 2); //
						}
						//check primary axis
						if (room.xMin < 1 || room.xMax > mapSize - 2) {
							//console.log("ERR: No room on primary (x) axis.");
							return { success: false };
						}
					} else if (dir == "n" || dir == "s") {
						//Primary y, secondary x
						//The furthest the floor can potentially reach given room size and door connectivity.
						room.xMin = door.x - (room.width - 2) + 1; //
						room.xMax = door.x + (room.width - 2) - 1; //
						if (dir == "n") {
							room.yMin = door.y - (room.height - 2); //
							room.yMax = door.y - 1; //
						} else if (dir == "s") {
							room.yMin = door.y + 1; //
							room.yMax = door.y + (room.height - 2); //
						}
						//check primary axis
						if (room.yMin < 1 || room.yMax > mapSize - 2) {
							//console.log("ERR: No room on primary (y) axis.")
							return { success: false };
						}
					}
					//console.log("xMin:" + room.xMin + " xMax:" + room.xMax + " yMin:" + room.yMin + " yMax:" + room.yMax);

					//Check x/y min/max against constraints of map
					//Off by one to allow for wall
					if (room.xMin < 1) {
						room.xMin = 1;
					}
					if (room.xMax > mapSize - 2) {
						room.xMax = mapSize - 2;
					}
					if (room.yMin < 1) {
						room.yMin = 1;
					}
					if (room.yMax > mapSize - 2) {
						room.yMax = mapSize - 2;
					}
					//console.log("Constrained to 0/mapSize:");
					//console.log("xMin:" + room.xMin + " xMax:" + room.xMax + " yMin:" + room.yMin + " yMax:" + room.yMax);

					//Check x/y min/max against min requirements of the room
					if (room.xMax - room.xMin + 1 < room.width - 2) {
						//console.log("ERR: x range available is too small for room:")
						//console.olog(room.map);
						//console.log("room.width:"+room.width+" xMin:"+room.xMin+" xMax:"+room.xMax+", thus max size with walls:"+(room.xMax-room.xMin + 3))
						return {
							success: false
						};
					}
					if (room.yMax - room.yMin + 1 < room.height - 2) {
						//console.log("ERR: y range available is too small for room:")
						//console.olog(room.map);
						//console.log("roomHeight:"+room.height+" yMin:"+room.yMin+" yMax:"+room.yMax+", thus max size with walls:"+(room.yMax-room.yMin + 3))
						return {
							success: false
						};
					}

					//Loop through primary axis (go straight from door)
					if (dir == "w" || dir == "e") {
						let yMin = room.yMin;
						let yMax = room.yMax;
						//Find min and max values on the y axis at each x
						for (let x = room.xMin; x < room.xMax; x++) {
							if (nextMap[door.y][x].state == "floor") {
								//console.log("ERR: Found a floor tile on primary axis at x:"+x+" y:"+door.y+", must abandon room.");
								return {
									success: false
								};
							}
							for (let y = room.yMin; y < door.y - 1; y++) {
								if (nextMap[y][x].state == "floor") {
									yMin = y + 1;
									//console.log("Found floor tile at x:"+x+" y:"+y+", new yMin is "+yMin);
								}
							}
							for (let y = room.yMax; y > door.y + 1; y--) {
								if (nextMap[y][x].state == "floor") {
									yMax = y - 1;
									//console.log("Found floor tile at x:"+x+" y:"+y+", new yMax is "+yMax);
								}
							}
						}
						room.yMin = yMin;
						room.yMax = yMax;
						//Choose a random startpoint on secondary axis
						let highestMin = door.y;
						if (door.y + room.height - 3 > room.yMax) {
							highestMin = room.yMax - (room.width - 2);
						}
						let rand = this.random(yMin, highestMin);
						//starting includes walls, min does not
						room.startingY = rand - 1;
						room.startingX = room.xMin - 1;
					} else if (dir == "n" || dir == "s") {
						let xMin = room.xMin;
						let xMax = room.xMax;
						//Primary y, secondary x
						for (let y = room.yMin; y < room.yMax; y++) {
							if (nextMap[y][door.x].state == "floor") {
								//console.log("ERR: Found a floor tile on primary axis at x:"+door.x+" y:"+y+", must abandon room.");
								return {
									success: false
								};
							}
							for (let x = room.xMin; x < door.x - 1; x++) {
								if (nextMap[y][x].state == "floor") {
									xMin = x + 1;
									//console.log("Found floor tile at x:"+x+" y:"+y+", new xMin is "+xMin);
								}
							}
							for (let x = room.xMax; x > door.x + 1; x--) {
								if (nextMap[y][x].state == "floor") {
									xMax = x - 1;
									//console.log("Found floor tile at x:"+x+" y:"+y+", new xMax is "+xMax);
								}
							}
						}
						room.xMin = xMin;
						room.xMax = xMax;
						//Choose a random startpoint on secondary axis
						let highestMin = door.x;
						if (door.x + room.width - 3 > xMax) {
							highestMin = xMax - (room.width - 2);
						}
						let rand = this.random(xMin, highestMin);
						//starting includes walls, min does not
						room.startingX = rand - 1;
						room.startingY = room.yMin - 1;
					}

					//Re-check all tiles for room placement
					//console.log("Will check tiles x:"+room.startingX+"-"+(room.startingX+(room.width-1))+
					//						" y:"+room.startingY+"-"+(room.startingY+(room.height-1)));
					room.isPlaceable = true;
					for (let y = room.startingY; y < room.startingY + (room.height - 1); y++) {
						for (let x = room.startingX; x < room.startingX + (room.width - 1); x++) {
							//console.log("Checking cell x:"+x+" y:"+y);
							if (
								typeof nextMap[y] == "undefined" ||
								typeof nextMap[y][x] == "undefined"
							) {
								//console.log("ERR: Undefined cell in tile check! x:"+x+" y:"+y)
								room.isPlaceable = false;
							} else {
								if (nextMap[y][x].state == "floor") {
									room.isPlaceable = false;
									//console.log("ERR: Found floor in proposed roomspace at x"+x+" y"+y+".")
								}
							}
						}
					}
					//console.log("Check complete. room.isPlaceable is " + room.isPlaceable);
				}
			} else {
				//what to do if door could not be placed
				//console.log("ERR: Could not place door. (placeDoor returned success:false)")
				return {
					success: false
				};
			}
		}

		//Replace empty tiles with room tiles
		if (room.isPlaceable) {
			for (let y = 0; y < room.height; y++) {
				for (let x = 0; x < room.width; x++) {
					let mapX = room.startingX + x;
					let mapY = room.startingY + y;
					if (
						typeof nextMap[mapY] == "undefined" ||
						typeof nextMap[mapY][mapX] == "undefined"
					) {
						console.log(
							"ERR: Undefined cell in replacement block at x:" +
								(room.startingX + x) +
								" y:" +
								(room.startingY + y)
						);
						return {success: false};
					}
					if (nextMap[mapY][mapX].state !== "rock") {
						//console.log("Skipping "+nextMap[mapY][mapX].state+" at x:"+mapX+" y:"+mapY+".");
						continue;
					}
					let cell = room.map[y][x];
					cell.roomNumber = room.number;
					nextMap[mapY][mapX] = cell;
					//console.log("Placing " + JSON.stringify(cell) + " at x:" + (mapX) + " y:" + (mapY) +".");
				}
			}
			console.log("Room " + room.number + " placed! nextMap:");
			console.olog(nextMap);
			return { success: true, nextMap: nextMap };
		} else {
			console.log(
				"ERR: room.isPlaceable is " + room.isPlaceable + ". Room not placed."
			);
			return { success: false };
		}
	}

	placeDoor(map) {
		//an array of coord pairs {x:int, y:int} of all non-corner walls in map
		let doorPlaced = false;
		let walls = this.scanMapFor("wall", map);
		//console.log("Found walls at:", walls, typeof walls);
		if (typeof walls !== "object" || walls.length < 1) {
			//error handling
			console.log("ERR: Did not find walls to place door.");
			return { success: false };
		}
		//if walls are found
		let n = this.random(0, walls.length - 1);
		let x = walls[n].x;
		let y = walls[n].y;
		//console.log("Placing door at y"+y+" x"+x+".");
		let nextMap = map;
		return {
			x: x,
			y: y,
			success: true
		};
	}

	//return a random int between min and max (inclusive)
	random(min, max) {
		return Math.floor((max - min + 1) * Math.random()) + min;
	}
	
	//apply a monster's modifiers and build their stats
	randomizeMonster(monster, tier){
		//Using Object.assign will prevent passing a reference to the $monster object,
		//instead simply assigning $randomized the same property values.
		let randomized = {};
		Object.assign(randomized, monster);		
		
		randomized.level = this.random(lvlRange[tier][0], lvlRange[tier][1]) + this.state.player.level;
		if(randomized.level < 1){
			randomized.level = 1;
		}
		
		let hp = monster.hp * baseHp;
		hp += hp*((Math.random()*hpVariation)*(randomized.level*monsterScaling));
		if (hp < 1) {hp = 1;}
		randomized.hp = Math.round(hp);
		randomized.hpMax = randomized.hp;
		
		let attack = monster.attack * baseAttack;
		attack += attack*((Math.random()*attackVariation)*(randomized.level*monsterScaling));
		if (attack < 1) {attack = 1;}
		randomized.attack = Math.round(attack);
		
		let armor = monster.armor + baseArmor;
		let randomArmor = Math.random()+Math.random()-1;
		armor += randomArmor*armorVariation*armor;
		randomized.armor = Math.round(armor*armorForInvincibility);
		
		return randomized;
	}

	//Scan a 2d array map for cells with a state and return a list of coords
	scanMapFor(state, map) {
		let coords = [];
		//console.log("Scanning this map for " + state + ":", map);
		for (let y = 0; y < map.length; y++) {
			for (let x = 0; x < map[y].length; x++) {
				if (map[y][x].state == state) {
					coords.push({ x: x, y: y });
				}
			}
		}
		//console.log("Found " + coords.length + " results.");
		return coords;
	}

	//Spawn a single entity
	spawn(type, entity, map, npcs, x = null, y = null) {
		
		let nextMap = map;
		let nextNpcs = npcs;
		let coord = {
			x: x,
			y: y
		}
		
		if (y && x && map[y][x].state == "floor" && !map[y][x][type]) {
			//if passed coords
			entity.x = x;
			entity.y = y;
			nextMap[y][x][type] = entity;
			if(type == "entity"){
				nextNpcs.push(entity);
			}
		} else {
			
			if(y&&x){
				console.log("Passed coords to spawn() but tile was not a floor tile or entity exists there. x:" + x + " y:" + y);
				return;
			}
			let openTiles = this.scanMapFor("floor", map);
			let random;
			
			let attempts = 0;
			let noMoreTilesAvailable = false;
			
			let isNextToPlayer = false;
			do {
				random = this.random(1, openTiles.length)-1;
				coord = openTiles[random];
				
				if (coord == undefined) {
					console.log("ERR: Found no more open floor tiles.");
					noMoreTilesAvailable = true;
					break;
				}
				
				//If there's a player, and spawning a monster, do not spawn it adjacent to player
				isNextToPlayer = false;
				if(type == "monster" && this.state.player.x){
					//console.log("Testing if next to player...");
					//console.olog(this.state.player);
					let player = this.state.player
					if(coord.x == player.x || coord.y == player.y) {
						if(coord.x == player.x + 1 || coord.x == player.x -1) {
							console.log("Skipped spawn location because it was next to the player.", coord);
							isNextToPlayer = true;
						} else if (coord.y == player.y + 1 || coord.y == player.y -1) {
							console.log("Skipped spawn location because it was next to the player.", coord);
							isNextToPlayer = true;
						}
					}
				}
				openTiles.splice(random, 1);
				attempts++;
			} while ((map[coord.y][coord.x].entity || isNextToPlayer) && attempts < 20);

			if (attempts == 20 || noMoreTilesAvailable) {
				console.log(
					"ERR: Max attempts reached or no more open tiles. Cannot spawn this:", entity
				);
				return {
					map: nextMap,
					error: true
				};
			}
			entity.y = coord.y;
			entity.x = coord.x;
			let category = type;
			if(type == "player" || type == "monster"){
				//Categorize player and monsters as entities
				category = "entity";
			}
			nextMap[coord.y][coord.x][category] = entity;
		}
		
		console.log("Spawning " + type + " '" + entity.name + "' at x:" + coord.x + " y:" + coord.y);
		
		if(type == "player") {
			let player = this.state.player;
			player.x = coord.x;
			player.y = coord.y;
			//To be passed to generateMap()
			return {
				player: player,
				map: nextMap
			};
		} else if(type == "monster") {
			//If it's a non-player entity (monster)
			nextNpcs.push(entity);
			return{
				map: nextMap,
				npcs: nextNpcs
			};
		} else if(type == "item") {
			nextMap[coord.y][coord.x].item = entity;
			return {
				map: nextMap
			}
		}
		console.log("ERR: spawn() called with unhandled type: " + type);
		return;
	}
	
	spawnHealingItems(map){
		let nextMap = map;
		let numberToSpawn = this.scanMapFor("floor", nextMap).length*healingItemDensity;
		let items = [];
		for(let i = 0; i < numberToSpawn; i ++){
			let item;
			if(Math.random() < healingItemRarity){
				item = healingItems.rare[this.random(1, healingItems.rare.length)-1];
			} else {
				item = healingItems.common[this.random(1, healingItems.common.length)-1];
			}
			item.type = "healing";
			items.push(item);
		}
		console.log("Spawning these healing items:", items);
		for(let i = 0; i < items.length-1; i++){
			nextMap = this.spawn("item", items[i], nextMap, this.state.npcs).map;
		}
		console.log("Finished spawning healing items. nextMap:");
		console.log(nextMap);
		return nextMap;
	}
	
	spawnMonsters(map){
		let monsterFamily = monsters[this.random(0, monsters.length-1)];
		console.log("Selected " + monsterFamily.name + " family of monsters.");
		let floorTiles = this.scanMapFor("floor", map);
		let monsterCount = floorTiles.length*monsterDensity;
		let monsterVariance = (Math.random()*2*monsterDensityVariation);
		monsterCount += (monsterCount*monsterVariance);
		monsterCount = Math.floor(monsterCount);
		console.log("Will spawn " + monsterCount + " monsters. monsterDensity:" + monsterDensity + " monsterDensityVariation:" + monsterDensityVariation + " floorTiles.length:" + floorTiles.length);
		
		//Make an array of random monsters to place
		let monstersToPlace = [];
		let e = Math.floor(monsterCount/2);
		let m = monsterCount - e - 1;
		let id = 1;
		for (let i = 0; i < e; i++){
			let monster = monsterFamily.easy[this.random(0,monsterFamily.easy.length-1)];
			let randomized = this.randomizeMonster(monster, "easy");
			randomized.type = "easy";
			randomized.id = id;
			id++
			monstersToPlace.push(randomized);		
		}
		for (let i = 0; i < m; i++){
			let monster = monsterFamily.medium[this.random(0,monsterFamily.medium.length-1)];
			let randomized = this.randomizeMonster(monster, "medium");
			randomized.type = "medium";
			randomized.id = id;
			id++
			monstersToPlace.push(randomized);		
		}
		let boss = monsterFamily.boss[this.random(0,monsterFamily.boss.length-1)];
		let randomized = this.randomizeMonster(boss, "boss");
		randomized.type = "boss";
		randomized.id = id;
		monstersToPlace.push(randomized);
		console.log("Here are the monstersToPlace:", monstersToPlace);
		
		//Then place them in the map
		let nextMap = map;
		let nextNpcs = [];
		let monstersSuccessfullySpawned = 0;
		for(let j = 0; j < monstersToPlace.length; j++){
			let spawn = this.spawn("monster", monstersToPlace[j], nextMap, nextNpcs);
			if(spawn.error){
				console.log("ERR: spawnMonsters(): Received error from spawn(). monsterDensity is probably too high (" + monsterDensity + "). Discontinuing spawning.");
				break;
			}
			nextMap = spawn.map;
			nextNpcs = spawn.npcs;
			monstersSuccessfullySpawned = j+1;
		}
		console.log("Map with monsters:", nextMap);
		console.log("spawnMonsters(): nextNpcs:", nextNpcs)
		return {
			npcs: nextNpcs,
			map: nextMap,
			monsterCount: monstersSuccessfullySpawned
		}
	}
	
	updateVisibleMap(map, x, y) {
		
		let visible = [];
		let radius = viewSize;
		let radiusMax = maxViewSize;
		let padding = radiusMax - radius;

		let xMin = x - radius;
		let xMax = x + radius + 1;
		let xBound = map[0].length - 1;
		let yMin = y - radius;
		let yMax = y + radius + 1;
		let yBound = map.length - 1;

		//set all currently visible tiles to not visible
		for (let row = 0; row < map.length - 1; row++) {
			for (let cell = 0; cell < map[0].length - 1; cell++) {
				map[row][cell].visible = false;
			}
		}

		//put new set of tiles into the visible map array
		for (let row = yMin; row < yMax; row++) {
			let newRow = [];
			for (let cell = xMin; cell < xMax; cell++) {
				let newCell;
				//if out of map bounds, add empty (rock) tiles
				if (cell < 0 || cell > xBound || row < 0 || row > yBound) {
					newCell = {
						state: "rock",
						x: cell,
						y: row
					};
					//console.log("adding nonexistent cell to visible x:" + cell + " y:" + row);
				} else {
					newCell = map[row][cell];
					//console.log(
					//	"adding map cell " +
					//		JSON.stringify(map[row][cell]) +
					//		" to visible x:" +
					//		cell +
					//		" y:" +
					//		row
					//);
				}
				
				//set as visible according to a circular radius
				let sq = ((cell - x) ** 2) + ((row - y) ** 2)+1;
				console.log("r**2: "+(radius**2) + " sq:"+sq + " cell:"+cell + " row:"+row);
				if (radius**2 >= sq) {
					newCell.visible = true;
				} else {
					newCell.visible = false;
				}
				newRow.push(newCell);
			}
			visible.push(newRow);
		}
		return visible;
	}
	
	
	//-----LIFECYCLE-----

	componentWillMount() {
		this.setState(this.generateMap());
		document.addEventListener("keydown", this.handleKeyDown.bind(this))
	}

	render() {
		return (
			<div id="wrapper">
				<Game map={this.state.visibleMap} player={this.state.player} log={this.state.combatLog} depth={this.state.depth} monsterCount={this.state.monsterCount}/>
				<Footer title="React Roguelike" />
			</div>
		);
	}
}

class Footer extends React.Component {
	render() {
		return (
			<div className="footer">
				<div className="heading">{this.props.title}</div>
				<div className="author">
					<span>
						By <a href={my.site}>Lewis Horwood</a>
					</span>
					<div className="button-group">
						<a href={my.github}>
							<div className="button">
								<span className="button-chevron">> </span>github
							</div>
						</a>
						<a href={my.freecodecamp}>
							<div className="button">
								<span className="button-chevron">> </span>freecodecamp
							</div>
						</a>
					</div>
				</div>
			</div>
		);
	}
}

class Game extends React.Component {
	render() {
		return (
			<div id="main">
				<Log log={this.props.log} />
				<Map map={this.props.map} />
				<Info 
					player={this.props.player} 
					depth={this.props.depth} 
					monsterCount={this.props.monsterCount}
				/>
			</div>
		);
	}
}

class CombatLog extends React.Component {
	render() {
		let items=this.props.log.slice(-10).reverse();
		let log = items.map((i, index) => {
			return <li id={index}>{i}</li>
		})
		return (
			<div id="combatLog">
				<ul>
					{log}
				</ul>
			</div>
		);
	}
}

class Log extends React.Component {
	render() {
		return (
			<div className="panel">
				<CombatLog log={this.props.log}/>
			</div>
		);
	}
}

class Map extends React.Component {
	render() {
		return (
			<div id="center">
				<div id="map">
					<Grid map={this.props.map} />
				</div>
			</div>
		);
	}
}

class Grid extends React.Component {
	renderMap() {
		let map = this.props.map;
		let output = [];
		for (let y = 0; y < map.length; y++) {
			let row = map[y].map((cell, index) => {
				let displayClass;
				if(cell.entity){
					displayClass = cell.entity.type;
				} else if (cell.item){
					displayClass = cell.item.type;
				} else {
					displayClass = cell.state;
				}
				return <div className={"mapCell " + displayClass + " " + (cell.visible ? "visible" : "notvisible")} />;
			});
			output.push(
				<div className="mapRow" id={y}>
					{row}
				</div>
			);
		}
		console.log("Rendering map:", map);
		return output;
	}

	render() {
		return <div id="grid">{this.renderMap()}</div>;
	}
}

class Info extends React.Component {
	render() {
		return (
			<div className="panel">
				<PlayerInfo 
					player={this.props.player}
					depth={this.props.depth}
					monsterCount={this.props.monsterCount}
				/>
			</div>
		);
	}
}

class PlayerInfo extends React.Component {
	render() {
		let pluralize = "s";
		if(this.props.monsterCount < 2){pluralize = ""}
		return (
			<div id="playerInfo">
				<p className="playerName">{this.props.player.name}</p>
				<p className="playerLevel">Level {this.props.player.level}</p>
				<p className="levelInfo">Floor {this.props.depth} -- {this.props.monsterCount} monster{pluralize} left</p>
				<p>
					<PlayerInfoBar type="hp" player={this.props.player} />
				</p>
				<p>
					<PlayerInfoBar type="xp" player={this.props.player} />
				</p>
			</div>
		);
	}
}

class PlayerInfoBar extends React.Component {
	render() {
		const type = this.props.type;
		let remaining, max;
		if (type == "hp") {
			remaining = this.props.player.hp;
			max = this.props.player.hpmax;
		} else if (type == "xp") {
			remaining = this.props.player.xp;
			max = this.props.player.xpmax;
		}
		return (
			<div className="playerInfoBar">
				<div className="typeName">{type.toUpperCase()}:</div>
				<div className={"bar " + this.props.type}>
					<div className="infoText">
						{remaining}/{max}
					</div>
					<div className="remaining" style={{ width: remaining / max * 200 }} />
					<div className="missing" style={{ width: (1 - remaining / max) * 200 }} />
				</div>
			</div>
		);
	}
}

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

              
            
!
999px

Console