diff --git a/source/Character.hx b/source/Character.hx
index 0c1eb4118..999dcbe54 100644
--- a/source/Character.hx
+++ b/source/Character.hx
@@ -689,6 +689,8 @@ class Character extends FlxSprite
 	 */
 	public function dance()
 	{
+		if (animation == null)
+			return;
 		if (!debugMode)
 		{
 			switch (curCharacter)
diff --git a/source/LoadingState.hx b/source/LoadingState.hx
index 016377ecb..263aa3fcb 100644
--- a/source/LoadingState.hx
+++ b/source/LoadingState.hx
@@ -187,7 +187,14 @@ class LoadingState extends MusicBeatState
 
 	static function getNextState(target:FlxState, stopMusic = false):FlxState
 	{
-		Paths.setCurrentLevel("week" + PlayState.storyWeek);
+		if (PlayState.storyWeek == 0)
+		{
+			Paths.setCurrentLevel('tutorial');
+		}
+		else
+		{
+			Paths.setCurrentLevel("week" + PlayState.storyWeek);
+		}
 		#if NO_PRELOAD_ALL
 		var loaded = isSoundLoaded(getSongPath())
 			&& (!PlayState.SONG.needsVoices || isSoundLoaded(getVocalPath()))
diff --git a/source/PlayState.hx b/source/PlayState.hx
index 8c441c39c..b7007e345 100644
--- a/source/PlayState.hx
+++ b/source/PlayState.hx
@@ -393,53 +393,9 @@ class PlayState extends MusicBeatState
 				loadStage(curStageId);
 
 			case "milf" | 'satin-panties' | 'high':
-				curStageId = 'limo';
-				defaultCamZoom *= 0.90;
+				curStageId = 'limoRide';
+				loadStage(curStageId);
 
-				var skyBG:FlxSprite = new FlxSprite(-120, -50).loadGraphic(Paths.image('limo/limoSunset'));
-
-				var overlayShader:OverlayBlend = new OverlayBlend();
-				var sunOverlay:FlxSprite = new FlxSprite().loadGraphic(Paths.image('limo/limoOverlay'));
-				sunOverlay.setGraphicSize(Std.int(sunOverlay.width * 2));
-				sunOverlay.updateHitbox();
-				overlayShader.funnyShit.input = sunOverlay.pixels;
-				skyBG.shader = overlayShader;
-
-				skyBG.scrollFactor.set(0.1, 0.1);
-				add(skyBG);
-
-				var bgLimo:FlxSprite = new FlxSprite(-200, 480);
-				bgLimo.frames = Paths.getSparrowAtlas('limo/bgLimo');
-				bgLimo.animation.addByPrefix('drive', "background limo pink", 24);
-				bgLimo.animation.play('drive');
-				bgLimo.scrollFactor.set(0.4, 0.4);
-				add(bgLimo);
-
-				grpLimoDancers = new FlxTypedGroup<BackgroundDancer>();
-				add(grpLimoDancers);
-
-				for (i in 0...5)
-				{
-					var dancer:BackgroundDancer = new BackgroundDancer((370 * i) + 130, bgLimo.y - 400);
-					dancer.scrollFactor.set(0.4, 0.4);
-					grpLimoDancers.add(dancer);
-				}
-
-				var overlayShit:FlxSprite = new FlxSprite(-500, -600).loadGraphic(Paths.image('limo/limoOverlay'));
-				overlayShit.alpha = 0.5;
-				// add(overlayShit);
-				// var shaderBullshit = new BlendModeEffect(new OverlayShader(), FlxColor.RED);
-				// FlxG.camera.setFilters([new ShaderFilter(cast shaderBullshit.shader)]);
-				// overlayShit.shader = shaderBullshit;
-
-				limo = new FlxSprite(-120, 550);
-				limo.frames = Paths.getSparrowAtlas('limo/limoDrive');
-				limo.animation.addByPrefix('drive', "Limo stage", 24);
-				limo.animation.play('drive');
-				limo.antialiasing = true;
-
-				fastCar = new FlxSprite(-300, 160).loadGraphic(Paths.image('limo/fastCarLol'));
-			// add(limo);
 			case "cocoa" | 'eggnog':
 				curStageId = 'mall';
 
@@ -734,12 +690,6 @@ class PlayState extends MusicBeatState
 		// REPOSITIONING PER STAGE
 		switch (curStageId)
 		{
-			case 'limo':
-				boyfriend.y -= 220;
-				boyfriend.x += 260;
-
-				resetFastCar();
-				add(fastCar);
 			case 'mall':
 				boyfriend.x += 200;
 			case 'mallEvil':
@@ -776,7 +726,7 @@ class PlayState extends MusicBeatState
 					gf.x -= 170;
 					gf.y -= 75;
 				}
-			case 'stage' | 'phillyStreets':
+			case 'phillyStreets':
 				dad.y = 870 - dad.height;
 		}
 
@@ -801,10 +751,6 @@ class PlayState extends MusicBeatState
 			bfTankCutsceneLayer = new FlxGroup();
 			add(bfTankCutsceneLayer);
 
-			// Shitty layering but whatev it works LOL
-			if (curStageId == 'limo')
-				add(limo);
-
 			add(dad);
 			add(boyfriend);
 		}
@@ -2802,28 +2748,6 @@ class PlayState extends MusicBeatState
 		FlxG.camera.focusOn(camFollow.getPosition());
 	}
 
-	var fastCarCanDrive:Bool = true;
-
-	function resetFastCar():Void
-	{
-		fastCar.x = -12600;
-		fastCar.y = FlxG.random.int(140, 250);
-		fastCar.velocity.x = 0;
-		fastCarCanDrive = true;
-	}
-
-	function fastCarDrive()
-	{
-		FlxG.sound.play(Paths.soundRandom('carPass', 0, 1), 0.7);
-
-		fastCar.velocity.x = (FlxG.random.int(170, 220) / FlxG.elapsed) * 3;
-		fastCarCanDrive = false;
-		new FlxTimer().start(2, function(tmr:FlxTimer)
-		{
-			resetFastCar();
-		});
-	}
-
 	function moveTank():Void
 	{
 		if (!inCutscene)
@@ -2972,8 +2896,6 @@ class PlayState extends MusicBeatState
 					dancer.dance();
 				});
 
-				if (FlxG.random.bool(10) && fastCarCanDrive)
-					fastCarDrive();
 			case 'tank':
 				tankWatchtower.dance();
 		}
diff --git a/source/play/stage/Stage.hx b/source/play/stage/Stage.hx
index 125fa7c7d..0e6737811 100644
--- a/source/play/stage/Stage.hx
+++ b/source/play/stage/Stage.hx
@@ -25,6 +25,7 @@ class Stage extends FlxSpriteGroup implements IHook
 
 	var namedProps:Map<String, FlxSprite> = new Map<String, FlxSprite>();
 	var characters:Map<String, Character> = new Map<String, Character>();
+	var boppers:Array<Bopper> = new Array<Bopper>();
 
 	/**
 	 * The Stage elements get initialized at the beginning of the game.
@@ -58,7 +59,16 @@ class Stage extends FlxSpriteGroup implements IHook
 			trace('  Placing prop: ${dataProp.name} (${dataProp.assetPath})');
 
 			var isAnimated = dataProp.animations.length > 0;
-			var propSprite = new FlxSprite();
+
+			var propSprite:FlxSprite;
+			if (dataProp.danceEvery != 0)
+			{
+				propSprite = new Bopper(dataProp.danceEvery);
+			}
+			else
+			{
+				propSprite = new FlxSprite();
+			}
 
 			if (isAnimated)
 			{
@@ -91,7 +101,15 @@ class Stage extends FlxSpriteGroup implements IHook
 
 			for (propAnim in dataProp.animations)
 			{
-				propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop);
+				if (propAnim.frameIndices.length == 0)
+				{
+					propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop, propAnim.flipX, propAnim.flipY);
+				}
+				else
+				{
+					propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop,
+						propAnim.flipX, propAnim.flipY);
+				}
 			}
 
 			if (dataProp.startingAnimation != null)
@@ -99,18 +117,44 @@ class Stage extends FlxSpriteGroup implements IHook
 				propSprite.animation.play(dataProp.startingAnimation);
 			}
 
-			if (dataProp.name != null)
+			if (Std.isOfType(propSprite, Bopper))
 			{
-				namedProps.set(dataProp.name, propSprite);
+				addBopper(cast propSprite, dataProp.name);
+			}
+			else
+			{
+				addProp(propSprite, dataProp.name);
 			}
-
 			trace('    Prop placed.');
-			this.add(propSprite);
 		}
 
 		this.refresh();
 	}
 
+	/**
+	 * Add a sprite to the stage.
+	 * @param prop The sprite to add.
+	 * @param name (Optional) A unique name for the sprite.
+	 *   You can call `getNamedProp(name)` to retrieve it later.
+	 */
+	public function addProp(prop:FlxSprite, ?name:String = null)
+	{
+		if (name != null)
+		{
+			namedProps.set(name, prop);
+		}
+		this.add(prop);
+	}
+
+	/**
+	 * Add a sprite to the stage which animates to the beat of the song.
+	 */
+	public function addBopper(bopper:Bopper, ?name:String = null)
+	{
+		boppers.push(bopper);
+		this.addProp(bopper, name);
+	}
+
 	/**
 	 * Refreshes the stage, by redoing the render order of all props.
 	 * It does this based on the `zIndex` of each prop.
@@ -167,7 +211,12 @@ class Stage extends FlxSpriteGroup implements IHook
 	public function onBeatHit(curBeat:Int):Void
 	{
 		// Override me in your scripted stage to perform custom behavior!
-		// trace('Stage.onBeatHit(${curBeat})');
+		// Make sure to call super.onBeatHit(curBeat) if you want to keep the boppers dancing.
+
+		for (bopper in boppers)
+		{
+			bopper.onBeatHit(curBeat);
+		}
 	}
 
 	/**
@@ -271,6 +320,12 @@ class Stage extends FlxSpriteGroup implements IHook
 		}
 		characters.clear();
 
+		for (bopper in boppers)
+		{
+			bopper.destroy();
+		}
+		boppers = [];
+
 		for (sprite in this.group)
 		{
 			sprite.destroy();
diff --git a/source/play/stage/StageData.hx b/source/play/stage/StageData.hx
index 5912ad824..d5278f26c 100644
--- a/source/play/stage/StageData.hx
+++ b/source/play/stage/StageData.hx
@@ -32,7 +32,7 @@ class StageDataParser
 	{
 		// Clear any stages that are cached if there were any.
 		clearStageCache();
-		trace("Loading stage cache...");
+		trace("[STAGEDATA] Loading stage cache...");
 
 		#if polymod
 		//
@@ -91,14 +91,14 @@ class StageDataParser
 	{
 		if (stageCache.exists(stageId))
 		{
-			trace('Successfully fetch stage: ${stageId}');
+			trace('[STAGEDATA] Successfully fetch stage: ${stageId}');
 			var stage:Stage = stageCache.get(stageId);
 			stage.revive();
 			return stage;
 		}
 		else
 		{
-			trace('Failed to fetch stage, not found in cache: ${stageId}');
+			trace('[STAGEDATA] Failed to fetch stage, not found in cache: ${stageId}');
 			return null;
 		}
 	}
@@ -125,7 +125,7 @@ class StageDataParser
 	{
 		var rawJson:String = loadStageFile(stageId);
 
-		var stageData:StageData = migrateStageData(rawJson);
+		var stageData:StageData = migrateStageData(rawJson, stageId);
 
 		return validateStageData(stageId, stageData);
 	}
@@ -143,22 +143,32 @@ class StageDataParser
 		return rawJson;
 	}
 
-	static function migrateStageData(rawJson:String)
+	static function migrateStageData(rawJson:String, stageId:String)
 	{
 		// If you update the stage data format in a breaking way,
 		// handle migration here by checking the `version` value.
 
-		var stageData:StageData = cast Json.parse(rawJson);
-
-		return stageData;
+		try
+		{
+			var stageData:StageData = cast Json.parse(rawJson);
+			return stageData;
+		}
+		catch (e)
+		{
+			trace('  Error parsing data for stage: ${stageId}');
+			trace('    ${e}');
+			return null;
+		}
 	}
 
 	static final DEFAULT_NAME:String = "Untitled Stage";
 	static final DEFAULT_CAMERAZOOM:Float = 1.0;
 	static final DEFAULT_ZINDEX:Int = 0;
+	static final DEFAULT_DANCEEVERY:Int = 0;
 	static final DEFAULT_SCALE:Float = 1.0;
 	static final DEFAULT_POSITION:Array<Float> = [0, 0];
 	static final DEFAULT_SCROLL:Array<Float> = [0, 0];
+	static final DEFAULT_FRAMEINDICES:Array<Int> = [];
 
 	static final DEFAULT_CHARACTER_DATA:StageDataCharacter = {
 		zIndex: DEFAULT_ZINDEX,
@@ -174,6 +184,12 @@ class StageDataParser
 	 */
 	static function validateStageData(id:String, input:StageData):Null<StageData>
 	{
+		if (input == null)
+		{
+			trace('[STAGEDATA] ERROR: Could not parse stage data for "${id}".');
+			return null;
+		}
+
 		if (input.version != STAGE_DATA_VERSION)
 		{
 			trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version');
@@ -217,6 +233,11 @@ class StageDataParser
 				inputProp.zIndex = DEFAULT_ZINDEX;
 			}
 
+			if (inputProp.danceEvery == null)
+			{
+				inputProp.danceEvery = DEFAULT_DANCEEVERY;
+			}
+
 			if (inputProp.scale == null)
 			{
 				inputProp.scale = DEFAULT_SCALE;
@@ -261,6 +282,11 @@ class StageDataParser
 					inputAnimation.frameRate = 24;
 				}
 
+				if (inputAnimation.frameIndices == null)
+				{
+					inputAnimation.frameIndices = DEFAULT_FRAMEINDICES;
+				}
+
 				if (inputAnimation.loop == null)
 				{
 					inputAnimation.loop = true;
@@ -361,6 +387,15 @@ typedef StageDataProp =
 	 */
 	var scale:OneOfTwo<Float, Array<Float>>;
 
+	/**
+	 * If not zero, this prop will play an animation every X beats of the song.
+	 * This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
+	 * they will alternated between, otherwise the `idle` animation will be used.
+	 * 
+	 * @default 0
+	 */
+	var danceEvery:Null<Int>;
+
 	/**
 	 * How much the prop scrolls relative to the camera. Used to create a parallax effect.
 	 * Represented as a float or as an [x, y] array of two floats.
@@ -397,6 +432,14 @@ typedef StageDataPropAnimation =
 	 */
 	var prefix:String;
 
+	/**
+	 * If you want this animation to use only certain frames of an animation with a given prefix,
+	 * select them here.
+	 * @example [0, 1, 2, 3] (use only the first four frames)
+	 * @default [] (all frames)
+	 */
+	var frameIndices:Array<Int>;
+
 	/**
 	 * The speed of the animation in frames per second.
 	 * @default 24