1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00

Merge branch 'master' into beatmap-overlay-modded-stats

This commit is contained in:
Salman Ahmed 2022-07-23 10:38:13 +03:00 committed by GitHub
commit 2829a7e836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1491 additions and 313 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.720.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.722.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth;
public override int Version => 20220701;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public override int Version => 20220701;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private const double difficulty_multiplier = 0.0675;
private double hitWindowGreat;
public override int Version => 20220701;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -23,13 +23,29 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; }
private ControlPointInfo controlPointInfo { get; set; }
[BackgroundDependencyLoader]
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint());
IWorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
IWorkingBeatmap beatmap = CreateWorkingBeatmap(CreateBeatmap(new TaikoRuleset().RulesetInfo));
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset)) }
});
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
return new Beatmap
{
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo
@ -41,19 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Title = @"Sample Beatmap",
Author = { Username = @"peppy" },
},
Ruleset = new TaikoRuleset().RulesetInfo
Ruleset = ruleset
},
ControlPointInfo = controlPointInfo
});
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
};
}
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Child = new InputDrum(playfield.HitObjectContainer)
Child = new InputDrum()
}
});
}

View File

@ -0,0 +1,49 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneDrumTouchInputArea : OsuTestScene
{
private DrumTouchInputArea drumTouchInputArea = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create drum", () =>
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new InputDrum
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
};
});
}
[Test]
public void TestDrum()
{
AddStep("show drum", () => drumTouchInputArea.Show());
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private const double colour_skill_multiplier = 0.01;
private const double stamina_skill_multiplier = 0.021;
public override int Version => 20220701;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -10,8 +10,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@ -115,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Rim;
public readonly Sprite Centre;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped)
{
Masking = true;
@ -152,12 +147,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (e.Action == CentreAction)
{
target = Centre;
sampleTriggerSource.Play(HitType.Centre);
}
else if (e.Action == RimAction)
{
target = Rim;
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -4,11 +4,13 @@
#nullable disable
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko
{
[Cached] // Used for touch input, see DrumTouchInputArea.
public class TaikoInputManager : RulesetInputManager<TaikoAction>
{
public TaikoInputManager(RulesetInfo ruleset)

View File

@ -8,18 +8,18 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Skinning;
@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
protected override void UpdateAfterChildren()

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
{
InternalChildren = new Drawable[]
{
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
switch (e.Action)
{
case TaikoAction.LeftRim:
leftRimSampleTriggerSource.Play(HitType.Rim);
break;
case TaikoAction.LeftCentre:
leftCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightCentre:
rightCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightRim:
rightRimSampleTriggerSource.Play(HitType.Rim);
break;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
}
}
}

View File

@ -0,0 +1,243 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
/// <summary>
/// An overlay that captures and displays osu!taiko mouse and touch input.
/// </summary>
public class DrumTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
private KeyBindingContainer<TaikoAction> keyBindingContainer = null!;
private readonly Dictionary<object, TaikoAction> trackedActions = new Dictionary<object, TaikoAction>();
private Container mainContent = null!;
private QuarterCircle leftCentre = null!;
private QuarterCircle rightCentre = null!;
private QuarterCircle leftRim = null!;
private QuarterCircle rightRim = null!;
[BackgroundDependencyLoader]
private void load(TaikoInputManager taikoInputManager, OsuColour colours)
{
Debug.Assert(taikoInputManager.KeyBindingContainer != null);
keyBindingContainer = taikoInputManager.KeyBindingContainer;
// Container should handle input everywhere.
RelativeSizeAxes = Axes.Both;
const float centre_region = 0.80f;
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = 350,
Y = 20,
Masking = true,
FillMode = FillMode.Fit,
Children = new Drawable[]
{
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
},
rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Rotation = 90,
},
leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
Scale = new Vector2(centre_region),
},
rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Scale = new Vector2(centre_region),
Rotation = 90,
}
}
},
}
},
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!validMouse(e))
return false;
handleDown(e.Button, e.ScreenSpaceMousePosition);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!validMouse(e))
return;
handleUp(e.Button);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
handleUp(e.Touch.Source);
base.OnTouchUp(e);
}
private void handleDown(object source, Vector2 position)
{
Show();
TaikoAction taikoAction = getTaikoActionFromInput(position);
// Not too sure how this can happen, but let's avoid throwing.
if (trackedActions.ContainsKey(source))
return;
trackedActions.Add(source, taikoAction);
keyBindingContainer.TriggerPressed(taikoAction);
}
private void handleUp(object source)
{
keyBindingContainer.TriggerReleased(trackedActions[source]);
trackedActions.Remove(source);
}
private bool validMouse(MouseButtonEvent e) =>
leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition);
private TaikoAction getTaikoActionFromInput(Vector2 inputPosition)
{
bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition);
bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2;
if (leftSide)
return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim;
return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim;
}
protected override void PopIn()
{
mainContent.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
mainContent.FadeOut(300);
}
private class QuarterCircle : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly Circle overlay;
private readonly TaikoAction handledAction;
private readonly Circle circle;
public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos);
public QuarterCircle(TaikoAction handledAction, Color4 colour)
{
this.handledAction = handledAction;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
InternalChildren = new Drawable[]
{
new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colour.Multiply(1.4f).Darken(2.8f),
Alpha = 0.8f,
Scale = new Vector2(2),
},
overlay = new Circle
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = colour,
Scale = new Vector2(2),
}
}
},
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeTo(1f, 80, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeOut(1000, Easing.OutQuint);
}
}
}
}

View File

@ -12,8 +12,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osuTK;
@ -27,13 +25,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
private const float middle_split = 0.025f;
[Cached]
private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(HitObjectContainer hitObjectContainer)
public InputDrum()
{
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
}
@ -48,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
},
sampleTriggerSource
};
}
@ -116,9 +108,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centre;
private readonly Sprite centreHit;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
Masking = true;
@ -179,15 +168,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{
target = centreHit;
back = centre;
sampleTriggerSource.Play(HitType.Centre);
}
else if (e.Action == RimAction)
{
target = rimHit;
back = rim;
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
inputDrum = new InputDrum(HitObjectContainer)
inputDrum = new InputDrum
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -164,6 +164,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy(),
new DrumSamplePlayer(HitObjectContainer),
// this is added at the end of the hierarchy to receive input before taiko objects.
// but is proxied below everything to not cover visual effects such as hit explosions.
inputDrum,

View File

@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database
{
[HeadlessTest]
public class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
{
public IBindable<bool> IsPlaying => isPlaying;
private readonly Bindable<bool> isPlaying = new Bindable<bool>();
private BeatmapSetInfo importedSet = null!;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely();
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Set not playing", () => isPlaying.Value = false);
}
[Test]
public void TestDifficultyProcessing()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
[Test]
public void TestDifficultyProcessingWhilePlaying()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Set playing", () => isPlaying.Value = true);
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddWaitStep("wait some", 500);
AddAssert("Difficulty still not populated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
});
});
AddStep("Set not playing", () => isPlaying.Value = false);
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
public class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -15,6 +16,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
@ -91,6 +93,47 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
}
[Test]
public void TestFailScore()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.SmallBonus),
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.LargeBonus),
}
};
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.LargeTickHit });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].CreateJudgement()) { Type = HitResult.SmallTickMiss });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].CreateJudgement()) { Type = HitResult.SmallBonus });
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
scoreProcessor.FailScore(score);
Assert.That(score.Rank, Is.EqualTo(ScoreRank.F));
Assert.That(score.Passed, Is.False);
Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7));
Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2));
Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
@ -100,5 +143,17 @@ namespace osu.Game.Tests.Gameplay
MaxResult = maxResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public TestHitObject(HitResult maxResult = HitResult.Perfect)
{
this.maxResult = maxResult;
}
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
}
}
}

View File

@ -24,7 +24,6 @@ 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.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
@ -633,7 +632,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);

View File

@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneModPresetPanel : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestVariousModPresets()
{
AddStep("create content", () => Child = new FillFlowContainer
{
Width = 300,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(0, 5),
ChildrenEnumerable = createTestPresets().Select(preset => new ModPresetPanel(preset))
});
}
private static IEnumerable<ModPreset> createTestPresets() => new[]
{
new ModPreset
{
Name = "First preset",
Description = "Please ignore",
Mods = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime()
}
},
new ModPreset
{
Name = "AR0",
Description = "For good readers",
Mods = new Mod[]
{
new OsuModDifficultyAdjust
{
ApproachRate = { Value = 0 }
}
}
},
new ModPreset
{
Name = "This preset is going to have an extraordinarily long name",
Description = "This is done so that the capability to truncate overlong texts may be demonstrated",
Mods = new Mod[]
{
new OsuModFlashlight(),
new OsuModSpinIn()
}
}
};
}
}

View File

@ -0,0 +1,137 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
namespace osu.Game
{
public class BackgroundBeatmapProcessor : Component
{
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
protected virtual int TimeToSleepDuringGameplay => 30000;
protected override void LoadComplete()
{
base.LoadComplete();
Task.Run(() =>
{
Logger.Log("Beginning background beatmap processing..");
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
}).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
{
Logger.Log("Finished background aborted during shutdown");
return;
}
Logger.Log("Finished background beatmap processing!");
});
}
/// <summary>
/// Check whether the databased difficulty calculation version matches the latest ruleset provided version.
/// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated.
/// </summary>
private void checkForOutdatedStarRatings()
{
foreach (var ruleset in rulesetStore.AvailableRulesets)
{
// beatmap being passed in is arbitrary here. just needs to be non-null.
int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version;
if (ruleset.LastAppliedDifficultyVersion < currentVersion)
{
Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})");
int countReset = 0;
realmAccess.Write(r =>
{
foreach (var b in r.All<BeatmapInfo>())
{
if (b.Ruleset.ShortName == ruleset.ShortName)
{
b.StarRating = -1;
countReset++;
}
}
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
}
}
}
private void processBeatmapSetsWithMissingMetrics()
{
HashSet<Guid> beatmapSetIds = new HashSet<Guid>();
Logger.Log("Querying for beatmap sets to reprocess...");
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)))
{
Debug.Assert(b.BeatmapSet != null);
beatmapSetIds.Add(b.BeatmapSet.ID);
}
});
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
int i = 0;
foreach (var id in beatmapSetIds)
{
while (localUserPlayInfo?.IsPlaying.Value == true)
{
Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
realmAccess.Run(r =>
{
var set = r.Find<BeatmapSetInfo>(id);
if (set != null)
{
Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})");
beatmapUpdater.Process(set);
}
});
}
}
}
}

View File

@ -87,7 +87,11 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; } = string.Empty;
public double StarRating { get; set; }
/// <summary>
/// Defaults to -1 (meaning not-yet-calculated).
/// Will likely be superseded with a better storage considering ruleset/mods.
/// </summary>
public double StarRating { get; set; } = -1;
[Indexed]
public string MD5Hash { get; set; } = string.Empty;

View File

@ -63,8 +63,9 @@ namespace osu.Game.Database
/// 17 2022-07-16 Added CountryCode to RealmUser.
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
/// </summary>
private const int schema_version = 19;
private const int schema_version = 20;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -780,6 +781,15 @@ namespace osu.Game.Database
case 14:
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.UserSettings = new BeatmapUserSettings();
break;
case 20:
// As we now have versioned difficulty calculations, let's reset
// all star ratings and have `BackgroundBeatmapProcessor` recalculate them.
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.StarRating = -1;
break;
}
}

View File

@ -904,6 +904,8 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
// Multiplayer modes need to increase poll rate temporarily.
API.Activity.BindValueChanged(_ => updateChatPollRate(), true);

View File

@ -280,8 +280,7 @@ namespace osu.Game
AddInternal(difficultyCache);
// TODO: OsuGame or OsuGameBase?
beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
Children = new Drawable[]
{
@ -69,7 +69,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = multiplier_value_area_width + ModPanel.CORNER_RADIUS
Width = multiplier_value_area_width + ModSelectPanel.CORNER_RADIUS
},
new GridContainer
{
@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[]
{
contentBackground = new Box

View File

@ -105,20 +105,20 @@ namespace osu.Game.Overlays.Mods
TopLevelContent = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = ModPanel.CORNER_RADIUS,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModPanel.CORNER_RADIUS,
Height = header_height + ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[]
{
headerBackground = new Box
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModPanel.CORNER_RADIUS
Height = header_height + ModSelectPanel.CORNER_RADIUS
},
headerText = new OsuTextFlowContainer(t =>
{
@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Mods
Padding = new MarginPadding
{
Horizontal = 17,
Bottom = ModPanel.CORNER_RADIUS
Bottom = ModSelectPanel.CORNER_RADIUS
}
}
}
@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
BorderThickness = 3,
Children = new Drawable[]
{

View File

@ -1,144 +1,42 @@
// 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.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public class ModPanel : OsuClickableContainer
public class ModPanel : ModSelectPanel
{
public Mod Mod => modState.Mod;
public BindableBool Active => modState.Active;
public override BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70;
private readonly ModState modState;
protected readonly Box Background;
protected readonly Container SwitchContainer;
protected readonly Container MainContentContainer;
protected readonly Box TextBackground;
protected readonly FillFlowContainer TextFlow;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
protected const double TRANSITION_DURATION = 150;
public const float CORNER_RADIUS = 7;
protected const float HEIGHT = 42;
protected const float IDLE_SWITCH_WIDTH = 54;
protected const float EXPANDED_SWITCH_WIDTH = 70;
private Colour4 activeColour;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff;
private Sample? sampleOn;
public ModPanel(ModState modState)
{
this.modState = modState;
RelativeSizeAxes = Axes.X;
Height = 42;
Title = Mod.Name;
Description = Mod.Description;
// all below properties are applied to `Content` rather than the `ModPanel` in its entirety
// to allow external components to set these properties on the panel without affecting
// its "internal" appearance.
Content.Masking = true;
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Children = new Drawable[]
SwitchContainer.Child = new ModSwitchSmall(Mod)
{
Background = new Box
{
RelativeSizeAxes = Axes.Both
},
SwitchContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Child = new ModSwitchSmall(Mod)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Active = { BindTarget = Active },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
}
},
MainContentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
TextBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
TextFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 17.5f,
Vertical = 4
},
Direction = FillDirection.Vertical,
Children = new[]
{
new OsuSpriteText
{
Text = Mod.Name,
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
}
},
new OsuSpriteText
{
Text = Mod.Description,
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
}
}
}
}
}
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Active = { BindTarget = Active },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
};
Action = Active.Toggle;
}
public ModPanel(Mod mod)
@ -146,122 +44,21 @@ namespace osu.Game.Overlays.Mods
{
}
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler)
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
activeColour = colours.ForModType(Mod.Type);
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
AccentColour = colours.ForModType(Mod.Type);
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ =>
{
playStateChangeSamples();
UpdateState();
});
Filtered.BindValueChanged(_ => updateFilterState(), true);
UpdateState();
FinishTransforms(true);
}
private void playStateChangeSamples()
{
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
}
protected override bool OnHover(HoverEvent e)
{
UpdateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateState();
base.OnHoverLost(e);
}
private bool mouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
mouseDown = true;
UpdateState();
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
mouseDown = false;
UpdateState();
base.OnMouseUp(e);
}
protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : ColourProvider.Background3;
protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : ColourProvider.Background2;
protected virtual Colour4 TextColour => Active.Value ? ColourProvider.Background6 : Colour4.White;
protected virtual void UpdateState()
{
float targetWidth = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH;
double transitionDuration = TRANSITION_DURATION;
Colour4 backgroundColour = BackgroundColour;
Colour4 foregroundColour = ForegroundColour;
Colour4 textColour = TextColour;
// Hover affects colour of button background
if (IsHovered)
{
backgroundColour = backgroundColour.Lighten(0.1f);
foregroundColour = foregroundColour.Lighten(0.1f);
}
// Mouse down adds a halfway tween of the movement
if (mouseDown)
{
targetWidth = (float)Interpolation.Lerp(IDLE_SWITCH_WIDTH, EXPANDED_SWITCH_WIDTH, 0.5f);
transitionDuration *= 4;
}
Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint);
Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint);
SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint);
MainContentContainer.TransformTo(nameof(Padding), new MarginPadding
{
Left = targetWidth,
Right = CORNER_RADIUS
}, transitionDuration, Easing.OutQuint);
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
}
#region Filtering support
public void ApplyFilter(Func<Mod, bool>? filter)
{
Filtered.Value = filter != null && !filter.Invoke(Mod);
}
private void updateFilterState()
{
this.FadeTo(Filtered.Value ? 0 : 1);

View File

@ -0,0 +1,35 @@
// 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.Cursor;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>
{
public readonly ModPreset Preset;
public override BindableBool Active { get; } = new BindableBool();
public ModPresetPanel(ModPreset preset)
{
Preset = preset;
Title = preset.Name;
Description = preset.Description;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Orange1;
}
public ModPreset TooltipContent => Preset;
public ITooltip<ModPreset> GetCustomTooltip() => new ModPresetTooltip(ColourProvider);
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModPresetTooltip : VisibilityContainer, ITooltip<ModPreset>
{
protected override Container<Drawable> Content { get; }
private const double transition_duration = 200;
public ModPresetTooltip(OverlayColourProvider colourProvider)
{
Width = 250;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 7;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6
},
Content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7)
}
};
}
private ModPreset? lastPreset;
public void SetContent(ModPreset preset)
{
if (preset == lastPreset)
return;
lastPreset = preset;
Content.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod));
}
protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private class ModPresetRow : FillFlowContainer
{
public ModPresetRow(Mod mod)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(4);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
};
if (!string.IsNullOrEmpty(mod.SettingDescription))
{
AddInternal(new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 14 },
Text = mod.SettingDescription
});
}
}
}
}
}

View File

@ -0,0 +1,252 @@
// 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.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectPanel : OsuClickableContainer, IHasAccentColour
{
public abstract BindableBool Active { get; }
public Color4 AccentColour { get; set; }
public LocalisableString Title
{
get => titleText.Text;
set => titleText.Text = value;
}
public LocalisableString Description
{
get => descriptionText.Text;
set => descriptionText.Text = value;
}
public const float CORNER_RADIUS = 7;
protected const float HEIGHT = 42;
protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3;
protected virtual Colour4 ForegroundColour => Active.Value ? AccentColour : ColourProvider.Background2;
protected virtual Colour4 TextColour => Active.Value ? ColourProvider.Background6 : Colour4.White;
protected const double TRANSITION_DURATION = 150;
protected readonly Box Background;
protected readonly Container SwitchContainer;
protected readonly Container MainContentContainer;
protected readonly Box TextBackground;
protected readonly FillFlowContainer TextFlow;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
private readonly OsuSpriteText titleText;
private readonly OsuSpriteText descriptionText;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff;
private Sample? sampleOn;
protected ModSelectPanel()
{
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
// all below properties are applied to `Content` rather than the `ModPanel` in its entirety
// to allow external components to set these properties on the panel without affecting
// its "internal" appearance.
Content.Masking = true;
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Children = new Drawable[]
{
Background = new Box
{
RelativeSizeAxes = Axes.Both
},
SwitchContainer = new Container
{
RelativeSizeAxes = Axes.Y,
},
MainContentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
TextBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
TextFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 17.5f,
Vertical = 4
},
Direction = FillDirection.Vertical,
Children = new[]
{
titleText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
}
},
descriptionText = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
}
}
}
}
}
}
};
Action = () => Active.Toggle();
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
}
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ =>
{
playStateChangeSamples();
UpdateState();
});
UpdateState();
FinishTransforms(true);
}
private void playStateChangeSamples()
{
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
}
protected override bool OnHover(HoverEvent e)
{
UpdateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateState();
base.OnHoverLost(e);
}
private bool mouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
mouseDown = true;
UpdateState();
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
mouseDown = false;
UpdateState();
base.OnMouseUp(e);
}
protected virtual void UpdateState()
{
float targetWidth = Active.Value ? ExpandedSwitchWidth : IdleSwitchWidth;
double transitionDuration = TRANSITION_DURATION;
Colour4 backgroundColour = BackgroundColour;
Colour4 foregroundColour = ForegroundColour;
Colour4 textColour = TextColour;
// Hover affects colour of button background
if (IsHovered)
{
backgroundColour = backgroundColour.Lighten(0.1f);
foregroundColour = foregroundColour.Lighten(0.1f);
}
// Mouse down adds a halfway tween of the movement
if (mouseDown)
{
targetWidth = (float)Interpolation.Lerp(IdleSwitchWidth, ExpandedSwitchWidth, 0.5f);
transitionDuration *= 4;
}
Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint);
Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint);
SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint);
MainContentContainer.TransformTo(nameof(Padding), new MarginPadding
{
Left = targetWidth,
Right = CORNER_RADIUS
}, transitionDuration, Easing.OutQuint);
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
}
}
}

View File

@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Difficulty
private readonly IRulesetInfo ruleset;
private readonly IWorkingBeatmap beatmap;
/// <summary>
/// A yymmdd version which is used to discern when reprocessing is required.
/// </summary>
public virtual int Version => 0;
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
{
this.ruleset = ruleset;

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// A mod preset is a named collection of configured mods.
/// Presets are presented to the user in the mod select overlay for convenience.
/// </summary>
public class ModPreset
{
/// <summary>
/// The ruleset that the preset is valid for.
/// </summary>
public RulesetInfo RulesetInfo { get; set; } = null!;
/// <summary>
/// The name of the mod preset.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the mod preset.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The set of configured mods that are part of the preset.
/// </summary>
public ICollection<Mod> Mods { get; set; } = Array.Empty<Mod>();
}
}

View File

@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Testing;
using osu.Game.Rulesets.Difficulty;
using Realms;
namespace osu.Game.Rulesets
@ -22,6 +23,11 @@ namespace osu.Game.Rulesets
public string InstantiationInfo { get; set; } = string.Empty;
/// <summary>
/// Stores the last applied <see cref="DifficultyCalculator.Version"/>
/// </summary>
public int LastAppliedDifficultyVersion { get; set; }
public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID)
{
ShortName = shortName;
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets
Name = Name,
ShortName = ShortName,
InstantiationInfo = InstantiationInfo,
Available = Available
Available = Available,
LastAppliedDifficultyVersion = LastAppliedDifficultyVersion,
};
public Ruleset CreateInstance()

View File

@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Scoring
private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private Dictionary<HitResult, int>? maximumResultCounts;
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject;
@ -410,12 +413,16 @@ namespace osu.Game.Rulesets.Scoring
{
base.Reset(storeResults);
scoreResultCounts.Clear();
hitEvents.Clear();
lastHitObject = null;
if (storeResults)
{
maximumScoringValues = currentScoringValues;
maximumResultCounts = new Dictionary<HitResult, int>(scoreResultCounts);
}
scoreResultCounts.Clear();
currentScoringValues = default;
currentMaximumScoringValues = default;
@ -423,6 +430,7 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = 0;
Accuracy.Value = 1;
Combo.Value = 0;
Rank.Disabled = false;
Rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
}
@ -445,6 +453,36 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
}
/// <summary>
/// Populates the given score with remaining statistics as "missed" and marks it with <see cref="ScoreRank.F"/> rank.
/// </summary>
public void FailScore(ScoreInfo score)
{
if (Rank.Value == ScoreRank.F)
return;
score.Passed = false;
Rank.Value = ScoreRank.F;
Debug.Assert(maximumResultCounts != null);
if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick))
scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit);
if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick))
scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore;
int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value;
int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic;
PopulateScore(score);
}
public override void ResetFromReplayFrame(ReplayFrame frame)
{
base.ResetFromReplayFrame(frame);

View File

@ -267,12 +267,7 @@ namespace osu.Game.Screens.Play
},
FailOverlay = new FailOverlay
{
SaveReplay = () =>
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
return prepareAndImportScore();
},
SaveReplay = prepareAndImportScore,
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
@ -831,7 +826,6 @@ namespace osu.Game.Screens.Play
return false;
GameplayState.HasFailed = true;
Score.ScoreInfo.Passed = false;
updateGameplayState();
@ -849,9 +843,16 @@ namespace osu.Game.Screens.Play
return true;
}
// Called back when the transform finishes
/// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
private void onFailComplete()
{
// fail completion is a good point to mark a score as failed,
// since the last judgement that caused the fail only applies to score processor after onFail.
// todo: this should probably be handled better.
ScoreProcessor.FailScore(Score.ScoreInfo);
GameplayClockContainer.Stop();
FailOverlay.Retries = RestartCount;
@ -1028,10 +1029,7 @@ namespace osu.Game.Screens.Play
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null)
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
}
ScoreProcessor.FailScore(Score.ScoreInfo);
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.

View File

@ -133,7 +133,7 @@ namespace osu.Game.Screens.Ranking.Expanded
FillMode = FillMode.Fit,
}
},
scoreCounter = new TotalScoreCounter
scoreCounter = new TotalScoreCounter(!withFlair)
{
Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 },

View File

@ -4,6 +4,8 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
@ -21,13 +23,19 @@ namespace osu.Game.Screens.Ranking.Expanded
{
private readonly APIUser user;
private Sample appearanceSample;
private readonly bool playAppearanceSound;
/// <summary>
/// Creates a new <see cref="ExpandedPanelTopContent"/>.
/// </summary>
/// <param name="user">The <see cref="APIUser"/> to display.</param>
public ExpandedPanelTopContent(APIUser user)
/// <param name="playAppearanceSound">Whether the appearance sample should play</param>
public ExpandedPanelTopContent(APIUser user, bool playAppearanceSound = false)
{
this.user = user;
this.playAppearanceSound = playAppearanceSound;
Anchor = Anchor.TopCentre;
Origin = Anchor.Centre;
@ -35,8 +43,10 @@ namespace osu.Game.Screens.Ranking.Expanded
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
appearanceSample = audio.Samples.Get(@"Results/score-panel-top-appear");
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -62,5 +72,13 @@ namespace osu.Game.Screens.Ranking.Expanded
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
if (playAppearanceSound)
appearanceSample?.Play();
}
}
}

View File

@ -3,7 +3,11 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -22,11 +26,35 @@ namespace osu.Game.Screens.Ranking.Expanded
protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING;
public TotalScoreCounter()
private readonly bool playSamples;
private readonly Bindable<double> tickPlaybackRate = new Bindable<double>();
private double lastSampleTime;
private DrawableSample sampleTick;
public TotalScoreCounter(bool playSamples = false)
{
// Todo: AutoSize X removed here due to https://github.com/ppy/osu-framework/issues/3369
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
this.playSamples = playSamples;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
AddInternal(sampleTick = new DrawableSample(audio.Samples.Get(@"Results/score-tick-lesser")));
}
protected override void LoadComplete()
{
base.LoadComplete();
if (playSamples)
Current.BindValueChanged(_ => startTicking());
}
protected override LocalisableString FormatCount(long count) => count.ToString("N0");
@ -39,5 +67,35 @@ namespace osu.Game.Screens.Ranking.Expanded
s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true);
s.Spacing = new Vector2(-5, 0);
});
public override long DisplayedCount
{
get => base.DisplayedCount;
set
{
if (base.DisplayedCount == value)
return;
base.DisplayedCount = value;
if (playSamples && Time.Current > lastSampleTime + tickPlaybackRate.Value)
{
sampleTick?.Play();
lastSampleTime = Time.Current;
}
}
}
private void startTicking()
{
const double tick_debounce_rate_start = 10f;
const double tick_debounce_rate_end = 100f;
const double tick_volume_start = 0.5f;
const double tick_volume_end = 1.0f;
this.TransformBindableTo(tickPlaybackRate, tick_debounce_rate_start);
this.TransformBindableTo(tickPlaybackRate, tick_debounce_rate_end, RollingDuration, Easing.OutSine);
sampleTick.VolumeTo(tick_volume_start).Then().VolumeTo(tick_volume_end, RollingDuration, Easing.OutSine);
}
}
}

View File

@ -7,6 +7,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -60,6 +62,8 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry;
private readonly bool allowWatchingReplay;
private Sample popInSample;
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
@ -70,10 +74,12 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
FillFlowContainer buttons;
popInSample = audio.Samples.Get(@"UI/overlay-pop-in");
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -244,6 +250,8 @@ namespace osu.Game.Screens.Ranking
});
bottomPanel.FadeTo(1, 250);
popInSample?.Play();
}
public override bool OnExiting(ScreenExitEvent e)

View File

@ -6,13 +6,16 @@
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded;
@ -93,9 +96,12 @@ namespace osu.Game.Screens.Ranking
public readonly ScoreInfo Score;
private bool displayWithFlair;
[Resolved]
private OsuGameBase game { get; set; }
private Container content;
private DrawableAudioMixer mixer;
private bool displayWithFlair;
private Container topLayerContainer;
private Drawable topLayerBackground;
@ -107,6 +113,8 @@ namespace osu.Game.Screens.Ranking
private Container middleLayerContentContainer;
private Drawable middleLayerContent;
private DrawableSample samplePanelFocus;
public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
{
Score = score;
@ -116,13 +124,13 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
// ScorePanel doesn't include the top extruding area in its own size.
// Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
const float vertical_fudge = 20;
InternalChild = content = new Container
InternalChild = mixer = new DrawableAudioMixer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -174,7 +182,8 @@ namespace osu.Game.Screens.Ranking
},
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
}
}
},
samplePanelFocus = new DrawableSample(audio.Samples.Get(@"Results/score-panel-focus"))
}
};
}
@ -202,12 +211,32 @@ namespace osu.Game.Screens.Ranking
state = value;
if (IsLoaded)
{
updateState();
if (value == PanelState.Expanded)
playAppearSample();
}
StateChanged?.Invoke(value);
}
}
protected override void Update()
{
base.Update();
mixer.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1;
}
private void playAppearSample()
{
var channel = samplePanelFocus?.GetChannel();
if (channel == null) return;
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.2);
channel.Play();
}
private void updateState()
{
topLayerContent?.FadeOut(content_fade_duration).Expire();
@ -221,7 +250,8 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User) { Alpha = 0 });
bool firstLoad = topLayerContent == null;
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User, firstLoad) { Alpha = 0 });
middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 });
// only the first expanded display should happen with flair.
@ -244,7 +274,7 @@ namespace osu.Game.Screens.Ranking
break;
}
content.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0;

View File

@ -8,6 +8,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -35,6 +37,10 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly Container content;
private readonly LoadingSpinner spinner;
private bool wasOpened;
private Sample popInSample;
private Sample popOutSample;
public StatisticsPanel()
{
InternalChild = new Container
@ -56,9 +62,12 @@ namespace osu.Game.Screens.Ranking.Statistics
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
Score.BindValueChanged(populateStatistics, true);
popInSample = audio.Samples.Get(@"Results/statistics-panel-pop-in");
popOutSample = audio.Samples.Get(@"Results/statistics-panel-pop-out");
}
private CancellationTokenSource loadCancellation;
@ -216,9 +225,21 @@ namespace osu.Game.Screens.Ranking.Statistics
return true;
}
protected override void PopIn() => this.FadeIn(150, Easing.OutQuint);
protected override void PopIn()
{
this.FadeIn(150, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(150, Easing.OutQuint);
popInSample?.Play();
wasOpened = true;
}
protected override void PopOut()
{
this.FadeOut(150, Easing.OutQuint);
if (wasOpened)
popOutSample?.Play();
}
protected override void Dispose(bool isDisposing)
{

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.14.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.720.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" />

View File

@ -61,8 +61,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.720.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup>
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.720.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.722.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />