// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { /// /// Ensures that s are hit in-order. /// If a is hit out of order: /// /// The hit is blocked if it occurred earlier than the previous 's start time. /// The hit causes all previous s to missed otherwise. /// /// public class OrderedHitPolicy { private readonly HitObjectContainer hitObjectContainer; public OrderedHitPolicy(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; } /// /// Determines whether a can be hit at a point in time. /// /// The to check. /// The time to check. /// Whether can be hit at the given . public bool IsHittable(DrawableHitObject hitObject, double time) { DrawableHitObject blockingObject = null; // Find the last hitobject which blocks future hits. foreach (var obj in hitObjectContainer.AliveObjects) { if (obj == hitObject) break; if (drawableCanBlockFutureHits(obj)) blockingObject = obj; } // If there is no previous hitobject, allow the hit. if (blockingObject == null) return true; // A hit is allowed if: // 1. The last blocking hitobject has been judged. // 2. The current time is after the last hitobject's start time. // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) return true; return false; } /// /// Handles a being hit to potentially miss all earlier s. /// /// The that was hit. public void HandleHit(HitObject hitObject) { // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; double maximumTime = hitObject.StartTime; // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. foreach (var obj in hitObjectContainer.AliveObjects) { if (obj.Judged || obj.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(obj.HitObject)) applyMiss(obj); foreach (var nested in obj.NestedHitObjects) { if (nested.Judged || nested.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(nested.HitObject)) applyMiss(nested); } } static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); } /// /// Whether a blocks hits on future s until its start time is reached. /// /// /// This will ONLY match on top-most s. /// /// The to test. private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) { // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// /// /// This is more rigorous and may not match on top-most s as does. /// /// The to test. private static bool hitObjectCanBlockFutureHits(HitObject hitObject) { // Unlike the above we will receive slider tails, but they do not block future hits. if (hitObject is SliderTailCircle) return false; // All other hitcircles continue to block future hits. return hitObject is HitCircle; } } }