1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 07:33:20 +08:00

Merge pull request #26254 from frenzibyte/mania-combo-counter

Add argon/classic osu!mania combo counter
This commit is contained in:
Dean Herbert 2024-08-15 17:56:28 +09:00 committed by GitHub
commit 5710f0f302
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 516 additions and 164 deletions

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
CreateTest(); CreateTest();
} }
AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyComboCounter>().Any()); AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyDefaultComboCounter>().Any());
} }
} }
} }

View File

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -32,12 +31,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup: case SkinComponentsContainerLookup containerLookup:
if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) // Only handle per ruleset defaults here.
return base.GetDrawableComponent(lookup);
// Modifications for global components.
if (containerLookup.Ruleset == null) if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup) as Container; return base.GetDrawableComponent(lookup);
// Skin has configuration. // Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
@ -48,6 +44,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null; return null;
// Our own ruleset components default. // Our own ruleset components default.
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
@ -69,6 +68,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
} }
}; };
}
return null;
case CatchSkinComponentLookup catchSkinComponent: case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)

View File

@ -0,0 +1,38 @@
// 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.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Argon;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestSceneComboCounter : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup", () => SetContents(s =>
{
if (s is ArgonSkin)
return new ArgonManiaComboCounter();
if (s is LegacySkin)
return new LegacyManiaComboCounter();
return new LegacyManiaComboCounter();
}));
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
}
}

View File

@ -3,15 +3,22 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
public partial class TestScenePlayfield : ManiaSkinnableTestScene public partial class TestScenePlayfield : ManiaSkinnableTestScene
{ {
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
private List<StageDefinition> stageDefinitions = new List<StageDefinition>(); private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test] [Test]
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Child = new ManiaPlayfield(stageDefinitions) Child = new ManiaPlayfield(stageDefinitions)
}); });
}); });
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
} }
[TestCase(2)] [TestCase(2)]
@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
} }
}); });
}); });
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
} }
protected override IBeatmap CreateBeatmapForSkinProvider() protected override IBeatmap CreateBeatmapForSkinProvider()

View File

@ -0,0 +1,36 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
// play with a converted beatmap to allow dual stages mod to work.
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo());
protected override bool HasCustomSteps => true;
[Test]
public void TestSingleStage()
{
AddStep("Load single stage", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
[Test]
public void TestDualStage()
{
AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() }));
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonManiaComboCounter : ArgonComboCounter
{
protected override bool DisplayXSymbol => false;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
// the logic of flipping the position of the combo counter w.r.t. the direction does not work with "Closest" anchor,
// because it always forces the anchor to be top or bottom based on scrolling direction.
UsesFixedAnchor = true;
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
}
}
}

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -26,6 +28,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
switch (lookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.ShowLabel.Value = false;
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
})
{
new ArgonManiaComboCounter(),
};
}
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent: case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported. // This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)

View File

@ -0,0 +1,91 @@
// 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.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyManiaComboCounter : LegacyComboCounter
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
DisplayedCountText.Anchor = Anchor.Centre;
DisplayedCountText.Origin = Anchor.Centre;
PopOutCountText.Anchor = Anchor.Centre;
PopOutCountText.Origin = Anchor.Centre;
PopOutCountText.Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red;
}
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (!Anchor.HasFlag(Anchor.y1))
{
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
}
// since we flip the vertical anchor when changing scroll direction,
// we can use the sign of the Y value as an indicator to make the combo counter displayed correctly.
if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up))
Y = -Y;
}
protected override void OnCountIncrement()
{
base.OnCountIncrement();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f))
.ScaleTo(new Vector2(1f), 300, Easing.Out)
.FadeIn(120);
}
protected override void OnCountChange()
{
base.OnCountChange();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(1f);
}
protected override void OnCountRolling()
{
if (DisplayedCount > 0)
{
PopOutCountText.Text = FormatCount(DisplayedCount);
PopOutCountText.FadeTo(0.8f).FadeOut(200)
.ScaleTo(1f).ScaleTo(4f, 200);
DisplayedCountText.FadeTo(0.5f, 300);
}
base.OnCountRolling();
}
}
}

View File

@ -5,9 +5,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -78,6 +80,40 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
switch (lookup) switch (lookup)
{ {
case SkinComponentsContainerLookup containerLookup:
// Modifications for global components.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
})
{
new LegacyManiaComboCounter(),
};
}
return null;
case GameplaySkinComponentLookup<HitResult> resultComponent: case GameplaySkinComponentLookup<HitResult> resultComponent:
return getResult(resultComponent.Component); return getResult(resultComponent.Component);

View File

@ -74,11 +74,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// 340px is the default height inherit from stable // 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
} }
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
}
}) })
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new LegacyComboCounter(), new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
} }
}; };
@ -90,7 +99,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component) switch (osuComponent.Component)
{ {
case OsuSkinComponents.FollowPoint: case OsuSkinComponents.FollowPoint:
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false,
maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
case OsuSkinComponents.SliderScorePoint: case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);

View File

@ -443,7 +443,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyComboCounter>().Any()); AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyComboCounter>().Any());
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1); AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyComboCounter AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Drawable CreateArgonImplementation() => new ArgonComboCounter(); protected override Drawable CreateArgonImplementation() => new ArgonComboCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyDefaultComboCounter();
[Test] [Test]
public void TestComboCounterIncrementing() public void TestComboCounterIncrementing()

View File

@ -19,10 +19,12 @@ namespace osu.Game.Screens.Play.HUD
{ {
public partial class ArgonComboCounter : ComboCounter public partial class ArgonComboCounter : ComboCounter
{ {
private ArgonCounterTextComponent text = null!; protected ArgonCounterTextComponent Text = null!;
protected override double RollingDuration => 250; protected override double RollingDuration => 250;
protected virtual bool DisplayXSymbol => true;
[SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")] [SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{ {
@ -43,16 +45,16 @@ namespace osu.Game.Screens.Play.HUD
bool wasIncrease = combo.NewValue > combo.OldValue; bool wasIncrease = combo.NewValue > combo.OldValue;
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f);
float duration = wasMiss ? 2000 : 500; float duration = wasMiss ? 2000 : 500;
text.NumberContainer Text.NumberContainer
.ScaleTo(new Vector2(newScale)) .ScaleTo(new Vector2(newScale))
.ScaleTo(Vector2.One, duration, Easing.OutQuint); .ScaleTo(Vector2.One, duration, Easing.OutQuint);
if (wasMiss) if (wasMiss)
text.FlashColour(Color4.Red, duration, Easing.OutQuint); Text.FlashColour(Color4.Red, duration, Easing.OutQuint);
}); });
} }
@ -70,23 +72,23 @@ namespace osu.Game.Screens.Play.HUD
{ {
int digitsRequiredForDisplayCount = getDigitsRequiredForDisplayCount(); int digitsRequiredForDisplayCount = getDigitsRequiredForDisplayCount();
if (digitsRequiredForDisplayCount != text.WireframeTemplate.Length) if (digitsRequiredForDisplayCount != Text.WireframeTemplate.Length)
text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount); Text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount);
} }
private int getDigitsRequiredForDisplayCount() private int getDigitsRequiredForDisplayCount()
{ {
// one for the single presumed starting digit, one for the "x" at the end. // one for the single presumed starting digit, one for the "x" at the end (unless disabled).
int digitsRequired = 2; int digitsRequired = DisplayXSymbol ? 2 : 1;
long c = DisplayedCount; long c = DisplayedCount;
while ((c /= 10) > 0) while ((c /= 10) > 0)
digitsRequired++; digitsRequired++;
return digitsRequired; return digitsRequired;
} }
protected override LocalisableString FormatCount(int count) => $@"{count}x"; protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? $@"{count}x" : count.ToString();
protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) protected override IHasText CreateText() => Text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper())
{ {
WireframeOpacity = { BindTarget = WireframeOpacity }, WireframeOpacity = { BindTarget = WireframeOpacity },
ShowLabel = { BindTarget = ShowLabel }, ShowLabel = { BindTarget = ShowLabel },

View File

@ -32,6 +32,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -236,6 +237,9 @@ namespace osu.Game.Screens.Play
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods);
dependencies.CacheAs(DrawableRuleset); dependencies.CacheAs(DrawableRuleset);
if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset)
dependencies.CacheAs(scrollingRuleset.ScrollingInfo);
ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor = ruleset.CreateScoreProcessor();
ScoreProcessor.Mods.Value = gameplayMods; ScoreProcessor.Mods.Value = gameplayMods;
ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.ApplyBeatmap(playableBeatmap);

View File

@ -5,25 +5,17 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> /// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over. /// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary> /// </summary>
public partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable
{ {
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 }; public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
private uint scheduledPopOutCurrentId;
private const double big_pop_out_duration = 300;
private const double small_pop_out_duration = 100;
private const double fade_out_duration = 100; private const double fade_out_duration = 100;
/// <summary> /// <summary>
@ -31,9 +23,8 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
private const double rolling_duration = 20; private const double rolling_duration = 20;
private readonly Drawable popOutCount; protected readonly LegacySpriteText PopOutCountText;
protected readonly LegacySpriteText DisplayedCountText;
private readonly Drawable displayedCountSpriteText;
private int previousValue; private int previousValue;
@ -45,17 +36,10 @@ namespace osu.Game.Skinning
public bool UsesFixedAnchor { get; set; } public bool UsesFixedAnchor { get; set; }
public LegacyComboCounter() protected LegacyComboCounter()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
Margin = new MarginPadding(10);
Scale = new Vector2(1.28f);
InternalChildren = new[] InternalChildren = new[]
{ {
counterContainer = new Container counterContainer = new Container
@ -63,18 +47,16 @@ namespace osu.Game.Skinning
AlwaysPresent = true, AlwaysPresent = true,
Children = new[] Children = new[]
{ {
popOutCount = new LegacySpriteText(LegacyFont.Combo) PopOutCountText = new LegacySpriteText(LegacyFont.Combo)
{ {
Alpha = 0, Alpha = 0,
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
}, },
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) DisplayedCountText = new LegacySpriteText(LegacyFont.Combo)
{ {
Alpha = 0, Alpha = 0,
AlwaysPresent = true, AlwaysPresent = true,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
}, },
} }
@ -114,26 +96,12 @@ namespace osu.Game.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); DisplayedCountText.Text = FormatCount(Current.Value);
((IHasText)popOutCount).Text = formatCount(Current.Value); PopOutCountText.Text = FormatCount(Current.Value);
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
updateLayout(); counterContainer.Size = DisplayedCountText.Size;
}
private void updateLayout()
{
const float font_height_ratio = 0.625f;
const float vertical_offset = 9;
displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset);
displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset);
popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset);
counterContainer.Size = displayedCountSpriteText.Size;
} }
private void updateCount(bool rolling) private void updateCount(bool rolling)
@ -147,127 +115,84 @@ namespace osu.Game.Skinning
if (!rolling) if (!rolling)
{ {
FinishTransforms(false, nameof(DisplayedCount)); FinishTransforms(false, nameof(DisplayedCount));
isRolling = false; isRolling = false;
DisplayedCount = prev; DisplayedCount = prev;
if (prev + 1 == Current.Value) if (prev + 1 == Current.Value)
onCountIncrement(prev, Current.Value); OnCountIncrement();
else else
onCountChange(Current.Value); OnCountChange();
} }
else else
{ {
onCountRolling(displayedCount, Current.Value); OnCountRolling();
isRolling = true; isRolling = true;
} }
} }
private void transformPopOut(int newValue) /// <summary>
/// Raised when the counter should display the new value with transitions.
/// </summary>
protected virtual void OnCountIncrement()
{ {
((IHasText)popOutCount).Text = formatCount(newValue); if (DisplayedCount < Current.Value - 1)
DisplayedCount++;
popOutCount.ScaleTo(1.56f)
.ScaleTo(1, big_pop_out_duration);
popOutCount.FadeTo(0.6f)
.FadeOut(big_pop_out_duration);
}
private void transformNoPopOut(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1);
}
private void transformPopOutSmall(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1).Then()
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
}
private void scheduledPopOutSmall(uint id)
{
// Too late; scheduled task invalidated
if (id != scheduledPopOutCurrentId)
return;
DisplayedCount++; DisplayedCount++;
} }
private void onCountIncrement(int currentValue, int newValue) /// <summary>
/// Raised when the counter should roll to the new combo value (usually roll back to zero).
/// </summary>
protected virtual void OnCountRolling()
{ {
scheduledPopOutCurrentId++;
if (DisplayedCount < currentValue)
DisplayedCount++;
displayedCountSpriteText.Show();
transformPopOut(newValue);
uint newTaskId = scheduledPopOutCurrentId;
Scheduler.AddDelayed(delegate
{
scheduledPopOutSmall(newTaskId);
}, big_pop_out_duration - 140);
}
private void onCountRolling(int currentValue, int newValue)
{
scheduledPopOutCurrentId++;
// Hides displayed count if was increasing from 0 to 1 but didn't finish // Hides displayed count if was increasing from 0 to 1 but didn't finish
if (currentValue == 0 && newValue == 0) if (DisplayedCount == 0 && Current.Value == 0)
displayedCountSpriteText.FadeOut(fade_out_duration); DisplayedCountText.FadeOut(fade_out_duration);
transformRoll(currentValue, newValue); transformRoll(DisplayedCount, Current.Value);
} }
private void onCountChange(int newValue) /// <summary>
/// Raised when the counter should display the new combo value without any transitions.
/// </summary>
protected virtual void OnCountChange()
{ {
scheduledPopOutCurrentId++; if (Current.Value == 0)
DisplayedCountText.FadeOut();
if (newValue == 0) DisplayedCount = Current.Value;
displayedCountSpriteText.FadeOut();
DisplayedCount = newValue;
} }
private void onDisplayedCountRolling(int newValue) private void onDisplayedCountRolling(int newValue)
{ {
if (newValue == 0) if (newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration); DisplayedCountText.FadeOut(fade_out_duration);
else
displayedCountSpriteText.Show();
transformNoPopOut(newValue); DisplayedCountText.Text = FormatCount(newValue);
counterContainer.Size = DisplayedCountText.Size;
} }
private void onDisplayedCountChange(int newValue) private void onDisplayedCountChange(int newValue)
{ {
displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1);
transformNoPopOut(newValue); DisplayedCountText.Text = FormatCount(newValue);
counterContainer.Size = DisplayedCountText.Size;
} }
private void onDisplayedCountIncrement(int newValue) private void onDisplayedCountIncrement(int newValue)
{ {
displayedCountSpriteText.Show(); DisplayedCountText.Text = FormatCount(newValue);
transformPopOutSmall(newValue);
counterContainer.Size = DisplayedCountText.Size;
} }
private void transformRoll(int currentValue, int newValue) => private void transformRoll(int currentValue, int newValue) =>
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue));
private string formatCount(int count) => $@"{count}x"; protected virtual string FormatCount(int count) => $@"{count}";
private double getProportionalDuration(int currentValue, int newValue) private double getProportionalDuration(int currentValue, int newValue)
{ {

View File

@ -0,0 +1,85 @@
// 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.Threading;
using osuTK;
namespace osu.Game.Skinning
{
/// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary>
public partial class LegacyDefaultComboCounter : LegacyComboCounter
{
private const double big_pop_out_duration = 300;
private const double small_pop_out_duration = 100;
private ScheduledDelegate? scheduledPopOut;
public LegacyDefaultComboCounter()
{
Margin = new MarginPadding(10);
PopOutCountText.Anchor = Anchor.BottomLeft;
DisplayedCountText.Anchor = Anchor.BottomLeft;
}
protected override void LoadComplete()
{
base.LoadComplete();
const float font_height_ratio = 0.625f;
const float vertical_offset = 9;
DisplayedCountText.OriginPosition = new Vector2(0, font_height_ratio * DisplayedCountText.Height + vertical_offset);
DisplayedCountText.Position = new Vector2(0, -(1 - font_height_ratio) * DisplayedCountText.Height + vertical_offset);
PopOutCountText.OriginPosition = new Vector2(3, font_height_ratio * PopOutCountText.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
PopOutCountText.Position = new Vector2(0, -(1 - font_height_ratio) * PopOutCountText.Height + vertical_offset);
}
protected override void OnCountIncrement()
{
scheduledPopOut?.Cancel();
scheduledPopOut = null;
DisplayedCountText.Show();
PopOutCountText.Text = FormatCount(Current.Value);
PopOutCountText.ScaleTo(1.56f)
.ScaleTo(1, big_pop_out_duration);
PopOutCountText.FadeTo(0.6f)
.FadeOut(big_pop_out_duration);
this.Delay(big_pop_out_duration - 140).Schedule(() =>
{
base.OnCountIncrement();
DisplayedCountText.ScaleTo(1).Then()
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
}, out scheduledPopOut);
}
protected override void OnCountRolling()
{
scheduledPopOut?.Cancel();
scheduledPopOut = null;
base.OnCountRolling();
}
protected override void OnCountChange()
{
scheduledPopOut?.Cancel();
scheduledPopOut = null;
base.OnCountChange();
}
protected override string FormatCount(int count) => $@"{count}x";
}
}

View File

@ -39,6 +39,7 @@ namespace osu.Game.Skinning
public float HitPosition = DEFAULT_HIT_POSITION; public float HitPosition = DEFAULT_HIT_POSITION;
public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR;
public float ComboPosition = 111 * POSITION_SCALE_FACTOR;
public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR;
public bool ShowJudgementLine = true; public bool ShowJudgementLine = true;
public bool KeysUnderNotes; public bool KeysUnderNotes;

View File

@ -42,6 +42,7 @@ namespace osu.Game.Skinning
LeftLineWidth, LeftLineWidth,
RightLineWidth, RightLineWidth,
HitPosition, HitPosition,
ComboPosition,
ScorePosition, ScorePosition,
LightPosition, LightPosition,
StagePaddingTop, StagePaddingTop,
@ -63,6 +64,7 @@ namespace osu.Game.Skinning
JudgementLineColour, JudgementLineColour,
ColumnBackgroundColour, ColumnBackgroundColour,
ColumnLightColour, ColumnLightColour,
ComboBreakColour,
MinimumColumnWidth, MinimumColumnWidth,
LeftStageImage, LeftStageImage,
RightStageImage, RightStageImage,

View File

@ -94,6 +94,10 @@ namespace osu.Game.Skinning
currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break; break;
case "ComboPosition":
currentConfig.ComboPosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break;
case "ScorePosition": case "ScorePosition":
currentConfig.ScorePosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; currentConfig.ScorePosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break; break;

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
@ -24,6 +23,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
@ -155,6 +155,9 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.HitPosition: case LegacyManiaSkinConfigurationLookups.HitPosition:
return SkinUtils.As<TValue>(new Bindable<float>(existing.HitPosition)); return SkinUtils.As<TValue>(new Bindable<float>(existing.HitPosition));
case LegacyManiaSkinConfigurationLookups.ComboPosition:
return SkinUtils.As<TValue>(new Bindable<float>(existing.ComboPosition));
case LegacyManiaSkinConfigurationLookups.ScorePosition: case LegacyManiaSkinConfigurationLookups.ScorePosition:
return SkinUtils.As<TValue>(new Bindable<float>(existing.ScorePosition)); return SkinUtils.As<TValue>(new Bindable<float>(existing.ScorePosition));
@ -192,6 +195,9 @@ namespace osu.Game.Skinning
Debug.Assert(maniaLookup.ColumnIndex != null); Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As<TValue>(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}")); return SkinUtils.As<TValue>(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}"));
case LegacyManiaSkinConfigurationLookups.ComboBreakColour:
return SkinUtils.As<TValue>(getCustomColour(existing, "ColourBreak"));
case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(existing.MinimumColumnWidth)); return SkinUtils.As<TValue>(new Bindable<float>(existing.MinimumColumnWidth));
@ -361,10 +367,19 @@ namespace osu.Game.Skinning
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
if (containerLookup.Ruleset != null) if (containerLookup.Ruleset != null)
{ {
return new Container return new DefaultSkinComponentsContainer(container =>
{ {
RelativeSizeAxes = Axes.Both, var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
Child = new LegacyComboCounter(),
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
}
})
{
new LegacyDefaultComboCounter()
}; };
} }

View File

@ -213,6 +213,7 @@ namespace osu.Game.Skinning
// handle namespace changes... // handle namespace changes...
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress");
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter");
jsonContent = jsonContent.Replace(@"osu.Game.Skinning.LegacyComboCounter", @"osu.Game.Skinning.LegacyDefaultComboCounter");
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.PerformancePointsCounter", @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter"); jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.PerformancePointsCounter", @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter");
try try
@ -250,13 +251,15 @@ namespace osu.Game.Skinning
{ {
case 1: case 1:
{ {
// Combo counters were moved out of the global HUD components into per-ruleset.
// This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area).
if (target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents || if (target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents ||
!layout.TryGetDrawableInfo(null, out var globalHUDComponents) || !layout.TryGetDrawableInfo(null, out var globalHUDComponents) ||
resources == null) resources == null)
break; break;
var comboCounters = globalHUDComponents.Where(c => var comboCounters = globalHUDComponents.Where(c =>
c.Type.Name == nameof(LegacyComboCounter) || c.Type.Name == nameof(LegacyDefaultComboCounter) ||
c.Type.Name == nameof(DefaultComboCounter) || c.Type.Name == nameof(DefaultComboCounter) ||
c.Type.Name == nameof(ArgonComboCounter)).ToArray(); c.Type.Name == nameof(ArgonComboCounter)).ToArray();