diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneBreakNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneBreakNote.cs index c4ca501ed..a17c3f1b1 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneBreakNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneBreakNote.cs @@ -1,51 +1,51 @@ -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; -using osuTK; -using osuTK.Graphics; +using System.Collections.Generic; namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneBreakNote : OsuTestScene + public class TestSceneBreakNote : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - private int depthIndex; - - public TestSceneBreakNote() + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Single", () => testSingle()); - AddStep("Hit Single", () => testSingle(true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(bool auto = false) - { - var circle = new Tap + var beatmap = new Beatmap() { - Break = true, - StartTime = Time.Current + 1000, + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, }; - - circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - Add(new DrawableTap(circle) + for (int i = 0; i < 8; ++i) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto - }); + beatmap.HitObjects.Add(new Tap + { + Break = true, + StartTime = 500, + Lane = i + }); + beatmap.HitObjects.Add(new Hold + { + Break = true, + StartTime = 1000, + Duration = 300, + Lane = (i + 3).NormalizePath() + }); + beatmap.HitObjects.Add(new Slide + { + Break = true, + SlideInfoList = new List + { + new SentakkiSlideInfo { + ID = 1, + Duration = 500, + } + }, + StartTime = 1500, + Lane = (i + 7).NormalizePath() + }); + } + + return beatmap; } } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHitObject.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHitObject.cs new file mode 100644 index 000000000..fa99f9888 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHitObject.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using osu.Game.Tests.Visual; +using System.Linq; + +namespace osu.Game.Rulesets.Sentakki.Tests.Objects +{ + public abstract class TestSceneSentakkiHitObject : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new SentakkiRuleset(); + + protected override bool HasCustomSteps => true; + + private bool auto = false; + protected override bool Autoplay => auto; + + [Test] + public void TestMisses() + { + AddStep("Turn off auto", () => auto = false); + CreateTest(null); + AddUntilStep("Wait until all hitobjects are judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.AllJudged)); + } + + [Test] + public void TestHits() + { + AddStep("Turn on auto", () => auto = true); + CreateTest(null); + AddUntilStep("Wait until all hitobjects are judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.AllJudged)); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHoldNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHoldNote.cs index e46fb7477..c963bb17a 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneHoldNote.cs @@ -1,59 +1,29 @@ -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; - namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneHoldNote : OsuTestScene + public class TestSceneHoldNote : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - private int depthIndex; - - public TestSceneHoldNote() - { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Insane Short", () => testSingle(100)); - AddStep("Hit Insane Short", () => testSingle(100, true)); - AddStep("Miss Very Short", () => testSingle(200)); - AddStep("Hit Very Short", () => testSingle(200, true)); - AddStep("Miss Short", () => testSingle(500)); - AddStep("Hit Short", () => testSingle(500, true)); - AddStep("Miss Medium", () => testSingle(750)); - AddStep("Hit Medium", () => testSingle(750, true)); - AddStep("Miss Long", () => testSingle(1000)); - AddStep("Hit Long", () => testSingle(1000, true)); - AddStep("Miss Very Long", () => testSingle(3000)); - AddStep("Hit Very Long", () => testSingle(3000, true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(double duration, bool auto = false) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - var circle = new Hold + var beatmap = new Beatmap() { - StartTime = Time.Current + 1000, - EndTime = Time.Current + 1000 + duration, + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, }; - circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - Add(new DrawableHold(circle) + for (int i = 0; i < 8; ++i) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto - }); + beatmap.HitObjects.Add(new Hold + { + StartTime = 500 + (200 * i), + Duration = 100 + (200 * i), + Lane = i + }); + } + return beatmap; } } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs index 70e5e862d..9ddecc880 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs @@ -1,40 +1,22 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; +using System.Collections.Generic; namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneSlideNote : OsuTestScene + public class TestSceneSlideNote : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - protected override Ruleset CreateRuleset() => new SentakkiRuleset(); - - private int depthIndex; - - public TestSceneSlideNote() + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Single", () => testSingle(2000)); - AddStep("Hit Single", () => testSingle(2000, true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(double duration, bool auto = false) - { - var slide = new Slide + var beatmap = new Beatmap() + { + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, + }; + beatmap.HitObjects.Add(new Slide { - //Break = true, SlideInfoList = new List { new SentakkiSlideInfo { @@ -50,23 +32,9 @@ private void testSingle(double duration, bool auto = false) Duration = 2000, } }, - StartTime = Time.Current + 1000, - }; - - slide.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - DrawableSlide dSlide; - - Add(dSlide = new DrawableSlide(slide) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto + StartTime = 500, }); - - foreach (DrawableSentakkiHitObject nested in dSlide.NestedHitObjects) - nested.Auto = auto; + return beatmap; } } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTapNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTapNote.cs index dc653e939..34a0c43ea 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTapNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTapNote.cs @@ -1,48 +1,27 @@ -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneTapNote : OsuTestScene + public class TestSceneTapNote : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - private int depthIndex; - - public TestSceneTapNote() - { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Single", () => testSingle()); - AddStep("Hit Single", () => testSingle(true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(bool auto = false) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - var circle = new Tap + var beatmap = new Beatmap() { - StartTime = Time.Current + 1000, + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, }; + for (int i = 0; i < 8; ++i) + beatmap.HitObjects.Add(new Tap + { + StartTime = 500 + (100 * i), + Lane = i + }); - circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - Add(new DrawableTap(circle) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto - }); + return beatmap; } } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchHold.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchHold.cs index 3a750be32..ef2f03539 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchHold.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchHold.cs @@ -1,49 +1,26 @@ -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneTouchHold : OsuTestScene + public class TestSceneTouchHold : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - private int depthIndex; - - public TestSceneTouchHold() + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Single", () => testSingle()); - AddStep("Hit Single", () => testSingle(true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(bool auto = false) - { - var circle = new TouchHold + var beatmap = new Beatmap() { - StartTime = Time.Current + 1000, - Duration = 5000, + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, }; - - circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - Add(new DrawableTouchHold(circle) + beatmap.HitObjects.Add(new TouchHold { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto + StartTime = 500, + Duration = 1500 }); + + return beatmap; } } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs index 0943cb1ab..569a0d9cb 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneTouchNote.cs @@ -1,51 +1,29 @@ -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; + using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; -using osu.Game.Rulesets.Sentakki.Objects.Drawables; -using osu.Game.Tests.Visual; -using osuTK; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Sentakki.Tests.Objects { - [TestFixture] - public class TestSceneTouchNote : OsuTestScene + public class TestSceneTouchNote : TestSceneSentakkiHitObject { - private readonly Container content; - protected override Container Content => content; - - private int depthIndex; - - public TestSceneTouchNote() - { - base.Content.Add(content = new SentakkiInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Single", () => testSingle()); - AddStep("Hit Single", () => testSingle(true)); - AddUntilStep("Wait for object despawn", () => !Children.Any(h => (h is DrawableSentakkiHitObject) && (h as DrawableSentakkiHitObject).AllJudged == false)); - } - - private void testSingle(bool auto = false) + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { - var circle = new Touch + var beatmap = new Beatmap() { - StartTime = Time.Current + 1000, - Position = new Vector2(0, -1), + BeatmapInfo = + { + Ruleset = CreateRuleset()?.RulesetInfo ?? ruleset + }, }; + for (int i = 0; i < 8; ++i) + beatmap.HitObjects.Add(new Touch + { + StartTime = 500 + (100 * i), + Position = SentakkiExtensions.GetCircularPosition(RNG.NextSingle(250), RNG.NextSingle(360)) + }); - circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { }); - - Add(new DrawableTouch(circle) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = depthIndex++, - Auto = auto - }); + return beatmap; } - protected override Ruleset CreateRuleset() => new SentakkiRuleset(); } } diff --git a/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiTouchPoint.cs b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiTouchPoint.cs new file mode 100644 index 000000000..7969d2cf4 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/UI/TestSceneSentakkiTouchPoint.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Timing; +using osu.Game.Rulesets.Sentakki.UI.Components; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Tests.UI +{ + [TestFixture] + public class TestSceneSentakkiTouchPoint : OsuGridTestScene + { + public TestSceneSentakkiTouchPoint() : base(1, 2) + { + for (int i = 0; i < 2; ++i) + { + TouchVisualization.TouchPointer tmp; + int j = i; + Cell(i).Add(tmp = new TouchVisualization.TouchPointer().With(d => d.Scale = new Vector2(i == 0 ? -1 : 1, 1))); + tmp.Show(); + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs index bc79a487c..22c9bad53 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/SentakkiBeatmapConverter.cs @@ -23,7 +23,7 @@ public class SentakkiBeatmapConverter : BeatmapConverter { // todo: Check for conversion types that should be supported (ie. Beatmap.HitObjects.Any(h => h is IHasXPosition)) // https://github.com/ppy/osu/tree/master/osu.Game/Rulesets/Objects/Types - public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); + public override bool CanConvert() => true; public Bindable EnabledExperiments = new Bindable(); diff --git a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModAutoplay.cs b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModAutoplay.cs index 251918908..7f8c733db 100644 --- a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModAutoplay.cs +++ b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModAutoplay.cs @@ -33,10 +33,10 @@ public override Score CreateReplayScore(IBeatmap beatmap) public void ApplyToDrawableHitObjects(IEnumerable drawables) { - foreach (var d in drawables.OfType()) + /* foreach (var d in drawables.OfType()) { d.Auto = true; - } + } */ } } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs index e2c51599a..d3275207a 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableHoldHead.cs @@ -18,9 +18,6 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered) { - if (Auto && timeOffset > 0) - ApplyResult(r => r.Type = r.Judgement.MaxResult); - if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(r => r.Type = r.Judgement.MinResult); return; diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs index b8ae73d40..e9bc33cae 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSentakkiHitObject.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Sentakki.Objects.Drawables public class DrawableSentakkiHitObject : DrawableHitObject { protected override double InitialLifetimeOffset => AdjustedAnimationDuration; - public readonly BindableBool AutoBindable = new BindableBool(false); public bool Auto { diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideNode.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideNode.cs index 7e67ffb23..2b1c49622 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideNode.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableSlideNode.cs @@ -67,6 +67,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void Update() { base.Update(); + if (Judged || !IsHittable) return; diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs index db1eb2770..bbd0240ee 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/DrawableTap.cs @@ -74,9 +74,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered) { - if (Auto && timeOffset > 0) - ApplyResult(r => r.Type = r.Judgement.MaxResult); - else if (!HitObject.HitWindows.CanBeHit(timeOffset)) + if (!HitObject.HitWindows.CanBeHit(timeOffset)) ApplyResult(r => r.Type = r.Judgement.MinResult); return; diff --git a/osu.Game.Rulesets.Sentakki/Replays/SentakkiAutoGenerator.cs b/osu.Game.Rulesets.Sentakki/Replays/SentakkiAutoGenerator.cs index f54fc8610..113ccc33a 100644 --- a/osu.Game.Rulesets.Sentakki/Replays/SentakkiAutoGenerator.cs +++ b/osu.Game.Rulesets.Sentakki/Replays/SentakkiAutoGenerator.cs @@ -1,14 +1,21 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Replays; -using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; using osuTK; +using System; +using System.Linq; namespace osu.Game.Rulesets.Sentakki.Replays { public class SentakkiAutoGenerator : AutoGenerator { + public const double KEY_RELEASE_DELAY = 20; + public const double TOUCH_RELEASE_DELAY = 50; + public const double TOUCH_REACT_DELAY = 200; + protected Replay Replay; protected List Frames => Replay.Frames; @@ -18,12 +25,156 @@ public SentakkiAutoGenerator(IBeatmap beatmap) : base(beatmap) { Replay = new Replay(); - Frames.Add(new SentakkiReplayFrame { Position = new Vector2(-1000), Time = -500 }); } public override Replay Generate() { + //add some frames at the beginning so the cursor doesnt suddenly appear on the first note + Frames.Add(new SentakkiReplayFrame { Position = new Vector2(-1000), Time = -500 }); + + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); + + var actions = new List(); + var TouchReplayEvents = new TouchReplayEvent[10]; + + foreach (var group in pointGroups) + { + foreach (var point in group) + { + switch (point) + { + case KeyDown e: + actions.Add(SentakkiAction.Key1 + e.Lane); + break; + + case KeyUp e: + actions.Remove(SentakkiAction.Key1 + e.Lane); + break; + case TouchDown e: + TouchReplayEvents[e.PointNumber] = e.TouchReplayEvent; + break; + case TouchUp e: + TouchReplayEvents[e.PointNumber] = null; + break; + } + } + + // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame. + if (Replay.Frames.Count == 0) + Replay.Frames.Add(new SentakkiReplayFrame(group.First().Time - 1, new Vector2(-1000), false, Array.Empty())); + + Replay.Frames.Add(new SentakkiReplayFrame(group.First().Time, new Vector2(-1000), false, TouchReplayEvents.ToList().ToArray(), actions.ToArray())); + } + return Replay; } + + private IEnumerable generateActionPoints() + { + var touchPointInUsedUntil = new double?[10]; + + for (int i = 0; i < Beatmap.HitObjects.Count; i++) + { + var currentObject = Beatmap.HitObjects[i]; + double endTime = currentObject.GetEndTime(); + + int nextAvailableTouchPoint() => Array.IndexOf(touchPointInUsedUntil, touchPointInUsedUntil.First(t => !t.HasValue || t.Value < currentObject.StartTime)); + + switch (currentObject) + { + case SentakkiLanedHitObject laned: + var nextObjectInColumn = GetNextObject(i) as SentakkiLanedHitObject; // Get the next object that requires pressing the same button + + bool canDelayKeyUp = nextObjectInColumn == null || + nextObjectInColumn.StartTime > endTime + KEY_RELEASE_DELAY; + + double calculatedDelay = canDelayKeyUp ? KEY_RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9; + + yield return new KeyDown { Time = currentObject.StartTime, Lane = laned.Lane }; + yield return new KeyUp { Time = endTime + calculatedDelay, Lane = laned.Lane }; + + if (laned is Slide s) + { + foreach (var slideInfo in s.SlideInfoList) + { + double delay = Beatmap.ControlPointInfo.TimingPointAt(currentObject.StartTime).BeatLength * slideInfo.ShootDelay / 2; + if (delay >= slideInfo.Duration - 50) delay = 0; + yield return new TouchDown + { + Time = currentObject.StartTime + delay, + TouchReplayEvent = new TouchReplayEvent(slideInfo.SlidePath.Path, slideInfo.Duration - delay, currentObject.StartTime + delay, s.Lane.GetRotationForLane() - 22.5f), + PointNumber = nextAvailableTouchPoint() + }; + yield return new TouchUp { Time = currentObject.StartTime + slideInfo.Duration + TOUCH_RELEASE_DELAY, PointNumber = nextAvailableTouchPoint() }; + touchPointInUsedUntil[nextAvailableTouchPoint()] = currentObject.StartTime + slideInfo.Duration + TOUCH_REACT_DELAY; + } + } + break; + + case Touch _: + case TouchHold _: + yield return new TouchDown + { + Time = currentObject.StartTime, + PointNumber = nextAvailableTouchPoint(), + TouchReplayEvent = new TouchReplayEvent((currentObject is Touch x) ? x.Position : Vector2.Zero, TOUCH_RELEASE_DELAY, currentObject.StartTime) + }; + yield return new TouchUp { Time = endTime + TOUCH_RELEASE_DELAY, PointNumber = nextAvailableTouchPoint() }; + touchPointInUsedUntil[nextAvailableTouchPoint()] = endTime + TOUCH_REACT_DELAY; + break; + } + } + } + + protected override HitObject GetNextObject(int currentIndex) + { + int desiredLane = (Beatmap.HitObjects[currentIndex] as SentakkiLanedHitObject).Lane; + + for (int i = currentIndex + 1; i < Beatmap.HitObjects.Count; i++) + { + if (Beatmap.HitObjects[i] is SentakkiLanedHitObject laned && laned.Lane == desiredLane) + return Beatmap.HitObjects[i]; + } + + return null; + } + + private interface IActionPoint + { + double Time { get; set; } + } + private interface ILanedActionPoint : IActionPoint + { + int Lane { get; set; } + } + private interface ITouchActionPoint : IActionPoint + { + TouchReplayEvent TouchReplayEvent { get; set; } + int PointNumber { get; set; } + } + + private struct KeyDown : ILanedActionPoint + { + public double Time { get; set; } + public int Lane { get; set; } + } + + private struct KeyUp : ILanedActionPoint + { + public double Time { get; set; } + public int Lane { get; set; } + } + private struct TouchDown : ITouchActionPoint + { + public double Time { get; set; } + public TouchReplayEvent TouchReplayEvent { get; set; } + public int PointNumber { get; set; } + } + private struct TouchUp : ITouchActionPoint + { + public double Time { get; set; } + public TouchReplayEvent TouchReplayEvent { get; set; } + public int PointNumber { get; set; } + } } } diff --git a/osu.Game.Rulesets.Sentakki/Replays/SentakkiFramedReplayInputHandler.cs b/osu.Game.Rulesets.Sentakki/Replays/SentakkiFramedReplayInputHandler.cs index bd23567e0..95aa70690 100644 --- a/osu.Game.Rulesets.Sentakki/Replays/SentakkiFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Sentakki/Replays/SentakkiFramedReplayInputHandler.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Input.StateChanges; +using osu.Framework.Input; +using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Utils; using osu.Game.Replays; using osu.Game.Rulesets.Replays; using osuTK; +using System; namespace osu.Game.Rulesets.Sentakki.Replays { @@ -35,10 +37,34 @@ protected Vector2 Position public override void CollectPendingInputs(List inputs) { + if (CurrentFrame != null && CurrentFrame.TouchReplayEvents != null) + { + for (int i = 0; i < 10; ++i) + { + var activeTouchPoint = CurrentFrame.TouchReplayEvents[i]; + if (activeTouchPoint == null) + { + inputs.Add(new TouchInput(new Touch((TouchSource)i, Vector2.Zero), false)); + } + else + { + //get point position + double percentage = Math.Clamp(Interpolation.ValueAt(CurrentTime.Value, 0D, 1D, activeTouchPoint.StartTime, activeTouchPoint.StartTime + activeTouchPoint.Duration), 0, 1); + + var position = activeTouchPoint.MovementPath.PositionAt(percentage); + + position = SentakkiExtensions.RotatePointAroundOrigin(position, Vector2.Zero, activeTouchPoint.Rotation) + new Vector2(300); + + inputs.Add(new TouchInput(new Touch((TouchSource)i, GamefieldToScreenSpace(position)), true)); + } + } + } + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position), }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List(), diff --git a/osu.Game.Rulesets.Sentakki/Replays/SentakkiReplayFrame.cs b/osu.Game.Rulesets.Sentakki/Replays/SentakkiReplayFrame.cs index a10ad4638..22d6c7385 100644 --- a/osu.Game.Rulesets.Sentakki/Replays/SentakkiReplayFrame.cs +++ b/osu.Game.Rulesets.Sentakki/Replays/SentakkiReplayFrame.cs @@ -12,16 +12,19 @@ public class SentakkiReplayFrame : ReplayFrame, IConvertibleReplayFrame public Vector2 Position; public List Actions = new List(); + public TouchReplayEvent[] TouchReplayEvents { get; set; } + public bool UsingSensorMode; public SentakkiReplayFrame() { } - public SentakkiReplayFrame(double time, Vector2 position, bool usingSensorMode, params SentakkiAction[] actions) + public SentakkiReplayFrame(double time, Vector2 position, bool usingSensorMode, TouchReplayEvent[] TRE, params SentakkiAction[] actions) : base(time) { Position = position; + TouchReplayEvents = TRE; Actions.AddRange(actions); UsingSensorMode = usingSensorMode; } diff --git a/osu.Game.Rulesets.Sentakki/Replays/TouchReplayFrame.cs b/osu.Game.Rulesets.Sentakki/Replays/TouchReplayFrame.cs new file mode 100644 index 000000000..0c890b9e6 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Replays/TouchReplayFrame.cs @@ -0,0 +1,31 @@ +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Replays +{ + public class TouchReplayEvent + { + public TouchReplayEvent(Vector2 Position, double Duration, double startTime, float rotation = 0) + { + MovementPath = new SliderPath(new PathControlPoint[]{ + new PathControlPoint(Position) + }); + this.Duration = Duration; + StartTime = startTime; + Rotation = rotation; + } + + public TouchReplayEvent(SliderPath path, double Duration, double startTime, float rotation = 0) + { + MovementPath = path; + this.Duration = Duration; + StartTime = startTime; + Rotation = rotation; + } + + public SliderPath MovementPath; + public double Duration; + public double StartTime; + public float Rotation = 0; + } +} diff --git a/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs b/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs index 6057c9098..6fa1b1a9c 100644 --- a/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs +++ b/osu.Game.Rulesets.Sentakki/SentakkiExtensions.cs @@ -1,5 +1,6 @@ using System; using osu.Game.Rulesets.Scoring; +using osu.Framework.Utils; using osu.Game.Rulesets.Sentakki.UI; using osuTK; using osuTK.Graphics; @@ -69,5 +70,36 @@ public static Color4 GetColorForSentakkiResult(this HitResult result) return Color4.LightGray; } } + + public static int GetNoteLaneFromDegrees(this float degrees) + { + if (degrees < 0) degrees += 360; + if (degrees >= 360) degrees %= 360; + int result = 0; + + for (int i = 0; i < SentakkiPlayfield.LANEANGLES.Length; ++i) + { + if (SentakkiPlayfield.LANEANGLES[i] - degrees >= -22.5f && SentakkiPlayfield.LANEANGLES[i] - degrees <= 22.5f) + result = i; + } + return result; + } + + public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) + { + angle = -angle; + + point.X -= origin.X; + point.Y -= origin.Y; + + Vector2 ret; + ret.X = (point.X * MathF.Cos(MathUtils.DegreesToRadians(angle))) + (point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle))); + ret.Y = (point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle))) + (point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle))); + + ret.X += origin.X; + ret.Y += origin.Y; + + return ret; + } } } diff --git a/osu.Game.Rulesets.Sentakki/UI/Components/TouchVisualization.cs b/osu.Game.Rulesets.Sentakki/UI/Components/TouchVisualization.cs new file mode 100644 index 000000000..9aa6462a1 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/UI/Components/TouchVisualization.cs @@ -0,0 +1,153 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; +using osu.Framework.Input; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Colour; +using System.Collections.Generic; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Allocation; +using System; + +namespace osu.Game.Rulesets.Sentakki.UI.Components +{ + public class TouchVisualization : CompositeDrawable + { + private SentakkiInputManager sentakkiActionInputManager; + internal SentakkiInputManager SentakkiActionInputManager => sentakkiActionInputManager ??= GetContainingInputManager() as SentakkiInputManager; + + private DrawablePool pointerPool; + + + //private int leftCount; + //private int rightCount; + + public Dictionary InUsePointers = new Dictionary(); + + + public TouchVisualization() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(pointerPool = new DrawablePool(2)); + } + + + protected override void Update() + { + base.Update(); + + var touchInput = SentakkiActionInputManager.CurrentState.Touch; + foreach (var point in touchInput.ActiveSources) + { + Vector2 newPos = ToLocalSpace(touchInput.GetTouchPosition(point) ?? Vector2.Zero) - OriginPosition; + if (!InUsePointers.TryGetValue(point, out TouchPointer pointer)) + { + bool useLeftHand = false; + + /* if (newPos.X < 0) + { + if (leftCount <= rightCount) + useLeftHand = true; + } + else + { + if (leftCount < rightCount) + useLeftHand = true; + } + + if (useLeftHand) ++leftCount; + else ++rightCount; + Console.WriteLine(leftCount + ", " + rightCount); + */ + AddInternal(pointer = pointerPool.Get()); + pointer.Scale = new Vector2(useLeftHand ? -1 : 1, 1); + InUsePointers[point] = pointer; + } + pointer.Position = newPos; + } + + foreach (var pair in InUsePointers) + { + if (Time.Current - pair.Value.LastActiveTime >= 50 && pair.Value.InUse) + pair.Value.FinishUsage(); + else if (!pair.Value.IsInUse) + { + InUsePointers.Remove(pair.Key);/* + if (pair.Value.LeftHand) --leftCount; + else --rightCount; */ + } + } + } + + public class TouchPointer : PoolableDrawable + { + public override bool RemoveCompletedTransforms => false; + + public double LastActiveTime; + public bool LeftHand => Scale.X == -1; + + public bool InUse; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(50); + InternalChildren = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.Solid.HandPaper, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical(Color4.White, Color4.Gray) + }, + new SpriteIcon + { + Icon = FontAwesome.Regular.HandPaper, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Gray, + }, + }; + } + + public new Vector2 Position + { + get => base.Position; + set + { + base.Position = value; + LastActiveTime = Time.Current; + } + } + protected override void PrepareForUse() + { + this.FadeIn(); + LastActiveTime = Time.Current; + LifetimeStart = Time.Current; + InUse = true; + } + + public void FinishUsage() + { + this.FadeOut(50).Expire(false); + InUse = false; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs index 685711074..09146f626 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiPlayfield.cs @@ -63,7 +63,8 @@ public SentakkiPlayfield() judgementLayer = new Container { RelativeSizeAxes = Axes.Both, - } + }, + }); AddNested(lanedPlayfield); NewResult += onNewResult; @@ -80,6 +81,7 @@ private void load(DrawableSentakkiRuleset drawableRuleset, SentakkiRulesetConfig RegisterPool(2); RegisterPool(8); + AddInternal(new TouchVisualization()); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new SentakkiHitObjectLifetimeEntry(hitObject, sentakkiRulesetConfig, drawableSentakkiRuleset); diff --git a/osu.Game.Rulesets.Sentakki/UI/SentakkiReplayRecorder.cs b/osu.Game.Rulesets.Sentakki/UI/SentakkiReplayRecorder.cs index 792edb95c..c3c03fc16 100644 --- a/osu.Game.Rulesets.Sentakki/UI/SentakkiReplayRecorder.cs +++ b/osu.Game.Rulesets.Sentakki/UI/SentakkiReplayRecorder.cs @@ -4,6 +4,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osuTK; +using System; namespace osu.Game.Rulesets.Sentakki.UI { @@ -18,6 +19,6 @@ public SentakkiReplayRecorder(Score score, DrawableSentakkiRuleset ruleset) } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) - => new SentakkiReplayFrame(Time.Current, mousePosition, drawableRuleset.UseSensorMode, actions.ToArray()); + => new SentakkiReplayFrame(Time.Current, mousePosition, drawableRuleset.UseSensorMode, Array.Empty(), actions.ToArray()); } }