Compare commits
792 Commits
@@ -38,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi
|
||||
|
||||
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||
|
||||
## Developing or debugging
|
||||
## Developing a custom ruleset
|
||||
|
||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates).
|
||||
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
|
||||
|
||||
## Developing osu!
|
||||
|
||||
Please make sure you have the following prerequisites:
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.910.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Framework.Android;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
||||
public class OsuGameActivity : AndroidGameActivity
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuGameAndroid();
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
|
||||
|
||||
// We only care about testing misses, hits are tested via JuiceStream
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 923 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Schedule(() =>
|
||||
{
|
||||
area.AttemptCatch(fruit);
|
||||
area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
|
||||
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
|
||||
|
||||
drawable.Expire();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneComboCounter : CatchSkinnableTestScene
|
||||
{
|
||||
private ScoreProcessor scoreProcessor;
|
||||
|
||||
private Color4 judgedObjectColour = Color4.White;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
scoreProcessor = new ScoreProcessor();
|
||||
|
||||
SetContents(() => new CatchComboDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2.5f),
|
||||
});
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestCatchComboCounter()
|
||||
{
|
||||
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20);
|
||||
AddStep("perform miss", () => performJudgement(HitResult.Miss));
|
||||
|
||||
AddStep("randomize judged object colour", () =>
|
||||
{
|
||||
judgedObjectColour = new Color4(
|
||||
RNG.NextSingle(1f),
|
||||
RNG.NextSingle(1f),
|
||||
RNG.NextSingle(1f),
|
||||
1f
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void performJudgement(HitResult type, Judgement judgement = null)
|
||||
{
|
||||
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
|
||||
|
||||
var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
|
||||
scoreProcessor.ApplyResult(result);
|
||||
|
||||
foreach (var counter in CreatedDrawables.Cast<CatchComboDisplay>())
|
||||
counter.OnNewResult(judgedObject, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
|
||||
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var positionData = obj as IHasXPosition;
|
||||
var comboData = obj as IHasCombo;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
Droplet,
|
||||
CatcherIdle,
|
||||
CatcherFail,
|
||||
CatcherKiai
|
||||
CatcherKiai,
|
||||
CatchComboCounter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
mods = Score.Mods;
|
||||
|
||||
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect);
|
||||
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
|
||||
|
||||
@@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
|
||||
{
|
||||
public class CatchBananaJudgement : CatchJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 1100;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
|
||||
}
|
||||
}
|
||||
public override HitResult MaxResult => HitResult.LargeBonus;
|
||||
|
||||
public override bool ShouldExplodeFor(JudgementResult result) => true;
|
||||
}
|
||||
|
||||
@@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
|
||||
{
|
||||
public class CatchDropletJudgement : CatchJudgement
|
||||
{
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
public override HitResult MaxResult => HitResult.LargeTickHit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
|
||||
{
|
||||
public class CatchJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.Perfect;
|
||||
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
public override HitResult MaxResult => HitResult.Great;
|
||||
|
||||
/// <summary>
|
||||
/// Whether fruit on the platter should explode or drop.
|
||||
|
||||
@@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
|
||||
{
|
||||
public class CatchTinyDropletJudgement : CatchJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 0.02;
|
||||
}
|
||||
}
|
||||
public override HitResult MaxResult => HitResult.SmallTickHit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -86,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
if (CheckPosition == null) return;
|
||||
|
||||
if (timeOffset >= 0 && Result != null)
|
||||
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
|
||||
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(ArmedState state)
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
public override Replay Generate()
|
||||
{
|
||||
if (Beatmap.HitObjects.Count == 0)
|
||||
return Replay;
|
||||
|
||||
// todo: add support for HT DT
|
||||
const double dash_speed = Catcher.BASE_SPEED;
|
||||
const double movement_speed = dash_speed / 2;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
case HitResult.Great:
|
||||
case HitResult.Miss:
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public class CatchScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public override HitWindows CreateHitWindows() => new CatchHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using static osu.Game.Skinning.LegacySkinConfiguration;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
@@ -52,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
case CatchSkinComponents.CatcherKiai:
|
||||
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
|
||||
this.GetAnimation("fruit-ryuuta", true, true, true);
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||
|
||||
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
||||
if (this.HasFont(comboFont))
|
||||
return new LegacyComboCounter(Source);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using static osu.Game.Skinning.LegacySkinConfiguration;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
||||
/// </summary>
|
||||
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
|
||||
{
|
||||
private readonly LegacyRollingCounter counter;
|
||||
|
||||
private readonly LegacyRollingCounter explosion;
|
||||
|
||||
public LegacyComboCounter(ISkin skin)
|
||||
{
|
||||
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Alpha = 0f;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Scale = new Vector2(0.8f);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
|
||||
{
|
||||
Alpha = 0.65f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.5f),
|
||||
},
|
||||
counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private int lastDisplayedCombo;
|
||||
|
||||
public void UpdateCombo(int combo, Color4? hitObjectColour = null)
|
||||
{
|
||||
if (combo == lastDisplayedCombo)
|
||||
return;
|
||||
|
||||
// There may still be existing transforms to the counter (including value change after 250ms),
|
||||
// finish them immediately before new transforms.
|
||||
counter.SetCountWithoutRolling(lastDisplayedCombo);
|
||||
|
||||
lastDisplayedCombo = combo;
|
||||
|
||||
if (Time.Elapsed < 0)
|
||||
{
|
||||
// needs more work to make rewind somehow look good.
|
||||
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Combo fell to zero, roll down and fade out the counter.
|
||||
if (combo == 0)
|
||||
{
|
||||
counter.Current.Value = 0;
|
||||
explosion.Current.Value = 0;
|
||||
|
||||
this.FadeOut(400, Easing.Out);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
|
||||
|
||||
counter.ScaleTo(1.5f)
|
||||
.ScaleTo(0.8f, 250, Easing.Out)
|
||||
.OnComplete(c => c.SetCountWithoutRolling(combo));
|
||||
|
||||
counter.Delay(250)
|
||||
.ScaleTo(1f)
|
||||
.ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
|
||||
|
||||
explosion.Colour = hitObjectColour ?? Color4.White;
|
||||
|
||||
explosion.SetCountWithoutRolling(combo);
|
||||
explosion.ScaleTo(1.5f)
|
||||
.ScaleTo(1.9f, 400, Easing.Out)
|
||||
.FadeOutFromOne(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a component that displays a skinned <see cref="ICatchComboCounter"/> and handles combo judgement results for updating it accordingly.
|
||||
/// </summary>
|
||||
public class CatchComboDisplay : SkinnableDrawable
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
[CanBeNull]
|
||||
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
|
||||
|
||||
public CatchComboDisplay()
|
||||
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
ComboCounter?.UpdateCombo(currentCombo);
|
||||
}
|
||||
|
||||
public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
if (!result.Type.AffectsCombo() || !result.HasResult)
|
||||
return;
|
||||
|
||||
if (!result.IsHit)
|
||||
{
|
||||
updateCombo(0, null);
|
||||
return;
|
||||
}
|
||||
|
||||
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
if (!result.Type.AffectsCombo() || !result.HasResult)
|
||||
return;
|
||||
|
||||
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
|
||||
}
|
||||
|
||||
private void updateCombo(int newCombo, Color4? hitObjectColour)
|
||||
{
|
||||
currentCombo = newCombo;
|
||||
ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
explodingFruitContainer,
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer,
|
||||
CatcherArea
|
||||
CatcherArea,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public override void Add(DrawableHitObject h)
|
||||
{
|
||||
h.OnNewResult += onNewResult;
|
||||
h.OnRevertResult += onRevertResult;
|
||||
|
||||
base.Add(h);
|
||||
|
||||
@@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
|
||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
|
||||
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
||||
|
||||
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
@@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
|
||||
|
||||
public readonly Catcher MovableCatcher;
|
||||
private readonly CatchComboDisplay comboDisplay;
|
||||
|
||||
public Container ExplodingFruitTarget
|
||||
{
|
||||
@@ -34,12 +36,24 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
||||
Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
|
||||
Children = new Drawable[]
|
||||
{
|
||||
comboDisplay = new CatchComboDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
},
|
||||
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
};
|
||||
}
|
||||
|
||||
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
{
|
||||
if (result.Judgement is IgnoreJudgement)
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
void runAfterLoaded(Action action)
|
||||
@@ -86,8 +100,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
else
|
||||
MovableCatcher.Drop();
|
||||
}
|
||||
|
||||
comboDisplay.OnNewResult(fruit, result);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
=> comboDisplay.OnRevertResult(fruit, result);
|
||||
|
||||
public void OnReleased(CatchAction action)
|
||||
{
|
||||
}
|
||||
@@ -105,6 +124,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (state?.CatcherX != null)
|
||||
MovableCatcher.X = state.CatcherX.Value;
|
||||
|
||||
comboDisplay.X = MovableCatcher.X;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface providing a set of methods to update the combo counter.
|
||||
/// </summary>
|
||||
public interface ICatchComboCounter : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the counter to animate a transition from the old combo value it had to the current provided one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called regardless of whether the clock is rewinding.
|
||||
/// </remarks>
|
||||
/// <param name="combo">The new combo value.</param>
|
||||
/// <param name="hitObjectColour">The colour of the object if hit, null on miss.</param>
|
||||
void UpdateCombo(int combo, Color4? hitObjectColour = null);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
|
||||
{
|
||||
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneEditor : EditorTestScene
|
||||
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
|
||||
{
|
||||
@@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
|
||||
{
|
||||
@@ -23,7 +23,7 @@ using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneManiaHitObjectComposer : EditorClockTestScene
|
||||
{
|
||||
@@ -18,7 +18,7 @@ using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
|
||||
{
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public class TestSceneHoldNote : ManiaHitObjectTestScene
|
||||
{
|
||||
public TestSceneHoldNote()
|
||||
[Test]
|
||||
public void TestHoldNote()
|
||||
{
|
||||
AddToggleStep("toggle hitting", v =>
|
||||
{
|
||||
|
||||
@@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}, beatmap);
|
||||
|
||||
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type == HitResult.Miss));
|
||||
.All(j => !j.Type.IsHit()));
|
||||
|
||||
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type == HitResult.Perfect));
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
|
||||
@@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestFixture]
|
||||
public class TestSceneNotes : OsuTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestVariousNotes()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
DrawableNote note1 = null;
|
||||
DrawableNote note2 = null;
|
||||
DrawableHoldNote holdNote1 = null;
|
||||
DrawableHoldNote holdNote2 = null;
|
||||
|
||||
AddStep("create notes", () =>
|
||||
{
|
||||
Clock = new FramedClock(new ManualClock()),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
|
||||
createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
|
||||
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
|
||||
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
|
||||
}
|
||||
};
|
||||
Clock = new FramedClock(new ManualClock()),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new[]
|
||||
{
|
||||
createNoteDisplay(ScrollingDirection.Down, 1, out note1),
|
||||
createNoteDisplay(ScrollingDirection.Up, 2, out note2),
|
||||
createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
|
||||
createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
|
||||
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -68,14 +69,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new FastRandom(seed);
|
||||
|
||||
return base.ConvertBeatmap(original);
|
||||
return base.ConvertBeatmap(original, cancellationToken);
|
||||
}
|
||||
|
||||
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
||||
@@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
|
||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
if (original is ManiaHitObject maniaOriginal)
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
{
|
||||
base.InitialiseDefaults();
|
||||
|
||||
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1);
|
||||
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
|
||||
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteTickJudgement : ManiaJudgement
|
||||
{
|
||||
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 0.01;
|
||||
}
|
||||
}
|
||||
public override HitResult MaxResult => HitResult.LargeTickHit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,33 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class ManiaJudgement : Judgement
|
||||
{
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 50;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 100;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
|
||||
|
||||
case HitResult.Good:
|
||||
return 200;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Great:
|
||||
return 300;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 350;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
default:
|
||||
return base.HealthIncreaseFor(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
new SettingsSlider<double, TimeSlider>
|
||||
{
|
||||
LabelText = "Scroll speed",
|
||||
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime)
|
||||
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||
KeyboardStep = 5
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
if (Tail.AllJudged)
|
||||
{
|
||||
ApplyResult(r => r.Type = HitResult.Perfect);
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
endHold();
|
||||
}
|
||||
|
||||
if (Tail.Result.Type == HitResult.Miss)
|
||||
if (Tail.Judged && !Tail.IsHit)
|
||||
HasBroken = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
/// <summary>
|
||||
/// References the time at which the user started holding the hold note.
|
||||
/// </summary>
|
||||
@@ -73,9 +74,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
var startTime = HoldStartTime?.Invoke();
|
||||
|
||||
if (startTime == null || startTime > HitObject.StartTime)
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
else
|
||||
ApplyResult(r => r.Type = HitResult.Perfect);
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
|
||||
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
public override Replay Generate()
|
||||
{
|
||||
if (Beatmap.HitObjects.Count == 0)
|
||||
return Replay;
|
||||
|
||||
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
|
||||
|
||||
var actions = new List<ManiaAction>();
|
||||
|
||||
@@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
protected override double DefaultAccuracyPortion => 0.95;
|
||||
|
||||
protected override double DefaultComboPortion => 0.05;
|
||||
|
||||
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
|
||||
private Drawable getResult(HitResult result)
|
||||
{
|
||||
if (!hitresult_mapping.ContainsKey(result))
|
||||
return null;
|
||||
|
||||
string filename = this.GetManiaSkinConfig<string>(hitresult_mapping[result])?.Value
|
||||
?? default_hitresult_skin_filenames[result];
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (result.IsHit)
|
||||
hitPolicy.HandleHit(judgedObject);
|
||||
|
||||
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
if (!result.IsHit || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene
|
||||
{
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneObjectObjectSnap : TestSceneOsuEditor
|
||||
{
|
||||
private OsuPlayfield playfield;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestHitCircleSnapsToOtherHitCircle(bool distanceSnapEnabled)
|
||||
{
|
||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
if (!distanceSnapEnabled)
|
||||
AddStep("disable distance snap", () => InputManager.Key(Key.Q));
|
||||
|
||||
AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
|
||||
|
||||
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
|
||||
|
||||
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("both objects at same location", () =>
|
||||
{
|
||||
var objects = EditorBeatmap.HitObjects;
|
||||
|
||||
var first = (OsuHitObject)objects.First();
|
||||
var second = (OsuHitObject)objects.Last();
|
||||
|
||||
return first.Position == second.Position;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleSnapsToSliderEnd()
|
||||
{
|
||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("disable distance snap", () => InputManager.Key(Key.Q));
|
||||
|
||||
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
|
||||
|
||||
AddStep("start slider placement", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0)));
|
||||
|
||||
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0)));
|
||||
|
||||
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("circle is at slider's end", () =>
|
||||
{
|
||||
var objects = EditorBeatmap.HitObjects;
|
||||
|
||||
var first = (Slider)objects.First();
|
||||
var second = (OsuHitObject)objects.Last();
|
||||
|
||||
return Precision.AlmostEquals(first.EndPosition, second.Position);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene
|
||||
{
|
||||
@@ -4,10 +4,10 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneEditor : EditorTestScene
|
||||
public class TestSceneOsuEditor : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
}
|
||||
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestScenePathControlPointVisualiser : OsuTestScene
|
||||
{
|
||||
@@ -14,7 +14,7 @@ using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
@@ -16,7 +16,7 @@ using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
|
||||
{
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene
|
||||
{
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
public TestSceneHitCircle()
|
||||
[Test]
|
||||
public void TestVariousHitCircles()
|
||||
{
|
||||
AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
|
||||
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
|
||||
},
|
||||
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
|
||||
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Autoplay = false,
|
||||
Beatmap = beatmap,
|
||||
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
|
||||
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -209,9 +209,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -252,9 +252,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
|
||||
@@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
public TestSceneSlider()
|
||||
[Test]
|
||||
public void TestVariousSliders()
|
||||
{
|
||||
AddStep("Big Single", () => SetContents(() => testSimpleBig()));
|
||||
AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
|
||||
@@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(239, 176),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
@@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
|
||||
|
||||
private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
|
||||
private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5);
|
||||
|
||||
private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
|
||||
|
||||
private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
|
||||
private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15);
|
||||
|
||||
private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
|
||||
private const double time_offset = 1500;
|
||||
|
||||
private const float max_length = 200;
|
||||
|
||||
private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-(distance / 2), 0),
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(0, -(distance / 2)),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(distance, 0),
|
||||
new Vector2(0, distance),
|
||||
}, distance),
|
||||
RepeatCount = repeats,
|
||||
StackHeight = stackHeight
|
||||
@@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(-max_length / 2, 0),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(200, 200),
|
||||
new Vector2(400, 0)
|
||||
}, 600),
|
||||
new Vector2(max_length / 2, max_length / 2),
|
||||
new Vector2(max_length, 0)
|
||||
}, max_length * 1.5f),
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
@@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(-max_length / 2, 0),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 75),
|
||||
new Vector2(200, 0),
|
||||
new Vector2(300, -200),
|
||||
new Vector2(400, 0),
|
||||
new Vector2(430, 0)
|
||||
new Vector2(max_length * 0.375f, max_length * 0.18f),
|
||||
new Vector2(max_length / 2, 0),
|
||||
new Vector2(max_length * 0.75f, -max_length / 2),
|
||||
new Vector2(max_length * 0.95f, 0),
|
||||
new Vector2(max_length, 0)
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
@@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(-max_length / 2, 0),
|
||||
Path = new SliderPath(PathType.Bezier, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 75),
|
||||
new Vector2(200, 100),
|
||||
new Vector2(300, -200),
|
||||
new Vector2(430, 0)
|
||||
new Vector2(max_length * 0.375f, max_length * 0.18f),
|
||||
new Vector2(max_length / 2, max_length / 4),
|
||||
new Vector2(max_length * 0.75f, -max_length / 2),
|
||||
new Vector2(max_length, 0)
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
@@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(-200, 0),
|
||||
new Vector2(-max_length / 2, 0),
|
||||
new Vector2(0, 0),
|
||||
new Vector2(0, -200),
|
||||
new Vector2(-200, -200),
|
||||
new Vector2(0, -200)
|
||||
new Vector2(0, -max_length / 2),
|
||||
new Vector2(-max_length / 2, -max_length / 2),
|
||||
new Vector2(0, -max_length / 2)
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
@@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-100, 0),
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(-max_length / 4, 0),
|
||||
Path = new SliderPath(PathType.Catmull, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, -50),
|
||||
new Vector2(150, 50),
|
||||
new Vector2(200, 0)
|
||||
new Vector2(max_length * 0.125f, max_length * 0.125f),
|
||||
new Vector2(max_length * 0.375f, max_length * 0.125f),
|
||||
new Vector2(max_length / 2, 0)
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = repeatSamples
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
|
||||
});
|
||||
|
||||
AddAssert("Tracking retained", assertGreatJudge);
|
||||
AddAssert("Tracking retained", assertMaxJudge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
|
||||
});
|
||||
|
||||
AddAssert("Tracking retained", assertGreatJudge);
|
||||
AddAssert("Tracking retained", assertMaxJudge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
|
||||
});
|
||||
|
||||
AddAssert("Tracking retained", assertGreatJudge);
|
||||
AddAssert("Tracking retained", assertMaxJudge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
|
||||
});
|
||||
|
||||
AddAssert("Tracking kept", assertGreatJudge);
|
||||
AddAssert("Tracking kept", assertMaxJudge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -312,13 +312,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
|
||||
}
|
||||
|
||||
private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great);
|
||||
private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
|
||||
|
||||
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
|
||||
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
|
||||
|
||||
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great;
|
||||
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
|
||||
|
||||
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss;
|
||||
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
@@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK;
|
||||
return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
@@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
|
||||
// autoplay replay frames use track time;
|
||||
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
|
||||
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
|
||||
// as real time is what we care about for spinners
|
||||
// (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
|
||||
transformReplay(replay => applyRateAdjustment(replay, rate));
|
||||
AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
|
||||
|
||||
addSeekStep(1000);
|
||||
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
|
||||
|
||||
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
|
||||
protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var positionData = original as IHasPosition;
|
||||
var comboData = original as IHasCombo;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private double accuracy;
|
||||
private int scoreMaxCombo;
|
||||
private int countGreat;
|
||||
private int countGood;
|
||||
private int countOk;
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
accuracy = Score.Accuracy;
|
||||
scoreMaxCombo = Score.MaxCombo;
|
||||
countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
|
||||
countGood = Score.Statistics.GetOrDefault(HitResult.Good);
|
||||
countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
int amountHitObjectsWithAccuracy = countHitCircles;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
else
|
||||
betterAccuracyPercentage = 0;
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return accuracyValue;
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countGood + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countGood + countMeh;
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
private readonly ManualSliderBody body;
|
||||
|
||||
/// <summary>
|
||||
/// Offset in absolute (local) coordinates from the start of the curve.
|
||||
/// </summary>
|
||||
public Vector2 PathStartLocation => body.PathOffset;
|
||||
|
||||
public SliderBodyPiece()
|
||||
{
|
||||
InternalChild = body = new ManualSliderBody
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
|
||||
};
|
||||
|
||||
public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
|
||||
public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
|
||||
@@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
|
||||
Alpha = 0.5f,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
ring = new RingPiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
ring = new RingPiece()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||
/// </summary>
|
||||
private const double editor_hit_object_fade_out_extension = 500;
|
||||
private const double editor_hit_object_fade_out_extension = 700;
|
||||
|
||||
public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
: base(ruleset, beatmap, mods)
|
||||
@@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
switch (state)
|
||||
if (state == ArmedState.Idle)
|
||||
return;
|
||||
|
||||
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
|
||||
switch (hitObject)
|
||||
{
|
||||
case ArmedState.Miss:
|
||||
// Get the existing fade out transform
|
||||
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
|
||||
if (existing == null)
|
||||
return;
|
||||
default:
|
||||
// there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
|
||||
return;
|
||||
|
||||
hitObject.RemoveTransform(existing);
|
||||
case DrawableSlider _:
|
||||
// no specifics to sliders but let them fade slower below.
|
||||
break;
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
|
||||
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
|
||||
case DrawableHitCircle circle: // also handles slider heads
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(editor_hit_object_fade_out_extension)
|
||||
.Expire();
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the existing fade out transform
|
||||
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
|
||||
|
||||
if (existing == null)
|
||||
return;
|
||||
|
||||
hitObject.RemoveTransform(existing);
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
|
||||
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();
|
||||
|
||||
@@ -9,7 +9,9 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -39,21 +42,31 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
new SpinnerCompositionTool()
|
||||
};
|
||||
|
||||
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
|
||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||
|
||||
protected override IEnumerable<BindableBool> Toggles => new[]
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||
{
|
||||
distanceSnapToggle
|
||||
};
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
private Bindable<HitObject> placementObject;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||
|
||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
|
||||
// we may be entering the screen with a selection already active
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)
|
||||
@@ -84,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return snapResult;
|
||||
|
||||
// will be null if distance snap is disabled or not feasible for the current time value.
|
||||
if (distanceSnapGrid == null)
|
||||
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
|
||||
@@ -92,13 +109,57 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
{
|
||||
// check other on-screen objects for snapping/stacking
|
||||
var blueprints = BlueprintContainer.SelectionBlueprints.AliveChildren;
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
float snapRadius =
|
||||
playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
|
||||
playfield.GamefieldToScreenSpace(Vector2.Zero).X;
|
||||
|
||||
foreach (var b in blueprints)
|
||||
{
|
||||
if (b.IsSelected)
|
||||
continue;
|
||||
|
||||
var hitObject = (OsuHitObject)b.HitObject;
|
||||
|
||||
Vector2? snap = checkSnap(hitObject.Position);
|
||||
if (snap == null && hitObject.Position != hitObject.EndPosition)
|
||||
snap = checkSnap(hitObject.EndPosition);
|
||||
|
||||
if (snap != null)
|
||||
{
|
||||
// only return distance portion, since time is not really valid
|
||||
snapResult = new SnapResult(snap.Value, null, playfield);
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2? checkSnap(Vector2 checkPos)
|
||||
{
|
||||
Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
|
||||
|
||||
if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
|
||||
return checkScreenPos;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
snapResult = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateDistanceSnapGrid()
|
||||
{
|
||||
distanceSnapGridContainer.Clear();
|
||||
distanceSnapGridCache.Invalidate();
|
||||
distanceSnapGrid = null;
|
||||
|
||||
if (!distanceSnapToggle.Value)
|
||||
if (distanceSnapToggle.Value != TernaryState.True)
|
||||
return;
|
||||
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// 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 System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
@@ -10,40 +16,219 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuSelectionHandler : SelectionHandler
|
||||
{
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||
base.OnSelectionChanged();
|
||||
|
||||
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||
bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
|
||||
|
||||
SelectionBox.CanRotate = canOperate;
|
||||
SelectionBox.CanScaleX = canOperate;
|
||||
SelectionBox.CanScaleY = canOperate;
|
||||
}
|
||||
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referenceOrigin = null;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
|
||||
moveSelection(moveEvent.InstantDelta);
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
||||
/// </summary>
|
||||
private Vector2? referenceOrigin;
|
||||
|
||||
public override bool HandleFlip(Direction direction)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
|
||||
var centre = selectedObjectsQuad.Centre;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
if (h is Spinner)
|
||||
var pos = h.Position;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
// Spinners don't support position adjustments
|
||||
continue;
|
||||
case Direction.Horizontal:
|
||||
pos.X = centre.X - (pos.X - centre.X);
|
||||
break;
|
||||
|
||||
case Direction.Vertical:
|
||||
pos.Y = centre.Y - (pos.Y - centre.Y);
|
||||
break;
|
||||
}
|
||||
|
||||
// Stacking is not considered
|
||||
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
||||
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
||||
}
|
||||
h.Position = pos;
|
||||
|
||||
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
|
||||
return false;
|
||||
|
||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||
{
|
||||
if (h is Spinner)
|
||||
if (h is Slider slider)
|
||||
{
|
||||
// Spinners don't support position adjustments
|
||||
continue;
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
point.Position.Value = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
h.Position += moveEvent.InstantDelta;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor reference)
|
||||
{
|
||||
adjustScaleFromAnchor(ref scale, reference);
|
||||
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
||||
{
|
||||
Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
||||
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
point.Position.Value *= pathRelativeDeltaScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
// move the selection before scaling if dragging from top or left anchors.
|
||||
if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
|
||||
if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.Position = new Vector2(
|
||||
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
|
||||
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override bool HandleRotation(float delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
referenceOrigin ??= quad.Centre;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (var point in path.Path.ControlPoints)
|
||||
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool moveSelection(Vector2 delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
if (quad.TopLeft.X + delta.X < 0 ||
|
||||
quad.TopLeft.Y + delta.Y < 0 ||
|
||||
quad.BottomRight.X + delta.X > DrawWidth ||
|
||||
quad.BottomRight.Y + delta.Y > DrawHeight)
|
||||
return false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += delta;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
||||
getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
|
||||
{
|
||||
if (!SelectedHitObjects.Any())
|
||||
return new Quad();
|
||||
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||
|
||||
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||
foreach (var p in points)
|
||||
{
|
||||
minPosition = Vector2.ComponentMin(minPosition, p);
|
||||
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
||||
}
|
||||
|
||||
Vector2 size = maxPosition - minPosition;
|
||||
|
||||
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
|
||||
.OfType<OsuHitObject>()
|
||||
.Where(h => !(h is Spinner))
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a point around an arbitrary origin.
|
||||
/// </summary>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <param name="origin">The centre origin to rotate around.</param>
|
||||
/// <param name="angle">The angle to rotate (in degrees).</param>
|
||||
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
||||
{
|
||||
angle = -angle;
|
||||
|
||||
point.X -= origin.X;
|
||||
point.Y -= origin.Y;
|
||||
|
||||
Vector2 ret;
|
||||
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
||||
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
||||
|
||||
ret.X += origin.X;
|
||||
ret.Y += origin.Y;
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
||||
{
|
||||
public class OsuIgnoreJudgement : OsuJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override int NumericResultFor(HitResult result) => 0;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result) => 0;
|
||||
public override HitResult MaxResult => HitResult.IgnoreHit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,5 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
||||
public class OsuJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.Great;
|
||||
|
||||
protected override int NumericResultFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 50;
|
||||
|
||||
case HitResult.Good:
|
||||
return 100;
|
||||
|
||||
case HitResult.Great:
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
base.ApplyToDrawableHitObjects(drawables);
|
||||
}
|
||||
|
||||
private double lastSliderHeadFadeOutStartTime;
|
||||
private double lastSliderHeadFadeOutDuration;
|
||||
|
||||
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
if (!(drawable is DrawableOsuHitObject d))
|
||||
@@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableSliderTail sliderTail:
|
||||
// use stored values from head circle to achieve same fade sequence.
|
||||
fadeOutDuration = lastSliderHeadFadeOutDuration;
|
||||
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
|
||||
|
||||
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
||||
sliderTail.FadeOut(fadeOutDuration);
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSliderRepeat sliderRepeat:
|
||||
// use stored values from head circle to achieve same fade sequence.
|
||||
fadeOutDuration = lastSliderHeadFadeOutDuration;
|
||||
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
|
||||
|
||||
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
||||
// only apply to circle piece – reverse arrow is not affected by hidden.
|
||||
sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
|
||||
|
||||
break;
|
||||
|
||||
case DrawableHitCircle circle:
|
||||
|
||||
if (circle is DrawableSliderHead)
|
||||
{
|
||||
lastSliderHeadFadeOutDuration = fadeOutDuration;
|
||||
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
|
||||
}
|
||||
|
||||
// we don't want to see the approach circle
|
||||
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
||||
circle.ApproachCircle.Hide();
|
||||
|
||||
@@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
private void addConnection(FollowPointConnection connection)
|
||||
{
|
||||
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) =>
|
||||
{
|
||||
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
|
||||
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
// we always want to insert the new item after equal ones.
|
||||
// this is important for beatmaps with multiple hitobjects at the same point in time.
|
||||
// if we use standard comparison insert order, there will be a churn of connections getting re-updated to
|
||||
// the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
|
||||
// this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
|
||||
return -1;
|
||||
}));
|
||||
|
||||
if (index < connections.Count - 1)
|
||||
{
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
var circleResult = (OsuHitCircleJudgementResult)r;
|
||||
|
||||
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
|
||||
if (result != HitResult.Miss)
|
||||
if (result.IsHit())
|
||||
{
|
||||
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
|
||||
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
|
||||
|
||||