Skip to content

Add mines #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
11 changes: 11 additions & 0 deletions Quaver.API/Enums/HitObjectType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Quaver.API.Enums
{
/// <summary>
/// Indicates the type of a hit object
/// </summary>
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.
}
}
2 changes: 1 addition & 1 deletion Quaver.API/Maps/Processors/Scoring/ScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public ScoreProcessor(Replay replay, JudgementWindows windows = null)
/// <summary>
/// Adds a judgement to the score and recalculates the score.
/// </summary>
public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false);
public abstract void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false);

/// <summary>
/// Calculates the accuracy of the current play session.
Expand Down
32 changes: 17 additions & 15 deletions Quaver.API/Maps/Processors/Scoring/ScoreProcessorKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -173,7 +174,9 @@ public ScoreProcessorKeys(Replay replay, JudgementWindows windows = null) : base
/// <param name="hitDifference"></param>
/// <param name="keyPressType"></param>
/// <param name="calculateAllStats"></param>
public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true)
/// <param name="isMine"></param>
public Judgement CalculateScore(int hitDifference, KeyPressType keyPressType, bool calculateAllStats = true,
bool isMine = false)
{
var absoluteDifference = 0;

Expand Down Expand Up @@ -214,18 +217,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);
}

/// <inheritdoc />
/// <summary>
/// Calculate Score and Health increase/decrease with a given judgement.
/// </summary>
/// <param name="judgement"></param>
/// <param name="isLongNoteRelease"></param>
public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false)
/// <param name="isMine"></param>
public override void CalculateScore(Judgement judgement, bool isLongNoteRelease = false, bool isMine = false)
{
// Update Judgement count
CurrentJudgements[judgement]++;
Expand All @@ -249,7 +259,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)
Expand Down Expand Up @@ -364,17 +376,7 @@ protected override void InitializeHealthWeighting()
/// <returns></returns>
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);
}

/// <summary>
Expand Down
12 changes: 10 additions & 2 deletions Quaver.API/Maps/Qua.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -1110,8 +1111,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;
}
Expand Down
12 changes: 12 additions & 0 deletions Quaver.API/Maps/Structures/HitObjectInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public HitSounds HitSound
set;
}

/// <summary>
/// The hit object could be a normal note or a mine
/// </summary>
public HitObjectType Type { get; [MoonSharpVisible(false)] set; }

/// <summary>
/// Key sounds to play when this object is hit.
/// </summary>
Expand Down Expand Up @@ -95,6 +100,11 @@ public string TimingGroup
[YamlIgnore]
public bool IsLongNote => EndTime > 0;

/// <summary>
/// The number of judgements generated by this object
/// </summary>
[YamlIgnore] public int JudgementCount => IsLongNote && Type != HitObjectType.Mine ? 2 : 1;

/// <summary>
/// Returns if the object is allowed to be edited in lua scripts
/// </summary>
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Quaver.API/Replays/Replay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down
108 changes: 95 additions & 13 deletions Quaver.API/Replays/Virtual/VirtualReplayPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public class VirtualReplayPlayer
/// The score processor for the virtual replay.
/// </summary>
public ScoreProcessorKeys ScoreProcessor { get; }


/// <summary>
/// All of the mines that are currently active and available.
/// </summary>
public List<HitObjectInfo> ActiveMines { get; }


/// <summary>
/// The list of active mines that are scheduled for removal.
/// </summary>
public List<HitObjectInfo> ActiveMinesToRemove { get; set; }

/// <summary>
/// All of the HitObjects that are currently active and available.
Expand Down Expand Up @@ -95,8 +107,22 @@ public VirtualReplayPlayer(Replay replay, Qua map, JudgementWindows windows = nu

ActiveHitObjects = new List<HitObjectInfo>();
ActiveHeldLongNotes = new List<HitObjectInfo>();
ActiveMines = new List<HitObjectInfo>();

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.
switch (Map.Mode)
Expand Down Expand Up @@ -148,10 +174,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;
Expand All @@ -171,6 +199,7 @@ public void PlayNextFrame()
// Store the objects that need to be removed from the list of active objects.
ActiveHitObjectsToRemove = new List<HitObjectInfo>();
ActiveHeldLongNotesToRemove = new List<HitObjectInfo>();
ActiveMinesToRemove = new List<HitObjectInfo>();

if (CurrentFrame < Replay.Frames.Count)
{
Expand Down Expand Up @@ -207,6 +236,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<int>();

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++)
Expand All @@ -217,6 +248,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)
{
Expand All @@ -242,7 +300,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)
{
Expand All @@ -254,7 +312,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));
Expand Down Expand Up @@ -291,7 +349,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)
Expand All @@ -305,7 +363,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,
Expand All @@ -323,6 +381,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));
}

/// <summary>
Expand All @@ -342,7 +401,7 @@ private void HandleMissedLongNoteReleases()
// Judgement when a user doesn't release an LN.
var missedReleaseJudgement = Judgement.Good;

ScoreProcessor.CalculateScore(missedReleaseJudgement, true);
ScoreProcessor.CalculateScore(missedReleaseJudgement, true, false);

// Add new miss stat.
var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.EndTime, missedReleaseJudgement, int.MinValue,
Expand All @@ -367,19 +426,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);
}

Expand All @@ -390,10 +450,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));
}

/// <summary>
Expand Down
Loading
Loading