diff --git a/Cargo.lock b/Cargo.lock
index c94aed1c5b0af214129004fb50fd2737cf7971d7..1c95b736ee6c90c505d3cad2ef52ea762398986b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -30,7 +30,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.8",
  "once_cell",
  "version_check",
 ]
@@ -344,6 +344,16 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "bevy_ecs_tilemap"
+version = "0.9.0"
+source = "git+https://github.com/StarArawn/bevy_ecs_tilemap?rev=eb20fcaccdd253ea5bf280cac7ffc5a69b674df2#eb20fcaccdd253ea5bf280cac7ffc5a69b674df2"
+dependencies = [
+ "bevy",
+ "log",
+ "regex",
+]
+
 [[package]]
 name = "bevy_encase_derive"
 version = "0.9.1"
@@ -780,7 +790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "16750aae52cd35bd7b60eb61cee883420b250e11b4a290b8d44b2b2941795739"
 dependencies = [
  "ahash",
- "getrandom",
+ "getrandom 0.2.8",
  "hashbrown",
  "instant",
  "tracing",
@@ -1441,6 +1451,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "bevy",
+ "bevy_ecs_tilemap 0.9.0 (git+https://github.com/StarArawn/bevy_ecs_tilemap?rev=eb20fcaccdd253ea5bf280cac7ffc5a69b674df2)",
  "bevy_tweening",
  "fastrand",
  "iyes_loopless",
@@ -1448,6 +1459,7 @@ dependencies = [
  "micro_asset_io",
  "micro_banimate",
  "micro_musicbox",
+ "noise",
  "serde",
  "serde_json",
  "thiserror",
@@ -1455,6 +1467,17 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.8"
@@ -1464,7 +1487,7 @@ dependencies = [
  "cfg-if",
  "js-sys",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "wasm-bindgen",
 ]
 
@@ -1965,7 +1988,7 @@ checksum = "db0f957d47035e81cda7636e2c9e17aa8fb8816a29bd1739935893b605ad4a09"
 dependencies = [
  "anyhow",
  "bevy",
- "bevy_ecs_tilemap",
+ "bevy_ecs_tilemap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde",
  "serde_json",
 ]
@@ -2003,7 +2026,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
 dependencies = [
  "libc",
  "log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.42.0",
 ]
 
@@ -2148,6 +2171,17 @@ dependencies = [
  "memoffset",
 ]
 
+[[package]]
+name = "noise"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ba869e17168793186c10ca82c7079a4ffdeac4f1a7d9e755b9491c028180e40"
+dependencies = [
+ "num-traits",
+ "rand",
+ "rand_xorshift",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -2395,6 +2429,12 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
 [[package]]
 name = "proc-macro-crate"
 version = "1.2.1"
@@ -2436,6 +2476,56 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b"
 
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8"
+dependencies = [
+ "rand_core",
+]
+
 [[package]]
 name = "range-alloc"
 version = "0.1.2"
@@ -3015,7 +3105,7 @@ version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.8",
  "serde",
 ]
 
@@ -3054,6 +3144,12 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
diff --git a/assets/resources.apack b/assets/resources.apack
index b3121861cf1be79f9eaebfdcd0074d351e85a440..1f14276a8d0e7aeeac57a6708122b23946fdcd1c 100644
--- a/assets/resources.apack
+++ b/assets/resources.apack
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:14b68833d9afa61d753201afec2aa717ed8266344799c5a46bdb03892288331a
-size 44828
+oid sha256:e279a1551f7c02cfba8b16f358e4bf8ae62065e97d511f935984f5548a20fbf5
+size 49928
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index af78c8b55d9699089c7d2dd1150694471dc23aa3..59880345d99d8b513f08625262388a0c0b16ea1c 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -21,8 +21,10 @@ micro_musicbox.workspace = true
 
 micro_asset_io = { path = "../micro_asset_io" }
 
+bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap", rev = "eb20fcaccdd253ea5bf280cac7ffc5a69b674df2" }
 bevy_tweening = "0.6.0"
 toml = "0.5.9"
+noise = "0.8.2"
 
 #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
 
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index cd6d8dcfd7d64c2e9d4066f4b458ed62d3343301..d487f56ae4e6cfd195d84d3fa124ee1fe6d0fec9 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -5,3 +5,4 @@ pub mod multiplayer;
 pub mod splash_screen;
 pub mod states;
 pub mod system;
+pub mod world;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index ba118436d60bf522ed2e16438c5a429e1ff02229..f41b9b4ffec62f4b80e41baa2c8d82a9ed3937c5 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_ecs_tilemap::TilemapPlugin;
 use game_core::assets::AssetHandles;
 use game_core::system::flow::AppState;
 use game_core::system::resources::InitAppPlugins;
@@ -16,5 +17,7 @@ fn main() {
 		.add_plugin(game_core::states::StatesPlugin)
 		.add_plugin(micro_asset_io::MicroAssetIOPlugin)
 		.add_plugin(bevy_tweening::TweeningPlugin)
+		.add_plugin(game_core::world::WorldPlugin)
+		.add_plugin(TilemapPlugin)
 		.run();
 }
diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs
index e0638347a92677d9bbc5b4df9598f7b8e2c5b7cf..23ab273af1a8ac5c9b35a3e6774fd3f70079e2bd 100644
--- a/game_core/src/states/menu_state.rs
+++ b/game_core/src/states/menu_state.rs
@@ -3,8 +3,10 @@ use std::time::Duration;
 use bevy::prelude::*;
 use bevy_tweening::lens::TextColorLens;
 use bevy_tweening::{Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween};
+use iyes_loopless::state::NextState;
 
 use crate::assets::AssetHandles;
+use crate::system::flow::AppState;
 
 #[derive(Component)]
 pub struct MenuStateEntity;
@@ -19,15 +21,18 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
 	));
 
 	commands
-		.spawn(NodeBundle {
-			style: Style {
-				size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
-				flex_direction: FlexDirection::Column,
-				align_items: AlignItems::Center,
+		.spawn((
+			MenuStateEntity,
+			NodeBundle {
+				style: Style {
+					size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
+					flex_direction: FlexDirection::Column,
+					align_items: AlignItems::Center,
+					..Default::default()
+				},
 				..Default::default()
 			},
-			..Default::default()
-		})
+		))
 		.with_children(|commands| {
 			commands.spawn(TextBundle {
 				text: Text::from_section(
@@ -47,7 +52,7 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
 			commands.spawn((
 				TextBundle {
 					text: Text::from_section(
-						"> Get Trading <",
+						"> Press Space <",
 						TextStyle {
 							font_size: 48.0,
 							font: assets.font("default"),
@@ -77,6 +82,12 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
 		});
 }
 
+pub fn go_to_game(input: Res<Input<KeyCode>>, mut commands: Commands) {
+	if input.just_released(KeyCode::Space) {
+		commands.insert_resource(NextState(AppState::InGame));
+	}
+}
+
 pub fn despawn_menu_entities(mut commands: Commands, query: Query<Entity, With<MenuStateEntity>>) {
 	for entity in &query {
 		commands.entity(entity).despawn_recursive();
diff --git a/game_core/src/states/mod.rs b/game_core/src/states/mod.rs
index 20d5cbde4e5d2574926702faf92e26d62847f9d0..a75b179f11527d0472451760455a3929f6957d0a 100644
--- a/game_core/src/states/mod.rs
+++ b/game_core/src/states/mod.rs
@@ -1,6 +1,7 @@
 use bevy::app::{App, Plugin};
-use iyes_loopless::prelude::AppLooplessStateExt;
+use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
 
+use crate::states::menu_state::go_to_game;
 use crate::system::flow::AppState;
 
 mod menu_state;
@@ -9,6 +10,12 @@ pub struct StatesPlugin;
 impl Plugin for StatesPlugin {
 	fn build(&self, app: &mut App) {
 		app.add_enter_system(AppState::Menu, menu_state::spawn_menu_entities)
-			.add_exit_system(AppState::Menu, menu_state::despawn_menu_entities);
+			.add_exit_system(AppState::Menu, menu_state::despawn_menu_entities)
+			.add_system_set(
+				ConditionSet::new()
+					.run_in_state(AppState::Menu)
+					.with_system(go_to_game)
+					.into(),
+			);
 	}
 }
diff --git a/game_core/src/system/camera.rs b/game_core/src/system/camera.rs
index e3caa697be976867215dfc1f8cb7075486102bee..072a1856af668db76a8143c1461f1325d7fd2867 100644
--- a/game_core/src/system/camera.rs
+++ b/game_core/src/system/camera.rs
@@ -1,14 +1,16 @@
 use bevy::app::App;
+use bevy::input::mouse::MouseMotion;
 use bevy::math::{Vec2, Vec3Swizzles};
 use bevy::prelude::{
-	Camera2dBundle, Commands, Component, CoreStage, Entity, OrthographicProjection, Plugin, Query,
-	Transform, With,
+	Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, MouseButton,
+	OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With,
 };
 use bevy::render::camera::ScalingMode;
-use iyes_loopless::prelude::AppLooplessStateExt;
+use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
 
 use crate::system::flow::AppState;
 use crate::system::load_config::virtual_size;
+use crate::system::window::WindowManager;
 
 /// A flag component to indicate which entity should be followed by the camera
 #[derive(Component)]
@@ -39,6 +41,25 @@ pub fn spawn_orthographic_camera(mut commands: Commands) {
 	));
 }
 
+pub fn pan_camera(
+	mut ev_motion: EventReader<MouseMotion>,
+	input_mouse: Res<Input<MouseButton>>,
+	mut query: Query<(&mut Transform, &Projection), With<GameCamera>>,
+	windows: Res<Windows>,
+) {
+	let mut pan = Vec2::ZERO;
+
+	if input_mouse.pressed(MouseButton::Right) {
+		for ev in ev_motion.iter() {
+			pan += ev.delta;
+		}
+	}
+
+	for (mut transform, _) in &mut query {
+		if pan.length_squared() > 0.0 {}
+	}
+}
+
 /// System that takes the average location of all chase camera entities, and updates the location
 /// of all world cameras to track the average location.
 ///
@@ -49,6 +70,10 @@ pub fn sync_chase_camera_location(
 	chased_query: Query<&Transform, With<ChaseCam>>,
 	camera_query: Query<(Entity, &Transform), With<GameCamera>>,
 ) {
+	if chased_query.is_empty() {
+		return;
+	}
+
 	let mut average_location = Vec2::new(0.0, 0.0);
 	let mut count = 0;
 	for location in chased_query.iter() {
@@ -75,6 +100,12 @@ pub struct CameraManagementPlugin;
 impl Plugin for CameraManagementPlugin {
 	fn build(&self, app: &mut App) {
 		app.add_enter_system(AppState::Preload, spawn_orthographic_camera)
-			.add_system_to_stage(CoreStage::PreUpdate, sync_chase_camera_location);
+			.add_system_to_stage(CoreStage::PreUpdate, sync_chase_camera_location)
+			.add_system_set(
+				ConditionSet::new()
+					.run_in_state(AppState::InGame)
+					.with_system(pan_camera)
+					.into(),
+			);
 	}
 }
diff --git a/game_core/src/system/utilities.rs b/game_core/src/system/utilities.rs
index 0244a876e4e91ca99e6e4935d84ac387b8b956fb..180bb4226b0a162371f898f14644acc439fd5f39 100644
--- a/game_core/src/system/utilities.rs
+++ b/game_core/src/system/utilities.rs
@@ -1,3 +1,7 @@
+use std::ops::{Deref, DerefMut};
+
+use bevy::prelude::UVec2;
+
 #[inline]
 pub fn f32_max(a: f32, b: f32) -> f32 {
 	if a > b {
@@ -30,3 +34,54 @@ pub fn f32_min_mag(a: f32, b: f32) -> f32 {
 		b.abs()
 	}
 }
+
+#[derive(Debug, Copy, Clone)]
+pub struct Indexer {
+	width: usize,
+	height: usize,
+}
+
+impl Indexer {
+	pub fn new(width: usize, height: usize) -> Self {
+		Indexer { width, height }
+	}
+	pub fn index(&self, x: usize, y: usize) -> usize {
+		(y * self.width) + x
+	}
+	pub fn checked_index(&self, x: usize, y: usize) -> Option<usize> {
+		if self.is_coordinate_valid(x, y) {
+			Some(self.index(x, y))
+		} else {
+			None
+		}
+	}
+	pub fn reverse(&self, index: usize) -> (usize, usize) {
+		(index % self.width, index / self.width)
+	}
+	pub fn checked_reverse(&self, idx: usize) -> Option<(usize, usize)> {
+		if self.is_index_valid(idx) {
+			Some(self.reverse(idx))
+		} else {
+			None
+		}
+	}
+	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 is_uvec2_valid(&self, point: UVec2) -> bool {
+		self.is_coordinate_valid(point.x as usize, point.y as usize)
+	}
+	pub fn is_coordinate_valid(&self, x: usize, y: usize) -> bool {
+		x < self.width && y < self.height
+	}
+	pub fn is_index_valid(&self, idx: usize) -> bool {
+		idx < self.width * self.height
+	}
+	pub fn width(&self) -> usize {
+		self.width
+	}
+	pub fn height(&self) -> usize {
+		self.height
+	}
+}
diff --git a/game_core/src/world/debug_gen.rs b/game_core/src/world/debug_gen.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d8481accaf111268b0a819c6e1760ce879d457a1
--- /dev/null
+++ b/game_core/src/world/debug_gen.rs
@@ -0,0 +1,55 @@
+use bevy::prelude::*;
+use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
+use bevy::render::texture::{BevyDefault, TextureFormatPixelInfo};
+use fastrand::Rng;
+
+use crate::system::utilities::Indexer;
+use crate::world::generate_overworld::{generate_overworld, WorldTileType};
+
+pub fn generate_and_spawn_world(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
+	let world = generate_overworld(300, 300, Rng::new());
+	let format = TextureFormat::bevy_default();
+
+	let mut img = Image::new_fill(
+		Extent3d {
+			height: 300,
+			width: 300,
+			depth_or_array_layers: 1,
+		},
+		TextureDimension::D2,
+		vec![100; format.pixel_size()].as_slice(),
+		TextureFormat::bevy_default(),
+	);
+
+	log::info!("DOING IT");
+	log::info!("{:?}", format.describe());
+
+	let mut write_pixel = |index: usize, data: &[u8; 4]| {
+		img.data[index * format.pixel_size()] = data[0];
+		img.data[index * format.pixel_size() + 1] = data[1];
+		img.data[index * format.pixel_size() + 2] = data[2];
+		if format.pixel_size() == 4 {
+			img.data[index * format.pixel_size() + 3] = data[3];
+		}
+	};
+
+	for (index, tile) in world.terrain.iter().enumerate() {
+		match tile {
+			WorldTileType::Water => write_pixel(index, &[30, 144, 255, 255]),
+			WorldTileType::Sand => write_pixel(index, &[255, 255, 224, 255]),
+			WorldTileType::Dirt => write_pixel(index, &[165, 42, 42, 255]),
+			WorldTileType::Grass => write_pixel(index, &[34, 139, 34, 255]),
+			WorldTileType::Mountain => write_pixel(index, &[105, 105, 105, 255]),
+			WorldTileType::Swamp => write_pixel(index, &[75, 0, 130, 255]),
+			WorldTileType::Snow => write_pixel(index, &[240, 255, 255, 255]),
+			WorldTileType::Desert => write_pixel(index, &[240, 230, 140, 255]),
+		}
+	}
+
+	let handle = images.add(img);
+	commands.spawn(SpriteBundle {
+		texture: handle,
+		transform: Transform::from_xyz(0.0, 0.0, 100.0),
+		..Default::default()
+	});
+}
diff --git a/game_core/src/world/generate_overworld.rs b/game_core/src/world/generate_overworld.rs
new file mode 100644
index 0000000000000000000000000000000000000000..94702f068578f25aa2f8b2cc607169bfda1a63a0
--- /dev/null
+++ b/game_core/src/world/generate_overworld.rs
@@ -0,0 +1,335 @@
+use std::env::var;
+
+use bevy::math::{uvec2, UVec2};
+use fastrand::Rng;
+use noise::{Blend, Clamp, NoiseFn, Perlin, Simplex};
+
+use crate::system::utilities::Indexer;
+
+pub struct Overworld {
+	pub width: usize,
+	pub height: usize,
+	pub terrain: Vec<WorldTileType>,
+}
+
+#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
+#[repr(C)]
+pub enum WorldTileType {
+	Water = 0,
+	Sand = 1,
+	Dirt = 2,
+	Grass = 3,
+	Mountain = 4,
+	Swamp = 5,
+	Snow = 6,
+	Desert = 7,
+}
+
+impl WorldTileType {
+	pub fn get_basic_tile(&self) -> usize {
+		match self {
+			WorldTileType::Water => 225,
+			WorldTileType::Sand => vec![294, 295, 296, 297, 298][fastrand::usize(0..5)],
+			WorldTileType::Dirt => vec![1, 2, 3, 4, 5][fastrand::usize(0..5)],
+			WorldTileType::Grass => 0,
+			WorldTileType::Mountain => 68, // vec![68, 6, 7, 8, 9, 10][fastrand::usize(0..6)],
+			WorldTileType::Swamp => vec![43, 44, 45][fastrand::usize(0..3)],
+			WorldTileType::Snow => vec![11, 12, 13, 14][fastrand::usize(0..4)],
+			WorldTileType::Desert => vec![326, 327, 328, 329, 330][fastrand::usize(0..5)],
+		}
+	}
+}
+
+fn cap_variance(var: &mut usize) {
+	*var = (*var).clamp(35, 65);
+}
+
+fn adjust_variance(var: &mut usize, rng: &Rng) {
+	if (rng.f32() - (*var as f32 / 100.0)) > 0.3 {
+		*var = var.saturating_add(rng.usize(0..2));
+	} else {
+		*var = var.saturating_sub(rng.usize(0..2));
+	}
+}
+
+fn random_ground_spot(indexer: &Indexer, values: &[WorldTileType], rng: &Rng) -> UVec2 {
+	let width = indexer.width() as u32;
+	let height = indexer.height() as u32;
+
+	let min_x = width / 4;
+	let min_y = height / 4;
+	let max_x = (width / 4) * 3;
+	let max_y = (height / 4) * 3;
+
+	let mut attempts = 500;
+	while attempts > 0 {
+		let guess_x = rng.u32(min_x..=max_x);
+		let guess_y = rng.u32(min_y..=max_y);
+
+		if values[indexer.index(guess_x as usize, guess_y as usize)] == WorldTileType::Grass {
+			return uvec2(guess_x, guess_y);
+		}
+
+		attempts -= 1;
+	}
+
+	UVec2::new(0, 0)
+}
+
+fn draw_across(
+	x: usize,
+	y: usize,
+	width: usize,
+	tile: WorldTileType,
+	idx: &Indexer,
+	values: &mut [WorldTileType],
+) {
+	let half = width / 2;
+	for x_val in x - half..x + half {
+		if idx.is_coordinate_valid(x_val, y) && values[idx.index(x_val, y)] == WorldTileType::Grass
+		{
+			values[idx.index(x_val, y)] = tile;
+		}
+	}
+}
+
+fn draw_updown(
+	x: usize,
+	y: usize,
+	height: usize,
+	tile: WorldTileType,
+	idx: &Indexer,
+	values: &mut [WorldTileType],
+) {
+	let half = height / 2;
+	for y_val in y - half..y + half {
+		if idx.is_coordinate_valid(x, y_val) && values[idx.index(x, y_val)] == WorldTileType::Grass
+		{
+			values[idx.index(x, y_val)] = tile;
+		}
+	}
+}
+
+pub fn count_world_tiles(world: &Overworld, tile_type: WorldTileType) -> usize {
+	world.terrain.iter().fold(0, |count, current| {
+		if *current == tile_type {
+			count + 1
+		} else {
+			count
+		}
+	})
+}
+
+pub fn generate_overworld(width: usize, height: usize, rng: Rng) -> Overworld {
+	let mut attempts = 100;
+	let mut world = generate_overworld_data(width, height, &rng);
+
+	loop {
+		let total_tiles = width * height;
+		let water_tiles = count_world_tiles(&world, WorldTileType::Water);
+		let mountain_tiles = count_world_tiles(&world, WorldTileType::Mountain);
+		let ground_tiles = count_world_tiles(&world, WorldTileType::Grass);
+
+		// Regen world if it's not too good:
+		// - more than 10% mountain
+		// - less than 50% regular grass
+		if mountain_tiles > ((total_tiles - water_tiles) / 10)
+			|| ground_tiles < ((total_tiles - water_tiles) / 2)
+		{
+			world = generate_overworld_data(width, height, &rng);
+		} else {
+			return world;
+		}
+
+		attempts -= 1;
+		if attempts == 0 {
+			break;
+		}
+	}
+
+	world
+}
+
+pub fn generate_overworld_data(width: usize, height: usize, rng: &Rng) -> Overworld {
+	let indexer = Indexer::new(width, height);
+	let mut values = vec![WorldTileType::Grass; width * height];
+
+	let mut variance = 50;
+
+	for x in 0..width {
+		let mut y = variance;
+		for y_val in 0..y {
+			values[indexer.index(x, y_val)] = WorldTileType::Water;
+		}
+		for y_val in y..y + (y / 2) {
+			values[indexer.index(x, y_val)] = WorldTileType::Sand;
+		}
+
+		adjust_variance(&mut variance, &rng);
+	}
+
+	cap_variance(&mut variance);
+
+	for x in 0..width {
+		let mut y = height - variance;
+		for y_val in y..height {
+			values[indexer.index(x, y_val)] = WorldTileType::Water;
+		}
+		for y_val in y - (variance / 2)..y {
+			values[indexer.index(x, y_val)] = WorldTileType::Sand;
+		}
+
+		adjust_variance(&mut variance, &rng);
+	}
+
+	cap_variance(&mut variance);
+
+	for y in 0..width {
+		let mut x = variance;
+		for x_val in 0..x {
+			values[indexer.index(x_val, y)] = WorldTileType::Water;
+		}
+		for x_val in x..x + (x / 2) {
+			if values[indexer.index(x_val, y)] == WorldTileType::Water {
+				break;
+			}
+			values[indexer.index(x_val, y)] = WorldTileType::Sand;
+		}
+
+		adjust_variance(&mut variance, &rng);
+	}
+
+	cap_variance(&mut variance);
+
+	for y in 0..width {
+		let mut x = width - variance;
+		for x_val in x..width {
+			values[indexer.index(x_val, y)] = WorldTileType::Water;
+		}
+		// + (rng.usize(0..(variance / 2).max(1))))
+		for x_val in x - (variance / 2)..x {
+			if values[indexer.index(x_val, y)] == WorldTileType::Water {
+				break;
+			}
+			values[indexer.index(x_val, y)] = WorldTileType::Sand;
+		}
+
+		adjust_variance(&mut variance, &rng);
+	}
+
+	let mountains = rng.usize(1..4);
+	for _ in 0..mountains {
+		let position = random_ground_spot(&indexer, values.as_slice(), &rng);
+		let width = rng.usize(35..65);
+		let height = rng.usize(35..65);
+		let vertical = rng.bool();
+
+		if vertical {
+			let mut width_remaining = width;
+			for y in position.y as usize..position.y as usize + (height / 3) * 2 {
+				if width_remaining == 0 {
+					break;
+				}
+
+				draw_across(
+					position.x as usize,
+					y,
+					width_remaining,
+					WorldTileType::Mountain,
+					&indexer,
+					&mut values,
+				);
+
+				width_remaining -= rng.usize(0..3);
+			}
+
+			let mut width_remaining = width;
+			for y in (position.y as usize - (height / 3)..position.y as usize).rev() {
+				if width_remaining == 0 {
+					break;
+				}
+
+				draw_across(
+					position.x as usize,
+					y,
+					width_remaining,
+					WorldTileType::Mountain,
+					&indexer,
+					&mut values,
+				);
+
+				width_remaining -= rng.usize(0..4);
+			}
+		} else {
+			let mut height_remaining = height;
+			for x in position.x as usize..position.x as usize + (width / 3) * 2 {
+				if height_remaining == 0 {
+					break;
+				}
+
+				draw_updown(
+					x,
+					position.y as usize,
+					height_remaining,
+					WorldTileType::Mountain,
+					&indexer,
+					&mut values,
+				);
+
+				height_remaining -= rng.usize(0..3);
+			}
+
+			let mut height_remaining = height;
+			for x in (position.x as usize - (width / 3)..position.x as usize).rev() {
+				if height_remaining == 0 {
+					break;
+				}
+
+				draw_updown(
+					x,
+					position.y as usize,
+					height_remaining,
+					WorldTileType::Mountain,
+					&indexer,
+					&mut values,
+				);
+
+				height_remaining -= rng.usize(0..4);
+			}
+		}
+	}
+	//
+	let biome_noise = Blend::new(
+		Perlin::new(rng.u32(0..u32::MAX)),
+		Simplex::new(rng.u32(0..u32::MAX)),
+		Perlin::new(rng.u32(0..u32::MAX)),
+	);
+
+	// let biome_noise = Clamp::new(Simplex::new(rng.u32(0..u32::MAX)));
+
+	for x in 0..width {
+		for y in 0..height {
+			let value = biome_noise
+				.get([x as f64 / width as f64, y as f64 / height as f64])
+				.abs();
+
+			if values[indexer.index(x, y)] == WorldTileType::Grass {
+				if value > 0.8 {
+					values[indexer.index(x, y)] = WorldTileType::Mountain;
+				} else if value > 0.6 {
+					values[indexer.index(x, y)] = WorldTileType::Snow;
+				} else if value > 0.3 && value < 0.45 {
+					values[indexer.index(x, y)] = WorldTileType::Desert;
+				} else if value > 0.15 && value <= 0.2 {
+					values[indexer.index(x, y)] = WorldTileType::Swamp;
+				}
+			}
+		}
+	}
+
+	Overworld {
+		terrain: values,
+		width,
+		height,
+	}
+}
diff --git a/game_core/src/world/handle_overworld.rs b/game_core/src/world/handle_overworld.rs
new file mode 100644
index 0000000000000000000000000000000000000000..27f432d0158c37838bad6257ac22030863aead92
--- /dev/null
+++ b/game_core/src/world/handle_overworld.rs
@@ -0,0 +1,78 @@
+use bevy::math::vec3;
+use bevy::prelude::*;
+use bevy_ecs_tilemap::map::{TilemapSize, TilemapTileSize, TilemapType};
+use bevy_ecs_tilemap::prelude::{TilePos, TileTextureIndex, TilemapId, TilemapTexture};
+use bevy_ecs_tilemap::tiles::{TileBundle, TileStorage};
+use bevy_ecs_tilemap::TilemapBundle;
+use fastrand::Rng;
+
+use crate::assets::AssetHandles;
+use crate::system::camera::GameCamera;
+use crate::system::utilities::Indexer;
+use crate::world::generate_overworld::{generate_overworld, Overworld};
+
+pub fn spawn_new_map(
+	mut commands: Commands,
+	assets: Res<AssetHandles>,
+	mut query: Query<&mut Transform, With<GameCamera>>,
+) {
+	let world = generate_overworld(300, 300, Rng::new());
+	spawn_overworld(&world, &mut commands, &assets);
+	for mut camera_transform in &mut query {
+		log::info!("SET CAM TRANSFORM");
+		camera_transform.translation =
+			vec3(150.0 * 4.0, 150.0 * 4.0, camera_transform.translation.z);
+	}
+}
+
+pub fn spawn_overworld(
+	overworld: &Overworld,
+	commands: &mut Commands,
+	assets: &Res<AssetHandles>,
+) -> Entity {
+	let indexer = Indexer::new(overworld.width, overworld.height);
+	let tilemap_size = TilemapSize {
+		x: overworld.width as u32,
+		y: overworld.height as u32,
+	};
+
+	let mut map_entity = commands.spawn_empty().id();
+	let mut tile_storage = TileStorage::empty(tilemap_size);
+
+	for (idx, tile) in overworld.terrain.iter().enumerate() {
+		let (x, y) = indexer.reverse(idx);
+
+		let tile_pos = TilePos {
+			x: x as u32,
+			y: y as u32,
+		};
+
+		let tile_entity = commands
+			.spawn(TileBundle {
+				position: tile_pos,
+				tilemap_id: TilemapId(map_entity),
+				texture_index: TileTextureIndex(tile.get_basic_tile() as u32),
+				..Default::default()
+			})
+			.id();
+
+		tile_storage.set(&tile_pos, tile_entity)
+	}
+
+	let tile_size = TilemapTileSize { x: 4.0, y: 4.0 };
+	let grid_size = tile_size.into();
+	let map_type = TilemapType::Square;
+
+	commands.entity(map_entity).insert(TilemapBundle {
+		grid_size,
+		tile_size,
+		size: tilemap_size,
+		storage: tile_storage,
+		texture: TilemapTexture::Single(assets.image("overworld")),
+		map_type,
+		transform: Transform::from_xyz(0.0, 0.0, 100.0),
+		..Default::default()
+	});
+
+	map_entity
+}
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f9931258f6b783ec6d1ac18f0ba83f0d0387e288
--- /dev/null
+++ b/game_core/src/world/mod.rs
@@ -0,0 +1,18 @@
+use bevy::app::App;
+use bevy::prelude::Plugin;
+use fastrand::Rng;
+use iyes_loopless::prelude::AppLooplessStateExt;
+
+use crate::system::flow::AppState;
+use crate::world::debug_gen::generate_and_spawn_world;
+
+mod debug_gen;
+mod generate_overworld;
+mod handle_overworld;
+
+pub struct WorldPlugin;
+impl Plugin for WorldPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_enter_system(AppState::InGame, handle_overworld::spawn_new_map);
+	}
+}