Skip to content

Commit

Permalink
Merge pull request #487 from LumpBloom7/HR-maimai-judgement-mode
Browse files Browse the repository at this point in the history
Implement maimai judgement modes in HardRock
  • Loading branch information
LumpBloom7 authored Sep 6, 2023
2 parents c8ba623 + 8f3e9dc commit 747d05a
Show file tree
Hide file tree
Showing 29 changed files with 382 additions and 77 deletions.
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

0 comments on commit 747d05a

Please sign in to comment.