diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs index e67afb25b..3f2da27ea 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs @@ -61,19 +61,6 @@ public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) patternGenerator = new SentakkiPatternGenerator(beatmap); } - protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) - { - var convertedBeatmap = base.ConvertBeatmap(original, cancellationToken); - - // We don't use any of the standard difficulty values - // But we initialize to defaults so HR can adjust HitWindows in a controlled manner - // We clone beforehand to avoid altering the original (it really should be readonly :P) - convertedBeatmap.BeatmapInfo = convertedBeatmap.BeatmapInfo.Clone(); - convertedBeatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty(); - - return convertedBeatmap; - } - protected override Beatmap CreateBeatmap() => new SentakkiBeatmap(); protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Sentakki/Judgements/SentakkiJudgementResult.cs b/osu.Game.Rulesets.Sentakki/Judgements/SentakkiJudgementResult.cs new file mode 100644 index 000000000..bd0e357c1 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Judgements/SentakkiJudgementResult.cs @@ -0,0 +1,24 @@ +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Sentakki.Judgements; + +public class SentakkiJudgementResult : JudgementResult +{ + public SentakkiJudgementResult(HitObject hitObject, Judgement judgement) : base(hitObject, judgement) + { + } + + public new HitResult Type + { + get => base.Type; + set + { + Critical = value == HitResult.Perfect; + base.Type = Critical ? HitResult.Great : value; + } + } + + public bool Critical { get; private set; } +} diff --git a/osu.Game.Rulesets.Sentakki/Localisation/Mods/SentakkiModHardRockStrings.cs b/osu.Game.Rulesets.Sentakki/Localisation/Mods/SentakkiModHardRockStrings.cs new file mode 100644 index 000000000..85af995f8 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Localisation/Mods/SentakkiModHardRockStrings.cs @@ -0,0 +1,17 @@ +using osu.Framework.Localisation; + +namespace osu.Game.Rulesets.Sentakki.Localisation.Mods +{ + public static class SentakkiModHardRockStrings + { + private const string prefix = @"osu.Game.Rulesets.Sentakki.Resources.Localisation.Mods.SentakkiModHardRockStrings"; + + public static LocalisableString JudgementMode => new TranslatableString(getKey(@"judgement_mode"), @"Judgement mode"); + public static LocalisableString JudgementModeDescription => new TranslatableString(getKey(@"judgement_mode_description"), @"Judgement modes determine how strict the hitwindows are during gameplay."); + + public static LocalisableString MinimumResult => new TranslatableString(getKey(@"minimum_result"), @"Minimum hit result"); + public static LocalisableString MinimumResultDescription => new TranslatableString(getKey(@"minimum_result_description"), @"The minimum HitResult that is accepted during gameplay. Anything below will be considered a miss."); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModHardRock.cs b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModHardRock.cs index 36245b6c6..5b514b489 100644 --- a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModHardRock.cs +++ b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModHardRock.cs @@ -1,15 +1,50 @@ +using System.ComponentModel; +using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Sentakki.Localisation.Mods; +using osu.Game.Rulesets.Sentakki.Scoring; namespace osu.Game.Rulesets.Sentakki.Mods { - public class SentakkiModHardRock : ModHardRock + public class SentakkiModHardRock : ModHardRock, IApplicableToHitObject { - public override double ScoreMultiplier => 1.06; + public override double ScoreMultiplier => 1; public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { - difficulty.OverallDifficulty = 10f; + // This is a no-op since we don't use beatmap difficulty + // The only reason we still inherit from ModHardRock is to be able to use their localized strings + } + + [SettingSource(typeof(SentakkiModHardRockStrings), nameof(SentakkiModHardRockStrings.JudgementMode), nameof(SentakkiModHardRockStrings.JudgementModeDescription))] + public Bindable judgementMode { get; } = new Bindable(SentakkiJudgementMode.Maji); + + [SettingSource(typeof(SentakkiModHardRockStrings), nameof(SentakkiModHardRockStrings.MinimumResult), nameof(SentakkiModHardRockStrings.MinimumResultDescription))] + public Bindable minimumValidResult { get; } = new Bindable(SentakkiHitResult.Good); + + public void ApplyToHitObject(HitObject hitObject) + { + // Nested HitObjects should get the same treatment + foreach (var nested in hitObject.NestedHitObjects) + ApplyToHitObject(nested); + + if (hitObject.HitWindows is not SentakkiHitWindows shw) + return; + + shw.MinimumHitResult = (HitResult)minimumValidResult.Value; + shw.JudgementMode = judgementMode.Value; + } + + public enum SentakkiHitResult + { + Good = 3, + Great = 4, + Perfect = 5, + [Description("Critical Perfect")] Critical = 6, } } } diff --git a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModNoFail.cs b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModNoFail.cs index f3bf5e993..09133e5b4 100644 --- a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModNoFail.cs +++ b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModNoFail.cs @@ -1,11 +1,15 @@ using System; -using System.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Sentakki.Mods { public class SentakkiModNoFail : ModNoFail { - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(SentakkiModChallenge)).ToArray(); + public override Type[] IncompatibleMods => new Type[]{ + typeof(ModRelax), + typeof(ModFailCondition), + typeof(SentakkiModChallenge), + typeof(ModAutoplay) + }; } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHold.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHold.cs index 0bff41d23..b2bbac2fd 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHold.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHold.cs @@ -116,6 +116,10 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) else result = HitResult.Miss; + // This is specifically to accommodate the threshold setting in HR + if (!HitObject.HitWindows.IsHitResultAllowed(result)) + result = HitResult.Miss; + // Hold is over, but head windows are still active. // Only happens on super short holds // Force a miss on the head in this case diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs index 4671fb3d4..c9efbd842 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs @@ -22,7 +22,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered) { if (Auto && timeOffset > 0) - ApplyResult(Result.Judgement.MaxResult); + ApplyResult(HitResult.Perfect); if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(Result.Judgement.MinResult); diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScoreBonusObject.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScoreBonusObject.cs index 99935092e..39205d96f 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScoreBonusObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScoreBonusObject.cs @@ -1,6 +1,7 @@ using System; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Sentakki.Objects.Drawables { @@ -16,7 +17,16 @@ public DrawableScoreBonusObject(ScoreBonusObject? hitObject) { } - public void TriggerResult() => ApplyResult(static r => r.Type = (Math.Abs(r.TimeOffset) < 16) ? r.Judgement.MaxResult : r.Judgement.MinResult); + public void TriggerResult() + { + double timeOffset = Math.Abs(Time.Current - HitObject.StartTime); + + ApplyResult(r => + { + bool isCrit = r.HitObject.HitWindows.ResultFor(timeOffset) == HitResult.Perfect; + r.Type = isCrit ? r.Judgement.MaxResult : r.Judgement.MinResult; + }); + } public new void ApplyResult(Action application) { diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScorePaddingObject.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScorePaddingObject.cs index 93c37cae6..7ec91a1aa 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScorePaddingObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableScorePaddingObject.cs @@ -1,6 +1,7 @@ using System; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Judgements; namespace osu.Game.Rulesets.Sentakki.Objects.Drawables { @@ -16,6 +17,8 @@ public DrawableScorePaddingObject(ScorePaddingObject? hitObject) { } + protected override JudgementResult CreateResult(Judgement judgement) => new SentakkiJudgementResult(HitObject, judgement); + public new void ApplyResult(Action application) { if (!Result.HasResult) diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs index 7802f6289..bcb4e4dc4 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs @@ -2,8 +2,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Sentakki.Judgements; using osu.Game.Rulesets.Sentakki.UI; namespace osu.Game.Rulesets.Sentakki.Objects.Drawables @@ -53,9 +55,12 @@ protected override void OnApply() ExBindable.BindTo(HitObject.ExBindable); } + protected override JudgementResult CreateResult(Judgement judgement) => new SentakkiJudgementResult(HitObject, judgement); + protected void ApplyResult(HitResult result) { - void resultApplication(JudgementResult r) => r.Type = result; + void resultApplication(JudgementResult r) => ((SentakkiJudgementResult)r).Type = result; + ApplyResult(resultApplication); } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiJudgement.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiJudgement.cs index d3f5889b4..df704873b 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiJudgement.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiJudgement.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.Judgements; +using osu.Game.Rulesets.Sentakki.Scoring; using osu.Game.Rulesets.Sentakki.UI; using osuTK; using osuTK.Graphics; @@ -25,8 +27,6 @@ public partial class DrawableSentakkiJudgement : PoolableDrawable private SentakkiJudgementPiece judgementPiece = null!; private OsuSpriteText timingPiece = null!; - private HitResult result = HitResult.Good; - private readonly BindableBool detailedJudgements = new BindableBool(); [Resolved] @@ -55,7 +55,7 @@ private void load(SentakkiRulesetConfigManager sentakkiConfigs) Shadow = true, ShadowColour = Color4.Black }, - judgementPiece = new SentakkiJudgementPiece(result) + judgementPiece = new SentakkiJudgementPiece(HitResult.Great) } } ); @@ -63,11 +63,11 @@ private void load(SentakkiRulesetConfigManager sentakkiConfigs) public DrawableSentakkiJudgement Apply(JudgementResult result, DrawableHitObject hitObject) { - this.result = result.Type; + var senResult = (SentakkiJudgementResult)result; judgementPiece.JudgementText.Text = result.Type.GetDisplayNameForSentakkiResult().ToUpperInvariant(); judgementPiece.JudgementText.Colour = result.Type.GetColorForSentakkiResult(); - if (result.HitObject.HitWindows is HitWindows.EmptyHitWindows || result.Type == HitResult.Miss || !detailedJudgements.Value) + if (senResult.HitObject.HitWindows is SentakkiEmptyHitWindows || result.Type == HitResult.Miss || !detailedJudgements.Value) { timingPiece.Alpha = 0; } @@ -75,21 +75,21 @@ public DrawableSentakkiJudgement Apply(JudgementResult result, DrawableHitObject { timingPiece.Alpha = 1; - if (result.TimeOffset >= 16) + if (senResult.Critical) + { + timingPiece.Text = "CRITICAL"; + timingPiece.Colour = Color4.Orange; + } + else if (result.TimeOffset > 0) { timingPiece.Text = "LATE"; timingPiece.Colour = Color4.OrangeRed; } - else if (result.TimeOffset <= -16) + else if (result.TimeOffset < 0) { timingPiece.Text = "EARLY"; timingPiece.Colour = Color4.GreenYellow; } - else - { - timingPiece.Text = "CRITICAL"; - timingPiece.Colour = Color4.Orange; - } } LifetimeStart = result.TimeAbsolute; diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs index 998917ced..8735ed310 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs @@ -79,7 +79,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered) { if (Auto && timeOffset > 0) - ApplyResult(Result.Judgement.MaxResult); + ApplyResult(HitResult.Perfect); else if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(Result.Judgement.MinResult); diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs index 3da575bcd..9857956fe 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs @@ -100,7 +100,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered || Auto) { if (Auto && timeOffset > 0) - ApplyResult(Result.Judgement.MaxResult); + ApplyResult(HitResult.Perfect); else if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(Result.Judgement.MinResult); @@ -113,8 +113,8 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (result == HitResult.None) return; - // Hit before the early Great window - if (timeOffset < 0 && result != Result.Judgement.MaxResult) + // Hit before the Perfect window + if (timeOffset < 0 && result is not HitResult.Perfect) return; if (ExBindable.Value && result.IsHit()) diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouchHold.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouchHold.cs index 6f86f46f0..1753fec64 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouchHold.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouchHold.cs @@ -156,6 +156,10 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) else resultType = HitResult.Miss; + // This is specifically to accommodate the threshold setting in HR + if (!HitObject.HitWindows.IsHitResultAllowed(resultType)) + resultType = HitResult.Miss; + AccentColour.Value = colours.ForHitResult(resultType); ApplyResult(resultType); } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Hold.cs b/osu.Game.Rulesets.Sentakki/Objects/Hold.cs index 0e06cca52..e54e5051e 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Hold.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Hold.cs @@ -36,6 +36,8 @@ public double EndTime protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { + // We intentionally not call the base method to avoid break notes being added + AddNested(new HoldHead { Break = Break, @@ -47,12 +49,12 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok }); } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + protected override HitWindows CreateHitWindows() => new SentakkiEmptyHitWindows(); public class HoldHead : SentakkiLanedHitObject { public override Judgement CreateJudgement() => new SentakkiJudgement(); - protected override HitWindows CreateHitWindows() => new SentakkiHitWindows(); + protected override HitWindows CreateHitWindows() => new SentakkiTapHitWindows(); } } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs b/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs index 4a354cc89..1c3efbd97 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs @@ -46,7 +46,7 @@ public bool Ex [JsonIgnore] public virtual Color4 DefaultNoteColour => Color4Extensions.FromHex("FF0064"); - protected override HitWindows CreateHitWindows() => new SentakkiHitWindows(); + protected override HitWindows CreateHitWindows() => new SentakkiTapHitWindows(); // This special hitsample is used for Sentakki specific samples, with doesn't have bank specific variants public class SentakkiHitSampleInfo : HitSampleInfo, IEquatable diff --git a/osu.Game.Rulesets.Sentakki/Objects/SentakkiLanedHitObject.cs b/osu.Game.Rulesets.Sentakki/Objects/SentakkiLanedHitObject.cs index 092c21c97..3f18b841a 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/SentakkiLanedHitObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/SentakkiLanedHitObject.cs @@ -37,7 +37,11 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok AddNested(new ScorePaddingObject { StartTime = this.GetEndTime() }); // Add bonus for players hitting within the critical window - AddNested(new ScoreBonusObject { StartTime = this.GetEndTime() }); + AddNested(new ScoreBonusObject + { + StartTime = this.GetEndTime(), + HitWindows = HitWindows + }); } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/TouchHold.cs b/osu.Game.Rulesets.Sentakki/Objects/TouchHold.cs index 82a236408..68f337560 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/TouchHold.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/TouchHold.cs @@ -4,6 +4,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Sentakki.Scoring; namespace osu.Game.Rulesets.Sentakki.Objects { @@ -17,7 +18,7 @@ public double EndTime public double Duration { get; set; } - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + protected override HitWindows CreateHitWindows() => new SentakkiEmptyHitWindows(); public override IList AuxiliarySamples => CreateHoldSample(); diff --git a/osu.Game.Rulesets.Sentakki/Resources/Localisation/Mods/SentakkiModHardRockStrings.resx b/osu.Game.Rulesets.Sentakki/Resources/Localisation/Mods/SentakkiModHardRockStrings.resx new file mode 100644 index 000000000..6cf57adaa --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Resources/Localisation/Mods/SentakkiModHardRockStrings.resx @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Judgement mode + + + Judgement modes determine how strict the hitwindows are during gameplay. + + + Minimum hit result + + + The minimum HitResult that is accepted during gameplay. Anything below will be considered a miss. + + diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiEmptyHitWindows.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiEmptyHitWindows.cs new file mode 100644 index 000000000..d6ef9f8c0 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiEmptyHitWindows.cs @@ -0,0 +1,24 @@ +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Sentakki.Scoring; + +// A special case of EmptyHitWindows which also considers Gori, used by HOLD and TOUCHHOLD +public class SentakkiEmptyHitWindows : SentakkiHitWindows +{ + private static readonly DifficultyRange[] empty_ranges = + { + SimpleDifficultyRange(HitResult.Perfect, 0), + SimpleDifficultyRange(HitResult.Miss, 0), + }; + + public override bool IsHitResultAllowed(HitResult result) + { + // We additionally allow HitResult.Great because we don't acknowledge crits for those objects + if (result is HitResult.Great or HitResult.Miss) + return true; + + return base.IsHitResultAllowed(result); + } + + protected override DifficultyRange[] GetDefaultRanges() => empty_ranges; +} diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiHitWindows.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiHitWindows.cs index 476a55b9c..b6b185e3f 100644 --- a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiHitWindows.cs +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiHitWindows.cs @@ -2,16 +2,42 @@ namespace osu.Game.Rulesets.Sentakki.Scoring { - public class SentakkiHitWindows : HitWindows + public abstract class SentakkiHitWindows : HitWindows { + protected const double timing_unit = 1000 / 60.0; // A single frame + + public HitResult MinimumHitResult = HitResult.None; + + private SentakkiJudgementMode judgementMode = SentakkiJudgementMode.Normal; + public SentakkiJudgementMode JudgementMode + { + get => judgementMode; + set + { + if (value == judgementMode) + return; + + judgementMode = value; + SetDifficulty(0); + } + } + public override bool IsHitResultAllowed(HitResult result) { switch (result) { + // These are guaranteed to be valid + case HitResult.Perfect: + case HitResult.Miss: + return true; + + // These are conditional on the minimum valid result case HitResult.Great: case HitResult.Good: case HitResult.Ok: - case HitResult.Miss: + if (result < MinimumHitResult) + return false; + return true; default: @@ -19,12 +45,18 @@ public override bool IsHitResultAllowed(HitResult result) } } - protected override DifficultyRange[] GetRanges() => new[] + protected abstract DifficultyRange[] GetDefaultRanges(); + protected virtual DifficultyRange[] GetMajiRanges() => GetDefaultRanges(); + protected virtual DifficultyRange[] GetGachiRanges() => GetMajiRanges(); + + protected sealed override DifficultyRange[] GetRanges() => JudgementMode switch { - new DifficultyRange(HitResult.Miss, 144, 144, 72), - new DifficultyRange(HitResult.Ok, 144, 144, 72), - new DifficultyRange(HitResult.Good, 96, 96, 48), - new DifficultyRange(HitResult.Great, 48, 48, 24), + SentakkiJudgementMode.Maji => GetMajiRanges(), + SentakkiJudgementMode.Gati => GetGachiRanges(), + _ => GetDefaultRanges(), }; + + protected static DifficultyRange SimpleDifficultyRange(HitResult result, double range) + => new DifficultyRange(result, range, range, range); } } diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiJudgementMode.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiJudgementMode.cs new file mode 100644 index 000000000..fbeeda1d7 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiJudgementMode.cs @@ -0,0 +1,8 @@ +namespace osu.Game.Rulesets.Sentakki.Scoring; + +public enum SentakkiJudgementMode +{ + Normal, + Maji, + Gati +} diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiScoreProcessor.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiScoreProcessor.cs index 8106bcce3..83df6a761 100644 --- a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiScoreProcessor.cs +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiScoreProcessor.cs @@ -1,6 +1,5 @@ using System; using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Sentakki.Scoring { public partial class SentakkiScoreProcessor : ScoreProcessor diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiSlideHitWindows.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiSlideHitWindows.cs index 3b22d2f3c..caae2f153 100644 --- a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiSlideHitWindows.cs +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiSlideHitWindows.cs @@ -1,15 +1,25 @@ using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Sentakki.Scoring +namespace osu.Game.Rulesets.Sentakki.Scoring; + +public class SentakkiSlideHitWindows : SentakkiHitWindows { - public class SentakkiSlideHitWindows : SentakkiHitWindows - { - protected override DifficultyRange[] GetRanges() => new[] - { - new DifficultyRange(HitResult.Miss, 576, 576, 288), - new DifficultyRange(HitResult.Ok, 576, 576, 288), - new DifficultyRange(HitResult.Good, 416, 416, 208), - new DifficultyRange(HitResult.Great, 288, 288, 144) - }; - } + private static readonly DifficultyRange[] default_ranges = { + SimpleDifficultyRange(HitResult.Miss, 36 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 36 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 26 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 14 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 14 * timing_unit), + }; + + private static readonly DifficultyRange[] maji_ranges = { + SimpleDifficultyRange(HitResult.Miss, 26 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 26 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 14 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 14 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 14 * timing_unit), + }; + + protected override DifficultyRange[] GetDefaultRanges() => default_ranges; + protected override DifficultyRange[] GetMajiRanges() => maji_ranges; } diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTapHitWindows.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTapHitWindows.cs new file mode 100644 index 000000000..985f6be34 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTapHitWindows.cs @@ -0,0 +1,37 @@ +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Sentakki.Scoring; + +public class SentakkiTapHitWindows : SentakkiHitWindows +{ + private static readonly DifficultyRange[] default_ranges = + { + SimpleDifficultyRange(HitResult.Miss, 9 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 9 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 6 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 3 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 1 * timing_unit), + }; + + private static readonly DifficultyRange[] maji_ranges = + { + SimpleDifficultyRange(HitResult.Miss, 6 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 6 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 3 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 2 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 1 * timing_unit), + }; + + private static readonly DifficultyRange[] gachi_ranges = + { + SimpleDifficultyRange(HitResult.Miss, 6 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 6 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 3 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 1 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 1 * timing_unit), + }; + + protected override DifficultyRange[] GetDefaultRanges() => default_ranges; + protected override DifficultyRange[] GetMajiRanges() => maji_ranges; + protected override DifficultyRange[] GetGachiRanges() => gachi_ranges; +} diff --git a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTouchHitWindows.cs b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTouchHitWindows.cs index 0f59c8902..922862178 100644 --- a/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTouchHitWindows.cs +++ b/osu.Game.Rulesets.Sentakki/Scoring/SentakkiTouchHitWindows.cs @@ -1,15 +1,37 @@ using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Sentakki.Scoring +namespace osu.Game.Rulesets.Sentakki.Scoring; + +public class SentakkiTouchHitWindows : SentakkiHitWindows { - public class SentakkiTouchHitWindows : SentakkiHitWindows + private static readonly DifficultyRange[] default_ranges = + { + SimpleDifficultyRange(HitResult.Miss, 18 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 18 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 15 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 12 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 9 * timing_unit) + }; + + private static readonly DifficultyRange[] maji_ranges = { - protected override DifficultyRange[] GetRanges() => new[] - { - new DifficultyRange(HitResult.Miss, 288, 288, 144), - new DifficultyRange(HitResult.Ok, 288, 288, 144), - new DifficultyRange(HitResult.Good, 240, 240, 120), - new DifficultyRange(HitResult.Great, 192, 192, 96), - }; - } + SimpleDifficultyRange(HitResult.Miss, 15 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 15 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 12 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 10.5 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 9 * timing_unit) + }; + + private static readonly DifficultyRange[] gachi_ranges = + { + SimpleDifficultyRange(HitResult.Miss, 15 * timing_unit), + SimpleDifficultyRange(HitResult.Ok, 15 * timing_unit), + SimpleDifficultyRange(HitResult.Good, 12 * timing_unit), + SimpleDifficultyRange(HitResult.Great, 9 * timing_unit), + SimpleDifficultyRange(HitResult.Perfect, 9 * timing_unit) + }; + + protected override DifficultyRange[] GetDefaultRanges() => default_ranges; + protected override DifficultyRange[] GetMajiRanges() => maji_ranges; + protected override DifficultyRange[] GetGachiRanges() => gachi_ranges; } diff --git a/osu.Game.Rulesets.Sentakki/UI/Components/HitObjectLine/LineLifetimeEntry.cs b/osu.Game.Rulesets.Sentakki/UI/Components/HitObjectLine/LineLifetimeEntry.cs index a017f65b3..f422da1bf 100644 --- a/osu.Game.Rulesets.Sentakki/UI/Components/HitObjectLine/LineLifetimeEntry.cs +++ b/osu.Game.Rulesets.Sentakki/UI/Components/HitObjectLine/LineLifetimeEntry.cs @@ -5,9 +5,9 @@ using osu.Framework.Extensions; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Sentakki.Extensions; using osu.Game.Rulesets.Sentakki.Objects; using osuTK.Graphics; -using osu.Game.Rulesets.Sentakki.Extensions; namespace osu.Game.Rulesets.Sentakki.UI.Components.HitObjectLine { diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index 2ae3a24c0..48ba45181 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -3,8 +3,12 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; @@ -16,10 +20,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Framework.Input.Bindings; -using osu.Game.Input.Bindings; namespace osu.Game.Rulesets.Sentakki.UI { diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index 751a5a5c7..995755222 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -16,7 +17,6 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -using osu.Framework.Utils; namespace osu.Game.Rulesets.Sentakki.UI {