diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs new file mode 100644 index 0000000000..dc89fa3a59 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -0,0 +1,155 @@ +// 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 System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneDrawableTaikoMascot : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableTaikoMascot), + }).ToList(); + + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 5000 }, + }; + + private readonly List mascots = new List(); + private readonly List skinnables = new List(); + private readonly List playfields = new List(); + + [Test] + public void TestStateTextures() + { + AddStep("Create mascot (idle)", () => + { + skinnables.Clear(); + SetContents(() => + { + var skinnable = getMascot(); + skinnables.Add(skinnable); + return skinnable; + }); + }); + + AddUntilStep("Wait for SkinnableDrawable", () => skinnables.Any(d => d.Drawable is DrawableTaikoMascot)); + + AddStep("Collect mascots", () => + { + mascots.Clear(); + + foreach (var skinnable in skinnables) + { + if (skinnable.Drawable is DrawableTaikoMascot mascot) + mascots.Add(mascot); + } + }); + + AddStep("Clear state", () => setState(TaikoMascotAnimationState.Clear)); + + AddStep("Kiai state", () => setState(TaikoMascotAnimationState.Kiai)); + + AddStep("Fail state", () => setState(TaikoMascotAnimationState.Fail)); + } + + private void setState(TaikoMascotAnimationState state) + { + foreach (var mascot in mascots) + { + if (mascot == null) + continue; + + mascot.Dumb = true; + mascot.State = state; + } + } + + private SkinnableDrawable getMascot() => + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => new Container(), confineMode: ConfineMode.ScaleToFit) + { + RelativePositionAxes = Axes.Both + }; + + [Test] + public void TestPlayfield() + { + AddStep("Create playfield", () => + { + playfields.Clear(); + SetContents(() => + { + var playfield = new TaikoPlayfield(new ControlPointInfo()) + { + Height = 0.4f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }; + + playfields.Add(playfield); + + return playfield; + }); + }); + + AddUntilStep("Wait for SkinnableDrawable", () => playfields.Any(p => p.ChildrenOfType().Any())); + + AddStep("Collect mascots", () => + { + mascots.Clear(); + + foreach (var playfield in playfields) + { + var mascot = playfield.ChildrenOfType().SingleOrDefault(); + + if (mascot != null) + mascots.Add(mascot); + } + }); + + AddStep("Create hit (miss)", () => + { + foreach (var playfield in playfields) + addJudgement(playfield, HitResult.Miss); + }); + + AddAssert("Check if state is fail", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Fail)); + + AddStep("Create hit (great)", () => + { + foreach (var playfield in playfields) + addJudgement(playfield, HitResult.Great); + }); + + AddAssert("Check if state is idle", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Idle)); + } + + private void addJudgement(TaikoPlayfield playfield, HitResult result) + { + playfield.OnNewResult(new DrawableRimHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index cc79822417..dfc9297a33 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoDon: if (GetTexture("pippidonclear0") != null) - return new DrawableTaikoCharacter(); + return new DrawableTaikoMascot(); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs deleted file mode 100644 index aace96aa9b..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class DrawableTaikoCharacter : BeatSyncedContainer - { - private static TaikoDonTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; - - private TaikoDonAnimationState state; - - public DrawableTaikoCharacter() - { - RelativeSizeAxes = Axes.Both; - } - - private TaikoDonTextureAnimation getStateDrawable() => State switch - { - TaikoDonAnimationState.Idle => idleDrawable, - TaikoDonAnimationState.Clear => clearDrawable, - TaikoDonAnimationState.Kiai => kiaiDrawable, - TaikoDonAnimationState.Fail => failDrawable, - _ => null - }; - - public TaikoDonAnimationState State - { - get => state; - set - { - state = value; - - foreach (var child in InternalChildren) - child.Hide(); - - getStateDrawable().Show(); - } - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - InternalChildren = new[] - { - idleDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Idle), - clearDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Clear), - kiaiDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Kiai), - failDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Fail), - }; - - // sets the state, to make sure we have the correct sprite loaded and set. - State = TaikoDonAnimationState.Idle; - } - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) - { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - - getStateDrawable().Move(); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs new file mode 100644 index 0000000000..fbac1e9d0b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -0,0 +1,99 @@ +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class DrawableTaikoMascot : BeatSyncedContainer + { + private static TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; + private EffectControlPoint lastEffectControlPoint; + private TaikoMascotAnimationState state; + + public Bindable PlayfieldState; + + /// + /// Determines if there should be no "state logic", intended for testing. + /// + public bool Dumb { get; set; } + + public TaikoMascotAnimationState State + { + get => state; + set + { + state = value; + + foreach (var child in InternalChildren) + child.Hide(); + + var drawable = getStateDrawable(State); + + drawable?.Show(); + } + } + + public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) + { + RelativeSizeAxes = Axes.Both; + PlayfieldState = new Bindable(); + PlayfieldState.BindValueChanged((b) => + { + if (lastEffectControlPoint != null) + State = getFinalAnimationState(lastEffectControlPoint, b.NewValue); + }); + + State = startingState; + } + + private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) => state switch + { + TaikoMascotAnimationState.Idle => idleDrawable, + TaikoMascotAnimationState.Clear => clearDrawable, + TaikoMascotAnimationState.Kiai => kiaiDrawable, + TaikoMascotAnimationState.Fail => failDrawable, + _ => null + }; + + private TaikoMascotAnimationState getFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) + { + if (playfieldState == TaikoMascotAnimationState.Fail) + return playfieldState; + + return effectPoint.KiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + InternalChildren = new[] + { + idleDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), + clearDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), + kiaiDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), + failDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), + }; + + // making sure we have the correct sprite set + State = state; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!Dumb) + State = getFinalAnimationState(lastEffectControlPoint = effectPoint, PlayfieldState.Value); + + if (State == TaikoMascotAnimationState.Clear) + return; + + var drawable = getStateDrawable(State); + drawable.Move(); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs deleted file mode 100644 index 315cd57f13..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs +++ /dev/null @@ -1,61 +0,0 @@ -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Textures; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class TaikoDonTextureAnimation : TextureAnimation - { - private readonly TaikoDonAnimationState state; - private int currentFrame; - - public TaikoDonTextureAnimation(TaikoDonAnimationState state) : base(false) - { - this.state = state; - this.Stop(); - - Origin = Anchor.BottomLeft; - Anchor = Anchor.BottomLeft; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - for (int i = 0;; i++) - { - var textureName = $"pippidon{_getStateString(state)}{i}"; - Texture texture = skin.GetTexture(textureName); - - if (texture == null) - break; - - AddFrame(texture); - } - } - - /// - /// Advances the current frame by one. - /// - public void Move() - { - if (FrameCount <= currentFrame) - currentFrame = 0; - - GotoFrame(currentFrame); - - currentFrame++; - } - - private string _getStateString(TaikoDonAnimationState state) => state switch - { - TaikoDonAnimationState.Clear => "clear", - TaikoDonAnimationState.Fail => "fail", - TaikoDonAnimationState.Idle => "idle", - TaikoDonAnimationState.Kiai => "kiai", - _ => null - }; - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs similarity index 86% rename from osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs rename to osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs index 773710ee7e..02bf245b7b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Taiko.UI { - public enum TaikoDonAnimationState + public enum TaikoMascotAnimationState { Idle, Clear, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs new file mode 100644 index 0000000000..19a533156e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -0,0 +1,88 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class TaikoMascotTextureAnimation : TextureAnimation + { + private const float clear_animation_speed = 1000 / 10F; + private static readonly int[] clear_animation_sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; + private int currentFrame; + + public TaikoMascotAnimationState State { get; } + + public TaikoMascotTextureAnimation(TaikoMascotAnimationState state) + : base(true) + { + State = state; + + // We're animating on beat if it's not the clear animation + if (state == TaikoMascotAnimationState.Clear) + DefaultFrameLength = clear_animation_speed; + else + this.Stop(); + + Origin = Anchor.BottomLeft; + Anchor = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + if (State == TaikoMascotAnimationState.Clear) + { + foreach (var textureIndex in clear_animation_sequence) + { + var textureName = _getStateTextureName(textureIndex); + Texture texture = skin.GetTexture(textureName); + + if (texture == null) + break; + + AddFrame(texture); + } + } + else + { + for (int i = 0;; i++) + { + var textureName = _getStateTextureName(i); + Texture texture = skin.GetTexture(textureName); + + if (texture == null) + break; + + AddFrame(texture); + } + } + } + + /// Advances the current frame by one. + public void Move() + { + if (FrameCount == 0) // Frames are apparently broken + return; + + if (FrameCount <= currentFrame) + currentFrame = 0; + + GotoFrame(currentFrame); + + currentFrame += 1; + } + + private string _getStateTextureName(int i) => $"pippidon{_getStateString(State)}{i}"; + + private string _getStateString(TaikoMascotAnimationState state) => state switch + { + TaikoMascotAnimationState.Clear => "clear", + TaikoMascotAnimationState.Fail => "fail", + TaikoMascotAnimationState.Idle => "idle", + TaikoMascotAnimationState.Kiai => "kiai", + _ => null + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index c86a6f61b2..3bf5d084ee 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -268,16 +268,18 @@ namespace osu.Game.Rulesets.Taiko.UI break; } - if (characterDrawable.Drawable is DrawableTaikoCharacter character) + if (characterDrawable.Drawable is DrawableTaikoMascot mascot) { - if (result.Type == HitResult.Miss && result.Judgement.AffectsCombo) + var isFailing = result.Type == HitResult.Miss; + + // Only take combo in consideration when it's not a strong hit (it's always false) + if (!(judgedObject.HitObject is StrongHitObject)) { - character.State = TaikoDonAnimationState.Fail; - } - else - { - character.State = judgedObject.HitObject.Kiai ? TaikoDonAnimationState.Kiai : TaikoDonAnimationState.Idle; + if (isFailing) + isFailing = result.Judgement.AffectsCombo; } + + mascot.PlayfieldState.Value = isFailing ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; } } }