diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..c92ea0bae
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,8 @@
+# Ignore artifacts
+export
+
+# Ignore all asset files (including FlxAnimate JSONs)
+assets
+
+# Don't ignore data files
+!assets/preload/data
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2468e883a..ec86904ea 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -93,7 +93,7 @@
{
"label": "Windows / Debug",
"target": "windows",
- "args": ["-debug"]
+ "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
@@ -103,7 +103,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
- "args": ["-debug", "-DANIMATE"]
+ "args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@@ -113,7 +113,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
- "args": ["-debug", "-DFREEPLAY"]
+ "args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@@ -123,7 +123,11 @@
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
- "args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
+ "args": [
+ "-debug",
+ "-DSONG=bopeebo -DDIFFICULTY=normal",
+ "-DFORCE_DEBUG_VERSION"
+ ]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@@ -133,7 +137,7 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
- "args": ["-debug", "-DDIALOGUE"]
+ "args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Conversation Test)",
@@ -143,7 +147,7 @@
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
- "args": ["-debug", "-DCHARTING"]
+ "args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@@ -153,7 +157,7 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
- "args": ["-debug", "-DANIMDEBUG"]
+ "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@@ -163,7 +167,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
- "args": ["-debug", "-DLATENCY"]
+ "args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Latency Test)",
@@ -173,7 +177,7 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
- "args": ["-debug", "-DWAVEFORM"]
+ "args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Waveform Test)",
@@ -183,12 +187,12 @@
{
"label": "HTML5 / Debug",
"target": "html5",
- "args": ["-debug"]
+ "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
- "args": ["-debug", "-watch"]
+ "args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
}
],
"cmake.configureOnOpen": false,
diff --git a/Project.xml b/Project.xml
index c58153575..c368dacef 100644
--- a/Project.xml
+++ b/Project.xml
@@ -108,7 +108,7 @@
-
+
diff --git a/assets b/assets
index cb0fbb56b..f8c259584 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit cb0fbb56b9667f68a9776a216c16a4e2b29f7096
+Subproject commit f8c2595844eff9375b522f117bfdadbdc6728c49
diff --git a/hmm.json b/hmm.json
index 26cb0d0b4..700b42dfe 100644
--- a/hmm.json
+++ b/hmm.json
@@ -54,14 +54,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
- "ref": "8a7846b",
+ "ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
- "ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
+ "ref": "63a906a6148958dbfde8c7b48d90b0693767fd95",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 0a59fb70b..33674439d 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -146,12 +146,11 @@ class InitState extends FlxState
#end
// Make errors and warnings less annoying.
- #if FORCE_DEBUG_VERSION
+ // Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
- #end
//
// FLIXEL TRANSITIONS
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index e0212e573..6006939be 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -16,6 +16,20 @@ class Paths
currentLevel = name.toLowerCase();
}
+ public static function stripLibrary(path:String):String
+ {
+ var parts = path.split(':');
+ if (parts.length < 2) return path;
+ return parts[1];
+ }
+
+ public static function getLibrary(path:String):String
+ {
+ var parts = path.split(':');
+ if (parts.length < 2) return "preload";
+ return parts[0];
+ }
+
static function getPath(file:String, type:AssetType, library:Null)
{
if (library != null) return getLibraryPath(file, library);
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index e7ce68d08..ba157ed8e 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -23,7 +23,7 @@ import openfl.utils.AssetType;
@:nullSafety
class FunkinSound extends FlxSound implements ICloneable
{
- static final MAX_VOLUME:Float = 2.0;
+ static final MAX_VOLUME:Float = 1.0;
static var cache(default, null):FlxTypedGroup = new FlxTypedGroup();
@@ -40,7 +40,6 @@ class FunkinSound extends FlxSound implements ICloneable
override function set_volume(value:Float):Float
{
// Uncap the volume.
- fixMaxVolume();
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
updateTransform();
return _volume;
@@ -126,17 +125,6 @@ class FunkinSound extends FlxSound implements ICloneable
return this;
}
- function fixMaxVolume():Void
- {
- #if lime_openal
- // This code is pretty fragile, it reaches through 5 layers of private access.
- @:privateAccess
- var handle = this?._channel?.__source?.__backend?.handle;
- if (handle == null) return;
- lime.media.openal.AL.sourcef(handle, lime.media.openal.AL.MAX_GAIN, MAX_VOLUME);
- #end
- }
-
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
{
if (!exists) return this;
diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx
index b82d141e7..1f649b472 100644
--- a/source/funkin/audio/waveform/WaveformData.hx
+++ b/source/funkin/audio/waveform/WaveformData.hx
@@ -187,6 +187,8 @@ class WaveformData
*/
public function merge(that:WaveformData):WaveformData
{
+ if (that == null) return this.clone();
+
var result = this.clone([]);
for (channelIndex in 0...this.channels)
diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 0ccbe2f18..62a7eb0f7 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -61,7 +61,16 @@ abstract class BaseRegistry & Constructible = null;
+ try
+ {
+ entry = createScriptedEntry(entryCls);
+ }
+ catch (e:Dynamic)
+ {
+ log('Failed to create scripted entry (${entryCls})');
+ continue;
+ }
if (entry != null)
{
@@ -196,6 +205,11 @@ abstract class BaseRegistry & Constructible
{
+ if (version == null)
+ {
+ throw '[${registryId}] Entry ${id} could not be JSON-parsed or does not have a parseable version.';
+ }
+
// If a version rule is not specified, do not check against it.
if (versionRule == null || VersionUtil.validateVersion(version, versionRule))
{
diff --git a/source/funkin/data/event/SongEventRegistry.hx b/source/funkin/data/event/SongEventRegistry.hx
index dc5589813..9b0163557 100644
--- a/source/funkin/data/event/SongEventRegistry.hx
+++ b/source/funkin/data/event/SongEventRegistry.hx
@@ -108,8 +108,8 @@ class SongEventRegistry
public static function handleEvent(data:SongEventData):Void
{
- var eventType:String = data.event;
- var eventHandler:SongEvent = eventCache.get(eventType);
+ var eventKind:String = data.eventKind;
+ var eventHandler:SongEvent = eventCache.get(eventKind);
if (eventHandler != null)
{
@@ -117,7 +117,7 @@ class SongEventRegistry
}
else
{
- trace('WARNING: No event handler for event with id: ${eventType}');
+ trace('WARNING: No event handler for event with kind: ${eventKind}');
}
data.activated = true;
@@ -148,6 +148,29 @@ class SongEventRegistry
});
}
+ /**
+ * The currentTime has jumped far ahead or back.
+ * If we moved back in time, we need to reset all the events in that space.
+ * If we moved forward in time, we need to skip all the events in that space.
+ */
+ public static function handleSkippedEvents(events:Array, currentTime:Float):Void
+ {
+ for (event in events)
+ {
+ // Deactivate future events.
+ if (event.time > currentTime)
+ {
+ event.activated = false;
+ }
+
+ // Skip past events.
+ if (event.time < currentTime)
+ {
+ event.activated = true;
+ }
+ }
+ }
+
/**
* Reset activation of all the provided events.
*/
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index bba5f899f..24febea86 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -110,7 +110,8 @@ class SongMetadata implements ICloneable
*/
public function serialize(pretty:Bool = true):String
{
- var writer = new json2object.JsonWriter();
+ var ignoreNullOptionals = true;
+ var writer = new json2object.JsonWriter(ignoreNullOptionals);
// I believe @:jignored should be iggnored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
@@ -597,7 +598,8 @@ class SongChartData implements ICloneable
*/
public function serialize(pretty:Bool = true):String
{
- var writer = new json2object.JsonWriter();
+ var ignoreNullOptionals = true;
+ var writer = new json2object.JsonWriter(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
}
@@ -648,7 +650,7 @@ class SongEventDataRaw implements ICloneable
* Custom events can be added by scripts with the `ScriptedSongEvent` class.
*/
@:alias("e")
- public var event:String;
+ public var eventKind:String;
/**
* The data for the event.
@@ -668,10 +670,10 @@ class SongEventDataRaw implements ICloneable
@:jignored
public var activated:Bool = false;
- public function new(time:Float, event:String, value:Dynamic = null)
+ public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this.time = time;
- this.event = event;
+ this.eventKind = eventKind;
this.value = value;
}
@@ -687,19 +689,19 @@ class SongEventDataRaw implements ICloneable
public function clone():SongEventDataRaw
{
- return new SongEventDataRaw(this.time, this.event, this.value);
+ return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
-@:forward(time, event, value, activated, getStepTime, clone)
+@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
- public function new(time:Float, event:String, value:Dynamic = null)
+ public function new(time:Float, eventKind:String, value:Dynamic = null)
{
- this = new SongEventDataRaw(time, event, value);
+ this = new SongEventDataRaw(time, eventKind, value);
}
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
@@ -726,12 +728,12 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public inline function getHandler():Null
{
- return SongEventRegistry.getEvent(this.event);
+ return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null
{
- return SongEventRegistry.getEventSchema(this.event);
+ return SongEventRegistry.getEventSchema(this.eventKind);
}
public inline function getDynamic(key:String):Null
@@ -784,7 +786,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
var eventHandler = getHandler();
var eventSchema = getSchema();
- if (eventSchema == null) return 'Unknown Event: ${this.event}';
+ if (eventSchema == null) return 'Unknown Event: ${this.eventKind}';
var result = '${eventHandler.getTitle()}';
@@ -809,19 +811,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public function clone():SongEventData
{
- return new SongEventData(this.time, this.event, this.value);
+ return new SongEventData(this.time, this.eventKind, this.value);
}
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{
- return this.time == other.time && this.event == other.event && this.value == other.value;
+ return this.time == other.time && this.eventKind == other.eventKind && this.value == other.value;
}
@:op(A != B)
public function op_notEquals(other:SongEventData):Bool
{
- return this.time != other.time || this.event != other.event || this.value != other.value;
+ return this.time != other.time || this.eventKind != other.eventKind || this.value != other.value;
}
@:op(A > B)
@@ -853,7 +855,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
*/
public function toString():String
{
- return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})';
+ return 'SongEventData(${this.time}ms, ${this.eventKind}: ${this.value})';
}
}
@@ -1022,6 +1024,12 @@ class SongNoteDataRaw implements ICloneable
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
}
+
+ public function toString():String
+ {
+ return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ + (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
+ }
}
/**
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 7f3b01eb4..c93c5379a 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -47,7 +47,7 @@ class SongDataUtils
public static function offsetSongEventData(events:Array, offset:Float):Array
{
return events.map(function(event:SongEventData):SongEventData {
- return new SongEventData(event.time + offset, event.event, event.value);
+ return new SongEventData(event.time + offset, event.eventKind, event.value);
});
}
diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx
index 487aaac34..f47b4138a 100644
--- a/source/funkin/graphics/FunkinSprite.hx
+++ b/source/funkin/graphics/FunkinSprite.hx
@@ -6,9 +6,23 @@ import flixel.graphics.FlxGraphic;
/**
* An FlxSprite with additional functionality.
+ * - A more efficient method for creating solid color sprites.
+ * - TODO: Better cache handling for textures.
*/
class FunkinSprite extends FlxSprite
{
+ /**
+ * An internal list of all the textures cached with `cacheTexture`.
+ * This excludes any temporary textures like those from `FlxText` or `makeSolidColor`.
+ */
+ static var currentCachedTextures:Map = [];
+
+ /**
+ * An internal list of textures that were cached in the previous state.
+ * We don't know whether we want to keep them cached or not.
+ */
+ static var previousCachedTextures:Map = [];
+
/**
* @param x Starting X position
* @param y Starting Y position
@@ -18,19 +32,184 @@ class FunkinSprite extends FlxSprite
super(x, y);
}
+ /**
+ * Create a new FunkinSprite with a static texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function create(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite = new FunkinSprite(x, y);
+ sprite.loadTexture(key);
+ return sprite;
+ }
+
+ /**
+ * Create a new FunkinSprite with a Sparrow atlas animated texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function createSparrow(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite = new FunkinSprite(x, y);
+ sprite.loadSparrow(key);
+ return sprite;
+ }
+
+ /**
+ * Create a new FunkinSprite with a Packer atlas animated texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function createPacker(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite = new FunkinSprite(x, y);
+ sprite.loadPacker(key);
+ return sprite;
+ }
+
+ /**
+ * Load a static image as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadTexture(key:String):FunkinSprite
+ {
+ if (!isTextureCached(key)) FlxG.log.warn('Texture not cached, may experience stuttering! $key');
+
+ loadGraphic(key);
+
+ return this;
+ }
+
+ /**
+ * Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadSparrow(key:String):FunkinSprite
+ {
+ var graphicKey = Paths.image(key);
+ if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
+
+ this.frames = Paths.getSparrowAtlas(key);
+
+ return this;
+ }
+
+ /**
+ * Load an animated texture (Packer atlas spritesheet) as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadPacker(key:String):FunkinSprite
+ {
+ var graphicKey = Paths.image(key);
+ if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
+
+ this.frames = Paths.getPackerAtlas(key);
+
+ return this;
+ }
+
+ public static function isTextureCached(key:String):Bool
+ {
+ return FlxG.bitmap.get(key) != null;
+ }
+
+ public static function cacheTexture(key:String):Void
+ {
+ // We don't want to cache the same texture twice.
+ if (currentCachedTextures.exists(key)) return;
+
+ if (previousCachedTextures.exists(key))
+ {
+ // Move the graphic from the previous cache to the current cache.
+ var graphic = previousCachedTextures.get(key);
+ previousCachedTextures.remove(key);
+ currentCachedTextures.set(key, graphic);
+ return;
+ }
+
+ // Else, texture is currently uncached.
+ var graphic = flixel.graphics.FlxGraphic.fromAssetKey(key, false, null, true);
+ if (graphic == null)
+ {
+ FlxG.log.warn('Failed to cache graphic: $key');
+ }
+ else
+ {
+ trace('Successfully cached graphic: $key');
+ graphic.persist = true;
+ currentCachedTextures.set(key, graphic);
+ }
+ }
+
+ public static function cacheSparrow(key:String):Void
+ {
+ cacheTexture(Paths.image(key));
+ }
+
+ public static function cachePacker(key:String):Void
+ {
+ cacheTexture(Paths.image(key));
+ }
+
+ /**
+ * Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
+ */
+ public static function preparePurgeCache():Void
+ {
+ previousCachedTextures = currentCachedTextures;
+ currentCachedTextures = [];
+ }
+
+ public static function purgeCache():Void
+ {
+ // Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
+ for (graphicKey in previousCachedTextures.keys())
+ {
+ var graphic = previousCachedTextures.get(graphicKey);
+ FlxG.bitmap.remove(graphic);
+ graphic.destroy();
+ previousCachedTextures.remove(graphicKey);
+ }
+ }
+
+ static function isGraphicCached(graphic:FlxGraphic):Bool
+ {
+ if (graphic == null) return false;
+ var result = FlxG.bitmap.get(graphic.key);
+ if (result == null) return false;
+ if (result != graphic)
+ {
+ FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
+ return false;
+ }
+ return true;
+ }
+
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
- * at the expense of not being able to paint onto the sprite.
+ * at the expense of not being able to paint onto the resulting sprite.
*
* @param width The target width of the sprite.
* @param height The target height of the sprite.
* @param color The color to fill the sprite with.
+ * @return This sprite, for chaining.
*/
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
{
+ // Create a tiny solid color graphic and scale it up to the desired size.
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
frames = graphic.imageFrame;
- scale.set(width / 2, height / 2);
+ scale.set(width / 2.0, height / 2.0);
updateHitbox();
return this;
diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
index ae7a5708c..2329a2791 100644
--- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
+++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
@@ -18,7 +18,7 @@ class FlxAtlasSprite extends FlxAnimate
// ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end,
Antialiasing: true,
- ScrollFactor: new FlxPoint(1, 1),
+ ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
@@ -55,8 +55,9 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array
{
- // return this.anim.getFrameLabels();
- return [""];
+ if (this.anim == null) return [];
+ return this.anim.getFrameLabels();
+ // return [""];
}
/**
@@ -82,8 +83,10 @@ class FlxAtlasSprite extends FlxAnimate
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
*/
- public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false):Void
+ public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void
{
+ if (loop == null) loop = false;
+
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
@@ -110,15 +113,14 @@ class FlxAtlasSprite extends FlxAnimate
return;
}
- // Stop the current animation if it is playing.
- // This includes removing existing frame callbacks.
- if (this.currentAnimation != null) this.stopAnimation();
-
- // Add a callback to ensure `onAnimationFinish` is dispatched.
- addFrameCallback(getNextFrameLabel(id), function() {
- trace('Animation finished: ' + id);
- onAnimationFinish.dispatch(id);
- });
+ anim.callback = function(_, frame:Int) {
+ if (frame == (anim.getFrameLabel(id).duration - 1) + anim.getFrameLabel(id).index)
+ {
+ if (loop) playAnimation(id, true, false, true);
+ else
+ onAnimationFinish.dispatch(id);
+ }
+ };
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
@@ -128,6 +130,11 @@ class FlxAtlasSprite extends FlxAnimate
this.currentAnimation = id;
}
+ override public function update(elapsed:Float)
+ {
+ super.update(elapsed);
+ }
+
/**
* Stops the current animation.
*/
@@ -146,22 +153,22 @@ class FlxAtlasSprite extends FlxAnimate
frameLabel.add(callback);
}
- inline function goToFrameLabel(label:String):Void
+ function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
- inline function getNextFrameLabel(label:String):String
+ function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
- inline function getLabelIndex(label:String):Int
+ function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
- inline function goToFrameIndex(index:Int):Void
+ function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 18f934aee..5d522e3ae 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -106,12 +106,19 @@ class NoteScriptEvent extends ScriptEvent
*/
public var playSound(default, default):Bool;
+ /**
+ * A multiplier to the health gained or lost from this note.
+ * This affects both hits and misses. Remember that max health is 2.00.
+ */
+ public var healthMulti:Float;
+
public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
this.comboCount = comboCount;
this.playSound = true;
+ this.healthMulti = 1.0;
}
public override function toString():String
@@ -182,17 +189,17 @@ class SongEventScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
- public var event(default, null):funkin.data.song.SongData.SongEventData;
+ public var eventData(default, null):funkin.data.song.SongData.SongEventData;
- public function new(event:funkin.data.song.SongData.SongEventData):Void
+ public function new(eventData:funkin.data.song.SongData.SongEventData):Void
{
super(SONG_EVENT, true);
- this.event = event;
+ this.eventData = eventData;
}
public override function toString():String
{
- return 'SongEventScriptEvent(event=' + event + ')';
+ return 'SongEventScriptEvent(event=' + eventData + ')';
}
}
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 5b7ce9fc2..38e8986ef 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
@@ -214,7 +215,7 @@ class Countdown
if (spritePath == null) return;
- var countdownSprite:FlxSprite = new FlxSprite(0, 0).loadGraphic(Paths.image(spritePath));
+ var countdownSprite:FunkinSprite = FunkinSprite.create(Paths.image(spritePath));
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 74b39417e..b7e92d10f 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -4,16 +4,18 @@ import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.sound.FlxSound;
-import funkin.ui.story.StoryMenuState;
+import funkin.audio.FunkinSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinSprite;
-import funkin.ui.MusicBeatSubState;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
+import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.ui.freeplay.FreeplayState;
-import funkin.play.character.BaseCharacter;
+import funkin.ui.MusicBeatSubState;
+import funkin.ui.story.StoryMenuState;
+import openfl.utils.Assets;
/**
* A substate which renders over the PlayState when the player dies.
@@ -63,7 +65,7 @@ class GameOverSubState extends MusicBeatSubState
/**
* The music playing in the background of the state.
*/
- var gameOverMusic:FlxSound = new FlxSound();
+ var gameOverMusic:Null = null;
/**
* Whether the player has confirmed and prepared to restart the level.
@@ -71,6 +73,11 @@ class GameOverSubState extends MusicBeatSubState
*/
var isEnding:Bool = false;
+ /**
+ * Whether the death music is on its first loop.
+ */
+ var isStarting:Bool = true;
+
var isChartingMode:Bool = false;
var transparent:Bool;
@@ -140,14 +147,16 @@ class GameOverSubState extends MusicBeatSubState
// Set up the audio
//
- // Prepare the game over music.
- FlxG.sound.list.add(gameOverMusic);
- gameOverMusic.stop();
-
// The conductor now represents the BPM of the game over music.
Conductor.instance.update(0);
}
+ public function resetCameraZoom():Void
+ {
+ // Apply camera zoom level from stage data.
+ FlxG.camera.zoom = PlayState?.instance?.currentStage?.camZoom ?? 1.0;
+ }
+
var hasStartedAnimation:Bool = false;
override function update(elapsed:Float)
@@ -216,7 +225,7 @@ class GameOverSubState extends MusicBeatSubState
}
}
- if (gameOverMusic.playing)
+ if (gameOverMusic != null && gameOverMusic.playing)
{
// Match the conductor to the music.
// This enables the stepHit and beatHit events.
@@ -291,24 +300,71 @@ class GameOverSubState extends MusicBeatSubState
ScriptEventDispatcher.callEvent(boyfriend, event);
}
+ /**
+ * Rather than hardcoding stuff, we look for the presence of a music file
+ * with the given suffix, and strip it down until we find one that's valid.
+ */
+ function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null
+ {
+ var basePath = 'gameplay/gameover/gameOver';
+ if (starting) basePath += 'Start';
+ else if (ending) basePath += 'End';
+
+ var musicPath = Paths.music(basePath + suffix);
+ while (!Assets.exists(musicPath) && suffix.length > 0)
+ {
+ suffix = suffix.split('-').slice(0, -1).join('-');
+ musicPath = Paths.music(basePath + suffix);
+ }
+ if (!Assets.exists(musicPath)) return null;
+ trace('Resolved music path: ' + musicPath);
+ return musicPath;
+ }
+
/**
* Starts the death music at the appropriate volume.
* @param startingVolume
*/
- function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
+ public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{
- var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
- if (isEnding)
+ var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
+ var onComplete = null;
+ if (isStarting)
{
- musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
+ if (musicPath == null)
+ {
+ isStarting = false;
+ musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
+ }
+ else
+ {
+ isStarting = false;
+ onComplete = function() {
+ // We need to force to ensure that the non-starting music plays.
+ startDeathMusic(1.0, true);
+ };
+ }
}
- if (!gameOverMusic.playing || force)
+
+ if (musicPath == null)
{
- gameOverMusic.loadEmbedded(musicPath);
+ trace('Could not find game over music!');
+ return;
+ }
+ else if (gameOverMusic == null || !gameOverMusic.playing || force)
+ {
+ if (gameOverMusic != null) gameOverMusic.stop();
+ gameOverMusic = FunkinSound.load(musicPath);
gameOverMusic.volume = startingVolume;
- gameOverMusic.looped = !isEnding;
+ gameOverMusic.looped = !(isEnding || isStarting);
+ gameOverMusic.onComplete = onComplete;
gameOverMusic.play();
}
+ else
+ {
+ @:privateAccess
+ trace('Music already playing! ${gameOverMusic?._label}');
+ }
}
static var blueballed:Bool = false;
@@ -320,7 +376,14 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX()
{
blueballed = true;
- FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
+ {
+ FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ }
+ else
+ {
+ FlxG.log.error('Missing blue ball sound effect: ' + Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ }
}
var playingJeffQuote:Bool = false;
@@ -344,6 +407,14 @@ class GameOverSubState extends MusicBeatSubState
});
}
+ public override function destroy()
+ {
+ super.destroy();
+ if (gameOverMusic != null) gameOverMusic.stop();
+ gameOverMusic = null;
+ instance = null;
+ }
+
public override function toString():String
{
return "GameOverSubState";
diff --git a/source/funkin/play/GitarooPause.hx b/source/funkin/play/GitarooPause.hx
index edeb4229c..1ed9dcf3b 100644
--- a/source/funkin/play/GitarooPause.hx
+++ b/source/funkin/play/GitarooPause.hx
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.play.PlayState;
+import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.mainmenu.MainMenuState;
@@ -27,25 +28,22 @@ class GitarooPause extends MusicBeatState
{
if (FlxG.sound.music != null) FlxG.sound.music.stop();
- var bg:FlxSprite = new FlxSprite().loadGraphic(Paths.image('pauseAlt/pauseBG'));
+ var bg:FunkinSprite = FunkinSprite.create(Paths.image('pauseAlt/pauseBG'));
add(bg);
- var bf:FlxSprite = new FlxSprite(0, 30);
- bf.frames = Paths.getSparrowAtlas('pauseAlt/bfLol');
+ var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');
bf.animation.addByPrefix('lol', "funnyThing", 13);
bf.animation.play('lol');
add(bf);
bf.screenCenter(X);
- replayButton = new FlxSprite(FlxG.width * 0.28, FlxG.height * 0.7);
- replayButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
+ replayButton = FunkinSprite.createSparrow(FlxG.width * 0.28, FlxG.height * 0.7, 'pauseAlt/pauseUI');
replayButton.animation.addByPrefix('selected', 'bluereplay', 0, false);
replayButton.animation.appendByPrefix('selected', 'yellowreplay');
replayButton.animation.play('selected');
add(replayButton);
- cancelButton = new FlxSprite(FlxG.width * 0.58, replayButton.y);
- cancelButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
+ cancelButton = FunkinSprite.createSparrow(FlxG.width * 0.58, replayButton.y, 'pauseAlt/pauseUI');
cancelButton.animation.addByPrefix('selected', 'bluecancel', 0, false);
cancelButton.animation.appendByPrefix('selected', 'cancelyellow');
cancelButton.animation.play('selected');
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index 023b8d5be..1ae96268d 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -13,6 +13,7 @@ import flixel.util.FlxColor;
import funkin.play.PlayState;
import funkin.data.song.SongRegistry;
import funkin.ui.Alphabet;
+import funkin.graphics.FunkinSprite;
class PauseSubState extends MusicBeatSubState
{
@@ -72,7 +73,7 @@ class PauseSubState extends MusicBeatSubState
FlxG.sound.list.add(pauseMusic);
- bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
+ bg = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.alpha = 0;
bg.scrollFactor.set();
add(bg);
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 1dbba5b54..5bbf83e17 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -10,12 +10,14 @@ import flixel.addons.transition.Transition;
import flixel.addons.transition.Transition;
import flixel.FlxCamera;
import flixel.FlxObject;
-import flixel.FlxSprite;
import flixel.FlxState;
+import funkin.graphics.FunkinSprite;
import flixel.FlxSubState;
+import funkin.graphics.FunkinSprite;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
+import funkin.graphics.FunkinSprite;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
@@ -213,7 +215,7 @@ class PlayState extends MusicBeatSubState
* The current gameplay camera will always follow this object. Tween its position to move the camera smoothly.
*
* It needs to be an object in the scene for the camera to be configured to follow it.
- * We optionally make this an FlxSprite so we can draw a debug graphic with it.
+ * We optionally make this a sprite so we can draw a debug graphic with it.
*/
public var cameraFollowPoint:FlxObject;
@@ -400,7 +402,7 @@ class PlayState extends MusicBeatSubState
* The background image used for the health bar.
* Emma says the image is slightly skewed so I'm leaving it as an image instead of a `createGraphic`.
*/
- public var healthBarBG:FlxSprite;
+ public var healthBarBG:FunkinSprite;
/**
* The health icon representing the player.
@@ -568,12 +570,15 @@ class PlayState extends MusicBeatSubState
if (!assertChartExists()) return;
+ // TODO: Add something to toggle this on!
if (false)
{
// Displays the camera follow point as a sprite for debug purposes.
- cameraFollowPoint = new FlxSprite(0, 0).makeGraphic(8, 8, 0xFF00FF00);
+ var cameraFollowPoint = new FunkinSprite(0, 0);
+ cameraFollowPoint.makeSolidColor(8, 8, 0xFF00FF00);
cameraFollowPoint.visible = false;
cameraFollowPoint.zIndex = 1000000;
+ this.cameraFollowPoint = cameraFollowPoint;
}
else
{
@@ -918,6 +923,7 @@ class PlayState extends MusicBeatSubState
{
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
+ FlxG.watch.addQuick('health', health);
// TODO: Add a song event for Handle GF dance speed.
@@ -981,8 +987,21 @@ class PlayState extends MusicBeatSubState
}
}
+ processSongEvents();
+
+ // Handle keybinds.
+ processInputQueue();
+ if (!isInCutscene && !disableKeys) debugKeyShit();
+ if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
+
+ // Moving notes into position is now done by Strumline.update().
+ processNotes(elapsed);
+ }
+
+ function processSongEvents():Void
+ {
// Query and activate song events.
- // TODO: Check that these work even when songPosition is less than 0.
+ // TODO: Check that these work appropriately even when songPosition is less than 0, to play events during countdown.
if (songEvents != null && songEvents.length > 0)
{
var songEventsToActivate:Array = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
@@ -992,8 +1011,9 @@ class PlayState extends MusicBeatSubState
trace('Found ${songEventsToActivate.length} event(s) to activate.');
for (event in songEventsToActivate)
{
- // If an event is trying to play, but it's over 5 seconds old, skip it.
- if (event.time - Conductor.instance.songPosition < -5000)
+ // If an event is trying to play, but it's over 1 second old, skip it.
+ var eventAge:Float = Conductor.instance.songPosition - event.time;
+ if (eventAge > 1000)
{
event.activated = true;
continue;
@@ -1009,14 +1029,6 @@ class PlayState extends MusicBeatSubState
}
}
}
-
- // Handle keybinds.
- processInputQueue();
- if (!isInCutscene && !disableKeys) debugKeyShit();
- if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
-
- // Moving notes into position is now done by Strumline.update().
- processNotes(elapsed);
}
public override function dispatchEvent(event:ScriptEvent):Void
@@ -1348,7 +1360,7 @@ class PlayState extends MusicBeatSubState
function initHealthBar():Void
{
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
- healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
+ healthBarBG = FunkinSprite.create(0, healthBarYPos, Paths.image('healthBar'));
healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0);
add(healthBarBG);
@@ -1382,7 +1394,7 @@ class PlayState extends MusicBeatSubState
function initMinimalMode():Void
{
// Create the green background.
- var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat'));
+ var menuBG = FunkinSprite.create(Paths.image('menuDesat'));
menuBG.color = 0xFF4CAF50;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox();
@@ -1408,8 +1420,7 @@ class PlayState extends MusicBeatSubState
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event);
- // Apply camera zoom level from stage data.
- defaultCameraZoom = currentStage.camZoom;
+ resetCameraZoom();
// Add the stage to the scene.
this.add(currentStage);
@@ -1425,6 +1436,12 @@ class PlayState extends MusicBeatSubState
}
}
+ public function resetCameraZoom():Void
+ {
+ // Apply camera zoom level from stage data.
+ defaultCameraZoom = currentStage.camZoom;
+ }
+
/**
* Generates the character sprites and adds them to the stage.
*/
@@ -1750,7 +1767,7 @@ class PlayState extends MusicBeatSubState
currentChart.playInst(1.0, false);
}
- FlxG.sound.music.onComplete = endSong;
+ FlxG.sound.music.onComplete = endSong.bind(false);
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
@@ -1978,7 +1995,7 @@ class PlayState extends MusicBeatSubState
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
- onNoteMiss(note);
+ onNoteMiss(note, event.playSound, event.healthMulti);
note.handledMiss = true;
}
@@ -2030,6 +2047,7 @@ class PlayState extends MusicBeatSubState
}
}
+ // Respawns notes that were b
playerStrumline.handleSkippedNotes();
opponentStrumline.handleSkippedNotes();
}
@@ -2129,7 +2147,7 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
- popUpScore(note, input);
+ popUpScore(note, input, event.healthMulti);
if (note.isHoldNote && note.holdNoteSprite != null)
{
@@ -2143,15 +2161,11 @@ class PlayState extends MusicBeatSubState
* Called when a note leaves the screen and is considered missed by the player.
* @param note
*/
- function onNoteMiss(note:NoteSprite):Void
+ function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthLossMulti:Float = 1.0):Void
{
- // a MISS is when you let a note scroll past you!!
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
- dispatchEvent(event);
- // Calling event.cancelEvent() skips all the other logic! Neat!
- if (event.eventCanceled) return;
+ // If we are here, we already CALLED the onNoteMiss script hook!
- health -= Constants.HEALTH_MISS_PENALTY;
+ health -= Constants.HEALTH_MISS_PENALTY * healthLossMulti;
songScore -= 10;
if (!isPracticeMode)
@@ -2201,7 +2215,7 @@ class PlayState extends MusicBeatSubState
Highscore.tallies.combo = comboPopUps.displayCombo(0);
}
- if (event.playSound)
+ if (playSound)
{
vocals.playerVolume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
@@ -2274,11 +2288,6 @@ class PlayState extends MusicBeatSubState
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
- // Eject button
- if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
-
- if (FlxG.keys.justPressed.F5) debug_refreshModules();
-
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
{
@@ -2301,7 +2310,7 @@ class PlayState extends MusicBeatSubState
#if (debug || FORCE_DEBUG_VERSION)
// 1: End the song immediately.
- if (FlxG.keys.justPressed.ONE) endSong();
+ if (FlxG.keys.justPressed.ONE) endSong(true);
// 2: Gain 10% health.
if (FlxG.keys.justPressed.TWO) health += 0.1 * Constants.HEALTH_MAX;
@@ -2328,7 +2337,7 @@ class PlayState extends MusicBeatSubState
/**
* Handles health, score, and rating popups when a note is hit.
*/
- function popUpScore(daNote:NoteSprite, input:PreciseInputEvent):Void
+ function popUpScore(daNote:NoteSprite, input:PreciseInputEvent, healthGainMulti:Float = 1.0):Void
{
vocals.playerVolume = 1;
@@ -2359,19 +2368,19 @@ class PlayState extends MusicBeatSubState
{
case 'sick':
Highscore.tallies.sick += 1;
- health += Constants.HEALTH_SICK_BONUS;
+ health += Constants.HEALTH_SICK_BONUS * healthGainMulti;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
- health += Constants.HEALTH_GOOD_BONUS;
+ health += Constants.HEALTH_GOOD_BONUS * healthGainMulti;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
- health += Constants.HEALTH_BAD_BONUS;
+ health += Constants.HEALTH_BAD_BONUS * healthGainMulti;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
- health += Constants.HEALTH_SHIT_BONUS;
+ health += Constants.HEALTH_SHIT_BONUS * healthGainMulti;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
}
@@ -2495,16 +2504,35 @@ class PlayState extends MusicBeatSubState
if (skipHeldTimer >= 1.5)
{
- VideoCutscene.finishVideo();
+ skipVideoCutscene();
}
}
/**
- * End the song. Handle saving high scores and transitioning to the results screen.
+ * Handle logic for actually skipping a video cutscene after it has been held.
*/
- function endSong():Void
+ function skipVideoCutscene():Void
{
- dispatchEvent(new ScriptEvent(SONG_END));
+ VideoCutscene.finishVideo();
+ }
+
+ /**
+ * End the song. Handle saving high scores and transitioning to the results screen.
+ *
+ * Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something).
+ * Remember to call `endSong` again when the song should actually end!
+ * @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene.
+ */
+ public function endSong(rightGoddamnNow:Bool = false):Void
+ {
+ FlxG.sound.music.volume = 0;
+ vocals.volume = 0;
+ mayPauseGame = false;
+
+ // Check if any events want to prevent the song from ending.
+ var event = new ScriptEvent(SONG_END, true);
+ dispatchEvent(event);
+ if (event.eventCanceled) return;
#if sys
// spitter for ravy, teehee!!
@@ -2514,9 +2542,7 @@ class PlayState extends MusicBeatSubState
#end
deathCounter = 0;
- mayPauseGame = false;
- FlxG.sound.music.volume = 0;
- vocals.volume = 0;
+
if (currentSong != null && currentSong.validScore)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
@@ -2603,7 +2629,14 @@ class PlayState extends MusicBeatSubState
}
else
{
- moveToResultsScreen();
+ if (rightGoddamnNow)
+ {
+ moveToResultsScreen();
+ }
+ else
+ {
+ zoomIntoResultsScreen();
+ }
}
}
else
@@ -2621,10 +2654,10 @@ class PlayState extends MusicBeatSubState
// TODO: Softcode this cutscene.
if (currentSong.id == 'eggnog')
{
- var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
- -FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
- blackShit.scrollFactor.set();
- add(blackShit);
+ var blackBG:FunkinSprite = new FunkinSprite(-FlxG.width * FlxG.camera.zoom, -FlxG.height * FlxG.camera.zoom);
+ blackBG.makeSolidColor(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
+ blackBG.scrollFactor.set();
+ add(blackBG);
camHUD.visible = false;
isInCutscene = true;
@@ -2661,7 +2694,14 @@ class PlayState extends MusicBeatSubState
}
else
{
- moveToResultsScreen();
+ if (rightGoddamnNow)
+ {
+ moveToResultsScreen();
+ }
+ else
+ {
+ zoomIntoResultsScreen();
+ }
}
}
}
@@ -2715,9 +2755,9 @@ class PlayState extends MusicBeatSubState
}
/**
- * Play the camera zoom animation and move to the results screen.
+ * Play the camera zoom animation and then move to the results screen once it's done.
*/
- function moveToResultsScreen():Void
+ function zoomIntoResultsScreen():Void
{
trace('WENT TO RESULTS SCREEN!');
@@ -2771,22 +2811,30 @@ class PlayState extends MusicBeatSubState
{
ease: FlxEase.expoIn,
onComplete: function(_) {
- persistentUpdate = false;
- vocals.stop();
- camHUD.alpha = 1;
- var res:ResultState = new ResultState(
- {
- storyMode: PlayStatePlaylist.isStoryMode,
- title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
- tallies: Highscore.tallies,
- });
- res.camera = camHUD;
- openSubState(res);
+ moveToResultsScreen();
}
});
});
}
+ /**
+ * Move to the results screen right goddamn now.
+ */
+ function moveToResultsScreen():Void
+ {
+ persistentUpdate = false;
+ vocals.stop();
+ camHUD.alpha = 1;
+ var res:ResultState = new ResultState(
+ {
+ storyMode: PlayStatePlaylist.isStoryMode,
+ title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+ tallies: Highscore.tallies,
+ });
+ res.camera = camHUD;
+ openSubState(res);
+ }
+
/**
* Pauses music and vocals easily.
*/
@@ -2816,14 +2864,18 @@ class PlayState extends MusicBeatSubState
*/
function changeSection(sections:Int):Void
{
- FlxG.sound.music.pause();
+ // FlxG.sound.music.pause();
- var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
+ var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.stepsPerMeasure * sections);
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
+ // Don't go back in time to before the song started.
+ targetTimeMs = Math.max(0, targetTimeMs);
+
FlxG.sound.music.time = targetTimeMs;
handleSkippedNotes();
+ SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);
// regenNoteData(FlxG.sound.music.time);
Conductor.instance.update(FlxG.sound.music.time);
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 9ffeefcfd..223043c28 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -4,6 +4,7 @@ import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxBasic;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxBitmapFont;
import flixel.group.FlxGroup.FlxTypedGroup;
@@ -96,8 +97,7 @@ class ResultState extends MusicBeatSubState
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
- var gf:FlxSprite = new FlxSprite(500, 300);
- gf.frames = Paths.getSparrowAtlas('resultScreen/resultGirlfriendGOOD');
+ var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false;
gf.animation.finishCallback = _ -> {
@@ -105,8 +105,7 @@ class ResultState extends MusicBeatSubState
};
add(gf);
- var boyfriend:FlxSprite = new FlxSprite(640, -200);
- boyfriend.frames = Paths.getSparrowAtlas('resultScreen/resultBoyfriendGOOD');
+ var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
boyfriend.animation.addByPrefix("fall", "Boyfriend Good", 24, false);
boyfriend.visible = false;
boyfriend.animation.finishCallback = function(_) {
@@ -115,8 +114,7 @@ class ResultState extends MusicBeatSubState
add(boyfriend);
- var soundSystem:FlxSprite = new FlxSprite(-15, -180);
- soundSystem.frames = Paths.getSparrowAtlas("resultScreen/soundSystem");
+ var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
soundSystem.visible = false;
new FlxTimer().start(0.4, _ -> {
@@ -162,20 +160,17 @@ class ResultState extends MusicBeatSubState
FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
add(blackTopBar);
- var resultsAnim:FlxSprite = new FlxSprite(-200, -10);
- resultsAnim.frames = Paths.getSparrowAtlas("resultScreen/results");
+ var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results", 24, false);
resultsAnim.animation.play("result");
add(resultsAnim);
- var ratingsPopin:FlxSprite = new FlxSprite(-150, 120);
- ratingsPopin.frames = Paths.getSparrowAtlas("resultScreen/ratingsPopin");
+ var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
ratingsPopin.visible = false;
add(ratingsPopin);
- var scorePopin:FlxSprite = new FlxSprite(-180, 520);
- scorePopin.frames = Paths.getSparrowAtlas("resultScreen/scorePopin");
+ var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
scorePopin.animation.addByPrefix("score", "tally score", 24, false);
scorePopin.visible = false;
add(scorePopin);
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 69e3ca48e..f3c7d7613 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -305,9 +305,15 @@ class CharacterDataParser
icon = "darnell";
case "senpai-angry":
icon = "senpai";
+ case "tankman" | "tankman-atlas":
+ icon = "tankmen";
}
- return Paths.image("freeplay/icons/" + icon + "pixel");
+ var path = Paths.image("freeplay/icons/" + icon + "pixel");
+ if (Assets.exists(path)) return path;
+
+ // TODO: Hardcode some additional behavior or a fallback.
+ return null;
}
/**
diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 420a4fdc4..419c5b3ea 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -6,6 +6,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import funkin.play.character.CharacterData.CharacterDataParser;
import openfl.utils.Assets;
+import funkin.graphics.FunkinSprite;
import funkin.util.MathUtil;
/**
@@ -26,7 +27,7 @@ import funkin.util.MathUtil;
* @author MasterEric
*/
@:nullSafety
-class HealthIcon extends FlxSprite
+class HealthIcon extends FunkinSprite
{
/**
* The character this icon is representing.
@@ -408,7 +409,7 @@ class HealthIcon extends FlxSprite
if (!isLegacyStyle)
{
- frames = Paths.getSparrowAtlas('icons/icon-$charId');
+ loadSparrow('icons/icon-$charId');
loadAnimationNew();
}
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index 9553856a9..88ffa468c 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -3,6 +3,7 @@ package funkin.play.components;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
+import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
class PopUpStuff extends FlxTypedGroup
@@ -14,17 +15,20 @@ class PopUpStuff extends FlxTypedGroup
public function displayRating(daRating:String)
{
+ #if sys
+ var perfStart:Float = Sys.time();
+ #end
+
if (daRating == null) daRating = "good";
- var rating:FlxSprite = new FlxSprite(0, 0);
- rating.scrollFactor.set(0.2, 0.2);
-
- rating.zIndex = 1000;
var ratingPath:String = daRating;
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
- rating.loadGraphic(Paths.image(ratingPath));
+ var rating:FunkinSprite = FunkinSprite.create(0, 0, Paths.image(ratingPath));
+ rating.scrollFactor.set(0.2, 0.2);
+
+ rating.zIndex = 1000;
rating.x = FlxG.width * 0.50;
rating.x -= FlxG.camera.scroll.x * 0.2;
// make sure rating is visible lol!
@@ -61,10 +65,19 @@ class PopUpStuff extends FlxTypedGroup
},
startDelay: Conductor.instance.beatLengthMs * 0.001
});
+
+ #if sys
+ var perfEnd:Float = Sys.time();
+ trace("displayRating took: " + (perfEnd - perfStart));
+ #end
}
public function displayCombo(?combo:Int = 0):Int
{
+ #if sys
+ var perfStart:Float = Sys.time();
+ #end
+
if (combo == null) combo = 0;
var pixelShitPart1:String = "";
@@ -75,7 +88,7 @@ class PopUpStuff extends FlxTypedGroup
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
}
- var comboSpr:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2));
+ var comboSpr:FunkinSprite = FunkinSprite.create(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2));
comboSpr.y = FlxG.camera.height * 0.4 + 80;
comboSpr.x = FlxG.width * 0.50;
comboSpr.x -= FlxG.camera.scroll.x * 0.2;
@@ -129,8 +142,7 @@ class PopUpStuff extends FlxTypedGroup
var daLoop:Int = 1;
for (i in seperatedScore)
{
- var numScore:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2));
- numScore.y = comboSpr.y;
+ var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2));
if (PlayState.instance.currentStageId.startsWith('school'))
{
@@ -163,6 +175,11 @@ class PopUpStuff extends FlxTypedGroup
daLoop++;
}
+ #if sys
+ var perfEnd:Float = Sys.time();
+ trace("displayCombo took: " + (perfEnd - perfStart));
+ #end
+
return combo;
}
}
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 934919b65..75e69bf04 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -19,13 +19,22 @@ import hxcodec.flixel.FlxVideoSprite;
class VideoCutscene
{
static var blackScreen:FlxSprite;
+ static var cutsceneType:CutsceneType;
+
+ #if html5
+ static var vid:FlxVideo;
+ #end
+ #if hxCodec
+ static var vid:FlxVideoSprite;
+ #end
/**
* Play a video cutscene.
* TODO: Currently this is hardcoded to start the countdown after the video is done.
* @param path The path to the video file. Use Paths.file(path) to get the correct path.
+ * @param cutseneType The type of cutscene to play, determines what the game does after. Defaults to `CutsceneType.STARTING`.
*/
- public static function play(filePath:String):Void
+ public static function play(filePath:String, ?cutsceneType:CutsceneType = STARTING):Void
{
if (PlayState.instance == null) return;
@@ -36,6 +45,8 @@ class VideoCutscene
return;
}
+ var rawFilePath = Paths.stripLibrary(filePath);
+
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
@@ -47,12 +58,14 @@ class VideoCutscene
blackScreen.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(blackScreen);
+ VideoCutscene.cutsceneType = cutsceneType;
+
#if html5
playVideoHTML5(filePath);
- #end
-
- #if hxCodec
- playVideoNative(filePath);
+ #elseif hxCodec
+ playVideoNative(rawFilePath);
+ #else
+ throw "No video support for this platform!";
#end
}
@@ -66,8 +79,6 @@ class VideoCutscene
}
#if html5
- static var vid:FlxVideo;
-
static function playVideoHTML5(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -92,8 +103,6 @@ class VideoCutscene
#end
#if hxCodec
- static var vid:FlxVideoSprite;
-
static function playVideoNative(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -110,6 +119,15 @@ class VideoCutscene
PlayState.instance.refresh();
vid.play(filePath, false);
+
+ // Resize videos bigger or smaller than the screen.
+ vid.bitmap.onTextureSetup.add(() -> {
+ vid.setGraphicSize(FlxG.width, FlxG.height);
+ vid.updateHitbox();
+ vid.x = 0;
+ vid.y = 0;
+ // vid.scale.set(0.5, 0.5);
+ });
}
else
{
@@ -118,10 +136,17 @@ class VideoCutscene
}
#end
+ /**
+ * Finish the active video cutscene. Done when the video is finished or when the player skips the cutscene.
+ * @param transitionTime The duration of the transition to the next state. Defaults to 0.5 seconds (this time is always used when cancelling the video).
+ * @param finishCutscene The callback to call when the transition is finished.
+ */
public static function finishVideo(?transitionTime:Float = 0.5):Void
{
trace('ALERT: Finish video cutscene called!');
+ var cutsceneType:CutsceneType = VideoCutscene.cutsceneType;
+
#if html5
if (vid != null)
{
@@ -157,8 +182,32 @@ class VideoCutscene
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
- PlayState.instance.startCountdown();
+ onCutsceneFinish(cutsceneType);
}
});
}
+
+ /**
+ * The default callback used when a cutscene is finished.
+ * You can specify your own callback when calling `VideoCutscene#play()`.
+ */
+ static function onCutsceneFinish(cutsceneType:CutsceneType):Void
+ {
+ switch (cutsceneType)
+ {
+ case CutsceneType.STARTING:
+ PlayState.instance.startCountdown();
+ case CutsceneType.ENDING:
+ PlayState.instance.endSong(true); // true = right goddamn now
+ case CutsceneType.MIDSONG:
+ throw "Not implemented!";
+ }
+ }
+}
+
+enum CutsceneType
+{
+ STARTING; // The default cutscene type. Starts the countdown after the video is done.
+ MIDSONG; // TODO: Implement this!
+ ENDING; // Ends the song after the video is done.
}
diff --git a/source/funkin/play/notes/NoteSplash.hx b/source/funkin/play/notes/NoteSplash.hx
index 0ff8076c8..2411e5615 100644
--- a/source/funkin/play/notes/NoteSplash.hx
+++ b/source/funkin/play/notes/NoteSplash.hx
@@ -35,6 +35,7 @@ class NoteSplash extends FlxSprite
*/
function setup():Void
{
+ if (frameCollection?.parent?.isDestroyed ?? false) frameCollection = null;
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
@@ -75,6 +76,8 @@ class NoteSplash extends FlxSprite
this.playAnimation('splash${variant}Right');
}
+ if (animation.curAnim == null) return;
+
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx
index 0368b18e9..45862b26d 100644
--- a/source/funkin/play/notes/NoteSprite.hx
+++ b/source/funkin/play/notes/NoteSprite.hx
@@ -4,9 +4,10 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.HSVShader;
-class NoteSprite extends FlxSprite
+class NoteSprite extends FunkinSprite
{
static final DIRECTION_COLORS:Array = ['purple', 'blue', 'green', 'red'];
diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx
index 34c1ce9c3..d0cc09f6a 100644
--- a/source/funkin/play/notes/notestyle/NoteStyle.hx
+++ b/source/funkin/play/notes/notestyle/NoteStyle.hx
@@ -4,6 +4,7 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
+import funkin.graphics.FunkinSprite;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
@@ -100,6 +101,14 @@ class NoteStyle implements IRegistryEntry
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
{
+ if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath())))
+ {
+ FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}');
+ }
+
+ // Purge the note frames if the cached atlas is invalid.
+ if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
+
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
@@ -109,8 +118,6 @@ class NoteStyle implements IRegistryEntry
throw 'Could not load note frames for note style: $id';
}
- noteFrames.parent.persist = true;
-
return noteFrames;
}
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index af5765b25..c20202245 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -185,9 +185,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
switch (dataProp.animType)
{
case 'packer':
- propSprite.frames = Paths.getPackerAtlas(dataProp.assetPath);
+ propSprite.loadPacker(dataProp.assetPath);
default: // 'sparrow'
- propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
+ propSprite.loadSparrow(dataProp.assetPath);
}
}
else if (isSolidColor)
@@ -209,7 +209,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
else
{
// Initalize static sprite.
- propSprite.loadGraphic(Paths.image(dataProp.assetPath));
+ propSprite.loadTexture(Paths.image(dataProp.assetPath));
// Disables calls to update() for a performance boost.
propSprite.active = false;
@@ -397,15 +397,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
this.characters.set('bf', character);
charData = _data.characters.bf;
character.flipX = !character.getDataFlipX();
+ character.name = 'bf';
character.initHealthIcon(false);
case GF:
this.characters.set('gf', character);
charData = _data.characters.gf;
character.flipX = character.getDataFlipX();
+ character.name = 'gf';
case DAD:
this.characters.set('dad', character);
charData = _data.characters.dad;
character.flipX = character.getDataFlipX();
+ character.name = 'dad';
character.initHealthIcon(true);
default:
this.characters.set(character.characterId, character);
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 75352c21d..768ea9e43 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -3408,7 +3408,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the event sprite's position.
eventSprite.updateEventPosition(renderedEvents);
// Update the sprite's graphic. TODO: Is this inefficient?
- eventSprite.playAnimation(eventSprite.eventData.event);
+ eventSprite.playAnimation(eventSprite.eventData.eventKind);
}
else
{
@@ -4669,9 +4669,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
- if (eventKindToPlace != eventData.event)
+ if (eventKindToPlace != eventData.eventKind)
{
- eventData.event = eventKindToPlace;
+ eventData.eventKind = eventKindToPlace;
}
eventData.time = cursorSnappedMs;
diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
index 88f73cfed..423295f1a 100644
--- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
@@ -34,11 +34,11 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
- if (this.notes.length == 0 && this.events.length >= 1)
+ if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
- state.eventKindToPlace = eventSelected.event;
+ state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -46,7 +46,7 @@ class SelectItemsCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
- trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
+ trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -60,7 +60,7 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
- if (this.events.length == 0 && this.notes.length >= 1)
+ if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
index 5cc89e137..46fcca87c 100644
--- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
@@ -31,11 +31,11 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.currentEventSelection = events;
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
- if (this.notes.length == 0 && this.events.length >= 1)
+ if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
- state.eventKindToPlace = eventSelected.event;
+ state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -43,7 +43,7 @@ class SetItemSelectionCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
- trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
+ trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -57,7 +57,7 @@ class SetItemSelectionCommand implements ChartEditorCommand
}
// IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
- if (this.events.length == 0 && this.notes.length >= 1)
+ if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
index e3dae37cf..f680095d7 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
@@ -133,7 +133,7 @@ class ChartEditorEventSprite extends FlxSprite
public function playAnimation(?name:String):Void
{
- if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
+ if (name == null) name = eventData?.eventKind ?? DEFAULT_EVENT;
var correctedName = correctAnimationName(name);
this.animation.play(correctedName);
@@ -160,7 +160,7 @@ class ChartEditorEventSprite extends FlxSprite
else
{
this.visible = true;
- playAnimation(value.event);
+ playAnimation(value.eventKind);
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 363dc1567..5e3ffeb42 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -299,16 +299,14 @@ class ChartEditorAudioHandler
*/
public static function playSound(_state:ChartEditorState, path:String, volume:Float = 1.0):Void
{
- var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
- snd.loadEmbedded(asset);
+ var snd:FunkinSound = FunkinSound.load(asset);
snd.autoDestroy = true;
- FlxG.sound.list.add(snd);
snd.play(true);
snd.volume = volume;
}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
index 7b163ad3d..ec46e1f85 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
@@ -90,7 +90,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any selected events.
for (event in chartEditorState.currentEventSelection)
{
- event.event = chartEditorState.eventKindToPlace;
+ event.eventKind = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
@@ -255,7 +255,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
for (event in chartEditorState.currentEventSelection)
{
- event.event = chartEditorState.eventKindToPlace;
+ event.eventKind = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
index c384e7a6d..1432c9205 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
@@ -289,10 +289,10 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformMusic.waveform.forceUpdate = true;
var perfStart = haxe.Timer.stamp();
- var waveformData1 = playerVoice.waveformData;
- var waveformData2 = opponentVoice?.waveformData ?? playerVoice.waveformData; // this null check is for songs that only have 1 vocals file!
+ var waveformData1 = playerVoice?.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);
+ var waveformData = waveformData3.merge(waveformData1).merge(waveformData2);
trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
waveformMusic.waveform.waveformData = waveformData;
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
index fd9209294..af1d75444 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
@@ -270,24 +270,21 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformPlayer.waveform.forceUpdate = true;
- waveformPlayer.waveform.waveformData = playerVoice.waveformData;
+ waveformPlayer.waveform.waveformData = playerVoice?.waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
- waveformPlayer.waveform.duration = playerVoice.length / Constants.MS_PER_SEC;
+ waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000) / Constants.MS_PER_SEC;
// Build opponent waveform.
// waveformOpponent.waveform.forceUpdate = true;
// 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;
- }
+ waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
+ waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000) / Constants.MS_PER_SEC;
// Build instrumental waveform.
// waveformInstrumental.waveform.forceUpdate = true;
waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
- waveformInstrumental.waveform.duration = instTrack.length / Constants.MS_PER_SEC;
+ waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000) / Constants.MS_PER_SEC;
addOffsetsToAudioPreview();
}
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index b26082f98..26015161b 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -147,6 +147,8 @@ class ChartEditorDropdowns
dropDown.dataSource.add(value);
}
+ dropDown.dataSource.sort('id', ASCENDING);
+
return returnValue;
}
diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx
index 2417cdf9a..9d37fe2c1 100644
--- a/source/funkin/ui/freeplay/DJBoyfriend.hx
+++ b/source/funkin/ui/freeplay/DJBoyfriend.hx
@@ -156,8 +156,6 @@ class DJBoyfriend extends FlxAtlasSprite
function setupAnimations():Void
{
- // frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
-
// animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('boyfriend dj intro', 8, 3);
diff --git a/source/funkin/ui/freeplay/FreeplayFlames.hx b/source/funkin/ui/freeplay/FreeplayFlames.hx
index a116fb813..c20d85898 100644
--- a/source/funkin/ui/freeplay/FreeplayFlames.hx
+++ b/source/funkin/ui/freeplay/FreeplayFlames.hx
@@ -23,7 +23,7 @@ class FreeplayFlames extends FlxSpriteGroup
{
var flame:FlxSprite = new FlxSprite(flameX + (flameSpreadX * i), flameY + (flameSpreadY * i));
flame.frames = Paths.getSparrowAtlas("freeplay/freeplayFlame");
- flame.animation.addByPrefix("flame", "fire loop", FlxG.random.int(23, 25), false);
+ flame.animation.addByPrefix("flame", "fire loop full instance 1", FlxG.random.int(23, 25), false);
flame.animation.play("flame");
flame.visible = false;
flameCount = 0;
diff --git a/source/funkin/ui/freeplay/FreeplayScore.hx b/source/funkin/ui/freeplay/FreeplayScore.hx
index e266efca1..413b182e0 100644
--- a/source/funkin/ui/freeplay/FreeplayScore.hx
+++ b/source/funkin/ui/freeplay/FreeplayScore.hx
@@ -111,7 +111,7 @@ class ScoreNum extends FlxSprite
for (i in 0...10)
{
var stringNum:String = numToString[i];
- animation.addByPrefix(stringNum, stringNum, 24, false);
+ animation.addByPrefix(stringNum, '$stringNum DIGITAL', 24, false);
}
this.digit = initDigit;
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 39cab8759..e4a6b96d8 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -7,6 +7,7 @@ import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxGame;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import flixel.FlxState;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
@@ -226,17 +227,17 @@ class FreeplayState extends MusicBeatSubState
trace(FlxG.camera.initialZoom);
trace(FlxCamera.defaultZoom);
- var pinkBack:FlxSprite = new FlxSprite().loadGraphic(Paths.image('freeplay/pinkBack'));
+ var pinkBack:FunkinSprite = FunkinSprite.create(Paths.image('freeplay/pinkBack'));
pinkBack.color = 0xFFffd4e9; // sets it to pink!
pinkBack.x -= pinkBack.width;
FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
add(pinkBack);
- var orangeBackShit:FlxSprite = new FlxSprite(84, 440).makeGraphic(Std.int(pinkBack.width), 75, 0xFFfeda00);
+ var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFfeda00);
add(orangeBackShit);
- var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400);
+ var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFffd400);
add(alsoOrangeLOL);
exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL],
@@ -462,7 +463,7 @@ class FreeplayState extends MusicBeatSubState
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
- fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
+ fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore small instance 1", 24, false);
fnfHighscoreSpr.visible = false;
fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
fnfHighscoreSpr.updateHitbox();
diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx
index 86f443d1d..5f755872f 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -3,6 +3,7 @@ package funkin.ui.transition;
import flixel.FlxSprite;
import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
+import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import funkin.graphics.shaders.ScreenWipeShader;
@@ -44,11 +45,10 @@ class LoadingState extends MusicBeatState
override function create():Void
{
- var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
+ var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
add(bg);
- funkay = new FlxSprite();
- funkay.loadGraphic(Paths.image('funkay'));
+ funkay = FunkinSprite.create(Paths.image('funkay'));
funkay.setGraphicSize(0, FlxG.height);
funkay.updateHitbox();
add(funkay);
@@ -209,6 +209,43 @@ class LoadingState extends MusicBeatState
params.targetSong.cacheCharts(true);
}
+ // TODO: This section is a hack! Redo this later when we have a proper asset caching system.
+ FunkinSprite.preparePurgeCache();
+ FunkinSprite.cacheTexture(Paths.image('combo'));
+ FunkinSprite.cacheTexture(Paths.image('healthBar'));
+ FunkinSprite.cacheTexture(Paths.image('menuDesat'));
+ FunkinSprite.cacheTexture(Paths.image('combo'));
+ FunkinSprite.cacheTexture(Paths.image('num0'));
+ FunkinSprite.cacheTexture(Paths.image('num1'));
+ FunkinSprite.cacheTexture(Paths.image('num2'));
+ FunkinSprite.cacheTexture(Paths.image('num3'));
+ FunkinSprite.cacheTexture(Paths.image('num4'));
+ FunkinSprite.cacheTexture(Paths.image('num5'));
+ FunkinSprite.cacheTexture(Paths.image('num6'));
+ FunkinSprite.cacheTexture(Paths.image('num7'));
+ FunkinSprite.cacheTexture(Paths.image('num8'));
+ FunkinSprite.cacheTexture(Paths.image('num9'));
+ FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
+ FunkinSprite.cacheTexture(Paths.image('ready', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('set', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('go', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('sick', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('good', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('bad', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
+ FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
+
+ // FunkinSprite.cacheAllNoteStyleTextures(noteStyle) // This will replace the stuff above!
+ // FunkinSprite.cacheAllCharacterTextures(player)
+ // FunkinSprite.cacheAllCharacterTextures(girlfriend)
+ // FunkinSprite.cacheAllCharacterTextures(opponent)
+ // FunkinSprite.cacheAllStageTextures(stage)
+
+ FunkinSprite.purgeCache();
+
FlxG.switchState(playStateCtor);
#end
}
@@ -354,7 +391,7 @@ class MultiCallback
public static function coolSwitchState(state:NextState, transitionTex:String = "shaderTransitionStuff/coolDots", time:Float = 2)
{
- var screenShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("shaderTransitionStuff/coolDots"));
+ var screenShit:FunkinSprite = FunkinSprite.create(Paths.image("shaderTransitionStuff/coolDots"));
var screenWipeShit:ScreenWipeShader = new ScreenWipeShader();
screenWipeShit.funnyShit.input = screenShit.pixels;
diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/StickerSubState.hx
index e94eed7d5..40fce6f7d 100644
--- a/source/funkin/ui/transition/StickerSubState.hx
+++ b/source/funkin/ui/transition/StickerSubState.hx
@@ -3,6 +3,7 @@ package funkin.ui.transition;
import flixel.FlxSprite;
import haxe.Json;
import lime.utils.Assets;
+import funkin.graphics.FunkinSprite;
// import flxtyped group
import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
@@ -245,6 +246,10 @@ class StickerSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
+ // TODO: Rework this asset caching stuff
+ FunkinSprite.preparePurgeCache();
+ FunkinSprite.purgeCache();
+
// 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...
/*
@@ -301,14 +306,14 @@ class StickerSubState extends MusicBeatSubState
}
}
-class StickerSprite extends FlxSprite
+class StickerSprite extends FunkinSprite
{
public var timing:Float = 0;
public function new(x:Float, y:Float, stickerSet:String, stickerName:String):Void
{
super(x, y);
- loadGraphic(Paths.image('transitionSwag/' + stickerSet + '/' + stickerName));
+ loadTexture(Paths.image('transitionSwag/' + stickerSet + '/' + stickerName));
updateHitbox();
scrollFactor.set();
}