mirror of
https://github.com/ppy/osu.git
synced 2025-02-15 23:42:55 +08:00
Merge branch 'master' into match-subscreen-redesign
This commit is contained in:
commit
56a8b0d3f5
@ -2,7 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/.idea.osu.Desktop.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/.idea.osu.Desktop.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
|
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.622.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.623.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.704.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -9,7 +9,7 @@ using osu.Framework.Android;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)]
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
||||
public class OsuGameActivity : AndroidGameActivity
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuGameAndroid();
|
||||
|
@ -8,7 +8,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
public float Position
|
||||
{
|
||||
get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
|
||||
get => HitObject?.X ?? position;
|
||||
set => position = value;
|
||||
}
|
||||
|
||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
float width = (i % 10 + 1) / 20f;
|
||||
float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH;
|
||||
|
||||
beatmap.HitObjects.Add(new JuiceStream
|
||||
{
|
||||
X = 0.5f - width / 2,
|
||||
X = CatchPlayfield.CENTER_X - width / 2,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
|
||||
new Vector2(width, 0)
|
||||
}),
|
||||
StartTime = i * 2000,
|
||||
NewCombo = i % 8 == 0
|
||||
|
@ -4,6 +4,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
@ -22,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
};
|
||||
|
||||
for (int i = 0; i < 512; i++)
|
||||
beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 });
|
||||
{
|
||||
beatmap.HitObjects.Add(new Fruit
|
||||
{
|
||||
X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH,
|
||||
StartTime = i * 100,
|
||||
NewCombo = i % 8 == 0
|
||||
});
|
||||
}
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
CreateDrawableRepresentation = ((DrawableRuleset<CatchHitObject>)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
|
||||
},
|
||||
});
|
||||
|
@ -158,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private float getXCoords(bool hit)
|
||||
{
|
||||
const float x_offset = 0.2f;
|
||||
float xCoords = drawableRuleset.Playfield.Width / 2;
|
||||
const float x_offset = 0.2f * CatchPlayfield.WIDTH;
|
||||
float xCoords = CatchPlayfield.CENTER_X;
|
||||
|
||||
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
|
||||
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
|
||||
|
@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
};
|
||||
|
||||
// Should produce a hyper-dash (edge case test)
|
||||
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true });
|
||||
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true });
|
||||
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
|
||||
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
|
||||
|
||||
double startTime = 3000;
|
||||
|
||||
const float left_x = 0.02f;
|
||||
const float right_x = 0.98f;
|
||||
const float left_x = 0.02f * CatchPlayfield.WIDTH;
|
||||
const float right_x = 0.98f * CatchPlayfield.WIDTH;
|
||||
|
||||
createObjects(() => new Fruit { X = left_x });
|
||||
createObjects(() => new TestJuiceStream(right_x), 1);
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
new JuiceStream
|
||||
{
|
||||
X = 0.5f,
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
},
|
||||
new Banana
|
||||
{
|
||||
X = 0.5f,
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 1000,
|
||||
NewCombo = true
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
Path = curveData.Path,
|
||||
NodeSamples = curveData.NodeSamples,
|
||||
RepeatCount = curveData.RepeatCount,
|
||||
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
|
||||
X = positionData?.X ?? 0,
|
||||
NewCombo = comboData?.NewCombo ?? false,
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
|
||||
@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
Samples = obj.Samples,
|
||||
NewCombo = comboData?.NewCombo ?? false,
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
|
||||
X = positionData?.X ?? 0
|
||||
}.Yield();
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
case BananaShower bananaShower:
|
||||
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
|
||||
{
|
||||
banana.XOffset = (float)rng.NextDouble();
|
||||
banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
|
||||
rng.Next(); // osu!stable retrieved a random banana type
|
||||
rng.Next(); // osu!stable retrieved a random banana rotation
|
||||
rng.Next(); // osu!stable retrieved a random banana colour
|
||||
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
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;
|
||||
lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X;
|
||||
|
||||
// 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;
|
||||
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
catchObject.XOffset = 0;
|
||||
|
||||
if (catchObject is TinyDroplet)
|
||||
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
|
||||
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X);
|
||||
else if (catchObject is Droplet)
|
||||
rng.Next(); // osu!stable retrieved a random droplet rotation
|
||||
}
|
||||
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3)
|
||||
if (Math.Abs(positionDiff) < timeDiff / 3)
|
||||
applyOffset(ref offsetPosition, positionDiff);
|
||||
|
||||
hitObject.XOffset = offsetPosition - hitObject.X;
|
||||
@ -149,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
|
||||
{
|
||||
bool right = rng.NextBool();
|
||||
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
|
||||
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
|
||||
|
||||
if (right)
|
||||
{
|
||||
// Clamp to the right bound
|
||||
if (position + rand <= 1)
|
||||
if (position + rand <= CatchPlayfield.WIDTH)
|
||||
position += rand;
|
||||
else
|
||||
position -= rand;
|
||||
@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
|
||||
double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2;
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
|
||||
int lastDirection = 0;
|
||||
double lastExcess = halfCatcherWidth;
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@ -33,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
|
||||
NormalizedPosition = BaseObject.X * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.X * scalingFactor;
|
||||
|
||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||
StrainTime = Math.Max(40, DeltaTime);
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
|
||||
@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH)
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
private float x;
|
||||
|
||||
/// <summary>
|
||||
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
|
||||
/// </summary>
|
||||
public float X
|
||||
{
|
||||
get => x + XOffset;
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -70,12 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X;
|
||||
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
|
||||
|
||||
protected DrawableCatchHitObject(CatchHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
X = hitObject.X;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -80,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
StartTime = t + lastEvent.Value.Time,
|
||||
X = X + Path.PositionAt(
|
||||
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
|
||||
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Samples = dropletSamples,
|
||||
StartTime = e.Time,
|
||||
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
|
||||
X = X + Path.PositionAt(e.PathProgress).X,
|
||||
});
|
||||
break;
|
||||
|
||||
@ -108,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Samples = Samples,
|
||||
StartTime = e.Time,
|
||||
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
|
||||
X = X + Path.PositionAt(e.PathProgress).X,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
|
||||
public float EndX => X + this.CurvePositionAt(1).X;
|
||||
|
||||
public double Duration
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
// todo: add support for HT DT
|
||||
const double dash_speed = Catcher.BASE_SPEED;
|
||||
const double movement_speed = dash_speed / 2;
|
||||
float lastPosition = 0.5f;
|
||||
float lastPosition = CatchPlayfield.CENTER_X;
|
||||
double lastTime = 0;
|
||||
|
||||
void moveToNext(CatchHitObject h)
|
||||
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
bool impossibleJump = speedRequired > movement_speed * 2;
|
||||
|
||||
// todo: get correct catcher size, based on difficulty CS.
|
||||
const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f;
|
||||
const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
|
||||
|
||||
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
|
||||
{
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
|
||||
@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
|
||||
{
|
||||
Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
|
||||
Position = currentFrame.Position.X;
|
||||
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
|
||||
|
||||
if (Dashing)
|
||||
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
|
||||
|
||||
return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state);
|
||||
return new LegacyReplayFrame(Time, Position, null, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatchPlayfield : ScrollingPlayfield
|
||||
{
|
||||
public const float BASE_WIDTH = 512;
|
||||
/// <summary>
|
||||
/// The width of the playfield.
|
||||
/// The horizontal movement of the catcher is confined in the area of this width.
|
||||
/// </summary>
|
||||
public const float WIDTH = 512;
|
||||
|
||||
/// <summary>
|
||||
/// The center position of the playfield.
|
||||
/// </summary>
|
||||
public const float CENTER_X = WIDTH / 2;
|
||||
|
||||
internal readonly CatcherArea CatcherArea;
|
||||
|
||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
|
||||
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
|
||||
Size = Vector2.Divide(Vector2.One, Scale);
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <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 const double BASE_SPEED = 1.0;
|
||||
|
||||
public Container ExplodingFruitTarget;
|
||||
|
||||
@ -104,9 +104,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
this.trailsTarget = trailsTarget;
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
X = 0.5f;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
||||
@ -209,8 +206,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
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 catchObjectPosition = fruit.X;
|
||||
var catcherPosition = Position.X;
|
||||
|
||||
var validCatch =
|
||||
catchObjectPosition >= catcherPosition - halfCatchWidth &&
|
||||
@ -224,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
var target = fruit.HyperDashTarget;
|
||||
var timeDifference = target.StartTime - fruit.StartTime;
|
||||
double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
|
||||
double positionDifference = target.X - catcherPosition;
|
||||
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
|
||||
SetHyperDashState(Math.Abs(velocity), target.X);
|
||||
@ -331,7 +328,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public void UpdatePosition(float position)
|
||||
{
|
||||
position = Math.Clamp(position, 0, 1);
|
||||
position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
|
||||
|
||||
if (position == X)
|
||||
return;
|
||||
|
@ -31,14 +31,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = CATCHER_SIZE;
|
||||
Child = MovableCatcher = new Catcher(this, difficulty);
|
||||
}
|
||||
|
||||
public static float GetCatcherSize(BeatmapDifficulty difficulty)
|
||||
{
|
||||
return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
||||
Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
|
||||
}
|
||||
|
||||
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
[TestCase("convert-samples")]
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("slider-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
@ -29,13 +30,16 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
StartTime = hitObject.StartTime,
|
||||
EndTime = hitObject.GetEndTime(),
|
||||
Column = ((ManiaHitObject)hitObject).Column,
|
||||
NodeSamples = getSampleNames((hitObject as HoldNote)?.NodeSamples)
|
||||
Samples = getSampleNames(hitObject.Samples),
|
||||
NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
|
||||
};
|
||||
}
|
||||
|
||||
private IList<IList<string>> getSampleNames(List<IList<HitSampleInfo>> hitSampleInfo)
|
||||
=> hitSampleInfo?.Select(samples =>
|
||||
(IList<string>)samples.Select(sample => sample.LookupNames.First()).ToList())
|
||||
private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
|
||||
=> hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
|
||||
|
||||
private IList<IList<string>> getNodeSampleNames(List<IList<HitSampleInfo>> hitSampleInfo)
|
||||
=> hitSampleInfo?.Select(getSampleNames)
|
||||
.ToList();
|
||||
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
@ -51,14 +55,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
public double StartTime;
|
||||
public double EndTime;
|
||||
public int Column;
|
||||
public IList<string> Samples;
|
||||
public IList<IList<string>> NodeSamples;
|
||||
|
||||
public bool Equals(SampleConvertValue other)
|
||||
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
||||
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
|
||||
&& samplesEqual(NodeSamples, other.NodeSamples);
|
||||
&& samplesEqual(Samples, other.Samples)
|
||||
&& nodeSamplesEqual(NodeSamples, other.NodeSamples);
|
||||
|
||||
private static bool samplesEqual(ICollection<IList<string>> firstSampleList, ICollection<IList<string>> secondSampleList)
|
||||
private static bool samplesEqual(ICollection<string> firstSampleList, ICollection<string> secondSampleList)
|
||||
=> firstSampleList.SequenceEqual(secondSampleList);
|
||||
|
||||
private static bool nodeSamplesEqual(ICollection<IList<string>> firstSampleList, ICollection<IList<string>> secondSampleList)
|
||||
{
|
||||
if (firstSampleList == null && secondSampleList == null)
|
||||
return true;
|
||||
|
@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 3
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 3
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,1,0:0:0:0:
|
Before Width: | Height: | Size: 165 B After Width: | Height: | Size: 165 B |
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 899 B |
@ -9,4 +9,6 @@ Hit50: mania/hit50
|
||||
Hit100: mania/hit100
|
||||
Hit200: mania/hit200
|
||||
Hit300: mania/hit300
|
||||
Hit300g: mania/hit300g
|
||||
Hit300g: mania/hit300g
|
||||
StageLeft: mania/stage-left
|
||||
StageRight: mania/stage-right
|
@ -0,0 +1,49 @@
|
||||
// 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.Reflection;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
protected override IResourceStore<byte[]> Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestManiaHitObjectNormalSampleBank()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal2";
|
||||
|
||||
SetupSkins(expected_sample, expected_sample);
|
||||
|
||||
CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when a custom sample bank is used, layered hitsounds are not played
|
||||
/// (only the sample from the custom bank is looked up).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestManiaHitObjectCustomSampleBank()
|
||||
{
|
||||
const string expected_sample = "normal-hitwhistle2";
|
||||
const string unwanted_sample = "normal-hitnormal2";
|
||||
|
||||
SetupSkins(expected_sample, unwanted_sample);
|
||||
|
||||
CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
}
|
||||
}
|
@ -483,9 +483,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||
return null;
|
||||
|
||||
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
|
||||
|
||||
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
|
||||
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
|
||||
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
|
||||
|
||||
// avoid slicing the list & creating copies, if at all possible.
|
||||
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
||||
|
@ -30,6 +30,7 @@ using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
@ -309,6 +310,21 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
|
||||
}
|
||||
|
||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||
{
|
||||
new StatisticRow
|
||||
{
|
||||
Columns = new[]
|
||||
{
|
||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -9,7 +9,8 @@
|
||||
["normal-hitnormal"],
|
||||
["soft-hitnormal"],
|
||||
["drum-hitnormal"]
|
||||
]
|
||||
],
|
||||
"Samples": ["drum-hitnormal"]
|
||||
}, {
|
||||
"StartTime": 1875.0,
|
||||
"EndTime": 2750.0,
|
||||
@ -17,14 +18,16 @@
|
||||
"NodeSamples": [
|
||||
["soft-hitnormal"],
|
||||
["drum-hitnormal"]
|
||||
]
|
||||
],
|
||||
"Samples": ["drum-hitnormal"]
|
||||
}]
|
||||
}, {
|
||||
"StartTime": 3750.0,
|
||||
"Objects": [{
|
||||
"StartTime": 3750.0,
|
||||
"EndTime": 3750.0,
|
||||
"Column": 3
|
||||
"Column": 3,
|
||||
"Samples": ["normal-hitnormal"]
|
||||
}]
|
||||
}]
|
||||
}
|
@ -13,4 +13,4 @@ SliderTickRate:1
|
||||
|
||||
[HitObjects]
|
||||
88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0:
|
||||
259,118,3750,1,0,0:0:0:0:
|
||||
259,118,3750,1,0,1:0:0:0:
|
||||
|
@ -8,7 +8,8 @@
|
||||
"NodeSamples": [
|
||||
["normal-hitnormal"],
|
||||
[]
|
||||
]
|
||||
],
|
||||
"Samples": ["normal-hitnormal"]
|
||||
}]
|
||||
}, {
|
||||
"StartTime": 2000.0,
|
||||
@ -19,7 +20,8 @@
|
||||
"NodeSamples": [
|
||||
["drum-hitnormal"],
|
||||
[]
|
||||
]
|
||||
],
|
||||
"Samples": ["drum-hitnormal"]
|
||||
}]
|
||||
}]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 8470.0,
|
||||
"Objects": [{
|
||||
"StartTime": 8470.0,
|
||||
"EndTime": 8470.0,
|
||||
"Column": 0,
|
||||
"Samples": ["normal-hitnormal", "normal-hitclap"]
|
||||
}, {
|
||||
"StartTime": 8626.470587768974,
|
||||
"EndTime": 8626.470587768974,
|
||||
"Column": 1,
|
||||
"Samples": ["normal-hitnormal"]
|
||||
}, {
|
||||
"StartTime": 8782.941175537948,
|
||||
"EndTime": 8782.941175537948,
|
||||
"Column": 2,
|
||||
"Samples": ["normal-hitnormal", "normal-hitclap"]
|
||||
}]
|
||||
}]
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
osu file format v14
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6
|
||||
CircleSize:4
|
||||
OverallDifficulty:8
|
||||
ApproachRate:9.5
|
||||
SliderMultiplier:2.00000000596047
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,312.941176470588,4,1,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0:
|
@ -9,6 +9,9 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Skinning;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
@ -129,6 +132,15 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
return this.GetAnimation(filename, true, true);
|
||||
}
|
||||
|
||||
public override SampleChannel GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
// layered hit sounds never play in mania
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
return new SampleChannelVirtual();
|
||||
|
||||
return Source.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
|
132
osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
Normal file
132
osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene
|
||||
{
|
||||
private Box background;
|
||||
private Drawable object1;
|
||||
private Drawable object2;
|
||||
private TestAccuracyHeatmap accuracyHeatmap;
|
||||
private ScheduledDelegate automaticAdditionDelegate;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
automaticAdditionDelegate?.Cancel();
|
||||
automaticAdditionDelegate = null;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#333"),
|
||||
},
|
||||
object1 = new BorderCircle
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
Colour = Color4.Yellow,
|
||||
},
|
||||
object2 = new BorderCircle
|
||||
{
|
||||
Position = new Vector2(100, 300),
|
||||
},
|
||||
accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(130)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestManyHitPointsAutomatic()
|
||||
{
|
||||
AddStep("add scheduled delegate", () =>
|
||||
{
|
||||
automaticAdditionDelegate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var randomPos = new Vector2(
|
||||
RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2),
|
||||
RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2));
|
||||
|
||||
// The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene).
|
||||
accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500));
|
||||
InputManager.MoveMouseTo(background.ToScreenSpace(randomPos));
|
||||
}, 1, true);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for some hit points", 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManualPlacement()
|
||||
{
|
||||
AddStep("return user input", () => InputManager.UseParentInput = true);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
|
||||
return true;
|
||||
}
|
||||
|
||||
private class TestAccuracyHeatmap : AccuracyHeatmap
|
||||
{
|
||||
public TestAccuracyHeatmap(ScoreInfo score)
|
||||
: base(score, new TestBeatmap(new OsuRuleset().RulesetInfo))
|
||||
{
|
||||
}
|
||||
|
||||
public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
|
||||
=> base.AddPoint(start, end, hitPoint, radius);
|
||||
}
|
||||
|
||||
private class BorderCircle : CircularContainer
|
||||
{
|
||||
public BorderCircle()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(100);
|
||||
|
||||
Masking = true;
|
||||
BorderThickness = 2;
|
||||
BorderColour = Color4.White;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(4),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// 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.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Judgements
|
||||
{
|
||||
public class OsuHitCircleJudgementResult : OsuJudgementResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="HitCircle"/>.
|
||||
/// </summary>
|
||||
public HitCircle HitCircle => (HitCircle)HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the player's cursor when <see cref="HitCircle"/> was hit.
|
||||
/// </summary>
|
||||
public Vector2? CursorPositionAtHit;
|
||||
|
||||
public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement)
|
||||
: base(hitObject, judgement)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,11 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
public DrawableHitCircle(HitCircle h)
|
||||
: base(h)
|
||||
{
|
||||
@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = result);
|
||||
ApplyResult(r =>
|
||||
{
|
||||
var circleResult = (OsuHitCircleJudgementResult)r;
|
||||
|
||||
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
|
||||
if (result != HitResult.Miss)
|
||||
{
|
||||
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
|
||||
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
|
||||
}
|
||||
|
||||
circleResult.Type = result;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public Drawable ProxiedLayer => ApproachCircle;
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement);
|
||||
|
||||
public class HitReceptor : CompositeDrawable, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
// IsHovered is used
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
@ -193,9 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
SpmCounter.SetRotation(Disc.RotationAbsolute);
|
||||
|
||||
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
|
||||
Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint);
|
||||
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
|
||||
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
|
||||
|
||||
symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint);
|
||||
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.RotationAbsolute / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
|
@ -29,6 +29,10 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
@ -186,5 +190,31 @@ namespace osu.Game.Rulesets.Osu
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||
{
|
||||
new StatisticRow
|
||||
{
|
||||
Columns = new[]
|
||||
{
|
||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}),
|
||||
}
|
||||
},
|
||||
new StatisticRow
|
||||
{
|
||||
Columns = new[]
|
||||
{
|
||||
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,27 @@
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case HitCircle _:
|
||||
return new OsuHitCircleJudgementResult(hitObject, judgement);
|
||||
|
||||
default:
|
||||
return new OsuJudgementResult(hitObject, judgement);
|
||||
}
|
||||
}
|
||||
|
||||
public override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||
}
|
||||
|
297
osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
Normal file
297
osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
Normal file
@ -0,0 +1,297 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Statistics
|
||||
{
|
||||
public class AccuracyHeatmap : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of the inner circle containing the "hit" points, relative to the size of this <see cref="AccuracyHeatmap"/>.
|
||||
/// All other points outside of the inner circle are "miss" points.
|
||||
/// </summary>
|
||||
private const float inner_portion = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Number of rows/columns of points.
|
||||
/// ~4px per point @ 128x128 size (the contents of the <see cref="AccuracyHeatmap"/> are always square). 1089 total points.
|
||||
/// </summary>
|
||||
private const int points_per_dimension = 33;
|
||||
|
||||
private const float rotation = 45;
|
||||
|
||||
private BufferedContainer bufferedGrid;
|
||||
private GridContainer pointGrid;
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly IBeatmap playableBeatmap;
|
||||
|
||||
private const float line_thickness = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The highest count of any point currently being displayed.
|
||||
/// </summary>
|
||||
protected float PeakValue { get; private set; }
|
||||
|
||||
public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
|
||||
{
|
||||
this.score = score;
|
||||
this.playableBeatmap = playableBeatmap;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(inner_portion),
|
||||
Masking = true,
|
||||
BorderThickness = line_thickness,
|
||||
BorderColour = Color4.White,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#202624")
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(1),
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
|
||||
Width = line_thickness / 2,
|
||||
Rotation = -rotation,
|
||||
Alpha = 0.3f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
|
||||
Width = line_thickness / 2, // adjust for edgesmoothness
|
||||
Rotation = rotation
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Width = 10,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
Height = line_thickness / 2, // adjust for edgesmoothness
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
Width = line_thickness / 2, // adjust for edgesmoothness
|
||||
Height = 10,
|
||||
}
|
||||
}
|
||||
},
|
||||
bufferedGrid = new BufferedContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CacheDrawnFrameBuffer = true,
|
||||
BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0),
|
||||
Child = pointGrid = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Vector2 centre = new Vector2(points_per_dimension) / 2;
|
||||
float innerRadius = centre.X * inner_portion;
|
||||
|
||||
Drawable[][] points = new Drawable[points_per_dimension][];
|
||||
|
||||
for (int r = 0; r < points_per_dimension; r++)
|
||||
{
|
||||
points[r] = new Drawable[points_per_dimension];
|
||||
|
||||
for (int c = 0; c < points_per_dimension; c++)
|
||||
{
|
||||
HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius
|
||||
? HitPointType.Hit
|
||||
: HitPointType.Miss;
|
||||
|
||||
var point = new HitPoint(pointType, this)
|
||||
{
|
||||
Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
|
||||
};
|
||||
|
||||
points[r][c] = point;
|
||||
}
|
||||
}
|
||||
|
||||
pointGrid.Content = points;
|
||||
|
||||
if (score.HitEvents == null || score.HitEvents.Count == 0)
|
||||
return;
|
||||
|
||||
// Todo: This should probably not be done like this.
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
|
||||
|
||||
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
|
||||
{
|
||||
if (e.LastHitObject == null || e.Position == null)
|
||||
continue;
|
||||
|
||||
AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
|
||||
{
|
||||
if (pointGrid.Content.Length == 0)
|
||||
return;
|
||||
|
||||
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
|
||||
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
|
||||
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
|
||||
float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
|
||||
|
||||
// Consider two objects placed horizontally, with the start on the left and the end on the right.
|
||||
// The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
|
||||
// +pi | 0
|
||||
// O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi)
|
||||
// -pi | 0
|
||||
// E.g. If the hit point was directly above end, it would have an angle pi/2.
|
||||
//
|
||||
// It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form:
|
||||
// 0 | pi
|
||||
// O --------- O ----->
|
||||
// 2pi | pi
|
||||
//
|
||||
// However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted.
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
|
||||
float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
|
||||
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
|
||||
|
||||
// Find the most relevant hit point.
|
||||
int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
|
||||
int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
|
||||
|
||||
PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment());
|
||||
|
||||
bufferedGrid.ForceRedraw();
|
||||
}
|
||||
|
||||
private class HitPoint : Circle
|
||||
{
|
||||
private readonly HitPointType pointType;
|
||||
private readonly AccuracyHeatmap heatmap;
|
||||
|
||||
public override bool IsPresent => count > 0;
|
||||
|
||||
public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
|
||||
{
|
||||
this.pointType = pointType;
|
||||
this.heatmap = heatmap;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Alpha = 1;
|
||||
}
|
||||
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// Increment the value of this point by one.
|
||||
/// </summary>
|
||||
/// <returns>The value after incrementing.</returns>
|
||||
public int Increment()
|
||||
{
|
||||
return ++count;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// the point at which alpha is saturated and we begin to adjust colour lightness.
|
||||
const float lighten_cutoff = 0.95f;
|
||||
|
||||
// the amount of lightness to attribute regardless of relative value to peak point.
|
||||
const float non_relative_portion = 0.2f;
|
||||
|
||||
float amount = 0;
|
||||
|
||||
// give some amount of alpha regardless of relative count
|
||||
amount += non_relative_portion * Math.Min(1, count / 10f);
|
||||
|
||||
// add relative portion
|
||||
amount += (1 - non_relative_portion) * (count / heatmap.PeakValue);
|
||||
|
||||
// apply easing
|
||||
amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
|
||||
|
||||
Debug.Assert(amount <= 1);
|
||||
|
||||
Alpha = Math.Min(amount / lighten_cutoff, 1);
|
||||
if (pointType == HitPointType.Hit)
|
||||
Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff));
|
||||
}
|
||||
}
|
||||
|
||||
private enum HitPointType
|
||||
{
|
||||
Hit,
|
||||
Miss
|
||||
}
|
||||
}
|
||||
}
|
@ -21,9 +21,12 @@ using osu.Game.Rulesets.Taiko.Difficulty;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Taiko.Edit;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Skinning;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko
|
||||
@ -155,5 +158,20 @@ namespace osu.Game.Rulesets.Taiko
|
||||
public int LegacyID => 1;
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||
|
||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||
{
|
||||
new StatisticRow
|
||||
{
|
||||
Columns = new[]
|
||||
{
|
||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
|
||||
Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = border_thickness,
|
||||
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
|
||||
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE),
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = border_thickness,
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
@ -167,5 +168,64 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when a custom sample bank is used, both the normal and additional sounds will be looked up.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitObjectCustomSampleBank()
|
||||
{
|
||||
string[] expectedSamples =
|
||||
{
|
||||
"normal-hitnormal2",
|
||||
"normal-hitwhistle2"
|
||||
};
|
||||
|
||||
SetupSkins(expectedSamples[0], expectedSamples[1]);
|
||||
|
||||
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expectedSamples[0]);
|
||||
AssertUserLookup(expectedSamples[1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when a custom sample bank is used, but <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
|
||||
/// only the additional sound will be looked up.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitObjectCustomSampleBankWithoutLayered()
|
||||
{
|
||||
const string expected_sample = "normal-hitwhistle2";
|
||||
const string unwanted_sample = "normal-hitnormal2";
|
||||
|
||||
SetupSkins(expected_sample, unwanted_sample);
|
||||
disableLayeredHitSounds();
|
||||
|
||||
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when a normal sample bank is used and <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
|
||||
/// the normal sound will be looked up anyway.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitObjectNormalSampleBankWithoutLayered()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(expected_sample, expected_sample);
|
||||
disableLayeredHitSounds();
|
||||
|
||||
CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
|
||||
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
private void disableLayeredHitSounds()
|
||||
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,10 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
@ -70,6 +72,50 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
|
||||
}
|
||||
|
||||
[TestCase(typeof(OsuModDoubleTime), 1.5)]
|
||||
[TestCase(typeof(OsuModHalfTime), 0.75)]
|
||||
[TestCase(typeof(ModWindUp), 1.5)]
|
||||
[TestCase(typeof(ModWindDown), 0.75)]
|
||||
[TestCase(typeof(OsuModDoubleTime), 2)]
|
||||
[TestCase(typeof(OsuModHalfTime), 0.5)]
|
||||
[TestCase(typeof(ModWindUp), 2)]
|
||||
[TestCase(typeof(ModWindDown), 0.5)]
|
||||
public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
|
||||
{
|
||||
GameplayClockContainer gameplayContainer = null;
|
||||
TestDrawableStoryboardSample sample = null;
|
||||
|
||||
Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
|
||||
|
||||
switch (testedMod)
|
||||
{
|
||||
case ModRateAdjust m:
|
||||
m.SpeedChange.Value = expectedRate;
|
||||
break;
|
||||
|
||||
case ModTimeRamp m:
|
||||
m.InitialRate.Value = m.FinalRate.Value = expectedRate;
|
||||
break;
|
||||
}
|
||||
|
||||
AddStep("setup storyboard sample", () =>
|
||||
{
|
||||
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
|
||||
SelectedMods.Value = new[] { testedMod };
|
||||
|
||||
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0));
|
||||
|
||||
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
|
||||
{
|
||||
Clock = gameplayContainer.GameplayClock
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("start", () => gameplayContainer.Start());
|
||||
|
||||
AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
|
||||
}
|
||||
|
||||
private class TestSkin : LegacySkin
|
||||
{
|
||||
public TestSkin(string resourceName, AudioManager audioManager)
|
||||
@ -99,5 +145,28 @@ namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
|
||||
{
|
||||
private readonly AudioManager audio;
|
||||
|
||||
public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio)
|
||||
: base(ruleset, null, audio)
|
||||
{
|
||||
this.audio = audio;
|
||||
}
|
||||
|
||||
protected override ISkin GetSkin() => new TestSkin("test-sample", audio);
|
||||
}
|
||||
|
||||
private class TestDrawableStoryboardSample : DrawableStoryboardSample
|
||||
{
|
||||
public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
|
||||
: base(sampleInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public new SampleChannel Channel => base.Channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,6 +127,9 @@ namespace osu.Game.Tests.NonVisual
|
||||
var osu = loadOsu(host);
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
|
||||
string originalDirectory = storage.GetFullPath(".");
|
||||
|
||||
// ensure we perform a save
|
||||
host.Dependencies.Get<FrameworkConfigManager>().Save();
|
||||
|
||||
@ -145,25 +148,25 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
|
||||
|
||||
// ensure cache was not moved
|
||||
Assert.That(host.Storage.ExistsDirectory("cache"));
|
||||
Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache")));
|
||||
|
||||
// ensure nested cache was moved
|
||||
Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
||||
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
|
||||
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
||||
|
||||
foreach (var file in OsuStorage.IGNORE_FILES)
|
||||
{
|
||||
Assert.That(host.Storage.Exists(file), Is.True);
|
||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||
Assert.That(storage.Exists(file), Is.False);
|
||||
}
|
||||
|
||||
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
|
||||
{
|
||||
Assert.That(host.Storage.ExistsDirectory(dir), Is.True);
|
||||
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
|
||||
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
||||
}
|
||||
|
||||
Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
|
||||
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online
|
||||
Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeserialiseTimeRampMod()
|
||||
{
|
||||
// Create the mod with values different from default.
|
||||
var apiMod = new APIMod(new TestModTimeRamp
|
||||
{
|
||||
AdjustPitch = { Value = false },
|
||||
InitialRate = { Value = 1.25 },
|
||||
FinalRate = { Value = 0.25 }
|
||||
});
|
||||
|
||||
var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
|
||||
var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
|
||||
|
||||
Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
|
||||
Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
|
||||
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
|
||||
}
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
{
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new[] { new TestMod() };
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
|
||||
{
|
||||
new TestMod(),
|
||||
new TestModTimeRamp(),
|
||||
};
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
|
||||
|
||||
@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online
|
||||
Precision = 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
private class TestModTimeRamp : ModTimeRamp
|
||||
{
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TMTR";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Initial rate", "The starting speed of the track")]
|
||||
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 2,
|
||||
Default = 1.5,
|
||||
Value = 1.5,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Final rate", "The speed increase to ramp towards")]
|
||||
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Default = 0.5,
|
||||
Value = 0.5,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
public override BindableBool AdjustPitch { get; } = new BindableBool
|
||||
{
|
||||
Default = true,
|
||||
Value = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
5
osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
Normal file
5
osu.Game.Tests/Resources/skin-zero-alpha-colour.ini
Normal file
@ -0,0 +1,5 @@
|
||||
[General]
|
||||
Version: latest
|
||||
|
||||
[Colours]
|
||||
Combo1: 255,255,255,0
|
@ -108,5 +108,15 @@ namespace osu.Game.Tests.Skins
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeColourWithZeroAlpha()
|
||||
{
|
||||
var decoder = new LegacySkinDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private FailingLayer layer;
|
||||
|
||||
private readonly Bindable<bool> showHealth = new Bindable<bool>();
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
Child = layer = new FailingLayer();
|
||||
layer.BindHealthProcessor(new DrainingHealthProcessor(1));
|
||||
layer.ShowHealth.BindTo(showHealth);
|
||||
});
|
||||
|
||||
AddStep("show health", () => showHealth.Value = true);
|
||||
AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
|
||||
AddUntilStep("layer is visible", () => layer.IsPresent);
|
||||
}
|
||||
@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddWaitStep("wait for potential fade", 10);
|
||||
AddAssert("layer is still visible", () => layer.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLayerVisibilityWithDifferentOptions()
|
||||
{
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
|
||||
AddStep("don't show health", () => showHealth.Value = false);
|
||||
AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
|
||||
|
||||
AddStep("don't show health", () => showHealth.Value = false);
|
||||
AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
|
||||
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
|
||||
|
||||
AddStep("show health", () => showHealth.Value = true);
|
||||
AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
|
||||
|
||||
AddStep("show health", () => showHealth.Value = true);
|
||||
AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
|
||||
AddUntilStep("layer fade is visible", () => layer.IsPresent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[Cached]
|
||||
private OsuLogo logo;
|
||||
|
||||
protected OsuScreenStack IntroStack;
|
||||
|
||||
protected IntroTestScene()
|
||||
{
|
||||
OsuScreenStack introStack = null;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
logo.FinishTransforms();
|
||||
logo.IsTracking = false;
|
||||
|
||||
introStack?.Expire();
|
||||
IntroStack?.Expire();
|
||||
|
||||
Add(introStack = new OsuScreenStack
|
||||
Add(IntroStack = new OsuScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
introStack.Push(CreateScreen());
|
||||
IntroStack.Push(CreateScreen());
|
||||
});
|
||||
|
||||
AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu);
|
||||
AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu);
|
||||
}
|
||||
|
||||
protected abstract IScreen CreateScreen();
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
@ -11,5 +12,14 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
public class TestSceneIntroWelcome : IntroTestScene
|
||||
{
|
||||
protected override IScreen CreateScreen() => new IntroWelcome();
|
||||
|
||||
public TestSceneIntroWelcome()
|
||||
{
|
||||
AddUntilStep("wait for load", () => getTrack() != null);
|
||||
|
||||
AddAssert("check if menu music loops", () => getTrack().Looping);
|
||||
}
|
||||
|
||||
private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
// 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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestManyDistributedEvents()
|
||||
{
|
||||
createTest(CreateDistributedHitEvents());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroTimeOffset()
|
||||
{
|
||||
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoEvents()
|
||||
{
|
||||
createTest(new List<HitEvent>());
|
||||
}
|
||||
|
||||
private void createTest(List<HitEvent> events) => AddStep("create test", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#333")
|
||||
},
|
||||
new HitEventTimingDistributionGraph(events)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(600, 130)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
public static List<HitEvent> CreateDistributedHitEvents()
|
||||
{
|
||||
var hitEvents = new List<HitEvent>();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
|
||||
}
|
||||
|
||||
return hitEvents;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,32 @@
|
||||
// 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 System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneResultsScreen : ScreenTestScene
|
||||
public class TestSceneResultsScreen : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatmapManager beatmaps;
|
||||
|
||||
@ -41,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
|
||||
|
||||
[Test]
|
||||
public void ResultsWithoutPlayer()
|
||||
public void TestResultsWithoutPlayer()
|
||||
{
|
||||
TestResultsScreen screen = null;
|
||||
OsuScreenStack stack;
|
||||
@ -60,7 +69,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResultsWithPlayer()
|
||||
public void TestResultsWithPlayer()
|
||||
{
|
||||
TestResultsScreen screen = null;
|
||||
|
||||
@ -70,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResultsForUnranked()
|
||||
public void TestResultsForUnranked()
|
||||
{
|
||||
UnrankedSoloResultsScreen screen = null;
|
||||
|
||||
@ -79,6 +88,130 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowHideStatisticsViaOutsideClick()
|
||||
{
|
||||
TestResultsScreen screen = null;
|
||||
|
||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||
|
||||
AddStep("click expanded panel", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
|
||||
|
||||
AddUntilStep("expanded panel at the left of the screen", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
|
||||
});
|
||||
|
||||
AddStep("click to right of panel", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("statistics hidden", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Hidden);
|
||||
|
||||
AddUntilStep("expanded panel in centre of screen", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowHideStatistics()
|
||||
{
|
||||
TestResultsScreen screen = null;
|
||||
|
||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||
|
||||
AddStep("click expanded panel", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
|
||||
|
||||
AddUntilStep("expanded panel at the left of the screen", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
|
||||
});
|
||||
|
||||
AddStep("click expanded panel", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("statistics hidden", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Hidden);
|
||||
|
||||
AddUntilStep("expanded panel in centre of screen", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowStatisticsAndClickOtherPanel()
|
||||
{
|
||||
TestResultsScreen screen = null;
|
||||
|
||||
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
|
||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||
|
||||
ScorePanel expandedPanel = null;
|
||||
ScorePanel contractedPanel = null;
|
||||
|
||||
AddStep("click expanded panel then contracted panel", () =>
|
||||
{
|
||||
expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
|
||||
contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
|
||||
InputManager.MoveMouseTo(contractedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
|
||||
|
||||
AddAssert("contracted panel still contracted", () => contractedPanel.State == PanelState.Contracted);
|
||||
AddAssert("expanded panel still expanded", () => expandedPanel.State == PanelState.Expanded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFetchScoresAfterShowingStatistics()
|
||||
{
|
||||
DelayedFetchResultsScreen screen = null;
|
||||
|
||||
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000)));
|
||||
AddUntilStep("wait for loaded", () => screen.IsLoaded);
|
||||
AddStep("click expanded panel", () =>
|
||||
{
|
||||
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for fetch", () => screen.FetchCompleted);
|
||||
AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
|
||||
}
|
||||
|
||||
private class TestResultsContainer : Container
|
||||
{
|
||||
[Cached(typeof(Player))]
|
||||
@ -113,6 +246,58 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
{
|
||||
var scores = new List<ScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
||||
score.TotalScore += 10 - i;
|
||||
scores.Add(score);
|
||||
}
|
||||
|
||||
scoresCallback?.Invoke(scores);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class DelayedFetchResultsScreen : TestResultsScreen
|
||||
{
|
||||
public bool FetchCompleted { get; private set; }
|
||||
|
||||
private readonly double delay;
|
||||
|
||||
public DelayedFetchResultsScreen(ScoreInfo score, double delay)
|
||||
: base(score)
|
||||
{
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(delay));
|
||||
|
||||
var scores = new List<ScoreInfo>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
|
||||
score.TotalScore += 10 - i;
|
||||
scores.Add(score);
|
||||
}
|
||||
|
||||
scoresCallback?.Invoke(scores);
|
||||
|
||||
Schedule(() => FetchCompleted = true);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class UnrankedSoloResultsScreen : SoloResultsScreen
|
||||
|
48
osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
Normal file
48
osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public class TestSceneStatisticsPanel : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestScoreWithStatistics()
|
||||
{
|
||||
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents()
|
||||
};
|
||||
|
||||
loadPanel(score);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreWithoutStatistics()
|
||||
{
|
||||
loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullScore()
|
||||
{
|
||||
loadPanel(null);
|
||||
}
|
||||
|
||||
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
|
||||
{
|
||||
Child = new StatisticsPanel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Score = { Value = score }
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -17,11 +17,12 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneBeatmapCarousel : OsuTestScene
|
||||
public class TestSceneBeatmapCarousel : OsuManualInputManagerTestScene
|
||||
{
|
||||
private TestBeatmapCarousel carousel;
|
||||
private RulesetStore rulesets;
|
||||
@ -39,6 +40,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyRepeat()
|
||||
{
|
||||
loadBeatmaps();
|
||||
advanceSelection(false);
|
||||
|
||||
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
|
||||
|
||||
BeatmapInfo selection = null;
|
||||
|
||||
checkSelectionIterating(true);
|
||||
|
||||
AddStep("press up arrow", () => InputManager.PressKey(Key.Up));
|
||||
|
||||
checkSelectionIterating(true);
|
||||
|
||||
AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down));
|
||||
|
||||
checkSelectionIterating(true);
|
||||
|
||||
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
|
||||
|
||||
checkSelectionIterating(false);
|
||||
|
||||
void checkSelectionIterating(bool isIterating)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
AddStep("store selection", () => selection = carousel.SelectedBeatmap);
|
||||
if (isIterating)
|
||||
AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection);
|
||||
else
|
||||
AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRecommendedSelection()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.Gameplay.Components;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Components
|
||||
{
|
||||
public class TestSceneMatchScoreDisplay : LadderTestScene
|
||||
public class TestSceneMatchScoreDisplay : TournamentTestScene
|
||||
{
|
||||
[Cached(Type = typeof(MatchIPCInfo))]
|
||||
private MatchIPCInfo matchInfo = new MatchIPCInfo();
|
||||
|
@ -8,12 +8,11 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tournament.Components;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Components
|
||||
{
|
||||
public class TestSceneTournamentBeatmapPanel : OsuTestScene
|
||||
public class TestSceneTournamentBeatmapPanel : TournamentTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
@ -1,146 +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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tournament.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public abstract class LadderTestScene : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First();
|
||||
|
||||
Ruleset.BindTo(Ladder.Ruleset);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
TournamentMatch match = CreateSampleMatch();
|
||||
|
||||
Ladder.Rounds.Add(match.Round.Value);
|
||||
Ladder.Matches.Add(match);
|
||||
Ladder.Teams.Add(match.Team1.Value);
|
||||
Ladder.Teams.Add(match.Team2.Value);
|
||||
|
||||
Ladder.CurrentMatch.Value = match;
|
||||
}
|
||||
|
||||
public static TournamentMatch CreateSampleMatch() => new TournamentMatch
|
||||
{
|
||||
Team1 =
|
||||
{
|
||||
Value = new TournamentTeam
|
||||
{
|
||||
FlagName = { Value = "JP" },
|
||||
FullName = { Value = "Japan" },
|
||||
LastYearPlacing = { Value = 10 },
|
||||
Seed = { Value = "Low" },
|
||||
SeedingResults =
|
||||
{
|
||||
new SeedingResult
|
||||
{
|
||||
Mod = { Value = "NM" },
|
||||
Seed = { Value = 10 },
|
||||
Beatmaps =
|
||||
{
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 12345672,
|
||||
Seed = { Value = 24 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 1234567,
|
||||
Seed = { Value = 12 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 1234567,
|
||||
Seed = { Value = 16 },
|
||||
}
|
||||
}
|
||||
},
|
||||
new SeedingResult
|
||||
{
|
||||
Mod = { Value = "DT" },
|
||||
Seed = { Value = 5 },
|
||||
Beatmaps =
|
||||
{
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 3 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 6 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 12 },
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Players =
|
||||
{
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } },
|
||||
}
|
||||
}
|
||||
},
|
||||
Team2 =
|
||||
{
|
||||
Value = new TournamentTeam
|
||||
{
|
||||
FlagName = { Value = "US" },
|
||||
FullName = { Value = "United States" },
|
||||
Players =
|
||||
{
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
}
|
||||
}
|
||||
},
|
||||
Round =
|
||||
{
|
||||
Value = new TournamentRound { Name = { Value = "Quarterfinals" } }
|
||||
}
|
||||
};
|
||||
|
||||
public static BeatmapInfo CreateSampleBeatmapInfo() =>
|
||||
new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } };
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Editors;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneLadderEditorScreen : LadderTestScene
|
||||
public class TestSceneLadderEditorScreen : TournamentTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Ladder;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneLadderScreen : LadderTestScene
|
||||
public class TestSceneLadderScreen : TournamentTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -12,7 +12,7 @@ using osu.Game.Tournament.Screens.MapPool;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneMapPoolScreen : LadderTestScene
|
||||
public class TestSceneMapPoolScreen : TournamentTestScene
|
||||
{
|
||||
private MapPoolScreen screen;
|
||||
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneRoundEditorScreen : LadderTestScene
|
||||
public class TestSceneRoundEditorScreen : TournamentTestScene
|
||||
{
|
||||
public TestSceneRoundEditorScreen()
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ using osu.Game.Tournament.Screens.Editors;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneSeedingEditorScreen : LadderTestScene
|
||||
public class TestSceneSeedingEditorScreen : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneSeedingScreen : LadderTestScene
|
||||
public class TestSceneSeedingScreen : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneTeamEditorScreen : LadderTestScene
|
||||
public class TestSceneTeamEditorScreen : TournamentTestScene
|
||||
{
|
||||
public TestSceneTeamEditorScreen()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneTeamIntroScreen : LadderTestScene
|
||||
public class TestSceneTeamIntroScreen : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
@ -4,25 +4,19 @@
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osu.Game.Tournament.Screens.TeamWin;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Screens
|
||||
{
|
||||
public class TestSceneTeamWinScreen : LadderTestScene
|
||||
public class TestSceneTeamWinScreen : TournamentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var match = new TournamentMatch();
|
||||
match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
|
||||
match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
|
||||
var match = Ladder.CurrentMatch.Value;
|
||||
|
||||
match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
|
||||
match.Completed.Value = true;
|
||||
ladder.CurrentMatch.Value = match;
|
||||
|
||||
Add(new TeamWinScreen
|
||||
{
|
||||
|
@ -1,13 +1,151 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tournament.IPC;
|
||||
using osu.Game.Tournament.Models;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tournament.Tests
|
||||
{
|
||||
public abstract class TournamentTestScene : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; }
|
||||
|
||||
[Cached]
|
||||
protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Storage storage)
|
||||
{
|
||||
Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First();
|
||||
|
||||
TournamentMatch match = CreateSampleMatch();
|
||||
|
||||
Ladder.Rounds.Add(match.Round.Value);
|
||||
Ladder.Matches.Add(match);
|
||||
Ladder.Teams.Add(match.Team1.Value);
|
||||
Ladder.Teams.Add(match.Team2.Value);
|
||||
|
||||
Ladder.CurrentMatch.Value = match;
|
||||
|
||||
Ruleset.BindTo(Ladder.Ruleset);
|
||||
Dependencies.CacheAs(new StableInfo(storage));
|
||||
}
|
||||
|
||||
public static TournamentMatch CreateSampleMatch() => new TournamentMatch
|
||||
{
|
||||
Team1 =
|
||||
{
|
||||
Value = new TournamentTeam
|
||||
{
|
||||
Acronym = { Value = "JPN" },
|
||||
FlagName = { Value = "JP" },
|
||||
FullName = { Value = "Japan" },
|
||||
LastYearPlacing = { Value = 10 },
|
||||
Seed = { Value = "Low" },
|
||||
SeedingResults =
|
||||
{
|
||||
new SeedingResult
|
||||
{
|
||||
Mod = { Value = "NM" },
|
||||
Seed = { Value = 10 },
|
||||
Beatmaps =
|
||||
{
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 12345672,
|
||||
Seed = { Value = 24 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 1234567,
|
||||
Seed = { Value = 12 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 1234567,
|
||||
Seed = { Value = 16 },
|
||||
}
|
||||
}
|
||||
},
|
||||
new SeedingResult
|
||||
{
|
||||
Mod = { Value = "DT" },
|
||||
Seed = { Value = 5 },
|
||||
Beatmaps =
|
||||
{
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 3 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 6 },
|
||||
},
|
||||
new SeedingBeatmap
|
||||
{
|
||||
BeatmapInfo = CreateSampleBeatmapInfo(),
|
||||
Score = 234567,
|
||||
Seed = { Value = 12 },
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Players =
|
||||
{
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } },
|
||||
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } },
|
||||
}
|
||||
}
|
||||
},
|
||||
Team2 =
|
||||
{
|
||||
Value = new TournamentTeam
|
||||
{
|
||||
Acronym = { Value = "USA" },
|
||||
FlagName = { Value = "US" },
|
||||
FullName = { Value = "United States" },
|
||||
Players =
|
||||
{
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
new User { Username = "Hello" },
|
||||
}
|
||||
}
|
||||
},
|
||||
Round =
|
||||
{
|
||||
Value = new TournamentRound { Name = { Value = "Quarterfinals" } }
|
||||
}
|
||||
};
|
||||
|
||||
public static BeatmapInfo CreateSampleBeatmapInfo() =>
|
||||
new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } };
|
||||
|
||||
protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner();
|
||||
|
||||
public class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -66,6 +67,9 @@ namespace osu.Game.Tournament.Components
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -77,8 +81,6 @@ namespace osu.Game.Tournament.Components
|
||||
flow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
// Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
|
||||
Height = 1,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
LayoutDuration = 500,
|
||||
LayoutEasing = Easing.OutQuint,
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Tournament
|
||||
|
||||
public const float STREAM_AREA_WIDTH = 1366;
|
||||
|
||||
public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH;
|
||||
public const double REQUIRED_WIDTH = CONTROL_AREA_WIDTH * 2 + STREAM_AREA_WIDTH;
|
||||
|
||||
[Cached]
|
||||
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();
|
||||
|
@ -240,6 +240,9 @@ namespace osu.Game.Beatmaps
|
||||
beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
|
||||
}
|
||||
|
||||
if (beatmapInfo == null)
|
||||
return DefaultBeatmap;
|
||||
|
||||
lock (workingCache)
|
||||
{
|
||||
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
|
||||
|
@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case 2:
|
||||
position.X = ((IHasXPosition)hitObject).X * 512;
|
||||
position.X = ((IHasXPosition)hitObject).X;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
|
@ -103,7 +103,12 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
try
|
||||
{
|
||||
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), split.Length == 4 ? byte.Parse(split[3]) : (byte)255);
|
||||
byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
|
||||
|
||||
if (alpha == 0)
|
||||
alpha = 255;
|
||||
|
||||
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
/// </summary>
|
||||
public static class Parsing
|
||||
{
|
||||
public const int MAX_COORDINATE_VALUE = 65536;
|
||||
public const int MAX_COORDINATE_VALUE = 131072;
|
||||
|
||||
public const double MAX_PARSE_VALUE = int.MaxValue;
|
||||
|
||||
|
@ -2,9 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
@ -13,28 +15,86 @@ namespace osu.Game.IO
|
||||
{
|
||||
public class OsuStorage : WrappedStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
|
||||
/// </summary>
|
||||
public readonly OsuStorageError Error;
|
||||
|
||||
/// <summary>
|
||||
/// The custom storage path as selected by the user.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public string CustomStoragePath => storageConfig.Get<string>(StorageConfig.FullPath);
|
||||
|
||||
/// <summary>
|
||||
/// The default storage path to be used if a custom storage path hasn't been selected or is not accessible.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public string DefaultStoragePath => defaultStorage.GetFullPath(".");
|
||||
|
||||
private readonly GameHost host;
|
||||
private readonly StorageConfigManager storageConfig;
|
||||
private readonly Storage defaultStorage;
|
||||
|
||||
internal static readonly string[] IGNORE_DIRECTORIES = { "cache" };
|
||||
public static readonly string[] IGNORE_DIRECTORIES = { "cache" };
|
||||
|
||||
internal static readonly string[] IGNORE_FILES =
|
||||
public static readonly string[] IGNORE_FILES =
|
||||
{
|
||||
"framework.ini",
|
||||
"storage.ini"
|
||||
};
|
||||
|
||||
public OsuStorage(GameHost host)
|
||||
: base(host.Storage, string.Empty)
|
||||
public OsuStorage(GameHost host, Storage defaultStorage)
|
||||
: base(defaultStorage, string.Empty)
|
||||
{
|
||||
this.host = host;
|
||||
this.defaultStorage = defaultStorage;
|
||||
|
||||
storageConfig = new StorageConfigManager(host.Storage);
|
||||
storageConfig = new StorageConfigManager(defaultStorage);
|
||||
|
||||
var customStoragePath = storageConfig.Get<string>(StorageConfig.FullPath);
|
||||
if (!string.IsNullOrEmpty(CustomStoragePath))
|
||||
TryChangeToCustomStorage(out Error);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(customStoragePath))
|
||||
ChangeTargetStorage(host.GetStorage(customStoragePath));
|
||||
/// <summary>
|
||||
/// Resets the custom storage path, changing the target storage to the default location.
|
||||
/// </summary>
|
||||
public void ResetCustomStoragePath()
|
||||
{
|
||||
storageConfig.Set(StorageConfig.FullPath, string.Empty);
|
||||
storageConfig.Save();
|
||||
|
||||
ChangeTargetStorage(defaultStorage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to change to the user's custom storage path.
|
||||
/// </summary>
|
||||
/// <param name="error">The error that occurred.</param>
|
||||
/// <returns>Whether the custom storage path was used successfully. If not, <paramref name="error"/> will be populated with the reason.</returns>
|
||||
public bool TryChangeToCustomStorage(out OsuStorageError error)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(CustomStoragePath));
|
||||
|
||||
error = OsuStorageError.None;
|
||||
Storage lastStorage = UnderlyingStorage;
|
||||
|
||||
try
|
||||
{
|
||||
Storage userStorage = host.GetStorage(CustomStoragePath);
|
||||
|
||||
if (!userStorage.ExistsDirectory(".") || !userStorage.GetFiles(".").Any())
|
||||
error = OsuStorageError.AccessibleButEmpty;
|
||||
|
||||
ChangeTargetStorage(userStorage);
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = OsuStorageError.NotAccessible;
|
||||
ChangeTargetStorage(lastStorage);
|
||||
}
|
||||
|
||||
return error == OsuStorageError.None;
|
||||
}
|
||||
|
||||
protected override void ChangeTargetStorage(Storage newStorage)
|
||||
@ -145,4 +205,23 @@ namespace osu.Game.IO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OsuStorageError
|
||||
{
|
||||
/// <summary>
|
||||
/// No error.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the target storage directory is accessible but does not already contain game files.
|
||||
/// Only happens when the user changes the storage directory and then moves the files manually or mounts a different device to the same path.
|
||||
/// </summary>
|
||||
AccessibleButEmpty,
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the target storage directory cannot be accessed at all.
|
||||
/// </summary>
|
||||
NotAccessible,
|
||||
}
|
||||
}
|
||||
|
@ -767,7 +767,7 @@ namespace osu.Game
|
||||
Text = "Subsequent messages have been logged. Click to view log files.",
|
||||
Activated = () =>
|
||||
{
|
||||
Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
|
||||
Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
@ -312,11 +312,13 @@ namespace osu.Game
|
||||
base.SetHost(host);
|
||||
|
||||
// may be non-null for certain tests
|
||||
Storage ??= new OsuStorage(host);
|
||||
Storage ??= host.Storage;
|
||||
|
||||
LocalConfig ??= new OsuConfigManager(Storage);
|
||||
}
|
||||
|
||||
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
|
||||
|
||||
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
|
||||
|
||||
public async Task Import(params string[] paths)
|
||||
|
@ -42,25 +42,34 @@ namespace osu.Game.Overlays.Dialog
|
||||
set => icon.Icon = value;
|
||||
}
|
||||
|
||||
private string text;
|
||||
private string headerText;
|
||||
|
||||
public string HeaderText
|
||||
{
|
||||
get => text;
|
||||
get => headerText;
|
||||
set
|
||||
{
|
||||
if (text == value)
|
||||
if (headerText == value)
|
||||
return;
|
||||
|
||||
text = value;
|
||||
|
||||
headerText = value;
|
||||
header.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
private string bodyText;
|
||||
|
||||
public string BodyText
|
||||
{
|
||||
set => body.Text = value;
|
||||
get => bodyText;
|
||||
set
|
||||
{
|
||||
if (bodyText == value)
|
||||
return;
|
||||
|
||||
bodyText = value;
|
||||
body.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<PopupDialogButton> Buttons
|
||||
|
9
osu.Game/Rulesets/Mods/IApplicableToAudio.cs
Normal file
9
osu.Game/Rulesets/Mods/IApplicableToAudio.cs
Normal file
@ -0,0 +1,9 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public interface IApplicableToAudio : IApplicableToTrack, IApplicableToSample
|
||||
{
|
||||
}
|
||||
}
|
15
osu.Game/Rulesets/Mods/IApplicableToSample.cs
Normal file
15
osu.Game/Rulesets/Mods/IApplicableToSample.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// 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.Audio.Sample;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for mods that make adjustments to a sample.
|
||||
/// </summary>
|
||||
public interface IApplicableToSample : IApplicableMod
|
||||
{
|
||||
void ApplyToSample(SampleChannel sample);
|
||||
}
|
||||
}
|
@ -47,9 +47,25 @@ namespace osu.Game.Rulesets.Mods
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Combo.BindTo(scoreProcessor.Combo);
|
||||
|
||||
// Default value of ScoreProcessor's Rank in Flashlight Mod should be SS+
|
||||
scoreProcessor.Rank.Value = ScoreRank.XH;
|
||||
}
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy)
|
||||
{
|
||||
switch (rank)
|
||||
{
|
||||
case ScoreRank.X:
|
||||
return ScoreRank.XH;
|
||||
|
||||
case ScoreRank.S:
|
||||
return ScoreRank.SH;
|
||||
|
||||
default:
|
||||
return rank;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
|
||||
{
|
||||
|
@ -2,12 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModRateAdjust : Mod, IApplicableToTrack
|
||||
public abstract class ModRateAdjust : Mod, IApplicableToAudio
|
||||
{
|
||||
public abstract BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
@ -16,6 +17,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
|
||||
}
|
||||
|
||||
public virtual void ApplyToSample(SampleChannel sample)
|
||||
{
|
||||
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
|
||||
}
|
||||
|
||||
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,11 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Framework.Audio.Sample;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack
|
||||
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio
|
||||
{
|
||||
/// <summary>
|
||||
/// The point in the beatmap at which the final ramping rate should be reached.
|
||||
@ -58,6 +59,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
AdjustPitch.TriggerChange();
|
||||
}
|
||||
|
||||
public void ApplyToSample(SampleChannel sample)
|
||||
{
|
||||
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
|
||||
}
|
||||
|
||||
public virtual void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
HitObject lastObject = beatmap.HitObjects.LastOrDefault();
|
||||
@ -83,9 +89,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
|
||||
{
|
||||
// remove existing old adjustment
|
||||
track.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
|
||||
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
|
||||
|
||||
track.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
|
||||
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
|
||||
}
|
||||
|
||||
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
|
||||
|
@ -12,6 +12,7 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
@ -356,7 +357,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
Bank = bankInfo.Normal,
|
||||
Name = HitSampleInfo.HIT_NORMAL,
|
||||
Volume = bankInfo.Volume,
|
||||
CustomSampleBank = bankInfo.CustomSampleBank
|
||||
CustomSampleBank = bankInfo.CustomSampleBank,
|
||||
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
|
||||
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
|
||||
IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)
|
||||
}
|
||||
};
|
||||
|
||||
@ -409,7 +413,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
|
||||
}
|
||||
|
||||
internal class LegacyHitSampleInfo : HitSampleInfo
|
||||
public class LegacyHitSampleInfo : HitSampleInfo
|
||||
{
|
||||
private int customSampleBank;
|
||||
|
||||
@ -424,6 +428,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
Suffix = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this hit sample is layered.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
|
||||
/// using the <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> skin config option.
|
||||
/// </remarks>
|
||||
public bool IsLayered { get; set; }
|
||||
}
|
||||
|
||||
private class FileHitSampleInfo : LegacyHitSampleInfo
|
||||
|
@ -23,6 +23,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
@ -208,5 +209,14 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
|
||||
public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the statistics for a <see cref="ScoreInfo"/> to be displayed in the results screen.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to create the statistics for. The score is guaranteed to have <see cref="ScoreInfo.HitEvents"/> populated.</param>
|
||||
/// <param name="playableBeatmap">The <see cref="IBeatmap"/>, converted for this <see cref="Ruleset"/> with all relevant <see cref="Mod"/>s applied.</param>
|
||||
/// <returns>The <see cref="StatisticRow"/>s to display. Each <see cref="StatisticRow"/> may contain 0 or more <see cref="StatisticItem"/>.</returns>
|
||||
[NotNull]
|
||||
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticRow>();
|
||||
}
|
||||
}
|
||||
|
66
osu.Game/Rulesets/Scoring/HitEvent.cs
Normal file
66
osu.Game/Rulesets/Scoring/HitEvent.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="HitEvent"/> generated by the <see cref="ScoreProcessor"/> containing extra statistics around a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
public readonly struct HitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The time offset from the end of <see cref="HitObject"/> at which the event occurred.
|
||||
/// </summary>
|
||||
public readonly double TimeOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The hit result.
|
||||
/// </summary>
|
||||
public readonly HitResult Result;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> on which the result occurred.
|
||||
/// </summary>
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> occurring prior to <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly HitObject LastHitObject;
|
||||
|
||||
/// <summary>
|
||||
/// A position, if available, at the time of the event.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly Vector2? Position;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitEvent"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeOffset">The time offset from the end of <paramref name="hitObject"/> at which the event occurs.</param>
|
||||
/// <param name="result">The <see cref="HitResult"/>.</param>
|
||||
/// <param name="hitObject">The <see cref="HitObject"/> that triggered the event.</param>
|
||||
/// <param name="lastHitObject">The previous <see cref="HitObject"/>.</param>
|
||||
/// <param name="position">A position corresponding to the event.</param>
|
||||
public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
|
||||
{
|
||||
TimeOffset = timeOffset;
|
||||
Result = result;
|
||||
HitObject = hitObject;
|
||||
LastHitObject = lastHitObject;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HitEvent"/> with an optional positional offset.
|
||||
/// </summary>
|
||||
/// <param name="positionOffset">The positional offset.</param>
|
||||
/// <returns>The new <see cref="HitEvent"/>.</returns>
|
||||
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private double baseScore;
|
||||
private double bonusScore;
|
||||
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
private HitObject lastHitObject;
|
||||
|
||||
private double scoreMultiplier = 1;
|
||||
|
||||
public ScoreProcessor()
|
||||
@ -128,9 +132,20 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
|
||||
updateScore();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="JudgementResult"/> to describe.</param>
|
||||
/// <returns>The <see cref="HitEvent"/>.</returns>
|
||||
protected virtual HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null);
|
||||
|
||||
protected sealed override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
Combo.Value = result.ComboAtJudgement;
|
||||
@ -153,6 +168,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
hitEvents.RemoveAt(hitEvents.Count - 1);
|
||||
|
||||
updateScore();
|
||||
}
|
||||
|
||||
@ -207,6 +226,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
base.Reset(storeResults);
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
hitEvents.Clear();
|
||||
lastHitObject = null;
|
||||
|
||||
if (storeResults)
|
||||
{
|
||||
@ -247,6 +268,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
|
||||
score.Statistics[result] = GetStatistic(result);
|
||||
|
||||
score.HitEvents = new List<HitEvent>(hitEvents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -18,9 +18,6 @@ using osu.Game.Input.Handlers;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK.Input;
|
||||
using static osu.Game.Input.Handlers.ReplayInputHandler;
|
||||
using JoystickState = osu.Framework.Input.States.JoystickState;
|
||||
using KeyboardState = osu.Framework.Input.States.KeyboardState;
|
||||
using MouseState = osu.Framework.Input.States.MouseState;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -42,11 +39,7 @@ namespace osu.Game.Rulesets.UI
|
||||
}
|
||||
}
|
||||
|
||||
protected override InputState CreateInitialState()
|
||||
{
|
||||
var state = base.CreateInitialState();
|
||||
return new RulesetInputManagerInputState<T>(state.Mouse, state.Keyboard, state.Joystick);
|
||||
}
|
||||
protected override InputState CreateInitialState() => new RulesetInputManagerInputState<T>(base.CreateInitialState());
|
||||
|
||||
protected readonly KeyBindingContainer<T> KeyBindingContainer;
|
||||
|
||||
@ -203,8 +196,8 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public ReplayState<T> LastReplayState;
|
||||
|
||||
public RulesetInputManagerInputState(MouseState mouse = null, KeyboardState keyboard = null, JoystickState joystick = null)
|
||||
: base(mouse, keyboard, joystick)
|
||||
public RulesetInputManagerInputState(InputState state = null)
|
||||
: base(state)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -3,21 +3,26 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
{
|
||||
public class SequentialScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
private readonly Dictionary<double, double> positionCache;
|
||||
private static readonly IComparer<PositionMapping> by_position_comparer = Comparer<PositionMapping>.Create((c1, c2) => c1.Position.CompareTo(c2.Position));
|
||||
|
||||
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Stores a mapping of time -> position for each control point.
|
||||
/// </summary>
|
||||
private readonly List<PositionMapping> positionMappings = new List<PositionMapping>();
|
||||
|
||||
public SequentialScrollAlgorithm(IReadOnlyList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
positionCache = new Dictionary<double, double>();
|
||||
}
|
||||
|
||||
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
|
||||
@ -27,55 +32,31 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
|
||||
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
|
||||
{
|
||||
var objectLength = relativePositionAtCached(endTime, timeRange) - relativePositionAtCached(startTime, timeRange);
|
||||
var objectLength = relativePositionAt(endTime, timeRange) - relativePositionAt(startTime, timeRange);
|
||||
return (float)(objectLength * scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Caching is not used here as currentTime is unlikely to have been previously cached
|
||||
double timelinePosition = relativePositionAt(currentTime, timeRange);
|
||||
return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength);
|
||||
double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
|
||||
return (float)(timelineLength * scrollLength);
|
||||
}
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Convert the position to a length relative to time = 0
|
||||
double length = position / scrollLength + relativePositionAt(currentTime, timeRange);
|
||||
if (controlPoints.Count == 0)
|
||||
return position * timeRange;
|
||||
|
||||
// We need to consider all timing points until the specified time and not just the currently-active one,
|
||||
// since each timing point individually affects the positions of _all_ hitobjects after its start time
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
// Find the position at the current time, and the given length.
|
||||
double relativePosition = relativePositionAt(currentTime, timeRange) + position / scrollLength;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
var positionMapping = findControlPointMapping(timeRange, new PositionMapping(0, null, relativePosition), by_position_comparer);
|
||||
|
||||
// Figure out the length of control point
|
||||
var currentLength = currentDuration / timeRange * current.Multiplier;
|
||||
|
||||
if (currentLength > length)
|
||||
{
|
||||
// The point is within this control point
|
||||
return current.StartTime + length * timeRange / current.Multiplier;
|
||||
}
|
||||
|
||||
length -= currentLength;
|
||||
}
|
||||
|
||||
return 0; // Should never occur
|
||||
// Begin at the control point's time and add the remaining time to reach the given position.
|
||||
return positionMapping.Time + (relativePosition - positionMapping.Position) * timeRange / positionMapping.ControlPoint.Multiplier;
|
||||
}
|
||||
|
||||
private double relativePositionAtCached(double time, double timeRange)
|
||||
{
|
||||
if (!positionCache.TryGetValue(time, out double existing))
|
||||
positionCache[time] = existing = relativePositionAt(time, timeRange);
|
||||
return existing;
|
||||
}
|
||||
|
||||
public void Reset() => positionCache.Clear();
|
||||
public void Reset() => positionMappings.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Finds the position which corresponds to a point in time.
|
||||
@ -84,37 +65,100 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
/// <param name="time">The time to find the position at.</param>
|
||||
/// <param name="timeRange">The amount of time visualised by the scrolling area.</param>
|
||||
/// <returns>A positive value indicating the position at <paramref name="time"/>.</returns>
|
||||
private double relativePositionAt(double time, double timeRange)
|
||||
private double relativePositionAt(in double time, in double timeRange)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return time / timeRange;
|
||||
|
||||
double length = 0;
|
||||
var mapping = findControlPointMapping(timeRange, new PositionMapping(time));
|
||||
|
||||
// We need to consider all timing points until the specified time and not just the currently-active one,
|
||||
// since each timing point individually affects the positions of _all_ hitobjects after its start time
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
// Begin at the control point's position and add the remaining distance to reach the given time.
|
||||
return mapping.Position + (time - mapping.Time) / timeRange * mapping.ControlPoint.Multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a <see cref="MultiplierControlPoint"/>'s <see cref="PositionMapping"/> that is relevant to a given <see cref="PositionMapping"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to find the last <see cref="MultiplierControlPoint"/> occuring prior to a time value, or prior to a position value (if <see cref="by_position_comparer"/> is used).
|
||||
/// </remarks>
|
||||
/// <param name="timeRange">The time range.</param>
|
||||
/// <param name="search">The <see cref="PositionMapping"/> to find the closest <see cref="PositionMapping"/> to.</param>
|
||||
/// <param name="comparer">The comparison. If null, the default comparer is used (by time).</param>
|
||||
/// <returns>The <see cref="MultiplierControlPoint"/>'s <see cref="PositionMapping"/> that is relevant for <paramref name="search"/>.</returns>
|
||||
private PositionMapping findControlPointMapping(in double timeRange, in PositionMapping search, IComparer<PositionMapping> comparer = null)
|
||||
{
|
||||
generatePositionMappings(timeRange);
|
||||
|
||||
var mappingIndex = positionMappings.BinarySearch(search, comparer ?? Comparer<PositionMapping>.Default);
|
||||
|
||||
if (mappingIndex < 0)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
// If the search value isn't found, the _next_ control point is returned, but we actually want the _previous_ control point.
|
||||
// In doing so, we must make sure to not underflow the position mapping list (i.e. always use the 0th control point for time < first_control_point_time).
|
||||
mappingIndex = Math.Max(0, ~mappingIndex - 1);
|
||||
|
||||
// We don't need to consider any control points beyond the current time, since it will not yet
|
||||
// affect any hitobjects
|
||||
if (i > 0 && current.StartTime > time)
|
||||
continue;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
|
||||
// We want to consider the minimal amount of time that this control point has affected,
|
||||
// which may be either its duration, or the amount of time that has passed within it
|
||||
var durationInCurrent = Math.Min(currentDuration, time - current.StartTime);
|
||||
|
||||
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
|
||||
length += durationInCurrent / timeRange * current.Multiplier;
|
||||
Debug.Assert(mappingIndex < positionMappings.Count);
|
||||
}
|
||||
|
||||
return length;
|
||||
var mapping = positionMappings[mappingIndex];
|
||||
Debug.Assert(mapping.ControlPoint != null);
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the mapping of <see cref="MultiplierControlPoint"/> (and their respective start times) to their relative position from 0.
|
||||
/// </summary>
|
||||
/// <param name="timeRange">The time range.</param>
|
||||
private void generatePositionMappings(in double timeRange)
|
||||
{
|
||||
if (positionMappings.Count > 0)
|
||||
return;
|
||||
|
||||
if (controlPoints.Count == 0)
|
||||
return;
|
||||
|
||||
positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
|
||||
|
||||
for (int i = 0; i < controlPoints.Count - 1; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = controlPoints[i + 1];
|
||||
|
||||
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
|
||||
float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
|
||||
|
||||
positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct PositionMapping : IComparable<PositionMapping>
|
||||
{
|
||||
/// <summary>
|
||||
/// The time corresponding to this position.
|
||||
/// </summary>
|
||||
public readonly double Time;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MultiplierControlPoint"/> at <see cref="Time"/>.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly MultiplierControlPoint ControlPoint;
|
||||
|
||||
/// <summary>
|
||||
/// The relative position from 0 of <see cref="ControlPoint"/>.
|
||||
/// </summary>
|
||||
public readonly double Position;
|
||||
|
||||
public PositionMapping(double time, MultiplierControlPoint controlPoint = null, double position = default)
|
||||
{
|
||||
Time = time;
|
||||
ControlPoint = controlPoint;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
public int CompareTo(PositionMapping other) => Time.CompareTo(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,6 +166,10 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
[JsonIgnore]
|
||||
public List<HitEvent> HitEvents { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<ScoreFileInfo> Files { get; set; }
|
||||
|
||||
|
34
osu.Game/Screens/Menu/ConfirmExitDialog.cs
Normal file
34
osu.Game/Screens/Menu/ConfirmExitDialog.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// 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 osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public class ConfirmExitDialog : PopupDialog
|
||||
{
|
||||
public ConfirmExitDialog(Action confirm, Action cancel)
|
||||
{
|
||||
HeaderText = "Are you sure you want to exit?";
|
||||
BodyText = "Last chance to back out.";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Goodbye",
|
||||
Action = confirm
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Just a little more",
|
||||
Action = cancel
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -73,7 +73,6 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
MenuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice);
|
||||
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
|
||||
|
||||
seeya = audio.Samples.Get(SeeyaSampleName);
|
||||
|
||||
BeatmapSetInfo setInfo = null;
|
||||
|
@ -39,6 +39,8 @@ namespace osu.Game.Screens.Menu
|
||||
welcome = audio.Samples.Get(@"Intro/Welcome/welcome");
|
||||
|
||||
pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano");
|
||||
|
||||
Track.Looping = true;
|
||||
}
|
||||
|
||||
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||
|
@ -1,22 +1,21 @@
|
||||
// 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 osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Multi;
|
||||
@ -63,6 +62,8 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => background;
|
||||
|
||||
internal Track Track { get; private set; }
|
||||
|
||||
private Bindable<float> holdDelay;
|
||||
private Bindable<bool> loginDisplayed;
|
||||
|
||||
@ -168,22 +169,28 @@ namespace osu.Game.Screens.Menu
|
||||
return s;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
buttons.FadeInFromZero(500);
|
||||
|
||||
var track = Beatmap.Value.Track;
|
||||
Track = Beatmap.Value.Track;
|
||||
var metadata = Beatmap.Value.Metadata;
|
||||
|
||||
if (last is IntroScreen && track != null)
|
||||
if (last is IntroScreen && Track != null)
|
||||
{
|
||||
if (!track.IsRunning)
|
||||
if (!Track.IsRunning)
|
||||
{
|
||||
track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * track.Length);
|
||||
track.Start();
|
||||
Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length);
|
||||
Track.Start();
|
||||
}
|
||||
}
|
||||
|
||||
if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None)
|
||||
dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error));
|
||||
}
|
||||
|
||||
private bool exitConfirmed;
|
||||
@ -280,30 +287,5 @@ namespace osu.Game.Screens.Menu
|
||||
this.FadeOut(3000);
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
private class ConfirmExitDialog : PopupDialog
|
||||
{
|
||||
public ConfirmExitDialog(Action confirm, Action cancel)
|
||||
{
|
||||
HeaderText = "Are you sure you want to exit?";
|
||||
BodyText = "Last chance to back out.";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Goodbye",
|
||||
Action = confirm
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Just a little more",
|
||||
Action = cancel
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user