diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index 50b235cbebbe25b17a141fd8c5075dbffb027e64..aaf32237458044eaf7b2ae279504aa7f49e2ec15 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -20,6 +20,7 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) {
 	loader.load_images(&[("splash.png", "splash")]);
 	loader.load_audio(&[("splash_sting.mp3", "splash_sting")]);
 
+	loader.load_font(&[("fonts/Kaph.ttf", "main")]);
 	let sheet_config = SpriteSheetConfig::squares(16, SHEET_WIDTH, SHEET_HEIGHT);
 	loader.load_spritesheet(
 		&sheet_config,
diff --git a/game_core/src/control/actions.rs b/game_core/src/control/actions.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/game_core/src/control/mod.rs b/game_core/src/control/mod.rs
index f28d7c205ea9a9b4c0a1c6bf1404f8a0f7421669..632784b683760f1c0d88f21d0a25a4b2dd03ccfc 100644
--- a/game_core/src/control/mod.rs
+++ b/game_core/src/control/mod.rs
@@ -1 +1,25 @@
+pub mod actions;
 pub mod player;
+
+mod __plugin {
+	use bevy::app::{App, CoreStage};
+	use bevy::prelude::Plugin;
+	use iyes_loopless::prelude::ConditionSet;
+
+	use crate::system::flow::AppState;
+
+	pub struct ControlPlugin;
+	impl Plugin for ControlPlugin {
+		fn build(&self, app: &mut App) {
+			app.add_system_set_to_stage(
+				CoreStage::PreUpdate,
+				ConditionSet::new()
+					.run_in_state(AppState::InGame)
+					.with_system(super::player::handle_player_input)
+					.into(),
+			);
+		}
+	}
+}
+
+pub use __plugin::ControlPlugin;
diff --git a/game_core/src/control/player.rs b/game_core/src/control/player.rs
new file mode 100644
index 0000000000000000000000000000000000000000..44f52965a1ae898105d399cd39a807cde946fc09
--- /dev/null
+++ b/game_core/src/control/player.rs
@@ -0,0 +1,31 @@
+use bevy::math::ivec2;
+use bevy::prelude::*;
+
+use crate::entities::lifecycle::Player;
+use crate::world::level_map::GridPosition;
+
+pub fn handle_player_input(
+	input: Res<Input<KeyCode>>,
+	mut query: Query<&mut GridPosition, With<Player>>,
+) {
+	let mut dx = 0;
+	let mut dy = 0;
+
+	if input.just_released(KeyCode::D) || input.just_released(KeyCode::Right) {
+		dx += 1;
+	}
+	if input.just_released(KeyCode::W) || input.just_released(KeyCode::Up) {
+		dy += 1;
+	}
+	if input.just_released(KeyCode::A) || input.just_released(KeyCode::Left) {
+		dx -= 1;
+	}
+	if input.just_released(KeyCode::S) || input.just_released(KeyCode::Down) {
+		dy -= 1;
+	}
+
+	for mut position in &mut query {
+		let next_position = (position.0.as_ivec2()) + ivec2(dx, dy);
+		**position = next_position.as_uvec2();
+	}
+}
diff --git a/game_core/src/debug.rs b/game_core/src/debug.rs
index 928add3d7a5eacb15566f4c082ac034ae662e8cb..2722f4b0a7d2800f350e7f094d7cfab2f86dd38a 100644
--- a/game_core/src/debug.rs
+++ b/game_core/src/debug.rs
@@ -3,17 +3,64 @@ use bevy::prelude::*;
 use iyes_loopless::prelude::AppLooplessStateExt;
 use iyes_loopless::state::NextState;
 
-use crate::entities::spawner::EntitySpawner;
+use crate::assets::AssetHandles;
 use crate::system::flow::AppState;
-use crate::world::generation::generate_map;
+use crate::world::generators::drunkard_corridor::DrunkardGenerator;
 use crate::world::level_map::LevelMapBundle;
 
-pub fn spawn_player(mut spawner: EntitySpawner) {
-	log::info!("Spawning player");
-	spawner.spawn_player(uvec2(25, 25));
-	spawner
-		.commands
-		.spawn_bundle(LevelMapBundle::generate(50, 50));
+pub fn spawn_player(mut commands: Commands) {
+	commands.spawn_bundle(LevelMapBundle::generate::<DrunkardGenerator>(150, 150));
+}
+
+#[derive(Component)]
+pub struct FpsText;
+
+pub fn spawn_fps_overlay(mut commands: Commands, assets: Res<AssetHandles>) {
+	commands
+		.spawn_bundle(NodeBundle {
+			style: Style {
+				position_type: PositionType::Absolute,
+				position: UiRect::new(Val::Px(20.0), Val::Auto, Val::Px(20.0), Val::Auto),
+				flex_direction: FlexDirection::Row,
+				..Default::default()
+			},
+			color: Color::rgba(0.0, 0.0, 0.0, 0.0).into(),
+			..Default::default()
+		})
+		.with_children(|child_builder| {
+			child_builder.spawn_bundle(TextBundle {
+				text: Text::from_section(
+					"FPS: ",
+					TextStyle {
+						font_size: 20.0,
+						font: assets.font("main"),
+						color: Color::WHITE,
+					},
+				),
+				..Default::default()
+			});
+
+			child_builder
+				.spawn_bundle(TextBundle {
+					text: Text::from_section(
+						"000",
+						TextStyle {
+							font_size: 20.0,
+							font: assets.font("main"),
+							color: Color::YELLOW,
+						},
+					),
+					..Default::default()
+				})
+				.insert(FpsText);
+		});
+}
+
+pub fn update_fps_text(time: Res<Time>, mut text_query: Query<&mut Text, With<FpsText>>) {
+	let fps = format!("{:03.0}", 1.0 / time.delta_seconds());
+	for mut text in &mut text_query {
+		text.sections[0].value = fps.clone();
+	}
 }
 
 pub fn skip_menu(mut commands: Commands) {
@@ -24,6 +71,8 @@ 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);
+			.add_enter_system(AppState::Menu, spawn_fps_overlay)
+			.add_enter_system(AppState::InGame, spawn_player)
+			.add_system(update_fps_text);
 	}
 }
diff --git a/game_core/src/entities/lifecycle.rs b/game_core/src/entities/lifecycle.rs
index 8007a1dbfe41240fc35766f76eacb0646b92bf90..d4dcc2aee4fe5e4a6eef6aa01fe23db6ffb48a2c 100644
--- a/game_core/src/entities/lifecycle.rs
+++ b/game_core/src/entities/lifecycle.rs
@@ -5,6 +5,8 @@ use crate::system::flow::AppState;
 
 #[derive(Debug, Clone, Copy, Component)]
 pub struct GameEntity;
+#[derive(Debug, Clone, Copy, Component)]
+pub struct Player;
 
 pub fn remove_game_entities(mut commands: Commands, query: Query<Entity, With<GameEntity>>) {
 	for entity in &query {
diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs
index 7d78cbd43f5cae102ad802866f953aa051128e1d..32bacc64c631798b6704b5a57a52a3f45b4bf935 100644
--- a/game_core/src/entities/spawner.rs
+++ b/game_core/src/entities/spawner.rs
@@ -2,7 +2,7 @@ use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
 
 use crate::assets::AssetHandles;
-use crate::entities::lifecycle::GameEntity;
+use crate::entities::lifecycle::{GameEntity, Player};
 use crate::system::camera::ChaseCam;
 use crate::system::graphics::LAYER_CREATURE;
 use crate::world::level_map::GridPosition;
@@ -22,6 +22,7 @@ impl<'w, 's> EntitySpawner<'w, 's> {
 		entity
 			.insert(ChaseCam)
 			.insert(GameEntity)
+			.insert(Player)
 			.insert(GridPosition(grid_position));
 
 		entity.insert_bundle(SpriteSheetBundle {
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index bbe51c5948b1f5cb9158439cb9fecbce79d7c115..9e165a9060d5f313726a791cae039629f1bfe09d 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -21,5 +21,6 @@ fn main() {
 		>::new())
 		.add_plugin(game_core::debug::DebugPlugin)
 		.add_plugin(game_core::world::WorldPlugin)
+		.add_plugin(game_core::control::ControlPlugin)
 		.run();
 }
diff --git a/game_core/src/world/generation.rs b/game_core/src/world/generation.rs
deleted file mode 100644
index 4f39d6b74238c9d647ebc1c9554132504f24445b..0000000000000000000000000000000000000000
--- a/game_core/src/world/generation.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use fastrand::Rng;
-
-use crate::world::level_map::{Indexer, LevelMap, MapLayer, MapTile};
-
-const TMP_FLOOR_GROUP: usize = 6 * 64;
-const TMP_WALL_GROUP: usize = 3 * 64 + 28;
-
-#[derive(Copy, Clone, Debug, Default)]
-enum GenerationDirection {
-	Top,
-	Bottom,
-	Left,
-	Right,
-	#[default]
-	None,
-}
-
-pub fn generate_map(width: usize, height: usize) -> LevelMap {
-	generate_map_with_seed(fastrand::u64(0..u64::MAX), width, height)
-}
-
-pub fn generate_map_with_seed(seed: u64, width: usize, height: usize) -> LevelMap {
-	let rng = fastrand::Rng::with_seed(seed);
-	generate_map_with_rng(&rng, width, height)
-}
-
-pub fn generate_map_with_rng(rng: &Rng, width: usize, height: usize) -> LevelMap {
-	let indexer = Indexer::new(width, height);
-
-	let mut floor_layer = vec![0; width * height];
-	let mut wall_layer = vec![0; width * height];
-	let mut decoration_layer = vec![0; width * height];
-
-	let initial_x = width / 2;
-	let initial_y = width / 2;
-	let initial_width = rng.usize(3..=5);
-	let initial_height = rng.usize(3..=5);
-
-	draw_box(
-		initial_x - initial_width / 2,
-		initial_y - initial_height / 2,
-		initial_width,
-		initial_height,
-		TMP_FLOOR_GROUP,
-		&mut floor_layer,
-		&indexer,
-	);
-
-	for _ in 0..1000 {
-		let initial = find_next_door(1000, &floor_layer, &rng, &indexer);
-		if let Some((next_x, next_y, direction)) = initial {
-			let size_selection = rng.usize(1..=10);
-			let (room_width, room_height) = if size_selection < 8 {
-				(rng.usize(3..=6), rng.usize(3..=6))
-			} else {
-				(rng.usize(7..=12), rng.usize(7..=12))
-			};
-
-			// Align room drawing to top left corner of new box
-			match direction {
-				GenerationDirection::Top => {
-					let start_x = next_x - (room_width / 4);
-					let start_y = next_y + room_height;
-					draw_box(
-						start_x,
-						start_y,
-						room_width,
-						room_height,
-						TMP_FLOOR_GROUP,
-						&mut floor_layer,
-						&indexer,
-					);
-				}
-				GenerationDirection::Bottom => {
-					let start_x = next_x - (room_width / 4);
-					let start_y = next_y;
-					draw_box(
-						start_x,
-						start_y,
-						room_width,
-						room_height,
-						TMP_FLOOR_GROUP,
-						&mut floor_layer,
-						&indexer,
-					);
-				}
-				GenerationDirection::Left => {
-					let start_x = next_x - room_width;
-					let start_y = next_y;
-					draw_box(
-						start_x,
-						start_y,
-						room_width,
-						room_height,
-						TMP_FLOOR_GROUP,
-						&mut floor_layer,
-						&indexer,
-					);
-				}
-				GenerationDirection::Right => {
-					let start_x = next_x;
-					let start_y = next_y;
-					draw_box(
-						start_x,
-						start_y,
-						room_width,
-						room_height,
-						TMP_FLOOR_GROUP,
-						&mut floor_layer,
-						&indexer,
-					);
-				}
-				GenerationDirection::None => {}
-			}
-		}
-	}
-
-	LevelMap {
-		width,
-		height,
-		layers: vec![
-			MapLayer::from_sized_list(
-				width,
-				height,
-				floor_layer
-					.into_iter()
-					.map(|tile| match tile {
-						0 => None,
-						rest => Some(MapTile::new_floor(rest)),
-					})
-					.collect(),
-			),
-			MapLayer::from_sized_list(
-				width,
-				height,
-				wall_layer
-					.into_iter()
-					.map(|tile| match tile {
-						0 => None,
-						rest => Some(MapTile::new_wall(rest)),
-					})
-					.collect(),
-			),
-			MapLayer::from_sized_list(
-				width,
-				height,
-				decoration_layer
-					.into_iter()
-					.map(|tile| match tile {
-						0 => None,
-						rest => Some(MapTile::new_obstacle(rest)),
-					})
-					.collect(),
-			),
-		],
-	}
-}
-
-fn find_next_door(
-	max_attempts: usize,
-	floor: &Vec<usize>,
-	rng: &Rng,
-	idx: &Indexer,
-) -> Option<(usize, usize, GenerationDirection)> {
-	let mut remaining = max_attempts;
-	let mut found = None;
-	let mut last_direction = GenerationDirection::None;
-
-	while remaining > 0 && found.is_none() {
-		remaining = remaining.saturating_sub(1);
-
-		let target_index = rng.usize(0..floor.len());
-		let (x, y) = idx.reverse(target_index);
-
-		// Edges of the map are unsuitable for new rooms
-		if x == 0 || x == idx.width() - 1 || y == 0 || y == idx.height() - 1 {
-			continue;
-		}
-
-		let mut adjacents = 0;
-
-		// Check Left
-		if floor[idx.index(x - 1, y)] != 0 {
-			adjacents += 1;
-			last_direction = GenerationDirection::Right;
-		}
-		// Check Right
-		if floor[idx.index(x + 1, y)] != 0 {
-			adjacents += 1;
-			last_direction = GenerationDirection::Left;
-		}
-		// Check Top
-		if floor[idx.index(x, y + 1)] != 0 {
-			adjacents += 1;
-			last_direction = GenerationDirection::Bottom;
-		}
-		// Check Bottom
-		if floor[idx.index(x, y - 1)] != 0 {
-			adjacents += 1;
-			last_direction = GenerationDirection::Top;
-		}
-
-		if adjacents == 1 {
-			found = Some((x, y, last_direction));
-			break;
-		}
-	}
-
-	found
-}
-
-fn draw_box(
-	start_x: usize,
-	start_y: usize,
-	width: usize,
-	height: usize,
-	value: usize,
-	layer: &mut Vec<usize>,
-	indexer: &Indexer,
-) {
-	if !validate_box(start_x, start_y, width, height, layer, indexer) {
-		return;
-	}
-
-	for x in start_x..(start_x + width) {
-		for y in start_y..(start_y + height) {
-			let idx = indexer.index(x, y);
-			layer[idx] = value;
-		}
-	}
-}
-
-fn validate_box(
-	start_x: usize,
-	start_y: usize,
-	width: usize,
-	height: usize,
-	layer: &Vec<usize>,
-	indexer: &Indexer,
-) -> bool {
-	let map_width = indexer.width();
-	let map_height = indexer.height();
-
-	if start_y + height >= map_height {
-		return false;
-	}
-
-	if start_x + width >= map_width {
-		return false;
-	}
-
-	for x in start_x..(start_x + width) {
-		for y in start_y..(start_y + height) {
-			let idx = indexer.index(x, y);
-			if layer[idx] != 0 {
-				return false;
-			}
-		}
-	}
-
-	true
-}
diff --git a/game_core/src/world/generators/blobular.rs b/game_core/src/world/generators/blobular.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b552923c0a729bc5006585ba1cb74cc5c0ad6628
--- /dev/null
+++ b/game_core/src/world/generators/blobular.rs
@@ -0,0 +1,208 @@
+use bevy::math::uvec2;
+use fastrand::Rng;
+
+use crate::world::generators::utils::{draw_box, GenerationDirection};
+use crate::world::generators::{MapGenerator, TMP_FLOOR_GROUP};
+use crate::world::level_map::{Indexer, LevelMap, MapLayer, MapTile};
+
+pub struct Blobular;
+impl MapGenerator for Blobular {
+	fn generate(indexer: &Indexer, rng: &Rng) -> LevelMap {
+		let width = indexer.width();
+		let height = indexer.height();
+
+		let mut floor_layer = vec![0; width * height];
+		let mut wall_layer = vec![0; width * height];
+		let mut decoration_layer = vec![0; width * height];
+
+		let initial_x = width / 2;
+		let initial_y = width / 2;
+		let initial_width = rng.usize(3..=5);
+		let initial_height = rng.usize(3..=5);
+
+		draw_box(
+			initial_x - (6 / 2),
+			initial_y - (4 / 2),
+			6,
+			4,
+			TMP_FLOOR_GROUP,
+			&mut floor_layer,
+			&indexer,
+		);
+
+		draw_box(0, 0, 6, 10, TMP_FLOOR_GROUP, &mut floor_layer, &indexer);
+
+		// draw_box(
+		// 	initial_x - initial_width / 2,
+		// 	initial_y - initial_height / 2,
+		// 	initial_width,
+		// 	initial_height,
+		// 	TMP_FLOOR_GROUP,
+		// 	&mut floor_layer,
+		// 	&indexer,
+		// );
+		//
+		for _ in 0..1000 {
+			let initial = find_next_door(1000, &floor_layer, &rng, &indexer);
+			if let Some((next_x, next_y, direction)) = initial {
+				let size_selection = rng.usize(1..=10);
+				let (room_width, room_height) = if size_selection < 8 {
+					(rng.usize(3..=6), rng.usize(3..=6))
+				} else {
+					(rng.usize(7..=12), rng.usize(7..=12))
+				};
+
+				// Align room drawing to top left corner of new box
+				match direction {
+					GenerationDirection::Top => {
+						let start_x = next_x - (room_width / 4);
+						let start_y = next_y + room_height - 1;
+						draw_box(
+							start_x,
+							start_y,
+							room_width,
+							room_height,
+							TMP_FLOOR_GROUP,
+							&mut floor_layer,
+							&indexer,
+						);
+					}
+					GenerationDirection::Bottom => {
+						let start_x = next_x - (room_width / 4);
+						let start_y = next_y;
+						draw_box(
+							start_x,
+							start_y,
+							room_width,
+							room_height,
+							TMP_FLOOR_GROUP,
+							&mut floor_layer,
+							&indexer,
+						);
+					}
+					GenerationDirection::Left => {
+						let start_x = next_x - (room_width - 1);
+						let start_y = next_y;
+						draw_box(
+							start_x,
+							start_y,
+							room_width,
+							room_height,
+							TMP_FLOOR_GROUP,
+							&mut floor_layer,
+							&indexer,
+						);
+					}
+					GenerationDirection::Right => {
+						let start_x = next_x;
+						let start_y = next_y;
+						draw_box(
+							start_x,
+							start_y,
+							room_width,
+							room_height,
+							TMP_FLOOR_GROUP,
+							&mut floor_layer,
+							&indexer,
+						);
+					}
+					GenerationDirection::None => {}
+				}
+			}
+		}
+
+		LevelMap {
+			spawn: uvec2(initial_x as u32, initial_y as u32),
+			width,
+			height,
+			layers: vec![
+				MapLayer::from_sized_list(
+					width,
+					height,
+					floor_layer
+						.into_iter()
+						.map(|tile| match tile {
+							0 => None,
+							rest => Some(MapTile::new_floor(rest)),
+						})
+						.collect(),
+				),
+				MapLayer::from_sized_list(
+					width,
+					height,
+					wall_layer
+						.into_iter()
+						.map(|tile| match tile {
+							0 => None,
+							rest => Some(MapTile::new_wall(rest)),
+						})
+						.collect(),
+				),
+				MapLayer::from_sized_list(
+					width,
+					height,
+					decoration_layer
+						.into_iter()
+						.map(|tile| match tile {
+							0 => None,
+							rest => Some(MapTile::new_obstacle(rest)),
+						})
+						.collect(),
+				),
+			],
+		}
+	}
+}
+
+fn find_next_door(
+	max_attempts: usize,
+	floor: &Vec<usize>,
+	rng: &Rng,
+	idx: &Indexer,
+) -> Option<(usize, usize, GenerationDirection)> {
+	let mut remaining = max_attempts;
+	let mut found = None;
+	let mut last_direction = GenerationDirection::None;
+
+	while remaining > 0 && found.is_none() {
+		remaining = remaining.saturating_sub(1);
+
+		let target_index = rng.usize(0..floor.len());
+		let (x, y) = idx.reverse(target_index);
+
+		// Edges of the map are unsuitable for new rooms
+		if x == 0 || x == idx.width() - 1 || y == 0 || y == idx.height() - 1 {
+			continue;
+		}
+
+		let mut adjacents = 0;
+
+		// Check Left
+		if floor[idx.index(x - 1, y)] != 0 {
+			adjacents += 1;
+			last_direction = GenerationDirection::Right;
+		}
+		// Check Right
+		if floor[idx.index(x + 1, y)] != 0 {
+			adjacents += 1;
+			last_direction = GenerationDirection::Left;
+		}
+		// Check Top
+		if floor[idx.index(x, y + 1)] != 0 {
+			adjacents += 1;
+			last_direction = GenerationDirection::Bottom;
+		}
+		// Check Bottom
+		if floor[idx.index(x, y - 1)] != 0 {
+			adjacents += 1;
+			last_direction = GenerationDirection::Top;
+		}
+
+		if adjacents == 1 {
+			found = Some((x, y, last_direction));
+			break;
+		}
+	}
+
+	found
+}
diff --git a/game_core/src/world/generators/drunkard_corridor.rs b/game_core/src/world/generators/drunkard_corridor.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2161d994d8d69ebd5d9ff6d51a3e82bf75cfd3c7
--- /dev/null
+++ b/game_core/src/world/generators/drunkard_corridor.rs
@@ -0,0 +1,70 @@
+use bevy::math::uvec2;
+use fastrand::Rng;
+
+use crate::world::generators::{MapGenerator, TMP_FLOOR_GROUP};
+use crate::world::level_map::{Indexer, LevelMap, MapLayer, MapTile};
+
+pub struct DrunkardGenerator;
+impl MapGenerator for DrunkardGenerator {
+	fn generate(indexer: &Indexer, rng: &Rng) -> LevelMap {
+		let map_width = indexer.width();
+		let map_height = indexer.height();
+
+		let mut iterations = rng.usize(700..1500);
+		let mut floor = vec![0; map_width * map_height];
+
+		let start = uvec2(map_width as u32 / 2, map_height as u32 / 2);
+		let mut previous = start.clone();
+		floor[indexer.index(start.x as usize, start.y as usize)] = TMP_FLOOR_GROUP;
+
+		while iterations > 0 {
+			iterations -= 1;
+			let mut dx = 0;
+			let mut dy = 0;
+
+			match rng.u8(0..4) {
+				0 => {
+					dx += 1;
+				}
+				1 => {
+					dx -= 1;
+				}
+				2 => {
+					dy += 1;
+				}
+				3 => {
+					dy -= 1;
+				}
+				_ => unreachable!(),
+			}
+
+			let target_x = (previous.x as i32 + dx).max(0) as usize;
+			let target_y = (previous.y as i32 + dy).max(0) as usize;
+			let target_idx = indexer.index(target_x, target_y);
+			if indexer.index_within(target_idx) {
+				floor[target_idx] = TMP_FLOOR_GROUP;
+				previous = uvec2(target_x as u32, target_y as u32);
+			}
+		}
+
+		LevelMap::new(
+			map_width,
+			map_height,
+			start,
+			vec![MapLayer::from_sized_list(
+				map_width,
+				map_height,
+				floor
+					.into_iter()
+					.map(|tile| {
+						if tile == 0 {
+							None
+						} else {
+							Some(MapTile::new_floor(tile))
+						}
+					})
+					.collect(),
+			)],
+		)
+	}
+}
diff --git a/game_core/src/world/generators/mod.rs b/game_core/src/world/generators/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c8dc5112ebbcbd04b5e784d63abb4dc0af4bed2e
--- /dev/null
+++ b/game_core/src/world/generators/mod.rs
@@ -0,0 +1,14 @@
+use fastrand::Rng;
+
+use crate::world::level_map::{Indexer, LevelMap};
+
+pub mod blobular;
+pub mod drunkard_corridor;
+pub(crate) mod utils;
+
+pub(crate) const TMP_FLOOR_GROUP: usize = 6 * 64;
+pub(crate) const TMP_WALL_GROUP: usize = 3 * 64 + 28;
+
+pub trait MapGenerator {
+	fn generate(indexer: &Indexer, rng: &Rng) -> LevelMap;
+}
diff --git a/game_core/src/world/generators/utils.rs b/game_core/src/world/generators/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..51b926c6f9c7f74adc73c793ad57b15b1071248e
--- /dev/null
+++ b/game_core/src/world/generators/utils.rs
@@ -0,0 +1,63 @@
+use crate::world::level_map::Indexer;
+
+#[derive(Copy, Clone, Debug, Default)]
+pub enum GenerationDirection {
+	Top,
+	Bottom,
+	Left,
+	Right,
+	#[default]
+	None,
+}
+
+pub fn draw_box(
+	start_x: usize,
+	start_y: usize,
+	width: usize,
+	height: usize,
+	value: usize,
+	layer: &mut Vec<usize>,
+	indexer: &Indexer,
+) {
+	if !validate_box(start_x, start_y, width, height, layer, indexer) {
+		return;
+	}
+
+	for y in start_y..(start_y + height) {
+		for x in start_x..(start_x + width) {
+			let idx = indexer.index(x, y);
+			layer[idx] = value;
+		}
+	}
+}
+
+pub fn validate_box(
+	start_x: usize,
+	start_y: usize,
+	width: usize,
+	height: usize,
+	layer: &Vec<usize>,
+	indexer: &Indexer,
+) -> bool {
+	let map_width = indexer.width();
+	let map_height = indexer.height();
+
+	if start_y + height >= map_height {
+		return false;
+	}
+
+	if start_x + width >= map_width {
+		return false;
+	}
+
+	for x in start_x..(start_x + width) {
+		for y in start_y..(start_y + height) {
+			let idx = indexer.index(x, y);
+			if layer[idx] != 0 {
+				return false;
+			}
+		}
+	}
+
+	true
+}
diff --git a/game_core/src/world/handlers.rs b/game_core/src/world/handlers.rs
index 14d0d582c86cd923e3f635a2b3b49c6685623149..b528375ada7ab566c45d1c758a08dbb60b8e94ef 100644
--- a/game_core/src/world/handlers.rs
+++ b/game_core/src/world/handlers.rs
@@ -3,6 +3,7 @@ use bevy::prelude::*;
 
 use crate::assets::AssetHandles;
 use crate::entities::lifecycle::GameEntity;
+use crate::entities::spawner::EntitySpawner;
 use crate::system::graphics::LAYER_TILE;
 use crate::world::adjacency::get_floor_sprite_offset;
 use crate::world::level_map::{GridPosition, Indexer, LevelMap, Tile, WORLD_TILE_SIZE};
@@ -10,6 +11,7 @@ use crate::world::level_map::{GridPosition, Indexer, LevelMap, Tile, WORLD_TILE_
 pub fn spawn_new_world(
 	mut commands: Commands,
 	assets: Res<AssetHandles>,
+	mut spawner: EntitySpawner,
 	query: Query<(Entity, &LevelMap), Added<LevelMap>>,
 ) {
 	for (entity, map) in &query {
@@ -41,5 +43,7 @@ pub fn spawn_new_world(
 				}
 			});
 		}
+
+		spawner.spawn_player(map.spawn);
 	}
 }
diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs
index b674c1dd3496a4f722f7622256dda706a9259f8d..57be529f07c5d962c65d203e4e76ff5901a37563 100644
--- a/game_core/src/world/level_map.rs
+++ b/game_core/src/world/level_map.rs
@@ -1,12 +1,12 @@
 use std::fmt::{Debug, Formatter};
-use std::ops::Deref;
+use std::ops::{Deref, DerefMut};
 
 use bevy::math::UVec2;
 use bevy::prelude::*;
 use fastrand::Rng;
 
 use crate::world::adjacency::{BOTTOM, LEFT, NONE, RIGHT, TOP};
-use crate::world::generation::{generate_map, generate_map_with_rng, generate_map_with_seed};
+use crate::world::generators::MapGenerator;
 
 pub const WORLD_TILE_SIZE: f32 = 16.0;
 
@@ -24,6 +24,11 @@ impl Deref for GridPosition {
 		&self.0
 	}
 }
+impl DerefMut for GridPosition {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
 
 /// Take an entity's position on a grid, and sync it up to the transform used to render
 /// the sprite
@@ -147,6 +152,18 @@ pub struct LevelMap {
 	pub width: usize,
 	pub height: usize,
 	pub layers: Vec<MapLayer>,
+	pub spawn: UVec2,
+}
+
+impl LevelMap {
+	pub fn new(width: usize, height: usize, spawn: UVec2, layers: Vec<MapLayer>) -> Self {
+		LevelMap {
+			width,
+			height,
+			layers,
+			spawn,
+		}
+	}
 }
 
 #[derive(Bundle)]
@@ -167,14 +184,15 @@ impl LevelMapBundle {
 		}
 	}
 
-	pub fn generate(width: usize, height: usize) -> Self {
-		Self::new(generate_map(width, height))
+	pub fn generate<Gen: MapGenerator>(width: usize, height: usize) -> Self {
+		Self::generate_with_rng::<Gen>(&Rng::new(), width, height)
 	}
-	pub fn generate_with_seed(seed: u64, width: usize, height: usize) -> Self {
-		Self::new(generate_map_with_seed(seed, width, height))
+	pub fn generate_with_seed<Gen: MapGenerator>(seed: u64, width: usize, height: usize) -> Self {
+		Self::generate_with_rng::<Gen>(&Rng::with_seed(seed), width, height)
 	}
-	pub fn generate_with_rng(rng: &Rng, width: usize, height: usize) -> Self {
-		Self::new(generate_map_with_rng(rng, width, height))
+	pub fn generate_with_rng<Gen: MapGenerator>(rng: &Rng, width: usize, height: usize) -> Self {
+		let indexer = Indexer::new(width, height);
+		LevelMapBundle::new(Gen::generate(&indexer, rng))
 	}
 }
 
@@ -192,7 +210,11 @@ impl Indexer {
 		(y * self.width) + x
 	}
 	pub fn reverse(&self, index: usize) -> (usize, usize) {
-		(index / self.width, index % self.width)
+		(index % self.width, index / self.width)
+	}
+	pub fn index_within(&self, idx: usize) -> bool {
+		let (x, y) = self.reverse(idx);
+		x >= 0 && x < self.width && y >= 0 && y < self.height
 	}
 	pub fn width(&self) -> usize {
 		self.width
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
index 0997770ae74867bef99c1e8ee6cb3457d44ea0a5..0e3838efdb8c565847bac2a8e99342f67657c605 100644
--- a/game_core/src/world/mod.rs
+++ b/game_core/src/world/mod.rs
@@ -1,9 +1,9 @@
 pub mod adjacency;
-pub mod generation;
+pub mod generators;
 pub mod handlers;
 pub mod level_map;
 
-mod __internal {
+mod __plugin {
 	use bevy::app::{App, CoreStage, Plugin};
 	use iyes_loopless::condition::ConditionSet;
 
@@ -24,4 +24,4 @@ mod __internal {
 	}
 }
 
-pub use __internal::WorldPlugin;
+pub use __plugin::WorldPlugin;