diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs index 5d6ab9bd1a..e8e32ebf3d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs @@ -4,7 +4,6 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Graphics; @@ -76,20 +75,16 @@ namespace osu.Desktop.VisualTests.Tests } }); - Add(new SwellCircle(new CirclePiece + Add(new CirclePiece { - KiaiMode = kiai - }) - { - Position = new Vector2(100, 500) - }); - - Add(new SwellCircle(new StrongCirclePiece - { - KiaiMode = kiai - }) - { - Position = new Vector2(350, 500) + Position = new Vector2(100, 500), + Width = 0, + AccentColour = Color4.Orange, + KiaiMode = kiai, + Children = new[] + { + new SwellSymbolPiece() + } }); Add(new DrumRollCircle(new CirclePiece @@ -111,28 +106,6 @@ namespace osu.Desktop.VisualTests.Tests }); } - private class SwellCircle : BaseCircle - { - public SwellCircle(CirclePiece piece) - : base(piece) - { - Piece.Add(new TextAwesome - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextSize = CirclePiece.SYMBOL_INNER_SIZE, - Icon = FontAwesome.fa_asterisk, - Shadow = false - }); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Piece.AccentColour = colours.YellowDark; - } - } - private class DrumRollCircle : BaseCircle { public DrumRollCircle(CirclePiece piece) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs index a6491bd07b..ba7414b758 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs @@ -26,6 +26,7 @@ namespace osu.Desktop.VisualTests.Tests AddButton("Hit!", addHitJudgement); AddButton("Miss :(", addMissJudgement); + AddButton("Swell", addSwell); AddButton("Centre", () => addCentreHit(false)); AddButton("Strong Centre", () => addCentreHit(true)); AddButton("Rim", () => addRimHit(false)); @@ -74,6 +75,16 @@ namespace osu.Desktop.VisualTests.Tests }); } + private void addSwell() + { + playfield.Add(new DrawableSwell(new Swell + { + StartTime = Time.Current + 1000, + EndTime = Time.Current + 5000, + PreEmpt = 1000 + })); + } + private void addCentreHit(bool strong) { Hit h = new Hit diff --git a/osu.Game.Modes.Taiko/Objects/Drawable/DrawableSwell.cs b/osu.Game.Modes.Taiko/Objects/Drawable/DrawableSwell.cs index 15584ac73f..d789d12c90 100644 --- a/osu.Game.Modes.Taiko/Objects/Drawable/DrawableSwell.cs +++ b/osu.Game.Modes.Taiko/Objects/Drawable/DrawableSwell.cs @@ -1,37 +1,160 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; +using OpenTK.Graphics; using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Taiko.Judgements; +using osu.Game.Modes.Taiko.Objects.Drawable.Pieces; using System; +using System.Linq; namespace osu.Game.Modes.Taiko.Objects.Drawable { public class DrawableSwell : DrawableTaikoHitObject { + /// + /// Invoked when the swell has reached the hit target, i.e. when CurrentTime >= StartTime. + /// This is only ever invoked once. + /// + public event Action OnStart; + + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private readonly Swell swell; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + + private readonly CirclePiece circlePiece; + + private readonly Key[] rimKeys = { Key.D, Key.K }; + private readonly Key[] centreKeys = { Key.F, Key.J }; + private Key[] lastKeySet; + /// /// The amount of times the user has hit this swell. /// private int userHits; - private readonly Swell swell; + private bool hasStarted; + private readonly SwellSymbolPiece symbol; public DrawableSwell(Swell swell) : base(swell) { this.swell = swell; + + Children = new Framework.Graphics.Drawable[] + { + bodyContainer = new Container + { + Children = new Framework.Graphics.Drawable[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Size = new Vector2(TaikoHitObject.CIRCLE_RADIUS * 2), + BlendingMode = BlendingMode.Additive, + Masking = true, + Children = new [] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(TaikoHitObject.CIRCLE_RADIUS * 2), + Masking = true, + BorderThickness = target_ring_thick_border, + BlendingMode = BlendingMode.Additive, + Children = new Framework.Graphics.Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + }, + circlePiece = new CirclePiece + { + Children = new [] + { + symbol = new SwellSymbolPiece() + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + circlePiece.AccentColour = colours.YellowDark; + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } protected override void CheckJudgement(bool userTriggered) { if (userTriggered) { - if (Time.Current < HitObject.StartTime) - return; - userHits++; + var completion = (float)userHits / swell.RequiredHits; + + expandingRing.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50); + expandingRing.Delay(50); + expandingRing.FadeTo(completion / 8, 2000, EasingTypes.OutQuint); + expandingRing.DelayReset(); + + symbol.RotateTo((float)(completion * swell.Duration / 8), 4000, EasingTypes.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, EasingTypes.OutQuint); + if (userHits == swell.RequiredHits) { Judgement.Result = HitResult.Hit; @@ -43,6 +166,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable if (Judgement.TimeOffset < 0) return; + //TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP if (userHits > swell.RequiredHits / 2) { Judgement.Result = HitResult.Hit; @@ -55,11 +179,42 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable protected override void UpdateState(ArmedState state) { + const float preempt = 100; + + Delay(HitObject.StartTime - Time.Current - preempt, true); + + targetRing.ScaleTo(target_ring_scale, preempt * 4, EasingTypes.OutQuint); + + Delay(preempt, true); + + Delay(Judgement.TimeOffset + swell.Duration, true); + + const float out_transition_time = 300; + + switch (state) + { + case ArmedState.Hit: + bodyContainer.ScaleTo(1.4f, out_transition_time); + break; + } + + FadeOut(out_transition_time, EasingTypes.Out); + + Expire(); } protected override void UpdateScrollPosition(double time) { - base.UpdateScrollPosition(Math.Min(time, HitObject.StartTime)); + // Make the swell stop at the hit target + double t = Math.Min(HitObject.StartTime, time); + + if (t == HitObject.StartTime && !hasStarted) + { + OnStart?.Invoke(); + hasStarted = true; + } + + base.UpdateScrollPosition(t); } protected override bool HandleKeyPress(Key key) @@ -67,6 +222,18 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable if (Judgement.Result.HasValue) return false; + // Don't handle keys before the swell starts + if (Time.Current < HitObject.StartTime) + return false; + + // Find the keyset which this key corresponds to + var keySet = rimKeys.Contains(key) ? rimKeys : centreKeys; + + // Ensure alternating keysets + if (keySet == lastKeySet) + return false; + lastKeySet = keySet; + UpdateJudgement(true); return true; diff --git a/osu.Game.Modes.Taiko/Objects/Drawable/Pieces/SwellSymbolPiece.cs b/osu.Game.Modes.Taiko/Objects/Drawable/Pieces/SwellSymbolPiece.cs new file mode 100644 index 0000000000..2bd86406a7 --- /dev/null +++ b/osu.Game.Modes.Taiko/Objects/Drawable/Pieces/SwellSymbolPiece.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics; +using osu.Framework.Graphics; + +namespace osu.Game.Modes.Taiko.Objects.Drawable.Pieces +{ + /// + /// The symbol used for swell pieces. + /// + public class SwellSymbolPiece : TextAwesome + { + public SwellSymbolPiece() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + UseFullGlyphHeight = true; + TextSize = CirclePiece.SYMBOL_INNER_SIZE; + Icon = FontAwesome.fa_asterisk; + Shadow = false; + } + } +} diff --git a/osu.Game.Modes.Taiko/Objects/Swell.cs b/osu.Game.Modes.Taiko/Objects/Swell.cs index 20b9a6effb..4cbb5904c7 100644 --- a/osu.Game.Modes.Taiko/Objects/Swell.cs +++ b/osu.Game.Modes.Taiko/Objects/Swell.cs @@ -17,7 +17,7 @@ namespace osu.Game.Modes.Taiko.Objects /// /// The number of hits required to complete the swell successfully. /// - public int RequiredHits { get; protected set; } + public int RequiredHits { get; protected set; } = 10; public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) { diff --git a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs index eaa8863039..b6165b785b 100644 --- a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Primitives; +using osu.Game.Modes.Taiko.Objects.Drawable; namespace osu.Game.Modes.Taiko.UI { @@ -42,7 +43,7 @@ namespace osu.Game.Modes.Taiko.UI private readonly Container judgementContainer; private readonly Container hitObjectContainer; - //private Container topLevelHitContainer; + private readonly Container topLevelHitContainer; private readonly Container leftBackgroundContainer; private readonly Container rightBackgroundContainer; private readonly Box leftBackground; @@ -143,10 +144,10 @@ namespace osu.Game.Modes.Taiko.UI }, } }, - //topLevelHitContainer = new Container - //{ - // RelativeSizeAxes = Axes.Both, - //} + topLevelHitContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } }); } @@ -165,6 +166,11 @@ namespace osu.Game.Modes.Taiko.UI h.Depth = (float)h.HitObject.StartTime; base.Add(h); + + // Swells should be moved at the very top of the playfield when they reach the hit target + var swell = h as DrawableSwell; + if (swell != null) + swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy()); } public override void OnJudgement(DrawableHitObject judgedObject) diff --git a/osu.Game.Modes.Taiko/osu.Game.Modes.Taiko.csproj b/osu.Game.Modes.Taiko/osu.Game.Modes.Taiko.csproj index 07f9a5d4e6..0492c68036 100644 --- a/osu.Game.Modes.Taiko/osu.Game.Modes.Taiko.csproj +++ b/osu.Game.Modes.Taiko/osu.Game.Modes.Taiko.csproj @@ -66,6 +66,7 @@ +