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(); }