1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 15:47:26 +08:00

Merge branch 'master' into always-apply-ruleset-filter

This commit is contained in:
Dan Balasescu 2020-03-16 10:32:36 +09:00 committed by GitHub
commit 25a35a1370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1214 additions and 854 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.310.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.312.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new CatcherArea.Catcher SetContents(() => new Catcher
{ {
RelativePositionAxes = Axes.None, RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -4,7 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(CatcherArea.Catcher), typeof(Catcher),
typeof(DrawableCatchRuleset), typeof(DrawableCatchRuleset),
typeof(DrawableFruit), typeof(DrawableFruit),
typeof(DrawableJuiceStream), typeof(DrawableJuiceStream),
@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private DrawableCatchRuleset drawableRuleset; private DrawableCatchRuleset drawableRuleset;
private double playfieldTime => drawableRuleset.Playfield.Time.Current; private double playfieldTime => drawableRuleset.Playfield.Time.Current;
[BackgroundDependencyLoader] [SetUp]
private void load() public void Setup() => Schedule(() =>
{ {
var controlPointInfo = new ControlPointInfo(); var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint()); controlPointInfo.Add(0, new TimingControlPoint());
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests
ControlPointInfo = controlPointInfo ControlPointInfo = controlPointInfo
}); });
Add(new Container Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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)) 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("miss fruits", () => spawnFruits());
AddStep("hit fruits", () => spawnFruits(true)); AddUntilStep("wait for completion", () => playfieldIsEmpty);
AddStep("miss juicestream", () => spawnJuiceStream()); AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail);
AddStep("hit juicestream", () => spawnJuiceStream(true));
AddStep("miss bananas", () => spawnBananas());
AddStep("hit bananas", () => spawnBananas(true));
} }
[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) private void spawnFruits(bool hit = false)
{ {
for (int i = 1; i <= 4; i++) for (int i = 1; i <= 4; i++)

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
} }
private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher; private Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {

View File

@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
ApplyPositionOffsets(Beatmap); ApplyPositionOffsets(Beatmap);
initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
int index = 0; int index = 0;
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>()) foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
@ -76,6 +74,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
break; break;
case JuiceStream juiceStream: 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) foreach (var nested in juiceStream.NestedHitObjects)
{ {
var catchObject = (CatchHitObject)nested; var catchObject = (CatchHitObject)nested;
@ -90,20 +94,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
break; break;
} }
} }
initialiseHyperDash(beatmap);
} }
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) 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; float offsetPosition = hitObject.X;
double startTime = hitObject.StartTime; double startTime = hitObject.StartTime;
@ -116,7 +112,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
} }
float positionDiff = offsetPosition - lastPosition.Value; 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) if (timeDiff > 1000)
{ {
@ -132,7 +130,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
return; 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); applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X; hitObject.XOffset = offsetPosition - hitObject.X;
@ -191,14 +190,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
} }
} }
private void initialiseHyperDash(List<CatchHitObject> objects) private static void initialiseHyperDash(IBeatmap beatmap)
{ {
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>(); List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>();
foreach (var currentObject in objects) foreach (var currentObject in beatmap.HitObjects)
{ {
if (currentObject is Fruit) if (currentObject is Fruit fruitObject)
objectWithDroplets.Add(currentObject); objectWithDroplets.Add(fruitObject);
if (currentObject is JuiceStream) if (currentObject is JuiceStream)
{ {
@ -212,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); 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; int lastDirection = 0;
double lastExcess = halfCatcherWidth; double lastExcess = halfCatcherWidth;
@ -221,10 +220,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
CatchHitObject currentObject = objectWithDroplets[i]; CatchHitObject currentObject = objectWithDroplets[i];
CatchHitObject nextObject = objectWithDroplets[i + 1]; 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; 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 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); 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) if (distanceToHyper < 0)
{ {

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override int SectionLength => 750; protected override int SectionLength => 750;
private float halfCatcherWidth;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) protected override IEnumerable<DifficultyHitObject> 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; CatchHitObject lastObject = null;
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. // 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; continue;
if (lastObject != null) if (lastObject != null)
yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth); yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth);
lastObject = hitObject; 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[] protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{ {

View File

@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected override double DecayWeight => 0.94; protected override double DecayWeight => 0.94;
protected readonly float HalfCatcherWidth;
private float? lastPlayerPosition; private float? lastPlayerPosition;
private float lastDistanceMoved; private float lastDistanceMoved;
public Movement(float halfCatcherWidth)
{
HalfCatcherWidth = halfCatcherWidth;
}
protected override double StrainValueOf(DifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
var catchCurrent = (CatchDifficultyHitObject)current; var catchCurrent = (CatchDifficultyHitObject)current;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
{ {
private readonly CatcherArea.Catcher catcher; private readonly Catcher catcher;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate() public override Replay Generate()
{ {
// todo: add support for HT DT // 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; const double movement_speed = dash_speed / 2;
float lastPosition = 0.5f; float lastPosition = 0.5f;
double lastTime = 0; double lastTime = 0;

View File

@ -0,0 +1,460 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<CatchAction>
{
/// <summary>
/// Whether we are hyper-dashing or not.
/// </summary>
public bool HyperDashing => hyperDashModifier != 1;
/// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// </summary>
public const double BASE_SPEED = 1.0 / 512;
public Container ExplodingFruitTarget;
public Container AdditiveTarget;
public CatcherAnimationState CurrentState { get; private set; }
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X);
protected bool Dashing
{
get => dashing;
set
{
if (value == dashing) return;
dashing = value;
Trail |= dashing;
}
}
/// <summary>
/// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
/// </summary>
protected bool Trail
{
get => trail;
set
{
if (value == trail) return;
trail = value;
if (Trail)
beginTrail();
}
}
private Container<DrawableHitObject> 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<DrawableHitObject>
{
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();
}
/// <summary>
/// Add a caught fruit to the catcher's stack.
/// </summary>
/// <param name="fruit">The fruit that was caught.</param>
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)
});
}
/// <summary>
/// Let the catcher attempt to catch a fruit.
/// </summary>
/// <param name="fruit">The fruit to catch.</param>
/// <returns>Whether the catch is possible.</returns>
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;
}
/// <summary>
/// Set hyper-dash state.
/// </summary>
/// <param name="modifier">The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.</param>
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
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;
}
/// <summary>
/// Drop any fruit off the plate.
/// </summary>
public void Drop()
{
foreach (var f in caughtFruit.ToArray())
Drop(f);
}
/// <summary>
/// Explode any fruit off the plate.
/// </summary>
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<DrawableHitObject> 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();
}
}
}
}

View File

@ -2,15 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; 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.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects; 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.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
@ -28,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public const float CATCHER_SIZE = 106.75f; public const float CATCHER_SIZE = 106.75f;
protected internal readonly Catcher MovableCatcher;
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation; public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
public Container ExplodingFruitTarget public Container ExplodingFruitTarget
@ -37,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI
set => MovableCatcher.ExplodingFruitTarget = value; set => MovableCatcher.ExplodingFruitTarget = value;
} }
private DrawableCatchHitObject lastPlateableFruit;
public CatcherArea(BeatmapDifficulty difficulty = null) public CatcherArea(BeatmapDifficulty difficulty = null)
{ {
RelativeSizeAxes = Axes.X; 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) 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() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
@ -110,559 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.X = state.CatcherX.Value; MovableCatcher.X = state.CatcherX.Value;
} }
public void OnReleased(CatchAction action) protected internal readonly Catcher MovableCatcher;
{
}
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<CatchAction>
{
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X);
private Container<DrawableHitObject> 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<DrawableHitObject>
{
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;
/// <summary>
/// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
/// </summary>
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);
/// <summary>
/// Add a caught fruit to the catcher's stack.
/// </summary>
/// <param name="fruit">The fruit that was caught.</param>
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)
});
}
/// <summary>
/// Let the catcher attempt to catch a fruit.
/// </summary>
/// <param name="fruit">The fruit to catch.</param>
/// <returns>Whether the catch is possible.</returns>
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;
/// <summary>
/// Whether we are hyper-dashing or not.
/// </summary>
public bool HyperDashing => hyperDashModifier != 1;
/// <summary>
/// Set hyper-dash state.
/// </summary>
/// <param name="modifier">The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.</param>
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
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;
}
}
/// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// </summary>
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;
}
/// <summary>
/// Drop any fruit off the plate.
/// </summary>
public void Drop()
{
foreach (var f in caughtFruit.ToArray())
Drop(f);
}
/// <summary>
/// Explode any fruit off the plate.
/// </summary>
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<DrawableHitObject> 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);
}
} }
} }

View File

@ -0,0 +1,122 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplySkin(skin, allowFallback); base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(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) protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -16,16 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{ {
public Func<OsuAction?> GetInitialHitAction; public Func<OsuAction?> GetInitialHitAction;
public Color4 AccentColour
{
get => ball.Colour;
set => ball.Colour = value;
}
private readonly Slider slider; private readonly Slider slider;
private readonly Drawable followCircle; private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider; private readonly DrawableSlider drawableSlider;
private readonly CircularContainer ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null) public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{ {
@ -47,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0, Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
}, },
new CircularContainer ball = new CircularContainer
{ {
Masking = true, Masking = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Chat; 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 {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount);
AddAssert($"msg #{index} has the right action", hasExpectedActions); 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); AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks);
bool hasExpectedActions() bool hasExpectedActions()
@ -97,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online
return true; return true;
} }
bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font.Italics); //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font.Italics);
bool isShowingLinks() bool isShowingLinks()
{ {

View File

@ -38,8 +38,13 @@ namespace osu.Game.Tests.Visual.Online
private TestChatOverlay chatOverlay; private TestChatOverlay chatOverlay;
private ChannelManager channelManager; private ChannelManager channelManager;
private IEnumerable<Channel> visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+");
private IEnumerable<Channel> joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+");
private readonly List<Channel> channels; private readonly List<Channel> 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 channel1 => channels[0];
private Channel channel2 => channels[1]; private Channel channel2 => channels[1];
@ -91,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[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); 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("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[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("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); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
} }
@ -140,10 +145,67 @@ namespace osu.Game.Tests.Visual.Online
var targetNumberKey = oneBasedIndex % 10; var targetNumberKey = oneBasedIndex % 10;
var targetChannel = channels[zeroBasedIndex]; var targetChannel = channels[zeroBasedIndex];
AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); 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) private void pressChannelHotkey(int number)
{ {
var channelKey = Key.Number0 + number; var channelKey = Key.Number0 + number;
@ -187,6 +249,8 @@ namespace osu.Game.Tests.Visual.Online
{ {
public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value;
public new ChannelTabControl ChannelTabControl => base.ChannelTabControl;
public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay;
protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl();
@ -196,12 +260,22 @@ namespace osu.Game.Tests.Visual.Online
private class TestTabControl : ChannelTabControl private class TestTabControl : ChannelTabControl
{ {
protected override TabItem<Channel> CreateTabItem(Channel value) => new TestChannelTabItem(value); protected override TabItem<Channel> CreateTabItem(Channel value)
{
switch (value.Type)
{
case ChannelType.PM:
return new TestPrivateChannelTabItem(value);
default:
return new TestChannelTabItem(value);
}
}
public new IReadOnlyDictionary<Channel, TabItem<Channel>> TabMap => base.TabMap; public new IReadOnlyDictionary<Channel, TabItem<Channel>> TabMap => base.TabMap;
} }
private class TestChannelTabItem : PrivateChannelTabItem private class TestChannelTabItem : ChannelTabItem
{ {
public TestChannelTabItem(Channel channel) public TestChannelTabItem(Channel channel)
: base(channel) : base(channel)
@ -210,5 +284,15 @@ namespace osu.Game.Tests.Visual.Online
public new ClickableContainer CloseButton => base.CloseButton; public new ClickableContainer CloseButton => base.CloseButton;
} }
private class TestPrivateChannelTabItem : PrivateChannelTabItem
{
public TestPrivateChannelTabItem(Channel channel)
: base(channel)
{
}
public new ClickableContainer CloseButton => base.CloseButton;
}
} }
} }

View File

@ -497,7 +497,7 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
bool changed = false; bool changed = false;
AddStep($"Load {beatmapSets.Count} Beatmaps", () => AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () =>
{ {
carousel.Filter(new FilterCriteria()); carousel.Filter(new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSetsChanged = () => changed = true;
@ -697,6 +697,8 @@ namespace osu.Game.Tests.Visual.SongSelect
public new List<DrawableCarouselItem> Items => base.Items; public new List<DrawableCarouselItem> Items => base.Items;
public bool PendingFilterTask => PendingFilter != null; public bool PendingFilterTask => PendingFilter != null;
protected override IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => Enumerable.Empty<BeatmapSetInfo>();
} }
} }
} }

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Type> 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 },
}
};
}
}
}

View File

@ -22,9 +22,9 @@ namespace osu.Game.Tournament.Components
public ControlPanel() public ControlPanel()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Y;
AlwaysPresent = true; AlwaysPresent = true;
Width = 0.15f; Width = TournamentSceneManager.CONTROL_AREA_WIDTH;
Anchor = Anchor.TopRight; Anchor = Anchor.TopRight;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Components
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Width = 0.75f,
Position = new Vector2(0, 35f), Position = new Vector2(0, 35f),
Padding = new MarginPadding(5),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5f), Spacing = new Vector2(0, 5f),
}, },

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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");
}
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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");
}
}
}
}

View File

@ -1,16 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
new DrawableTournamentTitleText(), new DrawableTournamentHeaderText(),
new TournamentSpriteText new TournamentSpriteText
{ {
Text = match.Round.Value?.Name.Value ?? "Unknown Round", Text = match.Round.Value?.Name.Value ?? "Unknown Round",

View File

@ -16,7 +16,6 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tournament.Components namespace osu.Game.Tournament.Components
@ -125,13 +124,21 @@ namespace osu.Game.Tournament.Components
if (!string.IsNullOrEmpty(mods)) if (!string.IsNullOrEmpty(mods))
{ {
AddInternal(new Sprite AddInternal(new Container
{ {
Texture = textures.Get($"mods/{mods}"), RelativeSizeAxes = Axes.Y,
Width = 60,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Margin = new MarginPadding(10), 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}"),
}
}); });
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video; using osu.Framework.Graphics.Video;
using osu.Framework.Platform;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -28,13 +27,13 @@ namespace osu.Game.Tournament.Components
} }
[BackgroundDependencyLoader] [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) if (stream != null)
{ {
InternalChild = video = new VideoSprite(stream) InternalChild = video = new VideoSprite(stream, false)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,

View File

@ -163,12 +163,7 @@ namespace osu.Game.Tournament.IPC
{ {
try try
{ {
stableInstallPath = "G:\\My Drive\\Main\\osu!tourney"; stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH");
if (checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = "G:\\My Drive\\Main\\osu!mappool";
if (checkExists(stableInstallPath)) if (checkExists(stableInstallPath))
return stableInstallPath; return stableInstallPath;

View File

@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tournament.Screens.Gameplay.Components namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
@ -17,13 +14,39 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
private TeamScoreDisplay teamDisplay1; private TeamScoreDisplay teamDisplay1;
private TeamScoreDisplay teamDisplay2; private TeamScoreDisplay teamDisplay2;
private DrawableTournamentHeaderLogo logo;
private bool showScores = true;
public bool ShowScores public bool ShowScores
{ {
get => showScores;
set set
{ {
teamDisplay1.ShowScore = value; if (value == showScores)
teamDisplay2.ShowScore = value; 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, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Padding = new MarginPadding(20),
Spacing = new Vector2(5), Spacing = new Vector2(5),
Children = new Drawable[] Children = new Drawable[]
{ {
new DrawableTournamentTitleText logo = new DrawableTournamentHeaderLogo
{ {
Anchor = Anchor.Centre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.TopCentre,
Scale = new Vector2(1.2f) Alpha = showLogo ? 1 : 0
}, },
new RoundDisplay new DrawableTournamentHeaderText
{ {
Anchor = Anchor.Centre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.TopCentre,
},
new MatchRoundDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(0.4f) Scale = new Vector2(0.4f)
}, },
} }
@ -66,76 +95,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
}, },
}; };
}
}
public class TeamScoreDisplay : CompositeDrawable updateDisplay();
{
private readonly TeamColour teamColour;
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private readonly Bindable<TournamentTeam> currentTeam = new Bindable<TournamentTeam>();
private readonly Bindable<int?> currentTeamScore = new Bindable<int?>();
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 updateDisplay()
private void load(LadderInfo ladder)
{ {
currentMatch.BindTo(ladder.CurrentMatch); teamDisplay1.ShowScore = showScores;
currentMatch.BindValueChanged(matchChanged, true); teamDisplay2.ShowScore = showScores;
}
private void matchChanged(ValueChangedEvent<TournamentMatch> match) logo.Alpha = showLogo ? 1 : 0;
{
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),
};
} }
} }
} }

View File

@ -8,7 +8,7 @@ using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Gameplay.Components namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
public class RoundDisplay : TournamentSpriteTextWithBackground public class MatchRoundDisplay : TournamentSpriteTextWithBackground
{ {
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>(); private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components 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 }; Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
Winning = false; Winning = false;
DisplayedCountSpriteText.Spacing = new Vector2(-6);
} }
public bool Winning public bool Winning
{ {
set => DisplayedCountSpriteText.Font = value set => DisplayedCountSpriteText.Font = value
? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
} }
} }
} }

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private readonly Bindable<TournamentTeam> currentTeam = new Bindable<TournamentTeam>();
private readonly Bindable<int?> currentTeamScore = new Bindable<int?>();
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<TournamentMatch> 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),
};
}
}
}

View File

@ -47,7 +47,10 @@ namespace osu.Game.Tournament.Screens.Gameplay
Loop = true, Loop = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
header = new MatchHeader(), header = new MatchHeader
{
ShowLogo = false
},
new Container new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tournament.Screens.Ladder
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Loop = true, Loop = true,
}, },
new DrawableTournamentTitleText new DrawableTournamentHeaderText
{ {
Y = 100, Y = 100,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool
new MatchHeader(), new MatchHeader(),
mapFlows = new FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows = new FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>>
{ {
Y = 100, Y = 140,
Spacing = new Vector2(10, 10), Spacing = new Vector2(10, 10),
Padding = new MarginPadding(25), Padding = new MarginPadding(25),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -235,6 +235,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Height = 42,
}); });
} }
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Schedule
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
new DrawableTournamentTitleText(), new DrawableTournamentHeaderText(),
new Container new Container
{ {
Margin = new MarginPadding { Top = 40 }, Margin = new MarginPadding { Top = 40 },

View File

@ -3,7 +3,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -22,6 +25,7 @@ namespace osu.Game.Tournament.Screens
private FillFlowContainer fillFlow; private FillFlowContainer fillFlow;
private LoginOverlay loginOverlay; private LoginOverlay loginOverlay;
private ActionableInfo resolution;
[Resolved] [Resolved]
private MatchIPCInfo ipc { get; set; } private MatchIPCInfo ipc { get; set; }
@ -32,9 +36,13 @@ namespace osu.Game.Tournament.Screens
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
private Bindable<Size> windowSize;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(FrameworkConfigManager frameworkConfig)
{ {
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
InternalChild = fillFlow = new FillFlowContainer InternalChild = fillFlow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -48,6 +56,9 @@ namespace osu.Game.Tournament.Screens
reload(); reload();
} }
[Resolved]
private Framework.Game game { get; set; }
private void reload() private void reload()
{ {
var fileBasedIpc = ipc as FileBasedIPC; var fileBasedIpc = ipc as FileBasedIPC;
@ -97,9 +108,25 @@ namespace osu.Game.Tournament.Screens
Items = rulesets.AvailableRulesets, Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset, 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<T> : LabelledComponent<OsuDropdown<T>, T> public class LabelledDropdown<T> : LabelledComponent<OsuDropdown<T>, T>
{ {
public LabelledDropdown() public LabelledDropdown()

View File

@ -37,6 +37,8 @@ namespace osu.Game.Tournament
private Storage storage; private Storage storage;
private TournamentStorage tournamentStorage;
private DependencyContainer dependencies; private DependencyContainer dependencies;
private Bindable<Size> windowSize; private Bindable<Size> windowSize;
@ -54,14 +56,16 @@ namespace osu.Game.Tournament
{ {
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
Textures.AddStore(new TextureLoaderStore(new ResourceStore<byte[]>(new StorageBackedResourceStore(storage)))); dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage));
Textures.AddStore(new TextureLoaderStore(tournamentStorage));
this.storage = storage; this.storage = storage;
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize); windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() => 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; heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true); }), true);

View File

@ -33,6 +33,12 @@ namespace osu.Game.Tournament
private Container screens; private Container screens;
private TourneyVideo video; 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] [Cached]
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
@ -51,13 +57,13 @@ namespace osu.Game.Tournament
{ {
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Y,
X = 200, X = CONTROL_AREA_WIDTH,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
FillAspectRatio = 16 / 9f, FillAspectRatio = 16 / 9f,
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
Size = new Vector2(0.8f, 1), Width = STREAM_AREA_WIDTH,
//Masking = true, //Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -96,7 +102,7 @@ namespace osu.Game.Tournament
new Container new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 200, Width = CONTROL_AREA_WIDTH,
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -108,8 +114,8 @@ namespace osu.Game.Tournament
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(2), Spacing = new Vector2(5),
Padding = new MarginPadding(2), Padding = new MarginPadding(5),
Children = new Drawable[] Children = new Drawable[]
{ {
new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen }, new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen },

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<byte[]>
{
public TournamentStorage(Storage storage)
: base(new StorageBackedResourceStore(storage), "tournament")
{
AddExtension("m4v");
AddExtension("avi");
AddExtension("mp4");
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Formats namespace osu.Game.Beatmaps.Formats
{ {
@ -26,17 +25,5 @@ namespace osu.Game.Beatmaps.Formats
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last()))); AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last())));
SetFallbackDecoder<Beatmap>(() => new LegacyDifficultyCalculatorBeatmapDecoder()); SetFallbackDecoder<Beatmap>(() => new LegacyDifficultyCalculatorBeatmapDecoder());
} }
protected override TimingControlPoint CreateTimingControlPoint()
=> new LegacyDifficultyCalculatorTimingControlPoint();
private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint
{
public LegacyDifficultyCalculatorTimingControlPoint()
{
BeatLengthBindable.MinValue = double.MinValue;
BeatLengthBindable.MaxValue = double.MaxValue;
}
}
} }
} }

View File

@ -30,8 +30,15 @@ namespace osu.Game.Graphics
/// <param name="italics">Whether the font is italic.</param> /// <param name="italics">Whether the font is italic.</param>
/// <param name="fixedWidth">Whether all characters should be spaced the same distance apart.</param> /// <param name="fixedWidth">Whether all characters should be spaced the same distance apart.</param>
/// <returns>The <see cref="FontUsage"/>.</returns> /// <returns>The <see cref="FontUsage"/>.</returns>
public static FontUsage GetFont(Typeface typeface = Typeface.Exo, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) 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), italics, fixedWidth); => 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;
}
/// <summary> /// <summary>
/// Retrieves the string representation of a <see cref="Typeface"/>. /// Retrieves the string representation of a <see cref="Typeface"/>.
@ -42,9 +49,6 @@ namespace osu.Game.Graphics
{ {
switch (typeface) switch (typeface)
{ {
case Typeface.Exo:
return "Exo2.0";
case Typeface.Venera: case Typeface.Venera:
return "Venera"; return "Venera";
@ -62,7 +66,13 @@ namespace osu.Game.Graphics
/// <param name="weight">The <see cref="FontWeight"/>.</param> /// <param name="weight">The <see cref="FontWeight"/>.</param>
/// <returns>The string representation of <paramref name="weight"/> in the specified <paramref name="typeface"/>.</returns> /// <returns>The string representation of <paramref name="weight"/> in the specified <paramref name="typeface"/>.</returns>
public static string GetWeightString(Typeface typeface, FontWeight weight) 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);
}
/// <summary> /// <summary>
/// Retrieves the string representation of a <see cref="FontWeight"/>. /// Retrieves the string representation of a <see cref="FontWeight"/>.
@ -96,7 +106,6 @@ namespace osu.Game.Graphics
public enum Typeface public enum Typeface
{ {
Exo,
Venera, Venera,
Torus Torus
} }

View File

@ -173,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface
new HoverClickSounds() 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(); protected override void OnActivated() => fadeActive();

View File

@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface
new HoverClickSounds() 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(); protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();

View File

@ -12,5 +12,6 @@ namespace osu.Game.Online.Chat
Temporary, Temporary,
PM, PM,
Group, Group,
System,
} }
} }

View File

@ -138,30 +138,17 @@ namespace osu.Game
dependencies.Cache(LocalConfig); dependencies.Cache(LocalConfig);
AddFont(Resources, @"Fonts/osuFont"); 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-Basic");
AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto-CJK-Basic"); AddFont(Resources, @"Fonts/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); 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-Light");
AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Bold");
AddFont(Resources, @"Fonts/Venera-Black"); AddFont(Resources, @"Fonts/Venera-Black");

View File

@ -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!"); 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("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 "); passwordDescription.AddText("At least ");
characterCheckText = passwordDescription.AddText("8 characters long"); characterCheckText = passwordDescription.AddText("8 characters long");

View File

@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Changelog
Direction = FillDirection.Full, Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.BottomLeft,
} }
} }
}; };
@ -125,7 +126,7 @@ namespace osu.Game.Overlays.Changelog
title.AddText("by ", t => title.AddText("by ", t =>
{ {
t.Font = fontMedium.With(italics: true); t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;
t.Padding = new MarginPadding { Left = 10 }; t.Padding = new MarginPadding { Left = 10 };
}); });
@ -138,7 +139,7 @@ namespace osu.Game.Overlays.Changelog
Id = entry.GithubUser.UserId.Value Id = entry.GithubUser.UserId.Value
}, t => }, t =>
{ {
t.Font = fontMedium.With(italics: true); t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;
}); });
} }
@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Changelog
{ {
title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{ {
t.Font = fontMedium.With(italics: true); t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;
}); });
} }
@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Changelog
{ {
title.AddText(entry.GithubUser.DisplayName, t => title.AddText(entry.GithubUser.DisplayName, t =>
{ {
t.Font = fontMedium.With(italics: true); t.Font = fontMedium;
t.Colour = entryColour; t.Colour = entryColour;
}); });
} }

View File

@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs
public ChannelSelectorTabChannel() public ChannelSelectorTabChannel()
{ {
Name = "+"; Name = "+";
Type = ChannelType.System;
} }
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Chat.Tabs
// performTabSort might've made selectorTab's position wonky, fix it // performTabSort might've made selectorTab's position wonky, fix it
TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
((ChannelTabItem)item).OnRequestClose += tabCloseRequested; ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value);
base.AddTabItem(item, addToDropdown); base.AddTabItem(item, addToDropdown);
} }
@ -74,18 +74,24 @@ namespace osu.Game.Overlays.Chat.Tabs
/// <summary> /// <summary>
/// Removes a channel from the ChannelTabControl. /// 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.
/// </summary> /// </summary>
/// <param name="channel">The channel that is going to be removed.</param> /// <param name="channel">The channel that is going to be removed.</param>
public void RemoveChannel(Channel channel) public void RemoveChannel(Channel channel)
{ {
RemoveItem(channel);
if (Current.Value == channel) if (Current.Value == channel)
{ {
// Prefer non-selector channels first var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList();
Current.Value = Items.FirstOrDefault(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)) ?? Items.FirstOrDefault(); 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<Channel> tab) protected override void SelectTab(TabItem<Channel> tab)
@ -100,21 +106,6 @@ namespace osu.Game.Overlays.Chat.Tabs
selectorTab.Active.Value = false; selectorTab.Active.Value = false;
} }
private void tabCloseRequested(TabItem<Channel> 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 protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer
{ {
Direction = FillDirection.Full, Direction = FillDirection.Full,

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.News
Left = 25, Left = 25,
Bottom = 50, Bottom = 50,
}, },
Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold),
Text = info.Title, Text = info.Title,
}, },
new OsuSpriteText new OsuSpriteText
@ -87,7 +87,7 @@ namespace osu.Game.Overlays.News
Left = 25, Left = 25,
Bottom = 30, Bottom = 30,
}, },
Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold),
Text = "by " + info.Author Text = "by " + info.Author
} }
}; };
@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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(), Text = date.ToString("d MMM yyy").ToUpper(),
Margin = new MarginPadding Margin = new MarginPadding
{ {

View File

@ -14,22 +14,8 @@ namespace osu.Game.Overlays.Volume
public Func<GlobalAction, bool> ActionRequested; public Func<GlobalAction, bool> ActionRequested;
public Func<GlobalAction, float, bool, bool> ScrollActionRequested; public Func<GlobalAction, float, bool, bool> ScrollActionRequested;
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action) =>
{ ActionRequested?.Invoke(action) ?? false;
// 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 OnScroll(GlobalAction action, float amount, bool isPrecise) => public bool OnScroll(GlobalAction action, float amount, bool isPrecise) =>
ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false;

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (split.Length > 7) 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) if (length == 0)
length = null; length = null;
} }

View File

@ -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("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.Exo, 30, FontWeight.SemiBold)); textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold));
textFlow.NewParagraph(); textFlow.NewParagraph();
static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); 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();
textFlow.NewParagraph(); textFlow.NewParagraph();

View File

@ -259,11 +259,18 @@ namespace osu.Game.Screens.Menu
private class LazerLogo : CompositeDrawable private class LazerLogo : CompositeDrawable
{ {
private readonly Stream videoStream;
public LazerLogo(Stream videoStream) public LazerLogo(Stream videoStream)
{ {
this.videoStream = videoStream;
Size = new Vector2(960); Size = new Vector2(960);
}
InternalChild = new VideoSprite(videoStream) [BackgroundDependencyLoader]
private void load()
{
InternalChild = new VideoSprite(videoStream, false)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 }

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
if (host.NewValue != null) if (host.NewValue != null)
{ {
hostText.AddText("hosted by "); 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 }; flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both };
} }

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Ranking.Pages
rankText.AddText($"#{index + 1} ", s => 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; s.Colour = colours.YellowDark;
}); });

View File

@ -153,9 +153,11 @@ namespace osu.Game.Screens.Select
beatmaps.BeatmapHidden += beatmapHidden; beatmaps.BeatmapHidden += beatmapHidden;
beatmaps.BeatmapRestored += beatmapRestored; beatmaps.BeatmapRestored += beatmapRestored;
loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); loadBeatmapSets(GetLoadableBeatmaps());
} }
protected virtual IEnumerable<BeatmapSetInfo> GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable();
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{ {
var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID);

View File

@ -61,7 +61,7 @@ namespace osu.Game.Skinning
{ {
var iniRate = source.GetConfig<GlobalSkinConfiguration, int>(GlobalSkinConfiguration.AnimationFramerate); var iniRate = source.GetConfig<GlobalSkinConfiguration, int>(GlobalSkinConfiguration.AnimationFramerate);
if (iniRate != null) if (iniRate?.Value > 0)
return 1000f / iniRate.Value; return 1000f / iniRate.Value;
return 1000f / textures.Length; return 1000f / textures.Length;

View File

@ -22,8 +22,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.310.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.312.0" />
<PackageReference Include="Sentry" Version="2.1.0" /> <PackageReference Include="Sentry" Version="2.1.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.310.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.312.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
@ -79,7 +79,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.310.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.312.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />