diff --git a/osu.Android.props b/osu.Android.props index 6a8e66ee6a..1a63b893a1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index fbbe00bb6c..fe0d512166 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - SetContents(() => new CatcherArea.Catcher + SetContents(() => new Catcher { RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 070847c0c1..df5494aab0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(CatcherArea.Catcher), + typeof(Catcher), typeof(DrawableCatchRuleset), typeof(DrawableFruit), typeof(DrawableJuiceStream), @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Tests private DrawableCatchRuleset drawableRuleset; private double playfieldTime => drawableRuleset.Playfield.Time.Current; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void Setup() => Schedule(() => { var controlPointInfo = new ControlPointInfo(); controlPointInfo.Add(0, new TimingControlPoint()); @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests ControlPointInfo = controlPointInfo }); - Add(new Container + Child = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -66,16 +66,49 @@ namespace osu.Game.Rulesets.Catch.Tests { drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo)) } - }); + }; + }); + + [Test] + public void TestFruits() + { + AddStep("hit fruits", () => spawnFruits(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); AddStep("miss fruits", () => spawnFruits()); - AddStep("hit fruits", () => spawnFruits(true)); - AddStep("miss juicestream", () => spawnJuiceStream()); - AddStep("hit juicestream", () => spawnJuiceStream(true)); - AddStep("miss bananas", () => spawnBananas()); - AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); } + [Test] + public void TestJuicestream() + { + AddStep("hit juicestream", () => spawnJuiceStream(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss juicestream", () => spawnJuiceStream()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); + } + + [Test] + public void TestBananas() + { + AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss bananas", () => spawnBananas()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + } + + private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive); + + private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState; + private void spawnFruits(bool hit = false) { for (int i = 1; i <= 4; i++) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6f0d8f0a3a..49ff9df4d7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 1a5d0f983b..7c81bcdf0c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps ApplyPositionOffsets(Beatmap); - initialiseHyperDash((List)Beatmap.HitObjects); - int index = 0; foreach (var obj in Beatmap.HitObjects.OfType()) @@ -76,6 +74,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; case JuiceStream juiceStream: + // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. + lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH; + + // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. + lastStartTime = juiceStream.StartTime; + foreach (var nested in juiceStream.NestedHitObjects) { var catchObject = (CatchHitObject)nested; @@ -90,20 +94,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; } } + + initialiseHyperDash(beatmap); } private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - if (hitObject is JuiceStream stream) - { - lastPosition = stream.EndX; - lastStartTime = stream.EndTime; - return; - } - - if (!(hitObject is Fruit)) - return; - float offsetPosition = hitObject.X; double startTime = hitObject.StartTime; @@ -116,7 +112,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } float positionDiff = offsetPosition - lastPosition.Value; - double timeDiff = startTime - lastStartTime; + + // Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double. + int timeDiff = (int)(startTime - lastStartTime); if (timeDiff > 1000) { @@ -132,7 +130,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps return; } - if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d) + // ReSharper disable once PossibleLossOfFraction + if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3) applyOffset(ref offsetPosition, positionDiff); hitObject.XOffset = offsetPosition - hitObject.X; @@ -191,14 +190,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } - private void initialiseHyperDash(List objects) + private static void initialiseHyperDash(IBeatmap beatmap) { List objectWithDroplets = new List(); - foreach (var currentObject in objects) + foreach (var currentObject in beatmap.HitObjects) { - if (currentObject is Fruit) - objectWithDroplets.Add(currentObject); + if (currentObject is Fruit fruitObject) + objectWithDroplets.Add(fruitObject); if (currentObject is JuiceStream) { @@ -212,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2; int lastDirection = 0; double lastExcess = halfCatcherWidth; @@ -221,10 +220,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps CatchHitObject currentObject = objectWithDroplets[i]; CatchHitObject nextObject = objectWithDroplets[i + 1]; + // Reset variables in-case values have changed (e.g. after applying HR) + currentObject.HyperDashTarget = null; + currentObject.DistanceToHyperDash = 0; + int thisDirection = nextObject.X > currentObject.X ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); - float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext); + float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext); if (distanceToHyper < 0) { diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 44e1a8e5cc..5880a227c2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override int SectionLength => 750; + private float halfCatcherWidth; + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - float halfCatchWidth; - - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { - halfCatchWidth = catcher.CatchWidth * 0.5f; - halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } - CatchHitObject lastObject = null; // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. @@ -69,16 +63,25 @@ namespace osu.Game.Rulesets.Catch.Difficulty continue; if (lastObject != null) - yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth); + yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth); lastObject = hitObject; } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap) { - new Movement(), - }; + using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + { + halfCatcherWidth = catcher.CatchWidth * 0.5f; + halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. + } + + return new Skill[] + { + new Movement(halfCatcherWidth), + }; + } protected override Mod[] DifficultyAdjustmentMods => new Mod[] { diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 7cd569035b..fd164907e0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills protected override double DecayWeight => 0.94; + protected readonly float HalfCatcherWidth; + private float? lastPlayerPosition; private float lastDistanceMoved; + public Movement(float halfCatcherWidth) + { + HalfCatcherWidth = halfCatcherWidth; + } + protected override double StrainValueOf(DifficultyHitObject current) { var catchCurrent = (CatchDifficultyHitObject)current; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 4c72b9fd3e..1ef235f764 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition { - private readonly CatcherArea.Catcher catcher; + private readonly Catcher catcher; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 4649dcae90..b90b5812a6 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { // todo: add support for HT DT - const double dash_speed = CatcherArea.Catcher.BASE_SPEED; + const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; float lastPosition = 0.5f; double lastTime = 0; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs new file mode 100644 index 0000000000..a3dc58bc19 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -0,0 +1,460 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class Catcher : Container, IKeyBindingHandler + { + /// + /// Whether we are hyper-dashing or not. + /// + public bool HyperDashing => hyperDashModifier != 1; + + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + public const double BASE_SPEED = 1.0 / 512; + + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public CatcherAnimationState CurrentState { get; private set; } + + /// + /// Width of the area that can be used to attempt catches during gameplay. + /// + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + + protected bool Dashing + { + get => dashing; + set + { + if (value == dashing) return; + + dashing = value; + + Trail |= dashing; + } + } + + /// + /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// + protected bool Trail + { + get => trail; + set + { + if (value == trail) return; + + trail = value; + + if (Trail) + beginTrail(); + } + } + + private Container caughtFruit; + + private CatcherSprite catcherIdle; + private CatcherSprite catcherKiai; + private CatcherSprite catcherFail; + + private int currentDirection; + + private bool dashing; + + private bool trail; + + private double hyperDashModifier = 1; + private int hyperDashDirection; + private float hyperDashTargetPosition; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + + Size = new Vector2(CatcherArea.CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherFail = new CatcherSprite(CatcherAnimationState.Fail) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + } + }; + + updateCatcher(); + } + + /// + /// Add a caught fruit to the catcher's stack. + /// + /// The fruit that was caught. + public void PlaceOnPlate(DrawableCatchHitObject fruit) + { + var ourRadius = fruit.DisplayRadius; + float theirRadius = 0; + + const float allowance = 6; + + while (caughtFruit.Any(f => + f.LifetimeEnd == double.MaxValue && + Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) + { + var diff = (ourRadius + theirRadius) / allowance; + fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; + fruit.Y -= RNG.NextSingle() * diff; + } + + fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + + caughtFruit.Add(fruit); + + Add(new HitExplosion(fruit) + { + X = fruit.X, + Scale = new Vector2(fruit.HitObject.Scale) + }); + } + + /// + /// Let the catcher attempt to catch a fruit. + /// + /// The fruit to catch. + /// Whether the catch is possible. + public bool AttemptCatch(CatchHitObject fruit) + { + var halfCatchWidth = CatchWidth * 0.5f; + + // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. + var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; + var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + + var validCatch = + catchObjectPosition >= catcherPosition - halfCatchWidth && + catchObjectPosition <= catcherPosition + halfCatchWidth; + + // only update hyperdash state if we are catching a fruit. + // exceptions are Droplets and JuiceStreams. + if (!(fruit is Fruit)) return validCatch; + + if (validCatch && fruit.HyperDash) + { + var target = fruit.HyperDashTarget; + var timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; + var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + + SetHyperDashState(Math.Abs(velocity), target.X); + } + else + SetHyperDashState(); + + if (validCatch) + updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); + + return validCatch; + } + + /// + /// Set hyper-dash state. + /// + /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. + /// When this catcher crosses this position, this catcher ends hyper-dashing. + public void SetHyperDashState(double modifier = 1, float targetPosition = -1) + { + const float hyper_dash_transition_length = 180; + + var wasHyperDashing = HyperDashing; + + if (modifier <= 1 || X == targetPosition) + { + hyperDashModifier = 1; + hyperDashDirection = 0; + + if (wasHyperDashing) + { + this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); + Trail &= Dashing; + } + } + else + { + hyperDashModifier = modifier; + hyperDashDirection = Math.Sign(targetPosition - X); + hyperDashTargetPosition = targetPosition; + + if (!wasHyperDashing) + { + this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); + Trail = true; + + var hyperDashEndGlow = createAdditiveSprite(true); + + hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.FadeOut(1200); + hyperDashEndGlow.Expire(true); + } + } + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + + case CatchAction.MoveRight: + currentDirection++; + return true; + + case CatchAction.Dash: + Dashing = true; + return true; + } + + return false; + } + + public void OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + break; + + case CatchAction.MoveRight: + currentDirection--; + break; + + case CatchAction.Dash: + Dashing = false; + break; + } + } + + public void UpdatePosition(float position) + { + position = Math.Clamp(position, 0, 1); + + if (position == X) + return; + + Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); + X = position; + } + + /// + /// Drop any fruit off the plate. + /// + public void Drop() + { + foreach (var f in caughtFruit.ToArray()) + Drop(f); + } + + /// + /// Explode any fruit off the plate. + /// + public void Explode() + { + foreach (var f in caughtFruit.ToArray()) + Explode(f); + } + + public void Drop(DrawableHitObject fruit) + { + removeFromPlateWithTransform(fruit, f => + { + f.MoveToY(f.Y + 75, 750, Easing.InSine); + f.FadeOut(750); + }); + } + + public void Explode(DrawableHitObject fruit) + { + var originalX = fruit.X * Scale.X; + + removeFromPlateWithTransform(fruit, 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); + }); + } + + protected override void Update() + { + base.Update(); + + if (currentDirection == 0) return; + + var direction = Math.Sign(currentDirection); + + var dashModifier = Dashing ? 1 : 0.5; + var speed = BASE_SPEED * dashModifier * hyperDashModifier; + + UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); + + // Correct overshooting. + if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || + (hyperDashDirection < 0 && hyperDashTargetPosition > X)) + { + X = hyperDashTargetPosition; + SetHyperDashState(); + } + } + + private void updateCatcher() + { + catcherIdle.Hide(); + catcherKiai.Hide(); + catcherFail.Hide(); + + CatcherSprite current; + + switch (CurrentState) + { + default: + current = catcherIdle; + break; + + case CatcherAnimationState.Fail: + current = catcherFail; + break; + + case CatcherAnimationState.Kiai: + current = catcherKiai; + break; + } + + current.Show(); + (current.Drawable as IAnimation)?.GotoFrame(0); + } + + private void beginTrail() + { + Trail &= dashing || HyperDashing; + Trail &= AdditiveTarget != null; + + if (!Trail) return; + + var additive = createAdditiveSprite(HyperDashing); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Drawable createAdditiveSprite(bool hyperDash) + { + var additive = createCatcherSprite(); + + additive.Anchor = Anchor; + additive.Scale = Scale; + additive.Colour = hyperDash ? Color4.Red : Color4.White; + additive.Blending = BlendingParameters.Additive; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Position = Position; + + AdditiveTarget.Add(additive); + + return additive; + } + + private Drawable createCatcherSprite() + { + return new CatcherSprite(CurrentState); + } + + private void updateState(CatcherAnimationState state) + { + if (CurrentState == state) + return; + + CurrentState = state; + updateCatcher(); + } + + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) + { + if (ExplodingFruitTarget != null) + { + fruit.Anchor = Anchor.TopLeft; + fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); + + if (!caughtFruit.Remove(fruit)) + // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). + // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. + return; + + ExplodingFruitTarget.Add(fruit); + } + + var actionTime = Clock.CurrentTime; + + fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; + onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); + + void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) + { + using (fruit.BeginAbsoluteSequence(actionTime)) + action(fruit); + + fruit.Expire(); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9ee94636f1..e0d9ff759d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -2,15 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Bindings; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -20,7 +13,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { @@ -28,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI { public const float CATCHER_SIZE = 106.75f; - protected internal readonly Catcher MovableCatcher; - public Func> CreateDrawableRepresentation; public Container ExplodingFruitTarget @@ -37,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI set => MovableCatcher.ExplodingFruitTarget = value; } + private DrawableCatchHitObject lastPlateableFruit; + public CatcherArea(BeatmapDifficulty difficulty = null) { RelativeSizeAxes = Axes.X; @@ -47,7 +39,10 @@ namespace osu.Game.Rulesets.Catch.UI }; } - private DrawableCatchHitObject lastPlateableFruit; + public static float GetCatcherSize(BeatmapDifficulty difficulty) + { + return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) { @@ -100,6 +95,15 @@ namespace osu.Game.Rulesets.Catch.UI } } + public void OnReleased(CatchAction action) + { + } + + public bool AttemptCatch(CatchHitObject obj) + { + return MovableCatcher.AttemptCatch(obj); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -110,559 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.X = state.CatcherX.Value; } - public void OnReleased(CatchAction action) - { - } - - public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); - - public static float GetCatcherSize(BeatmapDifficulty difficulty) - { - return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - public class Catcher : Container, IKeyBindingHandler - { - /// - /// Width of the area that can be used to attempt catches during gameplay. - /// - internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X); - - private Container caughtFruit; - - public Container ExplodingFruitTarget; - - public Container AdditiveTarget; - - public Catcher(BeatmapDifficulty difficulty = null) - { - RelativePositionAxes = Axes.X; - X = 0.5f; - - Origin = Anchor.TopCentre; - - Size = new Vector2(CATCHER_SIZE); - if (difficulty != null) - Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, - catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherFail = new CatcherSprite(CatcherAnimationState.Fail) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - } - }; - - updateCatcher(); - } - - private CatcherSprite catcherIdle; - private CatcherSprite catcherKiai; - private CatcherSprite catcherFail; - - private void updateCatcher() - { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; - - switch (currentState) - { - default: - current = catcherIdle; - break; - - case CatcherAnimationState.Fail: - current = catcherFail; - break; - - case CatcherAnimationState.Kiai: - current = catcherKiai; - break; - } - - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); - } - - private int currentDirection; - - private bool dashing; - - protected bool Dashing - { - get => dashing; - set - { - if (value == dashing) return; - - dashing = value; - - Trail |= dashing; - } - } - - private bool trail; - - /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. - /// - protected bool Trail - { - get => trail; - set - { - if (value == trail) return; - - trail = value; - - if (Trail) - beginTrail(); - } - } - - private void beginTrail() - { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; - - if (!Trail) return; - - var additive = createAdditiveSprite(HyperDashing); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); - } - - private Drawable createAdditiveSprite(bool hyperDash) - { - var additive = createCatcherSprite(); - - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = hyperDash ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); - - return additive; - } - - private Drawable createCatcherSprite() => new CatcherSprite(currentState); - - /// - /// Add a caught fruit to the catcher's stack. - /// - /// The fruit that was caught. - public void PlaceOnPlate(DrawableCatchHitObject fruit) - { - float ourRadius = fruit.DisplayRadius; - float theirRadius = 0; - - const float allowance = 6; - - while (caughtFruit.Any(f => - f.LifetimeEnd == double.MaxValue && - Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) - { - float diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; - fruit.Y -= RNG.NextSingle() * diff; - } - - fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2); - - caughtFruit.Add(fruit); - - Add(new HitExplosion(fruit) - { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); - } - - /// - /// Let the catcher attempt to catch a fruit. - /// - /// The fruit to catch. - /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) - { - float halfCatchWidth = CatchWidth * 0.5f; - - // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. - var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; - var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; - - var validCatch = - catchObjectPosition >= catcherPosition - halfCatchWidth && - catchObjectPosition <= catcherPosition + halfCatchWidth; - - // only update hyperdash state if we are catching a fruit. - // exceptions are Droplets and JuiceStreams. - if (!(fruit is Fruit)) return validCatch; - - if (validCatch && fruit.HyperDash) - { - var target = fruit.HyperDashTarget; - double timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; - double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - - SetHyperDashState(Math.Abs(velocity), target.X); - } - else - { - SetHyperDashState(); - } - - if (validCatch) - updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else - updateState(CatcherAnimationState.Fail); - - return validCatch; - } - - private void updateState(CatcherAnimationState state) - { - if (currentState == state) - return; - - currentState = state; - updateCatcher(); - } - - private CatcherAnimationState currentState; - - private double hyperDashModifier = 1; - private int hyperDashDirection; - private float hyperDashTargetPosition; - - /// - /// Whether we are hyper-dashing or not. - /// - public bool HyperDashing => hyperDashModifier != 1; - - /// - /// Set hyper-dash state. - /// - /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. - /// When this catcher crosses this position, this catcher ends hyper-dashing. - public void SetHyperDashState(double modifier = 1, float targetPosition = -1) - { - const float hyper_dash_transition_length = 180; - - bool wasHyperDashing = HyperDashing; - - if (modifier <= 1 || X == targetPosition) - { - hyperDashModifier = 1; - hyperDashDirection = 0; - - if (wasHyperDashing) - { - this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); - Trail &= Dashing; - } - } - else - { - hyperDashModifier = modifier; - hyperDashDirection = Math.Sign(targetPosition - X); - hyperDashTargetPosition = targetPosition; - - if (!wasHyperDashing) - { - this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); - Trail = true; - - var hyperDashEndGlow = createAdditiveSprite(true); - - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); - hyperDashEndGlow.FadeOut(1200); - hyperDashEndGlow.Expire(true); - } - } - } - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - - case CatchAction.MoveRight: - currentDirection++; - return true; - - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public void OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - break; - - case CatchAction.MoveRight: - currentDirection--; - break; - - case CatchAction.Dash: - Dashing = false; - break; - } - } - - /// - /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. - /// - public const double BASE_SPEED = 1.0 / 512; - - protected override void Update() - { - base.Update(); - - if (currentDirection == 0) return; - - var direction = Math.Sign(currentDirection); - - double dashModifier = Dashing ? 1 : 0.5; - double speed = BASE_SPEED * dashModifier * hyperDashModifier; - - UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); - - // Correct overshooting. - if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || - (hyperDashDirection < 0 && hyperDashTargetPosition > X)) - { - X = hyperDashTargetPosition; - SetHyperDashState(); - } - } - - public void UpdatePosition(float position) - { - position = Math.Clamp(position, 0, 1); - - if (position == X) - return; - - Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); - X = position; - } - - /// - /// Drop any fruit off the plate. - /// - public void Drop() - { - foreach (var f in caughtFruit.ToArray()) - Drop(f); - } - - /// - /// Explode any fruit off the plate. - /// - public void Explode() - { - foreach (var f in caughtFruit.ToArray()) - Explode(f); - } - - public void Drop(DrawableHitObject fruit) => removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y + 75, 750, Easing.InSine); - f.FadeOut(750); - }); - - public void Explode(DrawableHitObject fruit) - { - var originalX = fruit.X * Scale.X; - - removeFromPlateWithTransform(fruit, 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); - }); - } - - private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) - { - if (ExplodingFruitTarget != null) - { - fruit.Anchor = Anchor.TopLeft; - fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - - if (!caughtFruit.Remove(fruit)) - // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). - // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. - return; - - ExplodingFruitTarget.Add(fruit); - } - - double actionTime = Clock.CurrentTime; - - fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; - onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); - - void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) - { - using (fruit.BeginAbsoluteSequence(actionTime)) - action(fruit); - - fruit.Expire(); - } - } - } - } - - public class HitExplosion : CompositeDrawable - { - private readonly CircularContainer largeFaint; - - public HitExplosion(DrawableCatchHitObject fruit) - { - Size = new Vector2(20); - Anchor = Anchor.TopCentre; - Origin = Anchor.BottomCentre; - - Color4 objectColour = fruit.AccentColour.Value; - - // scale roughly in-line with visual appearance of notes - - const float angle_variangle = 15; // should be less than 45 - - const float roundness = 100; - - const float initial_height = 10; - - var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); - - InternalChildren = new Drawable[] - { - largeFaint = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.8f), - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - const double duration = 400; - - largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) - .FadeOut(duration * 2); - - this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); - Expire(true); - } + protected internal readonly Catcher MovableCatcher; } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs new file mode 100644 index 0000000000..04a86f83be --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class HitExplosion : CompositeDrawable + { + private readonly CircularContainer largeFaint; + + public HitExplosion(DrawableCatchHitObject fruit) + { + Size = new Vector2(20); + Anchor = Anchor.TopCentre; + Origin = Anchor.BottomCentre; + + Color4 objectColour = fruit.AccentColour.Value; + + // scale roughly in-line with visual appearance of notes + + const float angle_variangle = 15; // should be less than 45 + + const float roundness = 100; + + const float initial_height = 10; + + var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); + + InternalChildren = new Drawable[] + { + largeFaint = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + // we want our size to be very small so the glow dominates it. + Size = new Vector2(0.8f), + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double duration = 400; + + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) + .FadeOut(duration * 2); + + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); + Expire(true); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7403649184..ccc731779d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplySkin(skin, allowFallback); bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; - Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; + Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index c871089acd..5a6dd49c44 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -16,16 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning; using osuTK.Graphics; using osu.Game.Skinning; using osuTK; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition + public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { public Func GetInitialHitAction; + public Color4 AccentColour + { + get => ball.Colour; + set => ball.Colour = value; + } + private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; + private readonly CircularContainer ball; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { @@ -47,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, - new CircularContainer + ball = new CircularContainer { Masking = true, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index c76d4fd5b8..7a257a1603 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat; @@ -78,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); AddAssert($"msg #{index} has the right action", hasExpectedActions); - AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); bool hasExpectedActions() @@ -97,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online return true; } - bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); + //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); bool isShowingLinks() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 19bdaff6ff..736bfd8e7d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -38,8 +38,13 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; + private IEnumerable visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+"); + private IEnumerable joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+"); private readonly List channels; + private Channel currentChannel => channelManager.CurrentChannel.Value; + private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1); + private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; @@ -91,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } @@ -102,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } @@ -140,10 +145,67 @@ namespace osu.Game.Tests.Visual.Online var targetNumberKey = oneBasedIndex % 10; var targetChannel = channels[zeroBasedIndex]; AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"channel #{oneBasedIndex} is selected", () => channelManager.CurrentChannel.Value == targetChannel); + AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); } } + private Channel expectedChannel; + + [Test] + public void TestCloseChannelWhileActive() + { + AddUntilStep("Join until dropdown has channels", () => + { + if (visibleChannels.Count() < joinedChannels.Count()) + return true; + + // Using temporary channels because they don't hide their names when not active + Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary }; + channelManager.JoinChannel(toAdd); + + return false; + }); + + AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()])); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel before dropdown + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Next channel selected", () => currentChannel == expectedChannel); + + // Depending on the window size, one more channel might need to be closed for the selectorTab to appear + AddUntilStep("Close channels until selector visible", () => + { + if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+") + return true; + + chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last()); + return false; + }); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel with dropdown no longer present + AddStep("Close last when selector next", () => + { + expectedChannel = previousChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to previous", () => currentChannel == expectedChannel); + + // Standard channel closing + AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to next", () => currentChannel == expectedChannel); + } + private void pressChannelHotkey(int number) { var channelKey = Key.Number0 + number; @@ -187,6 +249,8 @@ namespace osu.Game.Tests.Visual.Online { public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; + public new ChannelTabControl ChannelTabControl => base.ChannelTabControl; + public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); @@ -196,12 +260,22 @@ namespace osu.Game.Tests.Visual.Online private class TestTabControl : ChannelTabControl { - protected override TabItem CreateTabItem(Channel value) => new TestChannelTabItem(value); + protected override TabItem CreateTabItem(Channel value) + { + switch (value.Type) + { + case ChannelType.PM: + return new TestPrivateChannelTabItem(value); + + default: + return new TestChannelTabItem(value); + } + } public new IReadOnlyDictionary> TabMap => base.TabMap; } - private class TestChannelTabItem : PrivateChannelTabItem + private class TestChannelTabItem : ChannelTabItem { public TestChannelTabItem(Channel channel) : base(channel) @@ -210,5 +284,15 @@ namespace osu.Game.Tests.Visual.Online public new ClickableContainer CloseButton => base.CloseButton; } + + private class TestPrivateChannelTabItem : PrivateChannelTabItem + { + public TestPrivateChannelTabItem(Channel channel) + : base(channel) + { + } + + public new ClickableContainer CloseButton => base.CloseButton; + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..80e03d82e2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -497,7 +497,7 @@ namespace osu.Game.Tests.Visual.SongSelect } bool changed = false; - AddStep($"Load {beatmapSets.Count} Beatmaps", () => + AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => { carousel.Filter(new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; @@ -697,6 +697,8 @@ namespace osu.Game.Tests.Visual.SongSelect public new List Items => base.Items; public bool PendingFilterTask => PendingFilter != null; + + protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } } } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs new file mode 100644 index 0000000000..9f885ed827 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -0,0 +1,42 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Screens.Gameplay.Components; +using osuTK; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneMatchHeader : TournamentTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTournamentHeaderText), + typeof(DrawableTournamentHeaderLogo), + }; + + public TestSceneMatchHeader() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(50), + Children = new Drawable[] + { + new TournamentSpriteText { Text = "with logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader(), + new TournamentSpriteText { Text = "without logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowLogo = false }, + new TournamentSpriteText { Text = "without scores", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowScores = false }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index fa5c941f1a..ef8c8767e0 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tournament.Components public ControlPanel() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; AlwaysPresent = true; - Width = 0.15f; + Width = TournamentSceneManager.CONTROL_AREA_WIDTH; Anchor = Anchor.TopRight; InternalChildren = new Drawable[] @@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Components Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = 0.75f, Position = new Vector2(0, 35f), + Padding = new MarginPadding(5), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5f), }, diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs new file mode 100644 index 0000000000..3f5ab42fd7 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderLogo : CompositeDrawable + { + public DrawableTournamentHeaderLogo() + { + InternalChild = new LogoSprite(); + + Height = 82; + RelativeSizeAxes = Axes.X; + } + + private class LogoSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-logo"); + } + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs new file mode 100644 index 0000000000..bda696ba00 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderText : CompositeDrawable + { + public DrawableTournamentHeaderText() + { + InternalChild = new TextSprite(); + + Height = 22; + RelativeSizeAxes = Axes.X; + } + + private class TextSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-text"); + } + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs deleted file mode 100644 index 4fbc6cd060..0000000000 --- a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; - -namespace osu.Game.Tournament.Components -{ - public class DrawableTournamentTitleText : TournamentSpriteText - { - public DrawableTournamentTitleText() - { - Text = "osu!taiko world cup 2020"; - Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold); - } - } -} diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index dd56c83c57..bebede6782 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new TournamentSpriteText { Text = match.Round.Value?.Name.Value ?? "Unknown Round", diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 4116ffbec6..477bf4bd63 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -16,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Tournament.Models; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament.Components @@ -125,13 +124,21 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mods)) { - AddInternal(new Sprite + AddInternal(new Container { - Texture = textures.Get($"mods/{mods}"), + RelativeSizeAxes = Axes.Y, + Width = 60, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding(10), - Scale = new Vector2(0.8f) + Child = new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Texture = textures.Get($"mods/{mods}"), + } }); } } diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 43088d6b92..bc66fad8c1 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; -using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics; @@ -28,13 +27,13 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentStorage storage) { - var stream = storage.GetStream($@"videos/{filename}.m4v"); + var stream = storage.GetStream($@"videos/{filename}"); if (stream != null) { - InternalChild = video = new VideoSprite(stream) + InternalChild = video = new VideoSprite(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index b19f2bedf0..eefa9fcfe6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -163,12 +163,7 @@ namespace osu.Game.Tournament.IPC { try { - stableInstallPath = "G:\\My Drive\\Main\\osu!tourney"; - - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = "G:\\My Drive\\Main\\osu!mappool"; + stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); if (checkExists(stableInstallPath)) return stableInstallPath; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 69a68c946b..d790f4b754 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,14 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -17,13 +14,39 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private TeamScoreDisplay teamDisplay1; private TeamScoreDisplay teamDisplay2; + private DrawableTournamentHeaderLogo logo; + + private bool showScores = true; public bool ShowScores { + get => showScores; set { - teamDisplay1.ShowScore = value; - teamDisplay2.ShowScore = value; + if (value == showScores) + return; + + showScores = value; + + if (IsLoaded) + updateDisplay(); + } + } + + private bool showLogo = true; + + public bool ShowLogo + { + get => showLogo; + set + { + if (value == showLogo) + return; + + showLogo = value; + + if (IsLoaded) + updateDisplay(); } } @@ -38,19 +61,25 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), Spacing = new Vector2(5), Children = new Drawable[] { - new DrawableTournamentTitleText + logo = new DrawableTournamentHeaderLogo { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.2f) + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Alpha = showLogo ? 1 : 0 }, - new RoundDisplay + new DrawableTournamentHeaderText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new MatchRoundDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Scale = new Vector2(0.4f) }, } @@ -66,76 +95,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = Anchor.TopRight, }, }; - } - } - public class TeamScoreDisplay : CompositeDrawable - { - private readonly TeamColour teamColour; - - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); - private readonly Bindable currentTeamScore = new Bindable(); - - private TeamDisplay teamDisplay; - - public bool ShowScore { set => teamDisplay.ShowScore = value; } - - public TeamScoreDisplay(TeamColour teamColour) - { - this.teamColour = teamColour; - - RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; + updateDisplay(); } - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) + private void updateDisplay() { - currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); - } + teamDisplay1.ShowScore = showScores; + teamDisplay2.ShowScore = showScores; - private void matchChanged(ValueChangedEvent match) - { - currentTeamScore.UnbindBindings(); - currentTeam.UnbindBindings(); - - if (match.NewValue != null) - { - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); - } - - // team may change to same team, which means score is not in a good state. - // thus we handle this manually. - teamChanged(currentTeam.Value); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) - { - case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) - currentTeamScore.Value++; - return true; - - case MouseButton.Right: - if (currentTeamScore.Value > 0) - currentTeamScore.Value--; - return true; - } - - return base.OnMouseDown(e); - } - - private void teamChanged(TournamentTeam team) - { - InternalChildren = new Drawable[] - { - teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), - }; + logo.Alpha = showLogo ? 1 : 0; } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs similarity index 92% rename from osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs rename to osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index c8b0d3bdda..87793f7e1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class RoundDisplay : TournamentSpriteTextWithBackground + public class MatchRoundDisplay : TournamentSpriteTextWithBackground { private readonly Bindable currentMatch = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index ed14956793..2e7484542a 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -131,13 +132,15 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; Winning = false; + + DisplayedCountSpriteText.Spacing = new Vector2(-6); } public bool Winning { set => DisplayedCountSpriteText.Font = value - ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) - : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs new file mode 100644 index 0000000000..462015f004 --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Tournament.Models; +using osuTK.Input; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamScoreDisplay : CompositeDrawable + { + private readonly TeamColour teamColour; + + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeamScore = new Bindable(); + + private TeamDisplay teamDisplay; + + public bool ShowScore { set => teamDisplay.ShowScore = value; } + + public TeamScoreDisplay(TeamColour teamColour) + { + this.teamColour = teamColour; + + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindTo(ladder.CurrentMatch); + currentMatch.BindValueChanged(matchChanged, true); + } + + private void matchChanged(ValueChangedEvent match) + { + currentTeamScore.UnbindBindings(); + currentTeam.UnbindBindings(); + + if (match.NewValue != null) + { + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + } + + // team may change to same team, which means score is not in a good state. + // thus we handle this manually. + teamChanged(currentTeam.Value); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) + { + case MouseButton.Left: + if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + currentTeamScore.Value++; + return true; + + case MouseButton.Right: + if (currentTeamScore.Value > 0) + currentTeamScore.Value--; + return true; + } + + return base.OnMouseDown(e); + } + + private void teamChanged(TournamentTeam team) + { + InternalChildren = new Drawable[] + { + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 4d770855cd..8920990d1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -47,7 +47,10 @@ namespace osu.Game.Tournament.Screens.Gameplay Loop = true, RelativeSizeAxes = Axes.Both, }, - header = new MatchHeader(), + header = new MatchHeader + { + ShowLogo = false + }, new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 6f62b3ddba..534c402f6c 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Loop = true, }, - new DrawableTournamentTitleText + new DrawableTournamentHeaderText { Y = 100, Anchor = Anchor.TopCentre, diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 4f3f7cfdbf..2b0bfe0b74 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader(), mapFlows = new FillFlowContainer> { - Y = 100, + Y = 140, Spacing = new Vector2(10, 10), Padding = new MarginPadding(25), Direction = FillDirection.Vertical, @@ -235,6 +235,7 @@ namespace osu.Game.Tournament.Screens.MapPool { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Height = 42, }); } } diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 0fcec645e3..88289ad6bd 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Schedule Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new Container { Margin = new MarginPadding { Top = 40 }, diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 023582166c..b7f8b2bfd6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -22,6 +25,7 @@ namespace osu.Game.Tournament.Screens private FillFlowContainer fillFlow; private LoginOverlay loginOverlay; + private ActionableInfo resolution; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -32,9 +36,13 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + private Bindable windowSize; + [BackgroundDependencyLoader] - private void load() + private void load(FrameworkConfigManager frameworkConfig) { + windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + InternalChild = fillFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -48,6 +56,9 @@ namespace osu.Game.Tournament.Screens reload(); } + [Resolved] + private Framework.Game game { get; set; } + private void reload() { var fileBasedIpc = ipc as FileBasedIPC; @@ -97,9 +108,25 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, + resolution = new ActionableInfo + { + Label = "Stream area resolution", + ButtonText = "Set to 1080p", + Action = () => + { + windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); + } + } }; } + protected override void Update() + { + base.Update(); + + resolution.Value = $"{ScreenSpaceDrawQuad.Width:N0}x{ScreenSpaceDrawQuad.Height:N0}"; + } + public class LabelledDropdown : LabelledComponent, T> { public LabelledDropdown() diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 41165ca141..85db9e61fb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tournament private Storage storage; + private TournamentStorage tournamentStorage; + private DependencyContainer dependencies; private Bindable windowSize; @@ -54,14 +56,16 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - Textures.AddStore(new TextureLoaderStore(new ResourceStore(new StorageBackedResourceStore(storage)))); + dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage)); + + Textures.AddStore(new TextureLoaderStore(tournamentStorage)); this.storage = storage; windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => { - var minWidth = (int)(size.NewValue.Height / 9f * 16 + 400); + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; }), true); diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index ef8d16011d..23fcb01db7 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -33,6 +33,12 @@ namespace osu.Game.Tournament private Container screens; private TourneyVideo video; + public const float CONTROL_AREA_WIDTH = 160; + + public const float STREAM_AREA_WIDTH = 1366; + + public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH; + [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); @@ -51,13 +57,13 @@ namespace osu.Game.Tournament { new Container { - RelativeSizeAxes = Axes.Both, - X = 200, + RelativeSizeAxes = Axes.Y, + X = CONTROL_AREA_WIDTH, FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Size = new Vector2(0.8f, 1), + Width = STREAM_AREA_WIDTH, //Masking = true, Children = new Drawable[] { @@ -96,7 +102,7 @@ namespace osu.Game.Tournament new Container { RelativeSizeAxes = Axes.Y, - Width = 200, + Width = CONTROL_AREA_WIDTH, Children = new Drawable[] { new Box @@ -108,8 +114,8 @@ namespace osu.Game.Tournament { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(2), + Spacing = new Vector2(5), + Padding = new MarginPadding(5), Children = new Drawable[] { new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen }, diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs new file mode 100644 index 0000000000..139ad3857b --- /dev/null +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.IO.Stores; +using osu.Framework.Platform; + +namespace osu.Game.Tournament +{ + internal class TournamentStorage : NamespacedResourceStore + { + public TournamentStorage(Storage storage) + : base(new StorageBackedResourceStore(storage), "tournament") + { + AddExtension("m4v"); + AddExtension("avi"); + AddExtension("mp4"); + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 527f520172..3420fcf260 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Beatmaps.Formats { @@ -26,17 +25,5 @@ namespace osu.Game.Beatmaps.Formats AddDecoder(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last()))); SetFallbackDecoder(() => new LegacyDifficultyCalculatorBeatmapDecoder()); } - - protected override TimingControlPoint CreateTimingControlPoint() - => new LegacyDifficultyCalculatorTimingControlPoint(); - - private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint - { - public LegacyDifficultyCalculatorTimingControlPoint() - { - BeatLengthBindable.MinValue = double.MinValue; - BeatLengthBindable.MaxValue = double.MaxValue; - } - } } } diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 841936d2c5..7c78141b4d 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -30,8 +30,15 @@ namespace osu.Game.Graphics /// Whether the font is italic. /// Whether all characters should be spaced the same distance apart. /// The . - public static FontUsage GetFont(Typeface typeface = Typeface.Exo, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) - => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); + public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) + => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), getItalics(italics), fixedWidth); + + private static bool getItalics(in bool italicsRequested) + { + // right now none of our fonts support italics. + // should add exceptions to this rule if they come up. + return false; + } /// /// Retrieves the string representation of a . @@ -42,9 +49,6 @@ namespace osu.Game.Graphics { switch (typeface) { - case Typeface.Exo: - return "Exo2.0"; - case Typeface.Venera: return "Venera"; @@ -62,7 +66,13 @@ namespace osu.Game.Graphics /// The . /// The string representation of in the specified . public static string GetWeightString(Typeface typeface, FontWeight weight) - => GetWeightString(GetFamilyString(typeface), weight); + { + if (typeface == Typeface.Torus && weight == FontWeight.Medium) + // torus doesn't have a medium; fallback to regular. + weight = FontWeight.Regular; + + return GetWeightString(GetFamilyString(typeface), weight); + } /// /// Retrieves the string representation of a . @@ -96,7 +106,6 @@ namespace osu.Game.Graphics public enum Typeface { - Exo, Venera, Torus } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 6c883d9893..ca9f1330f9 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -173,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected override void OnActivated() => fadeActive(); diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index ddcb626701..d05a08108a 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index 7d2b661164..151efc4645 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -12,5 +12,6 @@ namespace osu.Game.Online.Chat Temporary, PM, Group, + System, } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b2277e2abf..3c7ab27651 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,30 +138,17 @@ namespace osu.Game dependencies.Cache(LocalConfig); AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Exo2.0-Medium"); - AddFont(Resources, @"Fonts/Exo2.0-MediumItalic"); + + AddFont(Resources, @"Fonts/Torus-Regular"); + AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto-CJK-Basic"); AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); - AddFont(Resources, @"Fonts/Exo2.0-Regular"); - AddFont(Resources, @"Fonts/Exo2.0-RegularItalic"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBold"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Bold"); - AddFont(Resources, @"Fonts/Exo2.0-BoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Light"); - AddFont(Resources, @"Fonts/Exo2.0-LightItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Black"); - AddFont(Resources, @"Fonts/Exo2.0-BlackItalic"); - - AddFont(Resources, @"Fonts/Torus-SemiBold"); - AddFont(Resources, @"Fonts/Torus-Bold"); - AddFont(Resources, @"Fonts/Torus-Regular"); - AddFont(Resources, @"Fonts/Torus-Light"); - AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Black"); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 2576900db8..a0b1b27ebf 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.AccountCreation usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); - emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Exo, weight: FontWeight.Bold)); + emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); passwordDescription.AddText("At least "); characterCheckText = passwordDescription.AddText("8 characters long"); diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 8aee76cb08..48bf6c2ddd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Changelog Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, } } }; @@ -125,7 +126,7 @@ namespace osu.Game.Overlays.Changelog title.AddText("by ", t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; t.Padding = new MarginPadding { Left = 10 }; }); @@ -138,7 +139,7 @@ namespace osu.Game.Overlays.Changelog Id = entry.GithubUser.UserId.Value }, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Changelog { title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Changelog { title.AddText(entry.GithubUser.DisplayName, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index d5d9a6c2ce..e3ede04edd 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs public ChannelSelectorTabChannel() { Name = "+"; + Type = ChannelType.System; } } } diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 104495ae01..a72f182450 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Chat.Tabs // performTabSort might've made selectorTab's position wonky, fix it TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); - ((ChannelTabItem)item).OnRequestClose += tabCloseRequested; + ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value); base.AddTabItem(item, addToDropdown); } @@ -74,18 +74,24 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Removes a channel from the ChannelTabControl. - /// If the selected channel is the one that is beeing removed, the next available channel will be selected. + /// If the selected channel is the one that is being removed, the next available channel will be selected. /// /// The channel that is going to be removed. public void RemoveChannel(Channel channel) { - RemoveItem(channel); - if (Current.Value == channel) { - // Prefer non-selector channels first - Current.Value = Items.FirstOrDefault(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)) ?? Items.FirstOrDefault(); + var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList(); + var isNextTabSelector = allChannels[allChannels.IndexOf(channel) + 1] == selectorTab.Value; + + // selectorTab is not switchable, so we have to explicitly select it if it's the only tab left + if (isNextTabSelector && allChannels.Count == 2) + SelectTab(selectorTab); + else + SwitchTab(isNextTabSelector ? -1 : 1); } + + RemoveItem(channel); } protected override void SelectTab(TabItem tab) @@ -100,21 +106,6 @@ namespace osu.Game.Overlays.Chat.Tabs selectorTab.Active.Value = false; } - private void tabCloseRequested(TabItem tab) - { - int totalTabs = TabContainer.Count - 1; // account for selectorTab - int currentIndex = Math.Clamp(TabContainer.IndexOf(tab), 1, totalTabs); - - if (tab == SelectedTab && totalTabs > 1) - // Select the tab after tab-to-be-removed's index, or the tab before if current == last - SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); - else if (totalTabs == 1 && !selectorTab.Active.Value) - // Open channel selection overlay if all channel tabs will be closed after removing this tab - SelectTab(selectorTab); - - OnRequestLeave?.Invoke(tab.Value); - } - protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer { Direction = FillDirection.Full, diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index f61b30b381..e381b629e4 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 50, }, - Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold), Text = info.Title, }, new OsuSpriteText @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 30, }, - Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold), Text = "by " + info.Author } }; @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), + Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false), Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 4bff8146b4..3478f18a40 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -14,22 +14,8 @@ namespace osu.Game.Overlays.Volume public Func ActionRequested; public Func ScrollActionRequested; - public bool OnPressed(GlobalAction action) - { - // if nothing else handles selection actions in the game, it's safe to let volume be adjusted. - switch (action) - { - case GlobalAction.SelectPrevious: - action = GlobalAction.IncreaseVolume; - break; - - case GlobalAction.SelectNext: - action = GlobalAction.DecreaseVolume; - break; - } - - return ActionRequested?.Invoke(action) ?? false; - } + public bool OnPressed(GlobalAction action) => + ActionRequested?.Invoke(action) ?? false; public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1fc51d2ce8..8d3ad5984f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 7) { - length = Math.Max(0, Parsing.ParseDouble(split[7])); + length = Math.Max(0, Parsing.ParseDouble(split[7], Parsing.MAX_COORDINATE_VALUE)); if (length == 0) length = null; } diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index ee8200321b..35091028ae 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -90,14 +90,14 @@ namespace osu.Game.Screens.Menu } }; - textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); - textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); + textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Light)); + textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold)); textFlow.NewParagraph(); static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); - textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold)); + textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold)); textFlow.NewParagraph(); textFlow.NewParagraph(); diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 4e51ff939a..be5762e68d 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -259,11 +259,18 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { + private readonly Stream videoStream; + public LazerLogo(Stream videoStream) { + this.videoStream = videoStream; Size = new Vector2(960); + } - InternalChild = new VideoSprite(videoStream) + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new VideoSprite(videoStream, false) { RelativeSizeAxes = Axes.Both, Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs index a55db096af..4152a9a3b2 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components if (host.NewValue != null) { hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold, italics: true)); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true)); flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both }; } diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs index f8fb192b5c..0d31805774 100644 --- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs +++ b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Ranking.Pages rankText.AddText($"#{index + 1} ", s => { - s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold); + s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold); s.Colour = colours.YellowDark; }); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71744d8b80..04c08cdbd2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -153,9 +153,11 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapHidden += beatmapHidden; beatmaps.BeatmapRestored += beatmapRestored; - loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); + loadBeatmapSets(GetLoadableBeatmaps()); } + protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index fa4de21eec..52328d43b2 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning { var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); - if (iniRate != null) + if (iniRate?.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cc1ab654ab..95d09e84eb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 04b688cfa3..ce7ff38988 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -79,7 +79,7 @@ - +