diff --git a/Cargo.lock b/Cargo.lock
index 966fca9a335ba5342913ea526686a48efcd86c74..02d372ed069c5b2183897bd8a88602ed8f7d72e2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -734,6 +734,16 @@ dependencies = [
  "bevy_reflect",
 ]
 
+[[package]]
+name = "bevy_tweening"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "164bcb41708fa1aeb435b6bba6cae61777b9f8cf3e18b207bb6209ca5b970e77"
+dependencies = [
+ "bevy",
+ "interpolation",
+]
+
 [[package]]
 name = "bevy_ui"
 version = "0.8.1"
@@ -1491,6 +1501,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "bevy",
+ "bevy_tweening",
  "fastrand",
  "iyes_loopless",
  "log 0.4.17",
@@ -1799,6 +1810,12 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "interpolation"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02"
+
 [[package]]
 name = "iovec"
 version = "0.1.4"
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 268949dafb357ee4661e16be8a906cfcf63bcd92..56d0c1efdeda1551239740b0c191bc7ba377c7b1 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -21,6 +21,7 @@ iyes_loopless = "0.7.1"
 
 micro_musicbox = { version = "0.4.0", features = ["serde", "mp3"] }
 remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
+bevy_tweening = "0.5.0"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 web-sys = { version = "0.3.60", features = ["Window"] }
diff --git a/game_core/index.html b/game_core/index.html
index ea8e997b61896868564fe7747edd45ef9352b0d6..925583234e93b163b8357e303b8e27754368b77d 100644
--- a/game_core/index.html
+++ b/game_core/index.html
@@ -46,8 +46,9 @@
 		}
 
 		#start_button {
-			width: 25%;
+			width: auto;
 			height: 25%;
+			aspect-ratio: 1;
 			background-color: black;
 			border-radius: 50%;
 			cursor: pointer;
diff --git a/game_core/src/control/mod.rs b/game_core/src/control/mod.rs
index d2f14fac6d4b4a4e5d93bb5e6677b4feb9847136..1f1c2de93e06ffb74e937b718c9b40234433fab8 100644
--- a/game_core/src/control/mod.rs
+++ b/game_core/src/control/mod.rs
@@ -13,7 +13,7 @@ mod __plugin {
 	impl Plugin for ControlPlugin {
 		fn build(&self, app: &mut App) {
 			app.add_system_set_to_stage(
-				CoreStage::PreUpdate,
+				CoreStage::First,
 				ConditionSet::new()
 					.run_in_state(AppState::InGame)
 					.with_system(super::player::handle_player_input)
diff --git a/game_core/src/control/player.rs b/game_core/src/control/player.rs
index 63f571f8a9361ccbac9d283e5da79133441dba68..aadd6053e53a01d66423ccb39df381c4ddd17eaf 100644
--- a/game_core/src/control/player.rs
+++ b/game_core/src/control/player.rs
@@ -4,9 +4,10 @@ use bevy::math::ivec2;
 use bevy::prelude::*;
 
 use crate::control::ai::{Automated, ShouldAct};
+use crate::entities::animations::{PositionBundle, PositionTween};
 use crate::entities::lifecycle::Player;
 use crate::entities::timing::ActionCooldown;
-use crate::world::level_map::GridPosition;
+use crate::world::level_map::{GridPosition, WORLD_TILE_SIZE};
 
 pub fn handle_player_input(
 	mut commands: Commands,
@@ -36,10 +37,17 @@ pub fn handle_player_input(
 	if dx != 0 || dy != 0 {
 		for (entity, mut position) in &mut player_query {
 			if dx != 0 || dy != 0 {
-				let next_position = (position.0.as_ivec2()) + ivec2(dx, dy);
-				**position = next_position.as_uvec2();
+				let current_position = **position;
+				let next_position = ((current_position.as_ivec2()) + ivec2(dx, dy)).as_uvec2();
+
+				**position = next_position;
 				commands
 					.entity(entity)
+					// .insert_bundle(PositionBundle::from(PositionTween::new(
+					// 	next_position.as_vec2() * WORLD_TILE_SIZE + WORLD_TILE_SIZE / 2.0,
+					// 	current_position.as_vec2() * WORLD_TILE_SIZE + WORLD_TILE_SIZE / 2.0,
+					// 	Duration::from_millis(100),
+					// )))
 					.insert(ActionCooldown::from(Duration::from_millis(250)))
 					.remove::<ShouldAct>();
 			}
diff --git a/game_core/src/entities/animations.rs b/game_core/src/entities/animations.rs
new file mode 100644
index 0000000000000000000000000000000000000000..272f6d342b542215a35952b3eaaa9de3e4f17b33
--- /dev/null
+++ b/game_core/src/entities/animations.rs
@@ -0,0 +1,157 @@
+use std::ops::{Deref, DerefMut};
+use std::time::Duration;
+
+use bevy::math::vec2;
+use bevy::prelude::*;
+use iyes_loopless::prelude::ConditionSet;
+use log::LevelFilter::Off;
+
+use crate::system::flow::AppState;
+
+#[derive(Clone, Debug, Component)]
+pub struct FrameAnimation {
+	frames: Vec<usize>,
+	current_index: usize,
+	frame_time: Duration,
+	current_time: Duration,
+}
+
+impl FrameAnimation {
+	pub fn new(frames: Vec<usize>, frame_time: Duration) -> Self {
+		Self {
+			current_index: 0,
+			frames,
+			frame_time,
+			current_time: Duration::ZERO,
+		}
+	}
+}
+
+pub fn tick_frame_animations(
+	time: Res<Time>,
+	mut query: Query<(&mut FrameAnimation, &mut TextureAtlasSprite)>,
+) {
+	let dt = time.delta();
+	for (mut animation, mut sprite) in &mut query {
+		animation.current_time += dt;
+		while animation.current_time > animation.frame_time {
+			animation.current_index += 1;
+			if animation.current_index == animation.frames.len() {
+				animation.current_index = 0;
+			}
+			let new_time = animation.current_time - animation.frame_time;
+			animation.current_time = new_time;
+		}
+		sprite.index = animation.frames[animation.current_index];
+	}
+}
+
+#[derive(Clone, Debug, Component)]
+pub struct Offset(Vec2);
+impl From<Vec2> for Offset {
+	fn from(other: Vec2) -> Self {
+		Self(other)
+	}
+}
+impl Deref for Offset {
+	type Target = Vec2;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for Offset {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+#[derive(Clone, Debug, Component)]
+pub struct PositionTween {
+	from: Vec2,
+	to: Vec2,
+	duration: Duration,
+	elapsed: Duration,
+}
+
+impl PositionTween {
+	pub fn new(from: Vec2, to: Vec2, duration: Duration) -> Self {
+		Self {
+			from,
+			to,
+			duration,
+			elapsed: Duration::ZERO,
+		}
+	}
+
+	pub fn get_value(&self) -> Vec2 {
+		if self.elapsed == Duration::ZERO {
+			self.to - self.from
+		} else {
+			(self.to - self.from) * (self.elapsed.as_secs_f32() / self.duration.as_secs_f32())
+		}
+	}
+}
+
+#[derive(Bundle)]
+pub struct PositionBundle {
+	position: PositionTween,
+	offset: Offset,
+}
+
+impl From<PositionTween> for PositionBundle {
+	fn from(other: PositionTween) -> Self {
+		PositionBundle {
+			offset: other.get_value().into(),
+			position: other,
+		}
+	}
+}
+
+pub fn sync_tween_offset(time: Res<Time>, mut query: Query<(&mut PositionTween, &mut Offset)>) {
+	let delta = time.delta();
+
+	for (mut tween, mut offset) in &mut query {
+		tween.elapsed = tween.duration.min(tween.elapsed + delta);
+		let percent = tween.elapsed.as_secs_f32() / tween.duration.as_secs_f32();
+		**offset = (tween.to - tween.from) * percent;
+	}
+}
+
+pub fn tween_cleanup(mut commands: Commands, query: Query<(Entity, &PositionTween)>) {
+	for (entity, position) in &query {
+		if position.elapsed >= position.duration {
+			commands
+				.entity(entity)
+				.remove::<PositionTween>()
+				.remove::<Offset>();
+		}
+	}
+}
+
+pub struct AnimationsPlugin;
+impl Plugin for AnimationsPlugin {
+	fn build(&self, app: &mut App) {
+		// app.add_system_set_to_stage(
+		// 	CoreStage::PreUpdate,
+		// 	ConditionSet::new()
+		// 		.run_in_state(AppState::InGame)
+		// 		.with_system(add_offset_to_tweened)
+		// 		.into(),
+		// )
+		app.add_system_set_to_stage(
+			CoreStage::Update,
+			ConditionSet::new()
+				.run_in_state(AppState::InGame)
+				.with_system(sync_tween_offset)
+				.with_system(tick_frame_animations)
+				.into(),
+		)
+		.add_system_set_to_stage(
+			CoreStage::Last,
+			ConditionSet::new()
+				.run_in_state(AppState::InGame)
+				.with_system(tween_cleanup)
+				.into(),
+		);
+	}
+}
diff --git a/game_core/src/entities/curses.rs b/game_core/src/entities/curses.rs
new file mode 100644
index 0000000000000000000000000000000000000000..99d63907a7efbc650ecb8490e9d8bd074f224d22
--- /dev/null
+++ b/game_core/src/entities/curses.rs
@@ -0,0 +1,87 @@
+use std::fmt::{Display, Formatter};
+
+use bevy::prelude::*;
+use iyes_loopless::prelude::ConditionSet;
+
+use crate::entities::spawner::EntitySpawner;
+use crate::entities::timing::GlobalTimer;
+use crate::system::flow::AppState;
+
+#[derive(Clone, Copy, Debug, Component)]
+pub struct CurseText;
+
+pub fn generate_curse(timer: Res<GlobalTimer>, mut events: EventWriter<CurseEvent>) {
+	if timer.is_triggered() {
+		let curse = match fastrand::usize(0..60) {
+			0..=9 => CurseEvent::CurseOfAmnesia,
+			10..=19 => CurseEvent::CurseOfPain,
+			20..=29 => CurseEvent::CurseOfMonsters,
+			30..=39 => CurseEvent::CurseOfWeakness,
+			40..=49 => CurseEvent::CurseOfLoss,
+			50..=59 => CurseEvent::CurseOfMidas,
+			_ => CurseEvent::CurseOfPain,
+		};
+
+		events.send(curse);
+	}
+}
+
+pub enum CurseEvent {
+	CurseOfPain,
+	CurseOfWeakness,
+	CurseOfMonsters,
+	CurseOfAmnesia,
+	CurseOfMidas,
+	CurseOfLoss,
+}
+
+impl Display for CurseEvent {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		write!(
+			f,
+			"{}",
+			match self {
+				CurseEvent::CurseOfPain => "Curse of Pain",
+				CurseEvent::CurseOfWeakness => "Curse of Weakness",
+				CurseEvent::CurseOfMonsters => "Curse of Monsters",
+				CurseEvent::CurseOfAmnesia => "Curse of Amnesia",
+				CurseEvent::CurseOfMidas => "Curse of Midas",
+				CurseEvent::CurseOfLoss => "Curse of Loss",
+			}
+		)
+	}
+}
+
+pub fn handle_curse(
+	mut spawner: EntitySpawner,
+	mut events: ResMut<Events<CurseEvent>>,
+	query: Query<Entity, With<CurseText>>,
+) {
+	for event in events.drain() {
+		for entity in &query {
+			spawner.commands.entity(entity).despawn_recursive();
+		}
+		spawner.spawn_curse_text(event);
+	}
+}
+
+pub struct CursesPlugin;
+impl Plugin for CursesPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_event::<CurseEvent>()
+			.add_system_set_to_stage(
+				CoreStage::PreUpdate,
+				ConditionSet::new()
+					.run_in_state(AppState::InGame)
+					.with_system(generate_curse)
+					.into(),
+			)
+			.add_system_set_to_stage(
+				CoreStage::PostUpdate,
+				ConditionSet::new()
+					.run_in_state(AppState::InGame)
+					.with_system(handle_curse)
+					.into(),
+			);
+	}
+}
diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs
index 89dfaa7f89d9e5af74214bd9c2585387419ea181..0568d6a81b6039b620789ace50ba0b1342d280b2 100644
--- a/game_core/src/entities/mod.rs
+++ b/game_core/src/entities/mod.rs
@@ -1,3 +1,5 @@
+pub mod animations;
+pub mod curses;
 pub mod lifecycle;
 pub mod spawner;
 pub mod timing;
@@ -8,7 +10,10 @@ mod __plugin {
 	pub struct EntitiesPluginGroup;
 	impl PluginGroup for EntitiesPluginGroup {
 		fn build(&mut self, group: &mut PluginGroupBuilder) {
-			group.add(super::timing::TimingPlugin);
+			group
+				.add(super::timing::TimingPlugin)
+				.add(super::animations::AnimationsPlugin)
+				.add(super::curses::CursesPlugin);
 		}
 	}
 }
diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs
index 7547a7451ce9e78899a8b07a0d298713ddb034f8..545508fa0bb8fe53280bdd84b5b65ebc78c9e7e7 100644
--- a/game_core/src/entities/spawner.rs
+++ b/game_core/src/entities/spawner.rs
@@ -1,8 +1,14 @@
+use std::time::Duration;
+
 use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
+use bevy_tweening::lens::{TextColorLens, UiPositionLens};
+use bevy_tweening::{Animator, EaseFunction, Tween, TweeningType};
 
 use crate::assets::AssetHandles;
 use crate::control::ai::ShouldAct;
+use crate::entities::animations::FrameAnimation;
+use crate::entities::curses::{CurseEvent, CurseText};
 use crate::entities::lifecycle::{GameEntity, Player};
 use crate::system::camera::ChaseCam;
 use crate::system::graphics::LAYER_CREATURE;
@@ -25,6 +31,10 @@ impl<'w, 's> EntitySpawner<'w, 's> {
 			.insert(GameEntity)
 			.insert(Player)
 			.insert(ShouldAct)
+			.insert(FrameAnimation::new(
+				vec![PLAYER_SPRITE, PLAYER_SPRITE + 1],
+				Duration::from_millis(250),
+			))
 			.insert(GridPosition(grid_position));
 
 		entity.insert_bundle(SpriteSheetBundle {
@@ -37,4 +47,55 @@ impl<'w, 's> EntitySpawner<'w, 's> {
 			..Default::default()
 		});
 	}
+
+	pub fn spawn_curse_text(&mut self, curse: CurseEvent) {
+		let position_animation = Tween::new(
+			EaseFunction::ExponentialIn,
+			TweeningType::Once,
+			Duration::from_millis(750),
+			UiPositionLens {
+				start: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(200.0), Val::Auto),
+				end: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(20.0), Val::Auto),
+			},
+		);
+
+		let colour_in_animation = Tween::new(
+			EaseFunction::SineIn,
+			TweeningType::Once,
+			Duration::from_millis(500),
+			TextColorLens {
+				section: 0,
+				start: (*Color::GOLD.set_a(0.0)).into(),
+				end: (*Color::GOLD.set_a(1.0)).into(),
+			},
+		);
+
+		self.commands
+			.spawn_bundle(NodeBundle {
+				color: Color::rgba(0.0, 0.0, 0.0, 0.0).into(),
+				style: Style {
+					position: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(100.0), Val::Auto),
+					position_type: PositionType::Absolute,
+					..Default::default()
+				},
+				..Default::default()
+			})
+			.insert(Animator::new(position_animation))
+			.insert(CurseText)
+			.with_children(|builder| {
+				builder
+					.spawn_bundle(TextBundle {
+						text: Text::from_section(
+							curse.to_string(),
+							TextStyle {
+								color: (*Color::GOLD.set_a(0.0)),
+								font: self.handles.font("main"),
+								font_size: 32.0,
+							},
+						),
+						..Default::default()
+					})
+					.insert(Animator::new(colour_in_animation));
+			});
+	}
 }
diff --git a/game_core/src/entities/timing.rs b/game_core/src/entities/timing.rs
index e210d3c72e7416bf8a0598030d3aaae4f140bd56..62d3232f8e0f42cfa82223d12025577eeeffdeb3 100644
--- a/game_core/src/entities/timing.rs
+++ b/game_core/src/entities/timing.rs
@@ -21,6 +21,9 @@ impl Default for GlobalTimer {
 
 impl GlobalTimer {
 	const GOAL: Duration = Duration::from_secs(10);
+	pub fn new() -> Self {
+		Self::default()
+	}
 	pub fn tick(&mut self, dt: Duration) {
 		self.internal += dt;
 	}
@@ -32,6 +35,9 @@ impl GlobalTimer {
 			self.internal -= Self::GOAL
 		}
 	}
+	pub fn reset(&mut self) {
+		self.internal = Duration::ZERO;
+	}
 	pub fn percent_complete(&self) -> f32 {
 		self.internal.as_secs_f32() / Self::GOAL.as_secs_f32()
 	}
@@ -42,6 +48,10 @@ pub fn tick_global_timer(time: Res<Time>, mut timer: ResMut<GlobalTimer>) {
 	timer.tick(time.delta());
 }
 
+pub fn reset_timer(mut timer: ResMut<GlobalTimer>) {
+	timer.reset();
+}
+
 #[derive(Copy, Clone, Default, Component)]
 pub struct GlobalTimerUi;
 pub fn spawn_global_timer_ui(mut commands: Commands, timer: Res<GlobalTimer>) {
@@ -58,7 +68,7 @@ pub fn spawn_global_timer_ui(mut commands: Commands, timer: Res<GlobalTimer>) {
 				),
 				..Default::default()
 			},
-			color: Color::ALICE_BLUE.into(),
+			color: Color::ORANGE_RED.into(),
 			..Default::default()
 		})
 		.insert(GlobalTimerUi);
@@ -119,6 +129,7 @@ impl Plugin for TimingPlugin {
 	fn build(&self, app: &mut App) {
 		app.init_resource::<GlobalTimer>()
 			.add_system_to_stage(CoreStage::First, tick_global_timer)
+			.add_enter_system(AppState::InGame, reset_timer)
 			.add_enter_system(AppState::InGame, spawn_global_timer_ui)
 			.add_system_set(
 				ConditionSet::new()
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index 4823b6c706c61a0263cd61189cb5973d49c3997a..66063751d05c52e867e27462aac73d383f0c41e3 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -1,4 +1,5 @@
 use bevy::prelude::*;
+use bevy_tweening::TweeningPlugin;
 use game_core::assets::AssetHandles;
 use game_core::system::flow::AppState;
 use game_core::system::resources::DefaultResourcesPlugin;
@@ -23,5 +24,6 @@ fn main() {
 		.add_plugin(game_core::world::WorldPlugin)
 		.add_plugin(game_core::control::ControlPlugin)
 		.add_plugins(game_core::entities::EntitiesPluginGroup)
+		.add_plugin(TweeningPlugin)
 		.run();
 }
diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs
index 5481bd2250cf6d9622f6e0bc0cb558ba311c11a0..a2d19c1784f392676142ea3fd1a1f5c058cf4f11 100644
--- a/game_core/src/system/load_config.rs
+++ b/game_core/src/system/load_config.rs
@@ -15,7 +15,7 @@ mod setup {
 	}
 
 	pub fn viewport_size() -> (f32, f32) {
-		(16.0 * 32.0, 16.0 * 18.0)
+		(16.0 * 24.0, 16.0 * 13.5)
 	}
 }
 
diff --git a/game_core/src/world/generators/mod.rs b/game_core/src/world/generators/mod.rs
index c8dc5112ebbcbd04b5e784d63abb4dc0af4bed2e..4a4fa09d53dfa58d4e1b8e763ac6bfc921b09f28 100644
--- a/game_core/src/world/generators/mod.rs
+++ b/game_core/src/world/generators/mod.rs
@@ -6,8 +6,8 @@ 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(crate) const TMP_FLOOR_GROUP: usize = 18 * 64;
+pub(crate) const TMP_WALL_GROUP: usize = 18 * 64 + 28;
 
 pub trait MapGenerator {
 	fn generate(indexer: &Indexer, rng: &Rng) -> LevelMap;
diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs
index 633689c905a3a3b670fab28bac38e7cbafbbf216..e327719c909e923362baf31e6eab01af2fab5656 100644
--- a/game_core/src/world/level_map.rs
+++ b/game_core/src/world/level_map.rs
@@ -5,6 +5,7 @@ use bevy::math::UVec2;
 use bevy::prelude::*;
 use fastrand::Rng;
 
+use crate::entities::animations::Offset;
 use crate::world::adjacency::{
 	get_floor_sprite_offset, get_wall_sprite_offset, BOTTOM, LEFT, NONE, RIGHT, TOP,
 };
@@ -34,10 +35,12 @@ impl DerefMut for GridPosition {
 
 /// 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);
+pub fn sync_grid_to_transform(mut query: Query<(&GridPosition, &mut Transform, Option<&Offset>)>) {
+	for (position, mut transform, offset) in &mut query {
+		transform.translation = ((position.as_vec2() * WORLD_TILE_SIZE)
+			+ (WORLD_TILE_SIZE / 2.0)
+			+ offset.map(|offset| **offset).unwrap_or(Vec2::ZERO))
+		.extend(transform.translation.z);
 	}
 }