1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 15:27:30 +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();
}
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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -32,12 +31,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case SkinComponentsContainerLookup containerLookup:
if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents)
return base.GetDrawableComponent(lookup);
// Modifications for global components.
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup) as Container;
return base.GetDrawableComponent(lookup);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
@ -48,27 +44,33 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
// Our own ruleset components default.
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
switch (containerLookup.Target)
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
if (keyCounter != null)
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
}
};
if (keyCounter != null)
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
}
};
}
return null;
case CatchSkinComponentLookup catchSkinComponent:
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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestScenePlayfield : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test]
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
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)]
@ -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()

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.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
@ -26,6 +28,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
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:
// This should eventually be moved to a skin setting, when supported.
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.Collections.Generic;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -78,6 +80,40 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
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:
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
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[]
{
new LegacyComboCounter(),
new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(),
}
};
@ -90,7 +99,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component)
{
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:
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("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,
Origin = Anchor.Centre,

View File

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

View File

@ -19,10 +19,12 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonComboCounter : ComboCounter
{
private ArgonCounterTextComponent text = null!;
protected ArgonCounterTextComponent Text = null!;
protected override double RollingDuration => 250;
protected virtual bool DisplayXSymbol => true;
[SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")]
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 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;
text.NumberContainer
Text.NumberContainer
.ScaleTo(new Vector2(newScale))
.ScaleTo(Vector2.One, duration, Easing.OutQuint);
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();
if (digitsRequiredForDisplayCount != text.WireframeTemplate.Length)
text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount);
if (digitsRequiredForDisplayCount != Text.WireframeTemplate.Length)
Text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount);
}
private int getDigitsRequiredForDisplayCount()
{
// one for the single presumed starting digit, one for the "x" at the end.
int digitsRequired = 2;
// one for the single presumed starting digit, one for the "x" at the end (unless disabled).
int digitsRequired = DisplayXSymbol ? 2 : 1;
long c = DisplayedCount;
while ((c /= 10) > 0)
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 },
ShowLabel = { BindTarget = ShowLabel },

View File

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

View File

@ -5,25 +5,17 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Skinning
{
/// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary>
public partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable
public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable
{
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;
/// <summary>
@ -31,9 +23,8 @@ namespace osu.Game.Skinning
/// </summary>
private const double rolling_duration = 20;
private readonly Drawable popOutCount;
private readonly Drawable displayedCountSpriteText;
protected readonly LegacySpriteText PopOutCountText;
protected readonly LegacySpriteText DisplayedCountText;
private int previousValue;
@ -45,17 +36,10 @@ namespace osu.Game.Skinning
public bool UsesFixedAnchor { get; set; }
public LegacyComboCounter()
protected LegacyComboCounter()
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
Margin = new MarginPadding(10);
Scale = new Vector2(1.28f);
InternalChildren = new[]
{
counterContainer = new Container
@ -63,18 +47,16 @@ namespace osu.Game.Skinning
AlwaysPresent = true,
Children = new[]
{
popOutCount = new LegacySpriteText(LegacyFont.Combo)
PopOutCountText = new LegacySpriteText(LegacyFont.Combo)
{
Alpha = 0,
Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
DisplayedCountText = new LegacySpriteText(LegacyFont.Combo)
{
Alpha = 0,
AlwaysPresent = true,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
}
@ -114,26 +96,12 @@ namespace osu.Game.Skinning
{
base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
((IHasText)popOutCount).Text = formatCount(Current.Value);
DisplayedCountText.Text = FormatCount(Current.Value);
PopOutCountText.Text = FormatCount(Current.Value);
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
updateLayout();
}
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;
counterContainer.Size = DisplayedCountText.Size;
}
private void updateCount(bool rolling)
@ -147,127 +115,84 @@ namespace osu.Game.Skinning
if (!rolling)
{
FinishTransforms(false, nameof(DisplayedCount));
isRolling = false;
DisplayedCount = prev;
if (prev + 1 == Current.Value)
onCountIncrement(prev, Current.Value);
OnCountIncrement();
else
onCountChange(Current.Value);
OnCountChange();
}
else
{
onCountRolling(displayedCount, Current.Value);
OnCountRolling();
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);
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;
if (DisplayedCount < Current.Value - 1)
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
if (currentValue == 0 && newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration);
if (DisplayedCount == 0 && Current.Value == 0)
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)
displayedCountSpriteText.FadeOut();
DisplayedCount = newValue;
DisplayedCount = Current.Value;
}
private void onDisplayedCountRolling(int newValue)
{
if (newValue == 0)
displayedCountSpriteText.FadeOut(fade_out_duration);
else
displayedCountSpriteText.Show();
DisplayedCountText.FadeOut(fade_out_duration);
transformNoPopOut(newValue);
DisplayedCountText.Text = FormatCount(newValue);
counterContainer.Size = DisplayedCountText.Size;
}
private void onDisplayedCountChange(int newValue)
{
displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1);
transformNoPopOut(newValue);
DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1);
DisplayedCountText.Text = FormatCount(newValue);
counterContainer.Size = DisplayedCountText.Size;
}
private void onDisplayedCountIncrement(int newValue)
{
displayedCountSpriteText.Show();
transformPopOutSmall(newValue);
DisplayedCountText.Text = FormatCount(newValue);
counterContainer.Size = DisplayedCountText.Size;
}
private void transformRoll(int currentValue, int 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)
{

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 LightPosition = (480 - 413) * POSITION_SCALE_FACTOR;
public float ComboPosition = 111 * POSITION_SCALE_FACTOR;
public float ScorePosition = 300 * POSITION_SCALE_FACTOR;
public bool ShowJudgementLine = true;
public bool KeysUnderNotes;

View File

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

View File

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

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
@ -24,6 +23,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning
@ -155,6 +155,9 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.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:
return SkinUtils.As<TValue>(new Bindable<float>(existing.ScorePosition));
@ -192,6 +195,9 @@ namespace osu.Game.Skinning
Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As<TValue>(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}"));
case LegacyManiaSkinConfigurationLookups.ComboBreakColour:
return SkinUtils.As<TValue>(getCustomColour(existing, "ColourBreak"));
case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(existing.MinimumColumnWidth));
@ -361,10 +367,19 @@ namespace osu.Game.Skinning
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
if (containerLookup.Ruleset != null)
{
return new Container
return new DefaultSkinComponentsContainer(container =>
{
RelativeSizeAxes = Axes.Both,
Child = new LegacyComboCounter(),
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
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...
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.Skinning.LegacyComboCounter", @"osu.Game.Skinning.LegacyDefaultComboCounter");
jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.PerformancePointsCounter", @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter");
try
@ -250,13 +251,15 @@ namespace osu.Game.Skinning
{
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 ||
!layout.TryGetDrawableInfo(null, out var globalHUDComponents) ||
resources == null)
break;
var comboCounters = globalHUDComponents.Where(c =>
c.Type.Name == nameof(LegacyComboCounter) ||
c.Type.Name == nameof(LegacyDefaultComboCounter) ||
c.Type.Name == nameof(DefaultComboCounter) ||
c.Type.Name == nameof(ArgonComboCounter)).ToArray();