/// <summary> /// Handles completely missed HitObjects. /// </summary> private void HandleMissedHitObjects() { // Handle missed notes. foreach (var hitObject in ActiveHitObjects) { 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); 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); ScoreProcessor.Stats.Add(stat); } ActiveHitObjectsToRemove.Add(hitObject); } else { break; } } // Remove all objects ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x)); ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x)); }
/// <summary> /// Handles the replay frames for missed long notes. /// </summary> private void HandleMissedLongNoteReleases() { // Handle missed LN releases. foreach (var hitObject in ActiveHeldLongNotes) { var releaseWindow = ScoreProcessor.JudgementWindow[Judgement.Okay] * ScoreProcessor.WindowReleaseMultiplier[Judgement.Okay]; // Check if the LN's release was missed. if (!(Time > hitObject.EndTime + releaseWindow)) { continue; } ScoreProcessor.CalculateScore(Judgement.Okay); // Add new miss stat. var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject, hitObject.EndTime, Judgement.Okay, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health); ScoreProcessor.Stats.Add(stat); // Queue the object to be removed. ActiveHeldLongNotesToRemove.Add(hitObject); } ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x)); }
/// <summary> /// Filters out only those stats that we care about and computes earliest and latest hit time. /// </summary> private void FilterHitStats() { StatsWithHitDifference = new List <HitStat>(); StatsWithoutHitDifference = new List <HitStat>(); EarliestHitTime = int.MaxValue; LatestHitTime = int.MinValue; foreach (var breakdown in Processor.Stats) { EarliestHitTime = Math.Min(EarliestHitTime, breakdown.SongPosition); LatestHitTime = Math.Max(LatestHitTime, breakdown.SongPosition); var hitDifference = breakdown.HitDifference; if (breakdown.KeyPressType == KeyPressType.Release && breakdown.Judgement != Judgement.Miss) { // Scale LN release hit errors to match regular hits. hitDifference = (int)(breakdown.HitDifference / Processor.WindowReleaseMultiplier[breakdown.Judgement]); } var hitStat = new HitStat(breakdown.Type, breakdown.KeyPressType, breakdown.HitObject, breakdown.SongPosition, breakdown.Judgement, hitDifference, breakdown.Accuracy, breakdown.Health); // No need to check for Type == Miss as all of them have hitDifference == int.MinValue. if (hitDifference != int.MinValue && Math.Abs(hitDifference) <= LargestHitWindow) { StatsWithHitDifference.Add(hitStat); } else { StatsWithoutHitDifference.Add(hitStat); } } }
/// <summary> /// Updates the active objects in the pool + adds to score when applicable. /// </summary> private void UpdateAndScoreActiveObjects() { // Add more hit objects to the pool if necessary foreach (var lane in HitObjectQueueLanes) { while (lane.Count > 0 && CurrentTrackPosition - GetPositionFromTime(lane.Peek().StartTime) > CreateObjectPosition) { CreatePoolObject(lane.Dequeue()); } } // Check to see if the player missed any active notes foreach (var lane in ActiveNoteLanes) { while (lane.Count > 0 && CurrentAudioPosition > lane.Peek().Info.StartTime + Ruleset.ScoreProcessor.JudgementWindow[Judgement.Okay]) { // Current hit object var hitObject = lane.Dequeue(); // Update scoreboard for simulated plays var screenView = (GameplayScreenView)Ruleset.Screen.View; screenView.UpdateScoreboardUsers(); // Add new hit stat data and update score var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject.Info, (int)Ruleset.Screen.Timing.Time, Judgement.Miss, int.MinValue, Ruleset.ScoreProcessor.Accuracy, Ruleset.ScoreProcessor.Health); Ruleset.ScoreProcessor.Stats.Add(stat); Ruleset.ScoreProcessor.CalculateScore(Judgement.Miss); // Perform Playfield animations var playfield = (GameplayPlayfieldKeys)Ruleset.Playfield; playfield.Stage.ComboDisplay.MakeVisible(); playfield.Stage.JudgementHitBurst.PerformJudgementAnimation(Judgement.Miss); // If HitObject is an LN, kill it and count it as another miss because of the tail. // - missing an LN counts as two misses if (hitObject.Info.IsLongNote) { KillPoolObject(hitObject); Ruleset.ScoreProcessor.CalculateScore(Judgement.Miss); Ruleset.ScoreProcessor.Stats.Add(stat); screenView.UpdateScoreboardUsers(); } // Otherwise just kill the object. else { KillPoolObject(hitObject); } } } // Update active objects. foreach (var lane in ActiveNoteLanes) { foreach (var hitObject in lane) { hitObject.UpdateSpritePositions(CurrentTrackPosition); } } }
/// <summary> /// Updates the held long note objects in the pool + adds to score when applicable. /// </summary> private void UpdateAndScoreHeldObjects() { // The release window. (Window * Multiplier) var window = Ruleset.ScoreProcessor.JudgementWindow[Judgement.Okay] * Ruleset.ScoreProcessor.WindowReleaseMultiplier[Judgement.Okay]; // Check to see if any LN releases were missed (Counts as an okay instead of a miss.) foreach (var lane in HeldLongNoteLanes) { while (lane.Count > 0 && CurrentAudioPosition > lane.Peek().Info.EndTime + window) { // Current hit object var hitObject = lane.Dequeue(); // The judgement that is given when a user completely fails to release. const Judgement missedJudgement = Judgement.Okay; // Add new hit stat data and update score var stat = new HitStat(HitStatType.Miss, KeyPressType.None, hitObject.Info, hitObject.Info.EndTime, Judgement.Okay, int.MinValue, Ruleset.ScoreProcessor.Accuracy, Ruleset.ScoreProcessor.Health); Ruleset.ScoreProcessor.Stats.Add(stat); var im = Ruleset.InputManager as KeysInputManager; if (im?.ReplayInputManager == null) { Ruleset.ScoreProcessor.CalculateScore(missedJudgement); } // Update scoreboard for simulated plays var screenView = (GameplayScreenView)Ruleset.Screen.View; screenView.UpdateScoreboardUsers(); screenView.UpdateScoreAndAccuracyDisplays(); // Perform Playfield animations var stage = ((GameplayPlayfieldKeys)Ruleset.Playfield).Stage; if (im?.ReplayInputManager == null) { stage.ComboDisplay.MakeVisible(); stage.JudgementHitBurst.PerformJudgementAnimation(missedJudgement); } stage.HitLightingObjects[hitObject.Info.Lane - 1].StopHolding(); // Update Pooling KillHoldPoolObject(hitObject); } } // Update the currently held long notes. foreach (var lane in HeldLongNoteLanes) { foreach (var hitObject in lane) { hitObject.UpdateSpritePositions(CurrentTrackPosition); } } }
/// <summary> /// Handles all key presses in the current replay frame. /// </summary> 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>(); // Update the key press state in the store. for (var i = 0; i < InputKeyStore.Count; i++) { InputKeyStore[i].Pressed = currentFramePressed.Contains(i); } // Check the difference in key press states for the current and previous frames. var keyDifferences = currentFramePressed.Except(previousFramePressed) .Concat(previousFramePressed.Except(currentFramePressed)) .ToList(); // Go through each frame and handle key presses/releases. foreach (var key in keyDifferences) { // This key was uniquely pressed during this frame. if (currentFramePressed.Contains(key)) { // Find the nearest object in the lane that the user has pressed. var nearestObjectIndex = GetIndexOfNearestLaneObject(key + 1, Time); if (nearestObjectIndex == -1) { continue; } // Grab the actual HitObject instance. var hitObject = ActiveHitObjects[nearestObjectIndex]; // Calculate the hit difference. var hitDifference = hitObject.StartTime - Time; // Calculate Score. var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Press); switch (judgement) { // Don't handle ghost key presses, so just continue further. case Judgement.Ghost: continue; // Object needs to be removed completely if it's a miss. case Judgement.Miss: // Add another miss for an LN (head and tail) if (hitObject.IsLongNote) { ScoreProcessor.CalculateScore(Judgement.Miss); ScoreProcessor.Stats.Add(new HitStat(HitStatType.Miss, KeyPressType.Press, hitObject, Time, Judgement.Miss, int.MinValue, ScoreProcessor.Accuracy, ScoreProcessor.Health)); } break; default: // Long notes need to be changed to a held status. if (hitObject.IsLongNote) { ActiveHeldLongNotes.Add(hitObject); } break; } // Add a new hit stat to the score processor. var stat = new HitStat(HitStatType.Hit, KeyPressType.Press, hitObject, Time, judgement, hitDifference, ScoreProcessor.Accuracy, ScoreProcessor.Health); ScoreProcessor.Stats.Add(stat); // Object needs to be removed from ActiveObjects. ActiveHitObjectsToRemove.Add(hitObject); } // This key was uniquely released during this frame. else if (previousFramePressed.Contains(key)) { // Find the index of the actual closest LN and handle the key release // if so. foreach (var hitObject in ActiveHeldLongNotes) { // Handle the release of the note. if (hitObject.Lane != key + 1) { continue; } // Calculate the hit difference. var hitDifference = hitObject.EndTime - Time; // Calculate Score var judgement = ScoreProcessor.CalculateScore(hitDifference, KeyPressType.Release); // LN was released during a hit window. if (judgement != Judgement.Ghost) { // Add a new hit stat to the score processor. var stat = new HitStat(HitStatType.Hit, KeyPressType.Release, hitObject, Time, judgement, hitDifference, ScoreProcessor.Accuracy, ScoreProcessor.Health); ScoreProcessor.Stats.Add(stat); } // The LN was released too early (miss) else { ScoreProcessor.CalculateScore(Judgement.Miss); // Add a new stat to ScoreProcessor. var stat = new HitStat(HitStatType.Hit, KeyPressType.Release, hitObject, Time, Judgement.Miss, hitDifference, ScoreProcessor.Accuracy, ScoreProcessor.Health); ScoreProcessor.Stats.Add(stat); } // Remove the object from its held state. ActiveHeldLongNotesToRemove.Add(hitObject); } } } // Remove all active objects after handling key presses/releases. ActiveHitObjectsToRemove.ForEach(x => ActiveHitObjects.Remove(x)); ActiveHeldLongNotesToRemove.ForEach(x => ActiveHeldLongNotes.Remove(x)); }