mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 21:02:54 +08:00
Merge branch 'master' into key-binding-deduplication
This commit is contained in:
commit
4885c55ef0
@ -17,12 +17,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
{
|
{
|
||||||
private const double offset = 18;
|
private const double offset = 18;
|
||||||
|
|
||||||
|
protected override bool AllowFail => true;
|
||||||
|
|
||||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||||
|
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
|
&& Player.ScoreProcessor.TotalScore.Value == 1_000_000,
|
||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
Beatmap = new Beatmap
|
Beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
@ -40,10 +44,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
public void TestHitWindowWithDoubleTime()
|
||||||
{
|
{
|
||||||
Mod = new ManiaModDoubleTime(),
|
var doubleTime = new ManiaModDoubleTime();
|
||||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = doubleTime,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||||
|
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
|
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
|
||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
Beatmap = new Beatmap
|
Beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
@ -61,3 +71,4 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -200,10 +200,12 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertHeadJudgement(HitResult.Perfect);
|
assertHeadJudgement(HitResult.Perfect);
|
||||||
assertComboAtJudgement(0, 1);
|
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||||
|
assertComboAtJudgement(1, 1);
|
||||||
assertTailJudgement(HitResult.Meh);
|
assertTailJudgement(HitResult.Meh);
|
||||||
assertComboAtJudgement(1, 0);
|
assertComboAtJudgement(2, 0);
|
||||||
assertComboAtJudgement(2, 1);
|
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||||
|
assertComboAtJudgement(4, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -380,7 +382,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
|
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
|
||||||
{
|
{
|
||||||
Note note;
|
// Next note within tail lenience
|
||||||
|
Note note = new Note { StartTime = time_tail + 50 };
|
||||||
|
|
||||||
var beatmap = new Beatmap<ManiaHitObject>
|
var beatmap = new Beatmap<ManiaHitObject>
|
||||||
{
|
{
|
||||||
@ -392,13 +395,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
Duration = time_tail - time_head,
|
Duration = time_tail - time_head,
|
||||||
Column = 0,
|
Column = 0,
|
||||||
},
|
},
|
||||||
{
|
note
|
||||||
// Next note within tail lenience
|
|
||||||
note = new Note
|
|
||||||
{
|
|
||||||
StartTime = time_tail + 50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
BeatmapInfo =
|
BeatmapInfo =
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
AddAssert("all objects perfectly judged",
|
AddAssert("all objects perfectly judged",
|
||||||
() => judgementResults.Select(result => result.Type),
|
() => judgementResults.Select(result => result.Type),
|
||||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
AddAssert("all objects perfectly judged",
|
AddAssert("all objects perfectly judged",
|
||||||
() => judgementResults.Select(result => result.Type),
|
() => judgementResults.Select(result => result.Type),
|
||||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||||
|
@ -385,6 +385,9 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
HitResult.Good,
|
HitResult.Good,
|
||||||
HitResult.Ok,
|
HitResult.Ok,
|
||||||
HitResult.Meh,
|
HitResult.Meh,
|
||||||
|
|
||||||
|
// HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as
|
||||||
|
// it would be a bit redundant to show this to the user.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -33,35 +32,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
public void UpdateResult() => base.UpdateResult(true);
|
public void UpdateResult() => base.UpdateResult(true);
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset) =>
|
||||||
{
|
|
||||||
Debug.Assert(HitObject.HitWindows != null);
|
|
||||||
|
|
||||||
// Factor in the release lenience
|
// Factor in the release lenience
|
||||||
timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
|
base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE);
|
||||||
|
|
||||||
if (!userTriggered)
|
protected override HitResult GetCappedResult(HitResult result)
|
||||||
{
|
|
||||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = HitObject.HitWindows.ResultFor(timeOffset);
|
|
||||||
if (result == HitResult.None)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ApplyResult(r =>
|
|
||||||
{
|
{
|
||||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||||
bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
|
bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
|
||||||
|
|
||||||
if (result > HitResult.Meh && hasComboBreak)
|
if (result > HitResult.Meh && hasComboBreak)
|
||||||
result = HitResult.Meh;
|
return HitResult.Meh;
|
||||||
|
|
||||||
r.Type = result;
|
return result;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool OnPressed(KeyBindingPressEvent<ManiaAction> e) => false; // Handled by the hold note
|
public override bool OnPressed(KeyBindingPressEvent<ManiaAction> e) => false; // Handled by the hold note
|
||||||
|
@ -13,6 +13,8 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Configuration;
|
using osu.Game.Rulesets.Mania.Configuration;
|
||||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
private Drawable headPiece;
|
private Drawable headPiece;
|
||||||
|
|
||||||
|
private DrawableNotePerfectBonus perfectBonus;
|
||||||
|
|
||||||
public DrawableNote()
|
public DrawableNote()
|
||||||
: this(null)
|
: this(null)
|
||||||
{
|
{
|
||||||
@ -89,7 +93,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
if (!userTriggered)
|
if (!userTriggered)
|
||||||
{
|
{
|
||||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||||
|
{
|
||||||
|
perfectBonus.TriggerResult(false);
|
||||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,9 +105,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
if (result == HitResult.None)
|
if (result == HitResult.None)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
result = GetCappedResult(result);
|
||||||
|
|
||||||
|
perfectBonus.TriggerResult(result == HitResult.Perfect);
|
||||||
ApplyResult(r => r.Type = result);
|
ApplyResult(r => r.Type = result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void MissForcefully()
|
||||||
|
{
|
||||||
|
perfectBonus.TriggerResult(false);
|
||||||
|
base.MissForcefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some objects in mania may want to limit the max result.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual HitResult GetCappedResult(HitResult result) => result;
|
||||||
|
|
||||||
public virtual bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
public virtual bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
||||||
{
|
{
|
||||||
if (e.Action != Action.Value)
|
if (e.Action != Action.Value)
|
||||||
@ -115,6 +137,32 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case DrawableNotePerfectBonus bonus:
|
||||||
|
AddInternal(perfectBonus = bonus);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ClearNestedHitObjects()
|
||||||
|
{
|
||||||
|
RemoveInternal(perfectBonus, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case NotePerfectBonus bonus:
|
||||||
|
return new DrawableNotePerfectBonus(bonus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.CreateNestedHitObject(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSnapColour()
|
private void updateSnapColour()
|
||||||
{
|
{
|
||||||
if (beatmap == null || HitObject == null) return;
|
if (beatmap == null || HitObject == null) return;
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
|
{
|
||||||
|
public partial class DrawableNotePerfectBonus : DrawableManiaHitObject<NotePerfectBonus>
|
||||||
|
{
|
||||||
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
|
public DrawableNotePerfectBonus()
|
||||||
|
: this(null!)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
|
||||||
|
: base(hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply a judgement result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hit">Whether this tick was reached.</param>
|
||||||
|
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.Judgements;
|
using osu.Game.Rulesets.Mania.Judgements;
|
||||||
|
|
||||||
@ -12,5 +13,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
public class Note : ManiaHitObject
|
public class Note : ManiaHitObject
|
||||||
{
|
{
|
||||||
public override Judgement CreateJudgement() => new ManiaJudgement();
|
public override Judgement CreateJudgement() => new ManiaJudgement();
|
||||||
|
|
||||||
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
|
AddNested(new NotePerfectBonus { StartTime = StartTime });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs
Normal file
20
osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Objects
|
||||||
|
{
|
||||||
|
public class NotePerfectBonus : ManiaHitObject
|
||||||
|
{
|
||||||
|
public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
|
||||||
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
|
||||||
|
public class NotePerfectBonusJudgement : ManiaJudgement
|
||||||
|
{
|
||||||
|
public override HitResult MaxResult => HitResult.SmallBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -123,9 +123,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
|
|
||||||
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
|
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
|
||||||
{
|
{
|
||||||
// ensure that the hold note is also faded out when the head/tail/any tick is missed.
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
// Ensure that the hold note is also faded out when the head/tail/body is missed.
|
||||||
|
// Importantly, we filter out unrelated objects like DrawableNotePerfectBonus.
|
||||||
|
case DrawableHoldNoteTail:
|
||||||
|
case DrawableHoldNoteHead:
|
||||||
|
case DrawableHoldNoteBody:
|
||||||
if (state == ArmedState.Miss)
|
if (state == ArmedState.Miss)
|
||||||
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
|
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
||||||
|
@ -109,6 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||||
|
|
||||||
RegisterPool<Note, DrawableNote>(10, 50);
|
RegisterPool<Note, DrawableNote>(10, 50);
|
||||||
|
RegisterPool<NotePerfectBonus, DrawableNotePerfectBonus>(10, 50);
|
||||||
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
|
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
|
||||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||||
|
111
osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs
Normal file
111
osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class SpinFramesGenerator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A small amount to spin beyond a given angle to mitigate floating-point precision errors.
|
||||||
|
/// </summary>
|
||||||
|
public const float SPIN_ERROR = MathF.PI / 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The offset from the centre of the spinner at which to spin.
|
||||||
|
/// </summary>
|
||||||
|
private const float centre_spin_offset = 50;
|
||||||
|
|
||||||
|
private readonly double startTime;
|
||||||
|
private readonly float startAngle;
|
||||||
|
private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="SpinFramesGenerator"/> that can be used to generate spinner spin frames.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime">The time at which to start spinning.</param>
|
||||||
|
/// <param name="startAngle">The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis.</param>
|
||||||
|
public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f)
|
||||||
|
{
|
||||||
|
this.startTime = startTime;
|
||||||
|
this.startAngle = startAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a single spin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">The amount of degrees to spin.</param>
|
||||||
|
/// <param name="duration">The time to spend to perform the spin.</param>
|
||||||
|
/// <returns>This <see cref="SpinFramesGenerator"/>.</returns>
|
||||||
|
public SpinFramesGenerator Spin(float delta, double duration)
|
||||||
|
{
|
||||||
|
sequences.Add((delta / 360 * 2 * MathF.PI, duration));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs the replay frames.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The replay frames.</returns>
|
||||||
|
public List<ReplayFrame> Build()
|
||||||
|
{
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
|
|
||||||
|
double lastTime = startTime;
|
||||||
|
float lastAngle = startAngle;
|
||||||
|
int lastDirection = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < sequences.Count; i++)
|
||||||
|
{
|
||||||
|
var seq = sequences[i];
|
||||||
|
|
||||||
|
int seqDirection = Math.Sign(seq.deltaAngle);
|
||||||
|
float seqError = SPIN_ERROR * seqDirection;
|
||||||
|
|
||||||
|
if (seqDirection == lastDirection)
|
||||||
|
{
|
||||||
|
// Spinning in the same direction, but the error was already added in the last rotation.
|
||||||
|
seqError = 0;
|
||||||
|
}
|
||||||
|
else if (lastDirection != 0)
|
||||||
|
{
|
||||||
|
// Spinning in a different direction, we need to account for the error of the start angle, so double it.
|
||||||
|
seqError *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
double seqStartTime = lastTime;
|
||||||
|
double seqEndTime = lastTime + seq.duration;
|
||||||
|
float seqStartAngle = lastAngle;
|
||||||
|
float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError;
|
||||||
|
|
||||||
|
// Intermediate spin frames.
|
||||||
|
for (; lastTime < seqEndTime; lastTime += 10)
|
||||||
|
frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton));
|
||||||
|
|
||||||
|
// Final frame at the end of the current spin.
|
||||||
|
frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton));
|
||||||
|
|
||||||
|
lastTime = seqEndTime;
|
||||||
|
lastAngle = seqEndAngle;
|
||||||
|
lastDirection = seqDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key release frame.
|
||||||
|
if (frames.Count > 0)
|
||||||
|
frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position));
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle)
|
||||||
|
{
|
||||||
|
float angle = startAngle + (endAngle - startAngle) * (float)p;
|
||||||
|
return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -356,15 +356,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
performTest(hitObjects, new List<ReplayFrame>
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
};
|
||||||
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
|
|
||||||
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
|
frames.AddRange(new SpinFramesGenerator(time_spinner + 10)
|
||||||
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
|
.Spin(360, 500)
|
||||||
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
.Build());
|
||||||
});
|
|
||||||
|
performTest(hitObjects, frames);
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddStep("rotate some", () => dho.RotationTracker.AddRotation(180));
|
AddStep("rotate some", () => dho.RotationTracker.AddRotation(180));
|
||||||
AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180);
|
AddAssert("rotation is set", () => dho.Result.TotalRotation == 180);
|
||||||
|
|
||||||
AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner
|
AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner
|
||||||
{
|
{
|
||||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
Duration = 1000,
|
Duration = 1000,
|
||||||
})));
|
})));
|
||||||
|
|
||||||
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
|
AddAssert("rotation is reset", () => dho.Result.TotalRotation == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Spinner prepareObject(Spinner circle)
|
private Spinner prepareObject(Spinner circle)
|
||||||
|
290
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs
Normal file
290
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene
|
||||||
|
{
|
||||||
|
private const int centre_x = 256;
|
||||||
|
private const int centre_y = 192;
|
||||||
|
private const double time_spinner_start = 1500;
|
||||||
|
private const double time_spinner_end = 8000;
|
||||||
|
|
||||||
|
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||||
|
|
||||||
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||||
|
private ManualClock? manualClock;
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||||
|
{
|
||||||
|
return manualClock == null
|
||||||
|
? base.CreateWorkingBeatmap(beatmap, storyboard)
|
||||||
|
: new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
manualClock = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
[Ignore("An upcoming implementation will fix this case")]
|
||||||
|
public void TestVibrateWithoutSpinningOffCentre()
|
||||||
|
{
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
|
|
||||||
|
const int vibrate_time = 50;
|
||||||
|
const float y_pos = centre_y - 50;
|
||||||
|
|
||||||
|
int direction = -1;
|
||||||
|
|
||||||
|
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
||||||
|
{
|
||||||
|
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton));
|
||||||
|
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton));
|
||||||
|
|
||||||
|
direction *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
performTest(frames);
|
||||||
|
|
||||||
|
assertTicksHit(0);
|
||||||
|
assertSpinnerHit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
[Ignore("An upcoming implementation will fix this case")]
|
||||||
|
public void TestVibrateWithoutSpinningOnCentre()
|
||||||
|
{
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
|
|
||||||
|
const int vibrate_time = 50;
|
||||||
|
|
||||||
|
int direction = -1;
|
||||||
|
|
||||||
|
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
||||||
|
{
|
||||||
|
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton));
|
||||||
|
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton));
|
||||||
|
|
||||||
|
direction *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
performTest(frames);
|
||||||
|
|
||||||
|
assertTicksHit(0);
|
||||||
|
assertSpinnerHit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spins in a single direction.
|
||||||
|
/// </summary>
|
||||||
|
[TestCase(180, 0)]
|
||||||
|
[TestCase(-180, 0)]
|
||||||
|
[TestCase(360, 1)]
|
||||||
|
[TestCase(-360, 1)]
|
||||||
|
[TestCase(540, 1)]
|
||||||
|
[TestCase(-540, 1)]
|
||||||
|
[TestCase(720, 2)]
|
||||||
|
[TestCase(-720, 2)]
|
||||||
|
public void TestSpinSingleDirection(float amount, int expectedTicks)
|
||||||
|
{
|
||||||
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
|
.Spin(amount, 500)
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
assertTicksHit(expectedTicks);
|
||||||
|
assertSpinnerHit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spin half-way clockwise then perform one full spin counter-clockwise.
|
||||||
|
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
[Ignore("An upcoming implementation will fix this case")]
|
||||||
|
public void TestSpinHalfBothDirections()
|
||||||
|
{
|
||||||
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
|
.Spin(180, 500) // Rotate to +0.5.
|
||||||
|
.Spin(-360, 500) // Rotate to -0.5
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
assertTicksHit(0);
|
||||||
|
assertSpinnerHit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spin in one direction then spin in the other.
|
||||||
|
/// </summary>
|
||||||
|
[TestCase(180, -540, 1)]
|
||||||
|
[TestCase(-180, 540, 1)]
|
||||||
|
[TestCase(180, -900, 2)]
|
||||||
|
[TestCase(-180, 900, 2)]
|
||||||
|
[Ignore("An upcoming implementation will fix this case")]
|
||||||
|
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
||||||
|
{
|
||||||
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
|
.Spin(direction1, 500)
|
||||||
|
.Spin(direction2, 500)
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
assertTicksHit(expectedTicks);
|
||||||
|
assertSpinnerHit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("An upcoming implementation will fix this case")]
|
||||||
|
public void TestRewind()
|
||||||
|
{
|
||||||
|
AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 });
|
||||||
|
|
||||||
|
List<ReplayFrame> frames = new SpinFramesGenerator(time_spinner_start)
|
||||||
|
.Spin(360, 500) // 2000ms -> 1 full CW spin
|
||||||
|
.Spin(-180, 500) // 2500ms -> 0.5 CCW spins
|
||||||
|
.Spin(90, 500) // 3000ms -> 0.25 CW spins
|
||||||
|
.Spin(450, 500) // 3500ms -> 1 full CW spin
|
||||||
|
.Spin(180, 500) // 4000ms -> 0.5 CW spins
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
loadPlayer(frames);
|
||||||
|
|
||||||
|
GameplayClockContainer clock = null!;
|
||||||
|
DrawableRuleset drawableRuleset = null!;
|
||||||
|
AddStep("get gameplay objects", () =>
|
||||||
|
{
|
||||||
|
clock = currentPlayer.ChildrenOfType<GameplayClockContainer>().Single();
|
||||||
|
drawableRuleset = currentPlayer.ChildrenOfType<DrawableRuleset>().Single();
|
||||||
|
});
|
||||||
|
|
||||||
|
addSeekStep(frames.Last().Time);
|
||||||
|
|
||||||
|
DrawableSpinner drawableSpinner = null!;
|
||||||
|
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
||||||
|
|
||||||
|
assertTotalRotation(4000, 900);
|
||||||
|
assertTotalRotation(3750, 810);
|
||||||
|
assertTotalRotation(3500, 720);
|
||||||
|
assertTotalRotation(3250, 530);
|
||||||
|
assertTotalRotation(3000, 540);
|
||||||
|
assertTotalRotation(2750, 540);
|
||||||
|
assertTotalRotation(2500, 540);
|
||||||
|
assertTotalRotation(2250, 360);
|
||||||
|
assertTotalRotation(2000, 180);
|
||||||
|
assertTotalRotation(1500, 0);
|
||||||
|
|
||||||
|
void assertTotalRotation(double time, float expected)
|
||||||
|
{
|
||||||
|
addSeekStep(time);
|
||||||
|
AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation,
|
||||||
|
() => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSeekStep(double time)
|
||||||
|
{
|
||||||
|
AddStep($"seek to {time}", () => clock.Seek(time));
|
||||||
|
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTicksHit(int count)
|
||||||
|
{
|
||||||
|
AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSpinnerHit(bool shouldBeHit)
|
||||||
|
{
|
||||||
|
AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPlayer(List<ReplayFrame> frames)
|
||||||
|
{
|
||||||
|
AddStep("load player", () =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Spinner
|
||||||
|
{
|
||||||
|
StartTime = time_spinner_start,
|
||||||
|
EndTime = time_spinner_end,
|
||||||
|
Position = new Vector2(centre_x, centre_y)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty(),
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||||
|
|
||||||
|
p.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
p.ScoreProcessor.NewJudgement += result =>
|
||||||
|
{
|
||||||
|
if (currentPlayer == p) judgementResults.Add(result);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadScreen(currentPlayer = p);
|
||||||
|
judgementResults.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performTest(List<ReplayFrame> frames)
|
||||||
|
{
|
||||||
|
loadPlayer(frames);
|
||||||
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||||
|
{
|
||||||
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||||
|
|
||||||
|
protected override bool PauseOnFocusLost => false;
|
||||||
|
|
||||||
|
public ScoreAccessibleReplayPlayer(Score score)
|
||||||
|
: base(score, new PlayerConfiguration
|
||||||
|
{
|
||||||
|
AllowPause = false,
|
||||||
|
ShowResults = false,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -11,14 +10,12 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -59,26 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
|
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ReplayFrame> generateReplay(int spins)
|
private static List<ReplayFrame> generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start)
|
||||||
{
|
.Spin(spins * 360, time_spinner_end - time_spinner_start)
|
||||||
var replayFrames = new List<ReplayFrame>();
|
.Build();
|
||||||
|
|
||||||
const int frames_per_spin = 30;
|
|
||||||
|
|
||||||
for (int i = 0; i < spins * frames_per_spin; ++i)
|
|
||||||
{
|
|
||||||
float totalProgress = i / (float)(spins * frames_per_spin);
|
|
||||||
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
|
|
||||||
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
|
|
||||||
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
|
|
||||||
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
|
|
||||||
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;
|
|
||||||
|
|
||||||
replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
|
|
||||||
}
|
|
||||||
|
|
||||||
return replayFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void performTest(List<ReplayFrame> frames)
|
private void performTest(List<ReplayFrame> frames)
|
||||||
{
|
{
|
||||||
|
@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
|
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
|
||||||
});
|
});
|
||||||
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
|
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
|
||||||
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100));
|
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100));
|
||||||
|
|
||||||
addSeekStep(0);
|
addSeekStep(0);
|
||||||
AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance));
|
AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance));
|
||||||
AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100));
|
AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(0).Within(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
||||||
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
|
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
|
||||||
});
|
});
|
||||||
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
|
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.TotalRotation);
|
||||||
|
|
||||||
addSeekStep(spinner_start_time + 2500);
|
addSeekStep(spinner_start_time + 2500);
|
||||||
AddAssert("disc rotation rewound",
|
AddAssert("disc rotation rewound",
|
||||||
@ -92,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
|
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
|
||||||
AddAssert("is cumulative rotation rewound",
|
AddAssert("is cumulative rotation rewound",
|
||||||
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
||||||
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
|
() => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
|
||||||
|
|
||||||
addSeekStep(spinner_start_time + 5000);
|
addSeekStep(spinner_start_time + 5000);
|
||||||
AddAssert("is disc rotation almost same",
|
AddAssert("is disc rotation almost same",
|
||||||
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
|
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
|
||||||
AddAssert("is cumulative rotation almost same",
|
AddAssert("is cumulative rotation almost same",
|
||||||
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
|
() => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||||
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||||
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
addSeekStep(0);
|
addSeekStep(0);
|
||||||
|
@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
performTest(hitObjects, new List<ReplayFrame>
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
};
|
||||||
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
|
|
||||||
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
|
frames.AddRange(new SpinFramesGenerator(time_spinner + 10)
|
||||||
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
|
.Spin(360, 500)
|
||||||
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
.Build());
|
||||||
});
|
|
||||||
|
performTest(hitObjects, frames);
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
|||||||
/// If Double Time is active instead (with a speed multiplier of 1.5x),
|
/// If Double Time is active instead (with a speed multiplier of 1.5x),
|
||||||
/// in the same scenario the property will return 720 * 1.5 = 1080.
|
/// in the same scenario the property will return 720 * 1.5 = 1080.
|
||||||
/// </example>
|
/// </example>
|
||||||
public float RateAdjustedRotation;
|
public float TotalRotation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
||||||
|
@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
// these become implicitly hit.
|
// these become implicitly hit.
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1);
|
return Math.Clamp(Result.TotalRotation / 360 / HitObject.SpinsRequired, 0, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
// don't update after end time to avoid the rate display dropping during fade out.
|
// don't update after end time to avoid the rate display dropping during fade out.
|
||||||
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
||||||
if (Time.Current <= HitObject.EndTime)
|
if (Time.Current <= HitObject.EndTime)
|
||||||
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
spmCalculator.SetRotation(Result.TotalRotation);
|
||||||
|
|
||||||
updateBonusScore();
|
updateBonusScore();
|
||||||
}
|
}
|
||||||
@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
if (ticks.Count == 0)
|
if (ticks.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int spins = (int)(Result.RateAdjustedRotation / 360);
|
int spins = (int)(Result.TotalRotation / 360);
|
||||||
|
|
||||||
if (spins < completedFullSpins)
|
if (spins < completedFullSpins)
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
|
int rotations = (int)(drawableSpinner.Result.TotalRotation / 360);
|
||||||
|
|
||||||
if (wholeRotationCount == rotations) return false;
|
if (wholeRotationCount == rotations) return false;
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
|
int rotations = (int)(drawableSpinner.Result.TotalRotation / 360);
|
||||||
|
|
||||||
if (wholeRotationCount == rotations) return false;
|
if (wholeRotationCount == rotations) return false;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
@ -22,11 +23,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
private readonly DrawableSpinner drawableSpinner;
|
private readonly DrawableSpinner drawableSpinner;
|
||||||
|
|
||||||
private Vector2 mousePosition;
|
private Vector2? mousePosition;
|
||||||
|
private float? lastAngle;
|
||||||
|
|
||||||
private float lastAngle;
|
|
||||||
private float currentRotation;
|
private float currentRotation;
|
||||||
|
|
||||||
private bool rotationTransferred;
|
private bool rotationTransferred;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
@ -63,17 +63,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
|
|
||||||
|
|
||||||
float delta = thisAngle - lastAngle;
|
if (mousePosition is Vector2 pos)
|
||||||
|
{
|
||||||
|
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||||
|
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
|
||||||
|
|
||||||
|
// Normalise the delta to -180 .. 180
|
||||||
|
if (delta > 180) delta -= 360;
|
||||||
|
if (delta < -180) delta += 360;
|
||||||
|
|
||||||
if (Tracking)
|
if (Tracking)
|
||||||
AddRotation(delta);
|
AddRotation(delta);
|
||||||
|
|
||||||
lastAngle = thisAngle;
|
lastAngle = thisAngle;
|
||||||
|
}
|
||||||
|
|
||||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||||
|
|
||||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,41 +89,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Will be a no-op if not a valid time to spin.
|
/// Will be a no-op if not a valid time to spin.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="angle">The delta angle.</param>
|
/// <param name="delta">The delta angle.</param>
|
||||||
public void AddRotation(float angle)
|
public void AddRotation(float delta)
|
||||||
{
|
{
|
||||||
if (!isSpinnableTime)
|
if (!isSpinnableTime)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!rotationTransferred)
|
if (!rotationTransferred)
|
||||||
{
|
{
|
||||||
currentRotation = Rotation * 2;
|
currentRotation = Rotation;
|
||||||
rotationTransferred = true;
|
rotationTransferred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (angle > 180)
|
currentRotation += delta;
|
||||||
{
|
|
||||||
lastAngle += 360;
|
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||||
angle -= 360;
|
|
||||||
}
|
Debug.Assert(Math.Abs(delta) <= 180);
|
||||||
else if (-angle > 180)
|
|
||||||
{
|
|
||||||
lastAngle -= 360;
|
|
||||||
angle += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRotation += angle;
|
|
||||||
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
|
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
|
||||||
// (see: ModTimeRamp)
|
// (see: ModTimeRamp)
|
||||||
drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate));
|
drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetState(DrawableHitObject obj)
|
private void resetState(DrawableHitObject obj)
|
||||||
{
|
{
|
||||||
Tracking = false;
|
Tracking = false;
|
||||||
IsSpinning.Value = false;
|
IsSpinning.Value = false;
|
||||||
mousePosition = default;
|
mousePosition = null;
|
||||||
lastAngle = currentRotation = Rotation = 0;
|
lastAngle = null;
|
||||||
|
currentRotation = 0;
|
||||||
|
Rotation = 0;
|
||||||
rotationTransferred = false;
|
rotationTransferred = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
|
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
|
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
|
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)]
|
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 317_626)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
|
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
|
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||||
@ -86,7 +86,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)]
|
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)]
|
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 30_398)]
|
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 31_928)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
|
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
|
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||||
|
@ -190,10 +190,9 @@ namespace osu.Game.Rulesets.Judgements
|
|||||||
return 200;
|
return 200;
|
||||||
|
|
||||||
case HitResult.Great:
|
case HitResult.Great:
|
||||||
return 300;
|
// Perfect doesn't actually give more score / accuracy directly.
|
||||||
|
|
||||||
case HitResult.Perfect:
|
case HitResult.Perfect:
|
||||||
return 315;
|
return 300;
|
||||||
|
|
||||||
case HitResult.SmallBonus:
|
case HitResult.SmallBonus:
|
||||||
return SMALL_BONUS_SCORE;
|
return SMALL_BONUS_SCORE;
|
||||||
|
@ -55,6 +55,13 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
[Order(1)]
|
[Order(1)]
|
||||||
Great,
|
Great,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is an optional timing window tighter than <see cref="Great"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// By default, this does not give any bonus accuracy or score.
|
||||||
|
/// To have it affect scoring, consider adding a nested bonus object.
|
||||||
|
/// </remarks>
|
||||||
[Description(@"Perfect")]
|
[Description(@"Perfect")]
|
||||||
[EnumMember(Value = "perfect")]
|
[EnumMember(Value = "perfect")]
|
||||||
[Order(0)]
|
[Order(0)]
|
||||||
|
Loading…
Reference in New Issue
Block a user