diff --git a/assets b/assets index d5de6a05f..91af5e79b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d5de6a05fab5bfa8280285caad5471f3800cfc83 +Subproject commit 91af5e79b24a3bd1e3c2b409061f90f3e359d5a7 diff --git a/hmm.json b/hmm.json index 10745f063..e287fadc3 100644 --- a/hmm.json +++ b/hmm.json @@ -56,11 +56,11 @@ "url": "https://github.com/FunkinCrew/flixel-addons" }, { - "name": "flxanimate", + "name": "flixel-animate", "type": "git", "dir": null, - "ref": "49214278b9124823582cdcecd94f4a1de9a4b36b", - "url": "https://github.com/FunkinCrew/flxanimate" + "ref": "9aacdf7afe5ffdcd086a2898ebb7abb966b95a3d", + "url": "https://github.com/MaybeMaru/flixel-animate" }, { "name": "format", diff --git a/project.hxp b/project.hxp index c80f72eef..dbd2f5afd 100644 --- a/project.hxp +++ b/project.hxp @@ -1304,7 +1304,7 @@ class Project extends HXProject } addHaxelib('polymod'); // Modding framework - addHaxelib('flxanimate'); // Texture atlas rendering + addHaxelib('flixel-animate'); // Texture atlas rendering addHaxelib('json2object'); // JSON parsing addHaxelib('jsonpath'); // JSON parsing diff --git a/source/funkin/Assets.hx b/source/funkin/Assets.hx index d26d0b738..da8adff83 100644 --- a/source/funkin/Assets.hx +++ b/source/funkin/Assets.hx @@ -1,13 +1,14 @@ package funkin; import openfl.utils.Future; +import funkin.util.macro.ConsoleMacro; /** * A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions. * Later we'll add Funkin-specific caching to this. */ @:nullSafety -class Assets +class Assets implements ConsoleClass { /** * The assets cache. diff --git a/source/funkin/FunkinMemory.hx b/source/funkin/FunkinMemory.hx index fb0554460..1315e4980 100644 --- a/source/funkin/FunkinMemory.hx +++ b/source/funkin/FunkinMemory.hx @@ -111,6 +111,16 @@ class FunkinMemory ///// TEXTURES ///// + /** + * Determine whether the texture with the given key is cached. + * @param key The key of the texture to check. + * @return Whether the texture is cached. + */ + public static function isTextureCached(key:String):Bool + { + return FlxG.bitmap.get(key) != null; + } + /** * Ensures a texture with the given key is cached. * @param key The key of the texture to cache. @@ -233,6 +243,7 @@ class FunkinMemory if (graphic != null) { FlxG.bitmap.remove(graphic); + graphic.persist = false; graphic.destroy(); previousCachedTextures.remove(graphicKey); Assets.cache.clear(graphicKey); @@ -250,7 +261,7 @@ class FunkinMemory { var obj:Null = FlxG.bitmap.get(key); - if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts")) + if (obj == null || (obj.persist && permanentCachedTextures.exists(key)) || key.contains("fonts")) { continue; } @@ -262,6 +273,7 @@ class FunkinMemory if (key.contains(purgeEntry)) { FlxG.bitmap.removeKey(key); + obj.persist = false; obj.destroy(); } } diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index fdc120cad..c3cac3630 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -2,13 +2,14 @@ package funkin; import flixel.graphics.frames.FlxAtlasFrames; import openfl.utils.AssetType; +import funkin.util.macro.ConsoleMacro; import haxe.io.Path; /** * A core class which handles determining asset paths. */ @:nullSafety -class Paths +class Paths implements ConsoleClass { static var currentLevel:Null = null; diff --git a/source/funkin/data/animation/AnimationData.hx b/source/funkin/data/animation/AnimationData.hx index cdc8cc826..8d8ae1202 100644 --- a/source/funkin/data/animation/AnimationData.hx +++ b/source/funkin/data/animation/AnimationData.hx @@ -83,7 +83,7 @@ typedef UnnamedAnimationData = /** * Optionally specify an asset path to use for this specific animation. - * ONLY for use by MultiSparrow characters. + * ONLY for use by MultiSparrow and MultiAnimateAtlas characters. * @default The assetPath of the parent sprite */ @:optional @@ -138,4 +138,13 @@ typedef UnnamedAnimationData = @:default([]) @:optional var frameIndices:Null>; + + /** + * The type of animation to use. + * Only available for texture atlases. + * Options: "framelabel", "symbol" + */ + @:default("framelabel") + @:optional + var animType:String; } diff --git a/source/funkin/data/character/CharacterData.hx b/source/funkin/data/character/CharacterData.hx index 6bff38a31..8fb89bfab 100644 --- a/source/funkin/data/character/CharacterData.hx +++ b/source/funkin/data/character/CharacterData.hx @@ -6,12 +6,14 @@ import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter; import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter; import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter; +import funkin.play.character.ScriptedCharacter.ScriptedMultiAnimateAtlasCharacter; import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter; import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter; import funkin.play.character.AnimateAtlasCharacter; import funkin.play.character.BaseCharacter; import funkin.play.character.SparrowCharacter; import funkin.play.character.MultiSparrowCharacter; +import funkin.play.character.MultiAnimateAtlasCharacter; import funkin.play.character.PackerCharacter; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; @@ -160,6 +162,25 @@ class CharacterDataParser } } + var scriptedCharClassNames5:Array = ScriptedMultiAnimateAtlasCharacter.listScriptClasses(); + if (scriptedCharClassNames5.length > 0) + { + trace(' Instantiating ${scriptedCharClassNames5.length} (Multi-Animate Atlas) scripted characters...'); + for (charCls in scriptedCharClassNames5) + { + try + { + var character:MultiAnimateAtlasCharacter = ScriptedMultiAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID); + characterScriptedClass.set(character.characterId, charCls); + } + catch (e) + { + trace(' FAILED to instantiate scripted Multi-Animate Atlas character: ${charCls}'); + trace(e); + } + } + } + // NOTE: Only instantiate the ones not populated above. // ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes. var scriptedCharClassNames:Array = ScriptedBaseCharacter.listScriptClasses(); @@ -167,7 +188,8 @@ class CharacterDataParser return !(scriptedCharClassNames1.contains(charCls) || scriptedCharClassNames2.contains(charCls) || scriptedCharClassNames3.contains(charCls) - || scriptedCharClassNames4.contains(charCls)); + || scriptedCharClassNames4.contains(charCls) + || scriptedCharClassNames5.contains(charCls)); }); if (scriptedCharClassNames.length > 0) @@ -226,6 +248,8 @@ class CharacterDataParser char = ScriptedSparrowCharacter.init(charScriptClass, charId); case CharacterRenderType.Packer: char = ScriptedPackerCharacter.init(charScriptClass, charId); + case CharacterRenderType.MultiAnimateAtlas: + char = ScriptedMultiAnimateAtlasCharacter.init(charScriptClass, charId); default: // We're going to assume that the script class does the rendering. char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom); @@ -243,6 +267,8 @@ class CharacterDataParser char = new SparrowCharacter(charId); case CharacterRenderType.Packer: char = new PackerCharacter(charId); + case CharacterRenderType.MultiAnimateAtlas: + char = new MultiAnimateAtlasCharacter(charId); default: trace(' WARNING '.bold().bg_yellow() + ' Creating character with undefined renderType ${charData.renderType}'); char = new BaseCharacter(charId, CharacterRenderType.Custom); @@ -431,6 +457,15 @@ class CharacterDataParser public static final DEFAULT_SCALE:Float = 1; public static final DEFAULT_SCROLL:Array = [0, 0]; public static final DEFAULT_STARTINGANIM:String = 'idle'; + public static final DEFAULT_APPLYSTAGEMATRIX:Bool = false; + public static final DEFAULT_ANIMTYPE:String = "framelabel"; + public static final DEFAULT_ATLASSETTINGS:funkin.data.stage.StageData.TextureAtlasData = + { + swfMode: true, + cacheOnLoad: false, + filterQuality: 1, + applyStageMatrix: false + }; /** * Set unspecified parameters to their defaults. @@ -559,6 +594,16 @@ class CharacterDataParser input.flipX = DEFAULT_FLIPX; } + if (input.applyStageMatrix == null) + { + input.applyStageMatrix = DEFAULT_APPLYSTAGEMATRIX; + } + + if (input.atlasSettings == null) + { + input.atlasSettings = DEFAULT_ATLASSETTINGS; + } + if (input.animations.length == 0 && input.startingAnimation != null) { return null; @@ -596,6 +641,11 @@ class CharacterDataParser { inputAnimation.flipY = DEFAULT_FLIPY; } + + if (inputAnimation.animType == null) + { + inputAnimation.animType = DEFAULT_ANIMTYPE; + } } // All good! @@ -624,10 +674,15 @@ enum abstract CharacterRenderType(String) from String to String public var MultiSparrow = 'multisparrow'; /** - * Renders the character using a spritesheet of symbols and JSON data. + * Renders the character using a single spritesheet of symbols and JSON data. */ public var AnimateAtlas = 'animateatlas'; + /** + * Renders the character using multiple spritesheets of symbols and JSON data. + */ + public var MultiAnimateAtlas = 'multianimateatlas'; + /** * Renders the character using a custom method. */ @@ -674,6 +729,9 @@ typedef CharacterData = */ var healthIcon:Null; + /** + * Optional data about the death animation for the character. + */ var death:Null; /** @@ -734,6 +792,23 @@ typedef CharacterData = * @default false */ var flipX:Null; + + /** + * NOTE: This only applies to animate atlas characters. + * + * Whether to apply the stage matrix, if it was exported from a symbol instance. + * Also positions the Texture Atlas as it displays in Animate. + * Turning this on is only recommended if you prepositioned the character in Animate. + * For other cases, it should be turned off to act similarly to a normal FlxSprite. + */ + var applyStageMatrix:Null; + + /** + * Various settings for the prop. + * Only available for texture atlases. + */ + @:optional + var atlasSettings:funkin.data.stage.StageData.TextureAtlasData; }; /** diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index 3258e5a6f..29a870d6a 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -103,6 +103,10 @@ class PlayerFreeplayDJData var assetPath:String; var animations:Array; + @:optional + @:default(false) + var applyStageMatrix:Bool; + @:optional @:default("BOYFRIEND") var text1:String; @@ -158,11 +162,16 @@ class PlayerFreeplayDJData } public inline function getAssetPath():String - return assetPath; // return Paths.animateAtlas(assetPath); + return assetPath; // return assetPath; public inline function getAnimationsList():Array return animations; + public function useApplyStageMatrix():Bool + { + return applyStageMatrix; + } + public function getFreeplayDJText(index:Int):String { switch (index) @@ -343,6 +352,10 @@ typedef PlayerResultsAnimationData = */ var renderType:String; + @:optional + @:default(false) + var applyStageMatrix:Bool; + @:optional var assetPath:Null; diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index 6e61aba8f..0a82403f1 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -196,7 +196,7 @@ typedef StageDataProp = /** * The animation type to use. - * Options: "sparrow", "packer" + * Options: "sparrow", "packer", "animateatlas" * @default "sparrow" */ @:default("sparrow") @@ -228,6 +228,52 @@ typedef StageDataProp = @:default("#FFFFFF") @:optional var color:String; + + /** + * Various settings for the prop. + * Only available for texture atlases. + */ + @:optional + var atlasSettings:TextureAtlasData; +}; + +typedef TextureAtlasData = +{ + /** + * If true, the texture atlas will behave as if it was exported as an SWF file. + * Notably, this allows MovieClip symbols to play. + */ + @:optional + var swfMode:Bool; + + /** + * If true, filters and masks will be cached when the atlas is loaded, instead of during runtime. + */ + @:optional + var cacheOnLoad:Bool; + + /** + * The filter quality. + * Available values are: HIGH, MEDIUM, LOW, and RUDY. + * + * If you're making an atlas sprite in HScript, you pass an Int instead: + * + * HIGH - 0 + * MEDIUM - 1 + * LOW - 2 + * RUDY - 3 + */ + @:optional + var filterQuality:Int; + + /** + * Whether to apply the stage matrix, if it was exported from a symbol instance. + * Also positions the Texture Atlas as it displays in Animate. + * Turning this on is only recommended if you prepositioned the character in Animate. + * For other cases, it should be turned off to act similarly to a normal FlxSprite. + */ + @:optional + var applyStageMatrix:Bool; }; typedef StageDataCharacter = diff --git a/source/funkin/graphics/FunkinAnimationController.hx b/source/funkin/graphics/FunkinAnimationController.hx new file mode 100644 index 000000000..92805c399 --- /dev/null +++ b/source/funkin/graphics/FunkinAnimationController.hx @@ -0,0 +1,35 @@ +package funkin.graphics; + +import funkin.graphics.FunkinSprite; +import animate.FlxAnimateController; + +class FunkinAnimationController extends FlxAnimateController +{ + /** + * The sprite that this animation controller is attached to. + */ + var _parentSprite:FunkinSprite; + + public function new(sprite:FunkinSprite) + { + super(sprite); + _parentSprite = sprite; + } + + /** + * We override `FlxAnimationController`'s `play` method to account for texture atlases. + */ + public override function play(animName:String, force = false, reversed = false, frame = 0):Void + { + if (animName == null || animName == '') animName = _parentSprite.getDefaultSymbol(); + + if (!_parentSprite.hasAnimation(animName)) + { + // Skip if the animation doesn't exist + trace('Animation ${animName} does not exist!'); + return; + } + + super.play(animName, force, reversed, frame); + } +} diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx index 72fe611b4..a6b39fc87 100644 --- a/source/funkin/graphics/FunkinSprite.hx +++ b/source/funkin/graphics/FunkinSprite.hx @@ -9,20 +9,97 @@ import funkin.graphics.framebuffer.FixedBitmapData; import openfl.display.BitmapData; import flixel.math.FlxRect; import flixel.math.FlxPoint; -import flixel.graphics.frames.FlxFrame.FlxFrameAngle; +import flixel.math.FlxMatrix; +import flixel.graphics.frames.FlxFrame; import flixel.FlxCamera; import openfl.system.System; +import flixel.system.FlxAssets.FlxGraphicAsset; import funkin.FunkinMemory; +import animate.internal.SymbolItem; +import animate.internal.elements.Element; +import animate.internal.elements.AtlasInstance; +import animate.internal.elements.SymbolInstance; +import animate.FlxAnimate; +import animate.FlxAnimateFrames; using StringTools; +typedef AtlasSpriteSettings = +{ + /** + * If true, the texture atlas will behave as if it was exported as an SWF file. + * Notably, this allows MovieClip symbols to play. + */ + @:optional + var swfMode:Bool; + + /** + * If true, filters and masks will be cached when the atlas is loaded, instead of during runtime. + */ + @:optional + var cacheOnLoad:Bool; + + /** + * The filter quality. + * Available values are: HIGH, MEDIUM, LOW, and RUDY. + * + * If you're making an atlas sprite in HScript, you pass an Int instead: + * + * HIGH - 0 + * MEDIUM - 1 + * LOW - 2 + * RUDY - 3 + */ + @:optional + var filterQuality:FilterQuality; + + /** + * Optional, an array of spritemaps for the atlas to load. + */ + @:optional + var spritemaps:Array; + + /** + * Optional, string of the metadata.json contents. + */ + @:optional + var metadataJson:String; + + /** + * Optional, force the cache to use a specific key to index the texture atlas. + */ + @:optional + var cacheKey:String; + + /** + * If true, the texture atlas will use a new slot in the cache. + */ + @:optional + var uniqueInCache:Bool; + + /** + * Optional callback for when a symbol is created. + */ + @:optional + var onSymbolCreate:animate.internal.SymbolItem->Void; + + /** + * Whether to apply the stage matrix, if it was exported from a symbol instance. + * Also positions the Texture Atlas as it displays in Animate. + * Turning this on is only recommended if you prepositioned the character in Animate. + * For other cases, it should be turned off to act similarly to a normal FlxSprite. + */ + @:optional + var applyStageMatrix:Bool; +} + /** * An FlxSprite with additional functionality. * - A more efficient method for creating solid color sprites. * - TODO: Better cache handling for textures. */ @:nullSafety -class FunkinSprite extends FlxSprite +class FunkinSprite extends FlxAnimate { /** * @param x Starting X position @@ -33,6 +110,16 @@ class FunkinSprite extends FlxSprite super(x, y); } + override function initVars():Void + { + super.initVars(); + + var newController:FunkinAnimationController = new FunkinAnimationController(this); + + animation = newController; + anim = newController; + } + /** * Create a new FunkinSprite with a static texture. * @param x The starting X position. @@ -75,6 +162,20 @@ class FunkinSprite extends FlxSprite return sprite; } + /** + * Create a new FunkinSprite with an Adobe Animate texture atlas. + * @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 createTextureAtlas(x:Float = 0.0, y:Float = 0.0, key:String, ?assetLibrary:Null, ?settings:AtlasSpriteSettings):FunkinSprite + { + var sprite:FunkinSprite = new FunkinSprite(x, y); + sprite.loadTextureAtlas(key, assetLibrary ?? "", settings); + return sprite; + } + /** * Load a static image as the sprite's texture. * @param key The key of the texture to load. @@ -83,7 +184,17 @@ class FunkinSprite extends FlxSprite public function loadTexture(key:String):FunkinSprite { var graphicKey:String = Paths.image(key); - if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); + + if (!Assets.exists(graphicKey, IMAGE)) + { + FlxG.log.error('Texture not found, check your path! $graphicKey'); + return this; + } + + if (!FunkinMemory.isTextureCached(graphicKey)) + { + FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); + } loadGraphic(graphicKey); @@ -163,6 +274,63 @@ class FunkinSprite extends FlxSprite return loadBitmapData(inputBitmap); } + /** + * Loads an Adobe Animate texture atlas as the sprite's texture. + * @param key The key of the texture to load. + * @param settings Additional settings for loading the atlas. + * @return This sprite, for chaining. + */ + public function loadTextureAtlas(key:Null, ?assetLibrary:Null, ?settings:AtlasSpriteSettings):FunkinSprite + { + if (key == null) + { + throw 'Null path specified for loadTextureAtlas()!'; + } + + var validatedSettings:AtlasSpriteSettings = + { + swfMode: settings?.swfMode ?? false, + cacheOnLoad: settings?.cacheOnLoad ?? false, + filterQuality: settings?.filterQuality ?? MEDIUM, + spritemaps: settings?.spritemaps ?? null, + metadataJson: settings?.metadataJson ?? null, + cacheKey: settings?.cacheKey ?? null, + uniqueInCache: settings?.uniqueInCache ?? false, + onSymbolCreate: settings?.onSymbolCreate ?? null, + applyStageMatrix: settings?.applyStageMatrix ?? false + }; + + var assetLibrary:String = assetLibrary ?? ""; + var graphicKey:String = ""; + + if (assetLibrary != "") + { + graphicKey = Paths.animateAtlas(key, assetLibrary); + } + else + { + graphicKey = Paths.animateAtlas(key); + } + + // Validate asset path. + if (!Assets.exists('${graphicKey}/Animation.json')) + { + throw 'No Animation.json file exists at the specified path (${graphicKey})'; + } + + this.applyStageMatrix = validatedSettings.applyStageMatrix ?? false; + + frames = FlxAnimateFrames.fromAnimate(graphicKey, validatedSettings.spritemaps, validatedSettings.metadataJson, validatedSettings.cacheKey, + validatedSettings.uniqueInCache, { + swfMode: validatedSettings.swfMode, + cacheOnLoad: validatedSettings.cacheOnLoad, + filterQuality: validatedSettings.filterQuality, + onSymbolCreate: validatedSettings.onSymbolCreate + }); + + return this; + } + /** * Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture. * @param key The key of the texture to load. @@ -171,7 +339,7 @@ class FunkinSprite extends FlxSprite public function loadSparrow(key:String):FunkinSprite { var graphicKey:String = Paths.image(key); - if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); + if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); this.frames = Paths.getSparrowAtlas(key); @@ -186,73 +354,13 @@ class FunkinSprite extends FlxSprite public function loadPacker(key:String):FunkinSprite { var graphicKey:String = Paths.image(key); - if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); + if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); this.frames = Paths.getPackerAtlas(key); return this; } - /** - * Determine whether the texture with the given key is cached. - * @param key The key of the texture to check. - * @return Whether the texture is cached. - */ - public static function isTextureCached(key:String):Bool - { - return FlxG.bitmap.get(key) != null; - } - - @:deprecated("Use FunkinMemory.cacheTexture() instead") - public static function cacheTexture(key:String):Void - { - FunkinMemory.cacheTexture(Paths.image(key)); - } - - @:deprecated("Use FunkinMemory.permanentCacheTexture() instead") - public static function permanentCacheTexture(key:String):Void - { - @:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key)); - } - - @:deprecated("Use FunkinMemory.cacheTexture() instead") - public static function cacheSparrow(key:String):Void - { - FunkinMemory.cacheTexture(Paths.image(key)); - } - - @:deprecated("Use FunkinMemory.cacheTexture() instead") - public static function cachePacker(key:String):Void - { - FunkinMemory.cacheTexture(Paths.image(key)); - } - - @:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead") - public static function preparePurgeCache():Void - { - FunkinMemory.preparePurgeTextureCache(); - } - - @:deprecated("Use FunkinMemory.purgeCache() instead") - public static function purgeCache():Void - { - FunkinMemory.purgeCache(); - } - - static function isGraphicCached(graphic:FlxGraphic):Bool - { - var result = null; - if (graphic == null) return false; - 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; - } - /** * @param id The animation ID to check. * @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations. @@ -266,6 +374,67 @@ class FunkinSprite extends FlxSprite return animData.numFrames > 1; } + /** + * Whether or not this sprite has an animation with the given ID. + * @param id The ID of the animation to check. + */ + public function hasAnimation(id:String):Bool + { + var animationList:Array = this.animation.getNameList(); + if (animationList.contains(id)) + { + return true; + } + else if (this.isAnimate && !animationList.contains(id)) + { + return addAnimationIfMissing(id); + } + + return false; + } + + /** + * Adds an animation if it doesn't exist. + * @param id The animation ID to check. + */ + function addAnimationIfMissing(id:String):Bool + { + @:privateAccess + var symbols:Array = this.library.dictionary.keys().array(); + var frameLabels:Array = listAnimations(); + + if (frameLabels.contains(id)) + { + // Animation exists as a frame label but wasn't added, so we add it + anim.addByFrameLabel(id, id, this.library.frameRate, false); + return true; + } + else if (symbols.contains(id)) + { + // Animation exists as a symbol but wasn't added, so we add it + anim.addBySymbol(id, id, this.library.frameRate, false); + return true; + } + + return false; + } + + /** + * Gets the current animation ID. + */ + public function getCurrentAnimation():String + { + return this.animation.curAnim?.name ?? ''; + } + + /** + * Whether or not the current animation is finished. + */ + public function isAnimationFinished():Bool + { + return this.animation?.finished ?? false; + } + /** * Acts similarly to `makeGraphic`, but with improved memory usage, * at the expense of not being able to paint onto the resulting sprite. @@ -286,6 +455,201 @@ class FunkinSprite extends FlxSprite return this; } + /** + * @return A list of all the animations this sprite has available. + */ + public function listAnimations():Array + { + var frameLabels:Array = getFrameLabelList(); + var animationList:Array = this.animation.getNameList(); + + return frameLabels.concat(animationList); + } + + /** + * TEXTURE ATLAS-EXCLUSIVE FUNCTIONS + * These functions only work if the sprite's texture is an Adobe Animate texture atlas. + * Calling these functions on non-texture atlases will do nothing. + */ + /** + * Gets a list of frame labels from the default timeline. + */ + public function getFrameLabelList():Array + { + if (!this.isAnimate) + { + trace('WARNING: getFrameLabelList() only works texture atlases!'); + return []; + } + + var foundLabels:Array = []; + var mainTimeline:Null = this.library.timeline; + + for (layer in mainTimeline.layers) + { + @:nullSafety(Off) + for (frame in layer.frames) + { + if (frame.name.rtrim() != '') + { + foundLabels.push(frame.name); + } + } + } + + return foundLabels; + } + + /** + * Gets a frame label by its name. + * @param name The name of the frame label to retrieve. + * @return The frame label, or null if it doesn't exist. + */ + public function getFrameLabel(name:String):Null + { + if (!this.isAnimate) + { + trace('WARNING: getFrameLabel() only works texture atlases!'); + return null; + } + + for (layer in this.timeline.layers) + { + @:nullSafety(Off) + for (frame in layer.frames) + { + if (frame.name == name) + { + return frame; + } + } + } + return null; + } + + /** + * Returns the default symbol in the atlas. + */ + public function getDefaultSymbol():String + { + if (!this.isAnimate) + { + trace('WARNING: getDefaultSymbol() only works texture atlases!'); + return ''; + } + + return library.timeline.name; + } + + /** + * Replaces the graphic of a symbol in the atlas. + * @param symbol The symbol to replace. + * @param graphic The new graphic to use. + * @param adjustScale Whether to adjust the scale of new frame to match the old one. + */ + public function replaceSymbolGraphic(symbol:String, ?graphic:Null, ?adjustScale:Bool = true):Void + { + if (!this.isAnimate) + { + trace('WARNING: replaceSymbolGraphic() only works texture atlases!'); + return; + } + + var elements:Array = getSymbolElements(symbol); + + for (element in elements) + { + var atlasInstance:AtlasInstance = element.toAtlasInstance(); + var frame:Null = graphic != null ? FlxG.bitmap.add(graphic).imageFrame.frame : null; + + atlasInstance.replaceFrame(frame, adjustScale); + element = atlasInstance; + } + } + + /** + * Returns the first element of a symbol in the atlas. + * @param symbol The symbol to get elements from. + * @return The first element of the symbol. WARNING: Can be null. + */ + public function getFirstElement(symbol:String):Null + { + if (!this.isAnimate) + { + trace('WARNING: getFirstElement() only works texture atlases!'); + return null; + } + + var symbolElements:Array = getSymbolElements(symbol); + return symbolElements.length > 0 ? symbolElements[0] : null; + } + + /** + * Returns the elements of a symbol in the atlas. + * @param symbol The symbol to get elements from. + */ + public function getSymbolElements(symbol:String):Array + { + if (!this.isAnimate) + { + trace('WARNING: getSymbolElements() only works texture atlases!'); + return []; + } + + var symbolInstance:Null = this.library.getSymbol(symbol); + + if (symbolInstance == null) + { + throw 'Symbol not found in atlas: ${symbol}'; + return []; + } + + var elements:Array = symbolInstance.timeline.getElementsAtIndex(0); + + if (elements?.length == 0) + { + trace('WARNING: No Atlas Elements found for "$symbol" symbol.'); + } + + return elements ?? []; + } + + /** + * Scales an element by a certain multiplier. + * @param element The element to scale. + * @param scale The scale multiplier. + * @param positionOffset The offset to apply to `tx` and `ty` after scaling. + * (Or in other words, the position of the element.) + */ + public function scaleElement(element:Element, scale:Float, positionOffset:Float = 0, scaleEverything:Bool = false):Void + { + if (!this.isAnimate) + { + trace('WARNING: scaleElement() only works texture atlases!'); + return; + } + + var elementMatrix:FlxMatrix = element.matrix; + + if (scaleEverything) + { + elementMatrix.scale(scale, scale); + return; + } + + var symbolInstance:SymbolInstance = element.parentFrame.convertToSymbol(0, 1); + var transformPoint:FlxPoint = symbolInstance.transformationPoint; + + elementMatrix.a += scale; + elementMatrix.d += scale; + + elementMatrix.tx -= transformPoint.x * scale; + elementMatrix.ty -= transformPoint.y * scale; + + elementMatrix.tx -= positionOffset; + elementMatrix.ty -= positionOffset; + } + /** * Ensure scale is applied when cloning a sprite.R * The default `clone()` method acts kinda weird TBH. @@ -325,77 +689,10 @@ class FunkinSprite extends FlxSprite return _rect; } - /** - * Returns the screen position of this object. - * - * @param result Optional arg for the returning point - * @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used. - * @return The screen position of this object. - */ - public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + override function preparePixelPerfectMatrix(matrix:FlxMatrix) { - if (result == null) result = FlxPoint.get(); - - if (camera == null) camera = FlxG.camera; - - result.set(x, y); - if (pixelPerfectPosition) - { - _rect.width = _rect.width / this.scale.x; - _rect.height = _rect.height / this.scale.y; - _rect.x = _rect.x / this.scale.x; - _rect.y = _rect.y / this.scale.y; - _rect.round(); - _rect.x = _rect.x * this.scale.x; - _rect.y = _rect.y * this.scale.y; - _rect.width = _rect.width * this.scale.x; - _rect.height = _rect.height * this.scale.y; - } - - return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y); - } - - override function drawSimple(camera:FlxCamera):Void - { - getScreenPosition(_point, camera).subtractPoint(offset); - if (isPixelPerfectRender(camera)) - { - _point.x = _point.x / this.scale.x; - _point.y = _point.y / this.scale.y; - _point.round(); - - _point.x = _point.x * this.scale.x; - _point.y = _point.y * this.scale.y; - } - - _point.copyToFlash(_flashPoint); - camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing); - } - - override function drawComplex(camera:FlxCamera):Void - { - _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY()); - _matrix.translate(-origin.x, -origin.y); - _matrix.scale(scale.x, scale.y); - - if (bakedRotationAngle <= 0) - { - updateTrig(); - - if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle); - } - - getScreenPosition(_point, camera).subtractPoint(offset); - _point.add(origin.x, origin.y); - _matrix.translate(_point.x, _point.y); - - if (isPixelPerfectRender(camera)) - { - _matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x; - _matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y; - } - - camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader); + matrix.tx = Math.round(matrix.tx / this.scale.x) * this.scale.x; + matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y; } public override function destroy():Void diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx deleted file mode 100644 index c4e4f31e5..000000000 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ /dev/null @@ -1,414 +0,0 @@ -package funkin.graphics.adobeanimate; - -import flixel.util.FlxSignal.FlxTypedSignal; -import flxanimate.FlxAnimate; -import flxanimate.FlxAnimate.Settings; -import flixel.graphics.frames.FlxFrame; -import flixel.system.FlxAssets.FlxGraphicAsset; -import flixel.math.FlxPoint; -import flxanimate.animate.FlxKeyFrame; - -/** - * A sprite which provides convenience functions for rendering a texture atlas with animations. - */ -@:nullSafety -class FlxAtlasSprite extends FlxAnimate -{ - static final SETTINGS:Settings = - { - // ?ButtonSettings:Map, - FrameRate: 24.0, - Reversed: false, - // ?OnComplete:Void -> Void, - ShowPivot: false, - Antialiasing: true, - ScrollFactor: null, - // Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset - }; - - /** - * Signal dispatched when an animation advances to the next frame. - */ - public var onAnimationFrame:FlxTypedSignalInt->Void> = new FlxTypedSignal(); - - /** - * Signal dispatched when a non-looping animation finishes playing. - */ - public var onAnimationComplete:FlxTypedSignalVoid> = new FlxTypedSignal(); - - /** - * Signal dispatched when a looping animation finishes playing. - */ - public var onAnimationLoop:FlxTypedSignalVoid> = new FlxTypedSignal(); - - var currentAnimation:String = ''; - - var canPlayOtherAnims:Bool = true; - - @:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0? - public function new(x:Float, y:Float, ?path:String, ?settings:Settings, ?pathSafety:Bool = true) - { - if (settings == null) settings = SETTINGS; - - if (path == null && pathSafety) throw 'Null path specified for FlxAtlasSprite!'; - - // Validate asset path. - if (!Assets.exists('${path}/Animation.json') - && pathSafety) throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})'; - - super(x, y, path, settings); - - if (this.anim.stageInstance == null && pathSafety) throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?'; - - onAnimationComplete.add(cleanupAnimation); - - // This defaults the sprite to play the first animation in the atlas, - // then pauses it. This ensures symbols are intialized properly. - if (this.anim.curInstance != null) - { - this.anim.play(''); - this.anim.pause(); - } - - this.anim.onComplete.add(_onAnimationComplete); - this.anim.onFrame.add(_onAnimationFrame); - } - - /** - * @return A list of all the animations this sprite has available. - */ - public function listAnimations():Array - { - var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name]; - if (mainSymbol == null) - { - FlxG.log.error('FlxAtlasSprite does not have its main symbol!'); - return []; - } - return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull(); - } - - /** - * @param id A string ID of the animation. - * @return Whether the animation was found on this sprite. - */ - public function hasAnimation(id:String):Bool - { - return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id); - } - - /** - * @return The current animation being played. - */ - public function getCurrentAnimation():String - { - return this.currentAnimation; - } - - var _completeAnim:Bool = false; - - var fr:Null = null; - - var looping:Bool = false; - - public var ignoreExclusionPref:Array = []; - - /** - * Plays an animation. - * @param id A string ID of the animation to play. - * @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 - * @param loop Whether to loop the animation - * @param startFrame The frame to start the animation on - * NOTE: `loop` and `ignoreOther` are not compatible with each other! - */ - public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void - { - // Skip if not allowed to play animations. - if ((!canPlayOtherAnims)) - { - if (this.currentAnimation == id && restart) {} - else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0) - { - var detected:Bool = false; - for (entry in ignoreExclusionPref) - { - if (StringTools.startsWith(id, entry)) - { - detected = true; - break; - } - } - if (!detected) return; - } - else - return; - } - - if (anim == null) return; - - if (id == null || id == '') id = this.currentAnimation; - - if (this.currentAnimation == id && !restart) - { - if (!anim.isPlaying) - { - if (fr != null) anim.curFrame = fr.index + startFrame; - else - anim.curFrame = startFrame; - - // Resume animation if it's paused. - anim.resume(); - } - - return; - } - else if (!hasAnimation(id)) - { - // Skip if the animation doesn't exist - trace('Animation ' + id + ' not found'); - return; - } - - this.currentAnimation = id; - anim.onComplete.removeAll(); - anim.onComplete.add(function() { - _onAnimationComplete(); - }); - - looping = loop; - - // Prevent other animations from playing if `ignoreOther` is true. - if (ignoreOther) canPlayOtherAnims = false; - - // Move to the first frame of the animation. - // goToFrameLabel(id); - // trace('Playing animation $id'); - if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null)) - { - this.anim.play(id, restart, false, startFrame); - - this.currentAnimation = anim.curSymbol.name; - - fr = null; - } - var frameLabelNames = getFrameLabelNames(); - // Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings! - if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1) - { - goToFrameLabel(id); - fr = anim.getFrameLabel(id); - anim.curFrame += startFrame; - // Resume animation if it's paused. - anim.resume(); - } - } - - override public function update(elapsed:Float):Void - { - super.update(elapsed); - } - - /** - * Returns true if the animation has finished playing. - * @return Whether the animation has finished playing. - */ - public function isAnimationFinished():Bool - { - return isLoopComplete(); - } - - /** - * Returns true if the animation has reached the last frame. - * Can be true even if animation is configured to loop. - * @return Whether the animation has reached the last frame. - */ - public function isLoopComplete():Bool - { - if (this.anim == null) return false; - if (!this.anim.isPlaying) return false; - - if (fr != null) - { - var curFrame = anim.curFrame; - - var startFrame = fr.index; - var endFrame = (fr.index + fr.duration); - - return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame); - } - - return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1)); - } - - /** - * Stops the current animation. - */ - public function stopAnimation():Void - { - if (this.currentAnimation == null) return; - - this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation)); - - goToFrameIndex(0); - } - - function addFrameCallback(label:String, callback:Void->Void):Void - { - var frameLabel = this.anim.getFrameLabel(label); - frameLabel.add(callback); - } - - function goToFrameLabel(label:String):Void - { - this.anim.goToFrameLabel(label); - } - - function getFrameLabelNames(?layer:haxe.extern.EitherType):Null> - { - var labels = this.anim.getFrameLabels(layer); - var array = []; - for (label in labels) - { - array.push(label.name); - } - - return array; - } - - function getNextFrameLabel(label:String):String - { - return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length]; - } - - function getLabelIndex(label:String):Int - { - return listAnimations().indexOf(label); - } - - function goToFrameIndex(index:Int):Void - { - this.anim.curFrame = index; - } - - public function cleanupAnimation(_:String):Void - { - canPlayOtherAnims = true; - // this.currentAnimation = null; - this.anim.pause(); - } - - function _onAnimationFrame(frame:Int):Void - { - if (currentAnimation != null) - { - onAnimationFrame.dispatch(currentAnimation, frame); - - if (isLoopComplete()) - { - anim.pause(); - - if (looping) - { - anim.curFrame = (fr != null) ? fr.index : 0; - anim.resume(); - _onAnimationLoop(); - } - else if (fr != null && anim.curFrame != anim.length - 1) - { - anim.curFrame--; - _onAnimationComplete(); - } - } - } - } - - function _onAnimationComplete():Void - { - if (currentAnimation != null) - { - onAnimationComplete.dispatch(currentAnimation); - } - else - { - onAnimationComplete.dispatch(''); - } - } - - function _onAnimationLoop():Void - { - if (currentAnimation != null) - { - onAnimationLoop.dispatch(currentAnimation); - } - else - { - onAnimationLoop.dispatch(''); - } - } - - var prevFrames:Map = []; - - public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void - { - var cond = false; - - if (graphic == null) cond = true; - else - { - if ((graphic is String)) cond = !Assets.exists(graphic) - else - cond = false; - } - if (cond) - { - var prevFrame:Null = prevFrames.get(index); - if (prevFrame == null) return; - - prevFrame.copyTo(frames.getByIndex(index)); - return; - } - - var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo(); - prevFrames.set(index, prevFrame); - - @:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed - var frame = FlxG.bitmap.add(graphic).imageFrame.frame; - frame.copyTo(frames.getByIndex(index)); - - // Additional sizing fix. - @:privateAccess - if (true) - { - var frame = frames.getByIndex(index); - frame.tileMatrix.a = prevFrame.frame.width / frame.frame.width; - frame.tileMatrix.d = prevFrame.frame.height / frame.frame.height; - } - } - - public function getBasePosition():Null - { - // var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty); - var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty); - var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0); - if (firstElement == null) return instancePos; - var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty); - - return instancePos + firstElementPos; - } - - public function getPivotPosition():Null - { - return anim.curInstance.symbol.transformationPoint; - } - - public override function destroy():Void - { - for (prevFrameId in prevFrames.keys()) - { - replaceFrameGraphic(prevFrameId, null); - } - - super.destroy(); - } -} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 2135aec6c..4e26ad33d 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -274,6 +274,11 @@ class PolymodHandler Polymod.addImportAlias('funkin.play.character.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser); Polymod.addImportAlias('funkin.play.character.CharacterData.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser); + // `FlxAtlasSprite` was merged into `FunkinSprite` and then removed. + // We add the import alias here so mods don't error out as much. + Polymod.addImportAlias('funkin.graphics.adobeanimate.FlxAtlasSprite', funkin.graphics.FunkinSprite); + Polymod.addImportAlias('funkin.modding.base.ScriptedFlxAtlasSprite', funkin.graphics.ScriptedFunkinSprite); + // `funkin.util.FileUtil` has unrestricted access to the file system. Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed); diff --git a/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx b/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx deleted file mode 100644 index e9ffd450d..000000000 --- a/source/funkin/modding/base/ScriptedFlxAtlasSprite.hx +++ /dev/null @@ -1,8 +0,0 @@ -package funkin.modding.base; - -/** - * A script that can be tied to an FlxAtlasSprite - * Create a scripted class that extends FlxAtlasSprite to use this. - */ -@:hscriptClass -class ScriptedFlxAtlasSprite extends funkin.graphics.adobeanimate.FlxAtlasSprite implements HScriptedClass {} diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 6acc1d15e..335442616 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -20,11 +20,9 @@ import funkin.audio.FunkinSound; import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.song.SongRegistry; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.LeftMaskShader; -import funkin.modding.base.ScriptedFlxAtlasSprite; import funkin.play.components.ClearPercentCounter; import funkin.play.components.TallyCounter; import funkin.play.scoring.Scoring; @@ -78,7 +76,7 @@ class ResultState extends MusicBeatSubState var characterAtlasAnimations:Array< { - sprite:FlxAtlasSprite, + sprite:FunkinSprite, delay:Float, forceLoop:Bool, startFrameLabel:String, @@ -193,7 +191,7 @@ class ResultState extends MusicBeatSubState add(soundSystem); // Fetch playable character data. Default to BF on the results screen if we can't find it. - playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); + playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId) ?? params.characterId; playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); trace('Got playable character: ${playerCharacter?.getName()}'); @@ -222,17 +220,22 @@ class ResultState extends MusicBeatSubState { case 'animateatlas': @:nullSafety(Off) - var animation:FlxAtlasSprite = null; + var animation:FunkinSprite = null; var xPos = offsets[0] + (FullScreenScaleMode.gameCutoutSize.x / 2); var yPos = offsets[1]; - if (animData.scriptClass != null) animation = ScriptedFlxAtlasSprite.init(animData.scriptClass, xPos, yPos); + if (animData.scriptClass != null) animation = ScriptedFunkinSprite.init(animData.scriptClass, xPos, yPos); else - animation = new FlxAtlasSprite(xPos, yPos, Paths.animateAtlas(animPath, animLibrary)); + animation = FunkinSprite.createTextureAtlas(xPos, yPos, animPath, animLibrary); if (animation == null) continue; + if (animData?.applyStageMatrix ?? false) + { + animation.applyStageMatrix = true; + } + animation.zIndex = animData.zIndex ?? 500; animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0); @@ -240,7 +243,7 @@ class ResultState extends MusicBeatSubState if (!(animData.looped ?? true)) { // Animation is not looped. - animation.onAnimationComplete.add((_name:String) -> { + animation.anim.onFinish.add((_name:String) -> { if (animation != null) { animation.anim.pause(); @@ -249,23 +252,24 @@ class ResultState extends MusicBeatSubState } else if (animData.loopFrameLabel != null) { - animation.onAnimationComplete.add((_name:String) -> { + animation.anim.onFinish.add((_name:String) -> { if (animation != null) { - animation.playAnimation(animData.loopFrameLabel ?? '', true, false, true); // unpauses this anim, since it's on PlayOnce! + animation.anim.play(animData.loopFrameLabel ?? '', true); // unpauses this anim, since it's on PlayOnce! + animation.anim.curAnim.looped = true; } }); } else if (animData.loopFrame != null) { - animation.onAnimationComplete.add((_name:String) -> { + animation.anim.onFinish.add((_name:String) -> { if (animation != null) { - animation.anim.curFrame = animData.loopFrame ?? 0; - animation.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.anim.play("", true, false, animData.loopFrame ?? 0); // unpauses this anim, since it's on PlayOnce! } }); } + // Hide until ready to play. animation.visible = false; // Queue to play. @@ -660,7 +664,7 @@ class ResultState extends MusicBeatSubState new FlxTimer().start(atlas.delay, _ -> { if (atlas.sprite == null) return; atlas.sprite.visible = true; - atlas.sprite.playAnimation(atlas.startFrameLabel); + atlas.sprite.anim.play(atlas.startFrameLabel); if (atlas.sound != "") { var sndPath:String = Paths.stripLibrary(atlas.sound); diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 60792bf02..db862ae99 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -1,35 +1,11 @@ package funkin.play.character; -import flixel.animation.FlxAnimationController; -import flixel.FlxCamera; -import flixel.FlxSprite; -import flixel.graphics.frames.FlxFrame; -import flixel.graphics.frames.FlxFramesCollection; -import flixel.math.FlxMath; -import flixel.math.FlxPoint.FlxCallbackPoint; -import flixel.math.FlxPoint; -import flixel.math.FlxRect; import funkin.graphics.FunkinSprite; -import flixel.system.FlxAssets.FlxGraphicAsset; -import flixel.util.FlxColor; -import flixel.util.FlxDestroyUtil; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.util.assets.FlxAnimationUtil; import funkin.modding.events.ScriptEvent; +import funkin.data.animation.AnimationData; import funkin.data.character.CharacterData.CharacterRenderType; -import flixel.util.FlxDirectionFlags; -import openfl.display.BitmapData; -import openfl.display.BlendMode; - -/** - * Individual animation data for an AnimateAtlasCharacter. - */ -typedef AnimateAtlasAnimation = -{ - name:String, - prefix:String, - offsets:Null>, - looped:Bool, -} +import flixel.math.FlxPoint; /** * An AnimateAtlasCharacter is a Character which is rendered by @@ -40,39 +16,13 @@ typedef AnimateAtlasAnimation = */ class AnimateAtlasCharacter extends BaseCharacter { - // BaseCharacter extends FlxSprite but we can't make it also extend FlxAtlasSprite UGH - // I basically copied the code from FlxSpriteGroup to make the FlxAtlasSprite a "child" of this class - var mainSprite:FlxAtlasSprite; - - var _skipTransformChildren:Bool = false; - - var animations:Map = new Map(); - var currentAnimName:Null = null; - var animFinished:Bool = false; + var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0); public function new(id:String) { super(id, CharacterRenderType.AnimateAtlas); } - override function initVars():Void - { - // this.flixelType = SPRITEGROUP; - - // TODO: Make `animation` a stub that redirects calls to `mainSprite`? - animation = new FlxAnimationController(this); - - offset = new FlxCallbackPoint(offsetCallback); - origin = new FlxCallbackPoint(originCallback); - scale = new FlxCallbackPoint(scaleCallback); - scrollFactor = new FlxCallbackPoint(scrollFactorCallback); - - scale.set(1, 1); - scrollFactor.set(1, 1); - - initMotionVars(); - } - override function onCreate(event:ScriptEvent):Void { // Display a custom scope for debugging purposes. @@ -83,756 +33,71 @@ class AnimateAtlasCharacter extends BaseCharacter try { trace('Loading assets for Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0")); - var atlasSprite:FlxAtlasSprite = loadAtlasSprite(); - setSprite(atlasSprite); - + loadAtlas(); loadAnimations(); } catch (e) { - throw "Exception thrown while building FlxAtlasSprite: " + e; + throw "Exception thrown while building sprite: " + e; } + trace('[ATLASCHAR] Successfully loaded texture atlas for ${characterId} with ${_data.animations.length} animations.'); super.onCreate(event); + + originalSizes.set(this.width, this.height); } - public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void - { - var correctName = correctAnimationName(name); - if (correctName == null) - { - trace('$characterName Could not find Atlas animation: ' + name); - return; - } - - var animData = getAnimationData(correctName); - currentAnimName = correctName; - var prefix:String = animData.prefix; - if (prefix == null) prefix = correctName; - var loop:Bool = animData.looped; - - this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop); - } - - public override function hasAnimation(name:String):Bool - { - return getAnimationData(name) != null; - } - - /** - * Returns true if the animation has finished playing. - * Never true if animation is configured to loop. - */ - public override function isAnimationFinished():Bool - { - return mainSprite?.isAnimationFinished() ?? false; - } - - function loadAtlasSprite():FlxAtlasSprite + function loadAtlas() { trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.'); + var assetLibrary:String = Paths.getLibrary(_data.assetPath); + var assetPath:String = Paths.stripLibrary(_data.assetPath); - var animLibrary:String = Paths.getLibrary(_data.assetPath); - var animPath:String = Paths.stripLibrary(_data.assetPath); - var assetPath:String = Paths.animateAtlas(animPath, animLibrary); + loadTextureAtlas(assetPath, assetLibrary, cast _data.atlasSettings); - var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, assetPath); - - // sprite.onAnimationComplete.removeAll(); - sprite.onAnimationComplete.add(this.onAnimationFinished); - - return sprite; - } - - override function onAnimationFinished(prefix:String):Void - { - super.onAnimationFinished(prefix); - - if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX) - && hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX)) + if (_data.isPixel) { - playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX); - } - - if (getAnimationData() != null && getAnimationData().looped) - { - if (StringTools.endsWith(prefix, "-hold")) trace(prefix); - playAnimation(prefix, true, false); + this.isPixel = true; + this.antialiasing = false; } else { - // Make the game hold on the last frame. - this.mainSprite.cleanupAnimation(prefix); - // currentAnimName = null; - - // Fallback to idle! - // playAnimation('idle', true, false); + this.isPixel = false; + this.antialiasing = true; } + + this.setScale(_data.scale); } - function setSprite(sprite:FlxAtlasSprite):Void + function loadAnimations() { - trace('[ATLASCHAR] Applying sprite properties to ${characterId}'); + trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}'); - this.mainSprite = sprite; + FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations); - mainSprite.ignoreExclusionPref = ["sing"]; - - // This forces the atlas to recalcuate its width and height - this.mainSprite.alpha = 0.0001; - this.mainSprite.draw(); - this.mainSprite.alpha = 1.0; - - var feetPos:FlxPoint = feetPosition; - this.updateHitbox(); - - sprite.x = this.x; - sprite.y = this.y; - sprite.alpha *= alpha; - sprite.flipX = flipX; - sprite.flipY = flipY; - sprite.scrollFactor.copyFrom(scrollFactor); - sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null - - if (clipRect != null) clipRectTransform(sprite, clipRect); - } - - function loadAnimations():Void - { - trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}'); - - var animData:Array = cast _data.animations; - - for (anim in animData) + for (anim in _data.animations) { - // Validate the animation before adding. - var prefix = anim.prefix; - if (!this.mainSprite.hasAnimation(prefix)) + if (anim.offsets == null) { - FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); - trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); - continue; + setAnimationOffsets(anim.name, 0, 0); + } + else + { + setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]); } - animations.set(anim.name, anim); - trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}'); } - trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}'); - } - - public override function getCurrentAnimation():String - { - // return this.mainSprite.getCurrentAnimation(); - return currentAnimName; - } - - function getAnimationData(name:String = null):AnimateAtlasAnimation - { - if (name == null) name = getCurrentAnimation(); - return animations.get(name); - } - - // - // - // Code copied from FlxSpriteGroup - // - // - - /** - * Handy function that allows you to quickly transform one property of sprites in this group at a time. - * - * @param callback Function to transform the sprites. Example: - * `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }` - * @param value Value which will passed to lambda function. - */ - @:generic - public function transformChildren(callback:FlxAtlasSprite->V->Void, value:V):Void - { - if (_skipTransformChildren || this.mainSprite == null) return; - - callback(this.mainSprite, value); - } - - /** - * Calls `kill()` on the group's members and then on the group itself. - * You can revive this group later via `revive()` after this. - */ - public override function kill():Void - { - _skipTransformChildren = true; - super.kill(); - _skipTransformChildren = false; - if (this.mainSprite != null) - { - this.mainSprite.kill(); - this.mainSprite = null; - } - } - - /** - * Revives the group. - */ - public override function revive():Void - { - _skipTransformChildren = true; - super.revive(); // calls set_exists and set_alive - _skipTransformChildren = false; - this.mainSprite.revive(); - } - - /** - * **WARNING:** A destroyed `FlxBasic` can't be used anymore. - * It may even cause crashes if it is still part of a group or state. - * You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later. - * - * This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects). - * - * Override this function to `null` out variables manually or call `destroy()` on class members if necessary. - * Don't forget to call `super.destroy()`! - */ - public override function destroy():Void - { - // normally don't have to destroy FlxPoints, but these are FlxCallbackPoints! - offset = FlxDestroyUtil.destroy(offset); - origin = FlxDestroyUtil.destroy(origin); - scale = FlxDestroyUtil.destroy(scale); - scrollFactor = FlxDestroyUtil.destroy(scrollFactor); - - this.mainSprite = FlxDestroyUtil.destroy(this.mainSprite); - - super.destroy(); - } - - /** - * Check and see if any sprite in this group is currently on screen. - * - * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. - * @return Whether the object is on screen or not. - */ - public override function isOnScreen(?camera:FlxCamera):Bool - { - if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible && this.mainSprite.isOnScreen(camera)) return true; - - return false; - } - - /** - * Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group. - * - * @param Point The point in world space you want to check. - * @param InScreenSpace Whether to take scroll factors into account when checking for overlap. - * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. - * @return Whether or not the point overlaps this group. - */ - public override function overlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, camera:FlxCamera = null):Bool - { - var result:Bool = false; - result = this.mainSprite.overlapsPoint(point, inScreenSpace, camera); - return result; - } - - /** - * Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels. - * This check is ALWAYS made in screen space, and always takes scroll factors into account. - * - * @param Point The point in world space you want to check. - * @param Mask Used in the pixel hit test to determine what counts as solid. - * @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera. - * @return Whether or not the point overlaps this object. - */ - public override function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, Camera:FlxCamera = null):Bool - { - var result:Bool = false; - if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible) - { - result = this.mainSprite.pixelsOverlapPoint(point, Mask, Camera); - } - return result; - } - - public override function update(elapsed:Float):Void - { - this.mainSprite.update(elapsed); - - if (moves) updateMotion(elapsed); - } - - public override function draw():Void - { - this.mainSprite.draw(); - - #if FLX_DEBUG - if (FlxG.debugger.drawDebug) drawDebug(); - #end - } - - inline function xTransform(sprite:FlxSprite, x:Float):Void - sprite.x += x; // addition - - inline function yTransform(sprite:FlxSprite, y:Float):Void - sprite.y += y; // addition - - inline function angleTransform(sprite:FlxSprite, angle:Float):Void - sprite.angle += angle; // addition - - inline function alphaTransform(sprite:FlxSprite, alpha:Float):Void - { - if (sprite.alpha != 0 || alpha == 0) - { - sprite.alpha *= alpha; // multiplication - } - else - { - sprite.alpha = 1 / alpha; // direct set to avoid stuck sprites - } - } - - inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void - sprite.alpha = alpha; // direct set - - inline function facingTransform(sprite:FlxSprite, facing:FlxDirectionFlags):Void - sprite.facing = facing; - - inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void - sprite.flipX = flipX; - - inline function flipYTransform(sprite:FlxSprite, flipY:Bool):Void - sprite.flipY = flipY; - - inline function movesTransform(sprite:FlxSprite, moves:Bool):Void - sprite.moves = moves; - - inline function pixelPerfectTransform(sprite:FlxSprite, pixelPerfect:Bool):Void - sprite.pixelPerfectRender = pixelPerfect; - - inline function gColorTransform(sprite:FlxSprite, color:Int):Void - sprite.color = color; - - inline function blendTransform(sprite:FlxSprite, blend:BlendMode):Void - sprite.blend = blend; - - inline function immovableTransform(sprite:FlxSprite, immovable:Bool):Void - sprite.immovable = immovable; - - inline function visibleTransform(sprite:FlxSprite, visible:Bool):Void - sprite.visible = visible; - - inline function activeTransform(sprite:FlxSprite, active:Bool):Void - sprite.active = active; - - inline function solidTransform(sprite:FlxSprite, solid:Bool):Void - sprite.solid = solid; - - inline function aliveTransform(sprite:FlxSprite, alive:Bool):Void - sprite.alive = alive; - - inline function existsTransform(sprite:FlxSprite, exists:Bool):Void - sprite.exists = exists; - - inline function cameraTransform(sprite:FlxSprite, camera:FlxCamera):Void - sprite.camera = camera; - - inline function camerasTransform(sprite:FlxSprite, cameras:Array):Void - sprite.cameras = cameras; - - inline function offsetTransform(sprite:FlxSprite, offset:FlxPoint):Void - sprite.offset.copyFrom(offset); - - inline function originTransform(sprite:FlxSprite, origin:FlxPoint):Void - sprite.origin.copyFrom(origin); - - inline function scaleTransform(sprite:FlxSprite, scale:FlxPoint):Void - sprite.scale.copyFrom(scale); - - inline function scrollFactorTransform(sprite:FlxSprite, scrollFactor:FlxPoint):Void - sprite.scrollFactor.copyFrom(scrollFactor); - - inline function clipRectTransform(sprite:FlxSprite, clipRect:FlxRect):Void - { - if (clipRect == null) - { - sprite.clipRect = null; - } - else - { - sprite.clipRect = FlxRect.get(clipRect.x - sprite.x + x, clipRect.y - sprite.y + y, clipRect.width, clipRect.height); - } - } - - var resS:FlxPoint = new FlxPoint(); - - /** - * Reset the character so it can be used at the start of the level. - * Call this when restarting the level. - */ - override public function resetCharacter(resetCamera:Bool = true):Void - { - trace("RESETTING ATLAS " + characterName); - - // Reset the animation offsets. This will modify x and y to be the absolute position of the character. - // this.animOffsets = [0, 0]; - - // Now we can set the x and y to be their original values without having to account for animOffsets. - this.resetPosition(); - mainSprite.setPosition(originalPosition.x, originalPosition.y); - - // Then reapply animOffsets... - // applyAnimationOffsets(getCurrentAnimation()); - - // Make sure we are playing the idle animation - // ...then update the hitbox so that this.width and this.height are correct. - - mainSprite.scale.set(1, 1); - mainSprite.alpha = 0.0001; - mainSprite.width = 0; - mainSprite.height = 0; - this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song. - - mainSprite.draw(); // refresh frame - - if (resS.x == 0) - { - resS.x = mainSprite.width; // clunky bizz - resS.y = mainSprite.height; - } - - mainSprite.alpha = alpha; - - mainSprite.width = resS.x; - mainSprite.height = resS.y; - frameWidth = 0; - frameHeight = 0; - - scaleCallback(scale); - this.updateHitbox(); - - // Reset the camera focus point while we're at it. - if (resetCamera) this.resetCameraFocusPoint(); - } - - inline function offsetCallback(offset:FlxPoint):Void - transformChildren(offsetTransform, offset); - - inline function originCallback(origin:FlxPoint):Void - transformChildren(originTransform, origin); - - inline function scaleCallback(scale:FlxPoint):Void - transformChildren(scaleTransform, scale); - - inline function scrollFactorCallback(scrollFactor:FlxPoint):Void - transformChildren(scrollFactorTransform, scrollFactor); - - override function set_camera(value:FlxCamera):FlxCamera - { - if (camera != value) transformChildren(cameraTransform, value); - return super.set_camera(value); - } - - override function set_cameras(value:Array):Array - { - if (cameras != value) transformChildren(camerasTransform, value); - return super.set_cameras(value); - } - - override function set_exists(value:Bool):Bool - { - if (exists != value) transformChildren(existsTransform, value); - return super.set_exists(value); - } - - override function set_visible(value:Bool):Bool - { - if (exists && visible != value) transformChildren(visibleTransform, value); - return super.set_visible(value); - } - - override function set_active(value:Bool):Bool - { - if (exists && active != value) transformChildren(activeTransform, value); - return super.set_active(value); - } - - override function set_alive(value:Bool):Bool - { - if (alive != value) transformChildren(aliveTransform, value); - return super.set_alive(value); - } - - override function set_x(value:Float):Float - { - if (!exists || x == value) return x; // early return (no need to transform) - - transformChildren(xTransform, value - x); // offset - return x = value; - } - - override function set_y(value:Float):Float - { - if (exists && y != value) transformChildren(yTransform, value - y); // offset - return y = value; - } - - override function set_angle(value:Float):Float - { - if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset - return angle = value; - } - - override function set_alpha(value:Float):Float - { - value = value.clamp(0, 1); - - if (exists && alpha != value) - { - transformChildren(directAlphaTransform, value); - } - return alpha = value; - } - - override function set_facing(value:FlxDirectionFlags):FlxDirectionFlags - { - if (exists && facing != value) transformChildren(facingTransform, value); - return facing = value; - } - - override function set_flipX(value:Bool):Bool - { - if (exists && flipX != value) transformChildren(flipXTransform, value); - return flipX = value; - } - - override function set_flipY(value:Bool):Bool - { - if (exists && flipY != value) transformChildren(flipYTransform, value); - return flipY = value; - } - - override function set_moves(value:Bool):Bool - { - if (exists && moves != value) transformChildren(movesTransform, value); - return moves = value; - } - - override function set_immovable(value:Bool):Bool - { - if (exists && immovable != value) transformChildren(immovableTransform, value); - return immovable = value; - } - - override function set_solid(value:Bool):Bool - { - if (exists && solid != value) transformChildren(solidTransform, value); - return super.set_solid(value); - } - - override function set_color(value:Int):Int - { - if (exists && color != value) transformChildren(gColorTransform, value); - return color = value; - } - - override function set_blend(value:BlendMode):BlendMode - { - if (exists && blend != value) transformChildren(blendTransform, value); - return blend = value; - } - - override function set_clipRect(rect:FlxRect):FlxRect - { - if (exists) transformChildren(clipRectTransform, rect); - return super.set_clipRect(rect); - } - - override function set_pixelPerfectRender(value:Bool):Bool - { - if (exists && pixelPerfectRender != value) transformChildren(pixelPerfectTransform, value); - return super.set_pixelPerfectRender(value); - } - - override function set_width(value:Float):Float - { - return value; + var animNames = this.anim.getNameList(); + trace('[ATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); } override function get_width():Float { - if (this.mainSprite == null) return 0; - - return this.mainSprite.width; - } - - /** - * Returns the left-most position of the left-most member. - * If there are no members, x is returned. - * - * @since 5.0.0 - * @return the left-most position of the left-most member - */ - public function findMinX():Float - { - return this.mainSprite == null ? x : findMinXHelper(); - } - - function findMinXHelper():Float - { - return this.mainSprite.x; - } - - /** - * Returns the right-most position of the right-most member. - * If there are no members, x is returned. - * - * @since 5.0.0 - * @return the right-most position of the right-most member - */ - public function findMaxX():Float - { - return this.mainSprite == null ? x : findMaxXHelper(); - } - - function findMaxXHelper():Float - { - return this.mainSprite.x + this.mainSprite.width; - } - - /** - * This functionality isn't supported in SpriteGroup - */ - override function set_height(value:Float):Float - { - return value; + return originalSizes.x; } override function get_height():Float { - if (this.mainSprite == null) return 0; - - return this.mainSprite.height; + return originalSizes.y; } - - /** - * Returns the top-most position of the top-most member. - * If there are no members, y is returned. - * - * @since 5.0.0 - * @return the top-most position of the top-most member - */ - public function findMinY():Float - { - return this.mainSprite == null ? y : findMinYHelper(); - } - - function findMinYHelper():Float - { - return this.mainSprite.y; - } - - /** - * Returns the top-most position of the top-most member. - * If there are no members, y is returned. - * - * @since 5.0.0 - * @return the bottom-most position of the bottom-most member - */ - public function findMaxY():Float - { - return this.mainSprite == null ? y : findMaxYHelper(); - } - - function findMaxYHelper():Float - { - return this.mainSprite.y + this.mainSprite.height; - } - - /** - * This functionality isn't supported in SpriteGroup - * @return this sprite group - */ - public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite - { - #if FLX_DEBUG - throw "This function is not supported in FlxSpriteGroup"; - #end - return this; - } - - /** - * This functionality isn't supported in SpriteGroup - * @return this sprite group - */ - public override function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false, - ?Key:String):FlxSprite - { - return this; - } - - /** - * This functionality isn't supported in SpriteGroup - * @return this sprite group - */ - public override function loadRotatedGraphic(Graphic:FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false, AutoBuffer:Bool = false, - ?Key:String):FlxSprite - { - #if FLX_DEBUG - throw "This function is not supported in FlxSpriteGroup"; - #end - return this; - } - - /** - * This functionality isn't supported in SpriteGroup - * @return this sprite group - */ - public override function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite - { - #if FLX_DEBUG - throw "This function is not supported in FlxSpriteGroup"; - #end - return this; - } - - override function set_pixels(value:BitmapData):BitmapData - { - return value; - } - - override function set_frame(value:FlxFrame):FlxFrame - { - return value; - } - - override function get_pixels():BitmapData - { - return null; - } - - /** - * Internal function to update the current animation frame. - * - * @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target - */ - override inline function calcFrame(RunOnCpp:Bool = false):Void - { - // Nothing to do here - } - - /** - * This functionality isn't supported in SpriteGroup - */ - override inline function resetHelpers():Void {} - - /** - * This functionality isn't supported in SpriteGroup - */ - public override inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {} - - override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection - { - return Frames; - } - - /** - * This functionality isn't supported in SpriteGroup - */ - override inline function updateColorTransform():Void {} } diff --git a/source/funkin/play/character/MultiAnimateAtlasCharacter.hx b/source/funkin/play/character/MultiAnimateAtlasCharacter.hx new file mode 100644 index 000000000..e9a78f001 --- /dev/null +++ b/source/funkin/play/character/MultiAnimateAtlasCharacter.hx @@ -0,0 +1,129 @@ +package funkin.play.character; + +import funkin.graphics.FunkinSprite; +import funkin.util.assets.FlxAnimationUtil; +import animate.FlxAnimateFrames; +import funkin.modding.events.ScriptEvent; +import funkin.data.animation.AnimationData; +import funkin.data.character.CharacterData.CharacterRenderType; +import flixel.math.FlxPoint; + +/** + * This render type is the most complex, and is used by characters which use + * multiple Adobe Animate texture atlases. This render type concatenates multiple + * texture atlases into a single sprite. + * + * BaseCharacter has game logic, MultiAnimateAtlasCharacter has only rendering logic. + * KEEP THEM SEPARATE! + */ +class MultiAnimateAtlasCharacter extends BaseCharacter +{ + var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0); + + public function new(id:String) + { + super(id, CharacterRenderType.MultiAnimateAtlas); + } + + override function onCreate(event:ScriptEvent):Void + { + // Display a custom scope for debugging purposes. + #if FEATURE_DEBUG_TRACY + cpp.vm.tracy.TracyProfiler.zoneScoped('MultiAnimateAtlasCharacter.create(${this.characterId})'); + #end + + try + { + trace('Loading assets for Multi-Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0")); + loadAtlases(); + loadAnimations(); + } + catch (e) + { + throw "Exception thrown while building sprite: " + e; + } + + trace('[MULTIATLASCHAR] Successfully loaded texture atlases for ${characterId} with ${_data.animations.length} animations.'); + super.onCreate(event); + + originalSizes.set(this.width, this.height); + } + + function loadAtlases() + { + trace('[MULTIATLASCHAR] Loading sprite atlases for ${characterId}.'); + + var assetList:Array = []; + for (anim in _data.animations) + { + if (anim.assetPath != null && !assetList.contains(anim.assetPath)) + { + assetList.push(anim.assetPath); + } + } + + var baseAssetLibrary:String = Paths.getLibrary(_data.assetPath); + var baseAssetPath:String = Paths.stripLibrary(_data.assetPath); + + loadTextureAtlas(baseAssetPath, baseAssetLibrary, cast _data.atlasSettings); + + for (asset in assetList) + { + var subAssetLibrary:String = Paths.getLibrary(asset); + var subAssetPath:String = Paths.stripLibrary(asset); + + var clone:FunkinSprite = FunkinSprite.createTextureAtlas(0, 0, subAssetPath, subAssetLibrary, cast _data.atlasSettings); + var subTexture:FlxAnimateFrames = clone.library; + + trace('Concatenating texture atlas: ${asset}'); + subTexture.parent.destroyOnNoUse = false; + + this.library.addAtlas(subTexture); + } + + if (_data.isPixel) + { + this.isPixel = true; + this.antialiasing = false; + } + else + { + this.isPixel = false; + this.antialiasing = true; + } + + this.setScale(_data.scale); + } + + function loadAnimations() + { + trace('[MULTIATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}'); + + FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations); + + for (anim in _data.animations) + { + if (anim.offsets == null) + { + setAnimationOffsets(anim.name, 0, 0); + } + else + { + setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]); + } + } + + var animNames = this.anim.getNameList(); + trace('[MULTIATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); + } + + override function get_width():Float + { + return originalSizes.x; + } + + override function get_height():Float + { + return originalSizes.y; + } +} diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index a57f8a10b..67e4dd982 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -126,9 +126,4 @@ class MultiSparrowCharacter extends BaseCharacter var animNames = this.animation.getNameList(); trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); } - - public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void - { - super.playAnimation(name, restart, ignoreOther, reverse); - } } diff --git a/source/funkin/play/character/ScriptedCharacter.hx b/source/funkin/play/character/ScriptedCharacter.hx index 3d9ad7ed4..1339a4ef3 100644 --- a/source/funkin/play/character/ScriptedCharacter.hx +++ b/source/funkin/play/character/ScriptedCharacter.hx @@ -26,6 +26,14 @@ class ScriptedSparrowCharacter extends SparrowCharacter implements polymod.hscri @:hscriptClass class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements polymod.hscript.HScriptedClass {} +/** + * A script that can be tied to a MultiAnimateAtlasCharacter, which persists across states. + * Create a scripted class that extends MultiAnimateAtlasCharacter, + * then call `super('charId')` in the constructor to use this. + */ +@:hscriptClass +class ScriptedMultiAnimateAtlasCharacter extends MultiAnimateAtlasCharacter implements polymod.hscript.HScriptedClass {} + /** * A script that can be tied to a PackerCharacter, which persists across states. * Create a scripted class that extends PackerCharacter, diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx index 31ba2a265..0d87bc5d0 100644 --- a/source/funkin/play/character/SparrowCharacter.hx +++ b/source/funkin/play/character/SparrowCharacter.hx @@ -49,8 +49,6 @@ class SparrowCharacter extends BaseCharacter { this.isPixel = true; this.antialiasing = false; - // pixelPerfectRender = true; - // pixelPerfectPosition = true; } else { diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 13a57dbf4..396306061 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -438,35 +438,6 @@ class HealthIcon extends FunkinSprite this.antialiasing = !isPixel; } - /** - * @return Name of the current animation being played by this health icon. - */ - public function getCurrentAnimation():String - { - if (this.animation == null || this.animation.curAnim == null) return ""; - return this.animation.curAnim.name; - } - - /** - * @param id The name of the animation to check for. - * @return Whether this sprite posesses the given animation. - * Only true if the animation was successfully loaded from the XML. - */ - public function hasAnimation(id:String):Bool - { - if (this.animation == null) return false; - - return this.animation.getByName(id) != null; - } - - /** - * @return Whether the current animation is in the finished state. - */ - public function isAnimationFinished():Bool - { - return this.animation.finished; - } - /** * Plays the animation with the given name. * @param name The name of the animation to play. diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index 592c1d2cf..e24dce83b 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -201,12 +201,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe applyAnimationOffsets(correctName); } - public function getCurrentAnimation():String - { - if (this.animation == null || this.animation.curAnim == null) return ""; - return this.animation.curAnim.name; - } - /** * Ensure that a given animation exists before playing it. * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play. diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 70aa88589..72ebe44ae 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -175,22 +175,6 @@ class StrumlineNote extends FunkinSprite } } - /** - * Returns the name of the animation that is currently playing. - * If no animation is playing (usually this means the sprite is BROKEN!), - * returns an empty string to prevent NPEs. - */ - public function getCurrentAnimation():String - { - if (this.animation == null || this.animation.curAnim == null) return ""; - return this.animation.curAnim.name; - } - - public function isAnimationFinished():Bool - { - return this.animation.finished; - } - static final DEFAULT_OFFSET:Int = 13; /** diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index de3927f6f..dd200f350 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -115,7 +115,7 @@ class NoteStyle implements IRegistryEntry return null; } - if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath))) + if (!FunkinMemory.isTextureCached(Paths.image(noteAssetPath))) { FlxG.log.warn('Note texture is not cached: ${noteAssetPath}'); } @@ -896,7 +896,7 @@ class NoteStyle implements IRegistryEntry return null; } - if (!FunkinSprite.isTextureCached(Paths.image(splashAssetPath))) + if (!FunkinMemory.isTextureCached(Paths.image(splashAssetPath))) { FlxG.log.warn('Note Splash texture not cached: ${splashAssetPath}'); } @@ -1046,7 +1046,7 @@ class NoteStyle implements IRegistryEntry return null; } - if (!FunkinSprite.isTextureCached(Paths.image(holdCoverAssetPath))) + if (!FunkinMemory.isTextureCached(Paths.image(holdCoverAssetPath))) { FlxG.log.warn('Hold Note Cover texture not cached: ${holdCoverAssetPath}'); } diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index b539da77f..10169b55b 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -203,13 +203,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass } } - public function hasAnimation(id:String):Bool - { - if (this.animation == null) return false; - - return this.animation.getByName(id) != null; - } - /** * Ensure that a given animation exists before playing it. * Will gracefully check for name, then name with stripped suffixes, then fail to play. @@ -327,27 +320,11 @@ class Bopper extends StageProp implements IPlayStateScriptedClass this.animOffsets = offsets; } - public function isAnimationFinished():Bool - { - return this.animation?.finished ?? false; - } - public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void { animationOffsets.set(name, [xOffset, yOffset]); } - /** - * Returns the name of the animation that is currently playing. - * If no animation is playing (usually this means the character is BROKEN!), - * returns an empty string to prevent NPEs. - */ - public function getCurrentAnimation():String - { - if (this.animation == null || this.animation.curAnim == null) return ""; - return this.animation.curAnim.name; - } - // override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets. override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint { diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 2620296dc..6d4270bb5 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -183,6 +183,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements { case 'packer': propSprite.loadPacker(dataProp.assetPath); + case 'animateatlas': + propSprite.loadTextureAtlas(dataProp.assetPath, _data.directory, cast dataProp.atlasSettings); default: // 'sparrow' propSprite.loadSparrow(dataProp.assetPath); } @@ -268,6 +270,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); } } + case 'animateatlas': + FlxAnimationUtil.addTextureAtlasAnimations(propSprite, dataProp.animations); default: // 'sparrow' FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations); if (Std.isOfType(propSprite, Bopper)) diff --git a/source/funkin/ui/charSelect/CharIcon.hx b/source/funkin/ui/charSelect/CharIcon.hx deleted file mode 100644 index 9757b7f48..000000000 --- a/source/funkin/ui/charSelect/CharIcon.hx +++ /dev/null @@ -1,18 +0,0 @@ -package funkin.ui.charSelect; - -import flixel.FlxSprite; - -@:nullSafety -class CharIcon extends FlxSprite -{ - public var locked:Bool = false; - - public function new(x:Float, y:Float, locked:Bool = false) - { - super(x, y); - - this.locked = locked; - - makeGraphic(128, 128); - } -} diff --git a/source/funkin/ui/charSelect/CharIconCharacter.hx b/source/funkin/ui/charSelect/CharIconCharacter.hx deleted file mode 100644 index 8916baf52..000000000 --- a/source/funkin/ui/charSelect/CharIconCharacter.hx +++ /dev/null @@ -1,50 +0,0 @@ -package funkin.ui.charSelect; - -import openfl.display.BitmapData; -import openfl.filters.DropShadowFilter; -import openfl.filters.ConvolutionFilter; -import funkin.graphics.shaders.StrokeShader; - -@:nullSafety -class CharIconCharacter extends CharIcon -{ - // public var dropShadowFilter:DropShadowFilter; - - var matrixFilter:Array = [ - 1, 1, 1, - 1, 1, 1, - 1, 1, 1 - ]; - - var divisor:Int = 1; - var bias:Int = 0; - // var convolutionFilter:ConvolutionFilter; - - // public var noDropShadow:BitmapData; - // public var withDropShadow:BitmapData; - - // var strokeShader:StrokeShader; - - public function new(path:String) - { - super(0, 0, false); - - loadGraphic(Paths.image('freeplay/icons/' + path + 'pixel')); - setGraphicSize(128, 128); - updateHitbox(); - antialiasing = false; - - // strokeShader = new StrokeShader(); - // shader = strokeShader; - - // noDropShadow = pixels.clone(); - - // dropShadowFilter = new DropShadowFilter(5, 45, 0, 1, 0, 0); - // convolutionFilter = new ConvolutionFilter(3, 3, matrixFilter, divisor, bias); - // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), dropShadowFilter); - // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), convolutionFilter); - // withDropShadow = pixels.clone(); - - // pixels = noDropShadow.clone(); - } -} diff --git a/source/funkin/ui/charSelect/CharIconLocked.hx b/source/funkin/ui/charSelect/CharIconLocked.hx deleted file mode 100644 index ba5d5cb0a..000000000 --- a/source/funkin/ui/charSelect/CharIconLocked.hx +++ /dev/null @@ -1,4 +0,0 @@ -package funkin.ui.charSelect; - -@:nullSafety -class CharIconLocked extends CharIcon {} diff --git a/source/funkin/ui/charSelect/CharSelectAtlasHandler.hx b/source/funkin/ui/charSelect/CharSelectAtlasHandler.hx new file mode 100644 index 000000000..ebf0258d6 --- /dev/null +++ b/source/funkin/ui/charSelect/CharSelectAtlasHandler.hx @@ -0,0 +1,45 @@ +package funkin.ui.charSelect; + +import animate.FlxAnimateFrames; +import flixel.FlxG; + +/** + * Utility class for handling the atlases loaded by CharSelect & co. in an efficient way. + * TODO: Maybe this should be a general utility class instead? + */ +class CharSelectAtlasHandler +{ + static final framesCache:Map = []; + + public static function loadAtlas(path:String, ?settings:FlxAnimateSettings):Null + { + if (framesCache.exists(path)) return framesCache.get(path); + + var result:FlxAnimateFrames = FlxAnimateFrames.fromAnimate(Paths.animateAtlas(path), + { + swfMode: settings?.swfMode ?? true, + filterQuality: settings?.filterQuality ?? MEDIUM, + cacheOnLoad: settings?.cacheOnLoad ?? false + }); + + if (result == null) + { + FlxG.log.error('Failed to load atlas at path $path!'); + return null; + } + + result.parent.destroyOnNoUse = false; + framesCache.set(path, result); + return result; + } + + public static function clearAtlasCache():Void + { + for (frames in framesCache.iterator()) + { + // NOTE: Doing this already calls checkUseCount! + frames.parent.destroyOnNoUse = true; + } + framesCache.clear(); + } +} diff --git a/source/funkin/ui/charSelect/CharSelectGF.hx b/source/funkin/ui/charSelect/CharSelectGF.hx index 9b7be2427..3ddf2cb4e 100644 --- a/source/funkin/ui/charSelect/CharSelectGF.hx +++ b/source/funkin/ui/charSelect/CharSelectGF.hx @@ -1,67 +1,34 @@ package funkin.ui.charSelect; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import flixel.math.FlxMath; -import funkin.util.FramesJSFLParser; -import funkin.util.FramesJSFLParser.FramesJSFLInfo; -import funkin.util.FramesJSFLParser.FramesJSFLFrame; +import funkin.graphics.FunkinSprite; import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.vis.dsp.SpectralAnalyzer; import funkin.data.freeplay.player.PlayerRegistry; +import funkin.ui.FullScreenScaleMode; +import flixel.math.FlxPoint; @:nullSafety -class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass +class CharSelectGF extends FunkinSprite implements IBPMSyncedScriptedClass { - var fadeTimer:Float = 0; - var fadingStatus:FadeStatus = OFF; - var fadeAnimIndex:Int = 0; - - var animInInfo:Null; - var animOutInfo:Null; - - var intendedYPos:Float = 0; - var intendedAlpha:Float = 0; - var list:Array> = []; - var analyzer:Null; var analyzerLevelsCache:Array = new Array(); - var currentGFPath:Null; + var currentGFPath:String = ""; var enableVisualizer:Bool = false; - public function new() - { - super(0, 0, Paths.animateAtlas("charSelect/gfChill")); + var danceEvery:Int = 2; - list = anim.curSymbol.getFrameLabelNames(); + public function new(x:Float, y:Float) + { + super(x, y); + this.applyStageMatrix = true; switchGF(Constants.DEFAULT_CHARACTER); } - override public function update(elapsed:Float):Void - { - super.update(elapsed); - - switch (fadingStatus) - { - case OFF: - // do nothing if it's off! - // or maybe force position to be 0,0? - // maybe reset timers? - resetFadeAnimParams(); - case FADE_OUT: - if (animOutInfo != null) doFade(animOutInfo); - case FADE_IN: - if (animInInfo != null) doFade(animInInfo); - default: - } - } - public function onStepHit(event:SongTimeScriptEvent):Void {} - var danceEvery:Int = 2; - public function onBeatHit(event:SongTimeScriptEvent):Void { // TODO: There's a minor visual bug where there's a little stutter. @@ -72,7 +39,7 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass if (getCurrentAnimation() == "idle" && (event.beat % danceEvery == 0)) { trace('GF beat hit'); - playAnimation("idle", true, false, false); + anim.play("idle", true); } }; @@ -87,9 +54,8 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass if (enableVisualizer && analyzer != null) { analyzerLevelsCache = analyzer.getLevels(analyzerLevelsCache); - var frame = anim.curSymbol.timeline.get("VIZ_bars").get(anim.curFrame); - if (frame == null) return; - var elements = frame.getList(); + var frame:Null = this.timeline.getLayer("VIZ_bars")?.getFrameAtIndex(anim.curAnim.curFrame) ?? null; + var elements:Array = frame?.elements ?? []; var len:Int = cast Math.min(elements.length, 7); for (i in 0...len) @@ -107,73 +73,31 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass animFrame = Std.int(Math.abs(animFrame - 12)); // shitty dumbass flip, cuz dave got da shit backwards lol! - elements[i].symbol.firstFrame = animFrame; + var convertedSymbol = elements[i].toSymbolInstance(); + convertedSymbol.firstFrame = animFrame; + + elements[i] = convertedSymbol; } } } - /** - * @param animInfo Should not be confused with animInInfo! - * This is merely a local var for the function! - */ - function doFade(animInfo:Null):Void - { - if (animInfo == null) - { - return; - } - - fadeTimer += FlxG.elapsed; - if (fadeTimer >= 1 / 24) - { - fadeTimer -= FlxG.elapsed; - // only inc the index for the first frame, used for reference of where to "start" - if (fadeAnimIndex == 0) - { - fadeAnimIndex++; - return; - } - - var curFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex]; - var prevFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex - 1]; - - var xDiff:Float = curFrame.x - prevFrame.x; - var yDiff:Float = curFrame.y - prevFrame.y; - var alphaDiff:Float = curFrame.alpha - prevFrame.alpha; - alphaDiff /= 100; // flash exports alpha as a whole number - - alpha += alphaDiff; - alpha = alpha.clamp(0, 1); - x += xDiff; - y += yDiff; - - fadeAnimIndex++; - } - - if (fadeAnimIndex >= animInfo.frames.length) fadingStatus = OFF; - } - - function resetFadeAnimParams() - { - fadeTimer = 0; - fadeAnimIndex = 0; - } - /** * For switching between "GFs" such as gf, nene, etc * @param bf Which BF we are selecting, so that we know the accompyaning GF */ public function switchGF(bf:String):Void { - var previousGFPath = currentGFPath; + var previousGFPath:String = currentGFPath; var bfObj = PlayerRegistry.instance.fetchEntry(bf); var gfData = bfObj?.getCharSelectData()?.gf; - if (gfData != null && gfData.assetPath != null) currentGFPath = Paths.animateAtlas(gfData.assetPath); + var assetPath:Null = gfData?.assetPath ?? ""; + + currentGFPath = assetPath; // We don't need to update any anims if we didn't change GF trace('currentGFPath(${currentGFPath})'); - if (currentGFPath == null) + if (currentGFPath == "") { this.visible = false; return; @@ -181,20 +105,24 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass else if (previousGFPath != currentGFPath) { this.visible = true; - loadAtlas(currentGFPath); + + var path:String = currentGFPath; + var texture:Null = CharSelectAtlasHandler.loadAtlas(path, {swfMode: true}); + if (texture != null) + { + frames = texture; + } + else + { + this.visible = false; + currentGFPath = ""; + return; + } enableVisualizer = gfData?.visualizer ?? false; - - var animInfoPath = Paths.file('images/${gfData?.animInfoPath}'); - - animInInfo = FramesJSFLParser.parse(animInfoPath + '/In.txt'); - animOutInfo = FramesJSFLParser.parse(animInfoPath + '/Out.txt'); - - if (animInInfo == null) trace(" ERROR ".bg_red().bold() + " Failed to load data for animInInfo, is the path provided correct?"); - if (animOutInfo == null) trace(" ERROR ".bg_red().bold() + " Failed to load data for animOutInfo, is the path provided correct?"); } - playAnimation("idle", true, false, false); + anim.play("idle", true); updateHitbox(); } @@ -207,10 +135,3 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass public function onUpdate(event:UpdateScriptEvent):Void {}; } - -enum FadeStatus -{ - OFF; - FADE_OUT; - FADE_IN; -} diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx index 8da8809b6..2164a2c44 100644 --- a/source/funkin/ui/charSelect/CharSelectPlayer.hx +++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx @@ -1,41 +1,60 @@ package funkin.ui.charSelect; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass; import funkin.modding.events.ScriptEvent; @:nullSafety -class CharSelectPlayer extends FlxAtlasSprite implements IBPMSyncedScriptedClass +class CharSelectPlayer extends FunkinSprite implements IBPMSyncedScriptedClass { + static final DEFAULT_PATH = "charSelect/bfChill"; + var initialX:Float = 0; var initialY:Float = 0; + var currentBFPath:Null; + public function new(x:Float, y:Float) { initialX = x; initialY = y; - super(x, y, Paths.animateAtlas("charSelect/bfChill")); + super(x, y); - onAnimationComplete.add(function(animLabel:String) { + loadTextureAtlas(DEFAULT_PATH, + { + applyStageMatrix: true, + swfMode: true + }); + + anim.onFinish.add(function(animLabel:String) { switch (animLabel) { case "slidein": if (hasAnimation("slidein idle point")) { - playAnimation("slidein idle point", true, false, false); + anim.play("slidein idle point", true); } else { - playAnimation("idle", true, false, false); + anim.play("idle", true); + anim.curAnim.looped = true; } case "deselect": - playAnimation("deselect loop start", true, false, true); - + anim.play("deselect loop start", true); case "slidein idle point", "cannot select Label", "unlock": - playAnimation("idle", true, false, false); + anim.play("idle", true); case "idle": trace('Waiting for onBeatHit'); + + // TODO: once char select data is refactored, add a `shouldBop` field or something IDK + if (currentBFPath != null) + { + if (currentBFPath.endsWith("locked")) + { + anim.curAnim.looped = true; + } + } } }); } @@ -50,36 +69,30 @@ class CharSelectPlayer extends FlxAtlasSprite implements IBPMSyncedScriptedClass // but isAnimationFinished() and isLoopComplete() both don't work! What the hell? // danceEvery isn't necessary if that gets fixed. // - if (getCurrentAnimation() == "idle") + if (getCurrentAnimation() == "idle" && isAnimationFinished()) { - playAnimation("idle", true, false, false); + anim.play("idle", true); } }; - public function updatePosition(str:String) + public function switchChar(str:String, playSlideAnim:Bool = true):Void { - switch (str) - { - case "bf" | 'pico' | "random": - x = initialX; - y = initialY; - } - } + var texture:Null = CharSelectAtlasHandler.loadAtlas('charSelect/${str}Chill'); - public function switchChar(str:String, playSlideAnim:Bool = true) - { - switch str + if (texture != null) { - default: - loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill")); + frames = texture; + } + else + { + trace('Failed to load character atlas for ${str}'); + return; } final animName:String = playSlideAnim ? "slidein" : "idle"; - playAnimation(animName, true, false, false); + anim.play(animName, true); updateHitbox(); - - updatePosition(str); } public function onScriptEvent(event:ScriptEvent):Void {}; diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 22006756a..83b767405 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -1,8 +1,6 @@ package funkin.ui.charSelect; -import openfl.filters.BitmapFilter; import flixel.FlxObject; -import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; @@ -11,33 +9,34 @@ import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxTimer; -import flixel.util.FlxColor; import funkin.audio.FunkinSound; +import funkin.data.freeplay.player.PlayerData.PlayerCharSelectData; import funkin.data.freeplay.player.PlayerRegistry; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import openfl.filters.DropShadowFilter; import funkin.graphics.FunkinCamera; +import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.BlueFade; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.stage.Stage; import funkin.save.Save; -import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.freeplay.FreeplayState; +import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.PixelatedIcon; +import funkin.util.FramesJSFLParser; +import funkin.util.FramesJSFLParser.FramesJSFLInfo; +import funkin.util.HapticUtil; import funkin.util.MathUtil; import funkin.vis.dsp.SpectralAnalyzer; import openfl.display.BlendMode; import openfl.filters.ShaderFilter; -import funkin.util.FramesJSFLParser; -import funkin.util.FramesJSFLParser.FramesJSFLInfo; -import funkin.graphics.FunkinSprite; +import openfl.filters.BitmapFilter; +import openfl.filters.DropShadowFilter; #if FEATURE_NEWGROUNDS import funkin.api.newgrounds.Medals; #end +#if FEATURE_TOUCH_CONTROLS import funkin.util.TouchUtil; -import funkin.util.SwipeUtil; -import funkin.util.HapticUtil; +#end @:nullSafety class CharSelectSubState extends MusicBeatSubState @@ -45,33 +44,30 @@ class CharSelectSubState extends MusicBeatSubState // what the actual hell // having a hard time trying to make my changes work so i chose to be less stubborn and just remove them for now. - Zack // Left this here so somebody can remind me - var cursor:FlxSprite = new FlxSprite(0, 0); + var cursor:FunkinSprite; - var cursorBlue:FlxSprite = new FlxSprite(0, 0); - var cursorDarkBlue:FlxSprite = new FlxSprite(0, 0); - var grpCursors:FlxTypedGroup = new FlxTypedGroup(); - var cursorConfirmed:FlxSprite = new FlxSprite(0, 0); - var cursorDenied:FlxSprite = new FlxSprite(0, 0); + var cursorBlue:FunkinSprite; + var cursorDarkBlue:FunkinSprite; + var grpCursors:FlxTypedGroup; + var cursorConfirmed:FunkinSprite; + var cursorDenied:FunkinSprite; var cursorX:Int = 0; var cursorY:Int = 0; var cursorFactor:Float = 110; var cursorOffsetX:Float = -16; var cursorOffsetY:Float = -48; var cursorLocIntended:FlxPoint = new FlxPoint(0, 0); - var tmrFrames:Int = 60; - // var currentStage:Stage; - var playerChill:CharSelectPlayer = new CharSelectPlayer(0, 0); - var playerChillOut:CharSelectPlayer = new CharSelectPlayer(0, 0); - var gfChill:CharSelectGF = new CharSelectGF(); - // var gfChillOut:CharSelectGF; - var barthing:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/barThing")); - var dipshitBacking:FlxSprite = new FlxSprite(423, -17); - var chooseDipshit:FlxSprite = new FlxSprite(426, -13); - var dipshitBlur:FlxSprite = new FlxSprite(419, -65); - var transitionGradient:FlxSprite = new FlxSprite(0, 0); + var playerChill:CharSelectPlayer; + var playerChillOut:CharSelectPlayer; + var gfChill:CharSelectGF; + var barthing:FunkinSprite; + var dipshitBacking:FunkinSprite; + var chooseDipshit:FunkinSprite; + var dipshitBlur:FunkinSprite; + var transitionGradient:FunkinSprite; var curChar(default, set):String = Constants.DEFAULT_CHARACTER; var rememberedChar:String; - var nametag:Nametag = new Nametag(Constants.DEFAULT_CHARACTER); + var nametag:Nametag; var camFollow:FlxObject = new FlxObject(0, 0, 1, 1); var autoFollow:Bool = false; var availableChars:Map = new Map(); @@ -98,11 +94,41 @@ class CharSelectSubState extends MusicBeatSubState var cutoutSize:Float = 0; + var fadeShader:BlueFade = new BlueFade(); + public function new(?params:CharSelectSubStateParams) { super(); - rememberedChar = params?.character ?? Constants.DEFAULT_CHARACTER; - loadAvailableCharacters(); + rememberedChar = params?.character ?? ""; + + grpCursors = new FlxTypedGroup(); + grpHitboxes = new FlxTypedGroup(); + + gfChill = new CharSelectGF(cutoutSize, 0); + playerChillOut = new CharSelectPlayer(cutoutSize, 0); + playerChill = new CharSelectPlayer(cutoutSize, 0); + + dipshitBlur = new FunkinSprite(cutoutSize + 419, -65); + dipshitBacking = new FunkinSprite(cutoutSize + 423, -17); + chooseDipshit = new FunkinSprite(cutoutSize + 426, -13); + + nametag = new Nametag(curChar); + + cursor = new FunkinSprite(0, 0); + cursorBlue = new FunkinSprite(0, 0); + cursorDarkBlue = new FunkinSprite(0, 0); + cursorConfirmed = new FunkinSprite(0, 0); + cursorDenied = new FunkinSprite(0, 0); + + charHitbox = new FlxObject(FlxG.width * 0.65, FlxG.height * 0.2, 300, 500); + + transitionGradient = new FunkinSprite(0, 0); + barthing = new FunkinSprite(0, 0); + + selectSound = new FunkinSound(); + unlockSound = new FunkinSound(); + lockedSound = new FunkinSound(); + staticSound = new FunkinSound(); } function loadAvailableCharacters():Void @@ -111,9 +137,7 @@ class CharSelectSubState extends MusicBeatSubState for (playerId in playerIds) { - var player:Null = PlayerRegistry.instance.fetchEntry(playerId); - if (player == null) continue; - var playerData = player.getCharSelectData(); + var playerData:Null = PlayerRegistry.instance.fetchEntry(playerId)?.getCharSelectData(); if (playerData == null) continue; var targetPosition:Int = playerData.position ?? 0; @@ -124,15 +148,26 @@ class CharSelectSubState extends MusicBeatSubState trace('Placing player ${playerId} at position ${targetPosition}'); availableChars.set(targetPosition, playerId); - } - } - var fadeShader:BlueFade = new BlueFade(); + CharSelectAtlasHandler.loadAtlas('charSelect/${playerId}Chill'); + + var gfPath:Null = playerData.gf?.assetPath; + if (gfPath != null) + { + CharSelectAtlasHandler.loadAtlas(gfPath); + } + } + + // Mr. Static also needs some caching... + CharSelectAtlasHandler.loadAtlas('charSelect/lockedChill', {filterQuality: LOW, cacheOnLoad: true}); + } override public function create():Void { super.create(); + loadAvailableCharacters(); + cutoutSize = FullScreenScaleMode.gameCutoutSize.x / 2; bopInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/iconBopInfo/iconBopInfo.txt")); @@ -141,35 +176,39 @@ class CharSelectSubState extends MusicBeatSubState trace(" ERROR ".bg_red().bold() + " Failed to load data for bopInfo, is the path provided correct?"); } - var bg:FlxSprite = new FlxSprite(cutoutSize + -153, -140); + var bg:FunkinSprite = new FunkinSprite(cutoutSize + -153, -140); bg.loadGraphic(Paths.image('charSelect/charSelectBG')); bg.scrollFactor.set(0.1, 0.1); add(bg); - var crowd:FlxAtlasSprite = new FlxAtlasSprite(cutoutSize, 0, Paths.animateAtlas("charSelect/crowd")); - crowd.anim.play(); - crowd.anim.onComplete.add(function() { - crowd.anim.play(); - }); + var crowd:FunkinSprite = FunkinSprite.createTextureAtlas(cutoutSize, 0, "charSelect/crowd", + { + applyStageMatrix: true + }); + crowd.anim.play(''); + crowd.anim.curAnim.looped = true; crowd.scrollFactor.set(0.3, 0.3); add(crowd); - var stageSpr:FlxAtlasSprite = new FlxAtlasSprite(cutoutSize + -2, 1, Paths.animateAtlas("charSelect/charSelectStage")); - stageSpr.anim.play(""); - stageSpr.anim.onComplete.add(function() { - stageSpr.anim.play(""); - }); + var stageSpr:FunkinSprite = FunkinSprite.createTextureAtlas(cutoutSize - 2, 1, "charSelect/charSelectStage", + { + applyStageMatrix: true + }); + stageSpr.anim.play(''); + stageSpr.anim.curAnim.looped = true; add(stageSpr); - var curtains:FlxSprite = new FlxSprite(cutoutSize + (-47 - 165), -49 - 50); + var curtains:FunkinSprite = new FunkinSprite(cutoutSize + -212, -99); curtains.loadGraphic(Paths.image('charSelect/curtains')); curtains.scrollFactor.set(1.4, 1.4); add(curtains); - barthing.anim.play(""); - barthing.anim.onComplete.add(function() { - barthing.anim.play(""); - }); + barthing.loadTextureAtlas("charSelect/barThing", + { + applyStageMatrix: true + }); + barthing.anim.play(''); + barthing.anim.curAnim.looped = true; barthing.blend = BlendMode.MULTIPLY; barthing.scale.x = 2.5; barthing.scrollFactor.set(0, 0); @@ -178,27 +217,23 @@ class CharSelectSubState extends MusicBeatSubState barthing.y += 80; FlxTween.tween(barthing, {y: barthing.y - 80}, 1.3, {ease: FlxEase.expoOut}); - var charLight:FlxSprite = new FlxSprite(cutoutSize + 800, 250); + var charLight:FunkinSprite = new FunkinSprite(cutoutSize + 800, 250); charLight.loadGraphic(Paths.image('charSelect/charLight')); add(charLight); - var charLightGF:FlxSprite = new FlxSprite(cutoutSize + 180, 240); + var charLightGF:FunkinSprite = new FunkinSprite(cutoutSize + 180, 240); charLightGF.loadGraphic(Paths.image('charSelect/charLight')); add(charLightGF); function setupPlayerChill(character:String) { - gfChill = new CharSelectGF(); gfChill.switchGF(character); - gfChill.x += cutoutSize; add(gfChill); - playerChillOut = new CharSelectPlayer(cutoutSize, 0); playerChillOut.switchChar(character, false); playerChillOut.visible = false; add(playerChillOut); - playerChill = new CharSelectPlayer(cutoutSize, 0); playerChill.switchChar(character, false); add(playerChill); } @@ -221,28 +256,27 @@ class CharSelectSubState extends MusicBeatSubState else setupPlayerChill(Constants.DEFAULT_CHARACTER); - var speakers:FlxAtlasSprite = new FlxAtlasSprite(cutoutSize - 10, 0, Paths.animateAtlas("charSelect/charSelectSpeakers")); - speakers.anim.play(""); - speakers.anim.onComplete.add(function() { - speakers.anim.play(""); - }); + var speakers:FunkinSprite = FunkinSprite.createTextureAtlas(cutoutSize - 10, 0, "charSelect/charSelectSpeakers", + { + applyStageMatrix: true + }); + speakers.anim.play(''); + speakers.anim.curAnim.looped = true; speakers.scrollFactor.set(1.8, 1.8); speakers.scale.set(1.05, 1.05); add(speakers); - var fgBlur:FlxSprite = new FlxSprite(cutoutSize + -125, 170); + var fgBlur:FunkinSprite = new FunkinSprite(cutoutSize + -125, 170); fgBlur.loadGraphic(Paths.image('charSelect/foregroundBlur')); - fgBlur.blend = openfl.display.BlendMode.MULTIPLY; + fgBlur.blend = BlendMode.MULTIPLY; add(fgBlur); - dipshitBlur = new FlxSprite(cutoutSize + 419, -65); dipshitBlur.frames = Paths.getSparrowAtlas("charSelect/dipshitBlur"); dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical offset instance 1", 24, true); dipshitBlur.blend = BlendMode.ADD; dipshitBlur.animation.play("idle"); add(dipshitBlur); - dipshitBacking = new FlxSprite(cutoutSize + 423, -17); dipshitBacking.frames = Paths.getSparrowAtlas("charSelect/dipshitBacking"); dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal offset instance 1", 24, true); dipshitBacking.blend = BlendMode.ADD; @@ -252,7 +286,6 @@ class CharSelectSubState extends MusicBeatSubState dipshitBacking.y += 210; FlxTween.tween(dipshitBacking, {y: dipshitBacking.y - 210}, 1.1, {ease: FlxEase.expoOut}); - chooseDipshit = new FlxSprite(cutoutSize + 426, -13); chooseDipshit.loadGraphic(Paths.image('charSelect/chooseDipshit')); add(chooseDipshit); @@ -266,7 +299,6 @@ class CharSelectSubState extends MusicBeatSubState dipshitBacking.scrollFactor.set(); dipshitBlur.scrollFactor.set(); - nametag = new Nametag(curChar); nametag.midpointX += cutoutSize; add(nametag); @@ -278,31 +310,14 @@ class CharSelectSubState extends MusicBeatSubState nametag.scrollFactor.set(); - FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSprite, ["x", "y", "alpha", "scale", "blend"])); - FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxAtlasSprite, ["x", "y"])); + FlxG.debugger.addTrackerProfile(new TrackerProfile(FunkinSprite, ["x", "y", "alpha", "scale", "blend"])); FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSound, ["pitch", "volume"])); - // FlxG.debugger.track(crowd); - // FlxG.debugger.track(stageSpr, "stageSpr"); - // FlxG.debugger.track(bfChill, "bf chill"); - // FlxG.debugger.track(playerChill, "player"); - // FlxG.debugger.track(nametag, "nametag"); - // FlxG.debugger.track(selectSound, "selectSound"); - // FlxG.debugger.track(chooseDipshit, "choose dipshit"); - // FlxG.debugger.track(barthing, "barthing"); - // FlxG.debugger.track(fgBlur, "fgBlur"); - // FlxG.debugger.track(dipshitBlur, "dipshitBlur"); - // FlxG.debugger.track(dipshitBacking, "dipshitBacking"); - // FlxG.debugger.track(charLightGF, "charLight"); - // FlxG.debugger.track(gfChill, "gfChill"); - add(grpCursors); cursor.loadGraphic(Paths.image('charSelect/charSelector')); cursor.color = 0xFFFFFF00; - // FFCC00 - cursorBlue.loadGraphic(Paths.image('charSelect/charSelector')); cursorBlue.color = 0xFF3EBBFF; @@ -328,7 +343,6 @@ class CharSelectSubState extends MusicBeatSubState grpCursors.add(cursorBlue); grpCursors.add(cursor); - charHitbox = new FlxObject(FlxG.width * 0.65, FlxG.height * 0.2, 300, 500); charHitbox.active = false; charHitbox.scrollFactor.set(); @@ -389,25 +403,17 @@ class CharSelectSubState extends MusicBeatSubState FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG}); - // FlxG.debugger.track(cursor); - FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"])); FlxG.debugger.track(this); add(camFollow); camFollow.screenCenter(); - // FlxG.camera.follow(camFollow, LOCKON, 0.01); FlxG.camera.follow(camFollow, LOCKON); var fadeShaderFilter:ShaderFilter = new ShaderFilter(fadeShader); FlxG.camera.filters = [fadeShaderFilter]; - var temp:FlxSprite = new FlxSprite(); - temp.loadGraphic(Paths.image('charSelect/placement')); - add(temp); - temp.alpha = 0.0; - Conductor.stepHit.add(spamOnStep); // FlxG.debugger.track(temp, "tempBG"); @@ -462,6 +468,7 @@ class CharSelectSubState extends MusicBeatSubState FlxG.sound.list.add(introSound); openSubState(new IntroSubState()); + subStateClosed.addOnce((_) -> { remove(blackScreen); if (!Save.instance.oldChar.value) @@ -477,6 +484,12 @@ class CharSelectSubState extends MusicBeatSubState }); } + override public function destroy():Void + { + CharSelectAtlasHandler.clearAtlasCache(); + super.destroy(); + } + function checkNewChar():Void { if (nonLocks.length > 0) selectTimer.start(2, (_) -> { @@ -511,7 +524,7 @@ class CharSelectSubState extends MusicBeatSubState } var grpIcons:FlxSpriteGroup = new FlxSpriteGroup(); - var grpHitboxes:FlxTypedGroup = new FlxTypedGroup(); + var grpHitboxes:FlxTypedGroup; var grpXSpread(default, set):Float = 107; var grpYSpread(default, set):Float = 127; var nonLocks = []; @@ -521,7 +534,6 @@ class CharSelectSubState extends MusicBeatSubState add(grpIcons); FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSpriteGroup, ["x", "y"])); - // FlxG.debugger.track(grpIcons, "iconGrp"); for (i in 0...9) { @@ -542,12 +554,13 @@ class CharSelectSubState extends MusicBeatSubState var isPlayerUnlocked:Bool = player?.isUnlocked() ?? false; if (availableChars.exists(i) && isPlayerUnlocked) nonLocks.push(i); - var temp:Lock = new Lock(0, 0, i); - temp.ID = 1; + var temp:Lock = new Lock(0, 0, i, + { + swfMode: true, + uniqueInCache: true + }); - // temp.onAnimationComplete.add(function(anim) { - // if (anim == "unlock") playerChill.playAnimation("unlock", true); - // }); + temp.ID = 1; grpIcons.add(temp); } @@ -582,6 +595,7 @@ class CharSelectSubState extends MusicBeatSubState var xThing = (copy - index - 2) * -1; // Look, I'd write better code but I had better aneurysms, my bad - Cheems // felt - Zack + // Krue - Abnormal cursorY = yThing; cursorX = xThing; @@ -592,26 +606,21 @@ class CharSelectSubState extends MusicBeatSubState selectTimer.start(0.5, function(_) { var lock:Lock = cast grpIcons.group.members[index]; - lock.anim.getFrameLabel("unlockAnim").add(function() { - playerChillOut.playAnimation("death"); + lock.anim.play("unlock"); + lock.anim.onFrameChange.add(function(animName:String, frame:Int, index:Int) { + if (frame == 40) + { + playerChillOut.anim.play("death"); + } }); - lock.playAnimation("unlock"); - unlockSound.volume = 0.7; unlockSound.play(true); - // Do not sync the lock, because otherwise the animation will be out of sync! - - /*syncLock = lock; - - sync = true; */ - - lock.onAnimationComplete.addOnce(function(_) { - // syncLock = null; - var char:Null = availableChars.get(index) ?? Constants.DEFAULT_CHARACTER; + lock.anim.onFinish.addOnce(function(_) { + var char:String = availableChars.get(index) ?? Constants.DEFAULT_CHARACTER; camera.flash(0xFFFFFFFF, 0.1); - playerChill.playAnimation("unlock"); + playerChill.anim.play("unlock"); playerChill.visible = true; var id = grpIcons.members.indexOf(lock); @@ -631,7 +640,7 @@ class CharSelectSubState extends MusicBeatSubState bopPlay = true; updateIconPositions(); - playerChillOut.onAnimationComplete.addOnce((_) -> if (_ == "death") + playerChillOut.anim.onFinish.addOnce((_) -> if (_ == "death") { // sync = false; playerChillOut.visible = false; @@ -671,7 +680,7 @@ class CharSelectSubState extends MusicBeatSubState }); } else - playerChill.onAnimationComplete.addOnce((_) -> unLock()); + playerChill.anim.onFinish.addOnce((_) -> unLock()); }); playerChill.visible = false; @@ -685,6 +694,7 @@ class CharSelectSubState extends MusicBeatSubState { grpIcons.x = cutoutSize + 450; grpIcons.y = 120; + for (index => member in grpIcons.members) { var posX:Float = (index % 3); @@ -710,34 +720,6 @@ class CharSelectSubState extends MusicBeatSubState } } - var sync:Bool = false; - var syncLock:Null; - var audioBizz:Float = 0; - - function syncAudio(elapsed:Float):Void - { - @:privateAccess - if (sync && unlockSound.time > 0) - { - // if (playerChillOut.anim.framerate > 0) - // { - // if (syncLock != null) syncLock.anim.framerate = 0; - // playerChillOut.anim.framerate = 0; - // } - - playerChillOut.anim._tick = 0; - if (syncLock != null) syncLock.anim._tick = 0; - - if ((unlockSound.time - audioBizz) >= ((delay) * 100)) - { - if (syncLock != null) syncLock.anim._tick = delay; - - playerChillOut.anim._tick = delay; - audioBizz += delay * 100; - } - } - } - function goToFreeplay():Void { allowInput = false; @@ -762,7 +744,6 @@ class CharSelectSubState extends MusicBeatSubState FlxTween.tween(dipshitBlur, {y: dipshitBlur.y + 220}, 0.8, {ease: FlxEase.backIn}); for (index => member in grpIcons.members) { - // member.y += 300; FlxTween.tween(member, {y: member.y + 300}, 0.8, {ease: FlxEase.backIn}); } FlxG.camera.follow(camFollow, LOCKON); @@ -813,8 +794,6 @@ class CharSelectSubState extends MusicBeatSubState if (controls.UI_UP_R || controls.UI_DOWN_R || controls.UI_LEFT_R || controls.UI_RIGHT_R #if FEATURE_TOUCH_CONTROLS || TouchUtil.justReleased #end) selectSound.pitch = 1; - syncAudio(elapsed); - if (allowInput && !pressedSelect) { #if FEATURE_TOUCH_CONTROLS @@ -938,10 +917,10 @@ class CharSelectSubState extends MusicBeatSubState cursorY = -1; } - if (availableChars.exists(getCurrentSelected()) - && PlayerRegistry.instance.isCharacterSeen(availableChars[getCurrentSelected()] ?? Constants.DEFAULT_CHARACTER)) + var currentCharacter:String = availableChars[getCurrentSelected()] ?? Constants.DEFAULT_CHARACTER; + if (availableChars.exists(getCurrentSelected()) && Save.instance.charactersSeen.value.contains(currentCharacter)) { - var charId = availableChars.get(getCurrentSelected()); + var charId:String = availableChars.get(getCurrentSelected()) ?? Constants.DEFAULT_CHARACTER; if (charId != null) curChar = charId; if (allowInput && pressedSelect && (controls.BACK #if FEATURE_TOUCH_CONTROLS || (mobileDeny && TouchUtil.justReleased) #end)) @@ -961,8 +940,8 @@ class CharSelectSubState extends MusicBeatSubState FlxTween.globalManager.cancelTweensOf(FlxG.sound.music); FlxTween.tween(FlxG.sound.music, {pitch: 1.0, volume: 1.0}, 1, {ease: FlxEase.quartInOut}); - playerChill.playAnimation("deselect"); - gfChill.playAnimation("deselect"); + playerChill.anim.play("deselect"); + gfChill.anim.play("deselect"); pressedSelect = false; FlxTween.tween(FlxG.sound.music, {pitch: 1.0}, 1, { @@ -970,8 +949,10 @@ class CharSelectSubState extends MusicBeatSubState onComplete: (_) -> { if (playerChill.getCurrentAnimation() == "deselect loop start" || playerChill.getCurrentAnimation() == "deselect") { - playerChill.playAnimation("idle", true, false, true); - gfChill.playAnimation("idle", true, false, true); + playerChill.anim.play("idle", true); + playerChill.anim.curAnim.looped = true; + gfChill.anim.play("idle", true); + gfChill.anim.curAnim.looped = true; } } }); @@ -1004,8 +985,9 @@ class CharSelectSubState extends MusicBeatSubState FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1, {ease: FlxEase.quadInOut}); FlxTween.tween(FlxG.sound.music, {volume: 0.0}, 1.5, {ease: FlxEase.quadInOut}); - playerChill.playAnimation("select"); - gfChill.playAnimation("confirm", true, false, true); + playerChill.anim.play("select"); + gfChill.anim.play("confirm", true); + gfChill.anim.curAnim.looped = true; pressedSelect = true; selectTimer.start(1.5, (_) -> { goToFreeplay(); @@ -1027,7 +1009,7 @@ class CharSelectSubState extends MusicBeatSubState { cursorDenied.visible = true; - playerChill.playAnimation("cannot select Label", true); + playerChill.anim.play("cannot select Label", true); lockedSound.play(true); @@ -1140,7 +1122,6 @@ class CharSelectSubState extends MusicBeatSubState { if (spamUp || spamDown || spamLeft || spamRight) { - // selectSound.changePitchBySemitone(1); if (selectSound.pitch > 5) selectSound.pitch = 5; selectSound.play(true); @@ -1182,22 +1163,20 @@ class CharSelectSubState extends MusicBeatSubState switch (lock.getCurrentAnimation()) { case "idle": - lock.playAnimation("selected"); + lock.anim.play("selected"); case "selected" | "clicked": - if (controls.ACCEPT) lock.playAnimation("clicked", true); + if (controls.ACCEPT) lock.anim.play("clicked", true); } } else { - lock.playAnimation("idle"); + lock.anim.play("idle"); } case 0: var memb:PixelatedIcon = cast member; if (index == getCurrentSelected()) { - // memb.pixels = memb.withDropShadow.clone(); - if (bopPlay) { if (bopRefX == 0) @@ -1216,19 +1195,15 @@ class CharSelectSubState extends MusicBeatSubState if (autoFollow && !pressedSelect && memb.animation.curAnim?.name != 'idle') { memb.animation.play("confirm", false, true); - @:nullSafety(Off) - var onFinish:String->Void = null; - onFinish = (_) -> { + var onFinish:String->Void = (_) -> { member.animation.play('idle'); - member.animation.onFinish.remove(onFinish); + member.animation.onFinish.removeAll(); }; member.animation.onFinish.add(onFinish); } } else { - // memb.pixels = memb.noDropShadow.clone(); - @:nullSafety(Off) memb.filters = null; memb.scale.set(2, 2); } @@ -1279,23 +1254,23 @@ class CharSelectSubState extends MusicBeatSubState gfChill.visible = false; playerChill.visible = false; playerChillOut.visible = true; - playerChillOut.playAnimation("slideout"); - var index = playerChillOut.anim.getFrameLabel("slideout").index; - playerChillOut.onAnimationFrame.removeAll(); - playerChillOut.onAnimationFrame.add((_, frame:Int) -> { - if (frame >= index + 1) + playerChillOut.anim.play("slideout"); + + playerChillOut.anim.onFrameChange.removeAll(); + playerChillOut.anim.onFrameChange.add(function(animName:String, frameNumber:Int, index:Int) { + if (!playerChill.visible) { playerChill.visible = true; playerChill.switchChar(value); gfChill.switchGF(value); gfChill.visible = true; } - if (frame >= index + 2) - { - playerChillOut.switchChar(value); - playerChillOut.visible = false; - playerChillOut.onAnimationFrame.removeAll(); - } + }); + + playerChillOut.anim.onFinish.addOnce(function(animName:String) { + playerChillOut.switchChar(value); + playerChillOut.visible = false; + playerChillOut.anim.onFrameChange.removeAll(); }); return value; @@ -1321,5 +1296,5 @@ class CharSelectSubState extends MusicBeatSubState */ typedef CharSelectSubStateParams = { - ?character:String, // ?fromFreeplaySelect:Bool, + ?character:String }; diff --git a/source/funkin/ui/charSelect/Lock.hx b/source/funkin/ui/charSelect/Lock.hx index b2deff00f..599998280 100644 --- a/source/funkin/ui/charSelect/Lock.hx +++ b/source/funkin/ui/charSelect/Lock.hx @@ -1,38 +1,44 @@ package funkin.ui.charSelect; import flixel.util.FlxColor; -import flxanimate.effects.FlxTint; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; +import flixel.FlxCamera; +import flixel.math.FlxPoint; @:nullSafety -class Lock extends FlxAtlasSprite +class Lock extends FunkinSprite { var colors:Array = [ - 0x31F2A5, 0x20ECCD, 0x24D9E8, - 0x20ECCD, 0x20C8D4, 0x209BDD, - 0x209BDD, 0x2362C9, 0x243FB9 - ]; // lock colors, in a nx3 matrix format + 0xFF31F2A5, 0xFF20ECCD, 0xFF24D9E8, + 0xFF20ECCD, 0xFF20C8D4, 0xFF209BDD, + 0xFF209BDD, 0xFF2362C9, 0xFF243FB9 + ]; - public function new(x:Float = 0, y:Float = 0, index:Int) + public function new(x:Float = 0, y:Float = 0, index:Int, settings:AtlasSpriteSettings) { - super(x, y, Paths.animateAtlas("charSelect/lock")); + var tint:FlxColor = colors[index]; - var tint:FlxTint = new FlxTint(colors[index], 1); + super(x, y); - var arr:Array = ["lock", "lock top 1", "lock top 2", "lock top 3", "lock base fuck it"]; + loadTextureAtlas("charSelect/lock", + { + swfMode: settings.swfMode, + uniqueInCache: settings.uniqueInCache, + onSymbolCreate: (symbol) -> { + if (symbol.timeline.getLayer("color") != null) + { + var colorSymbol:Null = symbol.timeline.getLayer("color")?.getFrameAtIndex(0)?.convertToSymbol(0, + 1) ?? null; - var func = function(name) { - var symbol = anim.symbolDictionary[name]; - if (symbol != null && symbol.timeline.get("color") != null) { - var color = symbol.timeline.get("color").get(0); - if (color != null) color.colorEffect = tint; - } - } - for (symbol in arr) - { - func(symbol); - } + if (colorSymbol != null) + { + colorSymbol.setColorTransform(0, 0, 0, 1, tint.red, tint.green, tint.blue, 0); + } + } + } + }); - playAnimation("idle"); + anim.play("idle"); + offset.set(230, 110); } } diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx index a42571e43..dab25a4fd 100644 --- a/source/funkin/ui/debug/anim/DebugBoundingState.hx +++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx @@ -51,7 +51,7 @@ class DebugBoundingState extends FlxState var offsetView:FlxGroup; var dropDownSetup:Bool = false; - var onionSkinChar:FlxSprite; + var onionSkinChar:BaseCharacter; var txtOffsetShit:FlxText; var offsetEditorDialog:CollapsibleDialog; @@ -177,16 +177,25 @@ class DebugBoundingState extends FlxState if (swagChar == null) return; if (swagChar.hasAnimation("idle")) swagChar.playAnimation("idle", true); - onionSkinChar.loadGraphicFromSprite(swagChar); - onionSkinChar.frame = swagChar.frame; onionSkinChar.alpha = 0.6; + + if (onionSkinChar.hasAnimation("idle")) + { + onionSkinChar.playAnimation("idle", true); + } + else + { + onionSkinChar.playAnimation(currentAnimationName, true); + } + + onionSkinChar.animation.pause(); onionSkinChar.flipX = swagChar.flipX; onionSkinChar.scale.set(swagChar.scale.x, swagChar.scale.y); onionSkinChar.updateHitbox(); onionSkinChar.offset.x = swagChar.offset.x + (swagChar.animOffsets[0] - swagChar.globalOffsets[0]) * swagChar.scale.x; onionSkinChar.offset.y = swagChar.offset.y + (swagChar.animOffsets[1] - swagChar.globalOffsets[1]) * swagChar.scale.y; - swagChar.playAnimation(currentAnimationName, true); // reset animation to the one it should be + swagChar.playAnimation(currentAnimationName, true); } function initOffsetView():Void @@ -194,10 +203,6 @@ class DebugBoundingState extends FlxState offsetView = new FlxGroup(); add(offsetView); - onionSkinChar = new FlxSprite().makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.TRANSPARENT); - onionSkinChar.visible = false; - offsetView.add(onionSkinChar); - txtOffsetShit = new FlxText(20, 20, 0, "", 20); txtOffsetShit.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); txtOffsetShit.cameras = [hudCam]; @@ -205,10 +210,6 @@ class DebugBoundingState extends FlxState offsetView.add(txtOffsetShit); var characters:Array = CharacterDataParser.listCharacterIds(); - characters = characters.filter(function(charId:String) { - var char = CharacterDataParser.fetchCharacterData(charId); - return char.renderType != AnimateAtlas; - }); characters.sort(SortUtil.alphabetically); var charDropdown:DropDown = offsetEditorDialog.findComponent('characterDropdown', DropDown); @@ -520,10 +521,23 @@ class DebugBoundingState extends FlxState swagChar.destroy(); } + if (onionSkinChar != null) + { + offsetView.remove(onionSkinChar); + onionSkinChar.destroy(); + } + swagChar = CharacterDataParser.fetchCharacter(char); - swagChar.x = onionSkinChar.x = 100; - swagChar.y = onionSkinChar.y = 100; + swagChar.x = 100; + swagChar.y = 100; swagChar.debug = true; + + onionSkinChar = CharacterDataParser.fetchCharacter(char); + onionSkinChar.x = swagChar.x; + onionSkinChar.y = swagChar.y; + onionSkinChar.debug = true; + + offsetView.add(onionSkinChar); offsetView.add(swagChar); if (swagChar == null || swagChar.frames == null) diff --git a/source/funkin/ui/debug/anim/FlxAnimateTest.hx b/source/funkin/ui/debug/anim/FlxAnimateTest.hx index cbf1c4339..f713c8a6b 100644 --- a/source/funkin/ui/debug/anim/FlxAnimateTest.hx +++ b/source/funkin/ui/debug/anim/FlxAnimateTest.hx @@ -1,16 +1,18 @@ package funkin.ui.debug.anim; import flixel.FlxG; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatState; +import funkin.util.macro.ConsoleMacro; +import funkin.Paths; /** * A simple test of FlxAnimate. * Delete this later? */ -class FlxAnimateTest extends MusicBeatState +class FlxAnimateTest extends MusicBeatState implements ConsoleClass { - var sprite:FlxAtlasSprite; + var sprite:FunkinSprite; public function new() { @@ -22,18 +24,29 @@ class FlxAnimateTest extends MusicBeatState { super.create(); - sprite = new FlxAtlasSprite(-100, 0, 'week3:assets/week3/images/philly/erect/picoFlowers'); + sprite = FunkinSprite.createTextureAtlas(0, 0, "charSelect/bfChill", + { + swfMode: false, // If to render like in a SWF file, rather than the Animate editor. + cacheOnLoad: true, // If to precache all animation filters and masks at once, rather than at runtime. + filterQuality: MEDIUM // Level of quality used to render filters. (HIGH, MEDIUM, LOW, RUDY) + }); + add(sprite); - sprite.playAnimation(sprite.anim.stageInstance.symbol.name, false, false, true); } public override function update(elapsed:Float):Void { super.update(elapsed); - if (FlxG.keys.justPressed.SPACE) ((sprite.anim.isPlaying) ? sprite.anim.pause() : sprite.playAnimation(null, false, false, true)); + if (FlxG.keys.justPressed.SPACE) (!(sprite.isAnimationFinished()) ? sprite.anim.pause() : sprite.anim.resume()); - if (FlxG.keys.anyJustPressed([A, LEFT])) sprite.anim.curFrame--; - if (FlxG.keys.anyJustPressed([D, RIGHT])) sprite.anim.curFrame++; + if (FlxG.keys.anyJustPressed([A, LEFT])) sprite.anim.curAnim.curFrame--; + if (FlxG.keys.anyJustPressed([D, RIGHT])) sprite.anim.curAnim.curFrame++; + + if (FlxG.keys.justPressed.Q) sprite.anim.play("slidein idle point", true); + if (FlxG.keys.justPressed.W) sprite.anim.play("slidein", true); + if (FlxG.keys.justPressed.E) sprite.anim.play("death", true); + if (FlxG.keys.justPressed.R) sprite.anim.play("cannot select Label", true); + if (FlxG.keys.justPressed.T) sprite.anim.play("idle", true); } } diff --git a/source/funkin/ui/debug/results/ResultsDebugSubState.hx b/source/funkin/ui/debug/results/ResultsDebugSubState.hx index 39dd19d55..0a673679e 100644 --- a/source/funkin/ui/debug/results/ResultsDebugSubState.hx +++ b/source/funkin/ui/debug/results/ResultsDebugSubState.hx @@ -54,7 +54,7 @@ class ResultsDebugSubState extends MusicBeatSubState FlxG.switchState(() -> new funkin.play.ResultState(resultsParams)); }); }); - createToggleListItem("Character", PlayerRegistry.instance.listBaseGameEntryIds(), function(result:String) { + createToggleListItem("Character", PlayerRegistry.instance.listEntryIds(), function(result:String) { resultsParams.characterId = result; }); createToggleListItem("Results Mode", ["Debug", "Story", "Freeplay"], function(result:String) { diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index df5af8028..7e06676e9 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -1,6 +1,6 @@ package funkin.ui.freeplay; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; import flixel.group.FlxSpriteGroup; import flixel.util.FlxSort; import flixel.util.FlxTimer; @@ -32,7 +32,9 @@ class AlbumRoll extends FlxSpriteGroup return value; } - var newAlbumArt:FlxAtlasSprite; + final ALBUM_ART_SYMBOL:String = "album art placeholder"; + + var newAlbumArt:FunkinSprite; var albumTitle:Null = null; var difficultyStars:DifficultyStars; @@ -41,26 +43,14 @@ class AlbumRoll extends FlxSpriteGroup var albumData:Null = null; - final animNames:Map = [ - "volume1-active" => "ENTRANCE", - "volume2-active" => "ENTRANCE VOL2", - "volume3-active" => "ENTRANCE VOL3", - "volume1-trans" => "VOL1 TRANS", - "volume2-trans" => "VOL2 TRANS", - "volume3-trans" => "VOL3 TRANS", - "volume1-idle" => "VOL1 STILL", - "volume2-idle" => "VOL2 STILL", - "volume3-idle" => "VOL3 STILL", - ]; - public function new() { super(); - newAlbumArt = new FlxAtlasSprite((FlxG.width - 640) - FullScreenScaleMode.gameNotchSize.x, 360, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum")); + newAlbumArt = FunkinSprite.createTextureAtlas((FlxG.width + -360) - FullScreenScaleMode.gameNotchSize.x, 220, "freeplay/albumRoll/freeplayAlbum"); newAlbumArt.visible = false; - difficultyStars = new DifficultyStars((FlxG.width - 1140) - FullScreenScaleMode.gameNotchSize.x, 39); + difficultyStars = new DifficultyStars((FlxG.width - 330) - FullScreenScaleMode.gameNotchSize.x, 209); difficultyStars.visible = false; add(newAlbumArt); @@ -69,7 +59,7 @@ class AlbumRoll extends FlxSpriteGroup buildAlbumTitle("freeplay/albumRoll/volume1-text"); if (albumTitle != null) albumTitle.visible = false; - newAlbumArt.onAnimationComplete.add(onAlbumFinish); + newAlbumArt.anim.onFinish.add(onAlbumFinish); } function onAlbumFinish(animName:String):Void @@ -77,11 +67,8 @@ class AlbumRoll extends FlxSpriteGroup // Play the idle animation for the current album. if (animName != "idle") { - newAlbumArt.playAnimation('idle', true, false, true); - } - else - { - newAlbumArt.cleanupAnimation('idle'); + newAlbumArt.anim.play('idle', true); + newAlbumArt.anim.curAnim.looped = true; } } @@ -108,7 +95,7 @@ class AlbumRoll extends FlxSpriteGroup // Update the album art. var albumGraphic = Paths.image(albumData.getAlbumArtAssetKey()); - newAlbumArt.replaceFrameGraphic(0, albumGraphic); + newAlbumArt.replaceSymbolGraphic(ALBUM_ART_SYMBOL, albumGraphic); buildAlbumTitle(albumData.getAlbumTitleAssetKey(), albumData.getAlbumTitleOffsets()); applyExitMovers(); @@ -174,7 +161,7 @@ class AlbumRoll extends FlxSpriteGroup if (albumTitle != null) albumTitle.visible = false; newAlbumArt.visible = true; - newAlbumArt.playAnimation('intro', true); + newAlbumArt.anim.play('intro', true); difficultyStars.visible = false; difficultyStars.flameCheck(); @@ -190,7 +177,7 @@ class AlbumRoll extends FlxSpriteGroup { this.visible = true; // Weird workaround - newAlbumArt.playAnimation('switch', true); + newAlbumArt.anim.play('switch', true); if (albumTitle != null) albumTitle.animation.play('switch'); } diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx index c232034b7..4abab441e 100644 --- a/source/funkin/ui/freeplay/DifficultyStars.hx +++ b/source/funkin/ui/freeplay/DifficultyStars.hx @@ -1,7 +1,7 @@ package funkin.ui.freeplay; import flixel.group.FlxSpriteGroup; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.HSVShader; @:nullSafety @@ -18,7 +18,7 @@ class DifficultyStars extends FlxSpriteGroup */ public var difficulty(default, set):Int = 1; - public var stars:FlxAtlasSprite; + public var stars:FunkinSprite; public var flames:FreeplayFlames; @@ -32,7 +32,7 @@ class DifficultyStars extends FlxSpriteGroup flames = new FreeplayFlames(0, 0); - stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars")); + stars = FunkinSprite.createTextureAtlas(0, 0, "freeplay/freeplayStars"); stars.anim.play("diff stars"); add(flames); @@ -56,7 +56,7 @@ class DifficultyStars extends FlxSpriteGroup // ...... // 1300-1499: 15 stars // 1500 : 0 stars - if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100) + if (curDifficulty < 15 && stars.anim.curAnim.curFrame >= (curDifficulty + 1) * 100) { stars.anim.play("diff stars", true, false, curDifficulty * 100); } @@ -97,6 +97,7 @@ class DifficultyStars extends FlxSpriteGroup function set_curDifficulty(value:Int):Int { curDifficulty = value; + if (curDifficulty == 15) { stars.anim.play("diff stars", true, false, 1500); @@ -104,7 +105,7 @@ class DifficultyStars extends FlxSpriteGroup } else { - stars.anim.curFrame = Std.int(curDifficulty * 100); + stars.anim.curAnim.curFrame = Std.int(curDifficulty * 100); stars.anim.play("diff stars", true, false, curDifficulty * 100); } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index eb526d6b7..e59f56049 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1564,21 +1564,7 @@ class FreeplayState extends MusicBeatSubState wait: 0.1 }); add(transitionGradient); - // FlxTween.tween(transitionGradient, {alpha: 0}, 1, {ease: FlxEase.circIn}); - // for (index => capsule in grpCapsules.members) - // { - // var distFromSelected:Float = Math.abs(index - curSelected) - 1; - // if (distFromSelected < 5) - // { - // capsule.doLerp = false; - // exitMoversCharSel.set([capsule], - // { - // y: -250, - // speed: 0.8, - // wait: 0.1 - // }); - // } - // } + funnyCam.filtersEnabled = true; fadeShader.fade(0.0, 1.0, 0.8, {ease: FlxEase.quadIn, onComplete: (twn) -> funnyCam.filtersEnabled = false}); @@ -1609,6 +1595,10 @@ class FreeplayState extends MusicBeatSubState }); } } + + // TODO: i have no fucking idea why the dj position is shitty when entering from char select + // the easiest way is to just snap them back into place + if (dj != null) dj.setPosition((CUTOUT_WIDTH * DJ_POS_MULTI) + 640, 366); } var spamTimer:Float = 0; @@ -1672,7 +1662,8 @@ class FreeplayState extends MusicBeatSubState if ((controls.FREEPLAY_CHAR_SELECT && !fromCharSelect #if FEATURE_TOUCH_CONTROLS || (TouchUtil.pressAction(djHitbox, funnyCam, false) && !SwipeUtil.swipeAny) #end) - && controls.active) + && controls.active + && !FlxG.debugger.visible) { tryOpenCharSelect(); } diff --git a/source/funkin/ui/freeplay/LetterSort.hx b/source/funkin/ui/freeplay/LetterSort.hx index ca88f586c..424fc9327 100644 --- a/source/funkin/ui/freeplay/LetterSort.hx +++ b/source/funkin/ui/freeplay/LetterSort.hx @@ -12,7 +12,9 @@ import flixel.util.FlxTimer; import funkin.input.Controls; import funkin.util.SwipeUtil; import funkin.util.TouchUtil; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.math.FlxPoint; +import flixel.FlxCamera; +import funkin.graphics.FunkinSprite; import funkin.audio.FunkinSound; class LetterSort extends FlxSpriteGroup @@ -53,7 +55,7 @@ class LetterSort extends FlxSpriteGroup for (i in 0...5) { - var letter:FreeplayLetter = new FreeplayLetter(i * 80, 0, i); + var letter:FreeplayLetter = new FreeplayLetter(i * 80, 0, i, curSelection); letter.x += 50; letter.y += 50; // letter.visible = false; @@ -247,9 +249,9 @@ class LetterSort extends FlxSpriteGroup } /** - * The actual FlxAtlasSprite for the letters, with their animation code stuff and regex stuff + * The actual FunkinSprite for the letters, with their animation code stuff and regex stuff */ -class FreeplayLetter extends FlxAtlasSprite +class FreeplayLetter extends FunkinSprite { /** * A preformatted array of letter strings, for use when doing regex @@ -268,9 +270,11 @@ class FreeplayLetter extends FlxAtlasSprite */ public var curLetter:Int = 0; - public function new(x:Float, y:Float, ?letterInd:Int) + public function new(x:Float, y:Float, ?letterInd:Int, curSelected:Int = 0) { - super(x, y, Paths.animateAtlas("freeplay/sortedLetters")); + super(x, y); + + loadTextureAtlas("freeplay/sortedLetters"); // this is used for the regex // /^[OR].*/gi doesn't work for showing the song Pico, so now it's @@ -290,11 +294,16 @@ class FreeplayLetter extends FlxAtlasSprite if (letterInd != null) { - this.anim.play(animLetters[letterInd] + " move"); - this.anim.pause(); + this.anim.play(animLetters[letterInd] + " move", true); curLetter = letterInd; - this.anim.onComplete.add(function() { - this.anim.play(animLetters[curLetter] + " move"); + + if (curSelected != curLetter) + { + this.anim.pause(); + } + + this.anim.onFinish.add(function(name:String) { + this.anim.play(animLetters[curLetter] + " move", true); }); } } @@ -328,6 +337,16 @@ class FreeplayLetter extends FlxAtlasSprite { this.anim.pause(); } - // updateHitbox(); + } + + /** + * Offset the letter. + */ + override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + { + var output:FlxPoint = super.getScreenPosition(result, camera); + output.x -= 50; + output.y -= 60; + return output; } } diff --git a/source/funkin/ui/freeplay/backcards/BackingCard.hx b/source/funkin/ui/freeplay/backcards/BackingCard.hx index 35faac9ba..103fe4604 100644 --- a/source/funkin/ui/freeplay/backcards/BackingCard.hx +++ b/source/funkin/ui/freeplay/backcards/BackingCard.hx @@ -6,7 +6,6 @@ import flixel.math.FlxPoint; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxSpriteUtil; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.FunkinSprite; import funkin.ui.freeplay.charselect.PlayableCharacter; import openfl.display.BlendMode; @@ -24,7 +23,7 @@ import funkin.util.BitmapUtil; */ class BackingCard extends FlxSpriteGroup implements IBPMSyncedScriptedClass implements IStateChangingScriptedClass implements IFreeplayScriptedClass { - public var backingTextYeah:FlxAtlasSprite; + public var backingTextYeah:FunkinSprite; public var orangeBackShit:FunkinSprite; public var alsoOrangeLOL:FunkinSprite; public var pinkBack:FunkinSprite; @@ -58,15 +57,7 @@ class BackingCard extends FlxSpriteGroup implements IBPMSyncedScriptedClass impl orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); - backingTextYeah = new FlxAtlasSprite((FreeplayState.CUTOUT_WIDTH * FreeplayState.DJ_POS_MULTI) + 640, 370, - Paths.animateAtlas("freeplay/backing-text-yeah"), { - FrameRate: 24.0, - Reversed: false, - // ?OnComplete:Void -> Void, - ShowPivot: false, - Antialiasing: true, - ScrollFactor: new FlxPoint(1, 1), - }); + backingTextYeah = FunkinSprite.createTextureAtlas((FreeplayState.CUTOUT_WIDTH * FreeplayState.DJ_POS_MULTI) + -320, 120, "freeplay/backing-text-yeah",); pinkBack.color = 0xFFFFD4E9; // sets it to pink! pinkBack.x -= pinkBack.width; diff --git a/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx b/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx index bb05cfe22..a10e898bb 100644 --- a/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx +++ b/source/funkin/ui/freeplay/backcards/NewCharacterCard.hx @@ -6,15 +6,12 @@ import flixel.FlxSprite; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.modding.events.ScriptEvent; import openfl.display.BlendMode; import funkin.util.BitmapUtil; class NewCharacterCard extends BackingCard { - var confirmAtlas:FlxAtlasSprite; - var darkBg:FlxSprite; var lightLayer:FlxSprite; var multiply1:FlxSprite; @@ -261,10 +258,4 @@ class NewCharacterCard extends BackingCard cardGlow.scale.set(1, 1); FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); } - - override public function confirm():Void - { - // confirmAtlas.visible = true; - // confirmAtlas.anim.play(""); - } } diff --git a/source/funkin/ui/freeplay/dj/AnimateAtlasFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/AnimateAtlasFreeplayDJ.hx index 5a31b0861..9573666e7 100644 --- a/source/funkin/ui/freeplay/dj/AnimateAtlasFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/AnimateAtlasFreeplayDJ.hx @@ -1,7 +1,5 @@ package funkin.ui.freeplay.dj; -import flixel.util.FlxSignal; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.data.freeplay.player.PlayerRegistry; /** @@ -11,15 +9,31 @@ import funkin.data.freeplay.player.PlayerRegistry; @:hscriptClass class ScriptedAnimateAtlasFreeplayDJ extends AnimateAtlasFreeplayDJ implements polymod.hscript.HScriptedClass {} +/** + * An AnimateAtlasFreeplayDJ is a Freeplay DJ which is rendered by + * displaying an animation derived from an Adobe Animate texture atlas spritesheet file. + * + * BaseFreeplayDJ has game logic, AnimateAtlasFreeplayDJ has only rendering logic. + * KEEP THEM SEPARATE! + */ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ { public function new(x:Float, y:Float, characterId:String) { super(x, y, characterId); - loadAtlas(Paths.animateAtlas(playableCharData.getAssetPath())); - onAnimationComplete.add(onFinishAnim); - onAnimationLoop.add(onFinishAnim); + loadTextureAtlas(playableCharData?.getAssetPath(), + { + swfMode: true + }); + + if (playableCharData?.useApplyStageMatrix() ?? false) + { + this.applyStageMatrix = true; + } + + animation.onFinish.add(onFinishAnim); + animation.onLoop.add(onFinishAnim); } public override function update(elapsed:Float):Void @@ -29,7 +43,7 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ case Intro: // Play the intro animation then leave this state immediately. var animPrefix = playableCharData?.getAnimationPrefix('intro'); - if (animPrefix != null && (getCurrentAnimation() != animPrefix || !this.anim.isPlaying)) + if (animPrefix != null && (getCurrentAnimation() != animPrefix)) { playFlashAnimation(animPrefix, true); } @@ -63,7 +77,7 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ if (getCurrentAnimation() == animPrefixA) { var endFrame = playableCharData?.getFistPumpIntroEndFrame() ?? 0; - if (endFrame > -1 && anim.curFrame >= endFrame) + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) { playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpIntroStartFrame()); } @@ -71,7 +85,7 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ else if (getCurrentAnimation() == animPrefixB) { var endFrame = playableCharData?.getFistPumpIntroBadEndFrame() ?? 0; - if (endFrame > -1 && anim.curFrame >= endFrame) + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) { playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpIntroBadStartFrame()); } @@ -88,7 +102,7 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ if (getCurrentAnimation() == animPrefixA) { var endFrame = playableCharData?.getFistPumpLoopEndFrame() ?? 0; - if (endFrame > -1 && anim.curFrame >= endFrame) + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) { playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpLoopStartFrame()); } @@ -96,7 +110,7 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ else if (getCurrentAnimation() == animPrefixB) { var endFrame = playableCharData?.getFistPumpLoopBadEndFrame() ?? 0; - if (endFrame > -1 && anim.curFrame >= endFrame) + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) { playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpLoopBadStartFrame()); } @@ -131,14 +145,12 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ } // Call the superclass function AFTER updating the current state and playing the next animation. - // This ensures that FlxAnimate starts rendering the new animation immediately. + // This ensures that flixel-animate starts rendering the new animation immediately. super.update(elapsed); } override function onFinishAnim(name:String):Void { - // var name = anim.curSymbol.name; - if (name == playableCharData?.getAnimationPrefix('intro')) { if (PlayerRegistry.instance.hasNewCharacter()) @@ -183,25 +195,6 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ // trace('Finished loss reaction'); currentState = Idle; } - else if (name == playableCharData?.getAnimationPrefix('cartoon')) - { - // trace('Finished cartoon'); - - var frame:Int = FlxG.random.bool(33) ? (playableCharData?.getCartoonLoopBlinkFrame() ?? 0) : (playableCharData?.getCartoonLoopFrame() ?? 0); - - // Character switches channels when the video ends, or at a 10% chance each time his idle loops. - if (FlxG.random.bool(5)) - { - frame = playableCharData?.getCartoonChannelChangeFrame() ?? 0; - // boyfriend switches channel code? - // Transefer into bf.hxc in scripts/freeplay/dj - // runTvLogic(); - } - trace('Replay idle: ${frame}'); - var animPrefix = playableCharData?.getAnimationPrefix('cartoon'); - if (animPrefix != null) playFlashAnimation(animPrefix, true, false, false, frame); - // trace('Finished confirm'); - } else if (name == playableCharData?.getAnimationPrefix('newUnlock')) { // Animation should loop. @@ -216,25 +209,14 @@ class AnimateAtlasFreeplayDJ extends BaseFreeplayDJ } } - override public function listAnimations():Array - { - var anims:Array = []; - @:privateAccess - for (animKey in anim.symbolDictionary) - anims.push(animKey.name); - - return anims; - } - - override public function getCurrentAnimation():String - { - if (this.anim == null || this.anim.curSymbol == null) return ""; - return this.anim.curSymbol.name; - } - override public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void { - playAnimation(id, Force, Reverse, Loop, Frame); - applyAnimOffset(); + animation.play(id, Force, Reverse, Frame); + + if (animation.curAnim != null) + { + animation.curAnim.looped = Loop; + } + applyAnimationOffset(); } } diff --git a/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx index 25f84a302..90ea98833 100644 --- a/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx @@ -1,16 +1,11 @@ package funkin.ui.freeplay.dj; -import flixel.graphics.frames.FlxFrame; -import flixel.FlxCamera; -import flixel.FlxBasic; import flixel.util.FlxSignal; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import funkin.audio.FunkinSound; +import funkin.graphics.FunkinSprite; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.freeplay.player.PlayerData.PlayerFreeplayDJData; -import funkin.util.assets.FlxAnimationUtil; -import funkin.modding.events.ScriptEvent; import funkin.modding.IScriptedClass.IFreeplayScriptedClass; +import funkin.modding.events.ScriptEvent; enum FreeplayDJState { @@ -64,7 +59,7 @@ enum FreeplayDJState } @:nullSafety -class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass +class BaseFreeplayDJ extends FunkinSprite implements IFreeplayScriptedClass { public var IDLE_EGG_PERIOD:Float = 60.0; public var IDLE_CARTOON_PERIOD:Float = 120.0; @@ -96,7 +91,7 @@ class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass final playableChar = PlayerRegistry.instance.fetchEntry(characterId); playableCharData = playableChar?.getFreeplayDJData(); - super(x, y, null, null, false); + super(x, y); } function onFinishAnim(name:String):Void {} @@ -109,7 +104,7 @@ class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void { // playAnimationSimple(id, Force, Reverse, Loop, Frame); - applyAnimOffset(); + applyAnimationOffset(); } public function onPlayerAction():Void @@ -213,32 +208,36 @@ class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass if (animPrefix != null) playFlashAnimation(animPrefix, true, false, false, playableCharData?.getFistPumpLoopBadStartFrame()); } - override public function listAnimations():Array + function applyAnimationOffset():Void { - // this is base dj dum dum, there's no animations - return []; - } + var animationName:String = getCurrentAnimation(); + var animationOffsets:Null> = playableCharData?.getAnimationOffsetsByPrefix(animationName); + var globalOffsets:Array = [this.x, this.y]; - override public function getCurrentAnimation():String - { - // this is base dj dum dum, there's no animations - return ""; - } - - function applyAnimOffset() - { - var animName = getCurrentAnimation(); - trace(animName); - var daOffset = playableCharData?.getAnimationOffsetsByPrefix(animName); - trace(daOffset); - if (daOffset != null) + if (animationOffsets != null) { - final xValue = daOffset[0]; - final yValue = daOffset[1]; - offset.set(xValue, yValue); + var finalOffsetX:Float = 0; + var finalOffsetY:Float = 0; + + if (this.applyStageMatrix) + { + finalOffsetX = animationOffsets[0]; + finalOffsetY = animationOffsets[1]; + } + else + { + finalOffsetX = globalOffsets[0] - animationOffsets[0] - (FreeplayState.CUTOUT_WIDTH * FreeplayState.DJ_POS_MULTI); + finalOffsetY = globalOffsets[1] - animationOffsets[1]; + } + + trace('Successfully applied offset ($animationName): ' + animationOffsets[0] + ', ' + animationOffsets[1]); + offset.set(finalOffsetX, finalOffsetY); } else + { + trace('No offset found ($animationName), defaulting to: 0, 0'); offset.set(0, 0); + } } public function onScriptEvent(event:ScriptEvent) {} @@ -299,344 +298,3 @@ class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass */ public function onFreeplayClose(event:FreeplayScriptEvent):Void {} } - -// Class for all non-atalas DJ's -class FlixelFramedFreeplayDJ extends BaseFreeplayDJ -{ - public function new(x:Float, y:Float, characterId:String) - { - super(x, y, characterId); - - loadFrames(); - loadAnimations(); - - animation.onFinish.add(onFinishAnim); - animation.onLoop.add(onFinishAnim); - - // animation.onFrameChange.add((name, num, index) -> trace('name:$name, num:$num, index:$index')); - } - - public function loadFrames():Void - { - trace("OVERRIDE ME"); - trace("LOADING FRAMES FUNC"); - } - - public function loadAnimations():Void - { - trace('[SPARROWCHAR] Loading ${playableCharData.getAnimationsList().length} animations for ${characterId}'); - - FlxAnimationUtil.addAtlasAnimations(this, playableCharData.getAnimationsList()); - - var animNames = this.animation.getNameList(); - trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); - } - - override function applyAnimOffset() - { - var animName = getCurrentAnimation(); - trace(animName); - var daOffset = playableCharData?.getAnimationOffsets(animName); - trace(daOffset); - if (daOffset != null) - { - final xValue = daOffset[0]; - final yValue = daOffset[1]; - offset.set(xValue, yValue); - } - else - offset.set(0, 0); - } - - override public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void - { - // No LOOP logic, because default Flixel's FlxAnimationController can normaly work with LOOPED animations - animation.play(id, Force, Reverse, Frame); - applyAnimOffset(); - } - - override public function listAnimations():Array - { - return animation.getNameList() ?? []; - } - - override public function getCurrentAnimation():String - { - return animation?.curAnim?.name ?? ""; - } - - override function updateAnimation(elapsed:Float):Void - { - animation.update(elapsed); - } - - override public function hasAnimation(anim:String):Bool - { - return listAnimations().contains(anim); - } - - override public function toCharSelect():Void - { - if (hasAnimation('charSelect')) - { - currentState = CharSelect; - playFlashAnimation('charSelect', true, false, false, 0); - } - else - { - FlxG.log.warn("Freeplay character does not have 'charSelect' animation!"); - currentState = Confirm; - // Call this immediately; otherwise, we get locked out of Character Select. - onCharSelectComplete(); - } - } - - override public function fistPumpIntro():Void - { - // We really don't want to play anything but the new character animation here. - if (PlayerRegistry.instance.hasNewCharacter()) - { - currentState = NewUnlock; - return; - } - - currentState = FistPumpIntro; - playFlashAnimation('fistPumpIntro', true, false, false); - } - - override public function fistPump():Void - { - // We really don't want to play anything but the new character animation here. - if (PlayerRegistry.instance.hasNewCharacter()) - { - currentState = NewUnlock; - return; - } - - currentState = FistPump; - playFlashAnimation('fistPump', true, false, false); - } - - override public function fistPumpLossIntro():Void - { - // We really don't want to play anything but the new character animation here. - if (PlayerRegistry.instance.hasNewCharacter()) - { - currentState = NewUnlock; - return; - } - - currentState = FistPumpIntro; - playFlashAnimation('lossIntro', true, false, false); - } - - override public function fistPumpLoss():Void - { - // We really don't want to play anything but the new character animation here. - if (PlayerRegistry.instance.hasNewCharacter()) - { - currentState = NewUnlock; - return; - } - - currentState = FistPump; - playFlashAnimation('loss', true, false, false); - } - - public override function update(elapsed:Float):Void - { - switch (currentState) - { - case Intro: - // Play the intro animation then leave this state immediately. - if (hasAnimation('intro') && (getCurrentAnimation() != 'intro' || this.animation.finished)) playFlashAnimation('intro', true); - timeIdling = 0; - case Idle: - // We are in this state the majority of the time. - if (hasAnimation('idle') && getCurrentAnimation() != 'idle') playFlashAnimation('idle', true, false, true); - - timeIdling += elapsed; - case NewUnlock: - if (!hasAnimation('newUnlock')) - { - currentState = Idle; - } - if (hasAnimation('newUnlock') && getCurrentAnimation() != 'newUnlock') - { - playFlashAnimation('newUnlock', true, false, true); - } - case Confirm: - if (hasAnimation('confirm') && getCurrentAnimation() != 'confirm') playFlashAnimation('confirm', false); - timeIdling = 0; - case FistPumpIntro: - // I shit my self - PurSnake - case FistPump: - // Twice - case IdleEasterEgg: - if (hasAnimation('idleEasterEgg') && getCurrentAnimation() != 'idleEasterEgg') - { - onIdleEasterEgg.dispatch(); - playFlashAnimation('idleEasterEgg', false); - seenIdleEasterEgg = true; - } - timeIdling = 0; - case Cartoon: - if (!hasAnimation('cartoon')) - { - currentState = IdleEasterEgg; - } - else - { - if (getCurrentAnimation() != 'cartoon') playFlashAnimation('cartoon', true); - timeIdling = 0; - } - default: - // I shit myself. - } - - super.update(elapsed); - } - - override function onFinishAnim(name:String):Void - { - if (name == 'intro') - { - if (PlayerRegistry.instance.hasNewCharacter()) - { - currentState = NewUnlock; - } - else - { - currentState = Idle; - } - onIntroDone.dispatch(); - } - else if (name == 'idle') - { - // trace('Finished idle') - if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) - { - currentState = IdleEasterEgg; - } - else if (timeIdling >= IDLE_CARTOON_PERIOD) - { - currentState = Cartoon; - } - } - else if (name == 'confirm') - { - // trace('Finished confirm'); - } - else if (name == 'fistPump') - { - // trace('Finished fist pump'); - currentState = Idle; - } - else if (name == 'idleEasterEgg') - { - // trace('Finished spook'); - currentState = Idle; - } - else if (name == 'loss') - { - // trace('Finished loss reaction'); - currentState = Idle; - } - else if (name == 'cartoon') - { - // trace('Finished cartoon'); - - // var frame:Int = FlxG.random.bool(33) ? (playableCharData?.getCartoonLoopBlinkFrame() ?? 0) : (playableCharData?.getCartoonLoopFrame() ?? 0); - - // Character switches channels when the video ends, or at a 10% chance each time his idle loops. - /*if (FlxG.random.bool(5)) - { - frame = playableCharData?.getCartoonChannelChangeFrame() ?? 0; - // boyfriend switches channel code? - // Transefer into bf.hxc in scripts/freeplay/dj - // runTvLogic(); - } - trace('Replay idle: ${frame}'); */ - playFlashAnimation('cartoon', true, false, false); - // YOU BETTER REDO THIS IN YOUR SCRIPT WITH DIFFERENT ANIMATIONS - // trace('Finished confirm'); - } - else if (name == 'newUnlock') - { - // Animation should loop. - } - else if (name == 'charSelect') - { - onCharSelectComplete(); - } - else - { - trace('Finished ${name}'); - } - } - - /// Draw - Logic - override public function draw():Void - { - checkEmptyFrame(); - - if (alpha == 0 || _frame.type == FlxFrameType.EMPTY) return; - - if (dirty) // rarely - calcFrame(useFramePixels); - - for (camera in getCamerasLegacy()) - { - if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue; - - if (isSimpleRender(camera)) drawSimple(camera); - else - drawComplex(camera); - - #if FLX_DEBUG - FlxBasic.visibleCount++; - #end - } - - #if FLX_DEBUG - if (FlxG.debugger.drawDebug) drawDebug(); - #end - } - - @:noCompletion - override function drawSimple(camera:FlxCamera):Void - { - getScreenPosition(_point, camera).subtract(offset); - if (isPixelPerfectRender(camera)) _point.floor(); - - _point.copyTo(_flashPoint); - camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing); - } - - @:noCompletion - override function drawComplex(camera:FlxCamera):Void - { - _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY()); - _matrix.translate(-origin.x, -origin.y); - _matrix.scale(scale.x, scale.y); - - if (bakedRotationAngle <= 0) - { - updateTrig(); - - if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle); - } - - getScreenPosition(_point, camera).subtract(offset); - _point.add(origin.x, origin.y); - _matrix.translate(_point.x, _point.y); - - if (isPixelPerfectRender(camera)) - { - _matrix.tx = Math.floor(_matrix.tx); - _matrix.ty = Math.floor(_matrix.ty); - } - - camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader); - } -} diff --git a/source/funkin/ui/freeplay/dj/MultiSparrowFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/MultiSparrowFreeplayDJ.hx index d33a2c401..bc6e89397 100644 --- a/source/funkin/ui/freeplay/dj/MultiSparrowFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/MultiSparrowFreeplayDJ.hx @@ -2,10 +2,8 @@ package funkin.ui.freeplay.dj; import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxAtlasFrames; +import funkin.util.assets.FlxAnimationUtil; import funkin.data.freeplay.player.PlayerRegistry; -import flixel.graphics.frames.FlxFrame; -import flixel.FlxCamera; -import flixel.FlxBasic; /** * A script that can be tied to a MultiSparrowFreeplayDJ. @@ -14,14 +12,27 @@ import flixel.FlxBasic; @:hscriptClass class ScriptedMultiSparrowFreeplayDJ extends MultiSparrowFreeplayDJ implements polymod.hscript.HScriptedClass {} -class MultiSparrowFreeplayDJ extends BaseFreeplayDJ.FlixelFramedFreeplayDJ +/** + * For some Freeplay DJs which use Sparrow atlases, the spritesheets need to be split + * into multiple files. This Freeplay DJ renderer concatenates these together into a single sprite. + * + * BaseFreeplayDJ has game logic, MultiSparrowFreeplayDJ has only rendering logic. + * KEEP THEM SEPARATE! + */ +class MultiSparrowFreeplayDJ extends BaseFreeplayDJ { public function new(x:Float, y:Float, characterId:String) { super(x, y, characterId); + + loadFrames(); + loadAnimations(); + + animation.onFinish.add(onFinishAnim); + animation.onLoop.add(onFinishAnim); } - override public function loadFrames():Void + public function loadFrames():Void { trace('Loading assets for Multi-Sparrow character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0")); @@ -57,4 +68,198 @@ class MultiSparrowFreeplayDJ extends BaseFreeplayDJ.FlixelFramedFreeplayDJ } this.frames = texture; } + + public function loadAnimations():Void + { + trace('[MULTISPARROWDJ] Loading ${playableCharData.getAnimationsList().length} animations for ${characterId}'); + + FlxAnimationUtil.addAtlasAnimations(this, playableCharData.getAnimationsList()); + + var animationList:Array = this.animation.getNameList(); + trace('[MULTISPARROWDJ] Successfully loaded ${animationList.length} animations for ${characterId}'); + } + + public override function update(elapsed:Float):Void + { + switch (currentState) + { + case Intro: + // Play the intro animation then leave this state immediately. + var animPrefix = playableCharData?.getAnimationPrefix('intro'); + if (animPrefix != null && (getCurrentAnimation() != animPrefix)) + { + playFlashAnimation(animPrefix, true); + } + timeIdling = 0; + case Idle: + // We are in this state the majority of the time. + var animPrefix = playableCharData?.getAnimationPrefix('idle'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + timeIdling += elapsed; + case NewUnlock: + var animPrefix = playableCharData?.getAnimationPrefix('newUnlock'); + if (animPrefix != null && !hasAnimation(animPrefix)) + { + currentState = Idle; + } + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + case Confirm: + var animPrefix = playableCharData?.getAnimationPrefix('confirm'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); + timeIdling = 0; + case FistPumpIntro: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpIntroEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpIntroStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpIntroBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpIntroBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation()); + } + + case FistPump: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpLoopEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpLoopStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpLoopBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpLoopBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation()); + } + + case IdleEasterEgg: + var animPrefix = playableCharData?.getAnimationPrefix('idleEasterEgg'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + onIdleEasterEgg.dispatch(); + playFlashAnimation(animPrefix, false); + seenIdleEasterEgg = true; + } + timeIdling = 0; + case Cartoon: + var animPrefix = playableCharData?.getAnimationPrefix('cartoon'); + if (animPrefix == null) + { + currentState = IdleEasterEgg; + } + else + { + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + } + default: + // I shit myself. + } + + // Call the superclass function AFTER updating the current state and playing the next animation. + // This ensures that flixel-animate starts rendering the new animation immediately. + super.update(elapsed); + } + + override function onFinishAnim(name:String):Void + { + if (name == playableCharData?.getAnimationPrefix('intro')) + { + if (PlayerRegistry.instance.hasNewCharacter()) + { + currentState = NewUnlock; + } + else + { + currentState = Idle; + } + onIntroDone.dispatch(); + } + else if (name == playableCharData?.getAnimationPrefix('idle')) + { + // trace('Finished idle'); + + if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) + { + currentState = IdleEasterEgg; + } + else if (timeIdling >= IDLE_CARTOON_PERIOD) + { + currentState = Cartoon; + } + } + else if (name == playableCharData?.getAnimationPrefix('confirm')) + { + // trace('Finished confirm'); + } + else if (name == playableCharData?.getAnimationPrefix('fistPump')) + { + // trace('Finished fist pump'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('idleEasterEgg')) + { + // trace('Finished spook'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('loss')) + { + // trace('Finished loss reaction'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('newUnlock')) + { + // Animation should loop. + } + else if (name == playableCharData?.getAnimationPrefix('charSelect')) + { + onCharSelectComplete(); + } + else + { + trace('Finished ${name}'); + } + } + + override public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void + { + animation.play(id, Force, Reverse, Frame); + + if (animation.curAnim != null) + { + animation.curAnim.looped = Loop; + } + applyAnimationOffset(); + } } diff --git a/source/funkin/ui/freeplay/dj/PackerFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/PackerFreeplayDJ.hx index b29f6068f..04450e19b 100644 --- a/source/funkin/ui/freeplay/dj/PackerFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/PackerFreeplayDJ.hx @@ -1,10 +1,8 @@ package funkin.ui.freeplay.dj; import flixel.graphics.frames.FlxFramesCollection; +import funkin.util.assets.FlxAnimationUtil; import funkin.data.freeplay.player.PlayerRegistry; -import flixel.graphics.frames.FlxFrame; -import flixel.FlxCamera; -import flixel.FlxBasic; /** * A script that can be tied to a PackerFreeplayDJ. @@ -13,21 +11,228 @@ import flixel.FlxBasic; @:hscriptClass class ScriptedPackerFreeplayDJ extends PackerFreeplayDJ implements polymod.hscript.HScriptedClass {} -class PackerFreeplayDJ extends BaseFreeplayDJ.FlixelFramedFreeplayDJ +/** + * A PackerFreeplayDJ is a Freeplay DJ which is rendered by + * displaying an animation derived from a Packer spritesheet file. + * + * BaseFreeplayDJ has game logic, PackerFreeplayDJ has only rendering logic. + * KEEP THEM SEPARATE! + */ +class PackerFreeplayDJ extends BaseFreeplayDJ { public function new(x:Float, y:Float, characterId:String) { super(x, y, characterId); + + loadFrames(); + loadAnimations(); + + animation.onFinish.add(onFinishAnim); + animation.onLoop.add(onFinishAnim); } - override public function loadFrames() + public function loadFrames():Void { final tex:FlxFramesCollection = Paths.getPackerAtlas(playableCharData.getAssetPath()); if (tex == null) { - trace('Could not load Packer sprite: ${playableCharData.getAssetPath()}'); + trace('Could not load sparrow sprite: ' + playableCharData.getAssetPath()); return; } this.frames = tex; } + + public function loadAnimations():Void + { + trace('[PACKERDJ] Loading ${playableCharData.getAnimationsList().length} animations for ${characterId}'); + + FlxAnimationUtil.addAtlasAnimations(this, playableCharData.getAnimationsList()); + + var animationList:Array = this.animation.getNameList(); + trace('[PACKERDJ] Successfully loaded ${animationList.length} animations for ${characterId}'); + } + + public override function update(elapsed:Float):Void + { + switch (currentState) + { + case Intro: + // Play the intro animation then leave this state immediately. + var animPrefix = playableCharData?.getAnimationPrefix('intro'); + if (animPrefix != null && (getCurrentAnimation() != animPrefix)) + { + playFlashAnimation(animPrefix, true); + } + timeIdling = 0; + case Idle: + // We are in this state the majority of the time. + var animPrefix = playableCharData?.getAnimationPrefix('idle'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + timeIdling += elapsed; + case NewUnlock: + var animPrefix = playableCharData?.getAnimationPrefix('newUnlock'); + if (animPrefix != null && !hasAnimation(animPrefix)) + { + currentState = Idle; + } + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + case Confirm: + var animPrefix = playableCharData?.getAnimationPrefix('confirm'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); + timeIdling = 0; + case FistPumpIntro: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpIntroEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpIntroStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpIntroBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpIntroBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation()); + } + + case FistPump: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpLoopEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpLoopStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpLoopBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpLoopBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation()); + } + + case IdleEasterEgg: + var animPrefix = playableCharData?.getAnimationPrefix('idleEasterEgg'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + onIdleEasterEgg.dispatch(); + playFlashAnimation(animPrefix, false); + seenIdleEasterEgg = true; + } + timeIdling = 0; + case Cartoon: + var animPrefix = playableCharData?.getAnimationPrefix('cartoon'); + if (animPrefix == null) + { + currentState = IdleEasterEgg; + } + else + { + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + } + default: + // I shit myself. + } + + // Call the superclass function AFTER updating the current state and playing the next animation. + // This ensures that flixel-animate starts rendering the new animation immediately. + super.update(elapsed); + } + + override function onFinishAnim(name:String):Void + { + if (name == playableCharData?.getAnimationPrefix('intro')) + { + if (PlayerRegistry.instance.hasNewCharacter()) + { + currentState = NewUnlock; + } + else + { + currentState = Idle; + } + onIntroDone.dispatch(); + } + else if (name == playableCharData?.getAnimationPrefix('idle')) + { + // trace('Finished idle'); + + if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) + { + currentState = IdleEasterEgg; + } + else if (timeIdling >= IDLE_CARTOON_PERIOD) + { + currentState = Cartoon; + } + } + else if (name == playableCharData?.getAnimationPrefix('confirm')) + { + // trace('Finished confirm'); + } + else if (name == playableCharData?.getAnimationPrefix('fistPump')) + { + // trace('Finished fist pump'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('idleEasterEgg')) + { + // trace('Finished spook'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('loss')) + { + // trace('Finished loss reaction'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('newUnlock')) + { + // Animation should loop. + } + else if (name == playableCharData?.getAnimationPrefix('charSelect')) + { + onCharSelectComplete(); + } + else + { + trace('Finished ${name}'); + } + } + + override public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void + { + animation.play(id, Force, Reverse, Frame); + + if (animation.curAnim != null) + { + animation.curAnim.looped = Loop; + } + applyAnimationOffset(); + } } diff --git a/source/funkin/ui/freeplay/dj/SparrowFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/SparrowFreeplayDJ.hx index ff156e3fc..454cbdb9d 100644 --- a/source/funkin/ui/freeplay/dj/SparrowFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/SparrowFreeplayDJ.hx @@ -1,10 +1,8 @@ package funkin.ui.freeplay.dj; import flixel.graphics.frames.FlxFramesCollection; +import funkin.util.assets.FlxAnimationUtil; import funkin.data.freeplay.player.PlayerRegistry; -import flixel.graphics.frames.FlxFrame; -import flixel.FlxCamera; -import flixel.FlxBasic; /** * A script that can be tied to a SparrowFreeplayDJ. @@ -13,21 +11,228 @@ import flixel.FlxBasic; @:hscriptClass class ScriptedSparrowFreeplayDJ extends SparrowFreeplayDJ implements polymod.hscript.HScriptedClass {} -class SparrowFreeplayDJ extends BaseFreeplayDJ.FlixelFramedFreeplayDJ +/** + * A SparrowFreeplayDJ is a Freeplay DJ which is rendered by + * displaying an animation derived from a SparrowV2 atlas spritesheet file. + * + * BaseFreeplayDJ has game logic, SparrowFreeplayDJ has only rendering logic. + * KEEP THEM SEPARATE! + */ +class SparrowFreeplayDJ extends BaseFreeplayDJ { public function new(x:Float, y:Float, characterId:String) { super(x, y, characterId); + + loadFrames(); + loadAnimations(); + + animation.onFinish.add(onFinishAnim); + animation.onLoop.add(onFinishAnim); } - override public function loadFrames() + public function loadFrames():Void { final tex:FlxFramesCollection = Paths.getSparrowAtlas(playableCharData.getAssetPath()); if (tex == null) { - trace('Could not load Sparrow sprite: ' + playableCharData.getAssetPath()); + trace('Could not load sparrow sprite: ' + playableCharData.getAssetPath()); return; } this.frames = tex; } + + public function loadAnimations():Void + { + trace('[SPARROWDJ] Loading ${playableCharData.getAnimationsList().length} animations for ${characterId}'); + + FlxAnimationUtil.addAtlasAnimations(this, playableCharData.getAnimationsList()); + + var animationList:Array = this.animation.getNameList(); + trace('[SPARROWDJ] Successfully loaded ${animationList.length} animations for ${characterId}'); + } + + public override function update(elapsed:Float):Void + { + switch (currentState) + { + case Intro: + // Play the intro animation then leave this state immediately. + var animPrefix = playableCharData?.getAnimationPrefix('intro'); + if (animPrefix != null && (getCurrentAnimation() != animPrefix)) + { + playFlashAnimation(animPrefix, true); + } + timeIdling = 0; + case Idle: + // We are in this state the majority of the time. + var animPrefix = playableCharData?.getAnimationPrefix('idle'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + timeIdling += elapsed; + case NewUnlock: + var animPrefix = playableCharData?.getAnimationPrefix('newUnlock'); + if (animPrefix != null && !hasAnimation(animPrefix)) + { + currentState = Idle; + } + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } + case Confirm: + var animPrefix = playableCharData?.getAnimationPrefix('confirm'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); + timeIdling = 0; + case FistPumpIntro: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpIntroEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpIntroStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpIntroBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpIntroBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation()); + } + + case FistPump: + var animPrefixA = playableCharData?.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData?.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData?.getFistPumpLoopEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData?.getFistPumpLoopStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData?.getFistPumpLoopBadEndFrame() ?? 0; + if (endFrame > -1 && anim.curAnim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData?.getFistPumpLoopBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation()); + } + + case IdleEasterEgg: + var animPrefix = playableCharData?.getAnimationPrefix('idleEasterEgg'); + if (animPrefix != null && getCurrentAnimation() != animPrefix) + { + onIdleEasterEgg.dispatch(); + playFlashAnimation(animPrefix, false); + seenIdleEasterEgg = true; + } + timeIdling = 0; + case Cartoon: + var animPrefix = playableCharData?.getAnimationPrefix('cartoon'); + if (animPrefix == null) + { + currentState = IdleEasterEgg; + } + else + { + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + } + default: + // I shit myself. + } + + // Call the superclass function AFTER updating the current state and playing the next animation. + // This ensures that flixel-animate starts rendering the new animation immediately. + super.update(elapsed); + } + + override function onFinishAnim(name:String):Void + { + if (name == playableCharData?.getAnimationPrefix('intro')) + { + if (PlayerRegistry.instance.hasNewCharacter()) + { + currentState = NewUnlock; + } + else + { + currentState = Idle; + } + onIntroDone.dispatch(); + } + else if (name == playableCharData?.getAnimationPrefix('idle')) + { + // trace('Finished idle'); + + if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) + { + currentState = IdleEasterEgg; + } + else if (timeIdling >= IDLE_CARTOON_PERIOD) + { + currentState = Cartoon; + } + } + else if (name == playableCharData?.getAnimationPrefix('confirm')) + { + // trace('Finished confirm'); + } + else if (name == playableCharData?.getAnimationPrefix('fistPump')) + { + // trace('Finished fist pump'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('idleEasterEgg')) + { + // trace('Finished spook'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('loss')) + { + // trace('Finished loss reaction'); + currentState = Idle; + } + else if (name == playableCharData?.getAnimationPrefix('newUnlock')) + { + // Animation should loop. + } + else if (name == playableCharData?.getAnimationPrefix('charSelect')) + { + onCharSelectComplete(); + } + else + { + trace('Finished ${name}'); + } + } + + override public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void + { + animation.play(id, Force, Reverse, Frame); + + if (animation.curAnim != null) + { + animation.curAnim.looped = Loop; + } + applyAnimationOffset(); + } } diff --git a/source/funkin/util/assets/FlxAnimationUtil.hx b/source/funkin/util/assets/FlxAnimationUtil.hx index cf435cdff..bfa0556ad 100644 --- a/source/funkin/util/assets/FlxAnimationUtil.hx +++ b/source/funkin/util/assets/FlxAnimationUtil.hx @@ -1,6 +1,7 @@ package funkin.util.assets; import flixel.FlxSprite; +import funkin.graphics.FunkinSprite; import flixel.graphics.frames.FlxFramesCollection; import funkin.data.animation.AnimationData; @@ -13,22 +14,55 @@ class FlxAnimationUtil public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData):Void { if (anim.prefix == null) return; - var frameRate = anim.frameRate == null ? 24 : anim.frameRate; - var looped = anim.looped == null ? false : anim.looped; - var flipX = anim.flipX == null ? false : anim.flipX; - var flipY = anim.flipY == null ? false : anim.flipY; + + var frameRate:Int = anim.frameRate ?? 24; + var looped:Bool = anim.looped ?? false; + var flipX:Bool = anim.flipX ?? false; + var flipY:Bool = anim.flipY ?? false; if (anim.frameIndices != null && anim.frameIndices.length > 0) { - // trace('addByIndices(${anim.name}, ${anim.prefix}, ${anim.frameIndices}, ${frameRate}, ${looped}, ${flipX}, ${flipY})'); target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, '', frameRate, looped, flipX, flipY); - // trace('RESULT:${target.animation.getAnimationList()}'); } else { - // trace('addByPrefix(${anim.name}, ${anim.prefix}, ${frameRate}, ${looped}, ${flipX}, ${flipY})'); target.animation.addByPrefix(anim.name, anim.prefix, frameRate, looped, flipX, flipY); - // trace('RESULT:${target.animation.getAnimationList()}'); + } + } + + /** + * Properly adds an animation to a texture atlas sprite based on the provided animation data. + */ + public static function addTextureAtlasAnimation(target:FunkinSprite, anim:AnimationData):Void + { + if (!target.isAnimate) return; + if (anim.prefix == null) return; + + var frameRate:Int = anim.frameRate ?? 24; + var looped:Bool = anim.looped ?? false; + var flipX:Bool = anim.flipX ?? false; + var flipY:Bool = anim.flipY ?? false; + var animType:String = anim.animType ?? "framelabel"; + + if (anim.frameIndices != null && anim.frameIndices.length > 0) + { + switch (animType) + { + case "framelabel": + target.anim.addByFrameLabelIndices(anim.name, anim.prefix, anim.frameIndices, frameRate, looped, flipX, flipY); + case "symbol": + target.anim.addBySymbolIndices(anim.name, anim.prefix, anim.frameIndices, frameRate, looped, flipX, flipY); + } + } + else + { + switch (animType) + { + case "framelabel": + target.anim.addByFrameLabel(anim.name, anim.prefix, frameRate, looped, flipX, flipY); + case "symbol": + target.anim.addBySymbol(anim.name, anim.prefix, frameRate, looped, flipX, flipY); + } } } @@ -43,6 +77,17 @@ class FlxAnimationUtil } } + /** + * Properly adds multiple animations to a texture atlas sprite based on the provided animation data. + */ + public static function addTextureAtlasAnimations(target:FunkinSprite, animations:Array):Void + { + for (anim in animations) + { + addTextureAtlasAnimation(target, anim); + } + } + /** * Combine two FlxFramesCollection objects into one. * @param a The first FlxFramesCollection diff --git a/source/funkin/util/plugins/NewgroundsMedalPlugin.hx b/source/funkin/util/plugins/NewgroundsMedalPlugin.hx index 4aa21980b..fa7d4fa56 100644 --- a/source/funkin/util/plugins/NewgroundsMedalPlugin.hx +++ b/source/funkin/util/plugins/NewgroundsMedalPlugin.hx @@ -6,24 +6,28 @@ import flixel.group.FlxContainer.FlxTypedContainer; import flixel.text.FlxText; import funkin.audio.FunkinSound; import flixel.graphics.FlxGraphic; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinSprite; import flixel.math.FlxRect; import funkin.api.newgrounds.Medals; +import funkin.util.macro.ConsoleMacro; +import funkin.ui.FullScreenScaleMode; @:nullSafety -class NewgroundsMedalPlugin extends FlxTypedContainer +class NewgroundsMedalPlugin extends FlxTypedContainer implements ConsoleClass { - var medal:FlxAtlasSprite; + public static var instance:Null = null; + + var medal:FunkinSprite; var points:FlxText; var name:FlxText; - public static var instance:Null = null; + var moveText:Bool = false; + var medalQueue:ArrayVoid> = []; - var tween:Bool = false; + var textSpeed:Float = 20; - var am:Float = 20; - - var funcs:ArrayVoid> = []; + final MEDAL_X = (FlxG.width - 250) * 0.5; + final MEDAL_Y = FlxG.height - 100; public function new() { @@ -34,12 +38,15 @@ class NewgroundsMedalPlugin extends FlxTypedContainer FlxG.console.registerClass(Medals); #end - var ecs = (FlxG.width - 250) * 0.5; - var why = FlxG.height - 100; FlxGraphic.defaultPersist = true; - medal = new FlxAtlasSprite(ecs, why, Paths.animateAtlas("ui/medal")); - points = new FlxText(171 + ecs, 10 + why, 50, 12, false); + medal = FunkinSprite.createTextureAtlas((MEDAL_X) + (FullScreenScaleMode.gameCutoutSize.x / 2), MEDAL_Y, "ui/medal", + { + swfMode: true, + filterQuality: HIGH + }); + + points = new FlxText((171 + MEDAL_X) + (FullScreenScaleMode.gameCutoutSize.x / 2), 17 + MEDAL_Y, 50, 12, false); points.fieldHeight = 18; points.systemFont = "Arial"; points.bold = true; @@ -50,64 +57,73 @@ class NewgroundsMedalPlugin extends FlxTypedContainer points.visible = false; points.scrollFactor.set(); - name = new FlxText(73 + ecs, 34 + why, 0, 26); + name = new FlxText((73 + MEDAL_X) + (FullScreenScaleMode.gameCutoutSize.x / 2), 37 + MEDAL_Y, 0, 26); name.font = Paths.font("ShareTechMono-Regular.ttf"); name.letterSpacing = -2; name.text = "Ono Boners Deluxe"; - name.clipRect = FlxRect.get(0, 0, 164, 35.2); name.visible = false; name.scrollFactor.set(); medal.scrollFactor.set(); - medal.anim.swfRender = true; medal.visible = false; - var fr = medal.anim.curSymbol.timeline.get(0).get(0); - if (fr != null) fr.name = "START"; // woerkaround - // fr.add(() -> FunkinSound.playOnce(Paths.sound('NGFadeIn'), 1.)); - - medal.anim.getFrameLabel("show").add(function() { - points.visible = true; - name.visible = true; - if (name.width > name.clipRect.width) + medal.anim.onFrameLabel.add(function(label:String) { + switch (label) { - // TODO: Remove this once FlxText.get_size deals with TextFormat's nullable size properly - @:nullSafety(Off) - am = (name.text.length * (name.size + 2) * 1.25) / name.clipRect.width * 10; - tween = true; - // FlxTimer.wait(0.3, () -> tween = true); + case "show": + points.visible = true; + name.visible = true; + if (name.width > name.clipRect.width) + { + @:nullSafety(Off) + textSpeed = (name.text.length * (name.size + 2) * 1.25) / name.clipRect.width * 10; + moveText = true; + } + case "fade": + FunkinSound.playOnce(Paths.sound('NGFadeOut'), 1.0); + case "hide": + points.visible = false; + name.visible = false; + moveText = false; + name.offset.x = 0; + name.clipRect.x = 0; + name.resetFrame(); } }); - // medal.anim.getFrameLabel("idle").add(() -> medal.anim.pause()); - medal.anim.getFrameLabel("fade").add(() -> FunkinSound.playOnce(Paths.sound('NGFadeOut'), 1.)); - medal.anim.getFrameLabel("hide").add(function() { - points.visible = false; - name.visible = false; - tween = false; - name.offset.x = 0; - name.clipRect.x = 0; - name.resetFrame(); - medal.replaceFrameGraphic(3, null); + medal.anim.onFinish.add(function(name:String) { + medal.visible = false; }); add(medal); add(points); add(name); + FlxGraphic.defaultPersist = false; } + /** + * Update the positions of the medal atlas in case the resolution changes! + */ + function updatePositions():Void + { + medal.x = MEDAL_X + (FullScreenScaleMode.gameCutoutSize.x / 2); + points.x = (175 + MEDAL_X) + (FullScreenScaleMode.gameCutoutSize.x / 2); + name.x = (79 + MEDAL_X) + (FullScreenScaleMode.gameCutoutSize.x / 2); + } + override public function update(elapsed:Float) { super.update(elapsed); - if (tween) + if (moveText) { - var val = am * elapsed; - name.offset.x += val; - name.clipRect.x += val; + var textX:Float = textSpeed * elapsed; + + name.offset.x += textX; + name.clipRect.x += textX; name.resetFrame(); } } @@ -120,10 +136,10 @@ class NewgroundsMedalPlugin extends FlxTypedContainer // instance is defined above so there's no need to worry about null safety here @:nullSafety(Off) - instance.medal.anim?.onComplete.add(function() { - if (instance.funcs.length > 0) + instance.medal.anim.onFinish.add(function(name:String) { + if (instance.medalQueue.length > 0) { - instance.funcs.shift()(); + instance.medalQueue.shift()(); } }); } @@ -131,22 +147,24 @@ class NewgroundsMedalPlugin extends FlxTypedContainer public static function play(points:Int = 100, name:String = "I LOVE CUM I LOVE CUM I LOVE CUM I LOVE CUM", ?graphic:FlxGraphic) { if (instance == null) return; - var func = function() { + + var playMedal:Void->Void = function() { instance.points.visible = false; instance.name.visible = false; instance.points.text = Std.string(points); instance.name.text = name; - - instance.medal.visible = true; - instance.medal.replaceFrameGraphic(3, graphic); - instance.medal.anim.play(true); + instance.updatePositions(); FunkinSound.playOnce(Paths.sound('NGFadeIn'), 1.0); + instance.medal.anim.play(""); + + instance.medal.visible = true; + instance.medal.replaceSymbolGraphic("NGMEDAL", graphic); } - if (!instance.medal.anim.isPlaying && instance.funcs.length == 0) func(); + if (instance.medal.isAnimationFinished() && instance.medalQueue.length == 0) playMedal(); else - instance.funcs.push(func); + instance.medalQueue.push(playMedal); } } #end