mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 09:02:58 +08:00
Merge branch 'master' into skin-editor-layer-select
This commit is contained in:
commit
051eb18b33
156
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs
Normal file
156
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs
Normal file
@ -0,0 +1,156 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneHitCircleLateFade : OsuTestScene
|
||||
{
|
||||
private float? alphaAtMiss;
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicMod()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicAndFullHiddenMods()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicAndApproachCircleOnlyHiddenMods()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleNoMod()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderClassicMod()
|
||||
{
|
||||
AddStep("Create slider", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||
createSlider();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Head circle transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNoMod()
|
||||
{
|
||||
AddStep("Create slider", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createSlider();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
private void createCircle()
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
||||
{
|
||||
StartTime = Time.Current + 500,
|
||||
Position = new Vector2(250)
|
||||
});
|
||||
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableHitCircle.OnNewResult += (_, _) =>
|
||||
{
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
};
|
||||
|
||||
Child = drawableHitCircle;
|
||||
}
|
||||
|
||||
private void createSlider()
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableSlider drawableSlider = new DrawableSlider(new Slider
|
||||
{
|
||||
StartTime = Time.Current + 500,
|
||||
Position = new Vector2(250),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(0, 100),
|
||||
})
|
||||
});
|
||||
|
||||
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableSlider.OnLoadComplete += _ =>
|
||||
{
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
||||
{
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
};
|
||||
};
|
||||
Child = drawableSlider;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
|
||||
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
|
||||
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
|
||||
|
||||
private bool usingHiddenFading;
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
if (ClassicNoteLock.Value)
|
||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
||||
|
||||
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject obj)
|
||||
@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
|
||||
break;
|
||||
|
||||
case DrawableHitCircle circle:
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(circle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEarlyFading(DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApplyCustomUpdateState += (o, _) =>
|
||||
{
|
||||
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
|
||||
{
|
||||
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Input;
|
||||
@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK.Input;
|
||||
@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(IGameplayClock))]
|
||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -61,6 +62,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClipboard clipboard { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SkinEditorOverlay? skinEditorOverlay { get; set; }
|
||||
|
||||
@ -81,6 +85,15 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
private EditorMenuItem undoMenuItem = null!;
|
||||
private EditorMenuItem redoMenuItem = null!;
|
||||
|
||||
private EditorMenuItem cutMenuItem = null!;
|
||||
private EditorMenuItem copyMenuItem = null!;
|
||||
private EditorMenuItem cloneMenuItem = null!;
|
||||
private EditorMenuItem pasteMenuItem = null!;
|
||||
|
||||
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
|
||||
|
||||
[Resolved]
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
|
||||
@ -146,6 +159,11 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
|
||||
new EditorMenuItemSpacer(),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -204,6 +222,21 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
|
||||
canCopy.Current.BindValueChanged(copy =>
|
||||
{
|
||||
copyMenuItem.Action.Disabled = !copy.NewValue;
|
||||
cloneMenuItem.Action.Disabled = !copy.NewValue;
|
||||
}, true);
|
||||
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
|
||||
|
||||
SelectedComponents.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
canCopy.Value = canCut.Value = SelectedComponents.Any();
|
||||
}, true);
|
||||
|
||||
clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true);
|
||||
|
||||
Show();
|
||||
|
||||
game?.RegisterImportHandler(this);
|
||||
@ -229,6 +262,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case PlatformAction.Cut:
|
||||
Cut();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Copy:
|
||||
Copy();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Paste:
|
||||
Paste();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Undo:
|
||||
Undo();
|
||||
return true;
|
||||
@ -316,15 +361,24 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
|
||||
{
|
||||
RequestPlacement = placeComponent
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the ruleset from the lookup to get base components.
|
||||
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
|
||||
{
|
||||
RequestPlacement = placeComponent
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
|
||||
void requestPlacement(Type type)
|
||||
{
|
||||
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
|
||||
|
||||
SelectedComponents.Clear();
|
||||
placeComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
private void skinChanged()
|
||||
@ -349,20 +403,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
hasBegunMutating = true;
|
||||
}
|
||||
|
||||
private void placeComponent(Type type)
|
||||
{
|
||||
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
|
||||
|
||||
placeComponent(component);
|
||||
}
|
||||
|
||||
private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
|
||||
/// <summary>
|
||||
/// Attempt to place a given component in the current target. If successful, the new component will be added to <see cref="SelectedComponents"/>.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to be placed.</param>
|
||||
/// <param name="applyDefaults">Whether to apply default anchor / origin / position values.</param>
|
||||
/// <returns>Whether placement succeeded. Could fail if no target is available, or if the current target has missing dependency requirements for the component.</returns>
|
||||
private bool placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
|
||||
{
|
||||
var targetContainer = getTarget(selectedTarget.Value);
|
||||
|
||||
if (targetContainer == null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
var drawableComponent = (Drawable)component;
|
||||
|
||||
@ -374,10 +426,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
||||
}
|
||||
|
||||
targetContainer.Add(component);
|
||||
try
|
||||
{
|
||||
targetContainer.Add(component);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// May fail if dependencies are not available, for instance.
|
||||
return false;
|
||||
}
|
||||
|
||||
SelectedComponents.Clear();
|
||||
SelectedComponents.Add(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void populateSettings()
|
||||
@ -410,6 +470,48 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
}
|
||||
|
||||
protected void Cut()
|
||||
{
|
||||
Copy();
|
||||
DeleteItems(SelectedComponents.ToArray());
|
||||
}
|
||||
|
||||
protected void Copy()
|
||||
{
|
||||
clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
|
||||
}
|
||||
|
||||
protected void Clone()
|
||||
{
|
||||
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
|
||||
if (!canCopy.Value)
|
||||
return;
|
||||
|
||||
Copy();
|
||||
Paste();
|
||||
}
|
||||
|
||||
protected void Paste()
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
var drawableInfo = JsonConvert.DeserializeObject<SerialisedDrawableInfo[]>(clipboard.Content.Value);
|
||||
|
||||
if (drawableInfo == null)
|
||||
return;
|
||||
|
||||
var instances = drawableInfo.Select(d => d.CreateInstance())
|
||||
.OfType<ISerialisableDrawable>()
|
||||
.ToArray();
|
||||
|
||||
SelectedComponents.Clear();
|
||||
|
||||
foreach (var i in instances)
|
||||
placeComponent(i, false);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
protected void Undo() => changeHandler?.RestoreState(-1);
|
||||
|
||||
protected void Redo() => changeHandler?.RestoreState(1);
|
||||
@ -451,8 +553,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
public void DeleteItems(ISerialisableDrawable[] items)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var item in items)
|
||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
#region Drag & drop import handling
|
||||
@ -489,6 +595,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
|
||||
};
|
||||
|
||||
SelectedComponents.Clear();
|
||||
placeComponent(sprite, false);
|
||||
|
||||
SkinSelectionHandler.ApplyClosestAnchor(sprite);
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osuTK;
|
||||
|
||||
@ -28,6 +29,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private SkinEditor? skinEditor;
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user