diff --git a/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs index a08e62ea8..de7d4962c 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs @@ -8,8 +8,8 @@ using osu.Game.Rulesets.Sentakki.Extensions; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -34,7 +34,7 @@ public TestSceneFanChevronVisual() RelativeSizeAxes = Axes.Both, Colour = Color4.White }); - Cell(0).Add(new SentakkiRing + Cell(0).Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) @@ -46,7 +46,7 @@ public TestSceneFanChevronVisual() Segments = [new SlideSegment(PathShape.Fan, 4, false)] } }); - Cell(0).Add(new SentakkiRing + Cell(0).Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) @@ -56,7 +56,7 @@ public TestSceneFanChevronVisual() RelativeSizeAxes = Axes.Both, Colour = Color4.White }); - Cell(1).Add(new SentakkiRing + Cell(1).Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs index 2795ef95c..0368cbe94 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs @@ -7,8 +7,8 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -32,7 +32,7 @@ public TestSceneAllSlides() { Add(chevronPool = new DrawablePool(62)); - Add(new SentakkiRing + Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs index 12b822755..7a3091b53 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; @@ -34,7 +35,7 @@ public TestSceneCircleChaining() { Add(chevronPool = new DrawablePool(62)); - Add(new SentakkiRing + Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs index 03a4450c9..93ad566fc 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs @@ -4,8 +4,8 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; @@ -25,7 +25,7 @@ public TestSceneFanSlide() { Add(chevronPool = new DrawablePool(62)); - Add(new SentakkiRing + Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs index 7eb2dc4ce..d6e13c1c3 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs @@ -8,8 +8,8 @@ using osu.Game.Rulesets.Sentakki.Extensions; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -39,7 +39,7 @@ protected TestSceneSlide() { Add(chevronPool = new DrawablePool(62)); - Add(new SentakkiRing + Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs index 88f984190..6c4fa47b0 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs @@ -10,8 +10,8 @@ using osu.Game.Rulesets.Sentakki.Objects.Drawables; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.Objects.SlidePath; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -45,7 +45,7 @@ public partial class TestSceneSlideFan : OsuTestScene public TestSceneSlideFan() { base.Content.Add(content = new SentakkiInputManager(new SentakkiRuleset().RulesetInfo)); - Add(new SentakkiRing + Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE), diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs index 5a0d0c321..96beb06ad 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs @@ -7,8 +7,8 @@ using osu.Game.Rulesets.Sentakki.Beatmaps; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -26,7 +26,7 @@ public partial class TestSceneTouchNote : OsuTestScene public TestSceneTouchNote() { base.Content.Add(content = new SentakkiInputManager(new SentakkiRuleset().RulesetInfo)); - base.Content.Add(new SentakkiRing + base.Content.Add(new PlayfieldRing { RelativeSizeAxes = Axes.None, Size = new Vector2(SentakkiPlayfield.RINGSIZE) diff --git a/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/sentakki/playfield-ring.png b/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/sentakki/playfield-ring.png new file mode 100644 index 000000000..0eebe0a6c Binary files /dev/null and b/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/sentakki/playfield-ring.png differ diff --git a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs index d419d9ffe..c94b3ad03 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs @@ -1,41 +1,49 @@ -using NUnit.Framework; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Sentakki.Skinning; +using osu.Game.Rulesets.Sentakki.Skinning.Default; +using osu.Game.Rulesets.Sentakki.Skinning.Legacy; using osu.Game.Rulesets.Sentakki.UI; -using osu.Game.Rulesets.Sentakki.UI.Components; -using osu.Game.Tests.Visual; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Sentakki.Tests.UI; [TestFixture] -public partial class TestSceneSentakkiRing : OsuTestScene +public partial class TestSceneSentakkiRing : SentakkiSkinnableTestScene { - private SentakkiRing ring = null!; - - public TestSceneSentakkiRing() + [Test] + public void TestPlayfieldRing() { - AddStep("Clear test", () => - { - Clear(); - Add(new Box - { - RelativeSizeAxes = Axes.Both - }); - }); - - AddStep("Create Ring", () => Add(ring = new SentakkiRing + AddStep("Create Ring", () => SetContents(_ => new SkinnableDrawable(new SentakkiSkinComponentLookup(SentakkiSkinComponents.PlayfieldRing), _ => new PlayfieldRing()) { RelativeSizeAxes = Axes.None, - Size = new Vector2(SentakkiPlayfield.RINGSIZE) + Size = new Vector2(SentakkiPlayfield.RINGSIZE), + Scale = new Vector2(0.3f), })); - AddUntilStep("Ring loaded", () => ring.IsLoaded && ring.Alpha == 1); - AddToggleStep("Toggle notestart Indicators", b => ring.NoteStartIndicators.Value = b); - AddRepeatStep("Trigger Kiai Beat", () => ring.KiaiBeat(), 5); - AddSliderStep("Test opacity", 0, 1, 1, f => - { - if (ring != null) ring.RingOpacity.Value = f; - }); + AddUntilStep("Ring loaded", () => CreatedDrawables.All(d => d.IsLoaded)); + AddToggleStep("Toggle notestart Indicators", toggleNotestartIndicators); + AddRepeatStep("Trigger Kiai Beat", triggerKiaiBeat, 5); + } + + private void toggleNotestartIndicators(bool b) + { + CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( + r => r.NoteStartIndicators.Value = b + ); + } + + private void triggerKiaiBeat() + { + CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( + r => r.Pulse() + ); + + CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( + r => r.Pulse() + ); } } diff --git a/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs b/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs index 014e50fc3..a5098418a 100644 --- a/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs @@ -17,12 +17,10 @@ protected override void InitialiseDefaults() { base.InitialiseDefaults(); - SetDefault(SentakkiRulesetSettings.KiaiEffects, true); SetDefault(SentakkiRulesetSettings.AnimationSpeed, 2.0f, 1.0f, 10.25f, 0.25f); SetDefault(SentakkiRulesetSettings.TouchAnimationSpeed, 2.0f, 1.0f, 10.25f, 0.25f); SetDefault(SentakkiRulesetSettings.ShowNoteStartIndicators, false); SetDefault(SentakkiRulesetSettings.RingColor, ColorOption.Default); - SetDefault(SentakkiRulesetSettings.RingOpacity, 1f, 0f, 1f, 0.01f); SetDefault(SentakkiRulesetSettings.SnakingSlideBody, true); SetDefault(SentakkiRulesetSettings.DetailedJudgements, false); SetDefault(SentakkiRulesetSettings.PreferPortraitLayout, false); @@ -40,9 +38,7 @@ public override TrackedSettings CreateTrackedSettings() => public enum SentakkiRulesetSettings { - KiaiEffects, AnimationSpeed, - RingOpacity, ShowNoteStartIndicators, RingColor, TouchAnimationSpeed, diff --git a/osu.Game.Rulesets.Sentakki/Localisation/SentakkiSettingsSubsectionStrings.cs b/osu.Game.Rulesets.Sentakki/Localisation/SentakkiSettingsSubsectionStrings.cs index bced6003d..21add4a77 100644 --- a/osu.Game.Rulesets.Sentakki/Localisation/SentakkiSettingsSubsectionStrings.cs +++ b/osu.Game.Rulesets.Sentakki/Localisation/SentakkiSettingsSubsectionStrings.cs @@ -6,11 +6,6 @@ public static class SentakkiSettingsSubsectionStrings { private const string prefix = @"osu.Game.Rulesets.Sentakki.Resources.Localisation.SentakkiSettingsSubsectionStrings"; - /// - /// "Show kiai effects" - /// - public static LocalisableString ShowKiaiEffects => new TranslatableString(getKey(@"show_kiai_effects"), @"Show Kiai effects"); - /// /// "Show note start indicators" /// diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs new file mode 100644 index 000000000..542175311 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -0,0 +1,184 @@ +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.Extensions; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Skinning.Default; + +public partial class PlayfieldRing : BeatSyncedContainer +{ + private readonly Container spawnIndicator; + private readonly Drawable pulseRing; + + public override bool RemoveCompletedTransforms => false; + + public PlayfieldRing() + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = + [ + pulseRing = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(608), + Colour = Color4.White, + InnerRadius = 12f / 304f, + Progress = 1, + Alpha = 0, + }, + new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(600), + InnerRadius = 8f / 300f, + Progress = 1, + Colour = Color4.Gray + }, + new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(596), + InnerRadius = 4f / 298f, + Progress = 1, + Colour = Color4.White + }, + spawnIndicator = new Container + { + Name = "Spawn indicators", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0 + } + ]; + + // Add dots to the actual ring + foreach (float pathAngle in SentakkiPlayfield.LANEANGLES) + { + AddInternal(new DotPiece + { + Position = MathExtensions.PointOnCircle(SentakkiPlayfield.INTERSECTDISTANCE, pathAngle), + }); + + spawnIndicator.Add(new DotPiece(size: new Vector2(16, 8)) + { + Rotation = pathAngle, + Position = MathExtensions.PointOnCircle(SentakkiPlayfield.NOTESTARTDISTANCE, pathAngle), + }); + } + } + + public readonly Bindable NoteStartIndicators = new Bindable(); + + private readonly Bindable ringColor = new Bindable(); + private IBindable beatmapDifficulty = null!; + + [BackgroundDependencyLoader] + private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, BeatmapDifficultyCache? difficultyCache) + { + settings?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); + + settings?.BindWith(SentakkiRulesetSettings.ShowNoteStartIndicators, NoteStartIndicators); + NoteStartIndicators.BindValueChanged(opacity => spawnIndicator.FadeTo(Convert.ToSingle(opacity.NewValue), 200), true); + + if (beatmap is null || difficultyCache is null) + return; + + // handle colouring of playfield elements + beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); + beatmapDifficulty.BindValueChanged(_ => updateColours()); + } + + protected override void LoadComplete() + { + // These usually animate in, but they shouldn't if the game was started with it already on + spawnIndicator.FinishTransforms(true); + + ringColor.BindValueChanged(_ => updateColours(), true); + } + + private bool wasKiai; + + protected override void Update() + { + base.Update(); + + if (EffectPoint.KiaiMode && !wasKiai) + { + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; + + if (isNearEffectPoint) + Pulse(BeatSyncSource.CurrentAmplitudes.Average); + } + + wasKiai = EffectPoint.KiaiMode; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!effectPoint.KiaiMode) + return; + + if (((beatIndex * 4) % timingPoint.TimeSignature.Numerator) == 0) + Pulse(amplitudes.Average); + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private void updateColours() + { + switch (ringColor.Value) + { + case ColorOption.Difficulty: + double starRating = beatmapDifficulty.Value.Stars; + var colour = colours.ForStarDifficulty(starRating); + + // Normalize the colors to make sure the ring is actually visible + colour = Interpolation.ValueAt(0.5f, colour, new HSPAColour(colour) { P = 0.6f }.ToColor4(), 0, 1); + + this.FadeColour(colour, 200); + break; + + case ColorOption.Skin: + this.FadeColour(skin.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White, 200); + break; + + default: + this.FadeColour(Color4.White, 200); + break; + } + } + + public void Pulse(float amplitude = 1) + { + pulseRing.FinishTransforms(); + pulseRing.ClearTransforms(); + pulseRing.ScaleTo(1).FadeTo(0.5f * amplitude).ScaleTo(1.06f, 200).FadeOut(200); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs new file mode 100644 index 000000000..9af0676c9 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs @@ -0,0 +1,138 @@ +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Skinning.Legacy; + +public partial class LegacyPlayfieldRing : BeatSyncedContainer +{ + private Drawable ringDrawable = null!; + private Drawable pulseRing = null!; + + public override bool RemoveCompletedTransforms => false; + + private readonly Bindable ringColor = new Bindable(); + private IBindable beatmapDifficulty = null!; + + public LegacyPlayfieldRing() + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, BeatmapDifficultyCache? difficultyCache) + { + // If we get here, we know the assets exist + pulseRing = skin.GetAnimation("sentakki/playfield-ring", true, true, true, "/")!; + pulseRing.RelativeSizeAxes = Axes.Both; + pulseRing.Size = new Vector2(1.0133333f); + pulseRing.Anchor = Anchor.Centre; + pulseRing.Origin = Anchor.Centre; + pulseRing.Alpha = 0; + + ringDrawable = skin.GetAnimation("sentakki/playfield-ring", true, true, true, "/")!; + ringDrawable.RelativeSizeAxes = Axes.Both; + ringDrawable.Size = Vector2.One; + ringDrawable.Anchor = Anchor.Centre; + ringDrawable.Origin = Anchor.Centre; + + InternalChildren = [ + pulseRing, + ringDrawable, + ]; + + settings?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); + + if (beatmap is null || difficultyCache is null) + return; + + // handle colouring of playfield elements + beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); + beatmapDifficulty.BindValueChanged(_ => updateColours()); + } + + protected override void LoadComplete() + { + + ringColor.BindValueChanged(_ => updateColours(), true); + } + + private bool wasKiai; + + protected override void Update() + { + base.Update(); + + if (EffectPoint.KiaiMode && !wasKiai) + { + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; + + if (isNearEffectPoint) + Pulse(BeatSyncSource.CurrentAmplitudes.Average); + } + + wasKiai = EffectPoint.KiaiMode; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!effectPoint.KiaiMode) + return; + + if (((beatIndex * 4) % timingPoint.TimeSignature.Numerator) == 0) + Pulse(amplitudes.Average); + } + + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private void updateColours() + { + switch (ringColor.Value) + { + case ColorOption.Difficulty: + double starRating = beatmapDifficulty.Value.Stars; + var colour = colours.ForStarDifficulty(starRating); + + // Normalize the colors to make sure the ring is actually visible + colour = Interpolation.ValueAt(0.5f, colour, new HSPAColour(colour) { P = 0.6f }.ToColor4(), 0, 1); + + this.FadeColour(colour, 200); + break; + + case ColorOption.Skin: + this.FadeColour(skin.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White, 200); + break; + + default: + this.FadeColour(Color4.White, 200); + break; + } + } + + public void Pulse(float amplitude = 1) + { + pulseRing.FinishTransforms(); + pulseRing.ClearTransforms(); + pulseRing.ScaleTo(1).FadeTo(0.5f * amplitude).ScaleTo(1.06f, 200).FadeOut(200); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/SentakkiLegacySkinTransformer.cs b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/SentakkiLegacySkinTransformer.cs index 5175ceb01..837f6d950 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/SentakkiLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/SentakkiLegacySkinTransformer.cs @@ -12,6 +12,11 @@ public class SentakkiLegacySkinTransformer(ISkin skin) : LegacySkinTransformer(s case SentakkiSkinComponentLookup sentakkiComponent: switch (sentakkiComponent.Component) { + case SentakkiSkinComponents.PlayfieldRing: + if (Skin.GetAnimation("sentakki/playfield-ring", true, true, true, "/") is null) + return null; + + return new LegacyPlayfieldRing(); default: break; } diff --git a/osu.Game.Rulesets.Sentakki/Skinning/SentakkiSkinComponents.cs b/osu.Game.Rulesets.Sentakki/Skinning/SentakkiSkinComponents.cs index 4097f906c..76494e45a 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/SentakkiSkinComponents.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/SentakkiSkinComponents.cs @@ -2,5 +2,5 @@ namespace osu.Game.Rulesets.Sentakki.Skinning; public enum SentakkiSkinComponents { - + PlayfieldRing } diff --git a/osu.Game.Rulesets.Sentakki/UI/Components/PlayfieldVisualization.cs b/osu.Game.Rulesets.Sentakki/UI/Components/PlayfieldVisualization.cs deleted file mode 100644 index 7ed874a83..000000000 --- a/osu.Game.Rulesets.Sentakki/UI/Components/PlayfieldVisualization.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Sentakki.Configuration; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Sentakki.UI.Components; - -public partial class PlayfieldVisualisation : Drawable, IHasAccentColour -{ - /// - /// The number of bars to jump each update iteration. - /// - private const int index_change = 5; - - /// - /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. - /// - private const float bar_length = 600; - - /// - /// The number of bars in one rotation of the visualiser. - /// - private const int bars_per_visualiser = 200; - - /// - /// How many times we should stretch around the circumference (overlapping overselves). - /// - private const float visualiser_rounds = 5; - - /// - /// How much should each bar go down each millisecond (based on a full bar). - /// - private const float decay_per_milisecond = 0.0024f; - - /// - /// Number of milliseconds between each amplitude update. - /// - private const float time_between_updates = 50; - - /// - /// The minimum amplitude to show a bar. - /// - private const float amplitude_dead_zone = 1f / bar_length; - - private int indexOffset; - - public Color4 AccentColour { get; set; } = Color4.White.Opacity(0.2f); - - private readonly float[] frequencyAmplitudes = new float[256]; - - private IShader shader = null!; - private Texture texture = null!; - - public PlayfieldVisualisation() - { - FillAspectRatio = 1; - FillMode = FillMode.Fit; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(.99f); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Blending = BlendingParameters.Additive; - } - - private readonly Bindable kiaiEffect = new Bindable(true); - - [BackgroundDependencyLoader] - private void load(IRenderer renderer, ShaderManager shaders, SentakkiRulesetConfigManager? settings) - { - texture = renderer.WhitePixel; - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); - - settings?.BindWith(SentakkiRulesetSettings.KiaiEffects, kiaiEffect); - kiaiEffect.BindValueChanged(k => - { - if (k.NewValue) - this.FadeIn(200); - else - this.FadeOut(500); - }); - } - - protected override void LoadComplete() - { - kiaiEffect.TriggerChange(); - } - - [Resolved] - private IBeatSyncProvider beatSyncProvider { get; set; } = null!; - - private bool wasKiai; - - // Returns true if amplitude have been updated - private void updateAmplitudes() - { - ReadOnlySpan temporalAmplitudes = beatSyncProvider.CurrentAmplitudes.FrequencyAmplitudes.Span; - - for (int i = 0; i < bars_per_visualiser; i++) - { - float targetAmplitude = temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]; - if (targetAmplitude > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = targetAmplitude; - } - - indexOffset = (indexOffset + index_change) % bars_per_visualiser; - } - - private double timeDelta; - - public bool ShouldDraw { get; private set; } - - protected override void Update() - { - base.Update(); - - timeDelta += Math.Abs(Time.Elapsed); - - bool isKiai = beatSyncProvider.CheckIsKiaiTime(); - - if (!wasKiai && isKiai) - timeDelta = time_between_updates; - - wasKiai = isKiai; - - if (timeDelta >= time_between_updates && isKiai) - { - timeDelta %= time_between_updates; - updateAmplitudes(); - } - - float decayFactor = Math.Abs((float)Time.Elapsed) * decay_per_milisecond; - - for (int i = 0; i < bars_per_visualiser; i++) - { - //3% of extra bar length to make it a little faster when bar is almost at it's minimum - frequencyAmplitudes[i] -= decayFactor * (frequencyAmplitudes[i] + 0.03f); - if (frequencyAmplitudes[i] < 0) - frequencyAmplitudes[i] = 0; - - if (frequencyAmplitudes[i] != 0) - ShouldDraw = true; - } - - // Don't invalidate the draw node if we don't plan to draw - if (!ShouldDraw) - return; - - Invalidate(Invalidation.DrawNode); - } - - protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); - - private class VisualisationDrawNode : DrawNode - { - protected new PlayfieldVisualisation Source => (PlayfieldVisualisation)base.Source; - - private IShader shader = null!; - private Texture texture = null!; - - // Assuming the logo is a circle, we don't need a second dimension. - private float size; - - private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f); - private readonly float[] audioData = new float[256]; - - private IVertexBatch? vertexBatch; - - public VisualisationDrawNode(PlayfieldVisualisation source) - : base(source) - { - } - - public override void ApplyState() - { - base.ApplyState(); - - shader = Source.shader; - texture = Source.texture; - size = Source.DrawSize.X; - Source.frequencyAmplitudes.AsSpan().CopyTo(audioData); - } - - protected override void Draw(IRenderer renderer) - { - if (!Source.ShouldDraw) - return; - - base.Draw(renderer); - - vertexBatch ??= renderer.CreateQuadBatch(100, 10); - - shader.Bind(); - - Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; - - ColourInfo colourInfo = DrawColourInfo.Colour; - colourInfo.ApplyChild(transparent_white); - - for (int j = 0; j < visualiser_rounds; j++) - { - for (int i = 0; i < bars_per_visualiser; i++) - { - if (audioData[i] < amplitude_dead_zone) - continue; - - float rotation = float.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); - float rotationCos = MathF.Cos(rotation); - float rotationSin = MathF.Sin(rotation); - // taking the cos and sin to the 0..1 range - var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; - - var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); - // The distance between the position and the sides of the bar. - var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); - // The distance between the bottom side of the bar and the top side. - var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); - - var rectangle = new Quad( - Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) - ); - - renderer.DrawQuad( - texture, - rectangle, - colourInfo, - null, - vertexBatch.AddAction, - // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. - Vector2.Divide(inflation, barSize.Yx)); - } - } - - shader.Unbind(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - vertexBatch?.Dispose(); - } - } -} diff --git a/osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs b/osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs deleted file mode 100644 index 33282a4c6..000000000 --- a/osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Rulesets.Sentakki.Configuration; -using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Sentakki.UI.Components; - -public partial class SentakkiRing : CompositeDrawable -{ - private readonly Container spawnIndicator; - - public SentakkiRing() - { - RelativeSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - InternalChildren = - [ - new CircularProgress - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(600), - InnerRadius = 8f / 300f, - Progress = 1, - Colour = Color4.Gray - }, - new CircularProgress - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(596), - InnerRadius = 4f / 298f, - Progress = 1, - Colour = Color4.White - }, - spawnIndicator = new Container - { - Name = "Spawn indicators", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0 - } - ]; - - // Add dots to the actual ring - foreach (float pathAngle in SentakkiPlayfield.LANEANGLES) - { - AddInternal(new DotPiece - { - Position = new Vector2(-(SentakkiPlayfield.INTERSECTDISTANCE * (float)Math.Cos((pathAngle + 90f) * (float)(Math.PI / 180))), - -(SentakkiPlayfield.INTERSECTDISTANCE * (float)Math.Sin((pathAngle + 90f) * (float)(Math.PI / 180)))), - }); - - spawnIndicator.Add(new DotPiece(size: new Vector2(16, 8)) - { - Rotation = pathAngle, - Position = new Vector2(-(SentakkiPlayfield.NOTESTARTDISTANCE * (float)Math.Cos((pathAngle + 90f) * (float)(Math.PI / 180))), - -(SentakkiPlayfield.NOTESTARTDISTANCE * (float)Math.Sin((pathAngle + 90f) * (float)(Math.PI / 180)))), - }); - } - } - - public readonly Bindable RingOpacity = new Bindable(1); - public readonly Bindable NoteStartIndicators = new Bindable(); - private readonly Bindable kiaiEffect = new Bindable(true); - - [BackgroundDependencyLoader] - private void load(SentakkiRulesetConfigManager? settings) - { - settings?.BindWith(SentakkiRulesetSettings.RingOpacity, RingOpacity); - RingOpacity.BindValueChanged(opacity => Alpha = opacity.NewValue, true); - - settings?.BindWith(SentakkiRulesetSettings.ShowNoteStartIndicators, NoteStartIndicators); - NoteStartIndicators.BindValueChanged(opacity => spawnIndicator.FadeTo(Convert.ToSingle(opacity.NewValue), 200), true); - - settings?.BindWith(SentakkiRulesetSettings.KiaiEffects, kiaiEffect); - } - - protected override void LoadComplete() - { - // These usually animate in, but they shouldn't if the game was started with it already on - spawnIndicator.FinishTransforms(true); - } - - public void KiaiBeat() - { - if (!kiaiEffect.Value) return; - - FinishTransforms(); - this.ScaleTo(1.01f, 100).Then().ScaleTo(1, 100); - } -} diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index 3aeba054f..e6639e63b 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -3,21 +3,19 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Sentakki.Configuration; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Skinning; +using osu.Game.Rulesets.Sentakki.Skinning.Default; using osu.Game.Rulesets.Sentakki.UI.Components; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Sentakki.UI; @@ -31,7 +29,7 @@ public partial class SentakkiPlayfield : Playfield private readonly DrawablePool judgementPool; private readonly DrawablePool explosionPool; - private readonly SentakkiRing ring; + private readonly SkinnableDrawable ring; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -72,8 +70,7 @@ public SentakkiPlayfield() RelativeSizeAxes = Axes.Both, Children = [ - new PlayfieldVisualisation(), - ring = new SentakkiRing() + ring = new SkinnableDrawable(new SentakkiSkinComponentLookup(SentakkiSkinComponents.PlayfieldRing), _ => new PlayfieldRing()) ] }, explosionLayer = new Container { RelativeSizeAxes = Axes.Both }, @@ -93,13 +90,7 @@ public SentakkiPlayfield() [Resolved] private DrawableSentakkiRuleset drawableSentakkiRuleset { get; set; } = null!; - [Resolved] - private SentakkiRulesetConfigManager? sentakkiRulesetConfig { get; set; } - private Bindable skin = null!; - private readonly Bindable ringColor = new Bindable(); - - private IBindable beatmapDifficulty = null!; [BackgroundDependencyLoader] private void load(SkinManager skinManager, IBeatmap beatmap, BeatmapDifficultyCache difficultyCache) @@ -107,19 +98,7 @@ private void load(SkinManager skinManager, IBeatmap beatmap, BeatmapDifficultyCa RegisterPool(2); RegisterPool(8); - // handle colouring of playfield elements - beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); - skin = skinManager.CurrentSkin.GetBoundCopy(); - sentakkiRulesetConfig?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - skin.BindValueChanged(_ => changePlayfieldAccent(), true); - ringColor.BindValueChanged(_ => changePlayfieldAccent(), true); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new SentakkiHitObjectLifetimeEntry(hitObject, drawableSentakkiRuleset); @@ -173,37 +152,9 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) if (judgedObject is DrawableSlideBody) return; - if (judgedObject.HitObject.Kiai) - ring.KiaiBeat(); - var explosion = explosionPool.Get().Apply(sentakkiHitObject); explosionLayer.Add(explosion); } - [Resolved] - private OsuColour colours { get; set; } = null!; - - private void changePlayfieldAccent() - { - switch (ringColor.Value) - { - case ColorOption.Difficulty: - double starRating = beatmapDifficulty.Value.Stars; - var colour = colours.ForStarDifficulty(starRating); - - // Normalize the colors to make sure the ring is actually visible - colour = Interpolation.ValueAt(0.5f, colour, new HSPAColour(colour) { P = 0.6f }.ToColor4(), 0, 1); - AccentContainer.FadeColour(colour, 200); - break; - - case ColorOption.Skin: - AccentContainer.FadeColour(skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White, 200); - break; - - default: - AccentContainer.FadeColour(Color4.White, 200); - break; - } - } } diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs index c87fc2aaf..91bb9e1db 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs @@ -27,11 +27,6 @@ private void load() Children = [ - new SettingsItemV2(new FormCheckBox { - Caption = SentakkiSettingsSubsectionStrings.ShowKiaiEffects, - Current = config.GetBindable(SentakkiRulesetSettings.KiaiEffects), - }) { Keywords = ["visualiser", "visualizer", "bounce"] }, - new SettingsItemV2(new FormCheckBox { Caption = SentakkiSettingsSubsectionStrings.ShowNoteStartIndicators, Current = config.GetBindable(SentakkiRulesetSettings.ShowNoteStartIndicators) @@ -63,12 +58,6 @@ private void load() Current = config.GetBindable(SentakkiRulesetSettings.TouchAnimationSpeed), LabelFormat = v => SentakkiSettingsSubsectionStrings.EntrySpeedTooltip(v, DrawableSentakkiRuleset.ComputeTouchNoteEntryTime(v)) }) { Keywords = ["scroll"] }, - - new SettingsItemV2(new FormSliderBar{ - Caption = SentakkiSettingsSubsectionStrings.RingOpacity, - Current = config.GetBindable(SentakkiRulesetSettings.RingOpacity), - DisplayAsPercentage = true, - }) { Keywords = ["transparency"] }, ]; if (!RuntimeInfo.IsMobile) diff --git a/osu.Game.Rulesets.Sentakki/osu.Game.Rulesets.Sentakki.csproj b/osu.Game.Rulesets.Sentakki/osu.Game.Rulesets.Sentakki.csproj index 347349584..0a54e2a76 100644 --- a/osu.Game.Rulesets.Sentakki/osu.Game.Rulesets.Sentakki.csproj +++ b/osu.Game.Rulesets.Sentakki/osu.Game.Rulesets.Sentakki.csproj @@ -16,4 +16,8 @@ sentakki for osu!lazer (dev build) + + + + \ No newline at end of file