diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 76126d106..8ea3b16f3 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -31,7 +31,7 @@ jobs:
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- name: build game
run: |
- haxelib run lime build html5 -release --times
+ haxelib run lime build html5 -release --times -DGITHUB_BUILD
ls
- uses: ./.github/actions/upload-itch
with:
@@ -68,7 +68,7 @@ jobs:
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: build game
run: |
- haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
+ haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD
dir
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
@@ -110,7 +110,7 @@ jobs:
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
- haxelib run lime build macos -release --times
+ haxelib run lime build macos -release --times -DGITHUB_BUILD
ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
@@ -119,7 +119,7 @@ jobs:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos
-
+
# test-unit-win:
# needs: create-nightly-win
# runs-on: windows-latest
diff --git a/Project.xml b/Project.xml
index 40f309e1f..6df879583 100644
--- a/Project.xml
+++ b/Project.xml
@@ -91,7 +91,8 @@
NOT USING A DIRECT THING TO THE ASSET!!!
-->
-
+
+
@@ -118,15 +119,15 @@
-
+
-
@@ -211,15 +212,8 @@
-
-
- -->
+ -->
-->
diff --git a/README.md b/README.md
index 8f919d231..39c098af5 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ This game was made with love to Newgrounds and it's community. Extra love to Tom
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
-To learn how to install the necessary dependencies and compile the game from source, please check out our [building the game]() guide.
+To learn how to install the necessary dependencies and compile the game from source, please check out our [building the game](/docs/compiling.md) guide.
# Contributing
diff --git a/assets b/assets
index 594853037..599169f47 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 594853037cbea06caa5c141b0d9ed3736818e592
+Subproject commit 599169f4786d1feb62a089d25112035977647805
diff --git a/docs/COMPILING.md b/docs/COMPILING.md
new file mode 100644
index 000000000..7f9c0cdb8
--- /dev/null
+++ b/docs/COMPILING.md
@@ -0,0 +1,19 @@
+# Compiling Friday Night Funkin'
+
+0. Setup
+ - Download Haxe from [Haxe.org](https://haxe.org)
+1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
+ - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin-secret.git`
+ - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
+2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
+3. Install all haxelibs of the current branch by running `hmm install`
+4. Platform setup
+ - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
+ - When prompted, select "Individual Components" and make sure to download the following:
+ - MSVC v143 VS 2022 C++ x64/x86 build tools
+ - Windows 10/11 SDK
+ - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
+ - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
+ - HTML5: Compiles without any extra setup
+5. If you are targeting for native, you likely need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
+6. `lime test PLATFORM` !
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 27208689b..3b93bab64 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -10,3 +10,6 @@
- `Class lists not properly generated. Try cleaning out your export folder, restarting your IDE, and rebuilding your project.`
- This is a bug specific to HTML5. Simply perform the steps listed (don't forget to restart the IDE too).
+
+- `LINK : fatal error LNK1201: error writing to program database ''; check for insufficient disk space, invalid path, or insufficient privilege`
+ - This error occurs if the PDB file located in your `export` folder is in use or exceeds 4 GB. Try deleting the `export` folder and building again from scratch.
diff --git a/hmm.json b/hmm.json
index 1a1e8ae80..a7f6b1a2c 100644
--- a/hmm.json
+++ b/hmm.json
@@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
- "ref": "07c6018008801972d12275690fc144fcc22e3de6",
+ "ref": "4d054bd10b05bb1309a0ba3427ffa5378e0b4b99",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@@ -88,8 +88,10 @@
},
{
"name": "hxcpp-debug-server",
- "type": "haxelib",
- "version": "1.2.4"
+ "type": "git",
+ "dir": "hxcpp-debug-server",
+ "ref": "147294123f983e35f50a966741474438069a7a8f",
+ "url": "https://github.com/FunkinCrew/hxcpp-debugger"
},
{
"name": "hxp",
@@ -152,6 +154,13 @@
"ref": "0b53e478bc375ec51b760b650201ac7a965d2ef4",
"url": "https://github.com/larsiusprime/polymod"
},
+ {
+ "name": "thx.core",
+ "type": "git",
+ "dir": null,
+ "ref": "22605ff44f01971d599641790d6bae4869f7d9f4",
+ "url": "https://github.com/FunkinCrew/thx.core"
+ },
{
"name": "thx.semver",
"type": "haxelib",
diff --git a/source/flixel/addons/transition/FlxTransitionableSubState.hx b/source/flixel/addons/transition/FlxTransitionableSubState.hx
index 7bb536bb2..ab416adbc 100644
--- a/source/flixel/addons/transition/FlxTransitionableSubState.hx
+++ b/source/flixel/addons/transition/FlxTransitionableSubState.hx
@@ -16,7 +16,7 @@ import flixel.addons.transition.FlxTransitionableState;
* var in:TransitionData = new TransitionData(...); // add your data where "..." is
* var out:TransitionData = new TransitionData(...);
*
- * FlxG.switchState(new FooState(in,out));
+ * FlxG.switchState(() -> new FooState(in,out));
* ```
*
* Method 2:
@@ -25,7 +25,7 @@ import flixel.addons.transition.FlxTransitionableState;
* FlxTransitionableSubState.defaultTransIn = new TransitionData(...);
* FlxTransitionableSubState.defaultTransOut = new TransitionData(...);
*
- * FlxG.switchState(new FooState());
+ * FlxG.switchState(() -> new FooState());
* ```
*/
class FlxTransitionableSubState extends FlxSubState
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 625a33ad7..399f52498 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -89,7 +89,7 @@ class InitState extends FlxState
//
// FLIXEL DEBUG SETUP
//
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
@@ -141,16 +141,14 @@ class InitState extends FlxState
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
+ #end
// Make errors and warnings less annoying.
- // TODO: Disable this so we know to fix warnings.
- if (false)
- {
- LogStyle.ERROR.openConsole = false;
- LogStyle.ERROR.errorSound = null;
- LogStyle.WARNING.openConsole = false;
- LogStyle.WARNING.errorSound = null;
- }
+ #if FORCE_DEBUG_VERSION
+ LogStyle.ERROR.openConsole = false;
+ LogStyle.ERROR.errorSound = null;
+ LogStyle.WARNING.openConsole = false;
+ LogStyle.WARNING.errorSound = null;
#end
//
@@ -247,21 +245,21 @@ class InitState extends FlxState
#elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
#elseif FREEPLAY // -DFREEPLAY
- FlxG.switchState(new FreeplayState());
+ FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
#elseif DIALOGUE // -DDIALOGUE
- FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
+ FlxG.switchState(() -> new funkin.ui.debug.dialogue.ConversationDebugState());
#elseif ANIMATE // -DANIMATE
- FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
+ FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
#elseif WAVEFORM // -DWAVEFORM
- FlxG.switchState(new funkin.ui.debug.WaveformTestState());
+ FlxG.switchState(() -> new funkin.ui.debug.WaveformTestState());
#elseif CHARTING // -DCHARTING
- FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
+ FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGEBUILD // -DSTAGEBUILD
- FlxG.switchState(new funkin.ui.debug.stage.StageBuilderState());
+ FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
#elseif ANIMDEBUG // -DANIMDEBUG
- FlxG.switchState(new funkin.ui.debug.anim.DebugBoundingState());
+ FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
#elseif LATENCY // -DLATENCY
- FlxG.switchState(new funkin.LatencyState());
+ FlxG.switchState(() -> new funkin.LatencyState());
#else
startGameNormally();
#end
@@ -277,7 +275,7 @@ class InitState extends FlxState
if (params.chart.shouldLoadChart)
{
- FlxG.switchState(new ChartEditorState(
+ FlxG.switchState(() -> new ChartEditorState(
{
fnfcTargetPath: params.chart.chartPath,
}));
@@ -285,7 +283,7 @@ class InitState extends FlxState
else
{
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
- FlxG.switchState(new TitleState());
+ FlxG.switchState(() -> new TitleState());
}
}
@@ -308,7 +306,7 @@ class InitState extends FlxState
// TODO: Do this in the loading state.
songData.cacheCharts(true);
- LoadingState.loadAndSwitchState(new funkin.play.PlayState(
+ LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState(
{
targetSong: songData,
targetDifficulty: difficultyId,
@@ -338,7 +336,7 @@ class InitState extends FlxState
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
- LoadingState.loadAndSwitchState(new funkin.play.PlayState(
+ LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState(
{
targetSong: targetSong,
targetDifficulty: difficultyId,
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 95ee117ab..73ecbce14 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -936,6 +936,28 @@ class SongNoteDataRaw implements ICloneable
return SongNoteData.buildDirectionName(this.data, strumlineSize);
}
+ /**
+ * The strumline index of the note, if applicable.
+ * Strips the direction from the data.
+ *
+ * 0 = player, 1 = opponent, etc.
+ */
+ public function getStrumlineIndex(strumlineSize:Int = 4):Int
+ {
+ return Math.floor(this.data / strumlineSize);
+ }
+
+ /**
+ * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side).
+ * TODO: The name of this function is a little misleading; what about mines?
+ * @param strumlineSize Defaults to 4.
+ * @return True if it's Boyfriend's note.
+ */
+ public function getMustHitNote(strumlineSize:Int = 4):Bool
+ {
+ return getStrumlineIndex(strumlineSize) == 0;
+ }
+
@:jignored
var _stepTime:Null = null;
@@ -1024,28 +1046,6 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
}
}
- /**
- * The strumline index of the note, if applicable.
- * Strips the direction from the data.
- *
- * 0 = player, 1 = opponent, etc.
- */
- public inline function getStrumlineIndex(strumlineSize:Int = 4):Int
- {
- return Math.floor(this.data / strumlineSize);
- }
-
- /**
- * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side).
- * TODO: The name of this function is a little misleading; what about mines?
- * @param strumlineSize Defaults to 4.
- * @return True if it's Boyfriend's note.
- */
- public inline function getMustHitNote(strumlineSize:Int = 4):Bool
- {
- return getStrumlineIndex(strumlineSize) == 0;
- }
-
@:jignored
public var isHoldNote(get, never):Bool;
diff --git a/source/funkin/graphics/FunkinCamera.hx b/source/funkin/graphics/FunkinCamera.hx
new file mode 100644
index 000000000..f80e799ea
--- /dev/null
+++ b/source/funkin/graphics/FunkinCamera.hx
@@ -0,0 +1,257 @@
+package funkin.graphics;
+
+import flash.geom.ColorTransform;
+import flixel.FlxCamera;
+import flixel.graphics.FlxGraphic;
+import flixel.graphics.frames.FlxFrame;
+import flixel.math.FlxMatrix;
+import flixel.math.FlxRect;
+import flixel.system.FlxAssets.FlxShader;
+import funkin.graphics.shaders.RuntimeCustomBlendShader;
+import funkin.graphics.framebuffer.BitmapDataUtil;
+import funkin.graphics.framebuffer.FixedBitmapData;
+import openfl.Lib;
+import openfl.display.BitmapData;
+import openfl.display.BlendMode;
+import openfl.display3D.textures.TextureBase;
+import openfl.filters.BitmapFilter;
+import openfl.filters.ShaderFilter;
+
+/**
+ * A FlxCamera with additional powerful features:
+ * - Grab the camera screen as a `BitmapData` and use it as a texture
+ * - Support `sprite.blend = DARKEN/HARDLIGHT/LIGHTEN/OVERLAY` to apply visual effects using certain sprites
+ * - NOTE: Several other blend modes work without FunkinCamera. Some still do not work.
+ * - NOTE: Framerate-independent camera tweening is fixed in Flixel 6.x. Rest in peace, SwagCamera.
+ */
+@:access(openfl.display.DisplayObject)
+@:access(openfl.display.BitmapData)
+@:access(openfl.display3D.Context3D)
+@:access(openfl.display3D.textures.TextureBase)
+@:access(flixel.graphics.FlxGraphic)
+@:access(flixel.graphics.frames.FlxFrame)
+class FunkinCamera extends FlxCamera
+{
+ final grabbed:Array = [];
+ final texturePool:Array = [];
+
+ final bgTexture:TextureBase;
+ final bgBitmap:BitmapData;
+ final bgFrame:FlxFrame;
+
+ final customBlendShader:RuntimeCustomBlendShader;
+ final customBlendFilter:ShaderFilter;
+
+ var filtersApplied:Bool = false;
+ var bgItemCount:Int = 0;
+
+ public var shouldDraw:Bool = true;
+
+ public function new(x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0)
+ {
+ super(x, y, width, height, zoom);
+ bgTexture = pickTexture(width, height);
+ bgBitmap = FixedBitmapData.fromTexture(bgTexture);
+ bgFrame = new FlxFrame(new FlxGraphic('', null));
+ bgFrame.parent.bitmap = bgBitmap;
+ bgFrame.frame = new FlxRect();
+ customBlendShader = new RuntimeCustomBlendShader();
+ customBlendFilter = new ShaderFilter(customBlendShader);
+ }
+
+ /**
+ * Grabs the camera screen and returns it as a `BitmapData`. The returned bitmap
+ * will not be referred by the camera so, changing it will not affect the scene.
+ * The returned bitmap **will be reused in the next frame**, so the content is available
+ * only in the current frame.
+ * @param applyFilters if this is `true`, the camera's filters will be applied to the grabbed bitmap,
+ * and the camera's filters will be disabled until the beginning of the next frame
+ * @param isolate if this is `true`, sprites to be rendered will only be rendered to the grabbed bitmap,
+ * and the grabbed bitmap will not include any previously rendered sprites
+ * @return the grabbed bitmap data
+ */
+ public function grabScreen(applyFilters:Bool, isolate:Bool = false):BitmapData
+ {
+ final texture = pickTexture(width, height);
+ final bitmap = FixedBitmapData.fromTexture(texture);
+ squashTo(bitmap, applyFilters, isolate);
+ grabbed.push(bitmap);
+ return bitmap;
+ }
+
+ /**
+ * Applies the filter immediately to the camera. This will be done independently from
+ * the camera's filters. This method can only be called after the first `grabScreen`
+ * in the frame.
+ * @param filter the filter
+ */
+ public function applyFilter(filter:BitmapFilter):Void
+ {
+ if (grabbed.length == 0)
+ {
+ FlxG.log.error('grab screen before you can apply a filter!');
+ return;
+ }
+ BitmapDataUtil.applyFilter(bgBitmap, filter);
+ }
+
+ function squashTo(bitmap:BitmapData, applyFilters:Bool, isolate:Bool, clearScreen:Bool = false):Void
+ {
+ if (applyFilters && isolate)
+ {
+ FlxG.log.error('cannot apply filters while isolating!');
+ }
+ if (filtersApplied && applyFilters)
+ {
+ FlxG.log.warn('filters already applied!');
+ }
+ static final matrix = new FlxMatrix();
+
+ // resize the background bitmap if needed
+ if (bgTexture.__width != width || bgTexture.__height != height)
+ {
+ BitmapDataUtil.resizeTexture(bgTexture, width, height);
+ bgBitmap.__resize(width, height);
+ bgFrame.parent.bitmap = bgBitmap;
+ }
+
+ // grab the bitmap
+ renderSkipping(isolate ? bgItemCount : 0);
+ bitmap.fillRect(bitmap.rect, 0);
+ matrix.setTo(1, 0, 0, 1, flashSprite.x, flashSprite.y);
+ if (applyFilters)
+ {
+ bitmap.draw(flashSprite, matrix);
+ flashSprite.filters = null;
+ filtersApplied = true;
+ }
+ else
+ {
+ final tmp = flashSprite.filters;
+ flashSprite.filters = null;
+ bitmap.draw(flashSprite, matrix);
+ flashSprite.filters = tmp;
+ }
+
+ if (!isolate)
+ {
+ // also copy to the background bitmap
+ bgBitmap.fillRect(bgBitmap.rect, 0);
+ bgBitmap.draw(bitmap);
+ }
+
+ if (clearScreen)
+ {
+ // clear graphics data
+ super.clearDrawStack();
+ canvas.graphics.clear();
+ }
+
+ // render the background bitmap
+ bgFrame.frame.set(0, 0, width, height);
+ matrix.setTo(viewWidth / width, 0, 0, viewHeight / height, viewMarginLeft, viewMarginTop);
+ drawPixels(bgFrame, matrix);
+
+ // count background draw items for future isolation
+ bgItemCount = 0;
+ {
+ var item = _headOfDrawStack;
+ while (item != null)
+ {
+ item = item.next;
+ bgItemCount++;
+ }
+ }
+ }
+
+ function renderSkipping(count:Int):Void
+ {
+ var item = _headOfDrawStack;
+ while (item != null)
+ {
+ if (--count < 0) item.render(this);
+ item = item.next;
+ }
+ }
+
+ override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false,
+ ?shader:FlxShader):Void
+ {
+ if (!shouldDraw) return;
+
+ if ( switch blend
+ {
+ case DARKEN | HARDLIGHT | LIGHTEN | OVERLAY: true;
+ case _: false;
+ })
+ {
+ // squash the screen
+ grabScreen(false);
+ // render without blend
+ super.drawPixels(frame, pixels, matrix, transform, null, smoothing, shader);
+ // get the isolated bitmap
+ final isolated = grabScreen(false, true);
+ // apply fullscreen blend
+ customBlendShader.blend = blend;
+ customBlendShader.source = isolated;
+ customBlendShader.updateViewInfo(FlxG.width, FlxG.height, this);
+ applyFilter(customBlendFilter);
+ }
+ else
+ {
+ super.drawPixels(frame, pixels, matrix, transform, blend, smoothing, shader);
+ }
+ }
+
+ override function destroy():Void
+ {
+ super.destroy();
+ disposeTextures();
+ }
+
+ override function clearDrawStack():Void
+ {
+ super.clearDrawStack();
+ // also clear grabbed bitmaps
+ for (bitmap in grabbed)
+ {
+ texturePool.push(bitmap.__texture);
+ bitmap.dispose(); // this doesn't release the texture
+ }
+ grabbed.clear();
+ // clear filters applied flag
+ filtersApplied = false;
+ bgItemCount = 0;
+ }
+
+ function pickTexture(width:Int, height:Int):TextureBase
+ {
+ // zero-sized textures will be problematic
+ width = width < 1 ? 1 : width;
+ height = height < 1 ? 1 : height;
+ if (texturePool.length > 0)
+ {
+ final res = texturePool.pop();
+ BitmapDataUtil.resizeTexture(res, width, height);
+ return res;
+ }
+ return Lib.current.stage.context3D.createTexture(width, height, BGRA, true);
+ }
+
+ function disposeTextures():Void
+ {
+ trace('disposing textures');
+ for (bitmap in grabbed)
+ {
+ bitmap.dispose();
+ }
+ grabbed.clear();
+ for (texture in texturePool)
+ {
+ texture.dispose();
+ }
+ texturePool.resize(0);
+ bgTexture.dispose();
+ bgBitmap.dispose();
+ }
+}
diff --git a/source/funkin/graphics/framebuffer/BitmapDataUtil.hx b/source/funkin/graphics/framebuffer/BitmapDataUtil.hx
new file mode 100644
index 000000000..7b49705e0
--- /dev/null
+++ b/source/funkin/graphics/framebuffer/BitmapDataUtil.hx
@@ -0,0 +1,123 @@
+package funkin.graphics.framebuffer;
+
+import flixel.FlxG;
+import openfl.Lib;
+import openfl.display.Bitmap;
+import openfl.display.BitmapData;
+import openfl.display.Sprite;
+import openfl.display3D.Context3DTextureFormat;
+import openfl.display3D.textures.TextureBase;
+import openfl.filters.BitmapFilter;
+
+/**
+ * Provides cool stuff for `BitmapData`s that have a hardware texture internally.
+ */
+@:access(openfl.display.BitmapData)
+@:access(openfl.display3D.textures.TextureBase)
+@:access(openfl.display3D.Context3D)
+class BitmapDataUtil
+{
+ static function getCache():{sprite:Sprite, bitmap:Bitmap}
+ {
+ static var cache:{sprite:Sprite, bitmap:Bitmap} = null;
+ if (cache == null)
+ {
+ final sprite = new Sprite();
+ final bitmap = new Bitmap();
+ sprite.addChild(bitmap);
+ cache =
+ {
+ sprite: sprite,
+ bitmap: bitmap
+ }
+ }
+ return cache;
+ }
+
+ /**
+ * Applies a bitmap filter to a bitmap immediately. The bitmap filter may refer
+ * the bitmap itself as a shader input.
+ * @param bitmap the bitmap data
+ * @param filter the bitmap filter
+ */
+ public static function applyFilter(bitmap:BitmapData, filter:BitmapFilter):Void
+ {
+ hardwareCheck(bitmap);
+ final cache = getCache();
+ cache.bitmap.bitmapData = bitmap;
+ cache.sprite.filters = [filter];
+ bitmap.draw(cache.sprite);
+ }
+
+ /**
+ * Creates a bitmap with a hardware texture.
+ * @param width the width
+ * @param height the height
+ * @param format the format if the internal texture
+ * @return the bitmap
+ */
+ public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):FixedBitmapData
+ {
+ final texture = Lib.current.stage.context3D.createTexture(width, height, format, true);
+ return FixedBitmapData.fromTexture(texture);
+ }
+
+ /**
+ * Resizes the bitmap.
+ * @param bitmap the bitmap data
+ * @param width the width
+ * @param height the height
+ */
+ public static function resize(bitmap:BitmapData, width:Int, height:Int):Void
+ {
+ hardwareCheck(bitmap);
+ if (bitmap.width == width && bitmap.height == height) return;
+ bitmap.width = width;
+ bitmap.height = height;
+ resizeTexture(bitmap.__texture, width, height);
+ }
+
+ /**
+ * Resizes the texture.
+ * @param texture the texture
+ * @param width the width
+ * @param height the height
+ */
+ public static function resizeTexture(texture:TextureBase, width:Int, height:Int):Void
+ {
+ if (texture.__width == width && texture.__height == height) return;
+ texture.__width = width;
+ texture.__height = height;
+ final context = texture.__context;
+ final gl = context.gl;
+ context.__bindGLTexture2D(texture.__textureID);
+ gl.texImage2D(gl.TEXTURE_2D, 0, texture.__internalFormat, width, height, 0, texture.__format, gl.UNSIGNED_BYTE, null);
+ context.__bindGLTexture2D(null);
+ }
+
+ /**
+ * Copies the content of `src` to `dst`. The destination bitmap `dst` will be resized
+ * so that it has the same size as `src`.
+ * @param dst the destination bitmap
+ * @param src the source bitmap
+ */
+ public static function copy(dst:BitmapData, src:BitmapData):Void
+ {
+ hardwareCheck(dst);
+ hardwareCheck(src);
+ final cache = getCache();
+ cache.bitmap.bitmapData = src;
+ cache.sprite.filters = null;
+ resize(dst, src.width, src.height);
+ dst.fillRect(dst.rect, 0);
+ dst.draw(cache.sprite);
+ }
+
+ static function hardwareCheck(bitmap:BitmapData):Void
+ {
+ if (bitmap.readable)
+ {
+ FlxG.log.error('do not use `BitmapDataUtil` for non-GPU bitmaps!');
+ }
+ }
+}
diff --git a/source/funkin/graphics/framebuffer/FixedBitmapData.hx b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
new file mode 100644
index 000000000..00b39ce1c
--- /dev/null
+++ b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
@@ -0,0 +1,42 @@
+package funkin.graphics.framebuffer;
+
+import openfl.display.BitmapData;
+import openfl.display.DisplayObject;
+import openfl.display.DisplayObjectContainer;
+import openfl.display.IBitmapDrawable;
+import openfl.display.OpenGLRenderer;
+import openfl.display3D.textures.TextureBase;
+
+/**
+ * `BitmapData` is kinda broken so I fixed it.
+ */
+@:access(openfl.display3D.textures.TextureBase)
+@:access(openfl.display.OpenGLRenderer)
+class FixedBitmapData extends BitmapData
+{
+ override function __drawGL(source:IBitmapDrawable, renderer:OpenGLRenderer):Void
+ {
+ if (Std.isOfType(source, DisplayObject))
+ {
+ final object:DisplayObjectContainer = cast source;
+ renderer.__stage = object.stage;
+ }
+ super.__drawGL(source, renderer);
+ }
+
+ /**
+ * Never use `BitmapData.fromTexture`, always use this.
+ * @param texture the texture
+ * @return the bitmap data
+ */
+ public static function fromTexture(texture:TextureBase):FixedBitmapData
+ {
+ if (texture == null) return null;
+ final bitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
+ bitmapData.readable = false;
+ bitmapData.__texture = texture;
+ bitmapData.__textureContext = texture.__textureContext;
+ bitmapData.image = null;
+ return bitmapData;
+ }
+}
diff --git a/source/funkin/graphics/framebuffer/FrameBuffer.hx b/source/funkin/graphics/framebuffer/FrameBuffer.hx
new file mode 100644
index 000000000..e99f72b77
--- /dev/null
+++ b/source/funkin/graphics/framebuffer/FrameBuffer.hx
@@ -0,0 +1,132 @@
+package funkin.graphics.framebuffer;
+
+import flixel.FlxSprite;
+import flixel.FlxCamera;
+import flixel.util.FlxColor;
+import openfl.Lib;
+import openfl.display.BitmapData;
+import openfl.display3D.textures.TextureBase;
+import openfl.geom.Matrix;
+import openfl.geom.Rectangle;
+
+/**
+ * A single frame buffer. Used by `FrameBufferManager`.
+ */
+class FrameBuffer
+{
+ /**
+ * The bitmap data of the frame buffer.
+ */
+ public var bitmap(default, null):BitmapData = null;
+
+ var texture:TextureBase;
+ final camera:FlxCamera;
+ final spriteCopies:Array = [];
+
+ public function new()
+ {
+ camera = new FlxCamera();
+ camera.antialiasing = false;
+ camera.bgColor = FlxColor.TRANSPARENT;
+ @:privateAccess camera.flashSprite.stage = Lib.current.stage;
+ }
+
+ /**
+ * Creates a frame buffer with the given size.
+ * @param width the width
+ * @param height the height
+ * @param bgColor the background color
+ */
+ public function create(width:Int, height:Int, bgColor:FlxColor):Void
+ {
+ dispose();
+ texture = Lib.current.stage.context3D.createTexture(width, height, BGRA, true);
+ bitmap = FixedBitmapData.fromTexture(texture);
+ camera.bgColor = bgColor;
+ }
+
+ /**
+ * Makes the internal camera follows the target camera.
+ * @param target the target camera
+ */
+ public function follow(target:FlxCamera):Void
+ {
+ camera.x = target.x;
+ camera.y = target.y;
+ camera.width = target.width;
+ camera.height = target.height;
+ camera.scroll.x = target.scroll.x;
+ camera.scroll.y = target.scroll.y;
+ camera.setScale(target.scaleX, target.scaleY);
+ }
+
+ /**
+ * Locks the frame buffer and clears the buffer.
+ */
+ @:access(flixel.FlxCamera)
+ public function lock():Void
+ {
+ camera.clearDrawStack();
+ camera.canvas.graphics.clear();
+ camera.fill(camera.bgColor.to24Bit(), camera.useBgAlphaBlending, camera.bgColor.alphaFloat);
+ #if FLX_DEBUG
+ camera.debugLayer.graphics.clear();
+ #end
+ }
+
+ /**
+ * Renders all sprite copies.
+ */
+ @:access(flixel.FlxCamera)
+ public function render():Void
+ {
+ for (spriteCopy in spriteCopies)
+ {
+ spriteCopy.render(camera);
+ }
+ camera.render();
+ }
+
+ /**
+ * Unlocks the frame buffer and makes the bitmap ready to use.
+ */
+ public function unlock():Void
+ {
+ bitmap.fillRect(new Rectangle(0, 0, bitmap.width, bitmap.height), 0);
+ bitmap.draw(camera.flashSprite, new Matrix(1, 0, 0, 1, camera.flashSprite.x, camera.flashSprite.y));
+ }
+
+ /**
+ * Diposes stuff. Call `create` again if you want to reuse the instance.
+ */
+ public function dispose():Void
+ {
+ if (texture != null)
+ {
+ texture.dispose();
+ texture = null;
+ bitmap.dispose();
+ bitmap = null;
+ }
+ spriteCopies.resize(0);
+ }
+
+ /**
+ * Adds a sprite copy to the frame buffer.
+ * @param spriteCopy the sprite copy
+ */
+ public function addSpriteCopy(spriteCopy:SpriteCopy):Void
+ {
+ spriteCopies.push(spriteCopy);
+ }
+
+ /**
+ * Adds the sprite to the frame buffer. The sprite will only be seen from
+ * the frame buffer.
+ * @param sprite the sprite
+ */
+ public function moveSprite(sprite:FlxSprite):Void
+ {
+ sprite.cameras = [camera];
+ }
+}
diff --git a/source/funkin/graphics/framebuffer/FrameBufferManager.hx b/source/funkin/graphics/framebuffer/FrameBufferManager.hx
new file mode 100644
index 000000000..4d484fb8f
--- /dev/null
+++ b/source/funkin/graphics/framebuffer/FrameBufferManager.hx
@@ -0,0 +1,127 @@
+package funkin.graphics.framebuffer;
+
+import flixel.FlxCamera;
+import flixel.FlxG;
+import flixel.FlxSprite;
+import flixel.util.FlxColor;
+import openfl.display.BitmapData;
+
+/**
+ * Manages frame buffers and gives access to each frame buffer.
+ */
+class FrameBufferManager
+{
+ final camera:FlxCamera;
+ final frameBufferMap:Map = [];
+
+ /**
+ * Creates a frame buffer manager that targets `camera`.
+ * @param camera the target camera.
+ */
+ public function new(camera:FlxCamera)
+ {
+ this.camera = camera;
+ }
+
+ /**
+ * Creates a new frame buffer with a name.
+ * @param name the name
+ * @param bgColor the background color
+ * @return the bitmap data of the frame buffer. the bitmap data instance
+ * will not be changed through frame buffer updates.
+ */
+ public function createFrameBuffer(name:String, bgColor:FlxColor):BitmapData
+ {
+ if (frameBufferMap.exists(name))
+ {
+ FlxG.log.warn('frame buffer "$name" already exists');
+ frameBufferMap[name].dispose();
+ frameBufferMap.remove(name);
+ }
+ final fb = new FrameBuffer();
+ fb.create(camera.width, camera.height, bgColor);
+ frameBufferMap[name] = fb;
+ return fb.bitmap;
+ }
+
+ /**
+ * Adds a copy of the sprite to the frame buffer.
+ * @param name the name of the frame buffer
+ * @param sprite the sprite
+ * @param color if this is not `null`, the sprite will be filled with the color.
+ * if this is `null`, the sprite will keep its original color.
+ */
+ public function copySpriteTo(name:String, sprite:FlxSprite, color:Null = null):Void
+ {
+ if (!frameBufferMap.exists(name))
+ {
+ FlxG.log.warn('frame buffer "$name" does not exist');
+ return;
+ }
+ frameBufferMap[name].addSpriteCopy(new SpriteCopy(sprite, color));
+ }
+
+ /**
+ * Adds the sprite to the frame buffer. The sprite will only be seen from the frame buffer.
+ * @param name the name of the frame buffer
+ * @param sprite the sprite
+ */
+ public function moveSpriteTo(name:String, sprite:FlxSprite):Void
+ {
+ if (!frameBufferMap.exists(name))
+ {
+ FlxG.log.warn('frame buffer "$name" does not exist');
+ return;
+ }
+ frameBufferMap[name].moveSprite(sprite);
+ }
+
+ /**
+ * Call this before drawing anything.
+ */
+ public function lock():Void
+ {
+ for (_ => fb in frameBufferMap)
+ {
+ fb.follow(camera);
+ fb.lock();
+ }
+ }
+
+ /**
+ * Unlocks the frame buffers. This updates the bitmap data of each frame buffer.
+ */
+ public function unlock():Void
+ {
+ for (_ => fb in frameBufferMap)
+ {
+ fb.render();
+ }
+ for (_ => fb in frameBufferMap)
+ {
+ fb.unlock();
+ }
+ }
+
+ /**
+ * Returns the bitmap data of the frame buffer
+ * @param name the name of the frame buffer
+ * @return the bitmap data
+ */
+ public function getFrameBuffer(name:String):BitmapData
+ {
+ return frameBufferMap[name].bitmap;
+ }
+
+ /**
+ * Disposes all frame buffers. The instance can be reused.
+ */
+ public function dispose():Void
+ {
+ for (_ => fb in frameBufferMap)
+ {
+ fb.dispose();
+ }
+ frameBufferMap.clear();
+ }
+}
diff --git a/source/funkin/graphics/framebuffer/SpriteCopy.hx b/source/funkin/graphics/framebuffer/SpriteCopy.hx
new file mode 100644
index 000000000..ea7de69dc
--- /dev/null
+++ b/source/funkin/graphics/framebuffer/SpriteCopy.hx
@@ -0,0 +1,59 @@
+package funkin.graphics.framebuffer;
+
+import flixel.FlxCamera;
+import flixel.FlxSprite;
+import flixel.util.FlxColor;
+
+/**
+ * A copy of a `FlxSprite` with a specified color. Used to render the sprite to a frame buffer.
+ */
+class SpriteCopy
+{
+ final sprite:FlxSprite;
+ final color:Null;
+
+ public function new(sprite:FlxSprite, color:Null)
+ {
+ this.sprite = sprite;
+ this.color = color;
+ }
+
+ /**
+ * Renders the copy to the camera.
+ * @param camera the camera
+ */
+ @:access(flixel.FlxSprite)
+ public function render(camera:FlxCamera):Void
+ {
+ if (color == null)
+ {
+ final tmpCameras = sprite._cameras;
+ sprite._cameras = [camera];
+ sprite.draw();
+ sprite._cameras = tmpCameras;
+ }
+ else
+ {
+ final rMult = sprite.colorTransform.redMultiplier;
+ final gMult = sprite.colorTransform.greenMultiplier;
+ final bMult = sprite.colorTransform.blueMultiplier;
+ final aMult = sprite.colorTransform.alphaMultiplier;
+ final rOff = Std.int(sprite.colorTransform.redOffset);
+ final gOff = Std.int(sprite.colorTransform.greenOffset);
+ final bOff = Std.int(sprite.colorTransform.blueOffset);
+ final aOff = Std.int(sprite.colorTransform.alphaOffset);
+ final tmpCameras = sprite._cameras;
+ final tmpShader = sprite.shader;
+
+ sprite._cameras = [camera];
+ sprite.shader = null;
+
+ sprite.setColorTransform(0, 0, 0, 1, color.red, color.green, color.blue, 0);
+ sprite.draw();
+
+ sprite._cameras = tmpCameras;
+ sprite.shader = tmpShader;
+ sprite.setColorTransform(rMult, gMult, bMult, aMult, rOff, gOff, bOff, aOff);
+ }
+ }
+}
diff --git a/source/funkin/graphics/shaders/PuddleShader.hx b/source/funkin/graphics/shaders/PuddleShader.hx
new file mode 100644
index 000000000..352568737
--- /dev/null
+++ b/source/funkin/graphics/shaders/PuddleShader.hx
@@ -0,0 +1,12 @@
+package funkin.graphics.shaders;
+
+import flixel.addons.display.FlxRuntimeShader;
+import openfl.Assets;
+
+class PuddleShader extends FlxRuntimeShader
+{
+ public function new()
+ {
+ super(Assets.getText(Paths.frag('puddle')));
+ }
+}
diff --git a/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx b/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx
new file mode 100644
index 000000000..a07124d23
--- /dev/null
+++ b/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx
@@ -0,0 +1,29 @@
+package funkin.graphics.shaders;
+
+import openfl.display.BitmapData;
+import openfl.display.BlendMode;
+import openfl.utils.Assets;
+
+class RuntimeCustomBlendShader extends RuntimePostEffectShader
+{
+ public var source(default, set):BitmapData;
+
+ function set_source(value:BitmapData):BitmapData
+ {
+ this.setBitmapData("source", value);
+ return source = value;
+ }
+
+ public var blend(default, set):BlendMode;
+
+ function set_blend(value:BlendMode):BlendMode
+ {
+ this.setInt("blendMode", cast value);
+ return blend = value;
+ }
+
+ public function new()
+ {
+ super(Assets.getText("assets/shaders/customBlend.frag"));
+ }
+}
diff --git a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx
new file mode 100644
index 000000000..9f49da075
--- /dev/null
+++ b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx
@@ -0,0 +1,105 @@
+package funkin.graphics.shaders;
+
+import flixel.FlxCamera;
+import flixel.FlxG;
+import flixel.addons.display.FlxRuntimeShader;
+import lime.graphics.opengl.GLProgram;
+import lime.utils.Log;
+
+class RuntimePostEffectShader extends FlxRuntimeShader
+{
+ @:glVertexHeader('
+ // normalized screen coord
+ // (0, 0) is the top left of the window
+ // (1, 1) is the bottom right of the window
+ varying vec2 screenCoord;
+ ', true)
+ @:glVertexBody('
+ screenCoord = vec2(
+ openfl_TextureCoord.x > 0.0 ? 1.0 : 0.0,
+ openfl_TextureCoord.y > 0.0 ? 1.0 : 0.0
+ );
+ ')
+ @:glFragmentHeader('
+ // normalized screen coord
+ // (0, 0) is the top left of the window
+ // (1, 1) is the bottom right of the window
+ varying vec2 screenCoord;
+
+ // equals (FlxG.width, FlxG.height)
+ uniform vec2 uScreenResolution;
+
+ // equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
+ uniform vec4 uCameraBounds;
+
+ // screen coord -> world coord conversion
+ // returns world coord in px
+ vec2 screenToWorld(vec2 screenCoord) {
+ float left = uCameraBounds.x;
+ float top = uCameraBounds.y;
+ float right = uCameraBounds.z;
+ float bottom = uCameraBounds.w;
+ vec2 scale = vec2(right - left, bottom - top);
+ vec2 offset = vec2(left, top);
+ return screenCoord * scale + offset;
+ }
+
+ // world coord -> screen coord conversion
+ // returns normalized screen coord
+ vec2 worldToScreen(vec2 worldCoord) {
+ float left = uCameraBounds.x;
+ float top = uCameraBounds.y;
+ float right = uCameraBounds.z;
+ float bottom = uCameraBounds.w;
+ vec2 scale = vec2(right - left, bottom - top);
+ vec2 offset = vec2(left, top);
+ return (worldCoord - offset) / scale;
+ }
+
+ // internally used to get the maximum `openfl_TextureCoordv`
+ vec2 bitmapCoordScale() {
+ return openfl_TextureCoordv / screenCoord;
+ }
+
+ // internally used to compute bitmap coord
+ vec2 screenToBitmap(vec2 screenCoord) {
+ return screenCoord * bitmapCoordScale();
+ }
+
+ // samples the frame buffer using a screen coord
+ vec4 sampleBitmapScreen(vec2 screenCoord) {
+ return texture2D(bitmap, screenToBitmap(screenCoord));
+ }
+
+ // samples the frame buffer using a world coord
+ vec4 sampleBitmapWorld(vec2 worldCoord) {
+ return sampleBitmapScreen(worldToScreen(worldCoord));
+ }
+ ', true)
+ public function new(fragmentSource:String = null, glVersion:String = null)
+ {
+ super(fragmentSource, null, glVersion);
+ uScreenResolution.value = [FlxG.width, FlxG.height];
+ }
+
+ // basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good
+ public function updateViewInfo(screenWidth:Float, screenHeight:Float, camera:FlxCamera):Void
+ {
+ uScreenResolution.value = [screenWidth, screenHeight];
+ uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom];
+ }
+
+ override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
+ {
+ try
+ {
+ final res = super.__createGLProgram(vertexSource, fragmentSource);
+ return res;
+ }
+ catch (error)
+ {
+ Log.warn(error); // prevent the app from dying immediately
+ return null;
+ }
+ }
+}
diff --git a/source/funkin/graphics/shaders/RuntimeRainShader.hx b/source/funkin/graphics/shaders/RuntimeRainShader.hx
new file mode 100644
index 000000000..05bc68f72
--- /dev/null
+++ b/source/funkin/graphics/shaders/RuntimeRainShader.hx
@@ -0,0 +1,144 @@
+package funkin.graphics.shaders;
+
+import flixel.system.FlxAssets.FlxShader;
+import openfl.display.BitmapData;
+import openfl.display.ShaderParameter;
+import openfl.display.ShaderParameterType;
+import openfl.utils.Assets;
+
+typedef Light =
+{
+ var position:Array;
+ var color:Array;
+ var radius:Float;
+}
+
+class RuntimeRainShader extends RuntimePostEffectShader
+{
+ static final MAX_LIGHTS:Int = 8;
+
+ public var lights:Array<
+ {
+ position:ShaderParameter,
+ color:ShaderParameter,
+ radius:ShaderParameter,
+ }>;
+
+ public var time(default, set):Float = 1;
+
+ function set_time(value:Float):Float
+ {
+ this.setFloat('uTime', value);
+ return time = value;
+ }
+
+ // The scale of the rain depends on the world coordinate system, so higher resolution makes
+ // the raindrops smaller. This parameter can be used to adjust the total scale of the scene.
+ // The size of the raindrops is proportional to the value of this parameter.
+ public var scale(default, set):Float = 1;
+
+ function set_scale(value:Float):Float
+ {
+ this.setFloat('uScale', value);
+ return scale = value;
+ }
+
+ // The intensity of the rain. Zero means no rain and one means the maximum amount of rain.
+ public var intensity(default, set):Float = 0.5;
+
+ function set_intensity(value:Float):Float
+ {
+ this.setFloat('uIntensity', value);
+ return intensity = value;
+ }
+
+ // the y coord of the puddle, used to mirror things
+ public var puddleY(default, set):Float = 0;
+
+ function set_puddleY(value:Float):Float
+ {
+ this.setFloat('uPuddleY', value);
+ return puddleY = value;
+ }
+
+ // the y scale of the puddle, the less this value the more the puddle effects squished
+ public var puddleScaleY(default, set):Float = 0;
+
+ function set_puddleScaleY(value:Float):Float
+ {
+ this.setFloat('uPuddleScaleY', value);
+ return puddleScaleY = value;
+ }
+
+ public var blurredScreen(default, set):BitmapData;
+
+ function set_blurredScreen(value:BitmapData):BitmapData
+ {
+ this.setBitmapData('uBlurredScreen', value);
+ return blurredScreen = value;
+ }
+
+ public var mask(default, set):BitmapData;
+
+ function set_mask(value:BitmapData):BitmapData
+ {
+ this.setBitmapData('uMask', value);
+ return mask = value;
+ }
+
+ public var lightMap(default, set):BitmapData;
+
+ function set_lightMap(value:BitmapData):BitmapData
+ {
+ this.setBitmapData('uLightMap', value);
+ return lightMap = value;
+ }
+
+ public var numLights(default, set):Int = 0;
+
+ function set_numLights(value:Int):Int
+ {
+ this.setInt('numLights', value);
+ return numLights = value;
+ }
+
+ public function new()
+ {
+ super(Assets.getText(Paths.frag('rain')));
+ }
+
+ public function update(elapsed:Float):Void
+ {
+ time += elapsed;
+ }
+
+ override function __processGLData(source:String, storageType:String):Void
+ {
+ super.__processGLData(source, storageType);
+ if (storageType == 'uniform')
+ {
+ lights = [
+ for (i in 0...MAX_LIGHTS)
+ {
+ position: addFloatUniform('lights[$i].position', 2),
+ color: addFloatUniform('lights[$i].color', 3),
+ radius: addFloatUniform('lights[$i].radius', 1),
+ }
+ ];
+ }
+ }
+
+ @:access(openfl.display.ShaderParameter)
+ function addFloatUniform(name:String, length:Int):ShaderParameter
+ {
+ final res = new ShaderParameter();
+ res.name = name;
+ res.type = [null, FLOAT, FLOAT2, FLOAT3, FLOAT4][length];
+ res.__arrayLength = 1;
+ res.__isFloat = true;
+ res.__isUniform = true;
+ res.__length = length;
+ __paramFloat.push(res);
+ return res;
+ }
+}
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 5ca6b03db..02055d4ed 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -6,6 +6,7 @@ import funkin.util.Constants;
import funkin.Paths;
import funkin.Preferences;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
+import flixel.system.debug.watch.Tracker;
// These are great.
using Lambda;
diff --git a/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx b/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx
new file mode 100644
index 000000000..e9ffd450d
--- /dev/null
+++ b/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx
@@ -0,0 +1,8 @@
+package funkin.modding.base;
+
+/**
+ * A script that can be tied to an FlxAtlasSprite
+ * Create a scripted class that extends FlxAtlasSprite to use this.
+ */
+@:hscriptClass
+class ScriptedFlxAtlasSprite extends funkin.graphics.adobeanimate.FlxAtlasSprite implements HScriptedClass {}
diff --git a/source/funkin/modding/base/ScriptedFunkinSprite.hx b/source/funkin/modding/base/ScriptedFunkinSprite.hx
new file mode 100644
index 000000000..dd8d15007
--- /dev/null
+++ b/source/funkin/modding/base/ScriptedFunkinSprite.hx
@@ -0,0 +1,8 @@
+package funkin.modding.base;
+
+/**
+ * A script that can be tied to an FlxSprite.
+ * Create a scripted class that extends FlxSprite to use this.
+ */
+@:hscriptClass
+class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements HScriptedClass {}
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 36f72237e..74b39417e 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -90,6 +90,7 @@ class GameOverSubState extends MusicBeatSubState
{
animationSuffix = "";
musicSuffix = "";
+ blueBallSuffix = "";
}
override public function create()
@@ -207,11 +208,11 @@ class GameOverSubState extends MusicBeatSubState
}
else if (PlayStatePlaylist.isStoryMode)
{
- FlxG.switchState(new StoryMenuState());
+ FlxG.switchState(() -> new StoryMenuState());
}
else
{
- FlxG.switchState(new FreeplayState());
+ FlxG.switchState(() -> new FreeplayState());
}
}
diff --git a/source/funkin/play/GitarooPause.hx b/source/funkin/play/GitarooPause.hx
index dbfbf5961..edeb4229c 100644
--- a/source/funkin/play/GitarooPause.hx
+++ b/source/funkin/play/GitarooPause.hx
@@ -66,11 +66,11 @@ class GitarooPause extends MusicBeatState
{
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
- FlxG.switchState(new PlayState(previousParams));
+ FlxG.switchState(() -> new PlayState(previousParams));
}
else
{
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
}
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index 62a4e3f4d..ff56dcb8f 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -41,7 +41,8 @@ class PauseSubState extends MusicBeatSubState
var practiceText:FlxText;
- var exitingToMenu:Bool = false;
+ public var exitingToMenu:Bool = false;
+
var bg:FlxSprite;
var metaDataGrp:FlxTypedGroup;
@@ -171,7 +172,7 @@ class PauseSubState extends MusicBeatSubState
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
@@ -237,11 +238,11 @@ class PauseSubState extends MusicBeatSubState
if (PlayStatePlaylist.isStoryMode)
{
PlayStatePlaylist.reset();
- openSubState(new funkin.ui.transition.StickerSubState(null, STORY));
+ openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
}
else
{
- openSubState(new funkin.ui.transition.StickerSubState(null, FREEPLAY));
+ openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
}
case 'Adjust Input Offsets':
openSubState(new LatencyState());
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index aee9f2210..be4fab254 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,68 +1,79 @@
package funkin.play;
-import funkin.ui.SwagCamera;
-import flixel.addons.transition.FlxTransitionableSubState;
-import funkin.ui.debug.charting.ChartEditorState;
-import haxe.Int64;
-import funkin.play.notes.notestyle.NoteStyle;
-import funkin.data.notestyle.NoteStyleData;
-import funkin.data.notestyle.NoteStyleRegistry;
import flixel.addons.display.FlxPieDial;
-import flixel.addons.transition.Transition;
+import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState;
+import flixel.addons.transition.FlxTransitionableState;
+import flixel.addons.transition.FlxTransitionableSubState;
+import flixel.addons.transition.FlxTransitionableSubState;
+import flixel.addons.transition.Transition;
+import flixel.addons.transition.Transition;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.FlxSubState;
-import flixel.input.keyboard.FlxKey;
import flixel.math.FlxMath;
-import funkin.play.components.ComboMilestone;
import flixel.math.FlxPoint;
-import funkin.play.components.HealthIcon;
-import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
import flixel.util.FlxColor;
-import funkin.api.newgrounds.NGio;
import flixel.util.FlxTimer;
+import funkin.api.newgrounds.NGio;
import funkin.audio.VoicesGroup;
-import funkin.save.Save;
+import funkin.audio.VoicesGroup;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.event.SongEventRegistry;
+import funkin.data.notestyle.NoteStyleData;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.song.SongData.SongCharacterData;
+import funkin.data.song.SongData.SongEventData;
+import funkin.data.song.SongData.SongNoteData;
+import funkin.data.song.SongRegistry;
+import funkin.data.stage.StageRegistry;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
-import funkin.ui.mainmenu.MainMenuState;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.components.ComboMilestone;
+import funkin.play.components.HealthIcon;
+import funkin.play.components.PopUpStuff;
+import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.Conversation;
-import funkin.data.dialogue.ConversationRegistry;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
-import funkin.data.event.SongEventRegistry;
-import funkin.play.notes.NoteSprite;
import funkin.play.notes.NoteDirection;
+import funkin.play.notes.NoteSplash;
+import funkin.play.notes.NoteSprite;
+import funkin.play.notes.NoteSprite;
+import funkin.play.notes.notestyle.NoteStyle;
+import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
-import funkin.data.song.SongRegistry;
-import funkin.data.stage.StageRegistry;
-import funkin.data.song.SongData.SongEventData;
-import funkin.data.song.SongData.SongNoteData;
-import funkin.data.song.SongData.SongCharacterData;
import funkin.play.stage.Stage;
-import funkin.ui.transition.LoadingState;
-import funkin.play.components.PopUpStuff;
-import funkin.ui.options.PreferencesMenu;
+import funkin.save.Save;
+import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.stage.StageOffsetSubState;
+import funkin.ui.mainmenu.MainMenuState;
+import funkin.ui.MusicBeatSubState;
+import funkin.ui.options.PreferencesMenu;
import funkin.ui.story.StoryMenuState;
+import funkin.graphics.FunkinCamera;
+import funkin.ui.transition.LoadingState;
import funkin.util.SerializerUtil;
-import funkin.util.SortUtil;
+import haxe.Int64;
import lime.ui.Haptic;
+import openfl.display.BitmapData;
+import openfl.geom.Rectangle;
+import openfl.Lib;
#if discord_rpc
import Discord.DiscordClient;
#end
@@ -83,10 +94,16 @@ typedef PlayStateParams =
*/
?targetDifficulty:String,
/**
- * The character to play as.
- * @default `bf`, or the first character in the song's character list.
+ * The variation to play on.
+ * @default `Constants.DEFAULT_VARIATION` .
*/
- ?targetCharacter:String,
+ ?targetVariation:String,
+ /**
+ * The instrumental to play with.
+ * Significant if the `targetSong` supports alternate instrumentals.
+ * @default `null`
+ */
+ ?targetInstrumental:String,
/**
* Whether the song should start in Practice Mode.
* @default `false`
@@ -147,9 +164,9 @@ class PlayState extends MusicBeatSubState
public var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
/**
- * The player character being used for this level, as a character ID.
+ * The currently selected variation.
*/
- public var currentPlayerId:String = 'bf';
+ public var currentVariation:String = Constants.DEFAULT_VARIATION;
/**
* The currently active Stage. This is the object containing all the props.
@@ -439,6 +456,17 @@ class PlayState extends MusicBeatSubState
return this.subState != null;
}
+ var isExitingViaPauseMenu(get, never):Bool;
+
+ function get_isExitingViaPauseMenu():Bool
+ {
+ if (this.subState == null) return false;
+ if (!Std.isOfType(this.subState, PauseSubState)) return false;
+
+ var pauseSubState:PauseSubState = cast this.subState;
+ return pauseSubState.exitingToMenu;
+ }
+
/**
* Data for the current difficulty for the current song.
* Includes chart data, scroll speed, and other information.
@@ -448,7 +476,7 @@ class PlayState extends MusicBeatSubState
function get_currentChart():SongDifficulty
{
if (currentSong == null || currentDifficulty == null) return null;
- return currentSong.getDifficulty(currentDifficulty);
+ return currentSong.getDifficulty(currentDifficulty, currentVariation);
}
/**
@@ -506,7 +534,7 @@ class PlayState extends MusicBeatSubState
// Apply parameters.
currentSong = params.targetSong;
if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
- if (params.targetCharacter != null) currentPlayerId = params.targetCharacter;
+ if (params.targetVariation != null) currentVariation = params.targetVariation;
isPracticeMode = params.practiceMode ?? false;
isMinimalMode = params.minimalMode ?? false;
startTimestamp = params.startTimestamp ?? 0.0;
@@ -638,7 +666,7 @@ class PlayState extends MusicBeatSubState
rightWatermarkText.cameras = [camHUD];
// Initialize some debug stuff.
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// Display the version number (and git commit hash) in the bottom right corner.
this.rightWatermarkText.text = Constants.VERSION;
@@ -684,9 +712,9 @@ class PlayState extends MusicBeatSubState
{
message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
}
- else if (currentSong.getDifficulty(currentDifficulty) == null)
+ else if (currentChart == null)
{
- message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.';
+ message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
@@ -699,7 +727,7 @@ class PlayState extends MusicBeatSubState
}
else
{
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
return false;
}
@@ -828,11 +856,11 @@ class PlayState extends MusicBeatSubState
// It's a reference to Gitaroo Man, which doesn't let you pause the game.
if (!isSubState && event.gitaroo)
{
- FlxG.switchState(new GitarooPause(
+ FlxG.switchState(() -> new GitarooPause(
{
targetSong: currentSong,
targetDifficulty: currentDifficulty,
- targetCharacter: currentPlayerId,
+ targetVariation: currentVariation,
}));
}
else
@@ -879,7 +907,7 @@ class PlayState extends MusicBeatSubState
// TODO: Add a song event for Handle GF dance speed.
// Handle player death.
- if (!isInCutscene && !disableKeys && !_exiting)
+ if (!isInCutscene && !disableKeys)
{
// RESET = Quick Game Over Screen
if (controls.RESET)
@@ -907,7 +935,7 @@ class PlayState extends MusicBeatSubState
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
if (FlxG.keys.pressed.THREE)
{
// TODO: Change the key or delete this?
@@ -918,7 +946,7 @@ class PlayState extends MusicBeatSubState
{
#end
persistentDraw = false;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
}
#end
@@ -1276,7 +1304,7 @@ class PlayState extends MusicBeatSubState
*/
function initCameras():Void
{
- camGame = new SwagCamera();
+ camGame = new FunkinCamera();
camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
@@ -1368,7 +1396,7 @@ class PlayState extends MusicBeatSubState
// Add the stage to the scene.
this.add(currentStage);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('stage', currentStage);
#end
}
@@ -1389,7 +1417,7 @@ class PlayState extends MusicBeatSubState
trace('Song difficulty could not be loaded.');
}
- var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the character we are playing as by manipulating currentPlayerId.
+ var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the variation we are playing on by manipulating targetVariation.
//
// GIRLFRIEND
@@ -1458,7 +1486,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(girlfriend, GF);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('gf', girlfriend);
#end
}
@@ -1467,7 +1495,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(boyfriend, BF);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('bf', boyfriend);
#end
}
@@ -1478,7 +1506,7 @@ class PlayState extends MusicBeatSubState
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('dad', dad);
#end
}
@@ -1733,7 +1761,7 @@ class PlayState extends MusicBeatSubState
*/
function resyncVocals():Void
{
- if (_exiting || vocals == null) return;
+ if (vocals == null) return;
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!FlxG.sound.music.playing) return;
@@ -1959,7 +1987,7 @@ class PlayState extends MusicBeatSubState
// Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0;
- currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
+ if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
}
}
}
@@ -2229,7 +2257,7 @@ class PlayState extends MusicBeatSubState
#end
// Eject button
- if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
if (FlxG.keys.justPressed.F5) debug_refreshModules();
@@ -2247,13 +2275,13 @@ class PlayState extends MusicBeatSubState
{
disableKeys = true;
persistentUpdate = false;
- FlxG.switchState(new ChartEditorState(
+ FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
}));
}
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong();
@@ -2267,7 +2295,7 @@ class PlayState extends MusicBeatSubState
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// PAGEUP: Skip forward two sections.
// SHIFT+PAGEUP: Skip forward twenty sections.
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
@@ -2513,8 +2541,8 @@ class PlayState extends MusicBeatSubState
{
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
- transIn = FlxTransitionableState.defaultTransIn;
- transOut = FlxTransitionableState.defaultTransOut;
+ // transIn = FlxTransitionableState.defaultTransIn;
+ // transOut = FlxTransitionableState.defaultTransOut;
// TODO: Rework week unlock logic.
// StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
@@ -2589,14 +2617,16 @@ class PlayState extends MusicBeatSubState
// TODO: Do this in the loading state.
targetSong.cacheCharts(true);
- var nextPlayState:PlayState = new PlayState(
- {
- targetSong: targetSong,
- targetDifficulty: PlayStatePlaylist.campaignDifficulty,
- targetCharacter: currentPlayerId,
- });
- nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
- LoadingState.loadAndSwitchState(nextPlayState);
+ LoadingState.loadAndSwitchState(() -> {
+ var nextPlayState:PlayState = new PlayState(
+ {
+ targetSong: targetSong,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
+ targetVariation: currentVariation,
+ });
+ nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
+ return nextPlayState;
+ });
});
}
else
@@ -2605,14 +2635,16 @@ class PlayState extends MusicBeatSubState
// Load and cache the song's charts.
// TODO: Do this in the loading state.
targetSong.cacheCharts(true);
- var nextPlayState:PlayState = new PlayState(
- {
- targetSong: targetSong,
- targetDifficulty: PlayStatePlaylist.campaignDifficulty,
- targetCharacter: currentPlayerId,
- });
- nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
- LoadingState.loadAndSwitchState(nextPlayState);
+ LoadingState.loadAndSwitchState(() -> {
+ var nextPlayState:PlayState = new PlayState(
+ {
+ targetSong: targetSong,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
+ targetVariation: currentVariation,
+ });
+ nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
+ return nextPlayState;
+ });
}
}
}
@@ -2650,13 +2682,16 @@ class PlayState extends MusicBeatSubState
{
// Stop the music.
FlxG.sound.music.pause();
- vocals.stop();
+ if (vocals != null) vocals.stop();
}
else
{
FlxG.sound.music.pause();
- vocals.pause();
- remove(vocals);
+ if (vocals != null)
+ {
+ vocals.pause();
+ remove(vocals);
+ }
}
// Remove reference to stage and remove sprites from it to save memory.
@@ -2768,7 +2803,7 @@ class PlayState extends MusicBeatSubState
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
/**
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 507fa1236..9ffeefcfd 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -352,11 +352,11 @@ class ResultState extends MusicBeatSubState
{
if (params.storyMode)
{
- FlxG.switchState(new StoryMenuState());
+ openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
}
else
{
- FlxG.switchState(new FreeplayState());
+ openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker)));
}
}
diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 0d90df5a0..420a4fdc4 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -148,11 +148,12 @@ class HealthIcon extends FlxSprite
{
if (characterId == 'bf-old')
{
- characterId = PlayState.instance.currentPlayerId;
+ PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false);
}
else
{
characterId = 'bf-old';
+ loadCharacter(characterId);
}
}
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 0434607f3..970aebc57 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -184,7 +184,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.get(diffId);
+ var variationSuffix = (variation != Constants.DEFAULT_VARIATION) ? '-$variation' : '';
+ var difficulty:Null = difficulties.get('$diffId$variationSuffix');
if (difficulty == null)
{
trace('Fabricated new difficulty for $diffId.');
difficulty = new SongDifficulty(this, diffId, variation);
var metadata = _metadata.get(variation);
- difficulties.set(diffId, difficulty);
+ difficulties.set('$diffId$variationSuffix', difficulty);
if (metadata != null)
{
@@ -254,41 +256,80 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry
+ public function getDifficulty(?diffId:String, ?variation:String, ?variations:Array):Null
{
- if (diffId == null) diffId = listDifficulties()[0];
+ if (diffId == null) diffId = listDifficulties(variation)[0];
+ if (variation == null) variation = Constants.DEFAULT_VARIATION;
+ if (variations == null) variations = [variation];
- return difficulties.get(diffId);
+ for (currentVariation in variations)
+ {
+ var variationSuffix = (currentVariation != Constants.DEFAULT_VARIATION) ? '-$currentVariation' : '';
+
+ if (difficulties.exists('$diffId$variationSuffix'))
+ {
+ return difficulties.get('$diffId$variationSuffix');
+ }
+ }
+
+ return null;
+ }
+
+ public function getFirstValidVariation(?diffId:String, ?possibleVariations:Array):Null
+ {
+ if (variations == null) possibleVariations = variations;
+ if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
+
+ for (variation in variations)
+ {
+ if (difficulties.exists('$diffId-$variation')) return variation;
+ }
+
+ return null;
}
/**
* List all the difficulties in this song.
- * @param variationId Optionally filter by variation.
+ * @param variationId Optionally filter by a single variation.
+ * @param variationIds Optionally filter by multiple variations.
* @return The list of difficulties.
*/
- public function listDifficulties(?variationId:String):Array
+ public function listDifficulties(?variationId:String, ?variationIds:Array):Array
{
- if (variationId == '') variationId = null;
+ if (variationIds == null) variationIds = [];
+ if (variationId != null) variationIds.push(variationId);
- var diffFiltered:Array = difficulties.keys().array().filter(function(diffId:String):Bool {
- if (variationId == null) return true;
+ // The difficulties array contains entries like 'normal', 'nightmare-erect', and 'normal-pico',
+ // so we have to map it to the actual difficulty names.
+ // We also filter out difficulties that don't match the variation or that don't exist.
+
+ var diffFiltered:Array = difficulties.keys().array().map(function(diffId:String):Null {
var difficulty:Null = difficulties.get(diffId);
- if (difficulty == null) return false;
- return difficulty.variation == variationId;
- });
+ if (difficulty == null) return null;
+ if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
+ return difficulty.difficulty;
+ }).nonNull().unique();
diffFiltered.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
return diffFiltered;
}
- public function hasDifficulty(diffId:String, ?variationId:String):Bool
+ public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array):Bool
{
- if (variationId == '') variationId = null;
- var difficulty:Null = difficulties.get(diffId);
- return variationId == null ? (difficulty != null) : (difficulty != null && difficulty.variation == variationId);
+ if (variationIds == null) variationIds = [];
+ if (variationId != null) variationIds.push(variationId);
+
+ for (targetVariation in variationIds)
+ {
+ var variationSuffix = (targetVariation != Constants.DEFAULT_VARIATION) ? '-$targetVariation' : '';
+ if (difficulties.exists('$diffId$variationSuffix')) return true;
+ }
+ return false;
}
/**
@@ -445,12 +486,14 @@ class SongDifficulty
{
if (instrumental != '' && characters.altInstrumentals.contains(instrumental))
{
- FlxG.sound.cache(Paths.inst(this.song.id, instrumental));
+ var instId = '-$instrumental';
+ FlxG.sound.cache(Paths.inst(this.song.id, instId));
}
else
{
// Fallback to default instrumental.
- FlxG.sound.cache(Paths.inst(this.song.id, characters.instrumental));
+ var instId = (characters.instrumental ?? '') != '' ? '-${characters.instrumental}' : '';
+ FlxG.sound.cache(Paths.inst(this.song.id, instId));
}
}
else
@@ -492,7 +535,8 @@ class SongDifficulty
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Automatically resolve voices by removing suffixes.
- // For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`.
+ // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
+ // Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
var playerId:String = characters.player;
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
@@ -504,6 +548,19 @@ class SongDifficulty
// Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
+ if (voicePlayer == null)
+ {
+ // Try again without $suffix.
+ playerId = characters.player;
+ voicePlayer = Paths.voices(this.song.id, '-${playerId}');
+ while (voicePlayer != null && !Assets.exists(voicePlayer))
+ {
+ // Remove the last suffix.
+ playerId = playerId.split('-').slice(0, -1).join('-');
+ // Try again.
+ voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
+ }
+ }
var opponentId:String = characters.opponent;
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
@@ -514,6 +571,19 @@ class SongDifficulty
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
+ if (voiceOpponent == null)
+ {
+ // Try again without $suffix.
+ opponentId = characters.opponent;
+ voiceOpponent = Paths.voices(this.song.id, '-${opponentId}');
+ while (voiceOpponent != null && !Assets.exists(voiceOpponent))
+ {
+ // Remove the last suffix.
+ opponentId = opponentId.split('-').slice(0, -1).join('-');
+ // Try again.
+ voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
+ }
+ }
var result:Array = [];
if (voicePlayer != null) result.push(voicePlayer);
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index ac6c3705e..af5765b25 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -1,10 +1,17 @@
package funkin.play.stage;
+import openfl.display.BlendMode;
+import funkin.graphics.framebuffer.FrameBufferManager;
+import flixel.util.FlxColor;
+import funkin.graphics.framebuffer.SpriteCopy;
+import funkin.graphics.FunkinCamera;
+import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxSort;
+import openfl.display.BitmapData;
import flixel.util.FlxColor;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
@@ -46,6 +53,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
return _data?.cameraZoom ?? 1.0;
}
+ var frameBufferMan:FrameBufferManager;
+
+ /**
+ * The texture that has the mask information. Used for shader effects.
+ */
+ public var maskTexture:BitmapData;
+
var namedProps:Map = new Map();
var characters:Map = new Map();
var boppers:Array = new Array();
@@ -75,6 +89,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
*/
public function onCreate(event:ScriptEvent):Void
{
+ if (frameBufferMan != null) frameBufferMan.dispose();
+ frameBufferMan = new FrameBufferManager(FlxG.camera);
+ setupFrameBuffers();
+
buildStage();
this.refresh();
@@ -701,6 +719,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
debugIconGroup = null;
}
+
+ if (frameBufferMan != null)
+ {
+ frameBufferMan.dispose();
+ }
}
/**
@@ -724,13 +747,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
}
}
- public function onUpdate(event:UpdateScriptEvent)
- {
- if (FlxG.keys.justPressed.F3)
- {
- debugIconGroup.visible = !debugIconGroup.visible;
- }
- }
+ public function onUpdate(event:UpdateScriptEvent) {}
public override function kill()
{
@@ -753,6 +770,53 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
return Sprite;
}
+ override function draw():Void
+ {
+ if (frameBufferMan != null)
+ {
+ frameBufferMan.lock();
+ }
+ super.draw();
+ if (frameBufferMan != null)
+ {
+ frameBufferMan.unlock();
+ }
+ frameBuffersUpdated();
+ }
+
+ /**
+ * Called when the frame buffer manager is ready.
+ * Create frame buffers inside this method.
+ */
+ function setupFrameBuffers():Void {}
+
+ /**
+ * Called when all the frame buffers are updated. If you need any
+ * frame buffers before `grabScreen()`, make sure you
+ * grab the screen inside this method since it immediately uses the
+ * frame buffers.
+ */
+ function frameBuffersUpdated():Void {}
+
+ /**
+ * Grabs the current screen and returns it as a bitmap data. You can sefely modify it.
+ * @param applyFilters if this is `true`, the filters set to the camera will be applied to the resulting bitmap
+ * @return the grabbed screen
+ */
+ function grabScreen(applyFilters:Bool):BitmapData
+ {
+ if (Std.isOfType(FlxG.camera, FunkinCamera))
+ {
+ final cam:FunkinCamera = cast FlxG.camera;
+ return cam.grabScreen(applyFilters);
+ }
+ else
+ {
+ FlxG.log.error('cannot grab the screen: the main camera is not grabbable');
+ return null;
+ }
+ }
+
static function _fetchData(id:String):Null
{
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx
index 4013258af..3d91e3a9a 100644
--- a/source/funkin/ui/MusicBeatState.hx
+++ b/source/funkin/ui/MusicBeatState.hx
@@ -75,7 +75,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
function handleFunctionControls():Void
{
// Emergency exit button.
- if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx
index f93fbce0a..4671a9063 100644
--- a/source/funkin/ui/MusicBeatSubState.hx
+++ b/source/funkin/ui/MusicBeatSubState.hx
@@ -16,7 +16,7 @@ import funkin.input.Controls;
/**
* MusicBeatSubState reincorporates the functionality of MusicBeatState into an FlxSubState.
*/
-class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandler
+class MusicBeatSubState extends FlxSubState implements IEventHandler
{
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
@@ -60,7 +60,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
// Emergency exit button.
- if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
diff --git a/source/funkin/ui/SwagCamera.hx b/source/funkin/ui/SwagCamera.hx
deleted file mode 100644
index 70791d38f..000000000
--- a/source/funkin/ui/SwagCamera.hx
+++ /dev/null
@@ -1,103 +0,0 @@
-package funkin.ui;
-
-import flixel.FlxCamera;
-import flixel.FlxSprite;
-import flixel.math.FlxPoint;
-import funkin.util.MathUtil;
-
-class SwagCamera extends FlxCamera
-{
- /**
- * properly follow framerate
- * most of this just copied from FlxCamera,
- * only lines 96 and 97 are changed
- */
- override public function updateFollow():Void
- {
- // Either follow the object closely,
- // or double check our deadzone and update accordingly.
- if (deadzone == null)
- {
- target.getMidpoint(_point);
- _point.addPoint(targetOffset);
- focusOn(_point);
- }
- else
- {
- var edge:Float;
- var targetX:Float = target.x + targetOffset.x;
- var targetY:Float = target.y + targetOffset.y;
-
- if (style == SCREEN_BY_SCREEN)
- {
- if (targetX >= (scroll.x + width))
- {
- _scrollTarget.x += width;
- }
- else if (targetX < scroll.x)
- {
- _scrollTarget.x -= width;
- }
-
- if (targetY >= (scroll.y + height))
- {
- _scrollTarget.y += height;
- }
- else if (targetY < scroll.y)
- {
- _scrollTarget.y -= height;
- }
- }
- else
- {
- edge = targetX - deadzone.x;
- if (_scrollTarget.x > edge)
- {
- _scrollTarget.x = edge;
- }
- edge = targetX + target.width - deadzone.x - deadzone.width;
- if (_scrollTarget.x < edge)
- {
- _scrollTarget.x = edge;
- }
-
- edge = targetY - deadzone.y;
- if (_scrollTarget.y > edge)
- {
- _scrollTarget.y = edge;
- }
- edge = targetY + target.height - deadzone.y - deadzone.height;
- if (_scrollTarget.y < edge)
- {
- _scrollTarget.y = edge;
- }
- }
-
- if ((target is FlxSprite))
- {
- if (_lastTargetPosition == null)
- {
- _lastTargetPosition = FlxPoint.get(target.x, target.y); // Creates this point.
- }
- _scrollTarget.x += (target.x - _lastTargetPosition.x) * followLead.x;
- _scrollTarget.y += (target.y - _lastTargetPosition.y) * followLead.y;
-
- _lastTargetPosition.x = target.x;
- _lastTargetPosition.y = target.y;
- }
-
- if (followLerp >= 60 / FlxG.updateFramerate)
- {
- scroll.copyFrom(_scrollTarget); // no easing
- }
- else
- {
- // THIS THE PART THAT ACTUALLY MATTERS LOL
- scroll.x = MathUtil.coolLerp(scroll.x, _scrollTarget.x, followLerp);
- scroll.y = MathUtil.coolLerp(scroll.y, _scrollTarget.y, followLerp);
- // scroll.x += (_scrollTarget.x - scroll.x) * MathUtil.cameraLerp(followLerp);
- // scroll.y += (_scrollTarget.y - scroll.y) * MathUtil.cameraLerp(followLerp);
- }
- }
- }
-}
diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index 7e03b2d7a..718130356 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -90,7 +90,9 @@ class DebugMenuSubState extends MusicBeatSubState
function openChartEditor()
{
- FlxG.switchState(new ChartEditorState());
+ FlxTransitionableState.skipNextTransIn = true;
+
+ FlxG.switchState(() -> new ChartEditorState());
}
function openInputOffsetTesting()
@@ -101,7 +103,7 @@ class DebugMenuSubState extends MusicBeatSubState
function openAnimationEditor()
{
- FlxG.switchState(new funkin.ui.debug.anim.DebugBoundingState());
+ FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
trace('Animation Editor');
}
diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx
index 4e06913b4..5561a9dcd 100644
--- a/source/funkin/ui/debug/anim/DebugBoundingState.hx
+++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx
@@ -364,7 +364,7 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;
- if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
MouseUtil.mouseCamDrag();
MouseUtil.mouseWheelZoom();
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index dab79a21c..f082828f7 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -8,6 +8,7 @@ import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.graphics.FlxGraphic;
import flixel.group.FlxGroup.FlxTypedGroup;
+import funkin.graphics.FunkinCamera;
import flixel.group.FlxSpriteGroup;
import flixel.input.keyboard.FlxKey;
import flixel.input.mouse.FlxMouseEvent;
@@ -2094,7 +2095,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
loadPreferences();
- uiCamera = new FlxCamera();
+ uiCamera = new FunkinCamera();
FlxG.cameras.reset(uiCamera);
buildDefaultSongData();
@@ -3183,7 +3184,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handleTestKeybinds();
handleHelpKeybinds();
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
handleQuickWatch();
#end
@@ -5097,7 +5098,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
stopWelcomeMusic();
// TODO: PR Flixel to make onComplete nullable.
if (audioInstTrack != null) audioInstTrack.onComplete = null;
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
resetWindowTitle();
@@ -5363,8 +5364,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
targetSong: targetSong,
targetDifficulty: selectedDifficulty,
- // TODO: Add this.
- // targetCharacter: targetCharacter,
+ targetVariation: selectedVariation,
practiceMode: playtestPracticeMode,
minimalMode: minimal,
startTimestamp: startTimestamp,
@@ -5381,7 +5381,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Kill and replace the UI camera so it doesn't get destroyed during the state transition.
uiCamera.kill();
FlxG.cameras.remove(uiCamera, false);
- FlxG.cameras.reset(new FlxCamera());
+ FlxG.cameras.reset(new FunkinCamera());
this.persistentUpdate = false;
this.persistentDraw = false;
diff --git a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
index 6c5152a29..376d0cebd 100644
--- a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
@@ -138,7 +138,7 @@ class CopyItemsCommand implements ChartEditorCommand
state.txtCopyNotif.y = FlxG.mouse.y - 16;
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
{
- type: FlxTween.ONESHOT,
+ type: FlxTweenType.ONESHOT,
ease: FlxEase.quadOut,
onComplete: function(_) {
state.txtCopyNotif.visible = false;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 76b2a388e..2d4841e9c 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -1,7 +1,7 @@
package funkin.ui.debug.charting.handlers;
import flixel.system.FlxAssets.FlxSoundAsset;
-import flixel.system.FlxSound;
+import flixel.sound.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound;
import funkin.play.character.BaseCharacter.CharacterType;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 0318bf296..557875596 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -72,28 +72,28 @@ class ChartEditorImportExportHandler
{
state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation);
}
- }
- for (difficultyId in song.listDifficulties())
- {
- var diff:Null = song.getDifficulty(difficultyId);
- if (diff == null) continue;
+ for (difficultyId in song.listDifficulties(variation))
+ {
+ var diff:Null = song.getDifficulty(difficultyId, variation);
+ if (diff == null) continue;
- var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
- var voiceList:Array = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
+ var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
+ var voiceList:Array = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
- if (voiceList.length == 2)
- {
- state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
- state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId);
- }
- else if (voiceList.length == 1)
- {
- state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
- }
- else
- {
- trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
+ if (voiceList.length == 2)
+ {
+ state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
+ state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId);
+ }
+ else if (voiceList.length == 1)
+ {
+ state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
+ }
+ else
+ {
+ trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
+ }
}
}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
index 8d3554a08..c384e7a6d 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
@@ -290,7 +290,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
// waveformMusic.waveform.forceUpdate = true;
var perfStart = haxe.Timer.stamp();
var waveformData1 = playerVoice.waveformData;
- var waveformData2 = opponentVoice.waveformData;
+ var waveformData2 = opponentVoice?.waveformData ?? playerVoice.waveformData; // this null check is for songs that only have 1 vocals file!
var waveformData3 = chartEditorState.audioInstTrack.waveformData;
var waveformData = waveformData1.merge(waveformData2).merge(waveformData3);
trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
index 67ca82b1b..fd9209294 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
@@ -276,8 +276,13 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
// Build opponent waveform.
// waveformOpponent.waveform.forceUpdate = true;
- waveformOpponent.waveform.waveformData = opponentVoice.waveformData;
- waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC;
+ // note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
+ // so we null check
+ if (opponentVoice != null)
+ {
+ waveformOpponent.waveform.waveformData = opponentVoice.waveformData;
+ waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC;
+ }
// Build instrumental waveform.
// waveformInstrumental.waveform.forceUpdate = true;
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 3c6b52c6f..e7c615313 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -14,6 +14,7 @@ import flixel.group.FlxSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
+import funkin.graphics.FunkinCamera;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
@@ -54,8 +55,26 @@ import funkin.util.MathUtil;
import lime.app.Future;
import lime.utils.Assets;
+/**
+ * Parameters used to initialize the FreeplayState.
+ */
+typedef FreeplayStateParams =
+{
+ ?character:String,
+};
+
class FreeplayState extends MusicBeatSubState
{
+ //
+ // Params
+ //
+
+ /**
+ * The current character for this FreeplayState.
+ * You can't change this without transitioning to a new FreeplayState.
+ */
+ final currentCharacter:String;
+
/**
* For the audio preview, the duration of the fade-in effect.
*/
@@ -116,6 +135,8 @@ class FreeplayState extends MusicBeatSubState
var ostName:FlxText;
var difficultyStars:DifficultyStars;
+ var displayedVariations:Array;
+
var dj:DJBoyfriend;
var letterSort:LetterSort;
@@ -124,12 +145,13 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState;
- //
static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY;
static var rememberedSongId:Null = null;
- public function new(?stickers:StickerSubState = null)
+ public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
+ currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
+
if (stickers != null)
{
stickerSubState = stickers;
@@ -172,6 +194,10 @@ class FreeplayState extends MusicBeatSubState
// Add a null entry that represents the RANDOM option
songs.push(null);
+ // TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later.
+ // Default character (BF) shows default and Erect variations. Pico shows only Pico variations.
+ displayedVariations = (currentCharacter == "bf") ? [Constants.DEFAULT_VARIATION, "erect"] : [currentCharacter];
+
// programmatically adds the songs via LevelRegistry and SongRegistry
for (levelId in LevelRegistry.instance.listBaseGameLevelIds())
{
@@ -179,9 +205,12 @@ class FreeplayState extends MusicBeatSubState
{
var song:Song = SongRegistry.instance.fetchEntry(songId);
- songs.push(new FreeplaySongData(levelId, songId, song));
+ // Only display songs which actually have available charts for the current character.
+ var availableDifficultiesForSong = song.listDifficulties(displayedVariations);
+ if (availableDifficultiesForSong.length == 0) continue;
- for (difficulty in song.listDifficulties())
+ songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
+ for (difficulty in availableDifficultiesForSong)
{
diffIdsTotal.pushUnique(difficulty);
}
@@ -300,6 +329,8 @@ class FreeplayState extends MusicBeatSubState
x: -dj.width * 1.6,
speed: 0.5
});
+ // TODO: Replace this.
+ if (currentCharacter == "pico") dj.visible = false;
add(dj);
var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
@@ -543,7 +574,7 @@ class FreeplayState extends MusicBeatSubState
var swag:Alphabet = new Alphabet(1, 0, "swag");
- var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
+ var funnyCam = new FunkinCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
@@ -869,6 +900,16 @@ class FreeplayState extends MusicBeatSubState
changeDiff(1);
}
+ // TODO: DEBUG REMOVE THIS
+ if (FlxG.keys.justPressed.P)
+ {
+ var newParams:FreeplayStateParams =
+ {
+ character: currentCharacter == "bf" ? "pico" : "bf",
+ };
+ openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(newParams, sticker)));
+ }
+
if (controls.BACK && !typing.hasFocus)
{
FlxTween.globalManager.clear();
@@ -920,7 +961,7 @@ class FreeplayState extends MusicBeatSubState
}
else
{
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
});
}
@@ -1086,13 +1127,7 @@ class FreeplayState extends MusicBeatSubState
return;
}
var targetDifficulty:String = currentDifficulty;
-
- // TODO: Implement Pico into the interface properly.
- var targetCharacter:String = 'bf';
- if (FlxG.keys.pressed.P)
- {
- targetCharacter = 'pico';
- }
+ var targetVariation:String = targetSong.getFirstValidVariation(targetDifficulty);
PlayStatePlaylist.campaignId = cap.songData.levelId;
@@ -1106,11 +1141,11 @@ class FreeplayState extends MusicBeatSubState
new FlxTimer().start(1, function(tmr:FlxTimer) {
Paths.setCurrentLevel(cap.songData.levelId);
- LoadingState.loadAndSwitchState(new PlayState(
+ LoadingState.loadAndSwitchState(() -> new PlayState(
{
targetSong: targetSong,
targetDifficulty: targetDifficulty,
- targetCharacter: targetCharacter,
+ targetVariation: targetVariation,
}), true);
});
}
@@ -1275,31 +1310,33 @@ class FreeplaySongData
public var songRating(default, null):Int = 0;
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
+ public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION];
function set_currentDifficulty(value:String):String
{
if (currentDifficulty == value) return value;
currentDifficulty = value;
- updateValues();
+ updateValues(displayedVariations);
return value;
}
- public function new(levelId:String, songId:String, song:Song)
+ public function new(levelId:String, songId:String, song:Song, ?displayedVariations:Array)
{
this.levelId = levelId;
this.songId = songId;
this.song = song;
+ if (displayedVariations != null) this.displayedVariations = displayedVariations;
- updateValues();
+ updateValues(displayedVariations);
}
- function updateValues():Void
+ function updateValues(displayedVariations:Array):Void
{
- this.songDifficulties = song.listDifficulties();
+ this.songDifficulties = song.listDifficulties(displayedVariations);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
- var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty);
+ var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, displayedVariations);
if (songDifficulty == null) return;
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
diff --git a/source/funkin/ui/haxeui/HaxeUISubState.hx b/source/funkin/ui/haxeui/HaxeUISubState.hx
index 82c15be4c..ac53f4b51 100644
--- a/source/funkin/ui/haxeui/HaxeUISubState.hx
+++ b/source/funkin/ui/haxeui/HaxeUISubState.hx
@@ -45,7 +45,7 @@ class HaxeUISubState extends MusicBeatSubState
super.update(elapsed);
// Force quit.
- if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// Refresh the component.
if (FlxG.keys.justPressed.F5)
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index f41eac07d..8842c37de 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -8,10 +8,12 @@ import flixel.FlxState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.effects.FlxFlicker;
import flixel.graphics.frames.FlxAtlasFrames;
+import flixel.util.typeLimit.NextState;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.input.touch.FlxTouch;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
+import funkin.graphics.FunkinCamera;
import flixel.tweens.FlxTween;
import funkin.ui.MusicBeatState;
import flixel.util.FlxTimer;
@@ -94,7 +96,7 @@ class MainMenuState extends MusicBeatState
});
menuItems.enabled = true; // can move on intro
- createMenuItem('storymode', 'mainmenu/storymode', function() startExitState(new StoryMenuState()));
+ createMenuItem('storymode', 'mainmenu/storymode', function() startExitState(() -> new StoryMenuState()));
createMenuItem('freeplay', 'mainmenu/freeplay', function() {
persistentDraw = true;
persistentUpdate = false;
@@ -110,7 +112,7 @@ class MainMenuState extends MusicBeatState
#end
createMenuItem('options', 'mainmenu/options', function() {
- startExitState(new funkin.ui.options.OptionsState());
+ startExitState(() -> new funkin.ui.options.OptionsState());
});
// Reset position of menu items.
@@ -151,7 +153,7 @@ class MainMenuState extends MusicBeatState
function resetCamStuff()
{
- FlxG.cameras.reset(new SwagCamera());
+ FlxG.cameras.reset(new FunkinCamera());
FlxG.camera.follow(camFollow, null, 0.06);
}
@@ -255,7 +257,7 @@ class MainMenuState extends MusicBeatState
openSubState(prompt);
}
- function startExitState(state:FlxState)
+ function startExitState(state:NextState)
{
menuItems.enabled = false; // disable for exit
var duration = 0.4;
@@ -313,7 +315,7 @@ class MainMenuState extends MusicBeatState
if (controls.BACK && menuItems.enabled && !menuItems.busy)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
- FlxG.switchState(new TitleState());
+ FlxG.switchState(() -> new TitleState());
}
}
}
diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx
index ea04e1208..bda071846 100644
--- a/source/funkin/ui/options/ControlsMenu.hx
+++ b/source/funkin/ui/options/ControlsMenu.hx
@@ -4,6 +4,7 @@ import funkin.util.InputUtil;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
+import funkin.graphics.FunkinCamera;
import flixel.group.FlxGroup;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepadInputID;
@@ -47,7 +48,7 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page
{
super();
- menuCamera = new FlxCamera();
+ menuCamera = new FunkinCamera();
FlxG.cameras.add(menuCamera, false);
menuCamera.bgColor = 0x0;
camera = menuCamera;
diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx
index 53d972af1..7b233f03d 100644
--- a/source/funkin/ui/options/OptionsState.hx
+++ b/source/funkin/ui/options/OptionsState.hx
@@ -103,7 +103,7 @@ class OptionsState extends MusicBeatState
{
currentPage.enabled = false;
// TODO: Animate this transition?
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
}
diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx
index b4b3c7db8..c23c3f165 100644
--- a/source/funkin/ui/options/PreferencesMenu.hx
+++ b/source/funkin/ui/options/PreferencesMenu.hx
@@ -6,6 +6,7 @@ import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.options.OptionsState.Page;
+import funkin.graphics.FunkinCamera;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
@@ -20,7 +21,7 @@ class PreferencesMenu extends Page
{
super();
- menuCamera = new SwagCamera();
+ menuCamera = new FunkinCamera();
FlxG.cameras.add(menuCamera, false);
menuCamera.bgColor = 0x0;
camera = menuCamera;
diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx
index 1b9252fde..ea6940c4a 100644
--- a/source/funkin/ui/story/Level.hx
+++ b/source/funkin/ui/story/Level.hx
@@ -150,7 +150,8 @@ class Level implements IRegistryEntry
if (firstSong != null)
{
- for (difficulty in firstSong.listDifficulties())
+ // Don't display alternate characters in Story Mode.
+ for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"]))
{
difficulties.push(difficulty);
}
@@ -168,7 +169,7 @@ class Level implements IRegistryEntry
for (difficulty in difficulties)
{
- if (!song.hasDifficulty(difficulty))
+ if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, "erect"]))
{
difficulties.remove(difficulty);
}
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 112817f42..2f0111841 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -397,7 +397,7 @@ class StoryMenuState extends MusicBeatState
{
FlxG.sound.play(Paths.sound('cancelMenu'));
exitingMenu = true;
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
}
@@ -565,7 +565,7 @@ class StoryMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
- LoadingState.loadAndSwitchState(new PlayState(
+ LoadingState.loadAndSwitchState(() -> new PlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
diff --git a/source/funkin/ui/title/AttractState.hx b/source/funkin/ui/title/AttractState.hx
index 38cff7cc8..ade24bffa 100644
--- a/source/funkin/ui/title/AttractState.hx
+++ b/source/funkin/ui/title/AttractState.hx
@@ -105,6 +105,6 @@ class AttractState extends MusicBeatState
vid.destroy();
vid = null;
- FlxG.switchState(new TitleState());
+ FlxG.switchState(() -> new TitleState());
}
}
diff --git a/source/funkin/ui/title/OutdatedSubState.hx b/source/funkin/ui/title/OutdatedSubState.hx
index 012823541..bc938f24a 100644
--- a/source/funkin/ui/title/OutdatedSubState.hx
+++ b/source/funkin/ui/title/OutdatedSubState.hx
@@ -39,7 +39,7 @@ class OutdatedSubState extends MusicBeatState
if (controls.BACK)
{
leftState = true;
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
super.update(elapsed);
}
diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index a5dcd6def..5424e2255 100644
--- a/source/funkin/ui/title/TitleState.hx
+++ b/source/funkin/ui/title/TitleState.hx
@@ -9,6 +9,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxDirectionFlags;
import flixel.util.FlxTimer;
+import flixel.util.typeLimit.NextState;
import funkin.audio.visualize.SpectogramSprite;
import funkin.graphics.shaders.ColorSwap;
import funkin.graphics.shaders.LeftMaskShader;
@@ -213,7 +214,7 @@ class TitleState extends MusicBeatState
*/
function moveToAttract():Void
{
- FlxG.switchState(new AttractState());
+ FlxG.switchState(() -> new AttractState());
}
function playMenuMusic():Void
@@ -294,7 +295,7 @@ class TitleState extends MusicBeatState
{
if (touch.justPressed)
{
- FlxG.switchState(new FreeplayState());
+ FlxG.switchState(() -> new FreeplayState());
pressedEnter = true;
}
}
@@ -313,7 +314,7 @@ class TitleState extends MusicBeatState
// If you spam Enter, we should skip the transition.
if (pressedEnter && transitioning && skippedIntro)
{
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(() -> new MainMenuState());
}
if (pressedEnter && !transitioning && skippedIntro)
@@ -328,7 +329,7 @@ class TitleState extends MusicBeatState
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
transitioning = true;
- var targetState:FlxState = new MainMenuState();
+ var targetState:NextState = () -> new MainMenuState();
new FlxTimer().start(2, function(tmr:FlxTimer) {
// These assets are very unlikely to be used for the rest of gameplay, so it unloads them from cache/memory
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index da9aeb28b..63dcb8f68 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -1,7 +1,6 @@
package funkin.ui.transition;
import flixel.FlxSprite;
-import flixel.FlxState;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
@@ -21,12 +20,13 @@ import lime.utils.AssetManifest;
import lime.utils.Assets as LimeAssets;
import openfl.filters.ShaderFilter;
import openfl.utils.Assets;
+import flixel.util.typeLimit.NextState;
class LoadingState extends MusicBeatState
{
inline static var MIN_TIME = 1.0;
- var target:FlxState;
+ var target:NextState;
var stopMusic = false;
var callbacks:MultiCallback;
var danceLeft = false;
@@ -34,7 +34,7 @@ class LoadingState extends MusicBeatState
var loadBar:FlxSprite;
var funkay:FlxSprite;
- function new(target:FlxState, stopMusic:Bool)
+ function new(target:NextState, stopMusic:Bool)
{
super();
this.target = target;
@@ -172,12 +172,12 @@ class LoadingState extends MusicBeatState
return Paths.inst(PlayState.instance.currentSong.id);
}
- inline static public function loadAndSwitchState(nextState:FlxState, shouldStopMusic = false):Void
+ inline static public function loadAndSwitchState(nextState:NextState, shouldStopMusic = false):Void
{
FlxG.switchState(getNextState(nextState, shouldStopMusic));
}
- static function getNextState(nextState:FlxState, shouldStopMusic = false):FlxState
+ static function getNextState(nextState:NextState, shouldStopMusic = false):NextState
{
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
@@ -186,7 +186,7 @@ class LoadingState extends MusicBeatState
// && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath()))
// && isLibraryLoaded('shared');
//
- if (true) return new LoadingState(nextState, shouldStopMusic);
+ if (true) return () -> new LoadingState(nextState, shouldStopMusic);
#end
if (shouldStopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
@@ -332,7 +332,7 @@ class MultiCallback
public function getUnfired():ArrayVoid>
return unfired.array();
- public static function coolSwitchState(state:FlxState, transitionTex:String = "shaderTransitionStuff/coolDots", time:Float = 2)
+ public static function coolSwitchState(state:NextState, transitionTex:String = "shaderTransitionStuff/coolDots", time:Float = 2)
{
var screenShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("shaderTransitionStuff/coolDots"));
var screenWipeShit:ScreenWipeShader = new ScreenWipeShader();
@@ -343,9 +343,9 @@ class MultiCallback
ease: FlxEase.quadInOut,
onComplete: function(twn) {
screenShit.destroy();
- FlxG.switchState(new MainMenuState());
+ FlxG.switchState(state);
}
});
- FlxG.camera.setFilters([new ShaderFilter(screenWipeShit)]);
+ FlxG.camera.filters = [new ShaderFilter(screenWipeShit)];
}
}
diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/StickerSubState.hx
index 1c012e00c..e94eed7d5 100644
--- a/source/funkin/ui/transition/StickerSubState.hx
+++ b/source/funkin/ui/transition/StickerSubState.hx
@@ -19,6 +19,7 @@ import funkin.ui.freeplay.FreeplayState;
import openfl.geom.Matrix;
import openfl.display.Sprite;
import openfl.display.Bitmap;
+import flixel.FlxState;
using Lambda;
using StringTools;
@@ -30,7 +31,12 @@ class StickerSubState extends MusicBeatSubState
// yes... a damn OpenFL sprite!!!
public var dipshit:Sprite;
- var nextState:NEXTSTATE = FREEPLAY;
+ /**
+ * The state to switch to after the stickers are done.
+ * This is a FUNCTION so we can pass it directly to `FlxG.switchState()`,
+ * and we can add constructor parameters in the caller.
+ */
+ var targetState:StickerSubState->FlxState;
// what "folders" to potentially load from (as of writing only "keys" exist)
var soundSelections:Array = [];
@@ -38,10 +44,12 @@ class StickerSubState extends MusicBeatSubState
var soundSelection:String = "";
var sounds:Array = [];
- public function new(?oldStickers:Array, ?nextState:NEXTSTATE = FREEPLAY):Void
+ public function new(?oldStickers:Array, ?targetState:StickerSubState->FlxState):Void
{
super();
+ this.targetState = (targetState == null) ? ((sticker) -> new MainMenuState()) : targetState;
+
// todo still
// make sure that ONLY plays mp3/ogg files
// if there's no mp3/ogg file, then it regenerates/reloads the random folder
@@ -84,10 +92,6 @@ class StickerSubState extends MusicBeatSubState
trace(sounds);
- // trace(assetsInList);
-
- this.nextState = nextState;
-
grpStickers = new FlxTypedGroup();
add(grpStickers);
@@ -112,10 +116,19 @@ class StickerSubState extends MusicBeatSubState
{
grpStickers.cameras = FlxG.cameras.list;
- if (dipshit != null)
+ /*
+ if (dipshit != null)
+ {
+ FlxG.removeChild(dipshit);
+ dipshit = null;
+ }
+ */
+
+ if (grpStickers.members == null || grpStickers.members.length == 0)
{
- FlxG.removeChild(dipshit);
- dipshit = null;
+ switchingState = false;
+ close();
+ return;
}
for (ind => sticker in grpStickers.members)
@@ -228,33 +241,26 @@ class StickerSubState extends MusicBeatSubState
if (ind == grpStickers.members.length - 1)
{
switchingState = true;
+
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
- dipshit = new Sprite();
- var scrn:BitmapData = new BitmapData(FlxG.width, FlxG.height, true, 0x00000000);
- var mat:Matrix = new Matrix();
- scrn.draw(grpStickers.cameras[0].canvas, mat);
+ // I think this grabs the screen and puts it under the stickers?
+ // Leaving this commented out rather than stripping it out because it's cool...
+ /*
+ dipshit = new Sprite();
+ var scrn:BitmapData = new BitmapData(FlxG.width, FlxG.height, true, 0x00000000);
+ var mat:Matrix = new Matrix();
+ scrn.draw(grpStickers.cameras[0].canvas, mat);
- var bitmap:Bitmap = new Bitmap(scrn);
+ var bitmap:Bitmap = new Bitmap(scrn);
- dipshit.addChild(bitmap);
- FlxG.addChildBelowMouse(dipshit);
+ dipshit.addChild(bitmap);
+ // FlxG.addChildBelowMouse(dipshit);
+ */
- switch (nextState)
- {
- case FREEPLAY:
- FlxG.switchState(new FreeplayState(this));
- case STORY:
- FlxG.switchState(new StoryMenuState(this));
- case MAIN_MENU:
- FlxG.switchState(new MainMenuState());
- default:
- FlxG.switchState(new MainMenuState());
- }
+ FlxG.switchState(() -> targetState(this));
}
-
- // sticky.angle *= FlxG.random.float(0, 0.05);
});
});
}
@@ -368,10 +374,3 @@ typedef StickerShit =
stickers:Map>,
stickerPacks:Map>
}
-
-enum abstract NEXTSTATE(String)
-{
- var MAIN_MENU = 'mainmenu';
- var FREEPLAY = 'freeplay';
- var STORY = 'story';
-}
diff --git a/source/funkin/util/plugins/EvacuateDebugPlugin.hx b/source/funkin/util/plugins/EvacuateDebugPlugin.hx
index 1803c25ba..eb292b852 100644
--- a/source/funkin/util/plugins/EvacuateDebugPlugin.hx
+++ b/source/funkin/util/plugins/EvacuateDebugPlugin.hx
@@ -24,7 +24,7 @@ class EvacuateDebugPlugin extends FlxBasic
if (FlxG.keys.justPressed.F4)
{
- FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
+ FlxG.switchState(() -> new funkin.ui.mainmenu.MainMenuState());
}
}
diff --git a/source/funkin/util/plugins/WatchPlugin.hx b/source/funkin/util/plugins/WatchPlugin.hx
index 17b2dd129..defd9797d 100644
--- a/source/funkin/util/plugins/WatchPlugin.hx
+++ b/source/funkin/util/plugins/WatchPlugin.hx
@@ -22,6 +22,17 @@ class WatchPlugin extends FlxBasic
{
super.update(elapsed);
+ var stateClassName = Type.getClassName(Type.getClass(FlxG.state));
+ FlxG.watch.addQuick("currentState", stateClassName);
+ var subStateClassNames = [];
+ var subState = FlxG.state.subState;
+ while (subState != null)
+ {
+ subStateClassNames.push(Type.getClassName(Type.getClass(subState)));
+ subState = subState.subState;
+ }
+ FlxG.watch.addQuick("currentSubStates", subStateClassNames.join(", "));
+
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
FlxG.watch.addQuick("songPositionNoOffset", Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
FlxG.watch.addQuick("musicTime", FlxG.sound?.music?.time ?? 0.0);
diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx
index 0209cfc19..caf8e8aab 100644
--- a/source/funkin/util/tools/ArrayTools.hx
+++ b/source/funkin/util/tools/ArrayTools.hx
@@ -23,6 +23,24 @@ class ArrayTools
return result;
}
+ /**
+ * Returns a copy of the array with all `null` elements removed.
+ * @param array The array to remove `null` elements from.
+ * @return A copy of the array with all `null` elements removed.
+ */
+ public static function nonNull(array:Array>):Array
+ {
+ var result:Array = [];
+ for (element in array)
+ {
+ if (element != null)
+ {
+ result.push(element);
+ }
+ }
+ return result;
+ }
+
/**
* Return the first element of the array that satisfies the predicate, or null if none do.
* @param input The array to search
diff --git a/source/funkin/util/tools/Int64Tools.hx b/source/funkin/util/tools/Int64Tools.hx
index d53fa315d..b6ac84ade 100644
--- a/source/funkin/util/tools/Int64Tools.hx
+++ b/source/funkin/util/tools/Int64Tools.hx
@@ -18,9 +18,9 @@ class Int64Tools
public static function toFloat(i:Int64):Float
{
- var f:Float = Int64.getLow(i);
+ var f:Float = i.low;
if (f < 0) f += MAX_32_PRECISION;
- return (Int64.getHigh(i) * MAX_32_PRECISION + f);
+ return (i.high * MAX_32_PRECISION + f);
}
public static function isToIntSafe(i:Int64):Bool