Funkin/source/funkin/play/scoring/Scoring.hx
2023-01-22 22:25:45 -05:00

259 lines
6.9 KiB
Haxe

package funkin.play.scoring;
enum abstract ScoringSystem(String)
{
/**
* The scoring system used in versions of the game Week 6 and older.
* Scores the player based on judgement, represented by a step function.
*/
var LEGACY;
/**
* The scoring system used in Week 7. It has tighter scoring windows than Legacy.
* Scores the player based on judgement, represented by a step function.
*/
var WEEK7;
/**
* Points Based On Timing scoring system, version 1
* Scores the player based on the offset based on timing, represented by a sigmoid function.
*/
var PBOT1;
// WIFE1
// WIFE3
}
/**
* A static class which holds any functions related to scoring.
*/
class Scoring
{
/**
* Determine the score a note receives under a given scoring system.
* @param msTiming The difference between the note's time and when it was hit.
* @param scoringSystem The scoring system to use.
* @return The score the note receives.
*/
public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1)
{
switch (scoringSystem)
{
case LEGACY:
return scoreNote_LEGACY(msTiming);
case WEEK7:
return scoreNote_WEEK7(msTiming);
case PBOT1:
return scoreNote_PBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 0;
}
}
/**
* Determine the judgement a note receives under a given scoring system.
* @param msTiming The difference between the note's time and when it was hit.
* @return The judgement the note receives.
*/
public static function judgeNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):String
{
switch (scoringSystem)
{
case LEGACY:
return judgeNote_LEGACY(msTiming);
case WEEK7:
return judgeNote_WEEK7(msTiming);
case PBOT1:
return judgeNote_PBOT1(msTiming);
default:
trace('ERROR: Unknown scoring system: ' + scoringSystem);
return 'miss';
}
}
/**
* The maximum score received.
*/
public static var PBOT1_MAX_SCORE = 350;
/**
* The minimum score received.
*/
public static var PBOT1_MIN_SCORE = 0;
/**
* The threshold at which a note hit is considered perfect and always given the max score.
**/
public static var PBOT1_PERFECT_THRESHOLD = 5.0; // 5ms.
/**
* The threshold at which a note hit is considered missed and always given the min score.
**/
public static var PBOT1_MISS_THRESHOLD = (10 / 60) * 1000; // ~166ms
// Magic numbers used to tweak the shape of the scoring function.
public static var PBOT1_SCORING_SLOPE:Float = 0.052;
public static var PBOT1_SCORING_OFFSET:Float = 80.0;
static function scoreNote_PBOT1(msTiming:Float):Int
{
// Absolute value because otherwise late hits are always given the max score.
var absTiming = Math.abs(msTiming);
if (absTiming > PBOT1_MISS_THRESHOLD)
{
return PBOT1_MIN_SCORE;
}
else if (absTiming < PBOT1_PERFECT_THRESHOLD)
{
return PBOT1_MAX_SCORE;
}
else
{
// Calculate the score based on the timing using a sigmoid function.
var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET))));
var score = Std.int(PBOT1_MAX_SCORE * factor);
return score;
}
}
static function judgeNote_PBOT1(msTiming:Float):String
{
return judgeNote_WEEK7(msTiming);
}
/**
* The window of time in which a note is considered to be hit, on the Funkin Legacy scoring system.
* Currently equal to 10 frames at 60fps, or ~166ms.
*/
public static var LEGACY_HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps)
/**
* The threshold at which a note is considered a "Bad" hit rather than a "Shit" hit.
* Represented as a percentage of the total hit window.
*/
public static var LEGACY_BAD_THRESHOLD:Float = 0.9;
public static var LEGACY_GOOD_THRESHOLD:Float = 0.75;
public static var LEGACY_SICK_THRESHOLD:Float = 0.2;
public static var LEGACY_SHIT_SCORE = 50;
public static var LEGACY_BAD_SCORE = 100;
public static var LEGACY_GOOD_SCORE = 200;
public static var LEGACY_SICK_SCORE = 350;
static function scoreNote_LEGACY(msTiming:Float):Int
{
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD)
{
return LEGACY_SICK_SCORE;
}
else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD)
{
return LEGACY_GOOD_SCORE;
}
else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD)
{
return LEGACY_BAD_SCORE;
}
else if (absTiming < LEGACY_HIT_WINDOW)
{
return LEGACY_SHIT_SCORE;
}
else
{
return 0;
}
}
static function judgeNote_LEGACY(msTiming:Float):String
{
var absTiming = Math.abs(msTiming);
if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD)
{
return 'sick';
}
else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD)
{
return 'good';
}
else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD)
{
return 'bad';
}
else if (absTiming < LEGACY_HIT_WINDOW)
{
return 'shit';
}
else
{
return 'miss';
}
}
/**
* The window of time in which a note is considered to be hit, on the Funkin Classic scoring system.
* Same as L 10 frames at 60fps, or ~166ms.
*/
public static var WEEK7_HIT_WINDOW = LEGACY_HIT_WINDOW;
public static var WEEK7_BAD_THRESHOLD = 0.8; // 80% of the hit window, or ~125ms
public static var WEEK7_GOOD_THRESHOLD = 0.55; // 55% of the hit window, or ~91ms
public static var WEEK7_SICK_THRESHOLD = 0.2; // 20% of the hit window, or ~33ms
public static var WEEK7_SHIT_SCORE = 50;
public static var WEEK7_BAD_SCORE = 100;
public static var WEEK7_GOOD_SCORE = 200;
public static var WEEK7_SICK_SCORE = 350;
static function scoreNote_WEEK7(msTiming:Float):Int
{
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
{
return WEEK7_SICK_SCORE;
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
{
return WEEK7_GOOD_SCORE;
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
{
return WEEK7_BAD_SCORE;
}
else if (absTiming < WEEK7_HIT_WINDOW)
{
return WEEK7_SHIT_SCORE;
}
else
{
return 0;
}
}
static function judgeNote_WEEK7(msTiming:Float):String
{
var absTiming = Math.abs(msTiming);
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
{
return 'sick';
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
{
return 'good';
}
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
{
return 'bad';
}
else if (absTiming < WEEK7_HIT_WINDOW)
{
return 'shit';
}
else
{
return 'miss';
}
}
}