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

Save high scores and high ranks separately.

This commit is contained in:
EliteMasterEric 2024-06-11 00:40:43 -04:00
parent 00021523bc
commit b30faad7d9
6 changed files with 251 additions and 80 deletions

View file

@ -2818,8 +2818,13 @@ class PlayState extends MusicBeatSubState
deathCounter = 0; deathCounter = 0;
// TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
// `easy`, `erect`, `normal-pico`, etc.
var suffixedDifficulty = (currentVariation != Constants.DEFAULT_VARIATION
&& currentVariation != 'erect') ? '$currentDifficulty-${currentVariation}' : currentDifficulty;
var isNewHighscore = false; var isNewHighscore = false;
var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, currentDifficulty); var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, suffixedDifficulty);
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
@ -2844,13 +2849,21 @@ class PlayState extends MusicBeatSubState
// adds current song data into the tallies for the level (story levels) // adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) if (!isPracticeMode && !isBotPlayMode)
{
isNewHighscore = Save.instance.isSongHighScore(currentSong.id, suffixedDifficulty, data);
// If no high score is present, save both score and rank.
// If score or rank are better, save the highest one.
// If neither are higher, nothing will change.
Save.instance.applySongRank(currentSong.id, suffixedDifficulty, data);
if (isNewHighscore)
{ {
Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds #if newgrounds
NGio.postScore(score, currentSong.id); NGio.postScore(score, currentSong.id);
#end #end
isNewHighscore = true; }
} }
} }

View file

@ -356,7 +356,10 @@ class Scoring
// Perfect (Platinum) is a Sick Full Clear // Perfect (Platinum) is a Sick Full Clear
var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes; var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
if (isPerfectGold) return ScoringRank.PERFECT_GOLD; if (isPerfectGold)
{
return ScoringRank.PERFECT_GOLD;
}
// Else, use the standard grades // Else, use the standard grades
@ -397,62 +400,79 @@ enum abstract ScoringRank(String)
var GOOD; var GOOD;
var SHIT; var SHIT;
@:op(A > B) static function compare(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool /**
* Converts ScoringRank to an integer value for comparison.
* Better ranks should be tied to a higher value.
*/
static function getValue(rank:Null<ScoringRank>):Int
{
if (rank == null) return -1;
switch (rank)
{
case PERFECT_GOLD:
return 5;
case PERFECT:
return 4;
case EXCELLENT:
return 3;
case GREAT:
return 2;
case GOOD:
return 1;
case SHIT:
return 0;
default:
return -1;
}
}
// Yes, we really need a different function for each comparison operator.
@:op(A > B) static function compareGT(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{ {
if (a != null && b == null) return true; if (a != null && b == null) return true;
if (a == null || b == null) return false; if (a == null || b == null) return false;
var temp1:Int = 0; var temp1:Int = getValue(a);
var temp2:Int = 0; var temp2:Int = getValue(b);
// temp 1 return temp1 > temp2;
switch (a)
{
case PERFECT_GOLD:
temp1 = 5;
case PERFECT:
temp1 = 4;
case EXCELLENT:
temp1 = 3;
case GREAT:
temp1 = 2;
case GOOD:
temp1 = 1;
case SHIT:
temp1 = 0;
default:
temp1 = -1;
} }
// temp 2 @:op(A >= B) static function compareGTEQ(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
switch (b)
{ {
case PERFECT_GOLD: if (a != null && b == null) return true;
temp2 = 5; if (a == null || b == null) return false;
case PERFECT:
temp2 = 4; var temp1:Int = getValue(a);
case EXCELLENT: var temp2:Int = getValue(b);
temp2 = 3;
case GREAT: return temp1 >= temp2;
temp2 = 2;
case GOOD:
temp2 = 1;
case SHIT:
temp2 = 0;
default:
temp2 = -1;
} }
if (temp1 > temp2) @:op(A < B) static function compareLT(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{ {
return true; if (a != null && b == null) return true;
if (a == null || b == null) return false;
var temp1:Int = getValue(a);
var temp2:Int = getValue(b);
return temp1 < temp2;
} }
else
@:op(A <= B) static function compareLTEQ(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{ {
return false; if (a != null && b == null) return true;
} if (a == null || b == null) return false;
var temp1:Int = getValue(a);
var temp2:Int = getValue(b);
return temp1 <= temp2;
} }
// @:op(A == B) isn't necessary!
/** /**
* Delay in seconds * Delay in seconds
*/ */
@ -462,15 +482,15 @@ enum abstract ScoringRank(String)
{ {
case PERFECT_GOLD | PERFECT: case PERFECT_GOLD | PERFECT:
// return 2.5; // return 2.5;
return 95/24; return 95 / 24;
case EXCELLENT: case EXCELLENT:
return 0; return 0;
case GREAT: case GREAT:
return 5/24; return 5 / 24;
case GOOD: case GOOD:
return 3/24; return 3 / 24;
case SHIT: case SHIT:
return 2/24; return 2 / 24;
default: default:
return 3.5; return 3.5;
} }
@ -482,15 +502,15 @@ enum abstract ScoringRank(String)
{ {
case PERFECT_GOLD | PERFECT: case PERFECT_GOLD | PERFECT:
// return 2.5; // return 2.5;
return 95/24; return 95 / 24;
case EXCELLENT: case EXCELLENT:
return 97/24; return 97 / 24;
case GREAT: case GREAT:
return 95/24; return 95 / 24;
case GOOD: case GOOD:
return 95/24; return 95 / 24;
case SHIT: case SHIT:
return 95/24; return 95 / 24;
default: default:
return 3.5; return 3.5;
} }
@ -502,15 +522,15 @@ enum abstract ScoringRank(String)
{ {
case PERFECT_GOLD | PERFECT: case PERFECT_GOLD | PERFECT:
// return 2.5; // return 2.5;
return 129/24; return 129 / 24;
case EXCELLENT: case EXCELLENT:
return 122/24; return 122 / 24;
case GREAT: case GREAT:
return 109/24; return 109 / 24;
case GOOD: case GOOD:
return 107/24; return 107 / 24;
case SHIT: case SHIT:
return 186/24; return 186 / 24;
default: default:
return 3.5; return 3.5;
} }
@ -522,15 +542,15 @@ enum abstract ScoringRank(String)
{ {
case PERFECT_GOLD | PERFECT: case PERFECT_GOLD | PERFECT:
// return 2.5; // return 2.5;
return 140/24; return 140 / 24;
case EXCELLENT: case EXCELLENT:
return 140/24; return 140 / 24;
case GREAT: case GREAT:
return 129/24; return 129 / 24;
case GOOD: case GOOD:
return 127/24; return 127 / 24;
case SHIT: case SHIT:
return 207/24; return 207 / 24;
default: default:
return 3.5; return 3.5;
} }

View file

@ -1,6 +1,7 @@
package funkin.save; package funkin.save;
import flixel.util.FlxSave; import flixel.util.FlxSave;
import funkin.util.FileUtil;
import funkin.input.Controls.Device; import funkin.input.Controls.Device;
import funkin.play.scoring.Scoring; import funkin.play.scoring.Scoring;
import funkin.play.scoring.Scoring.ScoringRank; import funkin.play.scoring.Scoring.ScoringRank;
@ -58,7 +59,7 @@ class Save
this.data = data; this.data = data;
// Make sure the verison number is up to date before we flush. // Make sure the verison number is up to date before we flush.
this.data.version = Save.SAVE_DATA_VERSION; updateVersionToLatest();
} }
public static function getDefault():RawSaveData public static function getDefault():RawSaveData
@ -503,7 +504,7 @@ class Save
} }
/** /**
* Apply the score the user achieved for a given song on a given difficulty. * Directly set the score the user achieved for a given song on a given difficulty.
*/ */
public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
{ {
@ -518,6 +519,44 @@ class Save
flush(); flush();
} }
/**
* Only replace the ranking data for the song, because the old score is still better.
*/
public function applySongRank(songId:String, difficultyId:String, newScoreData:SaveScoreData):Void
{
var newRank = Scoring.calculateRank(newScoreData);
if (newScoreData == null || newRank == null) return;
var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
data.scores.songs.set(songId, song);
}
var previousScoreData = song.get(difficultyId);
var previousRank = Scoring.calculateRank(previousScoreData);
if (previousScoreData == null || previousRank == null)
{
// Directly set the highscore.
setSongScore(songId, difficultyId, newScoreData);
return;
}
// Set the high score and the high rank separately.
var newScore:SaveScoreData =
{
score: (previousScoreData.score > newScoreData.score) ? previousScoreData.score : newScoreData.score,
tallies: (previousRank > newRank) ? previousScoreData.tallies : newScoreData.tallies
};
song.set(difficultyId, newScore);
flush();
}
/** /**
* Is the provided score data better than the current high score for the given song? * Is the provided score data better than the current high score for the given song?
* @param songId The song ID to check. * @param songId The song ID to check.
@ -543,6 +582,39 @@ class Save
return score.score > currentScore.score; return score.score > currentScore.score;
} }
/**
* Is the provided score data better than the current rank for the given song?
* @param songId The song ID to check.
* @param difficultyId The difficulty to check.
* @param score The score to check the rank for.
* @return Whether the score's rank is better than the current rank.
*/
public function isSongHighRank(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
var newScoreRank = Scoring.calculateRank(score);
if (newScoreRank == null)
{
// The provided score is invalid.
return false;
}
var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
data.scores.songs.set(songId, song);
}
var currentScore = song.get(difficultyId);
var currentScoreRank = Scoring.calculateRank(currentScore);
if (currentScoreRank == null)
{
// There is no primary highscore for this song.
return true;
}
return newScoreRank > currentScoreRank;
}
/** /**
* Has the provided song been beaten on one of the listed difficulties? * Has the provided song been beaten on one of the listed difficulties?
* @param songId The song ID to check. * @param songId The song ID to check.
@ -832,6 +904,29 @@ class Save
return cast legacySave.data; return cast legacySave.data;
} }
} }
/**
* Serialize this Save into a JSON string.
* @param pretty Whether the JSON should be big ol string (false),
* or formatted with tabs (true)
* @return The JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<RawSaveData>(ignoreNullOptionals);
return writer.write(data, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.data.version = Save.SAVE_DATA_VERSION;
}
public function debug_dumpSave():Void
{
FileUtil.saveFile(haxe.io.Bytes.ofString(this.serialize()), [FileUtil.FILE_FILTER_JSON], null, null, './save.json', 'Write save data as JSON...');
}
} }
/** /**
@ -904,6 +999,9 @@ typedef SaveHighScoresData =
typedef SaveDataMods = typedef SaveDataMods =
{ {
var enabledMods:Array<String>; var enabledMods:Array<String>;
// TODO: Make this not trip up the serializer when debugging.
@:jignored
var modOptions:Map<String, Dynamic>; var modOptions:Map<String, Dynamic>;
} }

View file

@ -1608,7 +1608,19 @@ class FreeplayState extends MusicBeatSubState
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData; var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
if (daSong != null) if (daSong != null)
{ {
var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); // TODO: Make this actually be the variation you're focused on. We don't need to fetch the song metadata just to calculate it.
var targetSong:Song = SongRegistry.instance.fetchEntry(grpCapsules.members[curSelected].songData.songId);
if (targetSong == null)
{
FlxG.log.warn('WARN: could not find song with id (${grpCapsules.members[curSelected].songData.songId})');
return;
}
var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty);
// TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
var suffixedDifficulty = (targetVariation != Constants.DEFAULT_VARIATION
&& targetVariation != 'erect') ? '$currentDifficulty-${targetVariation}' : currentDifficulty;
var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, suffixedDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
rememberedDifficulty = currentDifficulty; rememberedDifficulty = currentDifficulty;

View file

@ -371,6 +371,33 @@ class MainMenuState extends MusicBeatState
} }
}); });
} }
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.R)
{
// Give the user a hypothetical overridden score,
// and see if we can maintain that golden P rank.
funkin.save.Save.instance.setSongScore('tutorial', 'easy',
{
score: 1234567,
tallies:
{
sick: 0,
good: 0,
bad: 0,
shit: 1,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 1,
totalNotes: 10,
}
});
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.E)
{
funkin.save.Save.instance.debug_dumpSave();
}
#end #end
if (FlxG.sound.music != null && FlxG.sound.music.volume < 0.8) if (FlxG.sound.music != null && FlxG.sound.music.volume < 0.8)

View file

@ -19,6 +19,7 @@ import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo;
class FileUtil class FileUtil
{ {
public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json");
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");