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

@ -2284,7 +2284,7 @@ class PlayState extends MusicBeatSubState
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
}
// Make sure the player keeps singing while the note is held by the bot.
if (isBotPlayMode && currentStage != null && currentStage.getBoyfriend() != null && currentStage.getBoyfriend().isSinging())
{
@ -2818,8 +2818,13 @@ class PlayState extends MusicBeatSubState
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 prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, currentDifficulty);
var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, suffixedDifficulty);
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)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
if (!isPracticeMode && !isBotPlayMode)
{
Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
isNewHighscore = true;
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)
{
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
}
}
}

View file

@ -356,7 +356,10 @@ class Scoring
// Perfect (Platinum) is a Sick Full Clear
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
@ -397,62 +400,79 @@ enum abstract ScoringRank(String)
var GOOD;
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 false;
var temp1:Int = 0;
var temp2:Int = 0;
var temp1:Int = getValue(a);
var temp2:Int = getValue(b);
// temp 1
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
switch (b)
{
case PERFECT_GOLD:
temp2 = 5;
case PERFECT:
temp2 = 4;
case EXCELLENT:
temp2 = 3;
case GREAT:
temp2 = 2;
case GOOD:
temp2 = 1;
case SHIT:
temp2 = 0;
default:
temp2 = -1;
}
if (temp1 > temp2)
{
return true;
}
else
{
return false;
}
return temp1 > temp2;
}
@:op(A >= B) static function compareGTEQ(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{
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) static function compareLT(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{
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) static function compareLTEQ(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
{
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
*/
@ -462,15 +482,15 @@ enum abstract ScoringRank(String)
{
case PERFECT_GOLD | PERFECT:
// return 2.5;
return 95/24;
return 95 / 24;
case EXCELLENT:
return 0;
case GREAT:
return 5/24;
return 5 / 24;
case GOOD:
return 3/24;
return 3 / 24;
case SHIT:
return 2/24;
return 2 / 24;
default:
return 3.5;
}
@ -482,15 +502,15 @@ enum abstract ScoringRank(String)
{
case PERFECT_GOLD | PERFECT:
// return 2.5;
return 95/24;
return 95 / 24;
case EXCELLENT:
return 97/24;
return 97 / 24;
case GREAT:
return 95/24;
return 95 / 24;
case GOOD:
return 95/24;
return 95 / 24;
case SHIT:
return 95/24;
return 95 / 24;
default:
return 3.5;
}
@ -502,15 +522,15 @@ enum abstract ScoringRank(String)
{
case PERFECT_GOLD | PERFECT:
// return 2.5;
return 129/24;
return 129 / 24;
case EXCELLENT:
return 122/24;
return 122 / 24;
case GREAT:
return 109/24;
return 109 / 24;
case GOOD:
return 107/24;
return 107 / 24;
case SHIT:
return 186/24;
return 186 / 24;
default:
return 3.5;
}
@ -522,15 +542,15 @@ enum abstract ScoringRank(String)
{
case PERFECT_GOLD | PERFECT:
// return 2.5;
return 140/24;
return 140 / 24;
case EXCELLENT:
return 140/24;
return 140 / 24;
case GREAT:
return 129/24;
return 129 / 24;
case GOOD:
return 127/24;
return 127 / 24;
case SHIT:
return 207/24;
return 207 / 24;
default:
return 3.5;
}

View file

@ -1,6 +1,7 @@
package funkin.save;
import flixel.util.FlxSave;
import funkin.util.FileUtil;
import funkin.input.Controls.Device;
import funkin.play.scoring.Scoring;
import funkin.play.scoring.Scoring.ScoringRank;
@ -58,7 +59,7 @@ class Save
this.data = data;
// 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
@ -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
{
@ -518,6 +519,44 @@ class Save
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?
* @param songId The song ID to check.
@ -543,6 +582,39 @@ class Save
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?
* @param songId The song ID to check.
@ -832,6 +904,29 @@ class Save
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 =
{
var enabledMods:Array<String>;
// TODO: Make this not trip up the serializer when debugging.
@:jignored
var modOptions:Map<String, Dynamic>;
}

View file

@ -1608,7 +1608,19 @@ class FreeplayState extends MusicBeatSubState
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
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;
intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
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
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
{
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_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");