mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2024-11-15 11:22:55 +00:00
Merge pull request #617 from FunkinCrew/bugfix/save-highest-rank
[FUNK-387] Save high scores and high ranks separately.
This commit is contained in:
commit
817c216d06
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1565,7 +1565,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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
Loading…
Reference in a new issue