Skip to content
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

Implement maimai judgement modes in HardRock #487

Merged
merged 13 commits into from
Sep 6, 2023
13 changes: 0 additions & 13 deletions osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,6 @@ public SentakkiBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
patternGenerator = new SentakkiPatternGenerator(beatmap);
}

protected override Beatmap<SentakkiHitObject> 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<SentakkiHitObject> CreateBeatmap() => new SentakkiBeatmap();

protected override IEnumerable<SentakkiHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
Expand Down
24 changes: 24 additions & 0 deletions osu.Game.Rulesets.Sentakki/Judgements/SentakkiJudgementResult.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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}";
}
}
41 changes: 38 additions & 3 deletions osu.Game.Rulesets.Sentakki/Mods/SentakkiModHardRock.cs
Original file line number Diff line number Diff line change
@@ -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<SentakkiJudgementMode> judgementMode { get; } = new Bindable<SentakkiJudgementMode>(SentakkiJudgementMode.Maji);

[SettingSource(typeof(SentakkiModHardRockStrings), nameof(SentakkiModHardRockStrings.MinimumResult), nameof(SentakkiModHardRockStrings.MinimumResultDescription))]
public Bindable<SentakkiHitResult> minimumValidResult { get; } = new Bindable<SentakkiHitResult>(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,
}
}
}
8 changes: 6 additions & 2 deletions osu.Game.Rulesets.Sentakki/Mods/SentakkiModNoFail.cs
Original file line number Diff line number Diff line change
@@ -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)
};
}
}
4 changes: 4 additions & 0 deletions osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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<JudgementResult> application)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -16,6 +17,8 @@ public DrawableScorePaddingObject(ScorePaddingObject? hitObject)
{
}

protected override JudgementResult CreateResult(Judgement judgement) => new SentakkiJudgementResult(HitObject, judgement);

public new void ApplyResult(Action<JudgementResult> application)
{
if (!Result.HasResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]
Expand Down Expand Up @@ -55,41 +55,41 @@ private void load(SentakkiRulesetConfigManager sentakkiConfigs)
Shadow = true,
ShadowColour = Color4.Black
},
judgementPiece = new SentakkiJudgementPiece(result)
judgementPiece = new SentakkiJudgementPiece(HitResult.Great)
}
}
);
}

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;
}
else
{
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 3 additions & 3 deletions osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTouch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 4 additions & 2 deletions osu.Game.Rulesets.Sentakki/Objects/Hold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
}
}
}
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Sentakki/Objects/SentakkiHitObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SentakkiHitSampleInfo>
Expand Down
6 changes: 5 additions & 1 deletion osu.Game.Rulesets.Sentakki/Objects/SentakkiLanedHitObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}

Expand Down
3 changes: 2 additions & 1 deletion osu.Game.Rulesets.Sentakki/Objects/TouchHold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<HitSampleInfo> AuxiliarySamples => CreateHoldSample();

Expand Down
Loading
Loading