fix bugs related to timing windows

add ability to adjust timing window size per-input
made rockers suck less
This commit is contained in:
minenice55 2024-05-12 18:45:23 -04:00
parent 423bf2e067
commit 087de8fab4
6 changed files with 86 additions and 64 deletions

View file

@ -322,11 +322,11 @@ namespace HeavenStudio
}
}
public void ScoreInputAccuracy(double beat, double accuracy, bool late, double time, float weight = 1, bool doDisplay = true)
public void ScoreInputAccuracy(double beat, double accuracy, bool late, double time, float pitch = 1, double margin = 0, float weight = 1, bool doDisplay = true)
{
// push the hit event to the timing display
if (doDisplay)
TimingAccuracyDisplay.instance.MakeAccuracyVfx(time, late);
TimingAccuracyDisplay.instance.MakeAccuracyVfx(time, pitch, margin, late);
if (weight > 0 && MarkerWeight > 0)
{

View file

@ -14,7 +14,10 @@ namespace HeavenStudio.Games
{
public class Minigame : MonoBehaviour
{
// root timing window values
public static double ngEarlyTimeBase = 0.1, justEarlyTimeBase = 0.05, aceEarlyTimeBase = 0.01, aceLateTimeBase = 0.01, justLateTimeBase = 0.05, ngLateTimeBase = 0.1;
// recommended added margin for release inputs
public static double releaseMargin = 0.01;
public static double rankHiThreshold = 0.8, rankOkThreshold = 0.6;
public static double ngEarlyTime => ngEarlyTimeBase * Conductor.instance?.SongPitch ?? 1;
@ -110,7 +113,7 @@ namespace HeavenStudio.Games
IA_PadBasicRelease, IA_TouchFlick, IA_BatonBasicRelease);
#endregion
public List<PlayerActionEvent> scheduledInputs = new List<PlayerActionEvent>();
[NonSerialized] public List<PlayerActionEvent> scheduledInputs = new List<PlayerActionEvent>();
/// <summary>
/// Schedule an Input for a later time in the minigame. Executes the methods put in parameters
@ -166,6 +169,22 @@ namespace HeavenStudio.Games
return evt;
}
public PlayerActionEvent ScheduleInput(
double startBeat,
double timer,
double margin,
PlayerInput.InputAction inputAction,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank,
PlayerActionEvent.ActionEventHittableQuery HittableQuery = null
)
{
PlayerActionEvent evt = ScheduleInput(startBeat, timer, inputAction, OnHit, OnMiss, OnBlank, HittableQuery);
evt.margin = margin;
return evt;
}
public PlayerActionEvent ScheduleAutoplayInput(double startBeat,
double timer,
PlayerInput.InputAction inputAction,
@ -236,7 +255,7 @@ namespace HeavenStudio.Games
{
PlayerActionEvent input = GetClosestScheduledInput(wantActionCategory);
if (input == null) return false;
return input.IsExpectingInputNow();
return input.IsExpectingInputNow(conductor);
}
public bool IsExpectingInputNow(PlayerInput.InputAction wantAction)
@ -245,46 +264,46 @@ namespace HeavenStudio.Games
}
// now should fix the fast bpm problem
public static double NgEarlyTime(float pitch = -1)
public static double NgEarlyTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 - ngEarlyTime;
return 1 - (ngEarlyTimeBase * pitch);
return 1 - (ngEarlyTime + margin);
return 1 - ((ngEarlyTime + margin) * pitch);
}
public static double JustEarlyTime(float pitch = -1)
public static double NgLateTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 - justEarlyTime;
return 1 - (justEarlyTimeBase * pitch);
return 1 + (ngLateTime + margin);
return 1 + ((ngLateTime + margin) * pitch);
}
public static double JustLateTime(float pitch = -1)
public static double JustEarlyTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 + justLateTime;
return 1 + (justLateTimeBase * pitch);
return 1 - (justEarlyTime + margin);
return 1 - ((justEarlyTime + margin) * pitch);
}
public static double NgLateTime(float pitch = -1)
public static double JustLateTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 + ngLateTime;
return 1 + (ngLateTimeBase * pitch);
return 1 + (justLateTime + margin);
return 1 + ((justLateTime + margin) * pitch);
}
public static double AceEarlyTime(float pitch = -1)
public static double AceEarlyTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 - aceEarlyTime;
return 1 - (aceEarlyTimeBase * pitch);
return 1 - (aceEarlyTime + margin);
return 1 - ((aceEarlyTime + margin) * pitch);
}
public static double AceLateTime(float pitch = -1)
public static double AceLateTime(float pitch = -1, double margin = 0)
{
if (pitch < 0)
return 1 + aceLateTime;
return 1 + (aceLateTimeBase * pitch);
return 1 + (aceLateTime + margin);
return 1 + ((aceLateTime + margin) * pitch);
}
public virtual void OnGameSwitch(double beat)
@ -340,7 +359,7 @@ namespace HeavenStudio.Games
public void ScoreMiss(float weight = 1f)
{
double beat = Conductor.instance?.songPositionInBeatsAsDouble ?? -1;
GameManager.instance.ScoreInputAccuracy(beat, 0, true, NgLateTime(), weight, false);
GameManager.instance.ScoreInputAccuracy(beat, 0, true, NgLateTime(), weight: weight, doDisplay: false);
}
public void ToggleSplitColoursDisplay(bool on)
@ -442,14 +461,17 @@ namespace HeavenStudio.Games
public Color GetColor() => MakeNewColor(startBeat, length, startColor, endColor, easeFunc);
public static Color MakeNewColor(double beat, float length, Color start, Color end, Util.EasingFunction.Function func)
{
if (length != 0) {
if (length != 0)
{
float normalizedBeat = length == 0 ? 1 : Mathf.Clamp01(Conductor.instance.GetPositionFromBeat(beat, length));
float newR = func(start.r, end.r, normalizedBeat);
float newG = func(start.g, end.g, normalizedBeat);
float newB = func(start.b, end.b, normalizedBeat);
return new Color(newR, newG, newB);
} else {
}
else
{
return end;
}
}
@ -471,7 +493,8 @@ namespace HeavenStudio.Games
/// The ease to use to transition between <paramref name="startColor"/> and <paramref name="endColor"/>.<br/>
/// Should be derived from <c>Util.EasingFunction.Ease</c>,
/// </param>
public ColorEase(double startBeat, float length, Color startColor, Color endColor, int ease) {
public ColorEase(double startBeat, float length, Color startColor, Color endColor, int ease)
{
this.startBeat = startBeat;
this.length = length;
(this.startColor, this.endColor) = (startColor, endColor);
@ -483,7 +506,8 @@ namespace HeavenStudio.Games
/// The constructor to use when initializing the ColorEase variable.
/// </summary>
/// <param name="defaultColor">The default color to initialize with.</param>
public ColorEase(Color? defaultColor = null) {
public ColorEase(Color? defaultColor = null)
{
startColor = endColor = defaultColor ?? Color.white;
easeFunc = Util.EasingFunction.Instant;
}

View file

@ -27,6 +27,7 @@ namespace HeavenStudio.Games
public double startBeat;
public double timer;
public float weight = 1f;
public double margin = 0;
public bool isEligible = true;
public bool canHit = true; //Indicates if you can still hit the cue or not. If set to false, it'll guarantee a miss
@ -108,7 +109,7 @@ namespace HeavenStudio.Games
}
//BUGFIX: ActionEvents destroyed too early
if (normalizedTime > Minigame.NgLateTime(cond.SongPitch)) Miss();
if (normalizedTime > Minigame.NgLateTime(cond.SongPitch, margin)) Miss();
if (lockedByEvent)
{
@ -122,10 +123,10 @@ namespace HeavenStudio.Games
if (!autoplayOnly && (IsHittable == null || IsHittable != null && IsHittable()) && IsCorrectInput(out double dt))
{
normalizedTime -= dt;
if (IsExpectingInputNow())
if (IsExpectingInputNow(cond))
{
double stateProg = ((normalizedTime - Minigame.JustEarlyTime()) / (Minigame.JustLateTime() - Minigame.JustEarlyTime()) - 0.5f) * 2;
Hit(stateProg, normalizedTime);
double stateProg = ((normalizedTime - Minigame.JustEarlyTime(cond.SongPitch, margin)) / (Minigame.JustLateTime(cond.SongPitch, margin) - Minigame.JustEarlyTime(cond.SongPitch, margin)) - 0.5f) * 2;
Hit(stateProg, normalizedTime, cond.SongPitch);
}
else
{
@ -198,7 +199,7 @@ namespace HeavenStudio.Games
}
}
public bool IsExpectingInputNow()
public bool IsExpectingInputNow(Conductor cond)
{
if (IsHittable != null)
{
@ -208,7 +209,7 @@ namespace HeavenStudio.Games
if (!isEligible) return false;
double normalizedBeat = GetNormalizedTime();
return normalizedBeat > Minigame.NgEarlyTime() && normalizedBeat < Minigame.NgLateTime();
return normalizedBeat > Minigame.NgEarlyTime(cond.SongPitch, margin) && normalizedBeat < Minigame.NgLateTime(cond.SongPitch, margin);
}
double GetNormalizedTime()
@ -242,7 +243,7 @@ namespace HeavenStudio.Games
}
//The state parameter is either -1 -> Early, 0 -> Perfect, 1 -> Late
public void Hit(double state, double time)
public void Hit(double state, double time, float pitch = 1)
{
GameManager gm = GameManager.instance;
if (OnHit != null && enabled)
@ -261,7 +262,7 @@ namespace HeavenStudio.Games
if (countsForAccuracy && gm.canInput && !(noAutoplay || autoplayOnly) && isEligible)
{
gm.ScoreInputAccuracy(startBeat + timer, TimeToAccuracy(time, pitchWhenHit), time > 1.0, time, weight, true);
gm.ScoreInputAccuracy(startBeat + timer, TimeToAccuracy(time, pitchWhenHit), time > 1.0, time, pitch, margin, weight, true);
if (state >= 1f || state <= -1f)
{
GoForAPerfect.instance.Miss();
@ -284,27 +285,27 @@ namespace HeavenStudio.Games
double TimeToAccuracy(double time, float pitch = -1)
{
if (pitch < 0) pitch = pitchWhenHit;
if (time >= Minigame.AceEarlyTime(pitch) && time <= Minigame.AceLateTime(pitch))
if (time >= Minigame.AceEarlyTime(pitch, margin) && time <= Minigame.AceLateTime(pitch, margin))
{
// Ace
return 1.0;
}
double state = 0;
if (time >= Minigame.JustEarlyTime(pitch) && time <= Minigame.JustLateTime(pitch))
if (time >= Minigame.JustEarlyTime(pitch, margin) && time <= Minigame.JustLateTime(pitch, margin))
{
// Good Hit
if (time > 1.0)
{
// late half of timing window
state = 1.0 - ((time - Minigame.AceLateTime(pitch)) / (Minigame.JustLateTime(pitch) - Minigame.AceLateTime(pitch)));
state = 1.0 - ((time - Minigame.AceLateTime(pitch, margin)) / (Minigame.JustLateTime(pitch, margin) - Minigame.AceLateTime(pitch, margin)));
state *= 1.0 - Minigame.rankHiThreshold;
state += Minigame.rankHiThreshold;
}
else
{
//early half of timing window
state = ((time - Minigame.JustEarlyTime(pitch)) / (Minigame.AceEarlyTime(pitch) - Minigame.JustEarlyTime(pitch)));
state = (time - Minigame.JustEarlyTime(pitch, margin)) / (Minigame.AceEarlyTime(pitch, margin) - Minigame.JustEarlyTime(pitch, margin));
state *= 1.0 - Minigame.rankHiThreshold;
state += Minigame.rankHiThreshold;
}
@ -314,13 +315,13 @@ namespace HeavenStudio.Games
if (time > 1.0)
{
// late half of timing window
state = 1.0 - ((time - Minigame.JustLateTime(pitch)) / (Minigame.NgLateTime(pitch) - Minigame.JustLateTime(pitch)));
state = 1.0 - ((time - Minigame.JustLateTime(pitch, margin)) / (Minigame.NgLateTime(pitch, margin) - Minigame.JustLateTime(pitch, margin)));
state *= Minigame.rankOkThreshold;
}
else
{
//early half of timing window
state = ((time - Minigame.JustEarlyTime(pitch)) / (Minigame.AceEarlyTime(pitch) - Minigame.JustEarlyTime(pitch)));
state = (time - Minigame.JustEarlyTime(pitch, margin)) / (Minigame.AceEarlyTime(pitch, margin) - Minigame.JustEarlyTime(pitch, margin));
state *= Minigame.rankOkThreshold;
}
}
@ -338,7 +339,7 @@ namespace HeavenStudio.Games
if (countsForAccuracy && !missable && gm.canInput && !(noAutoplay || autoplayOnly))
{
gm.ScoreInputAccuracy(startBeat + timer, 0, true, 2.0, weight, false);
gm.ScoreInputAccuracy(startBeat + timer, 0, true, 2.0, weight: weight, doDisplay: false);
GoForAPerfect.instance.Miss();
SectionMedalsManager.instance.MakeIneligible();
}

View file

@ -777,15 +777,15 @@ namespace HeavenStudio.Games
});
RockersInput riffComp = Instantiate(rockerInputRef, transform);
riffComp.Init(false, new int[6], beat, 3, GetSample(SoshiSamples[0]), SoshiPitches[0]);
ScheduleInput(beat, 3.5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, 3.5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
RockersInput riffComp2 = Instantiate(rockerInputRef, transform);
riffComp2.Init(false, new int[6], beat, 4.5f, GetSample(SoshiSamples[1]), SoshiPitches[1]);
ScheduleInput(beat, 5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, 5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
RockersInput riffComp3 = Instantiate(rockerInputRef, transform);
riffComp3.Init(false, new int[6], beat, 6, GetSample(SoshiSamples[2]), SoshiPitches[2]);
ScheduleInput(beat, 6.5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, 6.5f, InputAction_TriggerDown, JustMute, MuteMiss, Empty);
}
public void DefaultCmon(double beat, int[] JJSamples, int[] JJPitches, int[] SoshiSamples, int[] SoshiPitches, bool moveCamera)
@ -848,7 +848,7 @@ namespace HeavenStudio.Games
RockersInput riffComp3 = Instantiate(rockerInputRef, transform);
riffComp3.Init(false, new int[6], beat, 6, GetSample(SoshiSamples[2]), SoshiPitches[2]);
ScheduleInput(beat, 6.5f, InputAction_BasicPress, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, 6.5f, InputAction_BasicPress, JustMute, MuteMiss, Empty);
RockersInput riffComp4 = Instantiate(rockerInputRef, transform);
riffComp4.Init(false, new int[6], beat, 7, GetSample(SoshiSamples[3]), SoshiPitches[3], true);
@ -903,8 +903,7 @@ namespace HeavenStudio.Games
RockersInput riffComp = Instantiate(rockerInputRef, transform);
riffComp.Init(e["gcS"], new int[6] { e["1S"], e["2S"], e["3S"], e["4S"], e["5S"], e["6S"] }, beat, e.beat - beat,
GetSample(e["sampleS"]), e["pitchSampleS"]);
if (e.length <= 0.5f) ScheduleInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
else ScheduleAutoplayInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
}
else
{
@ -924,8 +923,7 @@ namespace HeavenStudio.Games
RockersInput riffComp = Instantiate(rockerInputRef, transform);
riffComp.Init(e["gcS"], new int[6] { e["1S"], e["2S"], e["3S"], e["4S"], e["5S"], e["6S"] }, beat, e.beat - beat,
GetSample(e["sampleS"]), e["pitchSampleS"], true);
if (e.length <= 0.5f) ScheduleInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
else ScheduleAutoplayInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, e.beat - beat + e.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
break;
}
}
@ -1139,8 +1137,7 @@ namespace HeavenStudio.Games
RockersInput riffComp = Instantiate(rockerInputRef, transform);
riffComp.Init(crEvent["gcS"], new int[6] { crEvent["1S"], crEvent["2S"], crEvent["3S"], crEvent["4S"], crEvent["5S"], crEvent["6S"] }, beat, relativeBeat,
GetSample(crEvent["sampleS"]), crEvent["pitchSampleS"]);
if (crEvent.length > 0.5f) ScheduleAutoplayInput(beat, relativeBeat + crEvent.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
else ScheduleInput(beat, relativeBeat + crEvent.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
ScheduleAutoplayInput(beat, relativeBeat + crEvent.length, InputAction_BasicPress, JustMute, MuteMiss, Empty);
}
else
{

View file

@ -26,7 +26,7 @@ namespace HeavenStudio.Games.Scripts_Rockers
this.sample = sample;
this.sampleTones = sampleTones;
this.jump = jump;
game.ScheduleInput(beat, length, Rockers.InputAction_FlickRelease, Just, Miss, Empty);
game.ScheduleInput(beat, length, Rockers.releaseMargin, Rockers.InputAction_FlickRelease, Just, Miss, Empty);
}
private void Just(PlayerActionEvent caller, float state)

View file

@ -70,7 +70,7 @@ namespace HeavenStudio.Common
MetreAnim.Play("NoPose", -1, 0f);
}
public void MakeAccuracyVfx(double time, bool late = false)
public void MakeAccuracyVfx(double time, float pitch, double margin, bool late = false)
{
if (!OverlaysManager.OverlaysEnabled) return;
GameObject it;
@ -87,12 +87,12 @@ namespace HeavenStudio.Common
// SetArrowPos(time);
// no Clamp() because double
time = System.Math.Max(Minigame.NgEarlyTime(), System.Math.Min(Minigame.NgLateTime(), time));
time = System.Math.Max(Minigame.NgEarlyTime(pitch, margin), System.Math.Min(Minigame.NgLateTime(pitch, margin), time));
if (time >= Minigame.AceEarlyTime() && time <= Minigame.AceLateTime())
if (time >= Minigame.AceEarlyTime(pitch, margin) && time <= Minigame.AceLateTime(pitch, margin))
{
type = Rating.Just;
frac = (float)((time - Minigame.AceEarlyTime()) / (Minigame.AceLateTime() - Minigame.AceEarlyTime()));
frac = (float)((time - Minigame.AceEarlyTime(pitch, margin)) / (Minigame.AceLateTime(pitch, margin) - Minigame.AceEarlyTime(pitch, margin)));
y = barJustTransform.localScale.y * frac - (barJustTransform.localScale.y * 0.5f);
}
else
@ -100,32 +100,32 @@ namespace HeavenStudio.Common
if (time > 1.0)
{
// goes "down"
if (time <= Minigame.JustLateTime())
if (time <= Minigame.JustLateTime(pitch, margin))
{
type = Rating.OK;
frac = (float)((time - Minigame.AceLateTime()) / (Minigame.JustLateTime() - Minigame.AceLateTime()));
frac = (float)((time - Minigame.AceLateTime(pitch, margin)) / (Minigame.JustLateTime(pitch, margin) - Minigame.AceLateTime(pitch, margin)));
y = ((barOKTransform.localScale.y - barJustTransform.localScale.y) * frac) + barJustTransform.localScale.y;
}
else
{
type = Rating.NG;
frac = (float)((time - Minigame.JustLateTime()) / (Minigame.NgLateTime() - Minigame.JustLateTime()));
frac = (float)((time - Minigame.JustLateTime(pitch, margin)) / (Minigame.NgLateTime(pitch, margin) - Minigame.JustLateTime(pitch, margin)));
y = ((barNGTransform.localScale.y - barOKTransform.localScale.y) * frac) + barOKTransform.localScale.y;
}
}
else
{
// goes "up"
if (time >= Minigame.JustEarlyTime())
if (time >= Minigame.JustEarlyTime(pitch, margin))
{
type = Rating.OK;
frac = (float)((time - Minigame.JustEarlyTime()) / (Minigame.AceEarlyTime() - Minigame.JustEarlyTime()));
frac = (float)((time - Minigame.JustEarlyTime(pitch, margin)) / (Minigame.AceEarlyTime(pitch, margin) - Minigame.JustEarlyTime(pitch, margin)));
y = ((barOKTransform.localScale.y - barJustTransform.localScale.y) * -frac) - barJustTransform.localScale.y;
}
else
{
type = Rating.NG;
frac = (float)((time - Minigame.NgEarlyTime()) / (Minigame.JustEarlyTime() - Minigame.NgEarlyTime()));
frac = (float)((time - Minigame.NgEarlyTime(pitch, margin)) / (Minigame.JustEarlyTime(pitch, margin) - Minigame.NgEarlyTime(pitch, margin)));
y = ((barNGTransform.localScale.y - barOKTransform.localScale.y) * -frac) - barOKTransform.localScale.y;
}
}