Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
4919 views
// Copyright (c) ppy Pty Ltd <[email protected]>. 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.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;

namespace osu.Game.Rulesets.Taiko.UI
{
    public sealed partial class TaikoMascotAnimation : BeatSyncedContainer
    {
        private readonly TextureAnimation textureAnimation;

        private int currentFrame;

        public double DisplayTime;

        public TaikoMascotAnimation(TaikoMascotAnimationState state)
        {
            InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
            {
                animation.Origin = animation.Anchor = Anchor.BottomLeft;
                // matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34)
                animation.Scale = new Vector2(0.6f);
            });

            RelativeSizeAxes = Axes.Both;
            Origin = Anchor = Anchor.BottomLeft;

            // needs to be always present to prevent the animation clock consuming time spent when not present.
            AlwaysPresent = true;
        }

        public bool Completed => !textureAnimation.IsPlaying || textureAnimation.PlaybackPosition >= textureAnimation.Duration;

        public override void Show()
        {
            base.Show();
            DisplayTime = Time.Current;
            textureAnimation.Seek(0);
        }

        protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
        {
            // assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched.
            if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying)
                return;

            textureAnimation.GotoFrame(currentFrame);
            currentFrame = (currentFrame + 1) % textureAnimation.FrameCount;
        }

        private static TextureAnimation createTextureAnimation(TaikoMascotAnimationState state)
        {
            switch (state)
            {
                case TaikoMascotAnimationState.Clear:
                    return new ClearMascotTextureAnimation();

                case TaikoMascotAnimationState.Idle:
                case TaikoMascotAnimationState.Kiai:
                case TaikoMascotAnimationState.Fail:
                    return new ManualMascotTextureAnimation(state);

                default:
                    throw new ArgumentOutOfRangeException(nameof(state), $"Mascot animations for state {state} are not supported");
            }
        }

        private partial class ManualMascotTextureAnimation : TextureAnimation
        {
            private readonly TaikoMascotAnimationState state;

            public ManualMascotTextureAnimation(TaikoMascotAnimationState state)
            {
                this.state = state;

                IsPlaying = false;
            }

            [BackgroundDependencyLoader]
            private void load(ISkinSource source)
            {
                ISkin? skin = source.FindProvider(s => getAnimationFrame(s, state, 0) != null);

                if (skin == null) return;

                for (int frameIndex = 0; true; frameIndex++)
                {
                    var texture = getAnimationFrame(skin, state, frameIndex);

                    if (texture == null)
                        break;

                    AddFrame(texture);
                }
            }
        }

        private partial class ClearMascotTextureAnimation : TextureAnimation
        {
            private const float clear_animation_speed = 1000 / 10f;

            private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 };

            public ClearMascotTextureAnimation()
            {
                DefaultFrameLength = clear_animation_speed;
                Loop = false;
            }

            [BackgroundDependencyLoader]
            private void load(ISkinSource source)
            {
                ISkin? skin = source.FindProvider(s => getAnimationFrame(s, TaikoMascotAnimationState.Clear, 0) != null);

                if (skin == null) return;

                foreach (int frameIndex in clear_animation_sequence)
                {
                    var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex);

                    if (texture == null)
                        // as per https://osu.ppy.sh/help/wiki/Skinning/osu!taiko#pippidon
                        break;

                    AddFrame(texture);
                }
            }
        }

        private static Texture? getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
        {
            var texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}{frameIndex}");

            if (frameIndex == 0 && texture == null)
                texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}");

            return texture;
        }
    }
}