diff --git a/assets b/assets index 26b7f54a0..51f7f0df7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 26b7f54a062f6961decd9c828754c9076cb19674 +Subproject commit 51f7f0df72bb9722ca66b5928ab3872875f6ac83 diff --git a/source/funkin/data/notestyle/CHANGELOG.md b/source/funkin/data/notestyle/CHANGELOG.md index d85c11cad..de9a19f11 100644 --- a/source/funkin/data/notestyle/CHANGELOG.md +++ b/source/funkin/data/notestyle/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + ## [1.1.0] ### Added - Added several new `assets`: diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx index dff48fa16..fb27e3813 100644 --- a/source/funkin/data/notestyle/NoteStyleData.hx +++ b/source/funkin/data/notestyle/NoteStyleData.hx @@ -290,4 +290,33 @@ typedef NoteStyleData_HoldNoteCover = @:optional @:default(true) var enabled:Bool; + + @:optional + var left:NoteStyleData_HoldNoteCoverDirectionData; + + @:optional + var down:NoteStyleData_HoldNoteCoverDirectionData; + + @:optional + var up:NoteStyleData_HoldNoteCoverDirectionData; + + @:optional + var right:NoteStyleData_HoldNoteCoverDirectionData; }; + +typedef NoteStyleData_HoldNoteCoverDirectionData = +{ + /** + * Optionally specify an asset path to use for this specific animation. + * @:default The assetPath of the main holdNoteCover asset + */ + @:optional + var assetPath:String; + + @:optional + var start:UnnamedAnimationData; + @:optional + var hold:UnnamedAnimationData; + @:optional + var end:UnnamedAnimationData; +} diff --git a/source/funkin/play/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx index 0d7842ad9..478b18b53 100644 --- a/source/funkin/play/notes/NoteHoldCover.hx +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -4,66 +4,37 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.graphics.frames.FlxFramesCollection; import funkin.util.assets.FlxAnimationUtil; import flixel.FlxSprite; +import funkin.play.notes.notestyle.NoteStyle; class NoteHoldCover extends FlxTypedSpriteGroup { static final FRAMERATE_DEFAULT:Int = 24; - static var glowFrames:FlxFramesCollection; - public var holdNote:SustainTrail; - var glow:FlxSprite; + public var glow:FlxSprite; + var sparks:FlxSprite; - public function new() + public function new(noteStyle:NoteStyle) { super(0, 0); - setup(); - } - - public static function preloadFrames():Void - { - glowFrames = null; - for (direction in Strumline.DIRECTIONS) - { - var directionName = direction.colorName.toTitleCase(); - - var atlas:FlxFramesCollection = Paths.getSparrowAtlas('holdCover${directionName}'); - atlas.parent.persist = true; - - if (glowFrames != null) - { - glowFrames = FlxAnimationUtil.combineFramesCollections(glowFrames, atlas); - } - else - { - glowFrames = atlas; - } - } + setupHoldNoteCover(noteStyle); } /** * Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times. */ - function setup():Void + function setupHoldNoteCover(noteStyle:NoteStyle):Void { glow = new FlxSprite(); add(glow); - if (glowFrames == null) preloadFrames(); - glow.frames = glowFrames; - for (direction in Strumline.DIRECTIONS) - { - var directionName = direction.colorName.toTitleCase(); + // TODO: null check here like how NoteSplash does + noteStyle.buildHoldCoverSprite(this); - glow.animation.addByPrefix('holdCoverStart$directionName', 'holdCoverStart${directionName}0', FRAMERATE_DEFAULT, false, false, false); - glow.animation.addByPrefix('holdCover$directionName', 'holdCover${directionName}0', FRAMERATE_DEFAULT, true, false, false); - glow.animation.addByPrefix('holdCoverEnd$directionName', 'holdCoverEnd${directionName}0', FRAMERATE_DEFAULT, false, false, false); - } - - glow.animation.finishCallback = this.onAnimationFinished; + glow.animation.onFinish.add(this.onAnimationFinished); if (glow.animation.getAnimationList().length < 3 * 4) { diff --git a/source/funkin/play/notes/NoteSplash.hx b/source/funkin/play/notes/NoteSplash.hx index eb9ba5813..744ea068e 100644 --- a/source/funkin/play/notes/NoteSplash.hx +++ b/source/funkin/play/notes/NoteSplash.hx @@ -12,19 +12,13 @@ class NoteSplash extends FlxSprite static var frameCollection:FlxFramesCollection; - public static function preloadFrames():Void - { - frameCollection = Paths.getSparrowAtlas('noteSplashes'); - frameCollection.parent.persist = true; - } - public function new(noteStyle:NoteStyle) { super(0, 0); setupSplashGraphic(noteStyle); - this.animation.finishCallback = this.onAnimationFinished; + this.animation.onFinish.add(this.onAnimationFinished); } /** @@ -55,6 +49,7 @@ class NoteSplash extends FlxSprite } // splashUP0, splashUP1, splashRIGHT0, etc. + // the animations are processed via `NoteStyle.fetchSplashAnimationData()` in this format this.playAnimation('splash${direction.nameUpper}${variant}'); if (animation.curAnim == null) return; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index c721cb9d6..2c941d31d 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -779,10 +779,10 @@ class Strumline extends FlxSpriteGroup splash.x = this.x; splash.x += getXPos(direction); splash.x += INITIAL_OFFSET; + splash.x += noteStyle.getSplashOffsets()[0] * splash.scale.x; + splash.y = this.y; splash.y -= INITIAL_OFFSET; - splash.y += 0; - splash.x += noteStyle.getSplashOffsets()[0] * splash.scale.x; splash.y += noteStyle.getSplashOffsets()[1] * splash.scale.y; } } @@ -806,12 +806,14 @@ class Strumline extends FlxSpriteGroup cover.x += getXPos(holdNote.noteDirection); cover.x += STRUMLINE_SIZE / 2; cover.x -= cover.width / 2; - cover.x += -12; // Manual tweaking because fuck. + cover.x += noteStyle.getHoldCoverOffsets()[0] * cover.scale.x; + cover.x += -12; // hardcoded adjustment, because we are evil. cover.y = this.y; cover.y += INITIAL_OFFSET; cover.y += STRUMLINE_SIZE / 2; - cover.y += -96; // Manual tweaking because fuck. + cover.y += noteStyle.getHoldCoverOffsets()[1] * cover.scale.y; + cover.y += -96; // hardcoded adjustment, because we are evil. } } @@ -916,7 +918,7 @@ class Strumline extends FlxSpriteGroup if (noteHoldCovers.length < noteHoldCovers.maxSize) { // Create a new note hold cover. - result = new NoteHoldCover(); + result = new NoteHoldCover(noteStyle); this.noteHoldCovers.add(result); } else diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index 7e3076919..3e8a9b6ff 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -2,6 +2,7 @@ package funkin.play.notes.notestyle; import funkin.play.Countdown; import flixel.graphics.frames.FlxAtlasFrames; +import flixel.graphics.frames.FlxFramesCollection; import funkin.data.animation.AnimationData; import funkin.data.IRegistryEntry; import funkin.graphics.FunkinSprite; @@ -99,6 +100,15 @@ class NoteStyle implements IRegistryEntry target.updateHitbox(); } + function getNoteAssetLibrary():Null + { + // library:path + var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; + if (parts.length == 1) return null; + return parts[0]; + } + var noteFrames:Null = null; function buildNoteFrames(force:Bool = false):Null @@ -123,7 +133,7 @@ class NoteStyle implements IRegistryEntry if (noteFrames != null && !force) return noteFrames; - noteFrames = Paths.getSparrowAtlas(noteAssetPath, getNoteAssetLibrary()); + noteFrames = Paths.getSparrowAtlas(noteAssetPath, getAssetLibrary(getNoteAssetPath(true))); if (noteFrames == null) { @@ -149,15 +159,6 @@ class NoteStyle implements IRegistryEntry return parts[1]; } - function getNoteAssetLibrary():Null - { - // library:path - var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; - if (parts.length == 0) return null; - if (parts.length == 1) return null; - return parts[0]; - } - function buildNoteAnimations(target:NoteSprite):Void { var leftData:Null = fetchNoteAnimationData(LEFT); @@ -232,7 +233,7 @@ class NoteStyle implements IRegistryEntry // TODO: Add support for multi-Sparrow. // Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772 - var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getStrumlineAssetLibrary()); + var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getAssetLibrary(getStrumlineAssetPath(true))); if (atlas == null) { @@ -258,14 +259,6 @@ class NoteStyle implements IRegistryEntry return parts[1]; } - function getStrumlineAssetLibrary():Null - { - // library:path - var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; - if (parts.length <= 1) return null; - return parts[0]; - } - public function applyStrumlineAnimations(target:StrumlineNote, dir:NoteDirection):Void { FlxAnimationUtil.addAtlasAnimations(target, getStrumlineAnimationData(dir)); @@ -921,7 +914,7 @@ class NoteStyle implements IRegistryEntry if (splashFrames != null && !force) return splashFrames; - splashFrames = Paths.getSparrowAtlas(splashAssetPath, getSplashAssetLibrary()); + splashFrames = Paths.getSparrowAtlas(splashAssetPath, getAssetLibrary(getSplashAssetPath(true))); splashFrames.parent.persist = true; if (splashFrames == null) { @@ -945,14 +938,6 @@ class NoteStyle implements IRegistryEntry return parts[1]; } - function getSplashAssetLibrary():Null - { - var parts = getSplashAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; - if (parts.length == 0) return null; - if (parts.length == 1) return null; - return parts[0]; - } - function buildSplashAnimations(target:NoteSplash):Void { final addSplashAnim:NoteDirection->Void = function(dir:NoteDirection) { @@ -1007,8 +992,189 @@ class NoteStyle implements IRegistryEntry return _data?.assets?.noteSplash?.data?.framerateVariance ?? fallback?.getSplashFramerateVariance() ?? 2; } + public function buildHoldCoverSprite(target:NoteHoldCover):Void + { + // NoteHoldCover has "glow" and "sparks". Right now it only implements "glow" + // but "sparks" I believe is meant to be used for the ending of the hold note + var glowAtlas:Null = buildHoldCoverFrames(false); + if (glowAtlas == null) + { + throw 'Could not load spritesheet for note style: $id'; + } + target.glow.frames = glowAtlas; + + target.antialiasing = !(_data.assets.holdNoteCover?.isPixel ?? false); + target.glow.antialiasing = !(_data.assets.holdNoteCover?.isPixel ?? false); + target.scale.set(_data.assets.holdNoteCover?.scale ?? 1.0, _data.assets.holdNoteCover?.scale ?? 1.0); + target.updateHitbox(); + target.glow.updateHitbox(); + + buildHoldCoverAnimations(target); + } + + // we use FlxFramesCollection here so we can combine em into one atlas later + var holdCoverFrames:Null = null; + + function buildHoldCoverFrames(force:Bool = false):Null + { + if (holdCoverFrames != null && !force) return holdCoverFrames; + for (direction in Strumline.DIRECTIONS) + { + // We make a FlxFramesCollection here so we can combine em into one atlas later + var atlas:Null = buildHoldCoverFrameForDirection(direction); + if (holdCoverFrames == null) holdCoverFrames = atlas; + else if (atlas != null) holdCoverFrames = FlxAnimationUtil.combineFramesCollections(holdCoverFrames, atlas); + } + return holdCoverFrames; + } + + function buildHoldCoverFrameForDirection(direction:NoteDirection):Null + { + var directionData = switch (direction) + { + case LEFT: _data.assets?.holdNoteCover?.data?.left; + case DOWN: _data.assets?.holdNoteCover?.data?.down; + case UP: _data.assets?.holdNoteCover?.data?.up; + case RIGHT: _data.assets?.holdNoteCover?.data?.right; + }; + + if (directionData == null) return null; + + var holdCoverAssetPath:Null = getHoldCoverDirectionAssetPath(direction) ?? fallback?.getHoldCoverDirectionAssetPath(direction); + if (holdCoverAssetPath == null) + { + FlxG.log.warn('Hold Note Cover asset path not found: ${id}'); + return null; + } + + if (!FunkinSprite.isTextureCached(Paths.image(holdCoverAssetPath))) + { + FlxG.log.warn('Hold Note Cover texture not cached: ${holdCoverAssetPath}'); + } + + var atlas:FlxFramesCollection = Paths.getSparrowAtlas(holdCoverAssetPath, getAssetLibrary(getHoldCoverDirectionAssetPath(direction, true))); + if (atlas == null) return null; + + atlas.parent.persist = true; + + return atlas; + } + + function buildHoldCoverAnimations(target:NoteHoldCover):Void + { + for (direction in Strumline.DIRECTIONS) + { + var animData:Null> = fetchHoldCoverAnimationData(direction); + if (animData != null) + { + // this entry is the base animation that plays ("holdCover$noteColor" animation), which we want to loop + animData[1].looped = true; + FlxAnimationUtil.addAtlasAnimations(target.glow, animData); + } + } + } + + function fetchHoldCoverAnimationData(dir:NoteDirection):Null> + { + var noteColor:String = dir.colorName.toTitleCase(); + var result:Array> = switch (dir) + { + case LEFT: [ + _data.assets?.holdNoteCover?.data?.left?.start?.toNamed('holdCoverStart$noteColor'), + _data.assets?.holdNoteCover?.data?.left?.hold?.toNamed('holdCover$noteColor'), + _data.assets?.holdNoteCover?.data?.left?.end?.toNamed('holdCoverEnd$noteColor'), + ]; + case DOWN: [ + _data.assets?.holdNoteCover?.data?.down?.start?.toNamed('holdCoverStart$noteColor'), + _data.assets?.holdNoteCover?.data?.down?.hold?.toNamed('holdCover$noteColor'), + _data.assets?.holdNoteCover?.data?.down?.end?.toNamed('holdCoverEnd$noteColor'), + ]; + case UP: [ + _data.assets?.holdNoteCover?.data?.up?.start?.toNamed('holdCoverStart$noteColor'), + _data.assets?.holdNoteCover?.data?.up?.hold?.toNamed('holdCover$noteColor'), + _data.assets?.holdNoteCover?.data?.up?.end?.toNamed('holdCoverEnd$noteColor'), + ]; + case RIGHT: [ + _data.assets?.holdNoteCover?.data?.right?.start?.toNamed('holdCoverStart$noteColor'), + _data.assets?.holdNoteCover?.data?.right?.hold?.toNamed('holdCover$noteColor'), + _data.assets?.holdNoteCover?.data?.right?.end?.toNamed('holdCoverEnd$noteColor'), + ]; + default: []; + }; + + // New variable so we can change the type. + var filteredResult:Array = thx.Arrays.filterNull(result); + + if (filteredResult.length == 0) return fallback?.fetchHoldCoverAnimationData(dir) ?? []; + + return filteredResult; + } + + // fallbacks: + // assetPath for a direction -> root assetpath -> fallback assetpath for direction -> fallback root assetpath + // `direction` here is required, however it only is used for fallback purposes + function getHoldCoverRootAssetPath(direction:NoteDirection, raw:Bool = false):Null + { + if (raw) + { + var rawPath:Null = _data?.assets?.holdNoteCover?.assetPath; + // remember, if we need a fallback for our *root* asset path, + // we fallback and look for the *direction* asset path first + if (rawPath == null) return fallback?.getHoldCoverDirectionAssetPath(direction, true); + return rawPath; + } + + var parts:Array = getHoldCoverRootAssetPath(direction, true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; + if (parts.length == 1) return getHoldCoverRootAssetPath(direction, true); + return parts[1]; + } + + function getHoldCoverDirectionAssetPath(direction:NoteDirection, raw:Bool = false):Null + { + if (raw) + { + var rawPath:Null = switch (direction) + { + case LEFT: _data?.assets?.holdNoteCover?.data?.left?.assetPath; + case DOWN: _data?.assets?.holdNoteCover?.data?.down?.assetPath; + case UP: _data?.assets?.holdNoteCover?.data?.up?.assetPath; + case RIGHT: _data?.assets?.holdNoteCover?.data?.right?.assetPath; + }; + + // we don't want to fallback directly just yet, we want to check our root assetpath first + if (rawPath == null) return getHoldCoverRootAssetPath(direction, true); + return rawPath; + } + + var parts:Array = getHoldCoverDirectionAssetPath(direction, true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; + if (parts.length == 1) return getHoldCoverDirectionAssetPath(direction, true); + return parts[1]; + } + + public function getHoldCoverOffsets():Array + { + return _data?.assets?.holdNoteCover?.offsets ?? fallback?.getHoldCoverOffsets() ?? [0.0, 0.0]; + } + public function destroy():Void {} + /** + * Returns a string of the library name for the given asset id + * `default:assets/images/awesome.png` returns `default` + * If you pass a asset path with no library name (no `:` aka LIBRARY_SEPARATOR) it will return null + * @param id The asset id to get the library name from + * @return Null The library name, or null if no library name is present + */ + function getAssetLibrary(?id:String):Null + { + var parts:Array = id?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return null; + + return parts[0]; + } + public function toString():String { return 'NoteStyle($id)'; diff --git a/source/funkin/util/assets/FlxAnimationUtil.hx b/source/funkin/util/assets/FlxAnimationUtil.hx index f94dc4dc3..ee5b0edec 100644 --- a/source/funkin/util/assets/FlxAnimationUtil.hx +++ b/source/funkin/util/assets/FlxAnimationUtil.hx @@ -9,7 +9,7 @@ class FlxAnimationUtil /** * Properly adds an animation to a sprite based on the provided animation data. */ - public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData) + public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData):Void { var frameRate = anim.frameRate == null ? 24 : anim.frameRate; var looped = anim.looped == null ? false : anim.looped; @@ -33,7 +33,7 @@ class FlxAnimationUtil /** * Properly adds multiple animations to a sprite based on the provided animation data. */ - public static function addAtlasAnimations(target:FlxSprite, animations:Array) + public static function addAtlasAnimations(target:FlxSprite, animations:Array):Void { for (anim in animations) { @@ -41,6 +41,12 @@ class FlxAnimationUtil } } + /** + * Combine two FlxFramesCollection objects into one. + * @param a The first FlxFramesCollection + * @param b The second FlxFramesCollection + * @return FlxFramesCollection The combined FlxFramesCollection + */ public static function combineFramesCollections(a:FlxFramesCollection, b:FlxFramesCollection):FlxFramesCollection { var result:FlxFramesCollection = new FlxFramesCollection(null, ATLAS, null);