diff --git a/assets b/assets index bc1650ba7..0351610af 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit bc1650ba789d675683a8c0cc27b1e2a42cb686cf +Subproject commit 0351610af02b45eb25e4bbfef0380f6f1d8cf737 diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index c2a56bdc2..254165a95 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -223,6 +223,7 @@ class InitState extends FlxState storyMode: false, title: "Cum Song Erect by Kawai Sprite", songId: "cum", + characterId: "pico-playable", difficultyId: "nightmare", isNewHighscore: true, scoreData: @@ -238,7 +239,7 @@ class InitState extends FlxState combo: 69, maxCombo: 69, totalNotesHit: 140, - totalNotes: 200 // 0, + totalNotes: 240 // 0, } }, })); diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index c461c9555..caae2adc6 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -38,6 +38,8 @@ class PlayerData @:optional public var freeplayDJ:Null = null; + public var results:Null = null; + /** * Whether this character is unlocked by default. * Use a ScriptedPlayableCharacter to add custom logic. @@ -86,7 +88,6 @@ class PlayerFreeplayDJData @:default("PROTECT YO NUTS") var text3:String; - @:jignored var animationMap:Map; @@ -120,12 +121,18 @@ class PlayerFreeplayDJData return Paths.animateAtlas(assetPath); } - public function getFreeplayDJText(index:Int):String { - switch (index) { - case 1: return text1; - case 2: return text2; - case 3: return text3; - default: return ''; + public function getFreeplayDJText(index:Int):String + { + switch (index) + { + case 1: + return text1; + case 2: + return text2; + case 3: + return text3; + default: + return ''; } } @@ -178,6 +185,51 @@ class PlayerFreeplayDJData } } +typedef PlayerResultsData = +{ + var perfect:Array; + var excellent:Array; + var great:Array; + var good:Array; + var loss:Array; +}; + +typedef PlayerResultsAnimationData = +{ + /** + * `sparrow` or `animate` or whatever + */ + var renderType:String; + + var assetPath:String; + + @:optional + @:default([0, 0]) + var offsets:Array; + + @:optional + @:default(500) + var zIndex:Int; + + @:optional + @:default(0.0) + var delay:Float; + + @:optional + @:default(1.0) + var scale:Float; + + @:optional + @:default('') + var startFrameLabel:Null; + + @:optional + var loopFrame:Null; + + @:optional + var loopFrameLabel:Null; +}; + typedef PlayerFreeplayDJCartoonData = { var soundClickFrame:Int; diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index 3de9efd41..4656a1286 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -58,7 +58,7 @@ class PlayerRegistry extends BaseRegistry * @param characterId The stage character ID. * @return The playable character. */ - public function getCharacterOwnerId(characterId:String):String + public function getCharacterOwnerId(characterId:String):Null { return ownedCharacterIds[characterId]; } diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 8a77c1c85..1684e8b33 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -131,12 +131,14 @@ class FlxAtlasSprite extends FlxAnimate anim.play('', false, false); } } - - // Skip if the animation doesn't exist - if (!hasAnimation(id)) + else { - trace('Animation ' + id + ' not found'); - return; + // Skip if the animation doesn't exist + if (!hasAnimation(id)) + { + trace('Animation ' + id + ' not found'); + return; + } } anim.callback = function(_, frame:Int) { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index f55cef388..61c2ad800 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -3156,6 +3156,7 @@ class PlayState extends MusicBeatSubState storyMode: PlayStatePlaylist.isStoryMode, songId: currentChart.song.id, difficultyId: currentDifficulty, + characterId: currentChart.characters.player, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), prevScoreData: prevScoreData, scoreData: diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index a2c5f7e62..ae1a9bcf8 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -14,6 +14,9 @@ import flixel.math.FlxRect; import flixel.text.FlxBitmapText; import funkin.ui.freeplay.FreeplayScore; import flixel.text.FlxText; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.freeplay.player.PlayerData; +import funkin.ui.freeplay.charselect.PlayableCharacter; import flixel.util.FlxColor; import flixel.tweens.FlxEase; import funkin.graphics.FunkinCamera; @@ -55,14 +58,16 @@ class ResultState extends MusicBeatSubState final highscoreNew:FlxSprite; final score:ResultScore; - var bfPerfect:Null = null; - var heartsPerfect:Null = null; - var bfExcellent:Null = null; - var bfGreat:Null = null; - var gfGreat:Null = null; - var bfGood:Null = null; - var gfGood:Null = null; - var bfShit:Null = null; + var characterAtlasAnimations:Array< + { + sprite:FlxAtlasSprite, + delay:Float + }> = []; + var characterSparrowAnimations:Array< + { + sprite:FunkinSprite, + delay:Float + }> = []; var rankBg:FunkinSprite; final cameraBG:FunkinCamera; @@ -157,118 +162,84 @@ class ResultState extends MusicBeatSubState soundSystem.zIndex = 1100; add(soundSystem); - switch (rank) + // Fetch playable character data. Default to BF on the results screen if we can't find it. + var playerCharacterId:Null = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); + var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); + + trace('Got playable character: ${playerCharacter?.getName()}'); + // Query JSON data based on the rank, then use that to build the animation(s) the player sees. + var playerAnimationDatas:Array = playerCharacter != null ? playerCharacter.getResultsAnimationDatas(rank) : []; + + for (animData in playerAnimationDatas) { - case PERFECT | PERFECT_GOLD: - heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared")); - heartsPerfect.visible = false; - heartsPerfect.zIndex = 501; - add(heartsPerfect); + if (animData == null) continue; - heartsPerfect.anim.onComplete = () -> { - if (heartsPerfect != null) + var animPath:String = Paths.stripLibrary(animData.assetPath); + var animLibrary:String = Paths.getLibrary(animData.assetPath); + var offsets = animData.offsets ?? [0, 0]; + switch (animData.renderType) + { + case 'animateatlas': + var animation:FlxAtlasSprite = new FlxAtlasSprite(offsets[0], offsets[1], Paths.animateAtlas(animPath, animLibrary)); + animation.zIndex = animData.zIndex ?? 500; + + animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0); + + if (animData.loopFrameLabel != null) { - // bfPerfect.anim.curFrame = 137; - heartsPerfect.anim.curFrame = 43; - heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.anim.onComplete = () -> { + if (animation != null) + { + animation.playAnimation(animData.loopFrameLabel ?? ''); // unpauses this anim, since it's on PlayOnce! + } + } } - }; - - bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared")); - bfPerfect.visible = false; - bfPerfect.zIndex = 500; - add(bfPerfect); - - bfPerfect.anim.onComplete = () -> { - if (bfPerfect != null) + else if (animData.loopFrame != null) { - // bfPerfect.anim.curFrame = 137; - bfPerfect.anim.curFrame = 137; - bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.anim.onComplete = () -> { + if (animation != null) + { + animation.anim.curFrame = animData.loopFrame ?? 0; + animation.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + } } - }; - case EXCELLENT: - bfExcellent = new FlxAtlasSprite(1329, 429, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared")); - bfExcellent.visible = false; - bfExcellent.zIndex = 500; - add(bfExcellent); + // Hide until ready to play. + animation.visible = false; + // Queue to play. + characterAtlasAnimations.push( + { + sprite: animation, + delay: animData.delay ?? 0.0 + }); + // Add to the scene. + add(animation); + case 'sparrow': + var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath); + animation.animation.addByPrefix('idle', '', 24, false, false, false); - bfExcellent.anim.onComplete = () -> { - if (bfExcellent != null) + if (animData.loopFrame != null) { - bfExcellent.anim.curFrame = 28; - bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.animation.finishCallback = (_name:String) -> { + if (animation != null) + { + animation.animation.play('idle', true, false, animData.loopFrame ?? 0); + } + } } - }; - case GREAT: - gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared")); - gfGreat.visible = false; - gfGreat.zIndex = 499; - add(gfGreat); - - gfGreat.scale.set(0.93, 0.93); - - gfGreat.anim.onComplete = () -> { - if (gfGreat != null) - { - gfGreat.anim.curFrame = 9; - gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! - } - }; - - bfGreat = new FlxAtlasSprite(929, 363, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/bf", "shared")); - bfGreat.visible = false; - bfGreat.zIndex = 500; - add(bfGreat); - - bfGreat.scale.set(0.93, 0.93); - - bfGreat.anim.onComplete = () -> { - if (bfGreat != null) - { - bfGreat.anim.curFrame = 15; - bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! - } - }; - - case GOOD: - gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD'); - gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); - gfGood.visible = false; - gfGood.zIndex = 500; - gfGood.animation.finishCallback = _ -> { - if (gfGood != null) - { - gfGood.animation.play('clap', true, false, 9); - } - }; - add(gfGood); - - bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD'); - bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false); - bfGood.visible = false; - bfGood.zIndex = 501; - bfGood.animation.finishCallback = function(_) { - if (bfGood != null) - { - bfGood.animation.play('fall', true, false, 14); - } - }; - add(bfGood); - - case SHIT: - bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared")); - bfShit.visible = false; - bfShit.zIndex = 500; - add(bfShit); - bfShit.onAnimationFinish.add((animName) -> { - if (bfShit != null) - { - bfShit.playAnimation('Loop Start'); - } - }); + // Hide until ready to play. + animation.visible = false; + // Queue to play. + characterSparrowAnimations.push( + { + sprite: animation, + delay: animData.delay ?? 0.0 + }); + // Add to the scene. + add(animation); + } } var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; @@ -587,94 +558,22 @@ class ResultState extends MusicBeatSubState { showSmallClearPercent(); - switch (rank) + for (atlas in characterAtlasAnimations) { - case PERFECT | PERFECT_GOLD: - if (bfPerfect == null) - { - trace("Could not build PERFECT animation!"); - } - else - { - bfPerfect.visible = true; - bfPerfect.playAnimation(''); - } - new FlxTimer().start(106 / 24, _ -> { - if (heartsPerfect == null) - { - trace("Could not build heartsPerfect animation!"); - } - else - { - heartsPerfect.visible = true; - heartsPerfect.playAnimation(''); - } - }); - case EXCELLENT: - if (bfExcellent == null) - { - trace("Could not build EXCELLENT animation!"); - } - else - { - bfExcellent.visible = true; - bfExcellent.playAnimation(''); - } - case GREAT: - if (bfGreat == null) - { - trace("Could not build GREAT animation!"); - } - else - { - bfGreat.visible = true; - bfGreat.playAnimation(''); - } + new FlxTimer().start(atlas.delay, _ -> { + if (atlas.sprite == null) return; + atlas.sprite.visible = true; + atlas.sprite.playAnimation(''); + }); + } - new FlxTimer().start(6 / 24, _ -> { - if (gfGreat == null) - { - trace("Could not build GREAT animation for gf!"); - } - else - { - gfGreat.visible = true; - gfGreat.playAnimation(''); - } - }); - case SHIT: - if (bfShit == null) - { - trace("Could not build SHIT animation!"); - } - else - { - bfShit.visible = true; - bfShit.playAnimation('Intro'); - } - case GOOD: - if (bfGood == null) - { - trace("Could not build GOOD animation!"); - } - else - { - bfGood.animation.play('fall'); - bfGood.visible = true; - new FlxTimer().start((1 / 24) * 22, _ -> { - // plays about 22 frames (at 24fps timing) after bf spawns in - if (gfGood != null) - { - gfGood.animation.play('clap', true); - gfGood.visible = true; - } - else - { - trace("Could not build GOOD animation!"); - } - }); - } - default: + for (sprite in characterSparrowAnimations) + { + new FlxTimer().start(sprite.delay, _ -> { + if (sprite.sprite == null) return; + sprite.sprite.visible = true; + sprite.sprite.animation.play('idle', true); + }); } } @@ -776,52 +675,6 @@ class ResultState extends MusicBeatSubState // })); // } - // if(heartsPerfect != null){ - // if (FlxG.keys.justPressed.I) - // { - // heartsPerfect.y -= 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.J) - // { - // heartsPerfect.x -= 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.L) - // { - // heartsPerfect.x += 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.K) - // { - // heartsPerfect.y += 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // } - - // if(bfGreat != null){ - // if (FlxG.keys.justPressed.W) - // { - // bfGreat.y -= 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.A) - // { - // bfGreat.x -= 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.D) - // { - // bfGreat.x += 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.S) - // { - // bfGreat.y += 1; - // trace(bfGreat.x, bfGreat.y); - // } - // } - // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; @@ -922,12 +775,21 @@ typedef ResultsStateParams = var storyMode:Bool; /** + * A readable title for the song we just played. * Either "Song Name by Artist Name" or "Week Name" */ var title:String; + /** + * The internal song ID for the song we just played. + */ var songId:String; + /** + * The character ID for the song we just played. + */ + var characterId:String; + /** * Whether the displayed score is a new highscore */ diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 6d7b96c58..c46b4b930 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -3,6 +3,7 @@ package funkin.ui.freeplay.charselect; import funkin.data.IRegistryEntry; import funkin.data.freeplay.player.PlayerData; import funkin.data.freeplay.player.PlayerRegistry; +import funkin.play.scoring.Scoring.ScoringRank; /** * An object used to retrieve data about a playable character (also known as "weeks"). @@ -87,6 +88,32 @@ class PlayableCharacter implements IRegistryEntry return _data.freeplayDJ.getFreeplayDJText(index); } + /** + * @param rank Which rank to get info for + * @return An array of animations. For example, BF Great has two animations, one for BF and one for GF + */ + public function getResultsAnimationDatas(rank:ScoringRank):Array + { + if (_data.results == null) + { + return []; + } + + switch (rank) + { + case PERFECT | PERFECT_GOLD: + return _data.results.perfect; + case EXCELLENT: + return _data.results.excellent; + case GREAT: + return _data.results.great; + case GOOD: + return _data.results.good; + case SHIT: + return _data.results.loss; + } + } + /** * Returns whether this character is unlocked. */