diff --git a/Quaver.API/Enums/HitObjectType.cs b/Quaver.API/Enums/HitObjectType.cs
new file mode 100644
index 000000000..0e9a48027
--- /dev/null
+++ b/Quaver.API/Enums/HitObjectType.cs
@@ -0,0 +1,11 @@
+namespace Quaver.API.Enums
+{
+ ///
+ /// Indicates the type of a hit object
+ ///
+ public enum HitObjectType
+ {
+ Normal, // Regular hit object. It should be hit normally.
+ Mine // A mine object. It should not be hit, and hitting it will result in a miss.
+ }
+}
\ No newline at end of file
diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs
index 70b866c6e..a07dc1db2 100644
--- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs
+++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs
@@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null)
///
/// Adds a judgement to the score and recalculates the score.
///
- public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false);
+ public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false);
///
/// Calculates the accuracy of the current play session.
diff --git a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs
index f679c4ff9..47fd8ded6 100644
--- a/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs
+++ b/Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs
@@ -14,6 +14,7 @@
using Quaver.API.Maps.Processors.Scoring.Data;
using Quaver.API.Maps.Processors.Scoring.Multiplayer;
using Quaver.API.Replays;
+using HitObjectType = Quaver.API.Enums.HitObjectType;
namespace Quaver.API.Maps.Processors.Scoring
{
@@ -173,7 +174,9 @@ public ScoreProcessorKeys(Replay replay, JudgementWindows windows = null) : base
///
///
///
- public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true)
+ ///
+ public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true,
+ bool isMine = false)
{
var absoluteDifference = 0;
@@ -222,18 +225,25 @@ public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bo
return judgement;
if (calculateAllStats)
- CalculateScore(judgement, keyPressType == KeyPressType.Release);
+ CalculateScore(judgement, keyPressType == KeyPressType.Release, isMine);
return judgement;
}
+ public void CalculateScore(HitStat hitStat)
+ {
+ CalculateScore(hitStat.Judgement, hitStat.KeyPressType == KeyPressType.Release,
+ hitStat.HitObject.Type is HitObjectType.Mine);
+ }
+
///
///
/// Calculate Score and Health increase/decrease with a given judgement.
///
///
///
- public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false)
+ ///
+ public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false)
{
// Update Judgement count
CurrentJudgements[judgement]++;
@@ -257,7 +267,9 @@ public override void CalculateScore(Judgement judgement, bool isLongNoteRelease
MultiplierCount++;
// Add to the combo since the user hit.
- Combo++;
+ // Only do this when the note is not a mine (so it is a regular note)
+ if (!isMine)
+ Combo++;
// Set the max combo if applicable.
if (Combo > MaxCombo)
@@ -372,17 +384,7 @@ protected override void InitializeHealthWeighting()
///
public int GetTotalJudgementCount()
{
- var judgements = 0;
-
- foreach (var o in Map.HitObjects)
- {
- if (o.IsLongNote)
- judgements += 2;
- else
- judgements++;
- }
-
- return judgements;
+ return Map.HitObjects.Sum(o => o.JudgementCount);
}
///
diff --git a/Quaver.API/Maps/Qua.cs b/Quaver.API/Maps/Qua.cs
index e51b41e68..09925b9fa 100644
--- a/Quaver.API/Maps/Qua.cs
+++ b/Quaver.API/Maps/Qua.cs
@@ -359,7 +359,8 @@ static HitObjectInfo SerializableHitObject(HitObjectInfo obj) =>
.Select(x => new KeySoundInfo { Sample = x.Sample, Volume = x.Volume == 100 ? 0 : x.Volume })
.ToList(),
Lane = obj.Lane, StartTime = obj.StartTime,
- TimingGroup = obj.TimingGroup == DefaultScrollGroupId ? null : obj.TimingGroup
+ TimingGroup = obj.TimingGroup == DefaultScrollGroupId ? null : obj.TimingGroup,
+ Type = obj.Type
};
static SoundEffectInfo SerializableSoundEffect(SoundEffectInfo x) =>
@@ -1101,8 +1102,15 @@ public HitObjectInfo GetHitObjectAtJudgementIndex(int index)
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (var h in HitObjects)
- if (total++ == index || (h.IsLongNote && total++ == index))
+ {
+ var judgementCount = h.JudgementCount;
+ if (total <= index && index < total + judgementCount)
+ {
return h;
+ }
+
+ total += judgementCount;
+ }
return null;
}
diff --git a/Quaver.API/Maps/Structures/HitObjectInfo.cs b/Quaver.API/Maps/Structures/HitObjectInfo.cs
index 6aa26f73c..197e32ddf 100644
--- a/Quaver.API/Maps/Structures/HitObjectInfo.cs
+++ b/Quaver.API/Maps/Structures/HitObjectInfo.cs
@@ -64,6 +64,11 @@ public HitSounds HitSound
set;
}
+ ///
+ /// The hit object could be a normal note or a mine
+ ///
+ public HitObjectType Type { get; [MoonSharpVisible(false)] set; }
+
///
/// Key sounds to play when this object is hit.
///
@@ -95,6 +100,11 @@ public string TimingGroup
[YamlIgnore]
public bool IsLongNote => EndTime > 0;
+ ///
+ /// The number of judgements generated by this object
+ ///
+ [YamlIgnore] public int JudgementCount => IsLongNote && Type != HitObjectType.Mine ? 2 : 1;
+
///
/// Returns if the object is allowed to be edited in lua scripts
///
@@ -175,6 +185,7 @@ public bool Equals(HitObjectInfo x, HitObjectInfo y)
x.Lane == y.Lane &&
x.EndTime == y.EndTime &&
x.HitSound == y.HitSound &&
+ x.Type == y.Type &&
x.KeySounds.SequenceEqual(y.KeySounds, KeySoundInfo.ByValueComparer) &&
x.EditorLayer == y.EditorLayer;
}
@@ -186,6 +197,7 @@ public int GetHashCode(HitObjectInfo obj)
var hashCode = obj.StartTime;
hashCode = (hashCode * 397) ^ obj.Lane;
hashCode = (hashCode * 397) ^ obj.EndTime;
+ hashCode = (hashCode * 397) ^ (int)obj.Type;
hashCode = (hashCode * 397) ^ (int)obj.HitSound;
foreach (var keySound in obj.KeySounds)
diff --git a/Quaver.API/Replays/Replay.cs b/Quaver.API/Replays/Replay.cs
index 3458e0d39..75d183d17 100644
--- a/Quaver.API/Replays/Replay.cs
+++ b/Quaver.API/Replays/Replay.cs
@@ -332,6 +332,9 @@ public static Replay GeneratePerfectReplayKeys(Replay replay, Qua map)
foreach (var hitObject in map.HitObjects)
{
+ if (hitObject.Type is HitObjectType.Mine)
+ continue;
+
// Add key press frame
nonCombined.Add(new ReplayAutoplayFrame(hitObject, ReplayAutoplayFrameType.Press, hitObject.StartTime, KeyLaneToPressState(hitObject.Lane)));
diff --git a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs
index 1e07de56a..da057f52e 100644
--- a/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs
+++ b/Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs
@@ -33,6 +33,18 @@ public class VirtualReplayPlayer
/// The score processor for the virtual replay.
///
public ScoreProcessorKeys ScoreProcessor { get; }
+
+
+ ///
+ /// All of the mines that are currently active and available.
+ ///
+ public List ActiveMines { get; }
+
+
+ ///
+ /// The list of active mines that are scheduled for removal.
+ ///
+ public List ActiveMinesToRemove { get; set; }
///
/// All of the HitObjects that are currently active and available.
@@ -96,8 +108,22 @@ public VirtualReplayPlayer(Replay replay, Qua map, JudgementWindows windows = nu
ActiveHitObjects = new List();
ActiveHeldLongNotes = new List();
+ ActiveMines = new List();
- map.HitObjects.ForEach(x => ActiveHitObjects.Add(x));
+ map.HitObjects.ForEach(x =>
+ {
+ switch (x.Type)
+ {
+ case HitObjectType.Normal:
+ ActiveHitObjects.Add(x);
+ break;
+ case HitObjectType.Mine:
+ ActiveMines.Add(x);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ });
// Add virtual key bindings based on the game mode of the replay.
InputKeyStore = new List();
@@ -127,10 +153,12 @@ public void PlayNextFrame()
{
var obj = Map.GetHitObjectAtJudgementIndex(i);
- ScoreProcessor.CalculateScore(Judgement.Miss);
+ var hitStat = new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime,
+ Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health);
+
+ ScoreProcessor.CalculateScore(hitStat);
- ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.None, obj, obj.StartTime,
- Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health));
+ ScoreProcessor.Stats.Add(hitStat);
if (!ScoreProcessor.Failed)
continue;
@@ -150,6 +178,7 @@ public void PlayNextFrame()
// Store the objects that need to be removed from the list of active objects.
ActiveHitObjectsToRemove = new List();
ActiveHeldLongNotesToRemove = new List();
+ ActiveMinesToRemove = new List();
if (CurrentFrame < Replay.Frames.Count)
{
@@ -186,6 +215,8 @@ private void HandleKeyPressesInFrame()
// Retrieve a list of the key press states in integer form.
var currentFramePressed = Replay.KeyPressStateToLanes(Replay.Frames[CurrentFrame].Keys);
var previousFramePressed = CurrentFrame > 0 ? Replay.KeyPressStateToLanes(Replay.Frames[CurrentFrame - 1].Keys) : new List();
+
+ var previousFrameTime = CurrentFrame > 0 ? Replay.Frames[CurrentFrame - 1].Time : Time;
// Update the key press state in the store.
for (var i = 0; i < InputKeyStore.Count; i++)
@@ -196,6 +227,33 @@ private void HandleKeyPressesInFrame()
.Concat(previousFramePressed.Except(currentFramePressed))
.ToList();
+ foreach (var lane in previousFramePressed)
+ {
+ foreach (var mine in ActiveMines)
+ {
+ var endTime = mine.IsLongNote ? mine.EndTime : mine.StartTime;
+ if (mine.Lane == lane + 1
+ && endTime + ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime
+ && Time >= mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv])
+ {
+ // Calculate the hit difference.
+ var hitDifference =
+ mine.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv] > previousFrameTime
+ ? (int)ScoreProcessor.JudgementWindow[Judgement.Marv]
+ : mine.StartTime - previousFrameTime;
+
+ // Add a new hit stat to the score processor.
+ var stat = new HitStat(HitStatType.Miss, KeyPressType.Press, mine, Time, Judgement.Miss, hitDifference,
+ ScoreProcessor.Accuracy, ScoreProcessor.Health);
+
+ ScoreProcessor.Stats.Add(stat);
+
+ // Object needs to be removed from ActiveObjects.
+ ActiveMinesToRemove.Add(mine);
+ }
+ }
+ }
+
// Go through each frame and handle key presses/releases.
foreach (var key in keyDifferences)
{
@@ -221,7 +279,7 @@ private void HandleKeyPressesInFrame()
var hitDifference = hitObject.StartTime - Time;
// Calculate Score.
- var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press);
+ var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press, isMine: false);
switch (judgement)
{
@@ -233,7 +291,7 @@ private void HandleKeyPressesInFrame()
// Add another miss for an LN (head and tail)
if (hitObject.IsLongNote)
{
- ScoreProcessor.CalculateScore(Judgement.Miss, true);
+ ScoreProcessor.CalculateScore(Judgement.Miss, true, false);
ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.Press, hitObject, Time, Judgement.Miss, int.MinValue,
ScoreProcessor.Accuracy, ScoreProcessor.Health));
@@ -270,7 +328,7 @@ private void HandleKeyPressesInFrame()
var hitDifference = hitObject.EndTime - Time;
// Calculate Score
- var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release);
+ var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release, isMine: false);
// LN was released during a hit window.
if (judgement != Judgement.Ghost && judgement != Judgement.Miss)
@@ -284,7 +342,7 @@ private void HandleKeyPressesInFrame()
// The LN was released too early (miss)
else
{
- ScoreProcessor.CalculateScore(Judgement.Miss, true);
+ ScoreProcessor.CalculateScore(Judgement.Miss, true, false);
// Add a new stat to ScoreProcessor.
var stat = new HitStat(HitStatType.Hit, KeyPressType.Release, hitObject, Time, Judgement.Miss, hitDifference,
@@ -302,6 +360,7 @@ private void HandleKeyPressesInFrame()
// Remove all active objects after handling key presses/releases.
ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x));
ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x));
+ ActiveMinesToRemove.ForEach(x => ActiveMines.Remove(x));
}
///
@@ -346,19 +405,20 @@ private void HandleMissedHitObjects()
{
if (Time > hitObject.StartTime + ScoreProcessor.JudgementWindow[Judgement.Okay])
{
- // Add a miss to the score.
- ScoreProcessor.CalculateScore(Judgement.Miss);
-
// Create a new HitStat to add to the ScoreProcessor.
var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Miss, int.MinValue,
ScoreProcessor.Accuracy, ScoreProcessor.Health);
+ // Add a miss to the score.
+ ScoreProcessor.CalculateScore(stat);
+
+
ScoreProcessor.Stats.Add(stat);
// Long notes count as two misses, so add another one if the object is one.
if (hitObject.IsLongNote)
{
- ScoreProcessor.CalculateScore(Judgement.Miss, true);
+ ScoreProcessor.CalculateScore(Judgement.Miss, true, false);
ScoreProcessor.Stats.Add(stat);
}
@@ -369,10 +429,32 @@ private void HandleMissedHitObjects()
break;
}
}
+ // Handle missed mines.
+ foreach (var hitObject in ActiveMines)
+ {
+ var endTime = hitObject.IsLongNote ? hitObject.EndTime : hitObject.StartTime;
+ if (Time > endTime + ScoreProcessor.JudgementWindow[Judgement.Marv])
+ {
+ // Create a new HitStat to add to the ScoreProcessor.
+ var stat = new HitStat(HitStatType.Hit, KeyPressType.None, hitObject, hitObject.StartTime, Judgement.Marv, 0,
+ ScoreProcessor.Accuracy, ScoreProcessor.Health);
+
+ // Add a miss to the score.
+ ScoreProcessor.CalculateScore(stat);
+
+ ScoreProcessor.Stats.Add(stat);
+ ActiveMinesToRemove.Add(hitObject);
+ }
+ else if (Time < hitObject.StartTime - ScoreProcessor.JudgementWindow[Judgement.Marv])
+ {
+ break;
+ }
+ }
// Remove all objects
ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x));
ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x));
+ ActiveMinesToRemove.ForEach(x => ActiveMines.Remove(x));
}
///
diff --git a/Quaver.Tools/Commands/RecalculateCommand.cs b/Quaver.Tools/Commands/RecalculateCommand.cs
index f27d110d9..9379fef22 100644
--- a/Quaver.Tools/Commands/RecalculateCommand.cs
+++ b/Quaver.Tools/Commands/RecalculateCommand.cs
@@ -63,22 +63,22 @@ private void Recalculate(JToken scores)
var processor = new ScoreProcessorKeys(qua, 0);
for (var i = 0; i < (int)score["count_marv"]; i++)
- processor.CalculateScore(Judgement.Marv);
+ processor.CalculateScore(Judgement.Marv, false, false);
for (var i = 0; i < (int)score["count_perf"]; i++)
- processor.CalculateScore(Judgement.Perf);
+ processor.CalculateScore(Judgement.Perf, false, false);
for (var i = 0; i < (int)score["count_great"]; i++)
- processor.CalculateScore(Judgement.Great);
+ processor.CalculateScore(Judgement.Great, false, false);
for (var i = 0; i < (int)score["count_good"]; i++)
- processor.CalculateScore(Judgement.Good);
+ processor.CalculateScore(Judgement.Good, false, false);
for (var i = 0; i < (int)score["count_okay"]; i++)
- processor.CalculateScore(Judgement.Okay);
+ processor.CalculateScore(Judgement.Okay, false, false);
for (var i = 0; i < (int)score["count_miss"]; i++)
- processor.CalculateScore(Judgement.Miss);
+ processor.CalculateScore(Judgement.Miss, false, false);
var difficultyRating = (double)score["performance_rating"] / Math.Pow((double)score["accuracy"] / 98, 6);