diff --git a/Project.xml b/Project.xml
index 4b81fd07b..8ba14e7dc 100644
--- a/Project.xml
+++ b/Project.xml
@@ -119,7 +119,7 @@
-
+
diff --git a/source/funkin/graphics/shaders/GaussianBlurShader.hx b/source/funkin/graphics/shaders/GaussianBlurShader.hx
index 81167655b..cecfdab80 100644
--- a/source/funkin/graphics/shaders/GaussianBlurShader.hx
+++ b/source/funkin/graphics/shaders/GaussianBlurShader.hx
@@ -20,6 +20,6 @@ class GaussianBlurShader extends FlxRuntimeShader
public function setAmount(value:Float):Void
{
this.amount = value;
- this.setFloat("amount", amount);
+ this.setFloat("_amount", amount);
}
}
diff --git a/source/funkin/graphics/shaders/Grayscale.hx b/source/funkin/graphics/shaders/Grayscale.hx
index 6673ace24..fbd0970e5 100644
--- a/source/funkin/graphics/shaders/Grayscale.hx
+++ b/source/funkin/graphics/shaders/Grayscale.hx
@@ -17,6 +17,6 @@ class Grayscale extends FlxRuntimeShader
public function setAmount(value:Float):Void
{
amount = value;
- this.setFloat("amount", amount);
+ this.setFloat("_amount", amount);
}
}
diff --git a/source/funkin/graphics/shaders/HSVShader.hx b/source/funkin/graphics/shaders/HSVShader.hx
index 733bbca7f..2dfdac2c9 100644
--- a/source/funkin/graphics/shaders/HSVShader.hx
+++ b/source/funkin/graphics/shaders/HSVShader.hx
@@ -20,7 +20,7 @@ class HSVShader extends FlxRuntimeShader
function set_hue(value:Float):Float
{
- this.setFloat('hue', value);
+ this.setFloat('_hue', value);
this.hue = value;
return this.hue;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index cb1cacbc1..b59c48888 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -245,20 +245,26 @@ class PlayState extends MusicBeatSubState
/**
* The current camera zoom level without any modifiers applied.
*/
- public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
+ public var currentCameraZoom:Float = FlxCamera.defaultZoom;
/**
- * currentCameraZoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
- * Defaults to 1.05, but may be larger or smaller depending on the current stage.
- * Tweened via the `ZoomCamera` song event in direct mode.
+ * Multiplier for currentCameraZoom for camera bops.
+ * Lerped back to 1.0x every frame.
*/
- public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
+ public var cameraBopMultiplier:Float = 1.0;
/**
- * Camera zoom applied on top of currentCameraZoom.
- * Tweened via the `ZoomCamera` song event in additive mode.
+ * Default camera zoom for the current stage.
+ * If we aren't in a stage, just use the default zoom (1.05x).
*/
- public var additiveCameraZoom:Float = 0;
+ public var stageZoom(get, never):Float;
+
+ function get_stageZoom():Float
+ {
+ if (currentStage != null) return currentStage.camZoom;
+ else
+ return FlxCamera.defaultZoom * 1.05;
+ }
/**
* The current HUD camera zoom level.
@@ -268,16 +274,18 @@ class PlayState extends MusicBeatSubState
public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0;
/**
- * Intensity of the gameplay camera zoom.
- * @default `1.5%`
+ * Camera bop intensity multiplier.
+ * Applied to cameraBopMultiplier on camera bops (usually every beat).
+ * @default `101.5%`
*/
- public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY;
+ public var cameraBopIntensity:Float = Constants.DEFAULT_BOP_INTENSITY;
/**
* Intensity of the HUD camera zoom.
+ * Need to make this a multiplier later. Just shoving in 0.015 for now so it doesn't break.
* @default `3.0%`
*/
- public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
+ public var hudCameraZoomIntensity:Float = 0.015 * 2.0;
/**
* How many beats (quarter notes) between camera zooms.
@@ -861,8 +869,8 @@ class PlayState extends MusicBeatSubState
regenNoteData();
// Reset camera zooming
- cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY;
- hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
+ cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
+ hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING;
@@ -957,11 +965,12 @@ class PlayState extends MusicBeatSubState
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
- // Lerp the camera zoom towards the target level.
+ // Apply camera zoom + multipliers.
if (subState == null)
{
- currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95);
- FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom;
+ cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
+ var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
+ FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
@@ -971,6 +980,7 @@ class PlayState extends MusicBeatSubState
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
FlxG.watch.addQuick('health', health);
+ FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
// TODO: Add a song event for Handle GF dance speed.
@@ -1367,15 +1377,15 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
- // Only zoom camera if we are zoomed by less than 35%.
+ // Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera
- && FlxG.camera.zoom < (1.35 * defaultCameraZoom)
+ && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
&& cameraZoomRate > 0
&& Conductor.instance.currentBeat % cameraZoomRate == 0)
{
- // Zoom camera in (1.5%)
- currentCameraZoom += cameraZoomIntensity * defaultCameraZoom;
- // Hud zooms double (3%)
+ // Set zoom multiplier for camera bop.
+ cameraBopMultiplier = cameraBopIntensity;
+ // HUD camera zoom still uses old system. To change. (+3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
}
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
@@ -1566,12 +1576,11 @@ class PlayState extends MusicBeatSubState
{
if (PlayState.instance.isMinimalMode) return;
// Apply camera zoom level from stage data.
- defaultCameraZoom = currentStage?.camZoom ?? 1.0;
- currentCameraZoom = defaultCameraZoom;
+ currentCameraZoom = stageZoom;
FlxG.camera.zoom = currentCameraZoom;
- // Reset additive zoom.
- additiveCameraZoom = 0;
+ // Reset bop multiplier.
+ cameraBopMultiplier = 1.0;
}
/**
@@ -3116,6 +3125,15 @@ class PlayState extends MusicBeatSubState
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
+ /**
+ * Sets the camera follow point's position and tweens the camera there.
+ */
+ public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void
+ {
+ cameraFollowPoint.setPosition(x, y);
+ tweenCameraToFollowPoint(duration, ease);
+ }
+
/**
* Disables camera following and tweens the camera to the follow point manually.
*/
@@ -3157,38 +3175,24 @@ class PlayState extends MusicBeatSubState
/**
* Tweens the camera zoom to the desired amount.
*/
- public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:NullFloat>):Void
+ public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void
{
// Cancel the current tween if it's active.
cancelCameraZoomTween();
- var targetZoom = zoom * FlxCamera.defaultZoom;
+ // Direct mode: Set zoom directly.
+ // Stage mode: Set zoom as a multiplier of the current stage's default zoom.
+ var targetZoom = zoom * (direct ? FlxCamera.defaultZoom : stageZoom);
- if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms.
+ if (duration == 0)
{
- if (duration == 0)
- {
- // Instant zoom. No tween needed.
- defaultCameraZoom = targetZoom;
- }
- else
- {
- // Zoom tween! Caching it so we can cancel/pause it later if needed.
- cameraZoomTween = FlxTween.tween(this, {defaultCameraZoom: targetZoom}, duration, {ease: ease});
- }
+ // Instant zoom. No tween needed.
+ currentCameraZoom = targetZoom;
}
- else // Additive mode: Tween additiveCameraZoom for ease-based zooms.
+ else
{
- if (duration == 0)
- {
- // Instant zoom. No tween needed.
- additiveCameraZoom = targetZoom;
- }
- else
- {
- // Zoom tween! Caching it so we can cancel/pause it later if needed.
- cameraZoomTween = FlxTween.tween(this, {additiveCameraZoom: targetZoom}, duration, {ease: ease});
- }
+ // Zoom tween! Caching it so we can cancel/pause it later if needed.
+ cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease});
}
}
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 3da51185f..0c05bc876 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -311,7 +311,7 @@ class VideoCutscene
blackScreen = null;
}
});
- FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.defaultCameraZoom}, transitionTime,
+ FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.stageZoom}, transitionTime,
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx
index c520c3e25..2c59eaba0 100644
--- a/source/funkin/play/cutscene/dialogue/Conversation.hx
+++ b/source/funkin/play/cutscene/dialogue/Conversation.hx
@@ -23,6 +23,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.util.SortUtil;
+import funkin.util.EaseUtil;
/**
* A high-level handler for dialogue.
@@ -179,7 +180,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
if (backdropData.fadeTime > 0.0)
{
backdrop.alpha = 0.0;
- FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
+ FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: EaseUtil.stepped(10)});
}
else
{
@@ -403,6 +404,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
type: ONESHOT, // holy shit like the game no way
startDelay: 0,
onComplete: (_) -> endOutro(),
+ ease: EaseUtil.stepped(8)
});
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx
index d4ab4f24f..1bcac9ad3 100644
--- a/source/funkin/play/event/FocusCameraSongEvent.hx
+++ b/source/funkin/play/event/FocusCameraSongEvent.hx
@@ -70,80 +70,76 @@ class FocusCameraSongEvent extends SongEvent
if (char == null) char = cast data.value;
- var useTween:Null = data.getBool('useTween');
- if (useTween == null) useTween = false;
var duration:Null = data.getFloat('duration');
if (duration == null) duration = 4.0;
var ease:Null = data.getString('ease');
- if (ease == null) ease = 'linear';
+ if (ease == null) ease = 'CLASSIC';
+
+ var currentStage = PlayState.instance.currentStage;
+
+ // Get target position based on char.
+ var targetX:Float = posX;
+ var targetY:Float = posY;
switch (char)
{
- case -1: // Position
+ case -1: // Position ("focus" on origin)
trace('Focusing camera on static position.');
- var xTarget:Float = posX;
- var yTarget:Float = posY;
- PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
- case 0: // Boyfriend
- // Focus the camera on the player.
- if (PlayState.instance.currentStage.getBoyfriend() == null)
+ case 0: // Boyfriend (focus on player)
+ if (currentStage.getBoyfriend() == null)
{
trace('No BF to focus on.');
return;
}
trace('Focusing camera on player.');
- var xTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
- var yTarget:Float = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
+ var bfPoint = currentStage.getBoyfriend().cameraFocusPoint;
+ targetX += bfPoint.x;
+ targetY += bfPoint.y;
- PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
- case 1: // Dad
- // Focus the camera on the dad.
- if (PlayState.instance.currentStage.getDad() == null)
+ case 1: // Dad (focus on opponent)
+ if (currentStage.getDad() == null)
{
trace('No dad to focus on.');
return;
}
- trace('Focusing camera on dad.');
- trace(PlayState.instance.currentStage.getDad());
- var xTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
- var yTarget:Float = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
+ trace('Focusing camera on opponent.');
+ var dadPoint = currentStage.getDad().cameraFocusPoint;
+ targetX += dadPoint.x;
+ targetY += dadPoint.y;
- PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
- case 2: // Girlfriend
- // Focus the camera on the girlfriend.
- if (PlayState.instance.currentStage.getGirlfriend() == null)
+ case 2: // Girlfriend (focus on girlfriend)
+ if (currentStage.getGirlfriend() == null)
{
trace('No GF to focus on.');
return;
}
trace('Focusing camera on girlfriend.');
- var xTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
- var yTarget:Float = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
+ var gfPoint = currentStage.getGirlfriend().cameraFocusPoint;
+ targetX += gfPoint.x;
+ targetY += gfPoint.y;
- PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
default:
trace('Unknown camera focus: ' + data);
}
- if (useTween)
+ // Apply tween based on ease.
+ switch (ease)
{
- switch (ease)
- {
- case 'INSTANT':
- PlayState.instance.tweenCameraToFollowPoint(0);
- default:
- var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
-
- var easeFunction:NullFloat> = Reflect.field(FlxEase, ease);
- if (easeFunction == null)
- {
- trace('Invalid ease function: $ease');
- return;
- }
-
- PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction);
- }
+ case 'CLASSIC': // Old-school. No ease. Just set follow point.
+ PlayState.instance.cancelCameraFollowTween();
+ PlayState.instance.cameraFollowPoint.setPosition(targetX, targetY);
+ case 'INSTANT': // Instant ease. Duration is automatically 0.
+ PlayState.instance.tweenCameraToPosition(targetX, targetY, 0);
+ default:
+ var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
+ var easeFunction:NullFloat> = Reflect.field(FlxEase, ease);
+ if (easeFunction == null)
+ {
+ trace('Invalid ease function: $ease');
+ return;
+ }
+ PlayState.instance.tweenCameraToPosition(targetX, targetY, durSeconds, easeFunction);
}
}
@@ -187,12 +183,6 @@ class FocusCameraSongEvent extends SongEvent
type: SongEventFieldType.FLOAT,
units: "px"
},
- {
- name: 'useTween',
- title: 'Use Tween',
- type: SongEventFieldType.BOOL,
- defaultValue: false
- },
{
name: 'duration',
title: 'Duration',
@@ -208,7 +198,9 @@ class FocusCameraSongEvent extends SongEvent
type: SongEventFieldType.ENUM,
keys: [
'Linear' => 'linear',
- 'Instant' => 'INSTANT',
+ 'Sine In' => 'sineIn',
+ 'Sine Out' => 'sineOut',
+ 'Sine In/Out' => 'sineInOut',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
@@ -221,15 +213,17 @@ class FocusCameraSongEvent extends SongEvent
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
+ 'Expo In' => 'expoIn',
+ 'Expo Out' => 'expoOut',
+ 'Expo In/Out' => 'expoInOut',
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
- 'Sine In' => 'sineIn',
- 'Sine Out' => 'sineOut',
- 'Sine In/Out' => 'sineInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut',
+ 'Instant (Ignores duration)' => 'INSTANT',
+ 'Classic (Ignores duration)' => 'CLASSIC'
]
}
]);
diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx
index a82577a5f..f3efc04e3 100644
--- a/source/funkin/play/event/SetCameraBopSongEvent.hx
+++ b/source/funkin/play/event/SetCameraBopSongEvent.hx
@@ -50,8 +50,8 @@ class SetCameraBopSongEvent extends SongEvent
var intensity:Null = data.getFloat('intensity');
if (intensity == null) intensity = 1.0;
- PlayState.instance.cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity;
- PlayState.instance.hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity * 2.0;
+ PlayState.instance.cameraBopIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity + 1.0;
+ PlayState.instance.hudCameraZoomIntensity = (Constants.DEFAULT_BOP_INTENSITY - 1.0) * intensity * 2.0;
PlayState.instance.cameraZoomRate = rate;
trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}');
}
diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx
index b913aebe7..748abda19 100644
--- a/source/funkin/play/event/ZoomCameraSongEvent.hx
+++ b/source/funkin/play/event/ZoomCameraSongEvent.hx
@@ -81,7 +81,6 @@ class ZoomCameraSongEvent extends SongEvent
PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
default:
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
-
var easeFunction:NullFloat> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
@@ -102,9 +101,9 @@ class ZoomCameraSongEvent extends SongEvent
* ```
* {
* 'zoom': FLOAT, // Target zoom level.
- * 'duration': FLOAT, // Optional duration in steps.
- * 'mode': ENUM, // Whether to set additive zoom or direct zoom.
- * 'ease': ENUM, // Optional easing function.
+ * 'duration': FLOAT, // Duration in steps.
+ * 'mode': ENUM, // Whether zoom is relative to the stage or absolute zoom.
+ * 'ease': ENUM, // Easing function.
* }
* @return SongEventSchema
*/
@@ -130,9 +129,9 @@ class ZoomCameraSongEvent extends SongEvent
{
name: 'mode',
title: 'Mode',
- defaultValue: 'direct',
+ defaultValue: 'stage',
type: SongEventFieldType.ENUM,
- keys: ['Additive' => 'additive', 'Direct' => 'direct']
+ keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
},
{
name: 'ease',
@@ -142,6 +141,9 @@ class ZoomCameraSongEvent extends SongEvent
keys: [
'Linear' => 'linear',
'Instant' => 'INSTANT',
+ 'Sine In' => 'sineIn',
+ 'Sine Out' => 'sineOut',
+ 'Sine In/Out' => 'sineInOut',
'Quad In' => 'quadIn',
'Quad Out' => 'quadOut',
'Quad In/Out' => 'quadInOut',
@@ -154,15 +156,15 @@ class ZoomCameraSongEvent extends SongEvent
'Quint In' => 'quintIn',
'Quint Out' => 'quintOut',
'Quint In/Out' => 'quintInOut',
+ 'Expo In' => 'expoIn',
+ 'Expo Out' => 'expoOut',
+ 'Expo In/Out' => 'expoInOut',
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
- 'Sine In' => 'sineIn',
- 'Sine Out' => 'sineOut',
- 'Sine In/Out' => 'sineInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
- 'Elastic In/Out' => 'elasticInOut',
+ 'Elastic In/Out' => 'elasticInOut'
]
}
]);
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index 47410b9c5..75766a75a 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -223,9 +223,10 @@ class Constants
public static final DEFAULT_VARIATION_LIST:Array = ['default', 'erect', 'pico'];
/**
- * The default intensity for camera zooms.
+ * The default intensity multiplier for camera bops.
+ * Prolly needs to be tuned bc it's a multiplier now.
*/
- public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
+ public static final DEFAULT_BOP_INTENSITY:Float = 1.015;
/**
* The default rate for camera zooms (in beats per zoom).
diff --git a/source/funkin/util/EaseUtil.hx b/source/funkin/util/EaseUtil.hx
new file mode 100644
index 000000000..200e74d07
--- /dev/null
+++ b/source/funkin/util/EaseUtil.hx
@@ -0,0 +1,17 @@
+package funkin.util;
+
+class EaseUtil
+{
+ /**
+ * Returns an ease function that eases via steps.
+ * Useful for "retro" style fades (week 6!)
+ * @param steps how many steps to ease over
+ * @return Float->Float
+ */
+ public static inline function stepped(steps:Int):Float->Float
+ {
+ return function(t:Float):Float {
+ return Math.floor(t * steps) / steps;
+ }
+ }
+}