From 221470c6afa7d73a15a0f2baba6db06d810b0c94 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 28 Jul 2026 16:53:44 +0200 Subject: [PATCH 01/10] Create legacy skin transformer in SentakkiRuleset --- osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs index f055788cb..6ab3835ea 100644 --- a/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/SentakkiRuleset.cs @@ -32,12 +32,14 @@ using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Replays; using osu.Game.Rulesets.Sentakki.Scoring; +using osu.Game.Rulesets.Sentakki.Skinning.Legacy; using osu.Game.Rulesets.Sentakki.Statistics; using osu.Game.Rulesets.Sentakki.UI; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -217,6 +219,15 @@ public override IEnumerable GetValidHitResults() public override LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetDisplayNameForSentakkiResult(); + public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) + { + return skin switch + { + LegacySkin => new SentakkiLegacySkinTransformer(skin), + _ => null + }; + } + public partial class SentakkiIcon : CompositeDrawable { private readonly Ruleset ruleset; From 3aff60816d1cdceb357c83f1c21c6d7a60fe4102 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Wed, 29 Jul 2026 14:29:03 +0200 Subject: [PATCH 02/10] Add special-skin to test resources Just the config file for now --- .../Resources/special-skin/skin.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/skin.ini new file mode 100644 index 000000000..f793094ff --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/skin.ini @@ -0,0 +1,4 @@ +[General] +Name: SentakkiTest +Author: Bloom +Version: 2.7 \ No newline at end of file From 36aa7b3dee52d1caac1b7a102a2def965bcfb2bd Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Thu, 30 Jul 2026 18:13:27 +0200 Subject: [PATCH 03/10] Move PlayfieldRing into skinning namespace --- .../Graphics/TestSceneFanChevronVisuals.cs | 8 +-- .../Objects/Slides/TestSceneAllSlides.cs | 4 +- .../Objects/Slides/TestSceneCircleChaining.cs | 3 +- .../Objects/Slides/TestSceneFanSlide.cs | 4 +- .../Objects/Slides/TestSceneSlide.cs | 4 +- .../Objects/TestSceneSlideFan.cs | 4 +- .../Objects/TestSceneTouchNote.cs | 4 +- .../UI/TestSceneSentakkiRing.cs | 63 +++++++++++-------- .../Default/PlayfieldRing.cs} | 9 +-- .../Skinning/SentakkiSkinComponents.cs | 2 +- .../UI/SentakkiPlayfield.cs | 10 +-- 11 files changed, 65 insertions(+), 50 deletions(-) rename osu.Game.Rulesets.Sentakki/{UI/Components/SentakkiRing.cs => Skinning/Default/PlayfieldRing.cs} (94%) 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/UI/TestSceneSentakkiRing.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs index d419d9ffe..7f2930bf9 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs @@ -1,41 +1,52 @@ -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.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); + AddSliderStep("Test opacity", 0, 1, 1, adjustOpacity); + } + + 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.KiaiBeat() + ); + } + + private void adjustOpacity(float f) + { + CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( + r => r.RingOpacity.Value = f + ); } } diff --git a/osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs similarity index 94% rename from osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs rename to osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs index 33282a4c6..074b68656 100644 --- a/osu.Game.Rulesets.Sentakki/UI/Components/SentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -1,4 +1,4 @@ -using System; +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -6,16 +6,17 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets.Sentakki.Configuration; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Sentakki.UI; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Sentakki.UI.Components; +namespace osu.Game.Rulesets.Sentakki.Skinning.Default; -public partial class SentakkiRing : CompositeDrawable +public partial class PlayfieldRing : CompositeDrawable { private readonly Container spawnIndicator; - public SentakkiRing() + public PlayfieldRing() { RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; 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/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index 3aeba054f..eb618ce27 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -13,6 +13,8 @@ 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; @@ -31,7 +33,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; @@ -73,7 +75,7 @@ public SentakkiPlayfield() Children = [ new PlayfieldVisualisation(), - ring = new SentakkiRing() + ring = new SkinnableDrawable(new SentakkiSkinComponentLookup(SentakkiSkinComponents.PlayfieldRing), _ => new PlayfieldRing()) ] }, explosionLayer = new Container { RelativeSizeAxes = Axes.Both }, @@ -173,8 +175,8 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) if (judgedObject is DrawableSlideBody) return; - if (judgedObject.HitObject.Kiai) - ring.KiaiBeat(); + if (judgedObject.HitObject.Kiai && ring.Drawable is PlayfieldRing defaultRing) + defaultRing.KiaiBeat(); var explosion = explosionPool.Get().Apply(sentakkiHitObject); explosionLayer.Add(explosion); From 78caeca03d525faebb85cca805e096a81d4d77b7 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 3 Aug 2026 12:52:55 +0200 Subject: [PATCH 04/10] Move ring related configuration to the Ring drawable The playfield shouldn't bear that responsibility in the first place --- .../Skinning/Default/PlayfieldRing.cs | 58 +++++++++++++++++-- .../UI/SentakkiPlayfield.cs | 51 ---------------- .../osu.Game.Rulesets.Sentakki.csproj | 4 ++ 3 files changed, 57 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs index 074b68656..0ec18df83 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -4,9 +4,15 @@ 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.Graphics; 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.Rulesets.Sentakki.UI.Components; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -24,6 +30,7 @@ public PlayfieldRing() InternalChildren = [ + new PlayfieldVisualisation(), new CircularProgress { Anchor = Anchor.Centre, @@ -56,15 +63,13 @@ public PlayfieldRing() { 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)))), + Position = MathExtensions.PointOnCircle(SentakkiPlayfield.INTERSECTDISTANCE, pathAngle), }); 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)))), + Position = MathExtensions.PointOnCircle(SentakkiPlayfield.NOTESTARTDISTANCE, pathAngle), }); } } @@ -73,9 +78,15 @@ public PlayfieldRing() public readonly Bindable NoteStartIndicators = new Bindable(); private readonly Bindable kiaiEffect = new Bindable(true); + private readonly Bindable ringColor = new Bindable(); + private IBindable beatmapDifficulty = null!; + + [BackgroundDependencyLoader] - private void load(SentakkiRulesetConfigManager? settings) + private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, BeatmapDifficultyCache? difficultyCache) { + settings?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); + settings?.BindWith(SentakkiRulesetSettings.RingOpacity, RingOpacity); RingOpacity.BindValueChanged(opacity => Alpha = opacity.NewValue, true); @@ -83,12 +94,49 @@ private void load(SentakkiRulesetConfigManager? settings) NoteStartIndicators.BindValueChanged(opacity => spawnIndicator.FadeTo(Convert.ToSingle(opacity.NewValue), 200), true); settings?.BindWith(SentakkiRulesetSettings.KiaiEffects, kiaiEffect); + + if (beatmap is null || difficultyCache is null) + return; + + // handle colouring of playfield elements + beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); } 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); + } + + [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 KiaiBeat() diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index eb618ce27..e6639e63b 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -3,14 +3,11 @@ 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; @@ -19,7 +16,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Sentakki.UI; @@ -74,7 +70,6 @@ public SentakkiPlayfield() RelativeSizeAxes = Axes.Both, Children = [ - new PlayfieldVisualisation(), ring = new SkinnableDrawable(new SentakkiSkinComponentLookup(SentakkiSkinComponents.PlayfieldRing), _ => new PlayfieldRing()) ] }, @@ -95,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) @@ -109,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); @@ -175,37 +152,9 @@ private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) if (judgedObject is DrawableSlideBody) return; - if (judgedObject.HitObject.Kiai && ring.Drawable is PlayfieldRing defaultRing) - defaultRing.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/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 From add3a300883f671861c6cbde3af9c1f4fe1bcdcd Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 3 Aug 2026 16:21:14 +0200 Subject: [PATCH 05/10] Implement simpler kiai effect --- .../UI/TestSceneSentakkiRing.cs | 2 +- .../Skinning/Default/PlayfieldRing.cs | 65 ++++++++++++++++--- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs index 7f2930bf9..937e863d0 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs @@ -39,7 +39,7 @@ private void toggleNotestartIndicators(bool b) private void triggerKiaiBeat() { CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( - r => r.KiaiBeat() + r => r.Pulse() ); } diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs index 0ec18df83..bac34c7a4 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -1,26 +1,32 @@ 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.Rulesets.Sentakki.UI.Components; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Sentakki.Skinning.Default; -public partial class PlayfieldRing : CompositeDrawable +public partial class PlayfieldRing : BeatSyncedContainer { + private readonly Container spawnIndicator; + private readonly Drawable pulseRing; + + public override bool RemoveCompletedTransforms => false; public PlayfieldRing() { @@ -30,7 +36,16 @@ public PlayfieldRing() InternalChildren = [ - new PlayfieldVisualisation(), + 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, @@ -81,7 +96,6 @@ public PlayfieldRing() private readonly Bindable ringColor = new Bindable(); private IBindable beatmapDifficulty = null!; - [BackgroundDependencyLoader] private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, BeatmapDifficultyCache? difficultyCache) { @@ -109,6 +123,40 @@ protected override void LoadComplete() ringColor.BindValueChanged(_ => updateColours(), true); } + private bool wasKiai; + + protected override void Update() + { + base.Update(); + + if (!kiaiEffect.Value) + return; + + 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 (!kiaiEffect.Value) + return; + + if (!effectPoint.KiaiMode) + return; + + if (((beatIndex * 4) % timingPoint.TimeSignature.Numerator) == 0) + Pulse(amplitudes.Average); + } + [Resolved] private OsuColour colours { get; set; } = null!; @@ -139,11 +187,10 @@ private void updateColours() } } - public void KiaiBeat() + public void Pulse(float amplitude = 1) { - if (!kiaiEffect.Value) return; - - FinishTransforms(); - this.ScaleTo(1.01f, 100).Then().ScaleTo(1, 100); + pulseRing.FinishTransforms(); + pulseRing.ClearTransforms(); + pulseRing.ScaleTo(1).FadeTo(0.5f * amplitude).ScaleTo(1.06f, 200).FadeOut(200); } } From 001b067afda37d8e08c07b1284c45f04c72c3dd9 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 3 Aug 2026 16:31:25 +0200 Subject: [PATCH 06/10] Nuke PlayfieldVisualisation --- .../UI/Components/PlayfieldVisualization.cs | 258 ------------------ 1 file changed, 258 deletions(-) delete mode 100644 osu.Game.Rulesets.Sentakki/UI/Components/PlayfieldVisualization.cs 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(); - } - } -} From dd614fa7273d11686bb86e2853ce92babe629768 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 3 Aug 2026 17:55:04 +0200 Subject: [PATCH 07/10] Remove KiaiEffect option Now that the default is less distracting, let's try forcing everyone to play by the same settings --- .../Configuration/SentakkiRulesetConfigManager.cs | 2 -- .../Localisation/SentakkiSettingsSubsectionStrings.cs | 5 ----- .../Skinning/Default/PlayfieldRing.cs | 9 --------- .../UI/SentakkiSettingsSubsection.cs | 5 ----- 4 files changed, 21 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs b/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs index 014e50fc3..dc369592b 100644 --- a/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs @@ -17,7 +17,6 @@ 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); @@ -40,7 +39,6 @@ public override TrackedSettings CreateTrackedSettings() => public enum SentakkiRulesetSettings { - KiaiEffects, AnimationSpeed, RingOpacity, ShowNoteStartIndicators, 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 index bac34c7a4..54557f8db 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -91,7 +91,6 @@ public PlayfieldRing() public readonly Bindable RingOpacity = new Bindable(1); public readonly Bindable NoteStartIndicators = new Bindable(); - private readonly Bindable kiaiEffect = new Bindable(true); private readonly Bindable ringColor = new Bindable(); private IBindable beatmapDifficulty = null!; @@ -107,8 +106,6 @@ private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, Bea settings?.BindWith(SentakkiRulesetSettings.ShowNoteStartIndicators, NoteStartIndicators); NoteStartIndicators.BindValueChanged(opacity => spawnIndicator.FadeTo(Convert.ToSingle(opacity.NewValue), 200), true); - settings?.BindWith(SentakkiRulesetSettings.KiaiEffects, kiaiEffect); - if (beatmap is null || difficultyCache is null) return; @@ -129,9 +126,6 @@ protected override void Update() { base.Update(); - if (!kiaiEffect.Value) - return; - if (EffectPoint.KiaiMode && !wasKiai) { bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; @@ -147,9 +141,6 @@ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!kiaiEffect.Value) - return; - if (!effectPoint.KiaiMode) return; diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs index c87fc2aaf..28f0a2ae9 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) From 6f2835bd5c24da4380961ab3baf4e43bee2dfa9e Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 4 Aug 2026 16:22:08 +0200 Subject: [PATCH 08/10] Implement LegacyPlayfieldring --- .../special-skin/sentakki/playfield-ring.png | Bin 0 -> 40893 bytes .../UI/TestSceneSentakkiRing.cs | 9 ++ .../Skinning/Legacy/LegacyPlayfieldRing.cs | 140 ++++++++++++++++++ .../Legacy/SentakkiLegacySkinTransformer.cs | 5 + 4 files changed, 154 insertions(+) create mode 100644 osu.Game.Rulesets.Sentakki.Tests/Resources/special-skin/sentakki/playfield-ring.png create mode 100644 osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs 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 0000000000000000000000000000000000000000..0eebe0a6c46d802e11a110452aad9372e9b65536 GIT binary patch literal 40893 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 K^ApG5g#Q5;N4RJJ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs index 937e863d0..f952830b5 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; 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.Skinning; using osuTK; @@ -41,6 +42,10 @@ 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() + ); } private void adjustOpacity(float f) @@ -48,5 +53,9 @@ private void adjustOpacity(float f) CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( r => r.RingOpacity.Value = f ); + + CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( + r => r.Pulse() + ); } } 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..22138e2ee --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs @@ -0,0 +1,140 @@ +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; + + public readonly Bindable RingOpacity = new Bindable(1); + 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); + + settings?.BindWith(SentakkiRulesetSettings.RingOpacity, RingOpacity); + RingOpacity.BindValueChanged(opacity => Alpha = opacity.NewValue, true); + + if (beatmap is null || difficultyCache is null) + return; + + // handle colouring of playfield elements + beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); + } + + 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; } From 558209fd80acc7d8c4fce0a9a300fb63ca804b87 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 4 Aug 2026 16:32:45 +0200 Subject: [PATCH 09/10] Remove ring opacity option We can let them achieve the same thing via skinning instead --- .../UI/TestSceneSentakkiRing.cs | 12 ------------ .../Configuration/SentakkiRulesetConfigManager.cs | 2 -- .../Skinning/Default/PlayfieldRing.cs | 5 ----- .../Skinning/Legacy/LegacyPlayfieldRing.cs | 4 ---- .../UI/SentakkiSettingsSubsection.cs | 6 ------ 5 files changed, 29 deletions(-) diff --git a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs index f952830b5..c94b3ad03 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiRing.cs @@ -27,7 +27,6 @@ public void TestPlayfieldRing() AddUntilStep("Ring loaded", () => CreatedDrawables.All(d => d.IsLoaded)); AddToggleStep("Toggle notestart Indicators", toggleNotestartIndicators); AddRepeatStep("Trigger Kiai Beat", triggerKiaiBeat, 5); - AddSliderStep("Test opacity", 0, 1, 1, adjustOpacity); } private void toggleNotestartIndicators(bool b) @@ -47,15 +46,4 @@ private void triggerKiaiBeat() r => r.Pulse() ); } - - private void adjustOpacity(float f) - { - CreatedDrawables.OfType().Select(d => d.Drawable).OfType().ForEach( - r => r.RingOpacity.Value = f - ); - - 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 dc369592b..a5098418a 100644 --- a/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Sentakki/Configuration/SentakkiRulesetConfigManager.cs @@ -21,7 +21,6 @@ protected override void InitialiseDefaults() 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,7 +39,6 @@ public override TrackedSettings CreateTrackedSettings() => public enum SentakkiRulesetSettings { AnimationSpeed, - RingOpacity, ShowNoteStartIndicators, RingColor, TouchAnimationSpeed, diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs index 54557f8db..7bd4ca149 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -22,7 +22,6 @@ namespace osu.Game.Rulesets.Sentakki.Skinning.Default; public partial class PlayfieldRing : BeatSyncedContainer { - private readonly Container spawnIndicator; private readonly Drawable pulseRing; @@ -89,7 +88,6 @@ public PlayfieldRing() } } - public readonly Bindable RingOpacity = new Bindable(1); public readonly Bindable NoteStartIndicators = new Bindable(); private readonly Bindable ringColor = new Bindable(); @@ -100,9 +98,6 @@ private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, Bea { settings?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); - 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); diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs index 22138e2ee..51dbf447d 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs @@ -22,7 +22,6 @@ public partial class LegacyPlayfieldRing : BeatSyncedContainer public override bool RemoveCompletedTransforms => false; - public readonly Bindable RingOpacity = new Bindable(1); private readonly Bindable ringColor = new Bindable(); private IBindable beatmapDifficulty = null!; @@ -60,9 +59,6 @@ private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, Bea settings?.BindWith(SentakkiRulesetSettings.RingColor, ringColor); - settings?.BindWith(SentakkiRulesetSettings.RingOpacity, RingOpacity); - RingOpacity.BindValueChanged(opacity => Alpha = opacity.NewValue, true); - if (beatmap is null || difficultyCache is null) return; diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs index 28f0a2ae9..91bb9e1db 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiSettingsSubsection.cs @@ -58,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) From 03d16d796a267732345f4cce518d4383c7d21212 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Tue, 4 Aug 2026 17:00:20 +0200 Subject: [PATCH 10/10] Fix inconsistent beatmap difficulty colours when upon changing skins --- osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs | 2 ++ .../Skinning/Legacy/LegacyPlayfieldRing.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs index 7bd4ca149..542175311 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Default/PlayfieldRing.cs @@ -106,12 +106,14 @@ private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, Bea // 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); } diff --git a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs index 51dbf447d..9af0676c9 100644 --- a/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs +++ b/osu.Game.Rulesets.Sentakki/Skinning/Legacy/LegacyPlayfieldRing.cs @@ -64,10 +64,12 @@ private void load(SentakkiRulesetConfigManager? settings, IBeatmap? beatmap, Bea // handle colouring of playfield elements beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo); + beatmapDifficulty.BindValueChanged(_ => updateColours()); } protected override void LoadComplete() { + ringColor.BindValueChanged(_ => updateColours(), true); }