diff --git a/.gitignore b/.gitignore
index 3ad33f15381af401366a6545020c9aaee92e0964..bca9cf9f69997da3ffdd87528b46c65c75507a80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@
 
 .idea/
 .vscode/
-dist/
\ No newline at end of file
+dist/
+
+*.tiled-session
\ No newline at end of file
diff --git a/CREDITS b/CREDITS
new file mode 100644
index 0000000000000000000000000000000000000000..6e4376fcee3eff579d0808c74d020b78d6c5fc49
--- /dev/null
+++ b/CREDITS
@@ -0,0 +1,7 @@
+[fonts]
+
+Kaph - GGBotNet - https://ggbot.itch.io/kaph-font
+
+[sprites]
+
+Dawnlike - DragonDePlatino / Dawnbringer - https://opengameart.org/content/dawnlike-16x16-universal-rogue-like-tileset-v181
\ No newline at end of file
diff --git a/README.md b/README.md
index 5abf2c6d68757e6505f73028d25eeca704a8c808..40aae88cc52848bd24754fba9fef70578d6974c5 100644
--- a/README.md
+++ b/README.md
@@ -52,5 +52,7 @@ The code source files found in this repository are covered by the license found
 
 The logo found in `assets/splash.png` is licensed under the following license for use only within the context of this project.
 
+Asset creators are listed in CREDITS with any relevant licenses
+
 <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><span property="dct:title">Microhacks Logo</span> by <span property="cc:attributionName">Microhacks Ltd</span> is licensed under <a href="http://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-ND 4.0</a></p>
 <img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nd.svg?ref=chooser-v1">
diff --git a/assets/fonts/Kaph.ttf b/assets/fonts/Kaph.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..bf079986090614d9483ebd2ead80c6070390293e
--- /dev/null
+++ b/assets/fonts/Kaph.ttf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1dba90d6614e66d673e5e3e476195799727e120b6c922327289f0204d67c34d6
+size 406568
diff --git a/assets/sprites/creatures.png b/assets/sprites/creatures.png
new file mode 100644
index 0000000000000000000000000000000000000000..74d5c5a3d2a7e1b48a8c8332e337f7b9894547f1
--- /dev/null
+++ b/assets/sprites/creatures.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d169fb7508b4524311499cccb7bddec281189fdc3bd6fd8ffd66293a1e486291
+size 27025
diff --git a/assets/sprites/environs.png b/assets/sprites/environs.png
new file mode 100644
index 0000000000000000000000000000000000000000..3e42df867d6619ccb465c23d20b714368400343d
--- /dev/null
+++ b/assets/sprites/environs.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfa6e683894dffe10ecbbd5fafd89a41985c2f08bf99e1a07927b4c67807f463
+size 130133
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index cb6d71ed2de4f8479c254b106a5effea69d0b012..e848c5fff0f0bd6adce82aada13ccc0799812bde 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -2,7 +2,7 @@ use bevy::asset::LoadState;
 use bevy::prelude::*;
 use iyes_loopless::prelude::NextState;
 
-use crate::assets::AssetTypeLoader;
+use crate::assets::{AssetTypeLoader, SpriteSheetConfig};
 use crate::system::flow::AppState;
 
 pub fn start_preload_resources(mut commands: Commands) {
@@ -14,6 +14,15 @@ pub fn start_preload_resources(mut commands: Commands) {
 pub fn start_load_resources(mut loader: AssetTypeLoader) {
 	loader.load_images(&[("splash.png", "splash")]);
 	loader.load_audio(&[("splash_sting.mp3", "splash_sting")]);
+
+	let sheet_config = SpriteSheetConfig::squares(16, 64, 64);
+	loader.load_spritesheet(
+		&sheet_config,
+		&[
+			("sprites/environs.png", "environs"),
+			("sprites/creatures.png", "creatures"),
+		],
+	);
 }
 
 pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) {
diff --git a/game_core/src/debug.rs b/game_core/src/debug.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0b87c5f02f21ca9524bccaf402d200c86a5b2291
--- /dev/null
+++ b/game_core/src/debug.rs
@@ -0,0 +1,24 @@
+use bevy::math::uvec2;
+use bevy::prelude::*;
+use iyes_loopless::prelude::AppLooplessStateExt;
+use iyes_loopless::state::NextState;
+
+use crate::entities::spawner::EntitySpawner;
+use crate::system::flow::AppState;
+
+pub fn spawn_player(mut spawner: EntitySpawner) {
+	log::info!("Spawning player");
+	spawner.spawn_player(uvec2(2, 2));
+}
+
+pub fn skip_menu(mut commands: Commands) {
+	commands.insert_resource(NextState(AppState::InGame));
+}
+
+pub struct DebugPlugin;
+impl Plugin for DebugPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_enter_system(AppState::Menu, skip_menu)
+			.add_enter_system(AppState::InGame, spawn_player);
+	}
+}
diff --git a/game_core/src/entities/lifecycle.rs b/game_core/src/entities/lifecycle.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4d105b24723296807d15e7946b9c19f9827031e9
--- /dev/null
+++ b/game_core/src/entities/lifecycle.rs
@@ -0,0 +1,10 @@
+use bevy::prelude::*;
+
+#[derive(Debug, Clone, Copy, Component)]
+pub struct GameEntity;
+
+pub fn remove_game_entities(mut commands: Commands, query: Query<Entity, With<GameEntity>>) {
+	for entity in &query {
+		commands.entity(entity).despawn_recursive();
+	}
+}
diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..13037dbe39af29500d714cea5a78c818e1367c67
--- /dev/null
+++ b/game_core/src/entities/mod.rs
@@ -0,0 +1,2 @@
+pub mod lifecycle;
+pub mod spawner;
diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ab539eaeff6c77f6fa5ec52ab0a03095d35b4cde
--- /dev/null
+++ b/game_core/src/entities/spawner.rs
@@ -0,0 +1,33 @@
+use bevy::ecs::system::SystemParam;
+use bevy::prelude::*;
+
+use crate::assets::AssetHandles;
+use crate::entities::lifecycle::GameEntity;
+use crate::system::camera::ChaseCam;
+use crate::system::graphics::LAYER_CREATURE;
+
+const PLAYER_SPRITE: usize = 2;
+
+#[derive(SystemParam)]
+pub struct EntitySpawner<'w, 's> {
+	pub commands: Commands<'w, 's>,
+	pub handles: Res<'w, AssetHandles>,
+}
+
+impl<'w, 's> EntitySpawner<'w, 's> {
+	pub fn spawn_player(&mut self, grid_position: UVec2) {
+		let mut entity = self.commands.spawn();
+
+		entity.insert(ChaseCam).insert(GameEntity);
+
+		entity.insert_bundle(SpriteSheetBundle {
+			texture_atlas: self.handles.atlas("creatures"),
+			transform: Transform::from_translation(grid_position.as_vec2().extend(LAYER_CREATURE)),
+			sprite: TextureAtlasSprite {
+				index: PLAYER_SPRITE,
+				..Default::default()
+			},
+			..Default::default()
+		});
+	}
+}
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index e0acb2fac262674fb983e9880091120b5ad2a646..aff889092a604f6530bdc59a3d05060465c8e162 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1,4 +1,7 @@
 pub mod assets;
+pub mod debug;
+pub mod entities;
 pub mod multiplayer;
 pub mod splash_screen;
 pub mod system;
+pub mod world;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index 8d7c57b49e5d161c1501e1ccb56f4c3709154641..879c0037d6db3da1d399500c0de73ac2016df47f 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -1,10 +1,10 @@
 use bevy::prelude::*;
+use game_core::assets::AssetHandles;
 use game_core::system::flow::AppState;
 use game_core::system::resources::DefaultResourcesPlugin;
 use iyes_loopless::prelude::AppLooplessStateExt;
 use micro_musicbox::CombinedAudioPlugins;
 use remote_events::RemoteEventPlugin;
-use game_core::assets::AssetHandles;
 
 fn main() {
 	App::new()
@@ -19,5 +19,6 @@ fn main() {
 			game_core::multiplayer::OutgoingEvent,
 			game_core::multiplayer::IncomingEvent,
 		>::new())
+		.add_plugin(game_core::debug::DebugPlugin)
 		.run();
 }
diff --git a/game_core/src/splash_screen/systems.rs b/game_core/src/splash_screen/systems.rs
index 1894942c7741b7865c187d05f6cfcf7156cc47c1..c0c90b444e47880b85519218c6e65cd406b6baf4 100644
--- a/game_core/src/splash_screen/systems.rs
+++ b/game_core/src/splash_screen/systems.rs
@@ -9,6 +9,7 @@ use crate::splash_screen::components::{
 	SplashAnimation, SplashAnimationBundle, SplashAnimationTimer, SplashAnimationType,
 };
 use crate::system::flow::AppState;
+use crate::system::load_config::initial_size;
 use crate::system::utilities::f32_min;
 use crate::system::window::WindowManager;
 
@@ -36,14 +37,19 @@ pub fn setup_splash_screen(
 	windows: WindowManager,
 	mut music_box: MusicBox<AssetHandles>,
 ) {
-	let window_size = match windows.get_primary_window() {
-		Some(size) => size,
-		None => {
-			log::error!("Missing window for splash screen");
-			commands.insert_resource(NextState(AppState::Menu));
-			return;
-		}
-	};
+	// Changed window scaling to auto, so layout is based on virtual pixel units, not
+	// window units
+	//
+	// let window_size = match windows.get_primary_window() {
+	// 	Some(size) => size,
+	// 	None => {
+	// 		log::error!("Missing window for splash screen");
+	// 		commands.insert_resource(NextState(AppState::Menu));
+	// 		return;
+	// 	}
+	// };
+
+	let window_size = initial_size();
 
 	let handle = match handles.images.get("splash") {
 		Some(handle) => handle,
@@ -59,11 +65,21 @@ pub fn setup_splash_screen(
 
 	commands.insert_resource(ClearColor(Color::hex("001122").unwrap()));
 
-	let scale_factor = match window_size.width() > window_size.height() {
-		true => window_size.height() / image_data.texture_descriptor.size.height as f32,
-		false => window_size.width() / image_data.texture_descriptor.size.width as f32,
+	let image_size = image_data.texture_descriptor.size;
+	let scale_factor = match window_size.0 > window_size.1 {
+		true => window_size.1 / image_size.height as f32,
+		false => window_size.0 / image_size.width as f32,
 	};
 
+	log::info!(
+		"Splash scaling: s{:.3}; i({}x{}); w({}x{})",
+		scale_factor,
+		image_size.width,
+		image_size.height,
+		window_size.0,
+		window_size.1
+	);
+
 	image_data.sampler_descriptor = ImageSampler::linear();
 	music_box.play_sfx("splash_sting");
 
diff --git a/game_core/src/system/graphics.rs b/game_core/src/system/graphics.rs
new file mode 100644
index 0000000000000000000000000000000000000000..985f237d96f38154c9d974d2b883cbfe2d47452e
--- /dev/null
+++ b/game_core/src/system/graphics.rs
@@ -0,0 +1,3 @@
+pub const LAYER_TILE: f32 = 50.0;
+pub const LAYER_ITEM: f32 = 300.0;
+pub const LAYER_CREATURE: f32 = 400.0;
diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs
index 4eba511274240a7a2e5f0e9938e09044e7a2d484..4221c06e7d68a555fa6b7721d297886efc31351d 100644
--- a/game_core/src/system/load_config.rs
+++ b/game_core/src/system/load_config.rs
@@ -10,7 +10,7 @@ mod setup {
 	}
 
 	pub fn initial_size() -> (f32, f32) {
-		(1280.0, 720.0)
+		(16.0 * 32.0, 16.0 * 18.0)
 	}
 }
 
@@ -22,8 +22,8 @@ mod setup {
 
 	#[cfg(feature = "no_aspect")]
 	pub fn initial_size() -> (f32, f32) {
-		static default_width: f32 = 1280.0;
-		static default_height: f32 = 720.0;
+		static default_width: f32 = 16.0 * 32.0;
+		static default_height: f32 = 16.0 * 18.0;
 
 		web_sys::window()
 			.and_then(|window: web_sys::Window| {
@@ -45,9 +45,9 @@ mod setup {
 
 	#[cfg(not(feature = "no_aspect"))]
 	pub fn initial_size() -> (f32, f32) {
-		static default_width: f32 = 1280.0;
-		static default_height: f32 = 720.0;
-		static ratio: f32 = 1280.0 / 720.0;
+		static default_width: f32 = 16.0 * 32.0;
+		static default_height: f32 = 16.0 * 18.0;
+		static ratio: f32 = default_width / default_height;
 
 		web_sys::window()
 			.and_then(|window: web_sys::Window| {
diff --git a/game_core/src/system/mod.rs b/game_core/src/system/mod.rs
index 309ab329d7595034fdc2a5fcbbe46f67f4859192..2a691923179309a9f74a937f691f5958e290bd96 100644
--- a/game_core/src/system/mod.rs
+++ b/game_core/src/system/mod.rs
@@ -1,5 +1,6 @@
 pub mod camera;
 pub mod flow;
+pub mod graphics;
 pub mod load_config;
 pub mod resources;
 pub mod utilities;
diff --git a/game_core/src/system/resources.rs b/game_core/src/system/resources.rs
index dfb7d23f9db124be02033a3b44f26d5665a619e8..9ac587c915a74e3e59a82e30393655786d1701c4 100644
--- a/game_core/src/system/resources.rs
+++ b/game_core/src/system/resources.rs
@@ -3,7 +3,6 @@ use bevy::prelude::*;
 use bevy::render::texture::ImageSettings;
 use bevy::window::PresentMode;
 
-use crate::system::camera::spawn_orthographic_camera;
 use crate::system::load_config::{get_asset_path_string, initial_size};
 
 pub struct DefaultResourcesPlugin;
@@ -12,10 +11,10 @@ impl Plugin for DefaultResourcesPlugin {
 		let (width, height) = initial_size();
 
 		app.insert_resource(WindowDescriptor {
-			width,
-			height,
+			width: width * 3.0,
+			height: height * 3.0,
 			resizable: true,
-			title: String::from("Bevy 2D Template"),
+			title: String::from("Ludum Dare 51"),
 			present_mode: PresentMode::AutoNoVsync,
 			..Default::default()
 		})
diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b6e2ed7be7c06a9cd0cddae1967c7097f11f93ba
--- /dev/null
+++ b/game_core/src/world/level_map.rs
@@ -0,0 +1,26 @@
+use std::ops::Deref;
+
+use bevy::math::UVec2;
+use bevy::prelude::*;
+
+pub const WORLD_TILE_SIZE: f32 = 16.0;
+
+/// Track the location of an entity within a grid
+#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Component)]
+#[repr(transparent)]
+pub struct GridPosition(pub UVec2);
+impl Deref for GridPosition {
+	type Target = UVec2;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+/// Take an entity's position on a grid, and sync it up to the transform used to render
+/// the sprite
+pub fn sync_grid_to_transform(mut query: Query<(&GridPosition, &mut Transform)>) {
+	for (position, mut transform) in &mut query {
+		transform.translation = ((position.as_vec2() * WORLD_TILE_SIZE) + (WORLD_TILE_SIZE / 2.0))
+			.extend(transform.translation.z);
+	}
+}
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c379ed7d24b0e81501eff93d8ae2a4db3351a1ec
--- /dev/null
+++ b/game_core/src/world/mod.rs
@@ -0,0 +1 @@
+pub mod level_map;