diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 7cad9306c1..2efb0c0707 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps yield return new Fruit { StartTime = obj.StartTime, - Position = ((IHasXPosition)obj).X / OsuPlayfield.BASE_SIZE.X + NewCombo = (obj as IHasCombo)?.NewCombo ?? false, + X = ((IHasXPosition)obj).X / OsuPlayfield.BASE_SIZE.X }; } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs new file mode 100644 index 0000000000..7fac19d135 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Beatmaps +{ + internal class CatchBeatmapProcessor : BeatmapProcessor + { + public override void PostProcess(Beatmap beatmap) + { + if (beatmap.ComboColors.Count == 0) + return; + + int comboIndex = 0; + int colourIndex = 0; + + CatchBaseHit lastObj = null; + + foreach (var obj in beatmap.HitObjects) + { + if (obj.NewCombo) + { + if (lastObj != null) lastObj.LastInCombo = true; + + comboIndex = 0; + colourIndex = (colourIndex + 1) % beatmap.ComboColors.Count; + } + + obj.ComboIndex = comboIndex++; + obj.ComboColour = beatmap.ComboColors[colourIndex]; + + lastObj = obj; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs b/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs index de0547580f..2f33cf1093 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs @@ -2,11 +2,23 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public abstract class CatchBaseHit : HitObject + public abstract class CatchBaseHit : HitObject, IHasXPosition, IHasCombo { - public float Position { get; set; } + public float X { get; set; } + + public Color4 ComboColour { get; set; } = Color4.Gray; + public int ComboIndex { get; set; } + + public virtual bool NewCombo { get; set; } + + /// + /// The next fruit starts a new combo. Used for explodey. + /// + public virtual bool LastInCombo { get; set; } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index 3dd086fb48..e0c9f0c028 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableFruit : DrawableScrollingHitObject { - private const float pulp_size = 30; + private const float pulp_size = 20; private class Pulp : Circle, IHasAccentColour { @@ -26,15 +26,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { Size = new Vector2(pulp_size); - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = AccentColour.Opacity(0.5f), - }; + Blending = BlendingMode.Additive; + Colour = Color4.White.Opacity(0.9f); } - public Color4 AccentColour { get; set; } = Color4.White; + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + accentColour = value; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Lighten(100), + }; + } + } } @@ -42,12 +53,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable : base(h) { Origin = Anchor.Centre; - Size = new Vector2(pulp_size * 2, pulp_size * 2.6f); + Size = new Vector2(pulp_size * 2.2f, pulp_size * 2.8f); RelativePositionAxes = Axes.Both; - X = h.Position; + X = h.X; - Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1); + AccentColour = HitObject.ComboColour; + + Masking = false; Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } @@ -71,6 +84,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativePositionAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + AccentColour = AccentColour, Scale = new Vector2(0.6f), }, new Pulp @@ -78,6 +92,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativePositionAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + AccentColour = AccentColour, Y = -0.08f }, new Pulp @@ -85,6 +100,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativePositionAxes = Axes.Both, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + AccentColour = AccentColour, Y = -0.08f }, new Pulp @@ -92,6 +108,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativePositionAxes = Axes.Both, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + AccentColour = AccentColour, }, } } diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 11e95622ca..16756e65f1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,7 +1,10 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -9,13 +12,22 @@ namespace osu.Game.Rulesets.Catch.Scoring { internal class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor() - { - } - public CatchScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } + + protected override void SimulateAutoplay(Beatmap beatmap) + { + foreach (var obj in beatmap.HitObjects) + { + var fruit = obj as Fruit; + + if (fruit != null) + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + } + + base.SimulateAutoplay(beatmap); + } } } diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs new file mode 100644 index 0000000000..8d18a712d8 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer + { + protected override Beatmap CreateBeatmap() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 256; i++) + beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 }); + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 1aee75fb78..8a6ef71996 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); + protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); + protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); protected override Playfield CreatePlayfield() => new CatchPlayfield(); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 5fc2cf9ef7..2930dbb7cc 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.MathUtils; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; @@ -20,20 +19,25 @@ namespace osu.Game.Rulesets.Catch.UI public class CatcherArea : Container { private Catcher catcher; + private Container explodingFruitContainer; public void Add(DrawableHitObject fruit, Vector2 screenPosition) => catcher.AddToStack(fruit, screenPosition); - public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.Position) < catcher.DrawSize.X / DrawSize.X / 2; + public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2; [BackgroundDependencyLoader] private void load() { Children = new Drawable[] { + explodingFruitContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, catcher = new Catcher { RelativePositionAxes = Axes.Both, - Anchor = Anchor.TopLeft, + ExplodingFruitTarget = explodingFruitContainer, Origin = Anchor.TopCentre, X = 0.5f, } @@ -51,18 +55,30 @@ namespace osu.Game.Rulesets.Catch.UI { private Texture texture; + private Container caughtFruit; + [BackgroundDependencyLoader] private void load(TextureStore textures) { texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); - Child = createCatcherSprite(); + Children = new Drawable[] + { + createCatcherSprite(), + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + } + }; } private int currentDirection; private bool dashing; + public Container ExplodingFruitTarget; + protected bool Dashing { get { return dashing; } @@ -139,16 +155,21 @@ namespace osu.Game.Rulesets.Catch.UI return false; } + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + private const double base_speed = 1.0 / 512; + protected override void Update() { base.Update(); if (currentDirection == 0) return; - float speed = Dashing ? 1.5f : 1; + double dashModifier = Dashing ? 1 : 0.5; Scale = new Vector2(Math.Sign(currentDirection), 1); - X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime / 1800 * speed, 0, 1); + X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1); } public void AddToStack(DrawableHitObject fruit, Vector2 absolutePosition) @@ -163,13 +184,45 @@ namespace osu.Game.Rulesets.Catch.UI float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; - while (Children.OfType().Any(f => Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) + while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) { fruit.X += RNG.Next(-5, 5); fruit.Y -= RNG.Next(0, 5); } - Add(fruit); + caughtFruit.Add(fruit); + + if (((CatchBaseHit)fruit.HitObject).LastInCombo) + explode(); + } + + private void explode() + { + var fruit = caughtFruit.ToArray(); + + foreach (var f in fruit) + { + var originalX = f.X * Scale.X; + + if (ExplodingFruitTarget != null) + { + f.Anchor = Anchor.TopLeft; + f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); + + caughtFruit.Remove(f); + + ExplodingFruitTarget.Add(f); + } + + f.MoveToY(f.Y - 50, 250, Easing.OutSine) + .Then() + .MoveToY(f.Y + 50, 500, Easing.InSine); + + f.MoveToX(f.X + originalX * 6, 1000); + f.FadeOut(750); + + f.Expire(); + } } } } diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index 4ef60cd80e..323a934e27 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -47,6 +47,7 @@ + @@ -57,6 +58,7 @@ + diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index d299faaae2..50239bf16c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -16,10 +16,6 @@ namespace osu.Game.Rulesets.Osu.Scoring { internal class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor() - { - } - public OsuScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index bb3122489e..277846ee80 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps { BeatmapInfo = beatmapInfo; BeatmapSetInfo = beatmapInfo.BeatmapSet; - Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo.Metadata; + Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); Mods.ValueChanged += mods => applyRateAdjustments(); } diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 49146c4173..4a25a52e36 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -16,7 +16,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { - internal class TestCasePlayer : OsuTestCase + public class TestCasePlayer : OsuTestCase { protected Player Player; private RulesetStore rulesets; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual loadPlayerFor(rulesets.Query().First()); } - private void loadPlayerFor(RulesetInfo r) + protected virtual Beatmap CreateBeatmap() { Beatmap beatmap; @@ -53,6 +53,13 @@ namespace osu.Game.Tests.Visual using (var reader = new StreamReader(stream)) beatmap = BeatmapDecoder.GetDecoder(reader).Decode(reader); + return beatmap; + } + + private void loadPlayerFor(RulesetInfo r) + { + var beatmap = CreateBeatmap(); + beatmap.BeatmapInfo.Ruleset = r; var instance = r.CreateInstance();