1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-15 11:22:55 +00:00

Migrate results animations to JSON data so we can implement them per-character

This commit is contained in:
EliteMasterEric 2024-07-03 22:50:39 -04:00
parent bd17d965f5
commit e32fcee77c
8 changed files with 201 additions and 256 deletions

2
assets

@ -1 +1 @@
Subproject commit bc1650ba789d675683a8c0cc27b1e2a42cb686cf Subproject commit 0351610af02b45eb25e4bbfef0380f6f1d8cf737

View file

@ -223,6 +223,7 @@ class InitState extends FlxState
storyMode: false, storyMode: false,
title: "Cum Song Erect by Kawai Sprite", title: "Cum Song Erect by Kawai Sprite",
songId: "cum", songId: "cum",
characterId: "pico-playable",
difficultyId: "nightmare", difficultyId: "nightmare",
isNewHighscore: true, isNewHighscore: true,
scoreData: scoreData:
@ -238,7 +239,7 @@ class InitState extends FlxState
combo: 69, combo: 69,
maxCombo: 69, maxCombo: 69,
totalNotesHit: 140, totalNotesHit: 140,
totalNotes: 200 // 0, totalNotes: 240 // 0,
} }
}, },
})); }));

View file

@ -38,6 +38,8 @@ class PlayerData
@:optional @:optional
public var freeplayDJ:Null<PlayerFreeplayDJData> = null; public var freeplayDJ:Null<PlayerFreeplayDJData> = null;
public var results:Null<PlayerResultsData> = null;
/** /**
* Whether this character is unlocked by default. * Whether this character is unlocked by default.
* Use a ScriptedPlayableCharacter to add custom logic. * Use a ScriptedPlayableCharacter to add custom logic.
@ -86,7 +88,6 @@ class PlayerFreeplayDJData
@:default("PROTECT YO NUTS") @:default("PROTECT YO NUTS")
var text3:String; var text3:String;
@:jignored @:jignored
var animationMap:Map<String, AnimationData>; var animationMap:Map<String, AnimationData>;
@ -120,12 +121,18 @@ class PlayerFreeplayDJData
return Paths.animateAtlas(assetPath); return Paths.animateAtlas(assetPath);
} }
public function getFreeplayDJText(index:Int):String { public function getFreeplayDJText(index:Int):String
switch (index) { {
case 1: return text1; switch (index)
case 2: return text2; {
case 3: return text3; case 1:
default: return ''; return text1;
case 2:
return text2;
case 3:
return text3;
default:
return '';
} }
} }
@ -178,6 +185,51 @@ class PlayerFreeplayDJData
} }
} }
typedef PlayerResultsData =
{
var perfect:Array<PlayerResultsAnimationData>;
var excellent:Array<PlayerResultsAnimationData>;
var great:Array<PlayerResultsAnimationData>;
var good:Array<PlayerResultsAnimationData>;
var loss:Array<PlayerResultsAnimationData>;
};
typedef PlayerResultsAnimationData =
{
/**
* `sparrow` or `animate` or whatever
*/
var renderType:String;
var assetPath:String;
@:optional
@:default([0, 0])
var offsets:Array<Float>;
@: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<String>;
@:optional
var loopFrame:Null<Int>;
@:optional
var loopFrameLabel:Null<String>;
};
typedef PlayerFreeplayDJCartoonData = typedef PlayerFreeplayDJCartoonData =
{ {
var soundClickFrame:Int; var soundClickFrame:Int;

View file

@ -58,7 +58,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
* @param characterId The stage character ID. * @param characterId The stage character ID.
* @return The playable character. * @return The playable character.
*/ */
public function getCharacterOwnerId(characterId:String):String public function getCharacterOwnerId(characterId:String):Null<String>
{ {
return ownedCharacterIds[characterId]; return ownedCharacterIds[characterId];
} }

View file

@ -131,13 +131,15 @@ class FlxAtlasSprite extends FlxAnimate
anim.play('', false, false); anim.play('', false, false);
} }
} }
else
{
// Skip if the animation doesn't exist // Skip if the animation doesn't exist
if (!hasAnimation(id)) if (!hasAnimation(id))
{ {
trace('Animation ' + id + ' not found'); trace('Animation ' + id + ' not found');
return; return;
} }
}
anim.callback = function(_, frame:Int) { anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1; var offset = loop ? 0 : -1;

View file

@ -3156,6 +3156,7 @@ class PlayState extends MusicBeatSubState
storyMode: PlayStatePlaylist.isStoryMode, storyMode: PlayStatePlaylist.isStoryMode,
songId: currentChart.song.id, songId: currentChart.song.id,
difficultyId: currentDifficulty, difficultyId: currentDifficulty,
characterId: currentChart.characters.player,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
prevScoreData: prevScoreData, prevScoreData: prevScoreData,
scoreData: scoreData:

View file

@ -14,6 +14,9 @@ import flixel.math.FlxRect;
import flixel.text.FlxBitmapText; import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore; import funkin.ui.freeplay.FreeplayScore;
import flixel.text.FlxText; 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.util.FlxColor;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
@ -55,14 +58,16 @@ class ResultState extends MusicBeatSubState
final highscoreNew:FlxSprite; final highscoreNew:FlxSprite;
final score:ResultScore; final score:ResultScore;
var bfPerfect:Null<FlxAtlasSprite> = null; var characterAtlasAnimations:Array<
var heartsPerfect:Null<FlxAtlasSprite> = null; {
var bfExcellent:Null<FlxAtlasSprite> = null; sprite:FlxAtlasSprite,
var bfGreat:Null<FlxAtlasSprite> = null; delay:Float
var gfGreat:Null<FlxAtlasSprite> = null; }> = [];
var bfGood:Null<FlxSprite> = null; var characterSparrowAnimations:Array<
var gfGood:Null<FlxSprite> = null; {
var bfShit:Null<FlxAtlasSprite> = null; sprite:FunkinSprite,
delay:Float
}> = [];
var rankBg:FunkinSprite; var rankBg:FunkinSprite;
final cameraBG:FunkinCamera; final cameraBG:FunkinCamera;
@ -157,118 +162,84 @@ class ResultState extends MusicBeatSubState
soundSystem.zIndex = 1100; soundSystem.zIndex = 1100;
add(soundSystem); add(soundSystem);
switch (rank) // Fetch playable character data. Default to BF on the results screen if we can't find it.
{ var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
case PERFECT | PERFECT_GOLD: var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf');
heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared"));
heartsPerfect.visible = false;
heartsPerfect.zIndex = 501;
add(heartsPerfect);
heartsPerfect.anim.onComplete = () -> { trace('Got playable character: ${playerCharacter?.getName()}');
if (heartsPerfect != null) // Query JSON data based on the rank, then use that to build the animation(s) the player sees.
var playerAnimationDatas:Array<PlayerResultsAnimationData> = playerCharacter != null ? playerCharacter.getResultsAnimationDatas(rank) : [];
for (animData in playerAnimationDatas)
{ {
// bfPerfect.anim.curFrame = 137; if (animData == null) continue;
heartsPerfect.anim.curFrame = 43;
heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! 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)
{
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)
{
// bfPerfect.anim.curFrame = 137;
bfPerfect.anim.curFrame = 137;
bfPerfect.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);
bfExcellent.anim.onComplete = () -> {
if (bfExcellent != null)
{
bfExcellent.anim.curFrame = 28;
bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
} }
}; else if (animData.loopFrame != null)
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; animation.anim.onComplete = () -> {
gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! if (animation != null)
}
};
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; animation.anim.curFrame = animData.loopFrame ?? 0;
bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! animation.anim.play(); // unpauses this anim, since it's on PlayOnce!
}
}
} }
};
case GOOD: // Hide until ready to play.
gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD'); animation.visible = false;
gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); // Queue to play.
gfGood.visible = false; characterAtlasAnimations.push(
gfGood.zIndex = 500;
gfGood.animation.finishCallback = _ -> {
if (gfGood != null)
{ {
gfGood.animation.play('clap', true, false, 9); sprite: animation,
} delay: animData.delay ?? 0.0
};
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');
}
}); });
// 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);
if (animData.loopFrame != null)
{
animation.animation.finishCallback = (_name:String) -> {
if (animation != null)
{
animation.animation.play('idle', true, false, animData.loopFrame ?? 0);
}
}
}
// 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'}'; var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}';
@ -587,94 +558,22 @@ class ResultState extends MusicBeatSubState
{ {
showSmallClearPercent(); showSmallClearPercent();
switch (rank) for (atlas in characterAtlasAnimations)
{ {
case PERFECT | PERFECT_GOLD: new FlxTimer().start(atlas.delay, _ -> {
if (bfPerfect == null) if (atlas.sprite == null) return;
{ atlas.sprite.visible = true;
trace("Could not build PERFECT animation!"); atlas.sprite.playAnimation('');
}
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(6 / 24, _ -> { for (sprite in characterSparrowAnimations)
if (gfGreat == null)
{ {
trace("Could not build GREAT animation for gf!"); new FlxTimer().start(sprite.delay, _ -> {
} if (sprite.sprite == null) return;
else sprite.sprite.visible = true;
{ sprite.sprite.animation.play('idle', true);
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:
} }
} }
@ -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; // maskShaderSongName.swagSprX = songName.x;
maskShaderDifficulty.swagSprX = difficulty.x; maskShaderDifficulty.swagSprX = difficulty.x;
@ -922,12 +775,21 @@ typedef ResultsStateParams =
var storyMode:Bool; var storyMode:Bool;
/** /**
* A readable title for the song we just played.
* Either "Song Name by Artist Name" or "Week Name" * Either "Song Name by Artist Name" or "Week Name"
*/ */
var title:String; var title:String;
/**
* The internal song ID for the song we just played.
*/
var songId:String; var songId:String;
/**
* The character ID for the song we just played.
*/
var characterId:String;
/** /**
* Whether the displayed score is a new highscore * Whether the displayed score is a new highscore
*/ */

View file

@ -3,6 +3,7 @@ package funkin.ui.freeplay.charselect;
import funkin.data.IRegistryEntry; import funkin.data.IRegistryEntry;
import funkin.data.freeplay.player.PlayerData; import funkin.data.freeplay.player.PlayerData;
import funkin.data.freeplay.player.PlayerRegistry; 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"). * An object used to retrieve data about a playable character (also known as "weeks").
@ -87,6 +88,32 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
return _data.freeplayDJ.getFreeplayDJText(index); 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<PlayerResultsAnimationData>
{
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. * Returns whether this character is unlocked.
*/ */