mirror of
https://github.com/ppy/osu.git
synced 2025-03-25 18:57:18 +08:00
Merge branch 'master' into bss/the-actual-submission
This commit is contained in:
commit
de9362dae5
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
build-only-android:
|
||||
name: Build only (Android)
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -114,10 +114,7 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET workloads
|
||||
# since windows image 20241113.3.0, not specifying a version here
|
||||
# installs the .NET 7 version of android workload for very unknown reasons.
|
||||
# revisit once we upgrade to .NET 9, it's probably fixed there.
|
||||
run: dotnet workload install android --version (dotnet --version)
|
||||
run: dotnet workload install android
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug osu.Android.slnf
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
public ArgonJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
Y = judgement_y_position;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
|
||||
if (Result.IsHit())
|
||||
{
|
||||
AddInternal(ringExplosion = new RingExplosion(Result)
|
||||
@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
|
||||
protected override SpriteText CreateJudgementText() =>
|
||||
new OsuSpriteText
|
||||
{
|
||||
@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
|
@ -2,12 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
Y = scorePosition - absoluteHitPosition;
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
|
||||
InternalChild = animation.With(d =>
|
||||
{
|
||||
@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
});
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
float finalPosition = scorePosition - absoluteHitPosition;
|
||||
|
||||
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
{
|
||||
(animation as IFramedAnimation)?.GotoFrame(0);
|
||||
|
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal file
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal file
@ -0,0 +1,75 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
{
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
|
||||
public override void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.None:
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.ScaleTo(0.8f);
|
||||
this.ScaleTo(1, 250, Easing.OutElastic);
|
||||
|
||||
this.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
|
||||
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,65 +3,32 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||
{
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
private IBindable<ScrollingDirection> direction;
|
||||
|
||||
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
Y = judgement_y_position;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
|
||||
public override void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.None:
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.ScaleTo(0.8f);
|
||||
this.ScaleTo(1, 250, Easing.OutElastic);
|
||||
|
||||
this.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
break;
|
||||
}
|
||||
}
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
}
|
||||
}
|
||||
|
@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.BottomCentre;
|
||||
j.Origin = Anchor.Centre;
|
||||
})!);
|
||||
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Slider slider = drawableSlider.HitObject;
|
||||
Position = slider.CurvePositionAt(completionProgress);
|
||||
|
||||
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
|
||||
// 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||
double checkDistance = 0.1 / slider.Path.Distance;
|
||||
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
|
||||
|
||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||
// Needed for when near completion, or in case of a very short slider.
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
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;
|
||||
@ -13,8 +14,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public abstract partial class FollowCircle : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
protected DrawableHitObject? ParentObject { get; private set; }
|
||||
protected DrawableSlider? DrawableObject { get; private set; }
|
||||
|
||||
private readonly IBindable<bool> tracking = new Bindable<bool>();
|
||||
|
||||
protected FollowCircle()
|
||||
{
|
||||
@ -22,65 +24,73 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(DrawableHitObject? hitObject)
|
||||
{
|
||||
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking =>
|
||||
DrawableObject = hitObject as DrawableSlider;
|
||||
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
|
||||
if (ParentObject.Judged)
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0)))
|
||||
tracking.BindTo(DrawableObject.Tracking);
|
||||
tracking.BindValueChanged(tracking =>
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
}
|
||||
}, true);
|
||||
if (DrawableObject.Judged)
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0)))
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (ParentObject != null)
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
ParentObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(ParentObject);
|
||||
DrawableObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(DrawableObject);
|
||||
|
||||
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(ParentObject, ParentObject.State.Value);
|
||||
DrawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(DrawableObject, DrawableObject.State.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||
{
|
||||
// Sane defaults when a new hitobject is applied to the drawable slider.
|
||||
this.ScaleTo(1f)
|
||||
.FadeOut();
|
||||
|
||||
// Immediately play out any pending transforms from press/release
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
|
||||
private void updateStateTransforms(DrawableHitObject d, ArmedState state)
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
Debug.Assert(DrawableObject != null);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
switch (drawableObject)
|
||||
switch (d)
|
||||
{
|
||||
case DrawableSliderTail:
|
||||
// Use ParentObject instead of drawableObject because slider tail's
|
||||
// Use DrawableObject instead of local object because slider tail's
|
||||
// HitStateUpdateTime is ~36ms before the actual slider end (aka slider
|
||||
// tail leniency)
|
||||
using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime))
|
||||
OnSliderEnd();
|
||||
break;
|
||||
|
||||
case DrawableSliderTick:
|
||||
case DrawableSliderRepeat:
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||
OnSliderTick();
|
||||
break;
|
||||
}
|
||||
@ -88,15 +98,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
switch (drawableObject)
|
||||
switch (d)
|
||||
{
|
||||
case DrawableSliderTail:
|
||||
case DrawableSliderTick:
|
||||
case DrawableSliderRepeat:
|
||||
// Despite above comment, ok to use drawableObject.HitStateUpdateTime
|
||||
// Despite above comment, ok to use d.HitStateUpdateTime
|
||||
// here, since on stable, the break anim plays right when the tail is
|
||||
// missed, not when the slider ends
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||
OnSliderBreak();
|
||||
break;
|
||||
}
|
||||
@ -109,10 +119,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (ParentObject != null)
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
ParentObject.HitObjectApplied -= onHitObjectApplied;
|
||||
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
DrawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||
DrawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected override void OnSliderPress()
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
Debug.Assert(DrawableObject != null);
|
||||
|
||||
double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
|
||||
double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current);
|
||||
|
||||
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
||||
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new osuTK.Vector2(0.5f),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSwell : DrawableTaikoHitObject<Swell>
|
||||
{
|
||||
private const float target_ring_thick_border = 1.4f;
|
||||
private const float target_ring_thin_border = 1f;
|
||||
private const float target_ring_scale = 5f;
|
||||
private const float inner_ring_alpha = 0.65f;
|
||||
|
||||
/// <summary>
|
||||
/// Offset away from the start time of the swell at which the ring starts appearing.
|
||||
/// </summary>
|
||||
@ -38,9 +28,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private Vector2 baseSize;
|
||||
|
||||
private readonly Container<DrawableSwellTick> ticks;
|
||||
private readonly Container bodyContainer;
|
||||
private readonly CircularContainer targetRing;
|
||||
private readonly CircularContainer expandingRing;
|
||||
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
@ -51,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
/// </summary>
|
||||
public bool MustAlternate { get; internal set; } = true;
|
||||
|
||||
public event Action<int> UpdateHitProgress;
|
||||
|
||||
public DrawableSwell()
|
||||
: this(null)
|
||||
{
|
||||
@ -61,87 +50,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
FillMode = FillMode.Fit;
|
||||
|
||||
Content.Add(bodyContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
expandingRing = new CircularContainer
|
||||
{
|
||||
Name = "Expanding ring",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = inner_ring_alpha,
|
||||
}
|
||||
}
|
||||
},
|
||||
targetRing = new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thick border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thick_border,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thin border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thin_border,
|
||||
BorderColour = Color4.White,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddInternal(ticks = new Container<DrawableSwellTick> { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
expandingRing.Colour = colours.YellowLight;
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell),
|
||||
_ => new SwellCirclePiece
|
||||
_ => new DefaultSwell
|
||||
{
|
||||
// to allow for rotation transform
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
protected override void RecreatePieces()
|
||||
@ -208,16 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
int numHits = ticks.Count(r => r.IsHit);
|
||||
|
||||
float completion = (float)numHits / HitObject.RequiredHits;
|
||||
|
||||
expandingRing
|
||||
.FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50)
|
||||
.Then()
|
||||
.FadeTo(completion / 8, 2000, Easing.OutQuint);
|
||||
|
||||
MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
|
||||
|
||||
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
|
||||
UpdateHitProgress?.Invoke(numHits);
|
||||
|
||||
if (numHits == HitObject.RequiredHits)
|
||||
ApplyMaxResult();
|
||||
@ -248,28 +156,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms()
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
|
||||
using (BeginDelayedSequence(-ring_appear_offset))
|
||||
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
const double transition_duration = 300;
|
||||
base.UpdateHitStateTransforms(state);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
expandingRing.FadeTo(0);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.Delay(300).FadeOut();
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
this.FadeOut(transition_duration, Easing.Out);
|
||||
bodyContainer.ScaleTo(1.4f, transition_duration);
|
||||
this.Delay(660).FadeOut();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
20
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs
Normal file
20
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonSwell : DefaultSwell
|
||||
{
|
||||
protected override Drawable CreateCentreCircle()
|
||||
{
|
||||
return new ArgonSwellCirclePiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
return new ArgonHitExplosion(taikoComponent.Component);
|
||||
|
||||
case TaikoSkinComponents.Swell:
|
||||
return new ArgonSwellCirclePiece();
|
||||
return new ArgonSwell();
|
||||
}
|
||||
|
||||
break;
|
||||
|
190
osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs
Normal file
190
osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs
Normal file
@ -0,0 +1,190 @@
|
||||
// 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.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
{
|
||||
public partial class DefaultSwell : Container
|
||||
{
|
||||
private const float target_ring_thick_border = 1.4f;
|
||||
private const float target_ring_thin_border = 1f;
|
||||
private const float target_ring_scale = 5f;
|
||||
private const float inner_ring_alpha = 0.65f;
|
||||
|
||||
private DrawableSwell drawableSwell = null!;
|
||||
|
||||
private readonly Container bodyContainer;
|
||||
private readonly CircularContainer targetRing;
|
||||
private readonly CircularContainer expandingRing;
|
||||
private readonly Drawable centreCircle;
|
||||
private int numHits;
|
||||
|
||||
public DefaultSwell()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Content.Add(bodyContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1,
|
||||
Children = new[]
|
||||
{
|
||||
expandingRing = new CircularContainer
|
||||
{
|
||||
Name = "Expanding ring",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = inner_ring_alpha,
|
||||
}
|
||||
}
|
||||
},
|
||||
targetRing = new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thick border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thick_border,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thin border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thin_border,
|
||||
BorderColour = Color4.White,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
centreCircle = CreateCentreCircle(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject hitObject, OsuColour colours)
|
||||
{
|
||||
drawableSwell = (DrawableSwell)hitObject;
|
||||
drawableSwell.UpdateHitProgress += animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
expandingRing.Colour = colours.YellowLight;
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected virtual Drawable CreateCentreCircle()
|
||||
{
|
||||
return new SwellCirclePiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
private void animateSwellProgress(int numHits)
|
||||
{
|
||||
this.numHits = numHits;
|
||||
|
||||
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
|
||||
expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
|
||||
|
||||
centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation,
|
||||
(float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed));
|
||||
expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X,
|
||||
1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed)));
|
||||
expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSwell))
|
||||
return;
|
||||
|
||||
Swell swell = drawableSwell.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(swell.StartTime))
|
||||
{
|
||||
if (state == ArmedState.Idle)
|
||||
expandingRing.FadeTo(0);
|
||||
|
||||
const double ring_appear_offset = 100;
|
||||
|
||||
targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
|
||||
{
|
||||
const double transition_duration = 300;
|
||||
|
||||
bodyContainer.FadeOut(transition_duration, Easing.OutQuad);
|
||||
bodyContainer.ScaleTo(1.4f, transition_duration);
|
||||
centreCircle.FadeOut(transition_duration, Easing.OutQuad);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSwell.IsNotNull())
|
||||
{
|
||||
drawableSwell.UpdateHitProgress -= animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
201
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs
Normal file
201
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs
Normal file
@ -0,0 +1,201 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Audio;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacySwell : Container
|
||||
{
|
||||
private const float scale_adjust = 768f / 480;
|
||||
private static readonly Vector2 swell_display_position = new Vector2(250f, 100f);
|
||||
|
||||
private DrawableSwell drawableSwell = null!;
|
||||
|
||||
private Container bodyContainer = null!;
|
||||
private Sprite warning = null!;
|
||||
private Sprite spinnerCircle = null!;
|
||||
private Sprite approachCircle = null!;
|
||||
private Sprite clearAnimation = null!;
|
||||
private SkinnableSound clearSample = null!;
|
||||
private LegacySpriteText remainingHitsText = null!;
|
||||
|
||||
private bool samplePlayed;
|
||||
|
||||
private int numHits;
|
||||
|
||||
public LegacySwell()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject hitObject, ISkinSource skin)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
warning = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-warning"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
bodyContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
spinnerCircle = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-circle"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
approachCircle = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-approachcircle"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.86f * 0.8f),
|
||||
Alpha = 0.8f,
|
||||
},
|
||||
remainingHitsText = new LegacySpriteText(LegacyFont.Score)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(0f, 130f),
|
||||
Scale = Vector2.One,
|
||||
},
|
||||
}
|
||||
},
|
||||
clearAnimation = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-osu"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
Y = -40,
|
||||
},
|
||||
},
|
||||
},
|
||||
clearSample = new SkinnableSound(new SampleInfo("spinner-osu")),
|
||||
};
|
||||
|
||||
drawableSwell = (DrawableSwell)hitObject;
|
||||
drawableSwell.UpdateHitProgress += animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void animateSwellProgress(int numHits)
|
||||
{
|
||||
this.numHits = numHits;
|
||||
remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture);
|
||||
spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
int requiredHits = drawableSwell.HitObject.RequiredHits;
|
||||
int remainingHits = requiredHits - numHits;
|
||||
remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously(
|
||||
remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed)));
|
||||
|
||||
spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed));
|
||||
spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously(
|
||||
spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed)));
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSwell))
|
||||
return;
|
||||
|
||||
Swell swell = drawableSwell.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(swell.StartTime))
|
||||
{
|
||||
if (state == ArmedState.Idle)
|
||||
{
|
||||
remainingHitsText.Text = $"{swell.RequiredHits}";
|
||||
samplePlayed = false;
|
||||
}
|
||||
|
||||
const double body_transition_duration = 200;
|
||||
|
||||
warning.MoveTo(swell_display_position, body_transition_duration)
|
||||
.ScaleTo(3, body_transition_duration, Easing.Out)
|
||||
.FadeOut(body_transition_duration);
|
||||
|
||||
bodyContainer.FadeIn(body_transition_duration);
|
||||
approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
|
||||
{
|
||||
const double clear_transition_duration = 300;
|
||||
const double clear_fade_in = 120;
|
||||
|
||||
bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad);
|
||||
spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad);
|
||||
|
||||
if (state == ArmedState.Hit)
|
||||
{
|
||||
if (!samplePlayed)
|
||||
{
|
||||
clearSample.Play();
|
||||
samplePlayed = true;
|
||||
}
|
||||
|
||||
clearAnimation
|
||||
.MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out)
|
||||
.ScaleTo(0.4f)
|
||||
.ScaleTo(1f, clear_fade_in * 2, Easing.Out)
|
||||
.FadeIn()
|
||||
.Delay(clear_fade_in * 3)
|
||||
.FadeOut(clear_fade_in * 2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSwell.IsNotNull())
|
||||
{
|
||||
drawableSwell.UpdateHitProgress -= animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return this.GetAnimation("sliderscorepoint", false, false);
|
||||
|
||||
case TaikoSkinComponents.Swell:
|
||||
// todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
|
||||
if (GetTexture("spinner-circle") != null)
|
||||
return new LegacySwell();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.HitTarget:
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomModValidity()
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
// For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment.
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomFreeModValidity()
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
// For now, all rate adjustment mods aren't allowed as free mods in multiplayer.
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
}
|
||||
|
||||
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
|
@ -8,11 +8,14 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
SpectatorList list = null!;
|
||||
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
|
||||
TestSpectatorClient client = new TestSpectatorClient();
|
||||
TestSpectatorClient spectatorClient = new TestSpectatorClient();
|
||||
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
|
||||
|
||||
AddStep("create spectator list", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
client,
|
||||
spectatorClient,
|
||||
multiplayerClient,
|
||||
new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(GameplayState), gameplayState),
|
||||
(typeof(SpectatorClient), client)
|
||||
(typeof(SpectatorClient), spectatorClient),
|
||||
(typeof(MultiplayerClient), multiplayerClient),
|
||||
],
|
||||
Child = list = new SpectatorList
|
||||
{
|
||||
@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddRepeatStep("add a user", () =>
|
||||
{
|
||||
int id = Interlocked.Increment(ref counter);
|
||||
((ISpectatorClient)client).UserStartedWatching([
|
||||
((ISpectatorClient)spectatorClient).UserStartedWatching([
|
||||
new SpectatorUser
|
||||
{
|
||||
OnlineID = id,
|
||||
@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
]);
|
||||
}, 10);
|
||||
|
||||
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
|
||||
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
|
||||
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
|
||||
|
||||
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
|
||||
|
@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Y = -ScreenFooter.HEIGHT,
|
||||
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
|
||||
FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods },
|
||||
},
|
||||
footer = new ScreenFooter(),
|
||||
},
|
||||
|
@ -12,11 +12,14 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModsAndRuleset()
|
||||
{
|
||||
AddStep("add another user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 0,
|
||||
Username = "User 0",
|
||||
RulesetsStatistics = new Dictionary<string, UserStatistics>
|
||||
{
|
||||
{
|
||||
Ruleset.Value.ShortName,
|
||||
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
|
||||
}
|
||||
},
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
});
|
||||
|
||||
MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable());
|
||||
});
|
||||
|
||||
AddStep("set user styles", () =>
|
||||
{
|
||||
MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1);
|
||||
MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID,
|
||||
[new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]);
|
||||
|
||||
MultiplayerClient.ChangeUserStyle(0, 259, 2);
|
||||
MultiplayerClient.ChangeUserMods(0,
|
||||
[new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]);
|
||||
});
|
||||
}
|
||||
|
||||
private void createNewParticipantsList()
|
||||
{
|
||||
ParticipantsList? participantsList = null;
|
||||
|
@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void assertQueueTabCount(int count)
|
||||
{
|
||||
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
|
||||
string queueTabText = count > 0 ? $"Up next ({count})" : "Up next";
|
||||
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
|
||||
{
|
||||
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()
|
||||
|
@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "please load a beatmap!",
|
||||
Title = "no beatmaps available!"
|
||||
Artist = "please select or load a beatmap!",
|
||||
Title = "no beatmap selected!"
|
||||
},
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
Difficulty = new BeatmapDifficulty
|
||||
|
@ -96,7 +96,6 @@ namespace osu.Game.Collections
|
||||
lastCreated = collections[changes.InsertedIndices[0]].ID;
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
|
||||
{
|
||||
var updatedItem = collections[i];
|
||||
|
||||
|
@ -15,6 +15,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@ -27,7 +28,7 @@ namespace osu.Game.Collections
|
||||
/// </summary>
|
||||
public partial class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>, IFilterable
|
||||
{
|
||||
private const float item_height = 35;
|
||||
private const float item_height = 45;
|
||||
private const float button_width = item_height * 0.75f;
|
||||
|
||||
protected TextBox TextBox => content.TextBox;
|
||||
@ -92,13 +93,11 @@ namespace osu.Game.Collections
|
||||
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
TextBox = new ItemTextBox
|
||||
TextBox = new ItemTextBox(collection)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
CornerRadius = item_height / 2,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = item_height,
|
||||
CommitOnFocusLost = true,
|
||||
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -125,11 +124,57 @@ namespace osu.Game.Collections
|
||||
{
|
||||
protected override float LeftRightPadding => item_height / 2;
|
||||
|
||||
private const float count_text_size = 12;
|
||||
|
||||
private readonly Live<BeatmapCollection> collection;
|
||||
|
||||
private OsuSpriteText countText = null!;
|
||||
|
||||
public ItemTextBox(Live<BeatmapCollection> collection)
|
||||
{
|
||||
this.collection = collection;
|
||||
|
||||
CornerRadius = item_height / 2;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f);
|
||||
BackgroundFocused = colours.GreySeaFoam;
|
||||
|
||||
if (collection.IsManaged)
|
||||
{
|
||||
TextContainer.Height *= (Height - count_text_size) / Height;
|
||||
TextContainer.Margin = new MarginPadding { Bottom = count_text_size };
|
||||
|
||||
TextContainer.Add(countText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Depth = float.MinValue,
|
||||
Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold),
|
||||
Margin = new MarginPadding { Top = 2, Left = 2 },
|
||||
Colour = colours.Yellow
|
||||
});
|
||||
|
||||
// interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly.
|
||||
// the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s -
|
||||
// but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections),
|
||||
// but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one.
|
||||
// when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly.
|
||||
int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count);
|
||||
|
||||
countText.Text = count == 1
|
||||
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
|
||||
// but also in this case we want support for formatting a number within a string).
|
||||
? $"{count:#,0} item"
|
||||
: $"{count:#,0} items";
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaceholderText = "Create a new collection";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,7 +255,7 @@ namespace osu.Game.Collections
|
||||
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
|
||||
}
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms => [(LocalisableString)Model.Value.Name];
|
||||
public IEnumerable<LocalisableString> FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []);
|
||||
|
||||
private bool matchingFilter = true;
|
||||
|
||||
|
@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings
|
||||
};
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and
|
||||
/// changes in order would cause key bindings to get associated with the wrong action.
|
||||
/// </remarks>
|
||||
public enum GlobalAction
|
||||
{
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))]
|
||||
|
@ -194,6 +194,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks");
|
||||
|
||||
/// <summary>
|
||||
/// "Open beatmap info page"
|
||||
/// </summary>
|
||||
public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page");
|
||||
|
||||
/// <summary>
|
||||
/// "Open beatmap discussion page"
|
||||
/// </summary>
|
||||
public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
public partial class TimeInfoContainer : BottomBarContainer
|
||||
{
|
||||
private OsuSpriteText bpm = null!;
|
||||
private OsuSpriteText progress = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components
|
||||
bpm = new OsuSpriteText
|
||||
{
|
||||
Colour = colours.Orange1,
|
||||
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||
Spacing = new Vector2(-1, 0),
|
||||
Position = new Vector2(0, 4),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
progress = new OsuSpriteText
|
||||
{
|
||||
Colour = colours.Purple1,
|
||||
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||
Spacing = new Vector2(-1, 0),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
|
||||
Position = new Vector2(2, 4),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private double? lastBPM;
|
||||
private double? lastProgress;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
|
||||
double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100);
|
||||
|
||||
if (lastBPM != newBPM)
|
||||
{
|
||||
lastBPM = newBPM;
|
||||
bpm.Text = @$"{newBPM:0} BPM";
|
||||
}
|
||||
|
||||
if (lastProgress != newProgress)
|
||||
{
|
||||
lastProgress = newProgress;
|
||||
progress.Text = @$"{newProgress:0}%";
|
||||
}
|
||||
}
|
||||
|
||||
private partial class TimestampControl : OsuClickableContainer
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -14,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
{
|
||||
public partial class TestGameplayButton : OsuButton
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected override SpriteText CreateText() => new OsuSpriteText
|
||||
{
|
||||
Depth = -1,
|
||||
@ -24,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
BackgroundColour = colours.Orange1;
|
||||
SpriteText.Colour = colourProvider.Background6;
|
||||
@ -33,5 +37,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
|
||||
Text = EditorStrings.TestBeatmap;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
Background.FadeColour(colours.Orange0, 500, Easing.OutQuint);
|
||||
// don't call base in order to block scale animation
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
Background.FadeColour(colours.Orange1, 300, Easing.OutQuint);
|
||||
// don't call base in order to block scale animation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1273,6 +1273,15 @@ namespace osu.Game.Screens.Edit
|
||||
yield return upload;
|
||||
}
|
||||
|
||||
if (editorBeatmap.BeatmapInfo.OnlineID > 0)
|
||||
{
|
||||
yield return new OsuMenuItemSpacer();
|
||||
yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard,
|
||||
() => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset)));
|
||||
yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard,
|
||||
() => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}"));
|
||||
}
|
||||
|
||||
yield return new OsuMenuItemSpacer();
|
||||
yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit);
|
||||
}
|
||||
|
@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2,
|
||||
Margin = new MarginPadding { Bottom = 2 }
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
@ -11,31 +11,22 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
||||
public partial class FooterButtonFreeMods : FooterButton
|
||||
{
|
||||
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>();
|
||||
public readonly IBindable<bool> Freestyle = new Bindable<bool>();
|
||||
|
||||
public Bindable<IReadOnlyList<Mod>> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
current.Current = value;
|
||||
}
|
||||
}
|
||||
protected override bool IsActive => FreeMods.Value.Count > 0;
|
||||
|
||||
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
|
||||
|
||||
@ -104,7 +95,8 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateModDisplay(), true);
|
||||
Freestyle.BindValueChanged(_ => updateModDisplay());
|
||||
FreeMods.BindValueChanged(_ => updateModDisplay(), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -114,16 +106,16 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
var availableMods = allAvailableAndValidMods.ToArray();
|
||||
|
||||
Current.Value = Current.Value.Count == availableMods.Length
|
||||
FreeMods.Value = FreeMods.Value.Count == availableMods.Length
|
||||
? Array.Empty<Mod>()
|
||||
: availableMods;
|
||||
}
|
||||
|
||||
private void updateModDisplay()
|
||||
{
|
||||
int currentCount = Current.Value.Count;
|
||||
int currentCount = FreeMods.Value.Count;
|
||||
|
||||
if (currentCount == allAvailableAndValidMods.Count())
|
||||
if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value)
|
||||
{
|
||||
count.Text = "all";
|
||||
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
||||
|
@ -8,23 +8,18 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue<bool>
|
||||
public partial class FooterButtonFreestyle : FooterButton
|
||||
{
|
||||
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
|
||||
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
|
||||
|
||||
public Bindable<bool> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
protected override bool IsActive => Freestyle.Value;
|
||||
|
||||
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
|
||||
|
||||
@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
public FooterButtonFreestyle()
|
||||
{
|
||||
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
|
||||
base.Action = () => current.Value = !current.Value;
|
||||
base.Action = () => Freestyle.Value = !Freestyle.Value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateDisplay(), true);
|
||||
Freestyle.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (current.Value)
|
||||
if (Freestyle.Value)
|
||||
{
|
||||
text.Text = "on";
|
||||
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -440,11 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
var rulesetInstance = GetGameplayRuleset().CreateInstance();
|
||||
|
||||
Mod[] allowedMods = item.Freestyle
|
||||
? rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray()
|
||||
: item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
// Remove any user mods that are no longer allowed.
|
||||
Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(UserMods.Value))
|
||||
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
|
||||
UserMods.Value = newUserMods;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
int beatmapId = GetGameplayBeatmap().OnlineID;
|
||||
@ -455,14 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
Ruleset.Value = GetGameplayRuleset();
|
||||
|
||||
bool freeMod = item.AllowedMods.Any();
|
||||
bool freestyle = item.Freestyle;
|
||||
|
||||
// For now, the game can never be in a state where freemod and freestyle are on at the same time.
|
||||
// This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert.
|
||||
Debug.Assert(!freeMod || !freestyle);
|
||||
|
||||
if (freeMod)
|
||||
if (allowedMods.Length > 0)
|
||||
{
|
||||
UserModsSection.Show();
|
||||
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
@ -474,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
UserModsSelectOverlay.IsValidMod = _ => false;
|
||||
}
|
||||
|
||||
if (freestyle)
|
||||
if (item.Freestyle)
|
||||
{
|
||||
UserStyleSection.Show();
|
||||
|
||||
@ -487,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
|
||||
{
|
||||
AllowReordering = false,
|
||||
AllowEditing = freestyle,
|
||||
AllowEditing = true,
|
||||
RequestEdit = _ => OpenStyleSelection()
|
||||
};
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
|
||||
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
||||
|
||||
protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer;
|
||||
|
||||
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod;
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
new Drawable?[]
|
||||
{
|
||||
// Participants column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -118,15 +117,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
}
|
||||
}
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Beatmap column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[] { new OverlinedHeader("Beatmap") },
|
||||
new Drawable[] { new OverlinedHeader("Beatmap queue") },
|
||||
new Drawable[]
|
||||
{
|
||||
addItemButton = new AddItemButton
|
||||
@ -147,80 +153,67 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
SelectedItem = SelectedItem
|
||||
}
|
||||
},
|
||||
new Drawable[]
|
||||
new[]
|
||||
{
|
||||
new Container
|
||||
UserModsSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Children = new[]
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
UserModsSection = new FillFlowContainer
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
new UserModSelectButton
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Height = 30,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
}
|
||||
},
|
||||
UserStyleSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
new ModDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
new[]
|
||||
{
|
||||
UserStyleSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
}
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Main right column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Right = 70 },
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userStyleDisplay = new StyleDisplayIcon(),
|
||||
userStyleDisplay = new StyleDisplayIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
userModsDisplay = new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.5f),
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
}
|
||||
@ -209,15 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
const double fade_time = 50;
|
||||
|
||||
MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
|
||||
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
|
||||
if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem)
|
||||
{
|
||||
int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID;
|
||||
int userRulesetId = User.RulesetId ?? currentItem.RulesetID;
|
||||
Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance();
|
||||
|
||||
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
|
||||
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
|
||||
int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank;
|
||||
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
|
||||
|
||||
if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID)
|
||||
userStyleDisplay.Style = null;
|
||||
else
|
||||
userStyleDisplay.Style = (userBeatmapId, userRulesetId);
|
||||
|
||||
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
|
||||
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
|
||||
Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty<Mod>() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList());
|
||||
}
|
||||
|
||||
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
|
||||
|
||||
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
|
||||
if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating)
|
||||
{
|
||||
userModsDisplay.FadeIn(fade_time);
|
||||
userStyleDisplay.FadeIn(fade_time);
|
||||
@ -228,20 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
userStyleDisplay.FadeOut(fade_time);
|
||||
}
|
||||
|
||||
if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID))
|
||||
userStyleDisplay.Style = null;
|
||||
else
|
||||
userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0);
|
||||
|
||||
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
|
||||
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
|
||||
|
||||
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
|
||||
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
|
||||
Schedule(() =>
|
||||
{
|
||||
userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty<Mod>();
|
||||
});
|
||||
}
|
||||
|
||||
public MenuItem[]? ContextMenuItems
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
|
||||
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
protected readonly Bindable<bool> Freestyle = new Bindable<bool>();
|
||||
protected readonly Bindable<bool> Freestyle = new Bindable<bool>(true);
|
||||
|
||||
private readonly Room room;
|
||||
private readonly PlaylistItem? initialItem;
|
||||
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
freeModSelect = new FreeModSelectOverlay
|
||||
{
|
||||
SelectedMods = { BindTarget = FreeMods },
|
||||
IsValidMod = IsValidFreeMod,
|
||||
IsValidMod = isValidFreeMod,
|
||||
};
|
||||
}
|
||||
|
||||
@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
ModsFooterButton.Enabled.Value = false;
|
||||
|
||||
@ -144,10 +145,10 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
|
||||
FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList();
|
||||
|
||||
// Reset the validity delegate to update the overlay's display.
|
||||
freeModSelect.IsValidMod = IsValidFreeMod;
|
||||
freeModSelect.IsValidMod = isValidFreeMod;
|
||||
}
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
|
||||
@ -194,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
|
||||
{
|
||||
IsValidMod = IsValidMod
|
||||
IsValidMod = isValidMod
|
||||
};
|
||||
|
||||
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
|
||||
@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
|
||||
{
|
||||
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null),
|
||||
(new FooterButtonFreestyle { Current = Freestyle }, null)
|
||||
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
|
||||
{
|
||||
FreeMods = { BindTarget = FreeMods },
|
||||
Freestyle = { BindTarget = Freestyle }
|
||||
}, null),
|
||||
(new FooterButtonFreestyle
|
||||
{
|
||||
Freestyle = { BindTarget = Freestyle }
|
||||
}, null)
|
||||
});
|
||||
|
||||
return baseButtons;
|
||||
@ -217,18 +225,18 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
/// </summary>
|
||||
/// <param name="mod">The <see cref="Mod"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
|
||||
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable);
|
||||
private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.
|
||||
/// </summary>
|
||||
/// <param name="mod">The <see cref="Mod"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
|
||||
protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod);
|
||||
|
||||
private bool checkCompatibleFreeMod(Mod mod)
|
||||
=> Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods.
|
||||
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods.
|
||||
private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type)
|
||||
// Mod must not be contained in the required mods.
|
||||
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
|
||||
// Mod must be compatible with all the required mods.
|
||||
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
new Drawable?[]
|
||||
{
|
||||
// Playlist items column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -176,73 +175,67 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
new Dimension(),
|
||||
}
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Middle column (mods and leaderboard)
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
new[]
|
||||
{
|
||||
new Container
|
||||
UserModsSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
Children = new[]
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
UserModsSection = new FillFlowContainer
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
new UserModSelectButton
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
}
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Height = 30,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
},
|
||||
UserStyleSection = new FillFlowContainer
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
new[]
|
||||
{
|
||||
UserStyleSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
}
|
||||
},
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -273,12 +266,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
}
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Main right column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -2,7 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Localisation.HUD;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -28,17 +31,17 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
private const int max_spectators_displayed = 10;
|
||||
|
||||
public BindableList<SpectatorUser> Spectators { get; } = new BindableList<SpectatorUser>();
|
||||
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
|
||||
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
|
||||
public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
protected OsuSpriteText Header { get; private set; } = null!;
|
||||
private BindableList<SpectatorUser> watchingUsers { get; } = new BindableList<SpectatorUser>();
|
||||
private Bindable<LocalUserPlayingState> userPlayingState { get; } = new Bindable<LocalUserPlayingState>();
|
||||
private int displayedSpectatorCount;
|
||||
|
||||
private OsuSpriteText header = null!;
|
||||
private FillFlowContainer mainFlow = null!;
|
||||
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
|
||||
private DrawablePool<SpectatorListEntry> pool = null!;
|
||||
@ -49,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[Resolved]
|
||||
private GameplayState gameplayState { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -63,7 +69,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Header = new OsuSpriteText
|
||||
header = new OsuSpriteText
|
||||
{
|
||||
Colour = colours.Blue0,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
||||
@ -78,18 +84,18 @@ namespace osu.Game.Screens.Play.HUD
|
||||
pool = new DrawablePool<SpectatorListEntry>(max_spectators_displayed),
|
||||
};
|
||||
|
||||
HeaderColour.Value = Header.Colour;
|
||||
HeaderColour.Value = header.Colour;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((IBindableList<SpectatorUser>)Spectators).BindTo(client.WatchingUsers);
|
||||
((IBindable<LocalUserPlayingState>)UserPlayingState).BindTo(gameplayState.PlayingState);
|
||||
((IBindableList<SpectatorUser>)watchingUsers).BindTo(client.WatchingUsers);
|
||||
((IBindable<LocalUserPlayingState>)userPlayingState).BindTo(gameplayState.PlayingState);
|
||||
|
||||
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
|
||||
UserPlayingState.BindValueChanged(_ => updateVisibility());
|
||||
watchingUsers.BindCollectionChanged(onSpectatorsChanged, true);
|
||||
userPlayingState.BindValueChanged(_ => updateVisibility());
|
||||
|
||||
Font.BindValueChanged(_ => updateAppearance());
|
||||
HeaderColour.BindValueChanged(_ => updateAppearance(), true);
|
||||
@ -100,6 +106,20 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
// the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores.
|
||||
// this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`.
|
||||
//
|
||||
// we do not generally wish to display other players in the room as spectators due to that implementation detail,
|
||||
// therefore this code is intended to filter out those players on the client side.
|
||||
//
|
||||
// note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions
|
||||
// (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls).
|
||||
// the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either)
|
||||
// is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component.
|
||||
var excludedUserIds = new HashSet<int>();
|
||||
if (multiplayerClient.Room != null)
|
||||
excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID));
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
@ -109,6 +129,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
var spectator = (SpectatorUser)e.NewItems![i]!;
|
||||
int index = Math.Max(e.NewStartingIndex, 0) + i;
|
||||
|
||||
if (excludedUserIds.Contains(spectator.OnlineID))
|
||||
continue;
|
||||
|
||||
if (index >= max_spectators_displayed)
|
||||
break;
|
||||
|
||||
@ -125,10 +148,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
for (int i = 0; i < spectatorsFlow.Count; i++)
|
||||
spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i);
|
||||
|
||||
if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
|
||||
if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
|
||||
{
|
||||
for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++)
|
||||
addNewSpectatorToList(i, Spectators[i]);
|
||||
addNewSpectatorToList(i, watchingUsers[i]);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -144,7 +167,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper();
|
||||
displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID));
|
||||
header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper();
|
||||
updateVisibility();
|
||||
|
||||
for (int i = 0; i < spectatorsFlow.Count; i++)
|
||||
@ -160,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
var entry = pool.Get(entry =>
|
||||
{
|
||||
entry.Current.Value = spectator;
|
||||
entry.UserPlayingState = UserPlayingState;
|
||||
entry.UserPlayingState = userPlayingState;
|
||||
});
|
||||
|
||||
spectatorsFlow.Insert(i, entry);
|
||||
@ -169,15 +193,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private void updateVisibility()
|
||||
{
|
||||
// We don't want to show spectators when we are watching a replay.
|
||||
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
|
||||
mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateAppearance()
|
||||
{
|
||||
Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
|
||||
Header.Colour = HeaderColour.Value;
|
||||
header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
|
||||
header.Colour = HeaderColour.Value;
|
||||
|
||||
Width = Header.DrawWidth;
|
||||
Width = header.DrawWidth;
|
||||
}
|
||||
|
||||
private partial class SpectatorListEntry : PoolableDrawable
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking
|
||||
return null;
|
||||
|
||||
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
|
||||
getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
|
||||
getScoreRequest.Success += r =>
|
||||
{
|
||||
var toDisplay = new List<ScoreInfo>();
|
||||
|
||||
for (int i = 0; i < r.Scores.Count; ++i)
|
||||
{
|
||||
var score = r.Scores[i];
|
||||
int position = i + 1;
|
||||
|
||||
if (score.MatchesOnlineID(Score))
|
||||
{
|
||||
// we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect,
|
||||
// so we have to fish out the actual drawable panel and set the position to it directly.
|
||||
var panel = ScorePanelList.GetPanelForScore(Score);
|
||||
Score.Position = panel.ScorePosition.Value = position;
|
||||
}
|
||||
else
|
||||
{
|
||||
var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo);
|
||||
converted.Position = position;
|
||||
toDisplay.Add(converted);
|
||||
}
|
||||
}
|
||||
|
||||
scoresCallback.Invoke(toDisplay);
|
||||
};
|
||||
return getScoreRequest;
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Used to show an initial animation hinting at the enabled state.
|
||||
/// </summary>
|
||||
protected virtual bool IsActive => false;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => SpriteText?.Text ?? default;
|
||||
@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
base.LoadComplete();
|
||||
Enabled.BindValueChanged(_ => updateDisplay(), true);
|
||||
|
||||
if (IsActive)
|
||||
{
|
||||
box.ClearTransforms();
|
||||
|
||||
using (box.BeginDelayedSequence(200))
|
||||
{
|
||||
box.FadeIn(200)
|
||||
.Then()
|
||||
.FadeOut(1500, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Action Hovered;
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -292,5 +293,45 @@ namespace osu.Game.Utils
|
||||
|
||||
return rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a mod can be applied to playlist items in the given match type.
|
||||
/// </summary>
|
||||
/// <param name="mod">The mod to test.</param>
|
||||
/// <param name="type">The match type.</param>
|
||||
public static bool IsValidModForMatchType(Mod mod, MatchType type)
|
||||
{
|
||||
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MatchType.Playlists:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return mod.ValidForMultiplayer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a mod can be applied as a free mod to playlist items in the given match type.
|
||||
/// </summary>
|
||||
/// <param name="mod">The mod to test.</param>
|
||||
/// <param name="type">The match type.</param>
|
||||
public static bool IsValidFreeModForMatchType(Mod mod, MatchType type)
|
||||
{
|
||||
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MatchType.Playlists:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return mod.ValidForMultiplayerAsFreeMod;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace osu.iOS
|
||||
updateOrientation();
|
||||
}
|
||||
|
||||
private void updateOrientation()
|
||||
private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() =>
|
||||
{
|
||||
bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad;
|
||||
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad);
|
||||
@ -60,7 +60,7 @@ namespace osu.iOS
|
||||
appDelegate.Orientations = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user