diff --git a/assets/config/config.phys b/assets/config/config.phys
index 727ab38ab46f77c6aca99964dee91de9d5f02e80..97800d765c671c6563e96fb53d92958cff0dfd9a 100644
--- a/assets/config/config.phys
+++ b/assets/config/config.phys
@@ -1,5 +1,5 @@
 {
-    "gravity": -123.0,
+    "gravity": -200.0,
     "jump": 200.0,
     "speed": 150.0
 }
\ No newline at end of file
diff --git a/assets/sprites/world.png b/assets/sprites/world.png
index e917b8fabbd77c5668553d489c87770dcda13677..b97f306515e0258af40be3e36abf7cfc93702107 100644
--- a/assets/sprites/world.png
+++ b/assets/sprites/world.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:eb63be0e3ef42965bba1364620643497fa267fa18894debb85a74c8f3faa59b5
-size 43342
+oid sha256:619c5382db0bb237eab80f42d6c45818f888b9c33305e5b8bf013b6b01350d7c
+size 43302
diff --git a/assets/sprites/world_ext.png b/assets/sprites/world_ext.png
new file mode 100644
index 0000000000000000000000000000000000000000..6cd47d8ccc15fef47d16d71dd93833691bbf9f34
--- /dev/null
+++ b/assets/sprites/world_ext.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d3d81edc05aed20cf92d0fcee4d04d98c0fe620cf10a662adfa2090dbfe7ea5c
+size 140389
diff --git a/assets/world.ldtk b/assets/world.ldtk
index 7baf762e183bfc8806dc7bd2428d640e554a0726..1891c61064ae7bfe734e87543a69584de62dae59 100644
--- a/assets/world.ldtk
+++ b/assets/world.ldtk
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:768f14ee00b4997d06c01dca23d1d74620bd0a7b5d1e210a189c22b72d53a6d8
+oid sha256:ef25cb1aa8d9a56f5979927b6fd97c3bd20359644648a4fd097b6dce38d3072f
 size 93007
diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs
index 7ad3ff274a92f9c9a03540edde7d0b083f507c51..d562f58c673a044835c406fa414318c48d17893f 100644
--- a/game_core/src/assets/loader.rs
+++ b/game_core/src/assets/loader.rs
@@ -64,7 +64,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 		assets: &[FixedAssetNameMapping],
 	) -> Vec<Handle<Image>> {
 		self.load_list(assets, |loader, path, key| {
-			let handle: Handle<Image> = loader.asset_server.load(&path);
+			let handle: Handle<Image> = loader.asset_server.load(path);
 
 			loader
 				.handles
@@ -76,8 +76,8 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 				Vec2::new(config.tile_width as f32, config.tile_height as f32),
 				config.columns,
 				config.rows,
-				None,
-				None,
+				config.padding.map(|v| Vec2::new(v as f32, v as f32)),
+				config.offset.map(|v| Vec2::new(v as f32, v as f32)),
 			);
 
 			let atlas_handle = loader.atlas.add(atlas);
diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs
index 61b629b55325b4c30c196f48d42a221b4f930119..83105bbc7b86fa6ebb9377d060cb4c15452d71f5 100644
--- a/game_core/src/assets/resources.rs
+++ b/game_core/src/assets/resources.rs
@@ -10,6 +10,8 @@ pub struct SpriteSheetConfig {
 	pub tile_height: usize,
 	pub columns: usize,
 	pub rows: usize,
+	pub padding: Option<usize>,
+	pub offset: Option<usize>,
 }
 
 impl SpriteSheetConfig {
@@ -19,6 +21,25 @@ impl SpriteSheetConfig {
 			tile_height: tile_wh,
 			columns,
 			rows,
+			padding: None,
+			offset: None,
+		}
+	}
+
+	pub fn extruded_squares(
+		tile_wh: usize,
+		columns: usize,
+		rows: usize,
+		padding: Option<usize>,
+		offset: Option<usize>,
+	) -> Self {
+		Self {
+			tile_width: tile_wh,
+			tile_height: tile_wh,
+			columns,
+			rows,
+			padding,
+			offset,
 		}
 	}
 
@@ -28,6 +49,26 @@ impl SpriteSheetConfig {
 			tile_height,
 			columns,
 			rows,
+			padding: None,
+			offset: None,
+		}
+	}
+
+	pub fn extruded_rectangles(
+		tile_width: usize,
+		tile_height: usize,
+		columns: usize,
+		rows: usize,
+		padding: Option<usize>,
+		offset: Option<usize>,
+	) -> Self {
+		Self {
+			tile_width,
+			tile_height,
+			columns,
+			rows,
+			padding,
+			offset,
 		}
 	}
 }
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index 3b1e45cfc5dd1025cb555f08f9416e7777404777..bb72806d67d840bb6024e0c9a8842ae98c20f58b 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -14,14 +14,16 @@ pub fn start_preload_resources(
 
 pub fn start_load_resources(mut loader: AssetTypeLoader) {
 	loader.load_spritesheet(
-		&SpriteSheetConfig::squares(18, 32, 64),
-		&[("sprites/world.png", "world")],
+		&SpriteSheetConfig::extruded_squares(18, 32, 64, Some(2), Some(1)),
+		&[("sprites/world_ext.png", "world")],
 	);
 
 	loader.load_spritesheet(
 		&SpriteSheetConfig::squares(24, 9, 3),
 		&[("sprites/characters.png", "characters")],
 	);
+
+	loader.load_ldtk(&[("world.ldtk", "world")]);
 }
 
 pub fn check_load_resources(loader: AssetTypeLoader, mut next_state: ResMut<NextState<AppState>>) {
diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs
index cea322b5d81b9ca3962ace899db460027d5de2d2..4d3f9f25bb2eebcba6f3c8687997c816012c8157 100644
--- a/game_core/src/entities/mod.rs
+++ b/game_core/src/entities/mod.rs
@@ -19,4 +19,8 @@ mod _plugin {
 
 pub use _plugin::EntityPluginSet;
 pub use graphics::{SimpleAnimation, SimpleAnimationBundle};
-pub use physics::{Acceleration, PhysicsLimits, PhysicsSet, Velocity, GRAVITY_ACC, GRAVITY_VEC};
+pub use physics::{
+	Acceleration, PhysicsLimits, PhysicsSet, PlatformerPhysicsHooks, Velocity, GRAVITY_ACC,
+	GRAVITY_VEC,
+};
+pub use player::Player;
diff --git a/game_core/src/entities/physics.rs b/game_core/src/entities/physics.rs
index 57392d7ae1d2f4e7eb6318fc2a5e2be2e558a54b..fb9d92ce653df4a5acdba6b5c4e1f8e389d1d204 100644
--- a/game_core/src/entities/physics.rs
+++ b/game_core/src/entities/physics.rs
@@ -1,8 +1,15 @@
 use crate::assets::FetchPhysicsConfig;
+use crate::entities::Player;
 use crate::system::run_in_game;
 use bevy::ecs::query::Has;
+use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
-use bevy_rapier2d::prelude::KinematicCharacterController;
+use bevy_rapier2d::geometry::SolverFlags;
+use bevy_rapier2d::pipeline::{ContactModificationContextView, PairFilterContextView};
+use bevy_rapier2d::prelude::{
+	BevyPhysicsHooks, KinematicCharacterController, KinematicCharacterControllerOutput,
+};
+use std::time::Duration;
 
 pub const GRAVITY_ACC: f32 = -150.0;
 pub const GRAVITY_VEC: Vec2 = Vec2::new(0.0, GRAVITY_ACC);
@@ -53,6 +60,40 @@ impl Acceleration {
 	}
 }
 
+#[derive(Copy, Clone, Debug, Component)]
+pub struct CoyoteTime {
+	elapsed: Duration,
+}
+
+impl CoyoteTime {
+	const LIMIT: Duration = Duration::from_millis(100);
+	pub fn new() -> CoyoteTime {
+		Self {
+			elapsed: Duration::ZERO,
+		}
+	}
+	pub fn tick(&mut self, amount: Duration) -> bool {
+		self.elapsed = self.elapsed.saturating_add(amount);
+		self.elapsed >= Self::LIMIT
+	}
+}
+
+fn tick_coyote_time(
+	time: Res<Time>,
+	mut commands: Commands,
+	mut coyote_query: Query<(Entity, &mut CoyoteTime)>,
+) {
+	let time = time.delta();
+	for (entity, mut coyote_time) in &mut coyote_query {
+		if coyote_time.tick(time) {
+			commands.entity(entity).despawn_recursive();
+		}
+	}
+}
+
+#[derive(Component)]
+pub struct Grounded;
+
 fn apply_acceleration(
 	time: Res<Time>,
 	mut query: Query<(
@@ -87,6 +128,51 @@ fn apply_velocity_to_kinematics(
 	}
 }
 
+fn filter_collisions(mut query: Query<&mut KinematicCharacterControllerOutput>) {
+	for mut controller in &mut query {
+		for coll in &controller.collisions {
+			// coll.
+		}
+	}
+}
+
+#[derive(SystemParam)]
+pub struct PlatformerPhysicsHooks<'w, 's> {
+	player_query: Query<
+		'w,
+		's,
+		(
+			&'static Transform,
+			&'static KinematicCharacterControllerOutput,
+		),
+		With<Player>,
+	>,
+}
+impl<'w, 's> PlatformerPhysicsHooks<'w, 's> {
+	pub fn fetch_player(
+		&self,
+		entity: Entity,
+	) -> Option<(&Transform, &KinematicCharacterControllerOutput)> {
+		self.player_query.get(entity).ok()
+	}
+}
+
+impl<'w, 's> BevyPhysicsHooks for PlatformerPhysicsHooks<'w, 's> {
+	fn filter_contact_pair(&self, _context: PairFilterContextView) -> Option<SolverFlags> {
+		info!("DAMN");
+		None
+	}
+
+	fn filter_intersection_pair(&self, _context: PairFilterContextView) -> bool {
+		info!("DAMN");
+		true
+	}
+
+	fn modify_solver_contacts(&self, _context: ContactModificationContextView) {
+		info!("MOIDIFY");
+	}
+}
+
 pub struct PhysicsPlugin;
 impl Plugin for PhysicsPlugin {
 	fn build(&self, app: &mut App) {
diff --git a/game_core/src/entities/player.rs b/game_core/src/entities/player.rs
index 5da2d04a9449af818bf66232b1172f80d04e3dfc..6889edd9b49e2c63f9e45d3c3fbffdb1fbfc661d 100644
--- a/game_core/src/entities/player.rs
+++ b/game_core/src/entities/player.rs
@@ -22,6 +22,7 @@ pub fn spawn_player(mut commands: Commands, assets: Res<AssetHandles>) {
 			..Default::default()
 		},
 		RigidBody::KinematicPositionBased,
+		ActiveEvents::all(),
 		Collider::cuboid(8.0, 12.0),
 		KinematicCharacterController {
 			snap_to_ground: Some(CharacterLength::Absolute(0.5)),
@@ -62,9 +63,11 @@ pub fn handle_input(
 		.map(|phys_config| phys_config.speed)
 		.unwrap_or_default();
 
+	let jump = phys_config.get().map(|p| p.jump).unwrap_or_default();
+
 	for (mut velocity, controller) in &mut query {
 		let delta_y = if controller.grounded && input.is_jump_just_pressed() {
-			Some(40.0)
+			Some(jump)
 		} else {
 			None
 		};
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index 24db2312511538727a30c6c224c78a429b18b228..ade7124bb368a1404a1dc8b31aaa69febf587790 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -2,3 +2,4 @@ pub mod assets;
 pub mod debug;
 pub mod entities;
 pub mod system;
+pub mod world;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index 8f74f60da0aa1258df574bef8e62874c760b251c..76aa876226cc948a705e39bbee1cbb882e35b03f 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -1,16 +1,23 @@
 use bevy::prelude::App;
+use game_core::entities::PlatformerPhysicsHooks;
+use micro_ldtk::set_ldtk_tile_scale_u32;
 
 fn main() {
+	set_ldtk_tile_scale_u32(18);
+
 	App::new()
 		.add_plugins(game_core::system::SystemPluginSet)
 		.add_plugins(game_core::assets::AssetLoadingPlugin)
 		.add_plugins(game_core::assets::ConfigsPlugin)
-		.add_plugins(game_core::entities::EntityPluginSet)
-		.add_plugins(game_core::debug::DebugPlugin)
-		.add_plugins(bevy_rapier2d::plugin::RapierPhysicsPlugin::<()>::pixels_per_meter(18.0))
+		.add_plugins(bevy_rapier2d::plugin::RapierPhysicsPlugin::<
+			PlatformerPhysicsHooks,
+		>::pixels_per_meter(18.0))
 		.add_plugins(micro_musicbox::CombinedAudioPlugins::<
 			game_core::assets::AssetHandles,
 		>::new())
 		.add_plugins(micro_ldtk::MicroLDTKPlugin)
+		.add_plugins(game_core::entities::EntityPluginSet)
+		.add_plugins(game_core::world::WorldPlugin)
+		.add_plugins(game_core::debug::DebugPlugin)
 		.run();
 }
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..06bdc9b8aaf2f4b89dc5dcaf06ed79f9f7d2430c
--- /dev/null
+++ b/game_core/src/world/mod.rs
@@ -0,0 +1,24 @@
+mod spawning;
+
+mod _plugin {
+	use crate::system::AppState;
+	use crate::world::spawning::has_level_changed;
+	use bevy::app::{App, PreUpdate};
+	use bevy::prelude::*;
+
+	pub struct WorldPlugin;
+	impl Plugin for WorldPlugin {
+		fn build(&self, app: &mut App) {
+			app.add_systems(
+				OnEnter(AppState::InGame),
+				super::spawning::set_initial_level,
+			)
+			.add_systems(
+				PreUpdate,
+				super::spawning::spawn_initial_level.run_if(has_level_changed),
+			);
+		}
+	}
+}
+
+pub use _plugin::WorldPlugin;
diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5bf9d32468f8ae0983af2e97b32c9e6eb2db8c9b
--- /dev/null
+++ b/game_core/src/world/spawning.rs
@@ -0,0 +1,78 @@
+use crate::assets::AssetHandles;
+use crate::entities::Player;
+use bevy::log::{error, info};
+use bevy::prelude::{
+	ClearColor, Color, Commands, DetectChanges, Query, Res, ResMut, Transform, TransformBundle,
+	With,
+};
+use bevy::sprite::{SpriteSheetBundle, TextureAtlasSprite};
+use bevy_rapier2d::prelude::{Collider, RigidBody};
+use micro_ldtk::{entity_centre, get_ldtk_tile_scale, ActiveLevel, LevelIndex, MapQuery};
+
+pub fn set_initial_level(mut commands: Commands) {
+	commands.insert_resource(ActiveLevel::new("w_0_0"));
+}
+
+pub fn has_level_changed(active: Option<Res<ActiveLevel>>) -> bool {
+	active.map(|a| a.is_changed()).unwrap_or(false)
+}
+
+pub fn spawn_initial_level(
+	mut commands: Commands,
+	mut query: MapQuery,
+	assets: Res<AssetHandles>,
+	mut player_q: Query<&mut Transform, With<Player>>,
+	l: Res<LevelIndex>,
+) {
+	let level = match query.get_active_level() {
+		Some(level) => level,
+		None => {
+			error!("Didn't get the right level!");
+			return;
+		}
+	};
+
+	commands.insert_resource(ClearColor(
+		Color::hex(level.level_ref().bg_color.as_str()).unwrap_or_default(),
+	));
+
+	let height = level.height();
+
+	MapQuery::for_each_layer_of(level, |layer| {
+		layer.for_each_tile(|x, y, data| {
+			commands.spawn(SpriteSheetBundle {
+				texture_atlas: assets.atlas("world"),
+				sprite: TextureAtlasSprite::new(data.tile.t as usize),
+				transform: Transform::from_xyz(
+					x as f32 * get_ldtk_tile_scale(),
+					height - y as f32 * get_ldtk_tile_scale(),
+					50.0,
+				),
+				..Default::default()
+			});
+		});
+	});
+
+	for ent in MapQuery::get_filtered_entities_of(level, "collider") {
+		let (px, py) = (ent.pivot[0] - 0.5, ent.pivot[1] - 0.5);
+		let offset_x = ent.width as f32 * (px as f32 * -1.0);
+		let offset_y = ent.height as f32 * (py as f32 * -1.0);
+
+		let x = ent.px[0] as f32 + offset_x - get_ldtk_tile_scale() / 2.0;
+		let y = level.height() - (ent.px[1] as f32 + offset_y) + get_ldtk_tile_scale() / 2.0;
+
+		commands.spawn((
+			Collider::cuboid(ent.width as f32 / 2.0, ent.height as f32 / 2.0),
+			RigidBody::Fixed,
+			TransformBundle::from_transform(Transform::from_xyz(x, y, 25.0)),
+		));
+	}
+
+	for ent in MapQuery::get_filtered_entities_of(level, "spawn_point") {
+		let area = entity_centre(level.height_i(), ent);
+		for mut p in &mut player_q {
+			p.translation.x = area.0;
+			p.translation.y = area.1;
+		}
+	}
+}