diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index f63d6c267394..dad08cbc6bc0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -1,6 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -12,16 +17,143 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject { + private DrawableTaikoRuleset drawableTaikoRuleset = null!; + + private TaikoPlayfieldAdjustmentContainer adjustmentContainer = null!; + + private IReadOnlyList mods = Array.Empty(); + + private const float hd_base_fade_out_duration = 0.375f; + + private const float hd_base_initial_alpha = 0.75f; + + private const float hdhr_base_fade_out_duration = 0.2f; + + private const float hdhr_base_initial_alpha = 0.2f; + + private const float hidden_base_aspect = 4f / 3f; + + /// + /// Time range at which notes start to become visible in milliseconds. + /// + private const double nm_fade_in_time_range = 10000d / 1.4d; + + /// + /// Duration notes take to fade in in milliseconds. + /// + private const float nm_fade_in_duration = 400f; + + /// + /// Whether note fading in is enabled. + /// + private bool fadeInEnabled = true; + + private readonly BindableFloat hiddenFadeOutDuration = new BindableFloat(hd_base_fade_out_duration); + + private readonly BindableFloat hiddenInitialAlpha = new BindableFloat(hd_base_initial_alpha); + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; - drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false; + drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; + adjustmentContainer = (TaikoPlayfieldAdjustmentContainer)drawableRuleset.PlayfieldAdjustmentContainer; + + // drawableRuleset.Mods should always be non-null here, but just in case. + mods = drawableRuleset.Mods ?? mods; + + // Disable uppper bound for playfield aspect ratio + // In stable, nomod time range is limited by fading notes in instead + adjustmentContainer.MaximumAspect = float.PositiveInfinity; + adjustmentContainer.MinimumAspect = 5f / 4f; + + TaikoModHidden? hidden = mods.OfType().FirstOrDefault(); + + if (mods.OfType().Any()) + { + // Hardrock disables note fading in + fadeInEnabled = false; + + // For hardrock, the playfield time range is clamped to within classicMaxTimeRange and the equivalent + // time range for a 16:10 aspect ratio. + adjustmentContainer.TrimOnOverflow = false; + + // Apply stable aspect ratio limits for hardrock (visually taken) + adjustmentContainer.MaximumAspect = 1.963f; + + // This is accurate to 4:3, but slightly off for 5:4 + adjustmentContainer.MinimumAspect = 1.666f; + + adjustmentContainer.MinimumRelativeHeight = 0.26f; + adjustmentContainer.MaximumRelativeHeight = 0.26f; + + if (hidden != null) + { + hiddenInitialAlpha.BindTo(hidden.InitialAlpha); + hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration); + drawableRuleset.OnUpdate += d => adjustHidden( + hdhr_base_fade_out_duration, hdhr_base_initial_alpha, 16f / 9f, 0.8f); + } + } + else if (hidden != null) + { + // Hidden disables note fading in + fadeInEnabled = false; + + // Stable limits the aspect ratio to 4:3 + adjustmentContainer.MaximumAspect = hidden_base_aspect; + + // Enable playfield trimming for hidden + adjustmentContainer.TrimOnOverflow = true; + + // Enable aspect ratio adjustment for hidden (see adjustHidden) + hiddenInitialAlpha.BindTo(hidden.InitialAlpha); + hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration); + drawableRuleset.OnUpdate += d => adjustHidden( + hd_base_fade_out_duration, hd_base_initial_alpha, hidden_base_aspect); + } } public void ApplyToDrawableHitObject(DrawableHitObject drawable) { if (drawable is DrawableTaikoHitObject hit) + { hit.SnapJudgementLocation = true; + + if (fadeInEnabled) + { + drawable.ApplyCustomUpdateState += fadeIn; + } + } + } + + // Fade in notes with fixed duration. + private void fadeIn(DrawableHitObject o, ArmedState state) + { + double preempt = nm_fade_in_time_range / drawableTaikoRuleset.ControlPointAt(o.HitObject.StartTime).Multiplier; + double start = o.HitObject.StartTime - preempt; + o.Alpha = 0; + + using (o.BeginAbsoluteSequence(start)) + { + o.FadeInFromZero(nm_fade_in_duration); + } + } + + // Adjust hidden initial alpha and fade out duration for different aspect ratios + private void adjustHidden( + float baseFadeOutDuration, + float baseInitialAlpha, + float baseAspect, + float adjustmentRatio = 1f) + { + float clampedAspect = adjustmentContainer.ClampedCurrentAspect; + + float fadeOutDurationAdjustment = clampedAspect / baseAspect - 1; + fadeOutDurationAdjustment *= adjustmentRatio; + hiddenFadeOutDuration.Value = baseFadeOutDuration + fadeOutDurationAdjustment; + + float initialAlphaAdjustment = clampedAspect / baseAspect - 1; + initialAlphaAdjustment *= adjustmentRatio; + hiddenInitialAlpha.Value = baseInitialAlpha + initialAlphaAdjustment; } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 2c3b4a8d184d..914e279e2485 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; @@ -29,7 +30,20 @@ public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset - private const float fade_out_duration = 0.375f; + public readonly BindableFloat FadeOutDuration = new BindableFloat(0.375f) + { + MinValue = 0f, + MaxValue = 1f + }; + + /// + /// The initial alpha of hitobjects when they appear. + /// + public readonly BindableFloat InitialAlpha = new BindableFloat(1f) + { + MinValue = 0f, + MaxValue = 1f + }; private DrawableTaikoRuleset drawableRuleset = null!; @@ -51,7 +65,8 @@ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, case DrawableHit: double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier; double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time; - double duration = preempt * fade_out_duration; + double duration = preempt * FadeOutDuration.Value; + hitObject.Alpha = InitialAlpha.Value; using (hitObject.BeginAbsoluteSequence(start)) { diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index b8e76be89e65..0eaf95b1a700 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -35,8 +35,6 @@ public partial class DrawableTaikoRuleset : DrawableScrollingRuleset base.TimeRange; - public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true); - public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer; @@ -102,10 +100,7 @@ public MultiplierControlPoint ControlPointAt(double time) return ControlPoints[result]; } - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer - { - LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange } - }; + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 0510f0806898..376d42fc6e07 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -121,6 +121,7 @@ private void load(OsuColour colours) new Container { Name = "Bar line content", + Masking = true, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset }, Children = new Drawable[] diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index c67f61052c78..7475c64a7eae 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.UI; @@ -12,12 +11,38 @@ namespace osu.Game.Rulesets.Taiko.UI { public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { - public const float MAXIMUM_ASPECT = 16f / 9f; - public const float MINIMUM_ASPECT = 5f / 4f; - private const float stable_gamefield_height = 480f; - public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); + /// + /// The maximum aspect ratio the playfield can be adjusted to. + /// + protected internal float MaximumAspect = 16f / 9f; + + /// + /// The minimum aspect ratio the playfield can be adjusted to. + /// + protected internal float MinimumAspect = 5f / 4f; + + /// + /// Aspect ratio of the playfield's resolution clamped between and + /// . + /// + protected internal float ClampedCurrentAspect { get; private set; } = 16f / 9f; + + /// + /// The maximum relative height of the playfield. This is a fraction of the available area. + /// + protected internal float MaximumRelativeHeight = 1f / 3f; + + /// + /// The minimum relative height of the playfield. This is a fraction of the available area. + /// + protected internal float MinimumRelativeHeight = 0f; + + /// + /// Whether the playfield should be trimmed when the aspect ratio exceeds the maximum. + /// + protected internal bool TrimOnOverflow = false; public TaikoPlayfieldAdjustmentContainer() { @@ -37,41 +62,50 @@ protected override void Update() float relativeHeight = base_relative_height; + float widthScale = 1.0f; + // Players coming from stable expect to be able to change the aspect ratio regardless of the window size. // We originally wanted to limit this more, but there was considerable pushback from the community. // // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit. // This is still a bit weird, because readability changes with window size, but it is what it is. - if (LockPlayfieldAspectRange.Value) - { - float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y; + // + // This is separate from CurrentAspect as this needs to be the unbounded aspect ratio. + float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y; - if (currentAspect > MAXIMUM_ASPECT) - relativeHeight *= currentAspect / MAXIMUM_ASPECT; - else if (currentAspect < MINIMUM_ASPECT) - relativeHeight *= currentAspect / MINIMUM_ASPECT; + if (currentAspect > MaximumAspect) + { + if (TrimOnOverflow) + { + widthScale = MaximumAspect / currentAspect; + } + else + { + relativeHeight *= currentAspect / MaximumAspect; + } + } + else if (currentAspect < MinimumAspect) + { + relativeHeight *= currentAspect / MinimumAspect; } // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. - relativeHeight = Math.Min(relativeHeight, 1f / 3f); + relativeHeight = Math.Clamp(relativeHeight, MinimumRelativeHeight, MaximumRelativeHeight); Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); - Width = 1 / Scale.X; + Width = 1 / Scale.X * widthScale; } public double ComputeTimeRange() { - float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y; - - if (LockPlayfieldAspectRange.Value) - currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT); + ClampedCurrentAspect = Math.Clamp(Parent!.ChildSize.X / Parent!.ChildSize.Y, MinimumAspect, MaximumAspect); // in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location. // however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point. // therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range". // see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168 const float stable_hit_location = 160f; - float widthScaled = currentAspect * stable_gamefield_height; + float widthScaled = ClampedCurrentAspect * stable_gamefield_height; float inLength = widthScaled - stable_hit_location; // also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms)