mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-09-01 03:15:53 +00:00
987 lines
30 KiB
Haxe
987 lines
30 KiB
Haxe
package funkin.ui.options;
|
|
|
|
import funkin.ui.MenuList.MenuTypedList;
|
|
import funkin.ui.TextMenuList.TextMenuItem;
|
|
import funkin.util.GRhythmUtil;
|
|
import funkin.mobile.ui.FunkinBackButton;
|
|
#if mobile
|
|
import funkin.mobile.ui.FunkinHitbox;
|
|
import funkin.mobile.ui.FunkinHitbox.FunkinHitboxControlSchemes;
|
|
import funkin.mobile.input.ControlsHandler;
|
|
import funkin.util.TouchUtil;
|
|
#end
|
|
import funkin.input.PreciseInputManager;
|
|
import funkin.audio.FunkinSound;
|
|
import funkin.play.notes.Strumline;
|
|
import funkin.play.notes.NoteSprite;
|
|
import funkin.graphics.FunkinCamera;
|
|
import funkin.graphics.FunkinSprite;
|
|
import funkin.data.song.SongData.SongNoteData;
|
|
import funkin.data.notestyle.NoteStyleRegistry;
|
|
import funkin.play.notes.notestyle.NoteStyle;
|
|
import funkin.ui.options.items.NumberPreferenceItem;
|
|
import haxe.Int64;
|
|
import flixel.FlxSprite;
|
|
import flixel.text.FlxText;
|
|
import flixel.FlxObject;
|
|
import flixel.util.FlxColor;
|
|
import flixel.math.FlxMath;
|
|
import flixel.tweens.FlxEase;
|
|
import flixel.tweens.FlxTween;
|
|
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
|
|
|
/*
|
|
ArrowData is a structure that holds the sprite and beat of an arrow.
|
|
@param sprite The sprite of the arrow.
|
|
@param beat The beat of the arrow.
|
|
*/
|
|
typedef ArrowData =
|
|
{
|
|
var sprite:FunkinSprite;
|
|
// var debugText:FlxText;
|
|
var beat:Float;
|
|
var direction:Int; // 0 = left, 1 = down, 2 = up, 3 = right
|
|
};
|
|
|
|
class OffsetMenu extends Page<OptionsState.OptionsMenuPageName>
|
|
{
|
|
static final BPM:Int = 100;
|
|
|
|
// Page<OptionsState.OptionsMenuPageName> stuff
|
|
var offsetItem:NumberPreferenceItem;
|
|
var items:TextMenuList;
|
|
var preferenceItems:FlxTypedSpriteGroup<FlxSprite>;
|
|
var backButton:FunkinBackButton;
|
|
|
|
// Background
|
|
var blackRect:FlxSprite;
|
|
|
|
// Text for the jump-in message and count
|
|
var jumpInText:FlxText;
|
|
var countText:FlxText;
|
|
|
|
// Elements for the offset calibration (receptor, arrows, strumline, etc)
|
|
var arrows:Array<ArrowData> = [];
|
|
var receptor:FunkinSprite;
|
|
var testStrumline:Strumline;
|
|
|
|
// Camera for the menu
|
|
var menuCamera:FunkinCamera;
|
|
// Variable to check if we're calibrating or testing
|
|
var calibrating:Bool = false;
|
|
|
|
// Variables for the offset calibration
|
|
var appliedOffsetLerp:Float = 0;
|
|
var savedOffset:Int = 0;
|
|
var tempOffset:Int = 0;
|
|
|
|
// Variables for transitioning between states
|
|
var lerped:Float = 0;
|
|
var shouldOffset:Int = 0;
|
|
var offsetLerp:Float = 0;
|
|
var scaleModifier:Float = 1;
|
|
|
|
// Variables for keeping time and beat
|
|
var localConductor:Conductor;
|
|
var arrowBeat:Float = 0;
|
|
|
|
// Variables for differences and consistency functionality
|
|
var _gotMad:Bool = false;
|
|
var differences:Array<Float> = [];
|
|
|
|
var msPerBeat(get, never):Float;
|
|
|
|
// The milliseconds per beat, calculated from the BPM.
|
|
function get_msPerBeat():Float
|
|
{
|
|
return 60000 / BPM;
|
|
}
|
|
|
|
/**
|
|
* Key press inputs which have been received but not yet processed.
|
|
* These are encoded with an OS timestamp, so we can account for input latency.
|
|
**/
|
|
var inputPressQueue:Array<PreciseInputEvent> = [];
|
|
|
|
/**
|
|
* Key release inputs which have been received but not yet processed.
|
|
* These are encoded with an OS timestamp, so we can account for input latency.
|
|
**/
|
|
var inputReleaseQueue:Array<PreciseInputEvent> = [];
|
|
|
|
/*
|
|
Creates an arrow at the specified beat.
|
|
The arrow will be positioned below the screen and will move up to the receptor.
|
|
@param beat The beat at which to create the arrow.
|
|
*/
|
|
public function createArrow(beat:Float):Void
|
|
{
|
|
var arrow = new FunkinSprite(0, 0);
|
|
arrow.loadGraphic(Paths.image('latencyArrow'));
|
|
arrow.origin.set(0.5, 0.5);
|
|
arrow.setPosition(FlxG.width / 2, FlxG.height + arrow.height); // Below the screen
|
|
arrow.updateHitbox();
|
|
arrow.cameras = [menuCamera];
|
|
add(arrow);
|
|
|
|
/*var debugText = new FlxText(0, 0);
|
|
debugText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, FlxTextAlign.CENTER);
|
|
debugText.text = 'Beat: ' + beat;
|
|
debugText.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
|
|
add(debugText); */
|
|
|
|
arrows.push(
|
|
{
|
|
sprite: arrow,
|
|
/*debugText: debugText,*/
|
|
beat: beat,
|
|
direction: 0
|
|
});
|
|
}
|
|
|
|
/*
|
|
Creates a directed arrow at the specified beat and direction.
|
|
The direction can be 0 (left), 1 (down), 2 (up), or 3 (right).
|
|
@param beat The beat at which to create the arrow.
|
|
@param direction The direction of the arrow.
|
|
*/
|
|
public function createDirectedArrow(beat:Float, direction:Int):Void
|
|
{
|
|
var arrow = new FunkinSprite(0, 0);
|
|
arrow.loadGraphic(Paths.image('latencyArrow'));
|
|
arrow.origin.set(0.5, 0.5);
|
|
arrow.setPosition(FlxG.width / 2, FlxG.height + arrow.height); // Below the screen
|
|
arrow.updateHitbox();
|
|
add(arrow);
|
|
|
|
arrows.push({sprite: arrow, beat: beat, direction: direction});
|
|
}
|
|
|
|
/*
|
|
Gets the arrow at the specified beat.
|
|
@param beat The beat at which to get the arrow.
|
|
@return The ArrowData object containing the sprite and beat, or null if no arrow is found.
|
|
*/
|
|
public function getArrowAtBeat(beat:Float):ArrowData
|
|
{
|
|
for (arrow in arrows)
|
|
{
|
|
if (arrow.beat == beat) return arrow;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
Gets the closest arrow to the specified beat.
|
|
This is used to find the arrow that is closest to the current time.
|
|
@param beat The beat at which to find the closest arrow.
|
|
@return The ArrowData object containing the sprite and beat of the closest arrow.
|
|
*/
|
|
public function getClosestArrowAtBeat(beat:Float):ArrowData
|
|
{
|
|
var closest:ArrowData = null;
|
|
var closestDiff:Float = 1000000; // A large number to start with
|
|
|
|
for (arrow in arrows)
|
|
{
|
|
var diff:Float = arrow.beat - beat;
|
|
// trace('Checking arrow at beat: ' + arrow.beat + ' (diff: ' + diff + ')');
|
|
if (diff < closestDiff)
|
|
{
|
|
closestDiff = diff;
|
|
closest = arrow;
|
|
}
|
|
}
|
|
|
|
// trace('Closest arrow at beat: ' + (closest != null ? closest.beat : 0) + ' (diff: ' + closestDiff + ')');
|
|
|
|
return closest;
|
|
}
|
|
|
|
public function new()
|
|
{
|
|
super();
|
|
|
|
localConductor = new Conductor();
|
|
localConductor.forceBPM(100);
|
|
|
|
menuCamera = new FunkinCamera('prefMenu');
|
|
FlxG.cameras.add(menuCamera, false);
|
|
menuCamera.bgColor = 0x0;
|
|
|
|
camera = menuCamera;
|
|
|
|
blackRect = new FlxSprite(0, 0);
|
|
blackRect.makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
|
blackRect.alpha = 0;
|
|
blackRect.scrollFactor.set(0, 0);
|
|
add(blackRect);
|
|
|
|
/*debugBeatText = new FlxText(0, 0);
|
|
debugBeatText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, FlxTextAlign.LEFT);
|
|
debugBeatText.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
|
|
debugBeatText.setPosition(10, 10);
|
|
debugBeatText.scrollFactor.set(0, 0);
|
|
add(debugBeatText);
|
|
|
|
debugBeatText.alpha = 0; */
|
|
|
|
receptor = new FunkinSprite(0, 0);
|
|
receptor.loadGraphic(Paths.image('latencyReceptor'));
|
|
receptor.origin.set(0.5, 0.5);
|
|
add(receptor);
|
|
|
|
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchDefault();
|
|
|
|
testStrumline = new Strumline(noteStyle, true);
|
|
// center
|
|
testStrumline.setPosition(FlxG.width / 2, FlxG.height / 2);
|
|
testStrumline.x -= testStrumline.width / 2;
|
|
testStrumline.scrollFactor.set(0, 0);
|
|
add(testStrumline);
|
|
|
|
testStrumline.cameras = [menuCamera];
|
|
|
|
testStrumline.conductorInUse = localConductor;
|
|
testStrumline.zIndex = 1001;
|
|
for (strum in testStrumline)
|
|
{
|
|
strum.alpha = 0;
|
|
}
|
|
|
|
receptor.alpha = 0;
|
|
receptor.centerOffsets();
|
|
receptor.scale.set(0, 0);
|
|
receptor.centerOrigin();
|
|
receptor.updateHitbox();
|
|
|
|
jumpInText = new FlxText(0, 0);
|
|
jumpInText.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.CENTER);
|
|
jumpInText.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.BLACK, 4);
|
|
add(jumpInText);
|
|
|
|
receptor.cameras = [menuCamera];
|
|
jumpInText.cameras = [menuCamera];
|
|
|
|
// below receptor
|
|
|
|
countText = new FlxText(0, 0);
|
|
countText.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.CENTER);
|
|
countText.setBorderStyle(FlxTextBorderStyle.OUTLINE, FlxColor.BLACK, 4);
|
|
add(countText);
|
|
|
|
jumpInText.alpha = 0;
|
|
jumpInText.setPosition(FlxG.width / 2, 150);
|
|
jumpInText.scrollFactor.set(0, 0);
|
|
|
|
countText.text = '';
|
|
countText.alpha = 0;
|
|
countText.setPosition(FlxG.width / 2, 600);
|
|
countText.scrollFactor.set(0, 0);
|
|
|
|
countText.cameras = [menuCamera];
|
|
|
|
add(items = new TextMenuList());
|
|
add(preferenceItems = new FlxTypedSpriteGroup<FlxSprite>());
|
|
|
|
offsetItem = createPrefItemNumber('Offset (Global)', 'Offset (Global)', function(value:Float) {
|
|
Preferences.globalOffset = Std.int(value);
|
|
}, null, Preferences.globalOffset, -1500, 1500, 1.0, 2, 5);
|
|
createButtonItem('Reset Offset', function() {
|
|
Preferences.globalOffset = 0;
|
|
offsetItem.currentValue = Preferences.globalOffset;
|
|
});
|
|
createButtonItem('Offset Calibration', function() {
|
|
// Reset calibration state and start another one.
|
|
|
|
jumpInText.text = 'Press any key to the beat!\nThe arrow will start to sync to the receptor.';
|
|
#if mobile
|
|
jumpInText.text = 'Tap to the beat!\nThe arrow will start to sync to the receptor.';
|
|
#end
|
|
|
|
jumpInText.y = 100;
|
|
|
|
countText.text = 'Current Offset: 0ms';
|
|
|
|
calibrating = true;
|
|
MenuTypedList.pauseInput = true;
|
|
OptionsState.instance.drumsBG.pause();
|
|
OptionsState.instance.drumsBG.time = FlxG.sound.music.time;
|
|
OptionsState.instance.drumsBG.resume();
|
|
OptionsState.instance.drumsBG.fadeIn(1, 0, 1);
|
|
canExit = false;
|
|
differences = [];
|
|
offsetLerp = 0;
|
|
savedOffset = Preferences.globalOffset;
|
|
Preferences.globalOffset = 0; // We save the offset and set it to 0 so the player can recalibrate.
|
|
shouldOffset = 1;
|
|
tempOffset = 0;
|
|
appliedOffsetLerp = 0;
|
|
|
|
arrowBeat = Math.floor(localConductor.currentBeatTime) + 4;
|
|
receptor.angle = 0;
|
|
|
|
_gotMad = false;
|
|
});
|
|
createButtonItem('Test', function() {
|
|
// Reset testing state and start another one.
|
|
// We do not reset the offset here, so the player can test their current offset.
|
|
|
|
shouldOffset = 1;
|
|
testStrumline.clean();
|
|
testStrumline.noteData = [];
|
|
testStrumline.nextNoteIndex = 0;
|
|
|
|
OptionsState.instance.drumsBG.pause();
|
|
OptionsState.instance.drumsBG.time = FlxG.sound.music.time;
|
|
OptionsState.instance.drumsBG.resume();
|
|
localConductor.update(FlxG.sound.music.time, true);
|
|
|
|
var floored = Math.floor(localConductor.currentBeatTime);
|
|
arrowBeat = floored - (floored % 4);
|
|
arrowBeat += 4;
|
|
_lastDirection = 0;
|
|
|
|
var diffBeats = Math.floor(arrowBeat - localConductor.currentBeatTime);
|
|
|
|
trace('Prestart arrowBeat: ' + arrowBeat);
|
|
|
|
if (diffBeats < 4) arrowBeat += (arrowBeat % 4) + 4; // Ensure we have at least 4 beats to test.
|
|
|
|
trace('Testing strumline at beat: ' + arrowBeat + ' diff: ' + diffBeats);
|
|
|
|
jumpInText.text = 'Hit the notes as they come in!';
|
|
#if mobile
|
|
if (OptionsState.instance.hitbox != null) OptionsState.instance.hitbox.visible = true;
|
|
if (!ControlsHandler.usingExternalInputDevice)
|
|
{
|
|
final amplification:Float = (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
|
|
final playerStrumlineScale:Float = ((FlxG.height / FlxG.width) * 1.95) * amplification;
|
|
final playerNoteSpacing:Float = ((FlxG.height / FlxG.width) * 2.8) * amplification;
|
|
|
|
testStrumline.strumlineScale.set(playerStrumlineScale, playerStrumlineScale);
|
|
testStrumline.setNoteSpacing(playerNoteSpacing);
|
|
testStrumline.width *= 2;
|
|
|
|
testStrumline.x = (FlxG.width - testStrumline.width) / 2 + Constants.STRUMLINE_X_OFFSET;
|
|
testStrumline.y = (FlxG.height - testStrumline.height) * 0.95 - Constants.STRUMLINE_Y_OFFSET;
|
|
testStrumline.y -= 10;
|
|
}
|
|
else
|
|
{
|
|
if (testStrumline != null)
|
|
{
|
|
testStrumline.destroy();
|
|
remove(testStrumline);
|
|
}
|
|
|
|
testStrumline = new Strumline(noteStyle, true);
|
|
// center
|
|
testStrumline.setPosition(FlxG.width / 2, FlxG.height / 2);
|
|
testStrumline.x -= testStrumline.width / 2;
|
|
testStrumline.scrollFactor.set(0, 0);
|
|
add(testStrumline);
|
|
}
|
|
#end
|
|
MenuTypedList.pauseInput = true;
|
|
OptionsState.instance.drumsBG.fadeIn(1, 0, 1);
|
|
canExit = false;
|
|
differences = [];
|
|
|
|
jumpInText.y = 350;
|
|
|
|
#if mobile
|
|
if (ControlsHandler.usingExternalInputDevice)
|
|
{
|
|
#end
|
|
testStrumline.y = Preferences.downscroll ? FlxG.height - (testStrumline.height + 45) - Constants.STRUMLINE_Y_OFFSET : (testStrumline.height / 2)
|
|
- Constants.STRUMLINE_Y_OFFSET;
|
|
if (Preferences.downscroll) jumpInText.y = FlxG.height - 425;
|
|
testStrumline.isDownscroll = Preferences.downscroll;
|
|
#if mobile
|
|
}
|
|
else
|
|
{
|
|
jumpInText.y = FlxG.height - 425;
|
|
}
|
|
#end
|
|
});
|
|
PreciseInputManager.instance.onInputPressed.add(onKeyPress);
|
|
PreciseInputManager.instance.onInputReleased.add(onKeyRelease);
|
|
|
|
// camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70);
|
|
// if (items != null) camFollow.y = items.selectedItem.y;
|
|
|
|
// menuCamera.follow(camFollow, null, 0.085);
|
|
// var margin = 160;
|
|
// menuCamera.deadzone.set(0, margin, menuCamera.width, menuCamera.height - margin * 2);
|
|
// menuCamera.minScrollY = 0;
|
|
|
|
// items.onChange.add(function(selected) {
|
|
// camFollow.y = selected.y;
|
|
// });
|
|
backButton = new FunkinBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, handleMobileExit);
|
|
#if FEATURE_TOUCH_CONTROLS // We do this here because we want to animate the back button (on Mobile), but we don't want it on Desktop.
|
|
add(backButton);
|
|
#end
|
|
}
|
|
|
|
/**
|
|
* Callback executed when one of the note keys is pressed.
|
|
*/
|
|
function onKeyPress(event:PreciseInputEvent):Void
|
|
{
|
|
// Do the minimal possible work here.
|
|
inputPressQueue.push(event);
|
|
}
|
|
|
|
/**
|
|
* Callback executed when one of the note keys is released.
|
|
*/
|
|
function onKeyRelease(event:PreciseInputEvent):Void
|
|
{
|
|
// Do the minimal possible work here.
|
|
inputReleaseQueue.push(event);
|
|
}
|
|
|
|
// Exits the calibration and resets the offset.
|
|
public function exitCalibration(cancel:Bool):Void
|
|
{
|
|
backButton.enabled = false;
|
|
shouldOffset = -1;
|
|
#if mobile
|
|
if (OptionsState.instance.hitbox != null) OptionsState.instance.hitbox.visible = false;
|
|
#end
|
|
tempOffset = 0;
|
|
if (cancel)
|
|
{
|
|
if (calibrating) Preferences.globalOffset = savedOffset;
|
|
#if !mobile
|
|
// mobile would play this twice
|
|
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
|
#end
|
|
}
|
|
else
|
|
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
|
offsetItem.currentValue = Preferences.globalOffset;
|
|
OptionsState.instance.drumsBG.fadeOut(1, 0);
|
|
}
|
|
|
|
// Handles the exit for mobile devices.
|
|
public function handleMobileExit():Void
|
|
{
|
|
if (shouldOffset == 1) exitCalibration(true);
|
|
else if (shouldOffset == 0) exit();
|
|
}
|
|
|
|
// Returns the average of the differences in milliseconds.
|
|
// Average is the sum of all differences divided by the number of differences.
|
|
public function getAverage():Float
|
|
{
|
|
if (differences.length == 0) return 0;
|
|
|
|
var avg:Float = 0;
|
|
for (i in 0...differences.length)
|
|
{
|
|
avg += differences[i];
|
|
}
|
|
avg /= differences.length;
|
|
|
|
return avg;
|
|
}
|
|
|
|
// Returns the consistency of the differences.
|
|
// Consistency is the average of the squared differences from the mean.
|
|
public function getConsistency():Float
|
|
{
|
|
if (differences.length == 0) return 0;
|
|
|
|
var avg:Float = getAverage();
|
|
|
|
var variance:Float = 0;
|
|
for (i in 0...differences.length)
|
|
{
|
|
variance += Math.pow(differences[i] - avg, 2);
|
|
}
|
|
variance /= differences.length;
|
|
|
|
return Math.sqrt(variance);
|
|
}
|
|
|
|
var _offsetLerpTime:Float = 0;
|
|
var _lastOffset:Float = 0;
|
|
var _lastDirection:Int = 0;
|
|
|
|
/* Adds a difference in milliseconds to the list.
|
|
If there are more than 4 differences, it calculates the average and sets the global offset.
|
|
This is used for calibrating the offset based on user input.
|
|
@param ms The difference in milliseconds to add.
|
|
@see Preferences.globalOffset
|
|
*/
|
|
public function addDifference(ms:Float):Void
|
|
{
|
|
differences.push(ms);
|
|
|
|
if (differences.length > 2 && differences.length % 2 != 0 && calibrating)
|
|
{
|
|
var avg:Float = getAverage();
|
|
tempOffset = Std.int(avg);
|
|
_lastOffset = appliedOffsetLerp;
|
|
_offsetLerpTime = 0;
|
|
}
|
|
}
|
|
|
|
var _lastBeat:Float = 0;
|
|
var _lastTime:Float = 0;
|
|
|
|
override function update(elapsed:Float):Void
|
|
{
|
|
super.update(elapsed);
|
|
localConductor.update(localConductor.songPosition + elapsed * 1000, false);
|
|
|
|
var b:Float = localConductor.currentBeatTime;
|
|
|
|
// Restart logic
|
|
if (FlxG.sound.music.time < _lastTime)
|
|
{
|
|
localConductor.update(FlxG.sound.music.time, !calibrating);
|
|
b = localConductor.currentBeatTime;
|
|
|
|
// Update arrows to be the correct distance away from the receptor.
|
|
var lastArrowBeat:Float = 0;
|
|
for (i in 0...arrows.length)
|
|
{
|
|
var arrow:ArrowData = arrows[i];
|
|
var beatDiff:Float = arrow.beat - _lastBeat;
|
|
|
|
arrow.beat = b + beatDiff;
|
|
lastArrowBeat = arrow.beat;
|
|
}
|
|
if (calibrating)
|
|
{
|
|
arrowBeat = lastArrowBeat;
|
|
}
|
|
else
|
|
arrowBeat = 4;
|
|
|
|
testStrumline.clean();
|
|
testStrumline.noteData = [];
|
|
testStrumline.nextNoteIndex = 0;
|
|
trace('Restarting conductor');
|
|
|
|
_lastTime = FlxG.sound.music.time;
|
|
return;
|
|
}
|
|
|
|
_lastBeat = b;
|
|
|
|
// Resync logic
|
|
var diff:Float = Math.abs((FlxG.sound.music.time + localConductor.combinedOffset) - localConductor.songPosition);
|
|
var diffBg:Float = Math.abs(FlxG.sound.music.time - OptionsState.instance.drumsBG.time);
|
|
if (diff > 50 || diffBg > 50)
|
|
{
|
|
trace('Resyncing conductor: ' + (diff > diffBg ? diff : diffBg) + 'ms difference');
|
|
|
|
// If the difference is greater than 50ms, we resync the conductor.
|
|
localConductor.update(FlxG.sound.music.time, true);
|
|
OptionsState.instance.drumsBG.pause();
|
|
OptionsState.instance.drumsBG.time = FlxG.sound.music.time;
|
|
OptionsState.instance.drumsBG.resume();
|
|
b = localConductor.currentBeatTime;
|
|
_lastBeat = b;
|
|
}
|
|
|
|
_lastTime = FlxG.sound.music.time;
|
|
|
|
// Back logic
|
|
if (controls.BACK && shouldOffset == 1)
|
|
{
|
|
exitCalibration(true);
|
|
return;
|
|
}
|
|
|
|
// Calibration logic
|
|
if (shouldOffset == 1 && calibrating)
|
|
{
|
|
// Lerp our offset
|
|
if (_offsetLerpTime < 1) _offsetLerpTime += elapsed * 2;
|
|
else
|
|
_offsetLerpTime = 1;
|
|
|
|
appliedOffsetLerp = FlxMath.lerp(_lastOffset, tempOffset, _offsetLerpTime);
|
|
|
|
countText.text = 'Current Offset: ' + Std.int(appliedOffsetLerp) + 'ms';
|
|
|
|
var toRemove:Array<ArrowData> = [];
|
|
var _lastArrowBeat:Float = 0;
|
|
// Update arrows
|
|
for (i in 0...arrows.length)
|
|
{
|
|
var arrow:ArrowData = arrows[i];
|
|
|
|
var beatOffset:Float = appliedOffsetLerp / msPerBeat;
|
|
|
|
var ms:Float = arrow.beat * msPerBeat;
|
|
var offset:Float = GRhythmUtil.getNoteY(ms + appliedOffsetLerp, 2, false, localConductor);
|
|
arrow.sprite.y = receptor.y + offset - (arrow.sprite.height / 2);
|
|
arrow.sprite.x = receptor.x - (arrow.sprite.width / 2);
|
|
|
|
// arrow.debugText.text = 'Beat: ' + arrow.beat;
|
|
// arrow.debugText.setPosition(arrow.sprite.x + arrow.sprite.width, arrow.sprite.y - 20);
|
|
|
|
if ((arrow.beat + beatOffset) < b - 0.25)
|
|
{
|
|
arrow.sprite.alpha -= elapsed * 5;
|
|
}
|
|
|
|
if (arrow.beat == _lastArrowBeat || arrow.sprite.alpha <= 0)
|
|
{
|
|
toRemove.push(arrow);
|
|
arrow.sprite.kill();
|
|
continue;
|
|
}
|
|
_lastArrowBeat = arrow.beat;
|
|
}
|
|
|
|
// Remove arrows that are marked for removal.
|
|
for (arrow in toRemove)
|
|
{
|
|
// trace("Removing arrow at beat: " + arrow.beat);
|
|
arrows.remove(arrow);
|
|
}
|
|
|
|
while (b >= arrowBeat - 1)
|
|
{
|
|
// trace("Spawning arrow at beat: " + arrowBeat);
|
|
// Create a new arrow at the next beat division.
|
|
arrowBeat = (arrowBeat - (arrowBeat % 2)) + 2;
|
|
var nextBeat:Float = arrowBeat;
|
|
createArrow(nextBeat);
|
|
}
|
|
|
|
// Hit a note (calibration)
|
|
if (FlxG.keys.justPressed.ANY #if FEATURE_TOUCH_CONTROLS || TouchUtil.justPressed #end)
|
|
{
|
|
var arrow:ArrowData = getClosestArrowAtBeat(b);
|
|
|
|
var closestBeat:Float = Math.round(b);
|
|
var diff:Float = closestBeat - b;
|
|
var ms:Float = diff * msPerBeat;
|
|
|
|
if (arrow != null) // eric sees this and goes "OMG NULL REF!!!!"
|
|
{
|
|
var beatOffset:Float = appliedOffsetLerp / msPerBeat;
|
|
|
|
var arrowDiff:Float = (arrow.beat + beatOffset) - b;
|
|
|
|
if (Math.abs(arrowDiff) < 0.25)
|
|
{
|
|
arrow.sprite.alpha = 0;
|
|
arrow.sprite.kill();
|
|
// arrow.debugText.kill();
|
|
arrows.remove(arrow);
|
|
}
|
|
}
|
|
|
|
var consistency:Float = getConsistency();
|
|
|
|
if (consistency > 80 && differences.length > 4)
|
|
{
|
|
jumpInText.text = 'Try to be a little more consistent with your timing!';
|
|
differences = [];
|
|
tempOffset = 0;
|
|
appliedOffsetLerp = 0;
|
|
_gotMad = true;
|
|
return;
|
|
}
|
|
|
|
addDifference(ms);
|
|
|
|
if (differences.length >= 16)
|
|
{
|
|
jumpInText.text = 'Calibration complete!';
|
|
Preferences.globalOffset = tempOffset;
|
|
exitCalibration(false);
|
|
return;
|
|
}
|
|
|
|
if (!_gotMad)
|
|
{
|
|
if (Math.abs(ms + tempOffset) < 45) jumpInText.text = 'Great job, keep going!';
|
|
else
|
|
jumpInText.text = 'Nice job, keep going!';
|
|
}
|
|
|
|
jumpInText.text += '\n' + differences.length + '/16';
|
|
|
|
_gotMad = false;
|
|
|
|
scaleModifier = 0.75;
|
|
}
|
|
}
|
|
// Testing logic
|
|
else if (shouldOffset == 1)
|
|
{
|
|
// If we are not calibrating, we are just testing the strumline.
|
|
|
|
processInputQueue();
|
|
|
|
// trace(b + ' - ' + arrowBeat);
|
|
|
|
while (b >= arrowBeat - 2 && b < 124)
|
|
{
|
|
// trace("Spawning arrow at beat: " + arrowBeat);
|
|
// Create a new arrow at the next beat division.
|
|
arrowBeat = arrowBeat + 1;
|
|
var data:SongNoteData = new SongNoteData(arrowBeat * msPerBeat, _lastDirection, 0, null, null);
|
|
testStrumline.addNoteData(data, false);
|
|
|
|
// Create a jump (double note) every 8 beats to visually indicate first beat - requested by Hundrec
|
|
if (Math.floor(arrowBeat % 8) == 0)
|
|
{
|
|
var data:SongNoteData = new SongNoteData(arrowBeat * msPerBeat, 2, 0, null, null);
|
|
testStrumline.addNoteData(data, false);
|
|
}
|
|
|
|
_lastDirection = (_lastDirection + 1) % 4; // Cycle through directions 0-3
|
|
}
|
|
if (b >= 124 && _lastDirection != 0) _lastDirection = 0; // reset direction on loop
|
|
}
|
|
// Remove arrows and what not for when we are exiting calibration/testing
|
|
else
|
|
{
|
|
var toRemove:Array<ArrowData> = [];
|
|
for (i in 0...arrows.length)
|
|
{
|
|
var arrow:ArrowData = arrows[i];
|
|
arrow.sprite.alpha -= elapsed * 5;
|
|
if (arrow.sprite.alpha <= 0)
|
|
{
|
|
arrow.sprite.kill();
|
|
// arrow.debugText.kill();
|
|
toRemove.push(arrow);
|
|
}
|
|
}
|
|
|
|
// Remove arrows that are marked for removal.
|
|
for (arrow in toRemove)
|
|
{
|
|
arrows.remove(arrow);
|
|
}
|
|
}
|
|
// Transitioning logic (animations and what not)
|
|
if (lerped < 1) lerped += elapsed / 2;
|
|
else if (lerped > 1) lerped = 1;
|
|
|
|
if (shouldOffset == 1)
|
|
{
|
|
offsetLerp += elapsed / 2;
|
|
if (offsetLerp >= 1) offsetLerp = 1;
|
|
}
|
|
else if (shouldOffset == -1)
|
|
{
|
|
offsetLerp -= elapsed / 3;
|
|
if (offsetLerp <= 0) // We're exiting the calibration OR testing state
|
|
{
|
|
backButton.enabled = true;
|
|
canExit = true;
|
|
calibrating = false;
|
|
MenuTypedList.pauseInput = false;
|
|
offsetLerp = 0;
|
|
shouldOffset = 0;
|
|
}
|
|
}
|
|
|
|
blackRect.alpha = FlxMath.lerp(0, 0.5, FlxEase.cubeInOut(lerped));
|
|
|
|
var yLerp = FlxMath.lerp(-480, 100, FlxEase.cubeInOut(lerped));
|
|
var xLerp = FlxMath.lerp(0, FlxG.width, FlxEase.cubeInOut(offsetLerp));
|
|
|
|
// center
|
|
var recW = receptor.width;
|
|
var recH = receptor.height;
|
|
jumpInText.x = FlxG.width / 2 - (jumpInText.width / 2);
|
|
countText.x = FlxG.width / 2 - (countText.width / 2);
|
|
|
|
receptor.x = FlxG.width / 2 - (recW / 2);
|
|
receptor.y = FlxG.height / 2 - (recH / 2);
|
|
|
|
jumpInText.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
|
|
if (calibrating)
|
|
{
|
|
receptor.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
// debugBeatText.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
countText.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
}
|
|
else
|
|
{
|
|
testStrumline.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
backButton.y = FlxMath.lerp(FlxG.height - 200, 50, FlxEase.cubeInOut(offsetLerp));
|
|
}
|
|
|
|
if (scaleModifier < 1)
|
|
{
|
|
// trace("scaleModifier: " + scaleModifier);
|
|
scaleModifier += elapsed / 2;
|
|
if (scaleModifier >= 1) scaleModifier = 1;
|
|
}
|
|
|
|
receptor.scale.x = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp)) * scaleModifier;
|
|
receptor.scale.y = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp)) * scaleModifier;
|
|
|
|
// Update alpha and note window (canHit)
|
|
for (note in testStrumline.notes.members)
|
|
{
|
|
if (note == null) continue;
|
|
|
|
GRhythmUtil.processWindow(note, true, localConductor);
|
|
note.alpha = FlxMath.lerp(0, 1, FlxEase.cubeInOut(offsetLerp));
|
|
}
|
|
|
|
/*debugBeatText.x = receptor.x + receptor.width * 2;
|
|
debugBeatText.y = receptor.y - 20;
|
|
|
|
debugBeatText.text = 'Beat: ' + b; */
|
|
|
|
// receptor.angle += angleVel * elapsed;
|
|
|
|
var ind = 0;
|
|
// Indent the selected item.
|
|
items.forEach(function(daItem:TextMenuItem) {
|
|
// Initializing thy text width (if thou text present)
|
|
var thyTextWidth:Int = 0;
|
|
switch (Type.typeof(daItem))
|
|
{
|
|
case TClass(NumberPreferenceItem):
|
|
var numPref:NumberPreferenceItem = cast(daItem, NumberPreferenceItem);
|
|
thyTextWidth = numPref.lefthandText.getWidth();
|
|
|
|
numPref.lefthandText.x = xLerp + (FlxG.width / 2) - ((thyTextWidth + daItem.atlasText.getWidth() + 20) / 2);
|
|
numPref.lefthandText.y = yLerp + ((120 * ind) + 30);
|
|
daItem.x = numPref.lefthandText.x + thyTextWidth + 20;
|
|
default:
|
|
daItem.x = xLerp + (FlxG.width / 2) - daItem.atlasText.getWidth() / 2;
|
|
}
|
|
|
|
daItem.y = yLerp + ((120 * ind) + 30);
|
|
ind++;
|
|
});
|
|
}
|
|
|
|
function hitNote(note:NoteSprite, input:PreciseInputEvent):Void
|
|
{
|
|
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
|
|
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
|
|
|
|
var diff:Float = note.noteData.time - localConductor.songPosition;
|
|
|
|
// trace('Input latency: ' + inputLatencyMs + 'ms (diff: ' + diff + 'ms)');
|
|
|
|
var totalDiff:Float = diff;
|
|
if (totalDiff < 0) totalDiff = diff + inputLatencyMs;
|
|
else
|
|
totalDiff = diff - inputLatencyMs;
|
|
|
|
var noteDiff:Int = Std.int(totalDiff);
|
|
|
|
addDifference(noteDiff);
|
|
|
|
if (noteDiff == 0)
|
|
{
|
|
// \n to signify a line break (because the original text has 3 lines)
|
|
jumpInText.text = 'Perfect!\n';
|
|
}
|
|
else
|
|
{
|
|
jumpInText.text = noteDiff > 0 ? 'Early!\n' + noteDiff + 'ms' : 'Late!\n' + noteDiff + 'ms';
|
|
}
|
|
|
|
jumpInText.text += '\nAvg: ' + Std.int(getAverage()) + 'ms';
|
|
|
|
testStrumline.hitNote(note);
|
|
}
|
|
|
|
/**
|
|
* PreciseInputEvents are put into a queue between update() calls,
|
|
* and then processed here.
|
|
*/
|
|
function processInputQueue():Void
|
|
{
|
|
if (inputPressQueue.length + inputReleaseQueue.length == 0 || shouldOffset != 1) return;
|
|
|
|
var notesInRange:Array<NoteSprite> = testStrumline.getNotesMayHit();
|
|
|
|
var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []];
|
|
|
|
for (note in notesInRange)
|
|
notesByDirection[note.direction].push(note);
|
|
|
|
while (inputPressQueue.length > 0)
|
|
{
|
|
var input:PreciseInputEvent = inputPressQueue.shift();
|
|
|
|
testStrumline.pressKey(input.noteDirection);
|
|
|
|
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
|
|
|
|
// trace('Processing input: ' + input.noteDirection + ' with ' + notesInDirection.length + ' notes in range.');
|
|
|
|
if (notesInDirection.length == 0)
|
|
{
|
|
testStrumline.playPress(input.noteDirection);
|
|
}
|
|
else
|
|
{
|
|
// Choose the first note, deprioritizing low priority notes.
|
|
var targetNote:Null<NoteSprite> = notesInDirection.find((note) -> !note.lowPriority);
|
|
if (targetNote == null) targetNote = notesInDirection[0];
|
|
if (targetNote == null) continue;
|
|
|
|
hitNote(targetNote, input);
|
|
notesInDirection.remove(targetNote);
|
|
|
|
// Play the strumline animation.
|
|
testStrumline.playConfirm(input.noteDirection);
|
|
}
|
|
}
|
|
|
|
while (inputReleaseQueue.length > 0)
|
|
{
|
|
var input:PreciseInputEvent = inputReleaseQueue.shift();
|
|
|
|
// Play the strumline animation.
|
|
testStrumline.playStatic(input.noteDirection);
|
|
|
|
testStrumline.releaseKey(input.noteDirection);
|
|
}
|
|
|
|
testStrumline.noteVibrations.tryNoteVibration();
|
|
}
|
|
|
|
// Creates a button item with a callback.
|
|
function createButtonItem(name:String, callback:Void->Void):Void
|
|
{
|
|
var item = items.createItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, name, BOLD, callback);
|
|
items.addItem(name, item);
|
|
}
|
|
|
|
// Creates a preference item with a number input.
|
|
function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
|
|
step:Float = 0.1, precision:Int, dragStepMultiplier:Float = 1):NumberPreferenceItem
|
|
{
|
|
var item = new NumberPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, defaultValue, min, max, step,
|
|
precision, onChange, valueFormatter, dragStepMultiplier);
|
|
items.addItem(prefName, item);
|
|
preferenceItems.add(item.lefthandText);
|
|
return item;
|
|
}
|
|
|
|
override public function destroy()
|
|
{
|
|
MenuTypedList.pauseInput = false;
|
|
exitCalibration(true);
|
|
super.destroy();
|
|
}
|
|
}
|