Input System Rewrite (#810)
* the freaking rewrite bro! * backwards compatibility * listened to some feedback * legacyJudge now determines on the API number * listened to feedback (again)
This commit is contained in:
@@ -118,6 +118,10 @@ final class NoteHitEvent extends CancellableEvent {
|
||||
* The attached healthIcon used distinction for icons amongst others
|
||||
*/
|
||||
public var healthIcon:HealthIcon;
|
||||
/**
|
||||
* Whether note hits are judged in the old way or not.
|
||||
*/
|
||||
public var legacyJudge(get, set):Bool;
|
||||
|
||||
/**
|
||||
* Prevents the default sing animation from being played.
|
||||
@@ -196,4 +200,15 @@ final class NoteHitEvent extends CancellableEvent {
|
||||
characters = [char];
|
||||
return char;
|
||||
}
|
||||
|
||||
private var _explicitLegacyJudge:Null<Bool> = null;
|
||||
private inline function get_legacyJudge():Bool {
|
||||
if (_explicitLegacyJudge != null)
|
||||
return _explicitLegacyJudge;
|
||||
return Flags.CURRENT_API_VERSION == 1;
|
||||
}
|
||||
private function set_legacyJudge(value:Bool):Bool {
|
||||
_explicitLegacyJudge = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class Flags {
|
||||
@:lazy public static var SAVE_PATH:String = haxe.macro.Compiler.getDefine("SAVE_PATH");
|
||||
@:lazy public static var SAVE_NAME:String = haxe.macro.Compiler.getDefine("SAVE_NAME");
|
||||
|
||||
public static var CURRENT_API_VERSION:Int = 1;
|
||||
public static var CURRENT_API_VERSION:Int = 2;
|
||||
public static var COMMIT_NUMBER:Int = GitCommitMacro.commitNumber;
|
||||
public static var COMMIT_HASH:String = GitCommitMacro.commitHash;
|
||||
public static var COMMIT_MESSAGE:String = 'Commit $COMMIT_NUMBER ($COMMIT_HASH)';
|
||||
|
||||
@@ -60,6 +60,15 @@ class Note extends FlxSprite
|
||||
*/
|
||||
public var sustainParent:Null<Note>;
|
||||
|
||||
/**
|
||||
* Number of active sustain pieces attached to this note
|
||||
*
|
||||
* Increases by 1 every time a hold piece is initialized.
|
||||
*
|
||||
* Decreases by 1 every time a hold piece gets destroyed.
|
||||
*/
|
||||
public var tailCount:Int = 0;
|
||||
|
||||
/**
|
||||
* Name of the splash.
|
||||
*/
|
||||
@@ -104,6 +113,8 @@ class Note extends FlxSprite
|
||||
|
||||
public var animSuffix:String = null;
|
||||
|
||||
public var tripTimer:Float = 0; // ranges from 0 to 1
|
||||
|
||||
|
||||
private static function customTypePathExists(path:String) {
|
||||
if (__customNoteTypeExists.exists(path))
|
||||
|
||||
@@ -29,6 +29,8 @@ import funkin.editors.charter.Charter;
|
||||
import funkin.editors.charter.CharterSelection;
|
||||
import funkin.game.SplashHandler;
|
||||
import funkin.game.cutscenes.*;
|
||||
import funkin.game.scoring.*;
|
||||
import funkin.game.scoring.RatingManager.Rating;
|
||||
import funkin.menus.*;
|
||||
import funkin.backend.week.WeekData;
|
||||
import funkin.savedata.FunkinSave;
|
||||
@@ -529,6 +531,10 @@ class PlayState extends MusicBeatState
|
||||
* Group containing all of the combo sprites.
|
||||
*/
|
||||
public var comboGroup:RotatingSpriteGroup;
|
||||
/**
|
||||
* Manager that helps judge note hits to return ratings.
|
||||
*/
|
||||
public var ratingManager:RatingManager = new RatingManager();
|
||||
/**
|
||||
* Whenever the Rating sprites should be shown or not.
|
||||
*
|
||||
@@ -1882,40 +1888,44 @@ class PlayState extends MusicBeatState
|
||||
* CALCULATES RATING
|
||||
*/
|
||||
var noteDiff = Math.abs(Conductor.songPosition - note.strumTime);
|
||||
var daRating:String = "sick";
|
||||
var score:Int = 300;
|
||||
var accuracy:Float = 1;
|
||||
|
||||
if (noteDiff > hitWindow * 0.9)
|
||||
{
|
||||
daRating = 'shit';
|
||||
score = 50;
|
||||
accuracy = 0.25;
|
||||
}
|
||||
else if (noteDiff > hitWindow * 0.75)
|
||||
{
|
||||
daRating = 'bad';
|
||||
score = 100;
|
||||
accuracy = 0.45;
|
||||
}
|
||||
else if (noteDiff > hitWindow * 0.2)
|
||||
{
|
||||
daRating = 'good';
|
||||
score = 200;
|
||||
accuracy = 0.75;
|
||||
}
|
||||
var daRating:Rating = ratingManager.judgeNote(noteDiff);
|
||||
|
||||
var event:NoteHitEvent;
|
||||
if (strumLine != null && !strumLine.cpu)
|
||||
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick", 0.5, true, 0.7, true, true, iconP1);
|
||||
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, daRating.score, note.isSustainNote ? null : daRating.accuracy, 0.023, daRating.name, Options.splashesEnabled && !note.isSustainNote && daRating.splash, 0.5, true, 0.7, true, true, iconP1);
|
||||
else
|
||||
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false, 0.5, true, 0.7, true, true, iconP2);
|
||||
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating.name, false, 0.5, true, 0.7, true, true, iconP2);
|
||||
event.deleteNote = !note.isSustainNote; // work around, to allow sustain notes to be deleted
|
||||
event = scripts.event(strumLine != null && !strumLine.cpu ? "onPlayerHit" : "onDadHit", event);
|
||||
strumLine.onHit.dispatch(event);
|
||||
gameAndCharsEvent("onNoteHit", event);
|
||||
|
||||
if (!event.cancelled) {
|
||||
if (event.legacyJudge) {
|
||||
event.rating = 'sick';
|
||||
event.score = 300;
|
||||
event.accuracy = 1;
|
||||
|
||||
if (noteDiff > hitWindow * 0.9)
|
||||
{
|
||||
event.rating = 'shit';
|
||||
event.score = 50;
|
||||
event.accuracy = 0.25;
|
||||
}
|
||||
else if (noteDiff > hitWindow * 0.75)
|
||||
{
|
||||
event.rating = 'bad';
|
||||
event.score = 100;
|
||||
event.accuracy = 0.45;
|
||||
}
|
||||
else if (noteDiff > hitWindow * 0.2)
|
||||
{
|
||||
event.rating = 'good';
|
||||
event.score = 200;
|
||||
event.accuracy = 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
if (!note.isSustainNote) {
|
||||
if (event.countScore) songScore += event.score;
|
||||
if (event.accuracy != null) {
|
||||
|
||||
@@ -161,6 +161,9 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
curLen = Math.min(len, Conductor.stepCrochet);
|
||||
notes.members[total-(il++)-1] = prev = new Note(this, note, true, curLen, note.sLen - len, prev);
|
||||
len -= curLen;
|
||||
|
||||
if (prev != null && prev.sustainParent != null)
|
||||
prev.sustainParent.tailCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,10 +239,23 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
|
||||
|
||||
if (__updateNote_event.strum == null) return;
|
||||
|
||||
if (__updateNote_event.__reposNote) __updateNote_event.strum.updateNotePosition(daNote);
|
||||
|
||||
|
||||
if (daNote.isSustainNote)
|
||||
{
|
||||
daNote.updateSustain(__updateNote_event.strum);
|
||||
|
||||
if (daNote.tripTimer > 0 && daNote.tailCount > 3)
|
||||
{
|
||||
daNote.tripTimer -= 0.05 / daNote.sustainLength;
|
||||
if (daNote.tripTimer <= 0)
|
||||
{
|
||||
daNote.tripTimer = 0;
|
||||
daNote.canBeHit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var __funcsToExec:Array<Note->Void> = [];
|
||||
@@ -249,16 +265,27 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
var __notePerStrum:Array<Note> = [];
|
||||
|
||||
function __inputProcessPressed(note:Note) {
|
||||
if (__pressed[note.strumID] && note.isSustainNote && note.strumTime < __updateNote_songPos && !note.wasGoodHit) {
|
||||
if (__pressed[note.strumID] && note.isSustainNote && note.sustainParent != null && note.sustainParent.wasGoodHit && note.strumTime < __updateNote_songPos && !note.wasGoodHit) {
|
||||
note.tripTimer = 1;
|
||||
PlayState.instance.goodNoteHit(this, note);
|
||||
note.updateSustainClip();
|
||||
}
|
||||
}
|
||||
function __inputProcessJustPressed(note:Note) {
|
||||
if (__justPressed[note.strumID] && !note.isSustainNote && !note.wasGoodHit && note.canBeHit) {
|
||||
if (__notePerStrum[note.strumID] == null) __notePerStrum[note.strumID] = note;
|
||||
else if (Math.abs(__notePerStrum[note.strumID].strumTime - note.strumTime) <= 2) deleteNote(note);
|
||||
else if (note.strumTime < __notePerStrum[note.strumID].strumTime) __notePerStrum[note.strumID] = note;
|
||||
var cur = __notePerStrum[note.strumID];
|
||||
var songPos = __updateNote_songPos;
|
||||
|
||||
var noteDist = Math.abs(note.strumTime - songPos);
|
||||
var curDist = cur != null ? Math.abs(cur.strumTime - songPos) : 999999;
|
||||
|
||||
var notePenalty = note.avoid ? 1 : 0;
|
||||
var curPenalty = (cur != null && cur.avoid) ? 1 : 0;
|
||||
|
||||
if (cur == null
|
||||
|| notePenalty < curPenalty
|
||||
|| (notePenalty == curPenalty && noteDist < curDist))
|
||||
__notePerStrum[note.strumID] = note;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,14 +299,14 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
if (cpu) return;
|
||||
|
||||
__funcsToExec.clear();
|
||||
__pressed.clear();
|
||||
__justPressed.clear();
|
||||
__justReleased.clear();
|
||||
__pressed.resize(members.length);
|
||||
__justPressed.resize(members.length);
|
||||
__justReleased.resize(members.length);
|
||||
|
||||
for(s in members) {
|
||||
__pressed.push(s.__getPressed(this));
|
||||
__justPressed.push(s.__getJustPressed(this));
|
||||
__justReleased.push(s.__getJustReleased(this));
|
||||
for(i in 0...members.length) {
|
||||
__pressed[i] = members[i].__getPressed(this);
|
||||
__justPressed[i] = members[i].__getJustPressed(this);
|
||||
__justReleased[i] = members[i].__getJustReleased(this);
|
||||
}
|
||||
|
||||
var event = EventManager.get(InputSystemEvent).recycle(__pressed, __justPressed, __justReleased, this, id);
|
||||
@@ -411,6 +438,8 @@ class StrumLine extends FlxTypedGroup<Strum> {
|
||||
var event:SimpleNoteEvent = EventManager.get(SimpleNoteEvent).recycle(note);
|
||||
onNoteDelete.dispatch(event);
|
||||
if (!event.cancelled) {
|
||||
if (note.isSustainNote && note.sustainParent != null && note.sustainParent.tailCount > 0)
|
||||
note.sustainParent.tailCount--;
|
||||
note.kill();
|
||||
notes.remove(note, true);
|
||||
note.destroy();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package funkin.game.scoring;
|
||||
|
||||
import haxe.ds.StringMap;
|
||||
|
||||
class HitWindowData
|
||||
{
|
||||
public static function getWindows(preset:WindowPreset):StringMap<Float>
|
||||
{
|
||||
var map = new StringMap<Float>();
|
||||
|
||||
switch (preset) {
|
||||
// Old Codename, really forgiving inputs (hard to get bad ratings)
|
||||
case CNE_CLASSIC:
|
||||
map.set("sick", 50.0);
|
||||
map.set("good", 187.5);
|
||||
map.set("bad", 225.0);
|
||||
map.set("shit", 250.0);
|
||||
// Week 7
|
||||
case FNF_CLASSIC:
|
||||
map.set("sick", 33.334);
|
||||
map.set("good", 125.0025);
|
||||
map.set("bad", 150.003);
|
||||
map.set("shit", 166.67);
|
||||
// V-Slice
|
||||
case FNF_VSLICE:
|
||||
map.set("sick", 45.0);
|
||||
map.set("good", 90.0);
|
||||
map.set("bad", 135.4);
|
||||
map.set("shit", 180.0);
|
||||
// Default, taken from Etterna
|
||||
case _:
|
||||
map.set("sick", 37.8);
|
||||
map.set("good", 75.6);
|
||||
map.set("bad", 113.4);
|
||||
map.set("shit", 180.0);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static var JUDGE_SCALES:Array<Float> = [1.5, 1.33, 1.16, 1.0, 0.84, 0.66, 0.5, 0.33, 0.2];
|
||||
public static function scaleWindows(windows:StringMap<Float>, scale:Float):StringMap<Float>
|
||||
{
|
||||
var scaled = new StringMap<Float>();
|
||||
for (k in windows.keys())
|
||||
scaled.set(k, windows.get(k) * scale);
|
||||
return scaled;
|
||||
}
|
||||
|
||||
public static function offsetWindows(windows:StringMap<Float>, offset:Float):StringMap<Float>
|
||||
{
|
||||
var adjusted = new StringMap<Float>();
|
||||
for (k in windows.keys())
|
||||
adjusted.set(k, windows.get(k) + offset);
|
||||
return adjusted;
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract WindowPreset(Int) from Int to Int
|
||||
{
|
||||
var DEFAULT = 0;
|
||||
var CNE_CLASSIC = 1;
|
||||
var FNF_CLASSIC = 2;
|
||||
var FNF_VSLICE = 3;
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return switch (cast this : WindowPreset)
|
||||
{
|
||||
case CNE_CLASSIC: "Codename (Classic)";
|
||||
case FNF_CLASSIC: "Funkin' (Week 7)";
|
||||
case FNF_VSLICE: "Funkin' (V-Slice)";
|
||||
case _: "Default";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package funkin.game.scoring;
|
||||
|
||||
import funkin.game.scoring.*;
|
||||
import funkin.game.scoring.HitWindowData.WindowPreset;
|
||||
|
||||
import haxe.ds.StringMap;
|
||||
|
||||
/**
|
||||
* Judges note hits and returns a rating.
|
||||
*/
|
||||
class RatingManager
|
||||
{
|
||||
public var hitWindows:StringMap<Float>;
|
||||
public var ratingData:Array<Rating> = [];
|
||||
|
||||
public function new(?preset:WindowPreset):Void
|
||||
{
|
||||
var usedPreset = preset != null ? preset : WindowPreset.DEFAULT;
|
||||
hitWindows = HitWindowData.getWindows(usedPreset);
|
||||
initDefaultData(hitWindows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rating based on a wimdow of time.
|
||||
* @param time The timing window to judge.
|
||||
*/
|
||||
public function judgeNote(time:Float):Rating
|
||||
{
|
||||
for (i => rating in ratingData)
|
||||
{
|
||||
if (rating.hittable && rating.window > -1 && time <= rating.window)
|
||||
{
|
||||
return rating;
|
||||
}
|
||||
}
|
||||
return ratingData.last();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the default rating data containing the four judgements.
|
||||
*
|
||||
* "Sick", "Good", "Bad", "Shit"
|
||||
*/
|
||||
public function initDefaultData(windows:StringMap<Float>)
|
||||
{
|
||||
inline function getWindow(name:String):Float
|
||||
{
|
||||
return windows.exists(name) ? windows.get(name) : -1;
|
||||
}
|
||||
|
||||
addRating({name: "sick", window: getWindow("sick"), accuracy: 1, score: 300, splash: true});
|
||||
addRating({name: "good", window: getWindow("good"), accuracy: 0.75, score: 200});
|
||||
addRating({name: "bad", window: getWindow("bad"), accuracy: 0.45, score: 100});
|
||||
addRating({name: "shit", window: getWindow("shit"), accuracy: 0.25, score: 50});
|
||||
}
|
||||
|
||||
public function addRating(data:Dynamic)
|
||||
{
|
||||
if (data == null || data.name == null) return;
|
||||
|
||||
var name = data.name.toLowerCase();
|
||||
var window = data.window != null
|
||||
? data.window
|
||||
: (hitWindows.exists(name) ? hitWindows.get(name) : -1);
|
||||
|
||||
var newRating:Rating = {
|
||||
name: name,
|
||||
window: window,
|
||||
accuracy: data.accuracy != null ? data.accuracy : 1,
|
||||
score: data.score != null ? data.score : 0,
|
||||
splash: data.splash == true,
|
||||
hittable: data.hittable != null ? data.hittable : true
|
||||
};
|
||||
|
||||
var existingIndex = -1;
|
||||
for (i in 0...ratingData.length)
|
||||
if (ratingData[i].name == name)
|
||||
existingIndex = i;
|
||||
|
||||
if (existingIndex >= 0)
|
||||
ratingData[existingIndex] = newRating;
|
||||
else
|
||||
ratingData.push(newRating);
|
||||
|
||||
ratingData.sort((a, b) -> Reflect.compare(a.window, b.window));
|
||||
}
|
||||
|
||||
public function removeRating(name:String):Void
|
||||
{
|
||||
if (name == null) return;
|
||||
name = name.toLowerCase();
|
||||
ratingData = ratingData.filter(r -> r.name != name);
|
||||
}
|
||||
|
||||
public function getHitWindow(name:String):Float
|
||||
{
|
||||
return hitWindows.exists(name) ? hitWindows.get(name) : -1;
|
||||
}
|
||||
}
|
||||
|
||||
@:structInit
|
||||
final class Rating
|
||||
{
|
||||
/**
|
||||
* Name of rating.
|
||||
*
|
||||
* Also used for the image file name of the rating.
|
||||
*/
|
||||
public var name:String = "unknown";
|
||||
|
||||
/**
|
||||
* Amount of accuracy given when earning this rating.
|
||||
*/
|
||||
public var accuracy:Float = 0.0;
|
||||
|
||||
/**
|
||||
* MS Timing Window to hit the rating.
|
||||
*/
|
||||
public var window:Float = -1;
|
||||
|
||||
/**
|
||||
* Amount of score given when earning this rating.
|
||||
*/
|
||||
public var score:Int = 0;
|
||||
|
||||
/**
|
||||
* If this rating was hit, a note splash will appear.
|
||||
*/
|
||||
@:optional public var splash:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the rating is hittable or not.
|
||||
*/
|
||||
@:optional public var hittable:Bool = true;
|
||||
}
|
||||
Reference in New Issue
Block a user