// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyMainCirclePiece : CompositeDrawable { public override bool RemoveCompletedTransforms => false; /// /// A prioritised prefix to perform texture lookups with. /// private readonly string? priorityLookupPrefix; private readonly bool hasNumber; protected Drawable CircleSprite = null!; protected Drawable OverlaySprite = null!; protected Container OverlayLayer { get; private set; } = null!; private SkinnableSpriteText hitCircleText = null!; private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes. private DrawableHitObject? drawableObject { get; set; } [Resolved] private ISkinSource skin { get; set; } = null!; public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true) { this.priorityLookupPrefix = priorityLookupPrefix; this.hasNumber = hasNumber; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } [BackgroundDependencyLoader] private void load() { var drawableOsuObject = (DrawableOsuHitObject?)drawableObject; // if a base texture for the specified prefix exists, continue using it for subsequent lookups. // otherwise fall back to the default prefix "hitcircle". string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle"; // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. Color4 objectColour = drawableOsuObject!.AccentColour.Value; int add = Math.Max(25, 300 - (int)(objectColour.R * 255) - (int)(objectColour.G * 255) - (int)(objectColour.B * 255)); Color4 finalColour = new Color4( (byte)Math.Min((byte)(objectColour.R * 255) + add, 255), (byte)Math.Min((byte)(objectColour.G * 255) + add, 255), (byte)Math.Min((byte)(objectColour.B * 255) + add, 255), 255); InternalChildren = new[] { CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = finalColour, }, OverlayLayer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = finalColour, Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }, } }; if (hasNumber) { OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponentLookup(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, }, confineMode: ConfineMode.NoScaling) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); } bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; if (overlayAboveNumber) OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue); if (drawableOsuObject != null) { accentColour.BindTo(drawableOsuObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); } } protected override void LoadComplete() { base.LoadComplete(); accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); if (hasNumber) indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); if (drawableObject != null) { drawableObject.ApplyCustomUpdateState += updateStateTransforms; updateStateTransforms(drawableObject, drawableObject.State.Value); } } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { const double legacy_fade_duration = 240; using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime)) { switch (state) { case ArmedState.Hit: CircleSprite.FadeOut(legacy_fade_duration); CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); OverlaySprite.FadeOut(legacy_fade_duration); OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); if (hasNumber) { decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; if (legacyVersion >= 2.0m) // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. hitCircleText.FadeOut(legacy_fade_duration / 4); else { // old skins scale and fade it normally along other pieces. hitCircleText.FadeOut(legacy_fade_duration); hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); } } break; } } } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (drawableObject != null) drawableObject.ApplyCustomUpdateState -= updateStateTransforms; } } }