mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 11:42:56 +08:00
Merge branch 'master' into colourise-control-point-table
This commit is contained in:
commit
783a463772
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.930.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();
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (!result.Type.AffectsCombo() || !result.HasResult)
|
||||
return;
|
||||
|
||||
if (result.Type == HitResult.Miss)
|
||||
if (!result.IsHit)
|
||||
{
|
||||
updateCombo(0, null);
|
||||
return;
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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));
|
||||
|
@ -2,10 +2,40 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class ManiaJudgement : Judgement
|
||||
{
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Meh:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.Ok:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
|
||||
|
||||
case HitResult.Good:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Great:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
default:
|
||||
return base.HealthIncreaseFor(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
|
||||
|
||||
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss;
|
||||
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
|
||||
|
||||
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit;
|
||||
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
|
||||
|
||||
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.IgnoreMiss;
|
||||
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> 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);
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (JudgedObject != null)
|
||||
{
|
||||
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
|
||||
lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
|
||||
lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
Ball = new SliderBall(s, this)
|
||||
@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Alpha = 0
|
||||
},
|
||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
};
|
||||
}
|
||||
|
||||
@ -110,6 +109,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public override void StopAllSamples()
|
||||
{
|
||||
base.StopAllSamples();
|
||||
slidingSample?.Stop();
|
||||
}
|
||||
|
||||
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
@ -244,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
||||
// this can only be done after we stop using LegacyLastTick.
|
||||
if (TailCircle.Result.Type != HitResult.Miss)
|
||||
if (TailCircle.IsHit)
|
||||
base.PlaySamples();
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,11 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private readonly Drawable scaleContainer;
|
||||
|
||||
public readonly Drawable CirclePiece;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
|
||||
@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = scaleContainer = new ReverseArrowPiece();
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
// no default for this; only visible in legacy skins.
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
|
||||
arrow = new ReverseArrowPiece(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
||||
@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private bool hasRotation;
|
||||
|
||||
private readonly ReverseArrowPiece arrow;
|
||||
|
||||
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
|
||||
{
|
||||
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
|
||||
@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
|
||||
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
while (Math.Abs(aimRotation - Rotation) > 180)
|
||||
aimRotation += aimRotation < Rotation ? 360 : -360;
|
||||
while (Math.Abs(aimRotation - arrow.Rotation) > 180)
|
||||
aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
|
||||
|
||||
if (!hasRotation)
|
||||
{
|
||||
Rotation = aimRotation;
|
||||
arrow.Rotation = aimRotation;
|
||||
hasRotation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
|
||||
Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||
arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +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 System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
|
||||
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
|
||||
{
|
||||
private readonly Slider slider;
|
||||
private readonly SliderTailCircle tailCircle;
|
||||
|
||||
/// <summary>
|
||||
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
|
||||
@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
||||
|
||||
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
|
||||
: base(hitCircle)
|
||||
private readonly SkinnableDrawable circlePiece;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
|
||||
public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
|
||||
: base(tailCircle)
|
||||
{
|
||||
this.slider = slider;
|
||||
|
||||
this.tailCircle = tailCircle;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
FillMode = FillMode.Fit;
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
AlwaysPresent = true;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// no default for this; only visible in legacy skins.
|
||||
circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
positionBindable.BindTo(hitCircle.PositionBindable);
|
||||
pathVersion.BindTo(slider.Path.Version);
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
|
||||
scaleBindable.BindTo(HitObject.ScaleBindable);
|
||||
}
|
||||
|
||||
positionBindable.BindValueChanged(_ => updatePosition());
|
||||
pathVersion.BindValueChanged(_ => updatePosition(), true);
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// TODO: This has no drawable content. Support for skins should be added.
|
||||
circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateStateTransforms(state);
|
||||
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
this.Delay(HitObject.TimePreempt).FadeOut(500);
|
||||
|
||||
Expire(true);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.FadeOut(100);
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
// todo: temporary / arbitrary
|
||||
this.Delay(800).FadeOut();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
private void updatePosition() => Position = HitObject.Position - slider.Position;
|
||||
public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
|
||||
Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public override void StopAllSamples()
|
||||
{
|
||||
base.StopAllSamples();
|
||||
spinningSample?.Stop();
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
@ -206,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return;
|
||||
|
||||
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
|
||||
foreach (var tick in ticks.Where(t => !t.IsHit))
|
||||
foreach (var tick in ticks.Where(t => !t.Result.HasResult))
|
||||
tick.TriggerResult(false);
|
||||
|
||||
ApplyResult(r =>
|
||||
@ -218,7 +224,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
else if (Progress > .75)
|
||||
r.Type = HitResult.Meh;
|
||||
else if (Time.Current >= Spinner.EndTime)
|
||||
r.Type = HitResult.Miss;
|
||||
r.Type = r.Judgement.MinResult;
|
||||
});
|
||||
}
|
||||
|
||||
@ -262,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
while (wholeSpins != spins)
|
||||
{
|
||||
var tick = ticks.FirstOrDefault(t => !t.IsHit);
|
||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||
|
||||
// tick may be null if we've hit the spin limit.
|
||||
if (tick != null)
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
private void load(TextureStore textures, DrawableHitObject drawableHitObject)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Origin = Anchor.Centre,
|
||||
Texture = textures.Get(@"Gameplay/osu/disc"),
|
||||
},
|
||||
new TrianglesPiece
|
||||
new TrianglesPiece((int)drawableHitObject.HitObject.StartTime)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
private Spinner spinner;
|
||||
|
||||
private const float initial_scale = 1.3f;
|
||||
private const float idle_alpha = 0.2f;
|
||||
private const float tracking_alpha = 0.4f;
|
||||
|
||||
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
// we are slightly bigger than our parent, to clip the top and bottom of the circle
|
||||
// this should probably be revisited when scaled spinners are a thing.
|
||||
Scale = new Vector2(1.3f);
|
||||
Scale = new Vector2(initial_scale);
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
@ -93,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -115,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
|
||||
}
|
||||
|
||||
const float initial_scale = 0.2f;
|
||||
float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
|
||||
const float initial_fill_scale = 0.2f;
|
||||
float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress;
|
||||
|
||||
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
|
||||
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
|
||||
@ -124,41 +127,57 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
centre.ScaleTo(0);
|
||||
mainContainer.ScaleTo(0);
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
{
|
||||
// constant ambient rotation to give the spinner "spinning" character.
|
||||
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
|
||||
|
||||
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
this.ScaleTo(initial_scale);
|
||||
this.RotateTo(0);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
|
||||
{
|
||||
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
// constant ambient rotation to give the spinner "spinning" character.
|
||||
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
|
||||
this.RotateTo(mainContainer.Rotation + 180, 320);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
{
|
||||
centre.ScaleTo(0);
|
||||
mainContainer.ScaleTo(0);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
|
||||
{
|
||||
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
|
||||
{
|
||||
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
|
||||
updateComplete(state == ArmedState.Hit, 0);
|
||||
|
||||
using (BeginDelayedSequence(spinner.Duration, true))
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
|
||||
this.RotateTo(mainContainer.Rotation + 180, 320);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
|
||||
break;
|
||||
}
|
||||
}
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
updateComplete(state == ArmedState.Hit, 0);
|
||||
}
|
||||
|
||||
private void updateComplete(bool complete, double duration)
|
||||
|
@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
protected override bool CreateNewTriangles => false;
|
||||
protected override float SpawnRatio => 0.5f;
|
||||
|
||||
public TrianglesPiece()
|
||||
public TrianglesPiece(int? seed = null)
|
||||
: base(seed)
|
||||
{
|
||||
TriangleScale = 1.2f;
|
||||
HideAlphaDiscrepancies = false;
|
||||
|
@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
// if this is to change, we should revisit this.
|
||||
AddNested(TailCircle = new SliderTailCircle(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = e.Time,
|
||||
Position = EndPosition,
|
||||
StackHeight = StackHeight
|
||||
@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
break;
|
||||
|
||||
case SliderEventType.Repeat:
|
||||
AddNested(new SliderRepeat
|
||||
AddNested(new SliderRepeat(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
SpanDuration = SpanDuration,
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
|
@ -1,9 +0,0 @@
|
||||
// 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.Osu.Objects
|
||||
{
|
||||
public class SliderCircle : HitCircle
|
||||
{
|
||||
}
|
||||
}
|
50
osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
Normal file
50
osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
Normal file
@ -0,0 +1,50 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// A hit circle which is at the end of a slider path (either repeat or final tail).
|
||||
/// </summary>
|
||||
public abstract class SliderEndCircle : HitCircle
|
||||
{
|
||||
private readonly Slider slider;
|
||||
|
||||
protected SliderEndCircle(Slider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
}
|
||||
|
||||
public int RepeatIndex { get; set; }
|
||||
|
||||
public double SpanDuration => slider.SpanDuration;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
if (RepeatIndex > 0)
|
||||
{
|
||||
// Repeat points after the first span should appear behind the still-visible one.
|
||||
TimeFadeIn = 0;
|
||||
|
||||
// The next end circle should appear exactly after the previous circle (on the same end) is hit.
|
||||
TimePreempt = SpanDuration * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// taken from osu-stable
|
||||
const float first_end_circle_preempt_adjust = 2 / 3f;
|
||||
|
||||
// The first end circle should fade in with the slider.
|
||||
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
|
||||
}
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
}
|
@ -1,35 +1,19 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SliderRepeat : OsuHitObject
|
||||
public class SliderRepeat : SliderEndCircle
|
||||
{
|
||||
public int RepeatIndex { get; set; }
|
||||
public double SpanDuration { get; set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
public SliderRepeat(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
// Out preempt should be one span early to give the user ample warning.
|
||||
TimePreempt += SpanDuration;
|
||||
|
||||
// We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
|
||||
// we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
|
||||
if (RepeatIndex > 0)
|
||||
TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
|
||||
|
||||
public class SliderRepeatJudgement : OsuJudgement
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Bindables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
@ -13,23 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// Note that this should not be used for timing correctness.
|
||||
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
||||
/// </summary>
|
||||
public class SliderTailCircle : SliderCircle
|
||||
public class SliderTailCircle : SliderEndCircle
|
||||
{
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
|
||||
public SliderTailCircle(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
pathVersion.BindTo(slider.Path.Version);
|
||||
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public override Judgement CreateJudgement() => new SliderTailJudgement();
|
||||
|
||||
public class SliderTailJudgement : OsuJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.IgnoreHit;
|
||||
public override HitResult MaxResult => HitResult.SmallTickHit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
ReverseArrow,
|
||||
HitCircleText,
|
||||
SliderHeadHitCircle,
|
||||
SliderTailHitCircle,
|
||||
SliderFollowCircle,
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
|
@ -1,9 +1,12 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
private bool disjointTrail;
|
||||
private double lastTrailTime;
|
||||
private IBindable<float> cursorSize;
|
||||
|
||||
public LegacyCursorTrail()
|
||||
{
|
||||
@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
private void load(ISkinSource skin, OsuConfigManager config)
|
||||
{
|
||||
Texture = skin.GetTexture("cursortrail");
|
||||
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
||||
@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||
Texture.ScaleAdjust *= 1.6f;
|
||||
}
|
||||
|
||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
||||
|
||||
protected override bool InterpolateMovements => !disjointTrail;
|
||||
|
||||
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
if (!disjointTrail)
|
||||
|
@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
public class LegacyMainCirclePiece : CompositeDrawable
|
||||
{
|
||||
private readonly string priorityLookup;
|
||||
private readonly bool hasNumber;
|
||||
|
||||
public LegacyMainCirclePiece(string priorityLookup = null)
|
||||
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
|
||||
{
|
||||
this.priorityLookup = priorityLookup;
|
||||
this.hasNumber = hasNumber;
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
}
|
||||
@ -70,7 +72,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
}
|
||||
},
|
||||
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||
};
|
||||
|
||||
if (hasNumber)
|
||||
{
|
||||
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Numeric.With(size: 40),
|
||||
UseFullGlyphHeight = false,
|
||||
@ -78,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||
|
||||
@ -107,7 +113,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
state.BindValueChanged(updateState, true);
|
||||
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
|
||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||
if (hasNumber)
|
||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||
}
|
||||
|
||||
private void updateState(ValueChangedEvent<ArmedState> state)
|
||||
@ -120,16 +127,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
|
||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
|
||||
else
|
||||
if (hasNumber)
|
||||
{
|
||||
// old skins scale and fade it normally along other pieces.
|
||||
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
|
||||
else
|
||||
{
|
||||
// old skins scale and fade it normally along other pieces.
|
||||
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -70,20 +70,30 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
this.FadeOut();
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
var spinner = (Spinner)drawableSpinner.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
this.FadeOut();
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||
|
||||
fixedMiddle.FadeColour(Color4.White);
|
||||
using (BeginAbsoluteSequence(spinner.StartTime, true))
|
||||
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
{
|
||||
fixedMiddle.FadeColour(Color4.White);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt, true))
|
||||
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -24,12 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
private Sprite metreSprite;
|
||||
private Container metre;
|
||||
|
||||
private bool spinnerBlink;
|
||||
|
||||
private const float sprite_scale = 1 / 1.6f;
|
||||
private const float final_metre_height = 692 * sprite_scale;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource source, DrawableHitObject drawableObject)
|
||||
{
|
||||
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
|
||||
|
||||
drawableSpinner = (DrawableSpinner)drawableObject;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -84,14 +88,20 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
this.FadeOut();
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
var spinner = drawableSpinner.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
|
||||
this.FadeOut();
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||
}
|
||||
@ -116,12 +126,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
private float getMetreHeight(float progress)
|
||||
{
|
||||
progress = Math.Min(99, progress * 100);
|
||||
progress *= 100;
|
||||
|
||||
// the spinner should still blink at 100% progress.
|
||||
if (spinnerBlink)
|
||||
progress = Math.Min(99, progress);
|
||||
|
||||
int barCount = (int)progress / 10;
|
||||
|
||||
// todo: add SpinnerNoBlink support
|
||||
if (RNG.NextBool(((int)progress % 10) / 10f))
|
||||
if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f))
|
||||
barCount++;
|
||||
|
||||
return (float)barCount / total_bars * final_metre_height;
|
||||
|
@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.SliderTailHitCircle:
|
||||
if (hasHitCircle.Value)
|
||||
return new LegacyMainCirclePiece("sliderendcircle", false);
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.SliderHeadHitCircle:
|
||||
if (hasHitCircle.Value)
|
||||
return new LegacyMainCirclePiece("sliderstartcircle");
|
||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
CursorRotate,
|
||||
HitCircleOverlayAboveNumber,
|
||||
HitCircleOverlayAboveNumer, // Some old skins will have this typo
|
||||
SpinnerFrequencyModulate
|
||||
SpinnerFrequencyModulate,
|
||||
SpinnerNoBlink
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
/// </summary>
|
||||
protected virtual bool InterpolateMovements => true;
|
||||
|
||||
protected virtual float IntervalMultiplier => 1.0f;
|
||||
|
||||
private Vector2? lastPosition;
|
||||
private readonly InputResampler resampler = new InputResampler();
|
||||
|
||||
@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
float distance = diff.Length;
|
||||
Vector2 direction = diff / distance;
|
||||
|
||||
float interval = partSize.X / 2.5f;
|
||||
float interval = partSize.X / 2.5f * IntervalMultiplier;
|
||||
|
||||
for (float d = interval; d < distance; d += interval)
|
||||
{
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
private readonly Random rng = new Random(1337);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestVariousHits()
|
||||
{
|
||||
AddStep("Hit", () => addHitJudgement(false));
|
||||
AddStep("Strong hit", () => addStrongHitJudgement(false));
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
case HitResult.SmallTickHit:
|
||||
return 0.15;
|
||||
|
||||
default:
|
||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!(obj is DrawableDrumRollTick))
|
||||
return;
|
||||
|
||||
if (result.Type > HitResult.Miss)
|
||||
if (result.IsHit)
|
||||
rollingHits++;
|
||||
else
|
||||
rollingHits--;
|
||||
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok);
|
||||
}
|
||||
else
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(ArmedState state)
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
return;
|
||||
|
||||
if (!validActionPressed)
|
||||
ApplyResult(r => r.Type = HitResult.Miss);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
else
|
||||
ApplyResult(r => r.Type = result);
|
||||
}
|
||||
|
@ -211,9 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
tick.TriggerResult(false);
|
||||
}
|
||||
|
||||
var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss;
|
||||
|
||||
ApplyResult(r => r.Type = hitResult);
|
||||
ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
private double hpMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// HP multiplier for a <see cref="HitResult.Miss"/>.
|
||||
/// HP multiplier for a <see cref="HitResult"/> that does not satisfy <see cref="HitResultExtensions.IsHit"/>.
|
||||
/// </summary>
|
||||
private double hpMissMultiplier;
|
||||
|
||||
@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(JudgementResult result)
|
||||
=> base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier);
|
||||
=> base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
if (r?.Type.AffectsCombo() == false)
|
||||
return;
|
||||
|
||||
passing = r == null || r.Type > HitResult.Miss;
|
||||
passing = r == null || r.IsHit;
|
||||
|
||||
foreach (var sprite in InternalChildren.OfType<ScrollerSprite>())
|
||||
sprite.Passing = passing;
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Alpha = 0.15f;
|
||||
Masking = true;
|
||||
|
||||
if (result == HitResult.Miss)
|
||||
if (!result.IsHit())
|
||||
return;
|
||||
|
||||
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
|
||||
|
@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
target = centreHit;
|
||||
back = centre;
|
||||
|
||||
if (gameplayClock?.IsSeeking != true)
|
||||
drumSample.Centre?.Play();
|
||||
drumSample.Centre?.Play();
|
||||
}
|
||||
else if (action == RimAction)
|
||||
{
|
||||
target = rimHit;
|
||||
back = rim;
|
||||
|
||||
if (gameplayClock?.IsSeeking != true)
|
||||
drumSample.Rim?.Play();
|
||||
drumSample.Rim?.Play();
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
|
@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing
|
||||
[TestFixture]
|
||||
public class EditorChangeHandlerTest
|
||||
{
|
||||
private int stateChangedFired;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
stateChangedFired = 0;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSaveRestoreState()
|
||||
{
|
||||
@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing
|
||||
addArbitraryChange(beatmap);
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
|
||||
@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.True);
|
||||
|
||||
Assert.That(stateChangedFired, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
||||
|
||||
string hash = handler.CurrentStateHash;
|
||||
|
||||
@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
|
||||
Assert.That(stateChangedFired, Is.EqualTo(1));
|
||||
|
||||
handler.RestoreState(-1);
|
||||
|
||||
@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing
|
||||
// we should only be able to restore once even though we saved twice.
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.True);
|
||||
Assert.That(stateChangedFired, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
{
|
||||
Assert.That(stateChangedFired, Is.EqualTo(i));
|
||||
|
||||
addArbitraryChange(beatmap);
|
||||
handler.SaveState();
|
||||
}
|
||||
@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
var beatmap = new EditorBeatmap(new Beatmap());
|
||||
|
||||
return (new EditorChangeHandler(beatmap), beatmap);
|
||||
var changeHandler = new EditorChangeHandler(beatmap);
|
||||
|
||||
changeHandler.OnStateChange += () => stateChangedFired++;
|
||||
return (changeHandler, beatmap);
|
||||
}
|
||||
|
||||
private void addArbitraryChange(EditorBeatmap beatmap)
|
||||
|
@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveGroupAlsoRemovedControlPoints()
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
|
||||
var group = cpi.GroupAt(1000, true);
|
||||
|
||||
group.Add(new SampleControlPoint());
|
||||
|
||||
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
|
||||
|
||||
cpi.RemoveGroup(group);
|
||||
|
||||
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddControlPointToGroup()
|
||||
{
|
||||
|
39
osu.Game.Tests/NonVisual/GameplayClockTest.cs
Normal file
39
osu.Game.Tests/NonVisual/GameplayClockTest.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class GameplayClockTest
|
||||
{
|
||||
[TestCase(0)]
|
||||
[TestCase(1)]
|
||||
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
|
||||
{
|
||||
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
|
||||
var gameplayClock = new TestGameplayClock(framedClock);
|
||||
|
||||
gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble());
|
||||
|
||||
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private class TestGameplayClock : GameplayClock
|
||||
{
|
||||
public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>();
|
||||
|
||||
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
|
||||
|
||||
public TestGameplayClock(IFrameBasedClock underlyingClock)
|
||||
: base(underlyingClock)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
Normal file
69
osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// 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.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public class TestSceneComposeSelectBox : OsuTestScene
|
||||
{
|
||||
private Container selectionArea;
|
||||
|
||||
public TestSceneComposeSelectBox()
|
||||
{
|
||||
SelectionBox selectionBox = null;
|
||||
|
||||
AddStep("create box", () =>
|
||||
Child = selectionArea = new Container
|
||||
{
|
||||
Size = new Vector2(400),
|
||||
Position = -new Vector2(150),
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
selectionBox = new SelectionBox
|
||||
{
|
||||
CanRotate = true,
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
|
||||
OnRotation = handleRotation,
|
||||
OnScale = handleScale
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state);
|
||||
AddToggleStep("toggle x", state => selectionBox.CanScaleX = state);
|
||||
AddToggleStep("toggle y", state => selectionBox.CanScaleY = state);
|
||||
}
|
||||
|
||||
private void handleScale(Vector2 amount, Anchor reference)
|
||||
{
|
||||
if ((reference & Anchor.y1) == 0)
|
||||
{
|
||||
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
|
||||
if (directionY < 0)
|
||||
selectionArea.Y += amount.Y;
|
||||
selectionArea.Height += directionY * amount.Y;
|
||||
}
|
||||
|
||||
if ((reference & Anchor.x1) == 0)
|
||||
{
|
||||
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
|
||||
if (directionX < 0)
|
||||
selectionArea.X += amount.X;
|
||||
selectionArea.Width += directionX * amount.X;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRotation(float angle)
|
||||
{
|
||||
// kinda silly and wrong, but just showing that the drag handles work.
|
||||
selectionArea.Rotation += angle;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
|
||||
@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
|
||||
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitWithoutSave()
|
||||
{
|
||||
AddStep("exit without save", () => Editor.Exit());
|
||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -0,0 +1,50 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public class TestSceneEditorSamplePlayback : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestSlidingSampleStopsOnSeek()
|
||||
{
|
||||
DrawableSlider slider = null;
|
||||
DrawableSample[] loopingSamples = null;
|
||||
DrawableSample[] onceOffSamples = null;
|
||||
|
||||
AddStep("get first slider", () =>
|
||||
{
|
||||
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||
onceOffSamples = slider.ChildrenOfType<DrawableSample>().Where(s => !s.Looping).ToArray();
|
||||
loopingSamples = slider.ChildrenOfType<DrawableSample>().Where(s => s.Looping).ToArray();
|
||||
});
|
||||
|
||||
AddStep("start playback", () => EditorClock.Start());
|
||||
|
||||
AddUntilStep("wait for slider sliding then seek", () =>
|
||||
{
|
||||
if (!slider.Tracking.Value)
|
||||
return false;
|
||||
|
||||
if (!loopingSamples.Any(s => s.Playing))
|
||||
return false;
|
||||
|
||||
EditorClock.Seek(20000);
|
||||
return true;
|
||||
});
|
||||
|
||||
AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing));
|
||||
AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing));
|
||||
}
|
||||
}
|
||||
}
|
@ -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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneGameplaySamplePlayback : PlayerTestScene
|
||||
{
|
||||
[Test]
|
||||
[Ignore("temporarily disabled pending investigation")]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
DrawableSlider slider = null;
|
||||
DrawableSample[] samples = null;
|
||||
ISamplePlaybackDisabler gameplayClock = null;
|
||||
|
||||
AddStep("get variables", () =>
|
||||
{
|
||||
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
|
||||
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for slider sliding then seek", () =>
|
||||
{
|
||||
if (!slider.Tracking.Value)
|
||||
return false;
|
||||
|
||||
if (!samples.Any(s => s.Playing))
|
||||
return false;
|
||||
|
||||
Player.ChildrenOfType<GameplayClockContainer>().First().Seek(40000);
|
||||
return true;
|
||||
});
|
||||
|
||||
AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
|
||||
|
||||
// because we are in frame stable context, it's quite likely that not all samples are "played" at this point.
|
||||
// the important thing is that at least one started, and that sample has since stopped.
|
||||
AddAssert("no samples are playing", () => Player.ChildrenOfType<PausableSkinnableSound>().All(s => !s.IsPlaying));
|
||||
|
||||
AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
|
||||
|
||||
AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value);
|
||||
AddUntilStep("any sample is playing", () => Player.ChildrenOfType<PausableSkinnableSound>().Any(s => s.IsPlaying));
|
||||
}
|
||||
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
base.Bindable = new Bindable<string>();
|
||||
|
||||
((OsuTextBox)Control).OnCommit = (sender, newText) =>
|
||||
((OsuTextBox)Control).OnCommit += (sender, newText) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -114,6 +114,25 @@ namespace osu.Game.Beatmaps
|
||||
return computeDifficulty(key, beatmapInfo, rulesetInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="DifficultyRating"/> that describes a star rating.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties
|
||||
/// </remarks>
|
||||
/// <param name="starRating">The star rating.</param>
|
||||
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
|
||||
public static DifficultyRating GetDifficultyRating(double starRating)
|
||||
{
|
||||
if (starRating < 2.0) return DifficultyRating.Easy;
|
||||
if (starRating < 2.7) return DifficultyRating.Normal;
|
||||
if (starRating < 4.0) return DifficultyRating.Hard;
|
||||
if (starRating < 5.3) return DifficultyRating.Insane;
|
||||
if (starRating < 6.5) return DifficultyRating.Expert;
|
||||
|
||||
return DifficultyRating.ExpertPlus;
|
||||
}
|
||||
|
||||
private CancellationTokenSource trackedUpdateCancellationSource;
|
||||
private readonly List<CancellationTokenSource> linkedCancellationSources = new List<CancellationTokenSource>();
|
||||
|
||||
@ -307,5 +326,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
|
||||
}
|
||||
|
||||
public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars);
|
||||
}
|
||||
}
|
||||
|
@ -135,21 +135,7 @@ namespace osu.Game.Beatmaps
|
||||
public List<ScoreInfo> Scores { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DifficultyRating DifficultyRating
|
||||
{
|
||||
get
|
||||
{
|
||||
var rating = StarDifficulty;
|
||||
|
||||
if (rating < 2.0) return DifficultyRating.Easy;
|
||||
if (rating < 2.7) return DifficultyRating.Normal;
|
||||
if (rating < 4.0) return DifficultyRating.Hard;
|
||||
if (rating < 5.3) return DifficultyRating.Insane;
|
||||
if (rating < 6.5) return DifficultyRating.Expert;
|
||||
|
||||
return DifficultyRating.ExpertPlus;
|
||||
}
|
||||
}
|
||||
public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty);
|
||||
|
||||
public string[] SearchableTerms => new[]
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public readonly WorkingBeatmap DefaultBeatmap;
|
||||
|
||||
public override string[] HandledExtensions => new[] { ".osz" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
|
@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public void RemoveGroup(ControlPointGroup group)
|
||||
{
|
||||
foreach (var item in group.ControlPoints.ToArray())
|
||||
group.Remove(item);
|
||||
|
||||
group.ItemAdded -= groupItemAdded;
|
||||
group.ItemRemoved -= groupItemRemoved;
|
||||
|
||||
|
@ -2,7 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -14,6 +18,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -21,9 +26,6 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip
|
||||
{
|
||||
private readonly BeatmapInfo beatmap;
|
||||
private readonly RulesetInfo ruleset;
|
||||
|
||||
private readonly Container iconContainer;
|
||||
|
||||
/// <summary>
|
||||
@ -35,23 +37,49 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
set => iconContainer.Size = value;
|
||||
}
|
||||
|
||||
public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null, bool shouldShowTooltip = true)
|
||||
[NotNull]
|
||||
private readonly BeatmapInfo beatmap;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly RulesetInfo ruleset;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
|
||||
private readonly bool shouldShowTooltip;
|
||||
private readonly IBindable<StarDifficulty> difficultyBindable = new Bindable<StarDifficulty>();
|
||||
|
||||
private Drawable background;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DifficultyIcon"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to show the difficulty of.</param>
|
||||
/// <param name="ruleset">The ruleset to show the difficulty with.</param>
|
||||
/// <param name="mods">The mods to show the difficulty with.</param>
|
||||
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
|
||||
public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true)
|
||||
: this(beatmap, shouldShowTooltip)
|
||||
{
|
||||
this.ruleset = ruleset ?? beatmap.Ruleset;
|
||||
this.mods = mods ?? Array.Empty<Mod>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DifficultyIcon"/> that follows the currently-selected ruleset and mods.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to show the difficulty of.</param>
|
||||
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
|
||||
public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true)
|
||||
{
|
||||
this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap));
|
||||
|
||||
this.ruleset = ruleset ?? beatmap.Ruleset;
|
||||
if (shouldShowTooltip)
|
||||
TooltipContent = beatmap;
|
||||
this.shouldShowTooltip = shouldShowTooltip;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
|
||||
}
|
||||
|
||||
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
|
||||
|
||||
public object TooltipContent { get; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -70,10 +98,10 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 5,
|
||||
},
|
||||
Child = new Box
|
||||
Child = background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.ForDifficultyRating(beatmap.DifficultyRating),
|
||||
Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) // Default value that will be re-populated once difficulty calculation completes
|
||||
},
|
||||
},
|
||||
new ConstrainedIconContainer
|
||||
@ -82,16 +110,73 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
// the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment)
|
||||
Icon = ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
|
||||
}
|
||||
Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
|
||||
},
|
||||
new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0),
|
||||
};
|
||||
|
||||
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating));
|
||||
}
|
||||
|
||||
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
|
||||
|
||||
public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null;
|
||||
|
||||
private class DifficultyRetriever : Component
|
||||
{
|
||||
public readonly Bindable<StarDifficulty> StarDifficulty = new Bindable<StarDifficulty>();
|
||||
|
||||
private readonly BeatmapInfo beatmap;
|
||||
private readonly RulesetInfo ruleset;
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
|
||||
private CancellationTokenSource difficultyCancellation;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyManager difficultyManager { get; set; }
|
||||
|
||||
public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.ruleset = ruleset;
|
||||
this.mods = mods;
|
||||
}
|
||||
|
||||
private IBindable<StarDifficulty> localStarDifficulty;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
difficultyCancellation = new CancellationTokenSource();
|
||||
localStarDifficulty = ruleset != null
|
||||
? difficultyManager.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token)
|
||||
: difficultyManager.GetBindableDifficulty(beatmap, difficultyCancellation.Token);
|
||||
localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
difficultyCancellation?.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private class DifficultyIconTooltipContent
|
||||
{
|
||||
public readonly BeatmapInfo Beatmap;
|
||||
public readonly IBindable<StarDifficulty> Difficulty;
|
||||
|
||||
public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable<StarDifficulty> difficulty)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
Difficulty = difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
private class DifficultyIconTooltip : VisibilityContainer, ITooltip
|
||||
{
|
||||
private readonly OsuSpriteText difficultyName, starRating;
|
||||
private readonly Box background;
|
||||
|
||||
private readonly FillFlowContainer difficultyFlow;
|
||||
|
||||
public DifficultyIconTooltip()
|
||||
@ -159,14 +244,22 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
background.Colour = colours.Gray3;
|
||||
}
|
||||
|
||||
private readonly IBindable<StarDifficulty> starDifficulty = new Bindable<StarDifficulty>();
|
||||
|
||||
public bool SetContent(object content)
|
||||
{
|
||||
if (!(content is BeatmapInfo beatmap))
|
||||
if (!(content is DifficultyIconTooltipContent iconContent))
|
||||
return false;
|
||||
|
||||
difficultyName.Text = beatmap.Version;
|
||||
starRating.Text = $"{beatmap.StarDifficulty:0.##}";
|
||||
difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating, true);
|
||||
difficultyName.Text = iconContent.Beatmap.Version;
|
||||
|
||||
starDifficulty.UnbindAll();
|
||||
starDifficulty.BindTo(iconContent.Difficulty);
|
||||
starDifficulty.BindValueChanged(difficulty =>
|
||||
{
|
||||
starRating.Text = $"{difficulty.NewValue.Stars:0.##}";
|
||||
difficultyFlow.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating, true);
|
||||
}, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
public class GroupedDifficultyIcon : DifficultyIcon
|
||||
{
|
||||
public GroupedDifficultyIcon(List<BeatmapInfo> beatmaps, RulesetInfo ruleset, Color4 counterColour)
|
||||
: base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, false)
|
||||
: base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, null, false)
|
||||
{
|
||||
AddInternal(new OsuSpriteText
|
||||
{
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Database
|
||||
|
||||
private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>();
|
||||
|
||||
public virtual string[] HandledExtensions => new[] { ".zip" };
|
||||
public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
|
||||
|
||||
public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Database
|
||||
@ -19,6 +20,6 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// An array of accepted file extensions (in the standard format of ".abc").
|
||||
/// </summary>
|
||||
string[] HandledExtensions { get; }
|
||||
IEnumerable<string> HandledExtensions { get; }
|
||||
}
|
||||
}
|
||||
|
@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// </summary>
|
||||
public float Velocity = 1;
|
||||
|
||||
private readonly Random stableRandom;
|
||||
|
||||
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
|
||||
|
||||
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
|
||||
|
||||
private IShader shader;
|
||||
private readonly Texture texture;
|
||||
|
||||
public Triangles()
|
||||
/// <summary>
|
||||
/// Construct a new triangle visualisation.
|
||||
/// </summary>
|
||||
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
|
||||
public Triangles(int? seed = null)
|
||||
{
|
||||
if (seed != null)
|
||||
stableRandom = new Random(seed.Value);
|
||||
|
||||
texture = Texture.WhitePixel;
|
||||
}
|
||||
|
||||
@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
TriangleParticle particle = CreateTriangle();
|
||||
|
||||
particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1);
|
||||
particle.ColourShade = RNG.NextSingle();
|
||||
particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1);
|
||||
particle.ColourShade = nextRandom();
|
||||
particle.Colour = CreateTriangleShade(particle.ColourShade);
|
||||
|
||||
return particle;
|
||||
@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
const float std_dev = 0.16f;
|
||||
const float mean = 0.5f;
|
||||
|
||||
float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats
|
||||
float u2 = 1 - RNG.NextSingle();
|
||||
float u1 = 1 - nextRandom(); //uniform(0,1] random floats
|
||||
float u2 = 1 - nextRandom();
|
||||
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
|
||||
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)
|
||||
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
Rank = Rank,
|
||||
Ruleset = ruleset,
|
||||
Mods = mods,
|
||||
IsLegacyScore = true
|
||||
};
|
||||
|
||||
if (Statistics != null)
|
||||
|
@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = textbox_height,
|
||||
PlaceholderText = "type your message",
|
||||
OnCommit = postMessage,
|
||||
ReleaseFocusOnCommit = false,
|
||||
HoldFocus = true,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
});
|
||||
|
||||
textbox.OnCommit += postMessage;
|
||||
}
|
||||
|
||||
Channel.BindValueChanged(channelChanged);
|
||||
|
@ -232,9 +232,9 @@ namespace osu.Game
|
||||
dependencies.Cache(new SessionStatics());
|
||||
dependencies.Cache(new OsuColour());
|
||||
|
||||
fileImporters.Add(BeatmapManager);
|
||||
fileImporters.Add(ScoreManager);
|
||||
fileImporters.Add(SkinManager);
|
||||
RegisterImportHandler(BeatmapManager);
|
||||
RegisterImportHandler(ScoreManager);
|
||||
RegisterImportHandler(SkinManager);
|
||||
|
||||
// tracks play so loud our samples can't keep up.
|
||||
// this adds a global reduction of track volume for the time being.
|
||||
@ -343,6 +343,18 @@ namespace osu.Game
|
||||
|
||||
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
|
||||
|
||||
/// <summary>
|
||||
/// Register a global handler for file imports. Most recently registered will have precedence.
|
||||
/// </summary>
|
||||
/// <param name="handler">The handler to register.</param>
|
||||
public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a global handler for file imports.
|
||||
/// </summary>
|
||||
/// <param name="handler">The previously registered handler.</param>
|
||||
public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler);
|
||||
|
||||
public async Task Import(params string[] paths)
|
||||
{
|
||||
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
|
||||
@ -354,7 +366,7 @@ namespace osu.Game
|
||||
}
|
||||
}
|
||||
|
||||
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();
|
||||
public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -146,7 +146,6 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 1,
|
||||
PlaceholderText = "type your message",
|
||||
OnCommit = postMessage,
|
||||
ReleaseFocusOnCommit = false,
|
||||
HoldFocus = true,
|
||||
}
|
||||
@ -186,6 +185,8 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
};
|
||||
|
||||
textbox.OnCommit += postMessage;
|
||||
|
||||
ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
|
||||
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
|
||||
ChannelSelectionOverlay.State.ValueChanged += state =>
|
||||
|
@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
|
||||
},
|
||||
};
|
||||
|
||||
filter.Search.OnCommit = (sender, newText) =>
|
||||
filter.Search.OnCommit += (sender, newText) =>
|
||||
{
|
||||
BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault();
|
||||
|
||||
|
@ -236,7 +236,6 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
PlaceholderText = "password",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
OnCommit = (sender, newText) => performLogin()
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
password.OnCommit += (sender, newText) => performLogin();
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
@ -40,17 +40,28 @@ namespace osu.Game.Rulesets.Edit
|
||||
Playfield.DisplayJudgements.Value = false;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.HitObjectAdded += addHitObject;
|
||||
beatmap.HitObjectUpdated += updateReplay;
|
||||
beatmap.HitObjectRemoved += removeHitObject;
|
||||
|
||||
if (changeHandler != null)
|
||||
{
|
||||
// for now only regenerate replay on a finalised state change, not HitObjectUpdated.
|
||||
changeHandler.OnStateChange += updateReplay;
|
||||
}
|
||||
else
|
||||
{
|
||||
beatmap.HitObjectUpdated += _ => updateReplay();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateReplay(HitObject obj = null) =>
|
||||
drawableRuleset.RegenerateAutoplay();
|
||||
private void updateReplay() => drawableRuleset.RegenerateAutoplay();
|
||||
|
||||
private void addHitObject(HitObject hitObject)
|
||||
{
|
||||
@ -58,8 +69,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
drawableRuleset.Playfield.Add(drawableObject);
|
||||
drawableRuleset.Playfield.PostProcess();
|
||||
|
||||
updateReplay();
|
||||
}
|
||||
|
||||
private void removeHitObject(HitObject hitObject)
|
||||
@ -68,8 +77,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
drawableRuleset.Playfield.Remove(drawableObject);
|
||||
drawableRuleset.Playfield.PostProcess();
|
||||
|
||||
drawableRuleset.RegenerateAutoplay();
|
||||
}
|
||||
|
||||
public override bool PropagatePositionalInputSubTree => false;
|
||||
|
@ -109,16 +109,16 @@ namespace osu.Game.Rulesets.Judgements
|
||||
return 0;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.SmallTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.Miss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE;
|
||||
@ -127,10 +127,10 @@ namespace osu.Game.Rulesets.Judgements
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
|
||||
case HitResult.Ok:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.01;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.Good:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
|
||||
|
||||
case HitResult.Great:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
@ -139,10 +139,10 @@ namespace osu.Game.Rulesets.Judgements
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.2;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
|
||||
/// Invoked by this or a nested <see cref="DrawableHitObject"/> after a <see cref="JudgementResult"/> has been applied.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, JudgementResult> OnNewResult;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
|
||||
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
|
||||
|
||||
@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
#region State / Transform Management
|
||||
|
||||
/// <summary>
|
||||
/// Bind to apply a custom state which can override the default implementation.
|
||||
/// Invoked by this or a nested <see cref="DrawableHitObject"/> to apply a custom state that can override the default implementation.
|
||||
/// </summary>
|
||||
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
|
||||
|
||||
@ -384,6 +384,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out.
|
||||
/// Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded.
|
||||
/// </summary>
|
||||
public virtual void StopAllSamples()
|
||||
{
|
||||
if (Samples?.Looping == true)
|
||||
Samples.Stop();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -452,6 +462,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
foreach (var nested in NestedHitObjects)
|
||||
nested.OnKilled();
|
||||
|
||||
// failsafe to ensure looping samples don't get stuck in a playing state.
|
||||
// this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped.
|
||||
StopAllSamples();
|
||||
|
||||
UpdateResult(false);
|
||||
}
|
||||
|
||||
@ -462,6 +476,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
|
||||
protected void ApplyResult(Action<JudgementResult> application)
|
||||
{
|
||||
if (Result.HasResult)
|
||||
throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
|
||||
|
||||
application?.Invoke(Result);
|
||||
|
||||
if (!Result.HasResult)
|
||||
@ -496,19 +513,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime);
|
||||
|
||||
switch (Result.Type)
|
||||
{
|
||||
case HitResult.None:
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
updateState(ArmedState.Miss);
|
||||
break;
|
||||
|
||||
default:
|
||||
updateState(ArmedState.Hit);
|
||||
break;
|
||||
}
|
||||
if (Result.HasResult)
|
||||
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
||||
|
||||
OnNewResult?.Invoke(this, Result);
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.UI
|
||||
public GameplayClock GameplayClock => stabilityGameplayClock;
|
||||
|
||||
[Cached(typeof(GameplayClock))]
|
||||
[Cached(typeof(ISamplePlaybackDisabler))]
|
||||
private readonly StabilityGameplayClock stabilityGameplayClock;
|
||||
|
||||
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
|
||||
@ -58,13 +59,16 @@ namespace osu.Game.Rulesets.UI
|
||||
private int direction;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(GameplayClock clock)
|
||||
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
|
||||
{
|
||||
if (clock != null)
|
||||
{
|
||||
parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
|
||||
GameplayClock.IsPaused.BindTo(clock.IsPaused);
|
||||
}
|
||||
|
||||
// this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes).
|
||||
stabilityGameplayClock.ParentSampleDisabler = sampleDisabler;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -207,11 +211,15 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private void setClock()
|
||||
{
|
||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||
parentGameplayClock ??= Clock;
|
||||
|
||||
Clock = GameplayClock;
|
||||
ProcessCustomClock = false;
|
||||
if (parentGameplayClock == null)
|
||||
{
|
||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||
parentGameplayClock ??= Clock;
|
||||
}
|
||||
else
|
||||
{
|
||||
Clock = GameplayClock;
|
||||
}
|
||||
}
|
||||
|
||||
public ReplayInputHandler ReplayInputHandler { get; set; }
|
||||
@ -220,6 +228,8 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public GameplayClock ParentGameplayClock;
|
||||
|
||||
public ISamplePlaybackDisabler ParentSampleDisabler;
|
||||
|
||||
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
|
||||
|
||||
public StabilityGameplayClock(FramedClock underlyingClock)
|
||||
@ -227,7 +237,11 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200;
|
||||
protected override bool ShouldDisableSamplePlayback =>
|
||||
// handle the case where playback is catching up to real-time.
|
||||
base.ShouldDisableSamplePlayback
|
||||
|| ParentSampleDisabler?.SamplePlaybackDisabled.Value == true
|
||||
|| (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -185,6 +185,34 @@ namespace osu.Game.Scoring
|
||||
[JsonProperty("position")]
|
||||
public int? Position { get; set; }
|
||||
|
||||
private bool isLegacyScore;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public bool IsLegacyScore
|
||||
{
|
||||
get
|
||||
{
|
||||
if (isLegacyScore)
|
||||
return true;
|
||||
|
||||
// The above check will catch legacy online scores that have an appropriate UserString + UserId.
|
||||
// For non-online scores such as those imported in, a heuristic is used based on the following table:
|
||||
//
|
||||
// Mode | UserString | UserId
|
||||
// --------------- | ---------- | ---------
|
||||
// stable | <username> | 1
|
||||
// lazer | <username> | <userid>
|
||||
// lazer (offline) | Guest | 1
|
||||
|
||||
return ID > 0 && UserID == 1 && UserString != "Guest";
|
||||
}
|
||||
set => isLegacyScore = value;
|
||||
}
|
||||
|
||||
public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
|
||||
{
|
||||
foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>())
|
||||
|
@ -10,6 +10,7 @@ using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
public class ScoreManager : DownloadableArchiveModelManager<ScoreInfo, ScoreFileInfo>
|
||||
{
|
||||
public override string[] HandledExtensions => new[] { ".osr" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osr" };
|
||||
|
||||
@ -149,23 +150,38 @@ namespace osu.Game.Scoring
|
||||
return;
|
||||
}
|
||||
|
||||
int? beatmapMaxCombo = score.Beatmap.MaxCombo;
|
||||
int beatmapMaxCombo;
|
||||
|
||||
if (beatmapMaxCombo == null)
|
||||
if (score.IsLegacyScore)
|
||||
{
|
||||
if (score.Beatmap.ID == 0 || difficulties == null)
|
||||
// This score is guaranteed to be an osu!stable score.
|
||||
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
|
||||
if (score.Beatmap.MaxCombo == null)
|
||||
{
|
||||
// We don't have enough information (max combo) to compute the score, so let's use the provided score.
|
||||
Value = score.TotalScore;
|
||||
if (score.Beatmap.ID == 0 || difficulties == null)
|
||||
{
|
||||
// We don't have enough information (max combo) to compute the score, so use the provided score.
|
||||
Value = score.TotalScore;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can compute the max combo locally after the async beatmap difficulty computation.
|
||||
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
|
||||
difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We can compute the max combo locally after the async beatmap difficulty computation.
|
||||
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
|
||||
difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
|
||||
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
|
||||
}
|
||||
else
|
||||
updateScore(beatmapMaxCombo.Value);
|
||||
{
|
||||
// This score is guaranteed to be an osu!lazer score.
|
||||
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
|
||||
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
|
||||
}
|
||||
|
||||
updateScore(beatmapMaxCombo);
|
||||
}
|
||||
|
||||
private void updateScore(int beatmapMaxCombo)
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = SelectionHandler.BORDER_RADIUS,
|
||||
BorderThickness = SelectionBox.BORDER_RADIUS,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
216
osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
Normal file
216
osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
Normal file
@ -0,0 +1,216 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class SelectionBox : CompositeDrawable
|
||||
{
|
||||
public Action<float> OnRotation;
|
||||
public Action<Vector2, Anchor> OnScale;
|
||||
public Action<Direction> OnFlip;
|
||||
|
||||
public Action OperationStarted;
|
||||
public Action OperationEnded;
|
||||
|
||||
private bool canRotate;
|
||||
|
||||
/// <summary>
|
||||
/// Whether rotation support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanRotate
|
||||
{
|
||||
get => canRotate;
|
||||
set
|
||||
{
|
||||
if (canRotate == value) return;
|
||||
|
||||
canRotate = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleX;
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scale support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleX
|
||||
{
|
||||
get => canScaleX;
|
||||
set
|
||||
{
|
||||
if (canScaleX == value) return;
|
||||
|
||||
canScaleX = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleY;
|
||||
|
||||
/// <summary>
|
||||
/// Whether horizontal scale support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleY
|
||||
{
|
||||
get => canScaleY;
|
||||
set
|
||||
{
|
||||
if (canScaleY == value) return;
|
||||
|
||||
canScaleY = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private FillFlowContainer buttons;
|
||||
|
||||
public const float BORDER_RADIUS = 3;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
recreate();
|
||||
}
|
||||
|
||||
private void recreate()
|
||||
{
|
||||
if (LoadState < LoadState.Loading)
|
||||
return;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
BorderThickness = BORDER_RADIUS,
|
||||
BorderColour = colours.YellowDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
},
|
||||
}
|
||||
},
|
||||
buttons = new FillFlowContainer
|
||||
{
|
||||
Y = 20,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
};
|
||||
|
||||
if (CanScaleX) addXScaleComponents();
|
||||
if (CanScaleX && CanScaleY) addFullScaleComponents();
|
||||
if (CanScaleY) addYScaleComponents();
|
||||
if (CanRotate) addRotationComponents();
|
||||
}
|
||||
|
||||
private void addRotationComponents()
|
||||
{
|
||||
const float separation = 40;
|
||||
|
||||
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
|
||||
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Colour = colours.YellowLight,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0.3f,
|
||||
Size = new Vector2(BORDER_RADIUS, separation),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Y = -separation,
|
||||
HandleDrag = e => OnRotation?.Invoke(e.Delta.X),
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addYScaleComponents()
|
||||
{
|
||||
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical));
|
||||
|
||||
addDragHandle(Anchor.TopCentre);
|
||||
addDragHandle(Anchor.BottomCentre);
|
||||
}
|
||||
|
||||
private void addFullScaleComponents()
|
||||
{
|
||||
addDragHandle(Anchor.TopLeft);
|
||||
addDragHandle(Anchor.TopRight);
|
||||
addDragHandle(Anchor.BottomLeft);
|
||||
addDragHandle(Anchor.BottomRight);
|
||||
}
|
||||
|
||||
private void addXScaleComponents()
|
||||
{
|
||||
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal));
|
||||
|
||||
addDragHandle(Anchor.CentreLeft);
|
||||
addDragHandle(Anchor.CentreRight);
|
||||
}
|
||||
|
||||
private void addButton(IconUsage icon, string tooltip, Action action)
|
||||
{
|
||||
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip)
|
||||
{
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded,
|
||||
Action = action
|
||||
});
|
||||
}
|
||||
|
||||
private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle
|
||||
{
|
||||
Anchor = anchor,
|
||||
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded
|
||||
});
|
||||
|
||||
private int activeOperations;
|
||||
|
||||
private void operationEnded()
|
||||
{
|
||||
if (--activeOperations == 0)
|
||||
OperationEnded?.Invoke();
|
||||
}
|
||||
|
||||
private void operationStarted()
|
||||
{
|
||||
if (activeOperations++ == 0)
|
||||
OperationStarted?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class SelectionBoxDragHandle : Container
|
||||
{
|
||||
public Action OperationStarted;
|
||||
public Action OperationEnded;
|
||||
|
||||
public Action<DragEvent> HandleDrag { get; set; }
|
||||
|
||||
private Circle circle;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(10);
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
circle = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
UpdateHoverState();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
UpdateHoverState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
UpdateHoverState();
|
||||
}
|
||||
|
||||
protected bool HandlingMouse;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
HandlingMouse = true;
|
||||
UpdateHoverState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
OperationStarted?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
HandleDrag?.Invoke(e);
|
||||
base.OnDrag(e);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
HandlingMouse = false;
|
||||
OperationEnded?.Invoke();
|
||||
|
||||
UpdateHoverState();
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
HandlingMouse = false;
|
||||
UpdateHoverState();
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected virtual void UpdateHoverState()
|
||||
{
|
||||
circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark);
|
||||
this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A drag "handle" which shares the visual appearance but behaves more like a clickable button.
|
||||
/// </summary>
|
||||
public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip
|
||||
{
|
||||
private SpriteIcon icon;
|
||||
|
||||
private readonly IconUsage iconUsage;
|
||||
|
||||
public Action Action;
|
||||
|
||||
public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip)
|
||||
{
|
||||
this.iconUsage = iconUsage;
|
||||
|
||||
TooltipText = tooltip;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size *= 2;
|
||||
AddInternal(icon = new SpriteIcon
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.5f),
|
||||
Icon = iconUsage,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
OperationStarted?.Invoke();
|
||||
Action?.Invoke();
|
||||
OperationEnded?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void UpdateHoverState()
|
||||
{
|
||||
base.UpdateHoverState();
|
||||
icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black;
|
||||
}
|
||||
|
||||
public string TooltipText { get; }
|
||||
}
|
||||
}
|
@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
{
|
||||
public const float BORDER_RADIUS = 2;
|
||||
|
||||
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
|
||||
private readonly List<SelectionBlueprint> selectedBlueprints;
|
||||
|
||||
@ -45,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private OsuSpriteText selectionDetailsText;
|
||||
|
||||
protected SelectionBox SelectionBox { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
@ -69,19 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = BORDER_RADIUS,
|
||||
BorderColour = colours.YellowDark,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
}
|
||||
},
|
||||
// todo: should maybe be inside the SelectionBox?
|
||||
new Container
|
||||
{
|
||||
Name = "info text",
|
||||
@ -100,11 +88,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Font = OsuFont.Default.With(size: 11)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public SelectionBox CreateSelectionBox()
|
||||
=> new SelectionBox
|
||||
{
|
||||
OperationStarted = OnOperationBegan,
|
||||
OperationEnded = OnOperationEnded,
|
||||
|
||||
OnRotation = angle => HandleRotation(angle),
|
||||
OnScale = (amount, anchor) => HandleScale(amount, anchor),
|
||||
OnFlip = direction => HandleFlip(direction),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a drag operation ends from the selection box.
|
||||
/// </summary>
|
||||
protected virtual void OnOperationBegan()
|
||||
{
|
||||
ChangeHandler.BeginChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a drag operation begins from the selection box.
|
||||
/// </summary>
|
||||
protected virtual void OnOperationEnded()
|
||||
{
|
||||
ChangeHandler.EndChange();
|
||||
}
|
||||
|
||||
#region User Input Handling
|
||||
|
||||
/// <summary>
|
||||
@ -119,7 +135,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
|
||||
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
|
||||
/// </returns>
|
||||
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true;
|
||||
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
|
||||
/// </summary>
|
||||
/// <param name="angle">The delta angle to apply to the selection.</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleRotation(float angle) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled.
|
||||
/// </summary>
|
||||
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
|
||||
/// <param name="anchor">The point of reference where the scale is originating from.</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handled the selected <see cref="DrawableHitObject"/>s being flipped.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction to flip</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleFlip(Direction direction) => false;
|
||||
|
||||
public bool OnPressed(PlatformAction action)
|
||||
{
|
||||
@ -222,11 +260,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
Show();
|
||||
OnSelectionChanged();
|
||||
}
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever more than one object is selected, on each change.
|
||||
/// Should update the selection box's state to match supported operations.
|
||||
/// </summary>
|
||||
protected virtual void OnSelectionChanged()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -79,7 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
};
|
||||
|
||||
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
|
||||
bank.BindValueChanged(bank => text.Text = $"{bank.NewValue}", true);
|
||||
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,8 +86,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
MidColour = colours.BlueDark,
|
||||
HighColour = colours.BlueDarker,
|
||||
},
|
||||
controlPoints = new TimelineControlPointDisplay(),
|
||||
ticks = new TimelineTickDisplay(),
|
||||
controlPoints = new TimelineControlPointDisplay(),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
private bool isNewBeatmap;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
@ -113,8 +115,6 @@ namespace osu.Game.Screens.Edit
|
||||
// todo: remove caching of this and consume via editorBeatmap?
|
||||
dependencies.Cache(beatDivisor);
|
||||
|
||||
bool isNewBeatmap = false;
|
||||
|
||||
if (Beatmap.Value is DummyWorkingBeatmap)
|
||||
{
|
||||
isNewBeatmap = true;
|
||||
@ -287,6 +287,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
protected void Save()
|
||||
{
|
||||
// no longer new after first user-triggered save.
|
||||
isNewBeatmap = false;
|
||||
|
||||
// apply any set-level metadata changes.
|
||||
beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
|
||||
|
||||
@ -435,10 +438,20 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog))
|
||||
if (!exitConfirmed)
|
||||
{
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
||||
return true;
|
||||
// if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save.
|
||||
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
|
||||
{
|
||||
confirmExit();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNewBeatmap || HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Background.FadeColour(Color4.White, 500);
|
||||
@ -456,6 +469,12 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private void confirmExit()
|
||||
{
|
||||
if (isNewBeatmap)
|
||||
{
|
||||
// confirming exit without save means we should delete the new beatmap completely.
|
||||
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
|
||||
}
|
||||
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit
|
||||
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
|
||||
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
|
||||
|
||||
public event Action OnStateChange;
|
||||
|
||||
private readonly LegacyEditorBeatmapPatcher patcher;
|
||||
private readonly List<byte[]> savedStates = new List<byte[]>();
|
||||
|
||||
@ -79,9 +81,6 @@ namespace osu.Game.Screens.Edit
|
||||
SaveState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current <see cref="Editor"/> state.
|
||||
/// </summary>
|
||||
public void SaveState()
|
||||
{
|
||||
if (bulkChangesStarted > 0)
|
||||
@ -109,6 +108,8 @@ namespace osu.Game.Screens.Edit
|
||||
savedStates.Add(newState);
|
||||
|
||||
currentState = savedStates.Count - 1;
|
||||
|
||||
OnStateChange?.Invoke();
|
||||
updateBindables();
|
||||
}
|
||||
}
|
||||
@ -136,6 +137,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
isRestoring = false;
|
||||
|
||||
OnStateChange?.Invoke();
|
||||
updateBindables();
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,5 @@ namespace osu.Game.Screens.Edit
|
||||
.Then()
|
||||
.FadeTo(1f, 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
public interface IEditorChangeHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired whenever a state change occurs.
|
||||
/// </summary>
|
||||
event Action OnStateChange;
|
||||
|
||||
/// <summary>
|
||||
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
|
||||
/// </summary>
|
||||
@ -29,5 +35,11 @@ namespace osu.Game.Screens.Edit
|
||||
/// This should be invoked as soon as possible after <see cref="BeginChange"/> to cause a state change.
|
||||
/// </remarks>
|
||||
void EndChange();
|
||||
|
||||
/// <summary>
|
||||
/// Immediately saves the current <see cref="Editor"/> state.
|
||||
/// Note that this will be a no-op if there is a change in progress via <see cref="BeginChange"/>.
|
||||
/// </summary>
|
||||
void SaveState();
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -23,14 +26,24 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public class SetupScreen : EditorScreen
|
||||
public class SetupScreen : EditorScreen, ICanAcceptFiles
|
||||
{
|
||||
public IEnumerable<string> HandledExtensions => ImageExtensions.Concat(AudioExtensions);
|
||||
|
||||
public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" };
|
||||
|
||||
public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" };
|
||||
|
||||
private FillFlowContainer flow;
|
||||
private LabelledTextBox artistTextBox;
|
||||
private LabelledTextBox titleTextBox;
|
||||
private LabelledTextBox creatorTextBox;
|
||||
private LabelledTextBox difficultyTextBox;
|
||||
private LabelledTextBox audioTrackTextBox;
|
||||
private Container backgroundSpriteContainer;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private MusicController music { get; set; }
|
||||
@ -83,19 +96,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
backgroundSpriteContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Child = new BeatmapBackgroundSprite(Beatmap.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
@ -144,12 +150,81 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
}
|
||||
};
|
||||
|
||||
updateBackgroundSprite();
|
||||
|
||||
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
|
||||
|
||||
foreach (var item in flow.OfType<LabelledTextBox>())
|
||||
item.OnCommit += onCommit;
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(params string[] paths)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
var firstFile = new FileInfo(paths.First());
|
||||
|
||||
if (ImageExtensions.Contains(firstFile.Extension))
|
||||
{
|
||||
ChangeBackgroundImage(firstFile.FullName);
|
||||
}
|
||||
else if (AudioExtensions.Contains(firstFile.Extension))
|
||||
{
|
||||
audioTrackTextBox.Text = firstFile.FullName;
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void updateBackgroundSprite()
|
||||
{
|
||||
LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
}, background =>
|
||||
{
|
||||
backgroundSpriteContainer.Child = background;
|
||||
background.FadeInFromZero(500);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
game.RegisterImportHandler(this);
|
||||
}
|
||||
|
||||
public bool ChangeBackgroundImage(string path)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (!info.Exists)
|
||||
return false;
|
||||
|
||||
var set = Beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
// remove the previous background for now.
|
||||
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
|
||||
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile);
|
||||
|
||||
using (var stream = info.OpenRead())
|
||||
{
|
||||
if (oldFile != null)
|
||||
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
|
||||
else
|
||||
beatmaps.AddFile(set, stream, info.Name);
|
||||
}
|
||||
|
||||
Beatmap.Value.Metadata.BackgroundFile = info.Name;
|
||||
updateBackgroundSprite();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ChangeAudioTrack(string path)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
@ -196,6 +271,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value;
|
||||
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
game?.UnregisterImportHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileChooserLabelledTextBox : LabelledTextBox
|
||||
@ -230,7 +311,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
public void DisplayFileChooser()
|
||||
{
|
||||
Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" })
|
||||
Target.Child = new FileSelector(validFileExtensions: SetupScreen.AudioExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 400,
|
||||
|
@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private IReadOnlyList<Drawable> createSections() => new Drawable[]
|
||||
{
|
||||
new GroupSection(),
|
||||
new TimingSection(),
|
||||
new DifficultySection(),
|
||||
new SampleSection(),
|
||||
|
@ -2,27 +2,23 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class DifficultySection : Section<DifficultyControlPoint>
|
||||
{
|
||||
private SettingsSlider<double> multiplier;
|
||||
private SliderWithTextBoxInput<double> multiplierSlider;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new[]
|
||||
{
|
||||
multiplier = new SettingsSlider<double>
|
||||
multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier")
|
||||
{
|
||||
LabelText = "Speed Multiplier",
|
||||
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = new DifficultyControlPoint().SpeedMultiplierBindable
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -31,7 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
|
||||
multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
|
||||
multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
119
osu.Game/Screens/Edit/Timing/GroupSection.cs
Normal file
119
osu.Game/Screens/Edit/Timing/GroupSection.cs
Normal file
@ -0,0 +1,119 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class GroupSection : CompositeDrawable
|
||||
{
|
||||
private LabelledTextBox textBox;
|
||||
|
||||
private TriangleButton button;
|
||||
|
||||
[Resolved]
|
||||
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding(10);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(10),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textBox = new LabelledTextBox
|
||||
{
|
||||
Label = "Time"
|
||||
},
|
||||
button = new TriangleButton
|
||||
{
|
||||
Text = "Use current time",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Action = () => changeSelectedGroupTime(clock.CurrentTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
textBox.OnCommit += (sender, isNew) =>
|
||||
{
|
||||
if (!isNew)
|
||||
return;
|
||||
|
||||
if (double.TryParse(sender.Text, out var newTime))
|
||||
{
|
||||
changeSelectedGroupTime(newTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedGroup.TriggerChange();
|
||||
}
|
||||
};
|
||||
|
||||
SelectedGroup.BindValueChanged(group =>
|
||||
{
|
||||
if (group.NewValue == null)
|
||||
{
|
||||
textBox.Text = string.Empty;
|
||||
|
||||
textBox.Current.Disabled = true;
|
||||
button.Enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
textBox.Current.Disabled = false;
|
||||
button.Enabled.Value = true;
|
||||
|
||||
textBox.Text = $"{group.NewValue.Time:n0}";
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void changeSelectedGroupTime(in double time)
|
||||
{
|
||||
if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time)
|
||||
return;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
|
||||
|
||||
foreach (var cp in currentGroupItems)
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp);
|
||||
|
||||
SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,18 +2,17 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class SampleSection : Section<SampleControlPoint>
|
||||
{
|
||||
private LabelledTextBox bank;
|
||||
private SettingsSlider<int> volume;
|
||||
private SliderWithTextBoxInput<int> volume;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
Label = "Bank Name",
|
||||
},
|
||||
volume = new SettingsSlider<int>
|
||||
volume = new SliderWithTextBoxInput<int>("Volume")
|
||||
{
|
||||
Bindable = new SampleControlPoint().SampleVolumeBindable,
|
||||
LabelText = "Volume",
|
||||
Current = new SampleControlPoint().SampleVolumeBindable,
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -37,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bank.Current = point.NewValue.SampleBankBindable;
|
||||
volume.Bindable = point.NewValue.SampleVolumeBindable;
|
||||
bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
volume.Current = point.NewValue.SampleVolumeBindable;
|
||||
volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
|
78
osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
Normal file
78
osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
Normal file
@ -0,0 +1,78 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
{
|
||||
private readonly SettingsSlider<T> slider;
|
||||
|
||||
public SliderWithTextBoxInput(string labelText)
|
||||
{
|
||||
LabelledTextBox textbox;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textbox = new LabelledTextBox
|
||||
{
|
||||
Label = labelText,
|
||||
},
|
||||
slider = new SettingsSlider<T>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
textbox.OnCommit += (t, isNew) =>
|
||||
{
|
||||
if (!isNew) return;
|
||||
|
||||
try
|
||||
{
|
||||
slider.Bindable.Parse(t.Text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// TriggerChange below will restore the previous text value on failure.
|
||||
}
|
||||
|
||||
// This is run regardless of parsing success as the parsed number may not actually trigger a change
|
||||
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
|
||||
Current.TriggerChange();
|
||||
};
|
||||
|
||||
Current.BindValueChanged(val =>
|
||||
{
|
||||
textbox.Text = val.NewValue.ToString();
|
||||
}, true);
|
||||
}
|
||||
|
||||
public Bindable<T> Current
|
||||
{
|
||||
get => slider.Bindable;
|
||||
set => slider.Bindable = value;
|
||||
}
|
||||
}
|
||||
}
|
@ -81,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -140,6 +143,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
controlGroups.BindCollectionChanged((sender, args) =>
|
||||
{
|
||||
table.ControlGroups = controlGroups;
|
||||
changeHandler.SaveState();
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,13 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bpmSlider.Bindable = point.NewValue.BeatLengthBindable;
|
||||
bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||
// no need to hook change handler here as it's the same bindable as above
|
||||
|
||||
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
|
||||
timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,18 +70,19 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
if (!isNew) return;
|
||||
|
||||
if (double.TryParse(Current.Value, out double doubleVal))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
|
||||
beatLengthBindable.Value = beatLengthToBpm(doubleVal);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// will restore the previous text value on failure.
|
||||
beatLengthBindable.TriggerChange();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// TriggerChange below will restore the previous text value on failure.
|
||||
}
|
||||
|
||||
// This is run regardless of parsing success as the parsed number may not actually trigger a change
|
||||
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
|
||||
beatLengthBindable.TriggerChange();
|
||||
};
|
||||
|
||||
beatLengthBindable.BindValueChanged(val =>
|
||||
@ -116,6 +122,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
|
||||
|
||||
base.Bindable = bpmBindable;
|
||||
|
||||
TransferValueOnCommit = true;
|
||||
}
|
||||
|
||||
public override Bindable<double> Bindable
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user