mirror of
https://github.com/ppy/osu.git
synced 2026-06-02 19:00:10 +08:00
Merge branch 'beatmap-card-nano' into missing-beatmap
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.SliderVelocityBindable;
|
||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
|
||||
@@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * SliderVelocity;
|
||||
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModDoubleTime : ModTestScene
|
||||
{
|
||||
private const double offset = 18;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModDoubleTime(),
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,11 @@ using System.Linq;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
@@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
|
||||
double beatLength;
|
||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
|
||||
else
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// May be attached to rate-adjustment mods to adjust hit windows adjust relative to gameplay rate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
|
||||
/// </remarks>
|
||||
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
|
||||
{
|
||||
BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
HitWindows HitWindows { get; set; }
|
||||
|
||||
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
HitWindows = new ManiaHitWindows(SpeedChange.Value);
|
||||
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
hitObject.HitWindows = HitWindows;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
hold.Head.HitWindows = HitWindows;
|
||||
hold.Tail.HitWindows = HitWindows;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
// 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.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDaycore : ModDaycore
|
||||
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// 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.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDoubleTime : ModDoubleTime
|
||||
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// 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.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHalfTime : ModHalfTime
|
||||
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
// 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.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public class ManiaHitWindows : HitWindows
|
||||
{
|
||||
private readonly double multiplier;
|
||||
|
||||
public ManiaHitWindows()
|
||||
: this(1)
|
||||
{
|
||||
}
|
||||
|
||||
public ManiaHitWindows(double multiplier)
|
||||
{
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
@@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||
new DifficultyRange(
|
||||
r.Result,
|
||||
r.Min * multiplier,
|
||||
r.Average * multiplier,
|
||||
r.Max * multiplier)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Lookup = lookup;
|
||||
ColumnIndex = columnIndex;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
new PathControlPoint(new Vector2(0, 6.25f))
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
SliderVelocity = 10
|
||||
SliderVelocityMultiplier = 10
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase("basic")]
|
||||
[TestCase("colinear-perfect-curve")]
|
||||
[TestCase("slider-ticks")]
|
||||
[TestCase("slider-ticks-edge-case")]
|
||||
[TestCase("repeat-slider")]
|
||||
[TestCase("uneven-repeat-slider")]
|
||||
[TestCase("old-stacking")]
|
||||
|
||||
@@ -142,6 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
drawableHitCircle.Scale = new Vector2(2f);
|
||||
|
||||
LoadComponent(drawableHitCircle);
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
|
||||
@@ -505,6 +505,195 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addClickActionAssert(0, ClickAction.Shake);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputDoesNotFallThroughOverlappingSliders()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1250;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
||||
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
|
||||
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1600;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
|
||||
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
|
||||
// because stable during replay playback only updates game state _when it encounters a replay frame_
|
||||
new OsuReplayFrame { Time = 1250, Position = midpoint },
|
||||
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
|
||||
{
|
||||
const double time_first_circle = 1000;
|
||||
const double time_second_circle = 1200;
|
||||
Vector2 positionFirstCircle = new Vector2(100);
|
||||
Vector2 positionSecondCircle = new Vector2(120);
|
||||
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle,
|
||||
},
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
|
||||
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
addJudgementOffsetAssert(hitObjects[1], -150);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
|
||||
{
|
||||
const double time_first_circle = 1000;
|
||||
const double time_second_circle = 1200;
|
||||
const double time_third_circle = 1400;
|
||||
Vector2 positionFirstCircle = new Vector2(100);
|
||||
Vector2 positionSecondCircle = new Vector2(200);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_third_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
|
||||
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
|
||||
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
|
||||
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
|
||||
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[1], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[2], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[2], 0);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||
@@ -523,6 +712,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
||||
{
|
||||
AddAssert($"{name} @ judged at {offset}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||
{
|
||||
Default = 4,
|
||||
Value = 4
|
||||
};
|
||||
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new OsuBeatmap();
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set up score multiplier", () =>
|
||||
{
|
||||
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||
});
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddStep("set score with misses and OKs", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
|
||||
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 100;
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||
public void ApplyMiss() => applyHitV1(0);
|
||||
|
||||
private void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
TotalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
private double currentBaseScore;
|
||||
private double maxBaseScore;
|
||||
private int currentHits;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(base_great);
|
||||
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||
|
||||
private void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||
|
||||
currentHits++;
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
{
|
||||
get
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
700000 * comboPortion / comboPortionMax +
|
||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great };
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok };
|
||||
protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = velocity,
|
||||
SliderVelocityMultiplier = velocity,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
||||
@@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = 0.1f,
|
||||
SliderVelocityMultiplier = 0.1f,
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
||||
@@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputFallsThroughJudgedSliders()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1250;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||
@@ -354,6 +400,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||
}
|
||||
|
||||
private void addJudgementOffsetAssert(string name, Func<OsuHitObject> hitObject, double offset)
|
||||
{
|
||||
AddAssert($"{name} @ judged at {offset}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
private List<JudgementResult> judgementResults;
|
||||
|
||||
@@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
SliderVelocity = 0.1f;
|
||||
SliderVelocityMultiplier = 0.1f;
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
||||
@@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BeginPlacement();
|
||||
|
||||
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
||||
|
||||
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
|
||||
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
|
||||
@@ -74,6 +74,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
|
||||
if (ClassicNoteLock.Value)
|
||||
blockInputToObjectsUnderSliderHead(head);
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
@@ -83,10 +87,27 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case DrawableHitCircle circle:
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(circle);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
|
||||
/// until the sliders they're part of have been fully judged.
|
||||
/// The purpose of this method is to restore that behaviour.
|
||||
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
|
||||
/// </summary>
|
||||
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
|
||||
{
|
||||
var oldHitAction = slider.HitArea.Hit;
|
||||
slider.HitArea.Hit = () =>
|
||||
{
|
||||
oldHitAction?.Invoke();
|
||||
return !slider.DrawableSlider.AllJudged;
|
||||
};
|
||||
}
|
||||
|
||||
private void applyEarlyFading(DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
@@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||
};
|
||||
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
|
||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
ExtendedMinValue = -10,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
@@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||
}
|
||||
|
||||
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||
{
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||
new ApproachRateSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar with more detailed approach rate info for its given value
|
||||
/// </summary>
|
||||
public partial class ApproachRateSlider : RoundedSliderBar<float>
|
||||
{
|
||||
public override LocalisableString TooltipText =>
|
||||
(Current as BindableNumber<float>)?.MinValue < 0
|
||||
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
|
||||
: base.TooltipText;
|
||||
|
||||
private double getPreemptTime(float approachRate)
|
||||
{
|
||||
var hitCircle = new HitCircle();
|
||||
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
|
||||
return hitCircle.TimePreempt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||
{
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield over a brief duration.
|
||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
SliderVelocity = original.SliderVelocity;
|
||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
case OsuAction.RightButton:
|
||||
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||
{
|
||||
HitAction = e.Action;
|
||||
HitAction ??= e.Action;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
|
||||
private int wholeSpins;
|
||||
private int completedFullSpins;
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
@@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
int spins = (int)(Result.RateAdjustedRotation / 360);
|
||||
|
||||
if (spins < wholeSpins)
|
||||
if (spins < completedFullSpins)
|
||||
{
|
||||
// rewinding, silently handle
|
||||
wholeSpins = spins;
|
||||
completedFullSpins = spins;
|
||||
return;
|
||||
}
|
||||
|
||||
while (wholeSpins != spins)
|
||||
while (completedFullSpins != spins)
|
||||
{
|
||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||
|
||||
@@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
tick.TriggerResult(true);
|
||||
|
||||
if (tick is DrawableSpinnerBonusTick)
|
||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
|
||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
|
||||
}
|
||||
|
||||
wholeSpins++;
|
||||
completedFullSpins++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public double SpanDuration => Duration / this.SpanCount();
|
||||
|
||||
/// <summary>
|
||||
/// Velocity of this <see cref="Slider"/>.
|
||||
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
|
||||
/// </summary>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
@@ -134,17 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public bool OnlyJudgeNestedObjects = true;
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
@@ -167,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
|
||||
Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME);
|
||||
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
|
||||
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
|
||||
double scoringDistance = Velocity * timingPoint.BeatLength;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public int SpinsRequired { get; protected set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count.
|
||||
/// </summary>
|
||||
public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap;
|
||||
|
||||
/// <summary>
|
||||
/// The gap between spinner completion and the first bonus-awarding spin.
|
||||
/// </summary>
|
||||
private const int bonus_spins_gap = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
|
||||
/// </summary>
|
||||
@@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
|
||||
const double stable_matching_fudge = 0.6;
|
||||
|
||||
// close to 477rpm
|
||||
const double maximum_rotations_per_second = 8;
|
||||
const double maximum_rotations_per_second = 477f / 60f;
|
||||
|
||||
double secondsDuration = Duration / 1000;
|
||||
|
||||
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
|
||||
|
||||
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired;
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
|
||||
|
||||
for (int i = 0; i < totalSpins; i++)
|
||||
{
|
||||
@@ -68,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
AddNested(i < SpinsRequiredForBonus
|
||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Mappings": [
|
||||
{
|
||||
"StartTime": 7493.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 7493.0,
|
||||
"EndTime": 7493.0,
|
||||
"X": 130.0,
|
||||
"Y": 232.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 7817.0,
|
||||
"EndTime": 7817.0,
|
||||
"X": 30.9946651,
|
||||
"Y": 208.5157,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 7843.0,
|
||||
"EndTime": 7843.0,
|
||||
"X": 33.7820168,
|
||||
"Y": 208.9957,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:3.4
|
||||
OverallDifficulty:4
|
||||
ApproachRate:5.5
|
||||
SliderMultiplier:1.1
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"aa.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Layer 4 (Overlay)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
6967,350.877192982456,6,2,1,55,1,0
|
||||
6967,-100,3,2,1,55,0,0
|
||||
7493,-111.111111111111,3,2,1,55,0,0
|
||||
|
||||
[HitObjects]
|
||||
130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0:
|
||||
@@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
if (time - part.Time >= 1)
|
||||
continue;
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
@@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
@@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
@@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
@@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||
public float Time;
|
||||
|
||||
[VertexMember(1, VertexAttribPointerType.Int)]
|
||||
private readonly int maskingIndex;
|
||||
|
||||
public TexturedTrailVertex(IRenderer renderer)
|
||||
{
|
||||
this = default;
|
||||
maskingIndex = renderer.CurrentMaskingIndex;
|
||||
}
|
||||
|
||||
public bool Equals(TexturedTrailVertex other)
|
||||
{
|
||||
return Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition)
|
||||
&& Colour.Equals(other.Colour)
|
||||
&& Time.Equals(other.Time)
|
||||
&& maskingIndex == other.maskingIndex;
|
||||
&& Time.Equals(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||
{
|
||||
Default = 4,
|
||||
Value = 4
|
||||
};
|
||||
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new TaikoBeatmap();
|
||||
for (int i = 0; i < maxCombo; ++i)
|
||||
beatmap.HitObjects.Add(new Hit());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set up score multiplier", () =>
|
||||
{
|
||||
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||
});
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddStep("set score with misses and OKs", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
|
||||
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 150;
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||
|
||||
public void ApplyMiss() => applyHitV1(0);
|
||||
|
||||
private void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
TotalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10);
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
private double currentBaseScore;
|
||||
private double maxBaseScore;
|
||||
private int currentHits;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
|
||||
private const double combo_base = 4;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||
|
||||
private void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
|
||||
currentHits++;
|
||||
|
||||
// `base_great` is INTENTIONALLY used above here instead of `baseScore`
|
||||
// see `BaseHitValue` override in `ScoreChangeTaiko` on stable
|
||||
comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
{
|
||||
get
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
250000 * comboPortion / comboPortionMax +
|
||||
750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great };
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok };
|
||||
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using JetBrains.Annotations;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
@@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||
|
||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||
@@ -186,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
|
||||
|
||||
double beatLength;
|
||||
if (obj.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
|
||||
else
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
|
||||
@@ -1024,10 +1024,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
|
||||
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
|
||||
|
||||
#pragma warning disable 618
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
|
||||
#pragma warning restore 618
|
||||
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
|
||||
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
|
||||
|
||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||
Assert.AreEqual(1, result.Links.Count);
|
||||
Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
|
||||
Assert.AreEqual(0, result.Links.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFakeProtocolLink()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." });
|
||||
|
||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||
Assert.AreEqual(0, result.Links.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSupportedProtocolLinkParsing()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" });
|
||||
|
||||
Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url);
|
||||
Assert.AreEqual("osump://12345", result.Links[1].Url);
|
||||
Assert.AreEqual("osu://chan/#english", result.Links[2].Url);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, false);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
var referenceObject = new Slider
|
||||
{
|
||||
SliderVelocity = slider_velocity
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
|
||||
@@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
||||
void main(void)
|
||||
{
|
||||
// Transform from screen space to masking space.
|
||||
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||
|
||||
v_Colour = m_Colour;
|
||||
|
||||
@@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNano()
|
||||
{
|
||||
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormal()
|
||||
{
|
||||
|
||||
@@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.AutoSizeAxes = Axes.Y;
|
||||
pill.Width = 90;
|
||||
}));
|
||||
|
||||
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeLabels()
|
||||
{
|
||||
AddStep("Change labels", () =>
|
||||
{
|
||||
foreach (var pill in this.ChildrenOfType<BeatmapSetOnlineStatusPill>())
|
||||
{
|
||||
switch (pill.Status)
|
||||
{
|
||||
// cycle at end
|
||||
case BeatmapOnlineStatus.Loved:
|
||||
pill.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
break;
|
||||
|
||||
// skip none
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
pill.Status = BeatmapOnlineStatus.Graveyard;
|
||||
break;
|
||||
|
||||
default:
|
||||
pill.Status = (pill.Status + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
new PathControlPoint(new Vector2(100, 0))
|
||||
}
|
||||
},
|
||||
SliderVelocity = 2
|
||||
SliderVelocityMultiplier = 2
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("unify slider velocity", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
||||
h.SliderVelocity = 1.5;
|
||||
h.SliderVelocityMultiplier = 1.5;
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
@@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
|
||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,499 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneScoring : OsuTestScene
|
||||
{
|
||||
private GraphContainer graphs = null!;
|
||||
private SettingsSlider<int> sliderMaxCombo = null!;
|
||||
|
||||
private FillFlowContainer legend = null!;
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("setup tests", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
graphs = new GraphContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
legend = new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Direction = FillDirection.Full,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
sliderMaxCombo = new SettingsSlider<int>
|
||||
{
|
||||
Width = 0.5f,
|
||||
TransferValueOnCommit = true,
|
||||
Current = new BindableInt(1024)
|
||||
{
|
||||
MinValue = 96,
|
||||
MaxValue = 8192,
|
||||
},
|
||||
LabelText = "max combo",
|
||||
},
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
|
||||
|
||||
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
|
||||
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
|
||||
|
||||
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
|
||||
|
||||
rerun();
|
||||
});
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 100;
|
||||
|
||||
private void rerun()
|
||||
{
|
||||
graphs.Clear();
|
||||
legend.Clear();
|
||||
|
||||
runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised);
|
||||
runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic);
|
||||
|
||||
runScoreV1();
|
||||
runScoreV2();
|
||||
}
|
||||
|
||||
private void runScoreV1()
|
||||
{
|
||||
int totalScore = 0;
|
||||
int currentCombo = 0;
|
||||
|
||||
void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const float score_multiplier = 1;
|
||||
|
||||
totalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
|
||||
() => applyHitV1(base_great),
|
||||
() => applyHitV1(base_ok),
|
||||
() => applyHitV1(0),
|
||||
() =>
|
||||
{
|
||||
// Arbitrary value chosen towards the upper range.
|
||||
const double score_multiplier = 4;
|
||||
|
||||
return (int)(totalScore * score_multiplier);
|
||||
});
|
||||
}
|
||||
|
||||
private void runScoreV2()
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
int currentCombo = 0;
|
||||
double comboPortion = 0;
|
||||
double currentBaseScore = 0;
|
||||
double maxBaseScore = 0;
|
||||
int currentHits = 0;
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
applyHitV2(base_great);
|
||||
|
||||
double comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
|
||||
void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||
|
||||
currentHits++;
|
||||
}
|
||||
|
||||
runForAlgorithm("ScoreV2", Color4.OrangeRed,
|
||||
() => applyHitV2(base_great),
|
||||
() => applyHitV2(base_ok),
|
||||
() =>
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}, () =>
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
700000 * comboPortion / comboPortionMax +
|
||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
var beatmap = new OsuBeatmap();
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
|
||||
processor.ApplyBeatmap(beatmap);
|
||||
|
||||
runForAlgorithm(name, colour,
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
|
||||
() => processor.GetDisplayScore(mode));
|
||||
}
|
||||
|
||||
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
List<float> results = new List<float>();
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
{
|
||||
if (graphs.MissLocations.Contains(i))
|
||||
applyMiss();
|
||||
else if (graphs.NonPerfectLocations.Contains(i))
|
||||
applyNonPerfect();
|
||||
else
|
||||
applyHit();
|
||||
|
||||
results.Add(getTotalScore());
|
||||
}
|
||||
|
||||
graphs.Add(new LineGraph
|
||||
{
|
||||
Name = name,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
LineColour = colour,
|
||||
Values = results
|
||||
});
|
||||
|
||||
legend.Add(new OsuSpriteText
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
|
||||
});
|
||||
|
||||
legend.Add(new OsuSpriteText
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
Text = $"final score {getTotalScore():#,0}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public partial class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
public readonly BindableList<double> MissLocations = new BindableList<double>();
|
||||
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
|
||||
|
||||
public Bindable<int> MaxCombo = new Bindable<int>();
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private readonly Box hoverLine;
|
||||
|
||||
private readonly Container missLines;
|
||||
private readonly Container verticalGridLines;
|
||||
|
||||
public int CurrentHoverCombo { get; private set; }
|
||||
|
||||
public GraphContainer()
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
verticalGridLines = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
hoverLine = new Box
|
||||
{
|
||||
Colour = Color4.Yellow,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.TopCentre,
|
||||
Alpha = 0,
|
||||
Width = 1,
|
||||
},
|
||||
missLines = new Container
|
||||
{
|
||||
Alpha = 0.6f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Content,
|
||||
}
|
||||
};
|
||||
|
||||
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||
|
||||
MaxCombo.BindValueChanged(_ =>
|
||||
{
|
||||
updateMissLocations();
|
||||
updateVerticalGridLines();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateVerticalGridLines()
|
||||
{
|
||||
verticalGridLines.Clear();
|
||||
|
||||
for (int i = 0; i < MaxCombo.Value; i++)
|
||||
{
|
||||
if (i % 100 == 0)
|
||||
{
|
||||
verticalGridLines.AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.2f),
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)i / MaxCombo.Value,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)i / MaxCombo.Value,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = $"{i:#,0}",
|
||||
Rotation = -30,
|
||||
Y = -20,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMissLocations()
|
||||
{
|
||||
missLines.Clear();
|
||||
|
||||
foreach (int miss in MissLocations)
|
||||
{
|
||||
missLines.Add(new Box
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)miss / MaxCombo.Value,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (int miss in NonPerfectLocations)
|
||||
{
|
||||
missLines.Add(new Box
|
||||
{
|
||||
Colour = Color4.Orange,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)miss / MaxCombo.Value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
hoverLine.Show();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hoverLine.Hide();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
|
||||
|
||||
hoverLine.X = e.MousePosition.X;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
MissLocations.Add(CurrentHoverCombo);
|
||||
else
|
||||
NonPerfectLocations.Add(CurrentHoverCombo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private GraphTooltip? tooltip;
|
||||
|
||||
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
|
||||
|
||||
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
|
||||
|
||||
public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
private readonly GraphContainer graphContainer;
|
||||
|
||||
private readonly OsuTextFlowContainer textFlow;
|
||||
|
||||
public GraphTooltip(GraphContainer graphContainer)
|
||||
{
|
||||
this.graphContainer = graphContainer;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.15f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
textFlow = new OsuTextFlowContainer
|
||||
{
|
||||
Colour = Color4.White,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private int? lastContentCombo;
|
||||
|
||||
public void SetContent(IEnumerable<LineGraph> content)
|
||||
{
|
||||
int relevantCombo = graphContainer.CurrentHoverCombo;
|
||||
|
||||
if (lastContentCombo == relevantCombo)
|
||||
return;
|
||||
|
||||
lastContentCombo = relevantCombo;
|
||||
textFlow.Clear();
|
||||
|
||||
textFlow.AddParagraph($"At combo {relevantCombo}:");
|
||||
|
||||
foreach (var graph in content)
|
||||
{
|
||||
float valueAtHover = graph.Values.ElementAt(relevantCombo);
|
||||
float ofTotal = valueAtHover / graph.Values.Last();
|
||||
|
||||
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
|
||||
}
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => this.MoveTo(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("test!");
|
||||
addMessageWithChecks("dev.ppy.sh!");
|
||||
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
|
||||
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
|
||||
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
|
||||
@@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("feels important", 0, true, true);
|
||||
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
|
||||
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
|
||||
|
||||
@@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreviousUsernames()
|
||||
{
|
||||
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
|
||||
{
|
||||
Id = 727,
|
||||
Username = "SomeoneIndecisive",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
|
||||
Groups = new[]
|
||||
{
|
||||
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
|
||||
},
|
||||
Statistics = new UserStatistics
|
||||
{
|
||||
IsRanked = false,
|
||||
// web will sometimes return non-empty rank history even for unranked users.
|
||||
RankHistory = new APIRankHistory
|
||||
{
|
||||
Mode = @"osu",
|
||||
Data = Enumerable.Range(2345, 85).ToArray()
|
||||
},
|
||||
},
|
||||
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
Description = "Outstanding help by being a voluntary test subject.",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png",
|
||||
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png",
|
||||
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
||||
},
|
||||
new Badge
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
Description = "Badge without a url.",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png",
|
||||
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
|
||||
},
|
||||
},
|
||||
Title = "osu!volunteer",
|
||||
|
||||
+12
-3
@@ -5,20 +5,29 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
||||
public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
|
||||
{
|
||||
private PreviousUsernames container = null!;
|
||||
private PreviousUsernamesDisplay container = null!;
|
||||
private OverlayColourProvider colourProvider = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = container = new PreviousUsernames
|
||||
colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
Child = container = new PreviousUsernamesDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
@@ -515,6 +515,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||
|
||||
var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||
firstAdded.Status = BeatmapOnlineStatus.Loved;
|
||||
|
||||
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||
|
||||
@@ -1176,12 +1177,18 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
if (beatmapSets == null)
|
||||
{
|
||||
beatmapSets = new List<BeatmapSetInfo>();
|
||||
var statuses = Enum.GetValues<BeatmapOnlineStatus>()
|
||||
.Except(new[] { BeatmapOnlineStatus.None }) // make sure a badge is always shown.
|
||||
.ToArray();
|
||||
|
||||
for (int i = 1; i <= (setCount ?? set_count); i++)
|
||||
{
|
||||
beatmapSets.Add(randomDifficulties
|
||||
var set = randomDifficulties
|
||||
? TestResources.CreateTestBeatmapSetInfo()
|
||||
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count));
|
||||
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count);
|
||||
set.Status = statuses[RNG.Next(statuses.Length)];
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets = null!;
|
||||
private TestBeatmapInfoWedgeV2 infoWedge = null!;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
// This exists only to make the wedge more visible in the test scene
|
||||
new Box
|
||||
{
|
||||
Y = -20,
|
||||
Colour = Colour4.Cornsilk.Darken(0.2f),
|
||||
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
|
||||
Width = 0.65f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 20, Left = -10 }
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 20 },
|
||||
Child = infoWedge = new TestBeatmapInfoWedgeV2
|
||||
{
|
||||
Width = 0.6f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
|
||||
{
|
||||
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
|
||||
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetChange()
|
||||
{
|
||||
selectBeatmap(Beatmap.Value.Beatmap);
|
||||
|
||||
AddWaitStep("wait for select", 3);
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
{
|
||||
var instance = rulesetInfo.CreateInstance();
|
||||
var testBeatmap = createTestBeatmap(rulesetInfo);
|
||||
|
||||
beatmaps.Add(testBeatmap);
|
||||
|
||||
setRuleset(rulesetInfo);
|
||||
|
||||
selectBeatmap(testBeatmap);
|
||||
|
||||
testBeatmapLabels(instance);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWedgeVisibility()
|
||||
{
|
||||
AddStep("hide", () => { infoWedge.Hide(); });
|
||||
AddWaitStep("wait for hide", 3);
|
||||
AddAssert("check visibility", () => infoWedge.Alpha == 0);
|
||||
AddStep("show", () => { infoWedge.Show(); });
|
||||
AddWaitStep("wait for show", 1);
|
||||
AddAssert("check visibility", () => infoWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
private void testBeatmapLabels(Ruleset ruleset)
|
||||
{
|
||||
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
|
||||
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTruncation()
|
||||
{
|
||||
selectBeatmap(createLongMetadata());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmapWithBackground()
|
||||
{
|
||||
selectBeatmap(null);
|
||||
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep("set ruleset", () =>
|
||||
{
|
||||
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
|
||||
if (!rulesetInfo.Equals(Ruleset.Value))
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
|
||||
Ruleset.Value = rulesetInfo;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private void selectBeatmap(IBeatmap? b)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||
{
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||
infoWedge.Show();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
List<HitObject> objects = new List<HitObject>();
|
||||
for (double i = 0; i < 50000; i += 1000)
|
||||
objects.Add(new TestHitObject { StartTime = i });
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = $"{ruleset.ShortName}Author" },
|
||||
Artist = $"{ruleset.ShortName}Artist",
|
||||
Source = $"{ruleset.ShortName}Source",
|
||||
Title = $"{ruleset.ShortName}Title"
|
||||
},
|
||||
Ruleset = ruleset,
|
||||
StarRating = 6,
|
||||
DifficultyName = $"{ruleset.ShortName}Version",
|
||||
Difficulty = new BeatmapDifficulty()
|
||||
},
|
||||
HitObjects = objects
|
||||
};
|
||||
}
|
||||
|
||||
private IBeatmap createLongMetadata()
|
||||
{
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = "WWWWWWWWWWWWWWW" },
|
||||
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
|
||||
Source = "Verrrrry long Source",
|
||||
Title = "Verrrrry long Title"
|
||||
},
|
||||
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
|
||||
Status = BeatmapOnlineStatus.Graveyard,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
|
||||
{
|
||||
public new Container? DisplayedContent => base.DisplayedContent;
|
||||
public new WedgeInfoText? Info => base.Info;
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public float X => 0;
|
||||
public float Y => 0;
|
||||
public Vector2 Position { get; } = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.FooterV2;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
footer = new FooterV2
|
||||
new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = footer = new FooterV2(),
|
||||
},
|
||||
overlay = new DummyOverlay()
|
||||
};
|
||||
@@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
overlay.Hide();
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowOptions()
|
||||
{
|
||||
AddStep("enable options", () =>
|
||||
{
|
||||
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
|
||||
|
||||
optionsButton.Enabled.Value = true;
|
||||
optionsButton.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestState()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneModEffectPreviewPanel : OsuTestScene
|
||||
{
|
||||
[Cached(typeof(BeatmapDifficultyCache))]
|
||||
private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache();
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private Container content = null!;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private BeatmapAttributesDisplay panel = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
difficultyCache,
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust();
|
||||
OsuModDoubleTime doubleTime = new OsuModDoubleTime();
|
||||
|
||||
AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddStep("set beatmap", () =>
|
||||
{
|
||||
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
BeatmapInfo =
|
||||
{
|
||||
BPM = 120
|
||||
}
|
||||
};
|
||||
|
||||
Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
|
||||
panel.BeatmapInfo.Value = beatmap.BeatmapInfo;
|
||||
});
|
||||
|
||||
AddSliderStep("change star rating", 0, 10d, 5, stars =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
previewStarRating(stars);
|
||||
});
|
||||
AddStep("preview ridiculously high SR", () => previewStarRating(1234));
|
||||
|
||||
AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust });
|
||||
|
||||
AddSliderStep("change AR", 0, 10f, 5, ar =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.ApproachRate.Value = ar;
|
||||
});
|
||||
AddSliderStep("change CS", 0, 10f, 5, cs =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.CircleSize.Value = cs;
|
||||
});
|
||||
AddSliderStep("change HP", 0, 10f, 5, hp =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.DrainRate.Value = hp;
|
||||
});
|
||||
AddSliderStep("change OD", 0, 10f, 5, od =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.OverallDifficulty.Value = od;
|
||||
});
|
||||
|
||||
AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime });
|
||||
AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
doubleTime.SpeedChange.Value = rate;
|
||||
});
|
||||
|
||||
AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed);
|
||||
}
|
||||
|
||||
private void previewStarRating(double stars)
|
||||
{
|
||||
difficultyCache.Difficulty = new StarDifficulty(stars, 0);
|
||||
panel.BeatmapInfo.TriggerChange();
|
||||
}
|
||||
|
||||
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||
{
|
||||
public StarDifficulty? Difficulty { get; set; }
|
||||
|
||||
public override Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Difficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("clear contents", Clear);
|
||||
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
|
||||
AddStep("set up presets", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
@@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Beatmap = Beatmap.Value,
|
||||
SelectedMods = { BindTarget = SelectedMods }
|
||||
});
|
||||
waitForColumnLoad();
|
||||
@@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
@@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
@@ -785,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
||||
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
||||
|
||||
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
|
||||
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
|
||||
@@ -794,7 +796,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
|
||||
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
|
||||
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
|
||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
||||
|
||||
@@ -1,68 +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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneModsEffectDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestModsEffectDisplay()
|
||||
{
|
||||
TestDisplay testDisplay = null!;
|
||||
Box background = null!;
|
||||
|
||||
AddStep("add display", () =>
|
||||
{
|
||||
Add(testDisplay = new TestDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
var boxes = testDisplay.ChildrenOfType<Box>();
|
||||
background = boxes.First();
|
||||
});
|
||||
|
||||
AddStep("set value to default", () => testDisplay.Current.Value = 50);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3);
|
||||
|
||||
AddStep("set value to less", () => testDisplay.Current.Value = 40);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction));
|
||||
|
||||
AddStep("set value to bigger", () => testDisplay.Current.Value = 60);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease));
|
||||
}
|
||||
|
||||
private partial class TestDisplay : ModsEffectDisplay
|
||||
{
|
||||
public Container<Drawable> Container => Content;
|
||||
|
||||
protected override LocalisableString Label => "Test display";
|
||||
|
||||
public TestDisplay()
|
||||
{
|
||||
Current.Default = 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -1,10 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
||||
public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyMultiplierDisplay()
|
||||
public void TestBasic()
|
||||
{
|
||||
DifficultyMultiplierDisplay multiplierDisplay = null;
|
||||
ScoreMultiplierDisplay multiplierDisplay = null!;
|
||||
|
||||
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
@@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||
{
|
||||
if (multiplierDisplay != null)
|
||||
if (multiplierDisplay.IsNotNull())
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
});
|
||||
}
|
||||
@@ -319,7 +319,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
|
||||
|
||||
if (reader is LegacyDirectoryArchiveReader legacyReader)
|
||||
if (reader is DirectoryArchiveReader legacyReader)
|
||||
{
|
||||
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
@@ -23,11 +23,16 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not slider ticks should be generated at this control point.
|
||||
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
|
||||
/// </summary>
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
|
||||
|
||||
/// <summary>
|
||||
@@ -41,11 +46,13 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public override bool IsRedundant(ControlPoint? existing)
|
||||
=> existing is DifficultyControlPoint existingDifficulty
|
||||
&& GenerateTicks == existingDifficulty.GenerateTicks
|
||||
&& SliderVelocity == existingDifficulty.SliderVelocity;
|
||||
|
||||
public override void CopyFrom(ControlPoint other)
|
||||
{
|
||||
SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity;
|
||||
GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks;
|
||||
|
||||
base.CopyFrom(other);
|
||||
}
|
||||
@@ -56,8 +63,10 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public bool Equals(DifficultyControlPoint? other)
|
||||
=> base.Equals(other)
|
||||
&& GenerateTicks == other.GenerateTicks
|
||||
&& SliderVelocity == other.SliderVelocity;
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity);
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
|
||||
{
|
||||
private const double animation_duration = 400;
|
||||
|
||||
private BeatmapOnlineStatus status;
|
||||
|
||||
public BeatmapOnlineStatus Status
|
||||
@@ -32,7 +34,12 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
status = value;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
AutoSizeDuration = (float)animation_duration;
|
||||
AutoSizeEasing = Easing.OutQuint;
|
||||
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +68,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
Masking = true;
|
||||
|
||||
Alpha = 0;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
@@ -83,21 +92,32 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateState();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Alpha = Status == BeatmapOnlineStatus.None ? 0 : 1;
|
||||
if (Status == BeatmapOnlineStatus.None)
|
||||
{
|
||||
this.FadeOut(animation_duration, Easing.OutQuint);
|
||||
return;
|
||||
}
|
||||
|
||||
statusText.Text = Status.GetLocalisableDescription().ToUpper();
|
||||
this.FadeIn(animation_duration, Easing.OutQuint);
|
||||
|
||||
Color4 statusTextColour;
|
||||
|
||||
if (colourProvider != null)
|
||||
statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
||||
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
||||
else
|
||||
statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
|
||||
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
|
||||
|
||||
background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter;
|
||||
statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint);
|
||||
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint);
|
||||
|
||||
statusText.Text = Status.GetLocalisableDescription().ToUpper();
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText
|
||||
|
||||
@@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
switch (size)
|
||||
{
|
||||
case BeatmapCardSize.Nano:
|
||||
return new BeatmapCardNano(beatmapSet);
|
||||
|
||||
case BeatmapCardSize.Normal:
|
||||
return new BeatmapCardNormal(beatmapSet, allowExpansion);
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
public partial class BeatmapCardNano : BeatmapCard
|
||||
{
|
||||
protected override Drawable IdleContent => idleBottomContent;
|
||||
protected override Drawable DownloadInProgressContent => downloadProgressBar;
|
||||
|
||||
private const float height = 60;
|
||||
private const float width = 300;
|
||||
private const float cover_width = 80;
|
||||
|
||||
[Cached]
|
||||
private readonly BeatmapCardContent content;
|
||||
|
||||
private BeatmapCardThumbnail thumbnail = null!;
|
||||
private CollapsibleButtonContainer buttonContainer = null!;
|
||||
|
||||
private FillFlowContainer idleBottomContent = null!;
|
||||
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public BeatmapCardNano(APIBeatmapSet beatmapSet)
|
||||
: base(beatmapSet, false)
|
||||
{
|
||||
content = new BeatmapCardContent(height);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
|
||||
Child = content.With(c =>
|
||||
{
|
||||
c.MainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
thumbnail = new BeatmapCardThumbnail(BeatmapSet)
|
||||
{
|
||||
Name = @"Left (icon) area",
|
||||
Size = new Vector2(cover_width, height),
|
||||
Padding = new MarginPadding { Right = CORNER_RADIUS },
|
||||
},
|
||||
buttonContainer = new CollapsibleButtonContainer(BeatmapSet)
|
||||
{
|
||||
X = cover_width - CORNER_RADIUS,
|
||||
Width = width - cover_width + CORNER_RADIUS,
|
||||
FavouriteState = { BindTarget = FavouriteState },
|
||||
ButtonsCollapsedWidth = CORNER_RADIUS,
|
||||
ButtonsExpandedWidth = 30,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = createArtistText(),
|
||||
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = @"Bottom content",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
idleBottomContent = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 3),
|
||||
AlwaysPresent = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LinkFlowContainer(s =>
|
||||
{
|
||||
s.Shadow = false;
|
||||
s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold);
|
||||
}).With(d =>
|
||||
{
|
||||
d.AutoSizeAxes = Axes.Both;
|
||||
d.Margin = new MarginPadding { Top = 2 };
|
||||
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
|
||||
d.AddUserLink(BeatmapSet.Author);
|
||||
}),
|
||||
}
|
||||
},
|
||||
downloadProgressBar = new BeatmapCardDownloadProgressBar
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 6,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
State = { BindTarget = DownloadTracker.State },
|
||||
Progress = { BindTarget = DownloadTracker.Progress }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
c.ExpandedContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
|
||||
Child = new BeatmapCardDifficultyList(BeatmapSet)
|
||||
};
|
||||
c.Expanded.BindTarget = Expanded;
|
||||
});
|
||||
}
|
||||
|
||||
private LocalisableString createArtistText()
|
||||
{
|
||||
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
|
||||
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
|
||||
}
|
||||
|
||||
protected override void UpdateState()
|
||||
{
|
||||
base.UpdateState();
|
||||
|
||||
bool showDetails = IsHovered;
|
||||
|
||||
buttonContainer.ShowDetails.Value = showDetails;
|
||||
thumbnail.Dimmed.Value = showDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
/// </summary>
|
||||
public enum BeatmapCardSize
|
||||
{
|
||||
Nano,
|
||||
Normal,
|
||||
Extra
|
||||
}
|
||||
|
||||
@@ -36,9 +36,10 @@ namespace osu.Game.Beatmaps
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
DrainRate = 0,
|
||||
CircleSize = 0,
|
||||
DrainRate = 0,
|
||||
OverallDifficulty = 0,
|
||||
ApproachRate = 0,
|
||||
},
|
||||
Ruleset = new DummyRuleset().RulesetInfo
|
||||
}, audio)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#pragma warning disable 618
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -103,15 +101,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;
|
||||
|
||||
if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint)
|
||||
{
|
||||
hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier;
|
||||
if (hitObject is IHasGenerateTicks hasGenerateTicks)
|
||||
hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks;
|
||||
}
|
||||
if (hitObject is IHasGenerateTicks hasGenerateTicks)
|
||||
hasGenerateTicks.GenerateTicks = difficultyControlPoint.GenerateTicks;
|
||||
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
|
||||
hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity;
|
||||
|
||||
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
}
|
||||
@@ -497,8 +491,9 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
|
||||
|
||||
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
|
||||
addControlPoint(time, new DifficultyControlPoint
|
||||
{
|
||||
GenerateTicks = !double.IsNaN(beatLength),
|
||||
SliderVelocity = speedMultiplier,
|
||||
}, timingChange);
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
foreach (var hitObject in hitObjects)
|
||||
{
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity };
|
||||
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocityMultiplier };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,63 +163,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
Mania,
|
||||
}
|
||||
|
||||
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
|
||||
public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable<LegacyDifficultyControlPoint>
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
|
||||
/// DO NOT USE THIS UNLESS 100% SURE.
|
||||
/// </summary>
|
||||
public double BpmMultiplier { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not slider ticks should be generated at this control point.
|
||||
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
|
||||
/// </summary>
|
||||
public bool GenerateTicks { get; private set; } = true;
|
||||
|
||||
public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
|
||||
: this()
|
||||
{
|
||||
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
|
||||
if (rulesetId == 1 || rulesetId == 3)
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
|
||||
else
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
|
||||
|
||||
GenerateTicks = !double.IsNaN(beatLength);
|
||||
}
|
||||
|
||||
public LegacyDifficultyControlPoint()
|
||||
{
|
||||
SliderVelocityBindable.Precision = double.Epsilon;
|
||||
}
|
||||
|
||||
public override bool IsRedundant(ControlPoint? existing)
|
||||
=> base.IsRedundant(existing)
|
||||
&& GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true);
|
||||
|
||||
public override void CopyFrom(ControlPoint other)
|
||||
{
|
||||
base.CopyFrom(other);
|
||||
|
||||
BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
|
||||
GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks;
|
||||
}
|
||||
|
||||
public override bool Equals(ControlPoint? other)
|
||||
=> other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint
|
||||
&& Equals(otherLegacyDifficultyControlPoint);
|
||||
|
||||
public bool Equals(LegacyDifficultyControlPoint? other)
|
||||
=> base.Equals(other)
|
||||
&& BpmMultiplier == other.BpmMultiplier
|
||||
&& GenerateTicks == other.GenerateTicks;
|
||||
|
||||
// ReSharper disable twice NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks);
|
||||
}
|
||||
|
||||
internal class LegacySampleControlPoint : SampleControlPoint, IEquatable<LegacySampleControlPoint>
|
||||
{
|
||||
public int CustomSampleBank;
|
||||
|
||||
@@ -46,9 +46,29 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public ArchiveReader GetReader()
|
||||
{
|
||||
return Stream != null
|
||||
? getReaderFrom(Stream)
|
||||
: getReaderFrom(Path);
|
||||
if (Stream == null)
|
||||
{
|
||||
if (ZipUtils.IsZipArchive(Path))
|
||||
return new ZipArchiveReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(Path));
|
||||
if (Directory.Exists(Path))
|
||||
return new DirectoryArchiveReader(Path);
|
||||
if (File.Exists(Path))
|
||||
return new SingleFileArchiveReader(Path);
|
||||
|
||||
throw new InvalidFormatException($"{Path} is not a valid archive");
|
||||
}
|
||||
|
||||
if (Stream is not MemoryStream memoryStream)
|
||||
{
|
||||
// Path used primarily in tests (converting `ManifestResourceStream`s to `MemoryStream`s).
|
||||
memoryStream = new MemoryStream(Stream.ReadAllBytesToArray());
|
||||
Stream.Dispose();
|
||||
}
|
||||
|
||||
if (ZipUtils.IsZipArchive(memoryStream))
|
||||
return new ZipArchiveReader(memoryStream, Path);
|
||||
|
||||
return new MemoryStreamArchiveReader(memoryStream, Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,43 +80,6 @@ namespace osu.Game.Database
|
||||
File.Delete(Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ArchiveReader"/> from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">A seekable stream containing the archive content.</param>
|
||||
/// <returns>A reader giving access to the archive's content.</returns>
|
||||
private ArchiveReader getReaderFrom(Stream stream)
|
||||
{
|
||||
if (!(stream is MemoryStream memoryStream))
|
||||
{
|
||||
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
|
||||
memoryStream = new MemoryStream(stream.ReadAllBytesToArray());
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
if (ZipUtils.IsZipArchive(memoryStream))
|
||||
return new ZipArchiveReader(memoryStream, Path);
|
||||
|
||||
return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
|
||||
/// </summary>
|
||||
/// <param name="path">A file or folder path resolving the archive content.</param>
|
||||
/// <returns>A reader giving access to the archive's content.</returns>
|
||||
private ArchiveReader getReaderFrom(string path)
|
||||
{
|
||||
if (ZipUtils.IsZipArchive(path))
|
||||
return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path));
|
||||
if (Directory.Exists(path))
|
||||
return new LegacyDirectoryArchiveReader(path);
|
||||
if (File.Exists(path))
|
||||
return new LegacyFileArchiveReader(path);
|
||||
|
||||
throw new InvalidFormatException($"{path} is not a valid archive");
|
||||
}
|
||||
|
||||
public override string ToString() => System.IO.Path.GetFileName(Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedButton : OsuClickableContainer
|
||||
{
|
||||
public const float HEIGHT = 50;
|
||||
public const float CORNER_RADIUS = 7;
|
||||
public const float BORDER_THICKNESS = 2;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
@@ -83,12 +87,10 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </param>
|
||||
public ShearedButton(float? width = null)
|
||||
{
|
||||
Height = 50;
|
||||
Height = HEIGHT;
|
||||
Padding = new MarginPadding { Horizontal = shear * 50 };
|
||||
|
||||
const float corner_radius = 7;
|
||||
|
||||
Content.CornerRadius = corner_radius;
|
||||
Content.CornerRadius = CORNER_RADIUS;
|
||||
Content.Shear = new Vector2(shear, 0);
|
||||
Content.Masking = true;
|
||||
Content.Anchor = Content.Origin = Anchor.Centre;
|
||||
@@ -98,9 +100,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
backgroundLayer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
CornerRadius = corner_radius,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
Masking = true,
|
||||
BorderThickness = 2,
|
||||
BorderThickness = BORDER_THICKNESS,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
|
||||
@@ -30,6 +30,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private const float star_spacing = 4;
|
||||
|
||||
public virtual FillDirection Direction
|
||||
{
|
||||
set => stars.Direction = value;
|
||||
}
|
||||
|
||||
private float current;
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +69,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
stars = new FillFlowContainer<Star>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(star_spacing),
|
||||
ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar())
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ namespace osu.Game.IO.Archives
|
||||
/// <summary>
|
||||
/// Allows reading a single file from the provided byte array.
|
||||
/// </summary>
|
||||
public class LegacyByteArrayReader : ArchiveReader
|
||||
public class ByteArrayArchiveReader : ArchiveReader
|
||||
{
|
||||
private readonly byte[] content;
|
||||
|
||||
public LegacyByteArrayReader(byte[] content, string filename)
|
||||
public ByteArrayArchiveReader(byte[] content, string filename)
|
||||
: base(filename)
|
||||
{
|
||||
this.content = content;
|
||||
+3
-3
@@ -8,13 +8,13 @@ using System.Linq;
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads an archive from a directory on disk.
|
||||
/// Reads an archive directly from a directory on disk.
|
||||
/// </summary>
|
||||
public class LegacyDirectoryArchiveReader : ArchiveReader
|
||||
public class DirectoryArchiveReader : ArchiveReader
|
||||
{
|
||||
private readonly string path;
|
||||
|
||||
public LegacyDirectoryArchiveReader(string path)
|
||||
public DirectoryArchiveReader(string path)
|
||||
: base(Path.GetFileName(path))
|
||||
{
|
||||
// re-get full path to standardise with Directory.GetFiles return values below.
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows reading a single file from the provided memory stream.
|
||||
/// </summary>
|
||||
public class MemoryStreamArchiveReader : ArchiveReader
|
||||
{
|
||||
private readonly MemoryStream stream;
|
||||
|
||||
public MemoryStreamArchiveReader(MemoryStream stream, string filename)
|
||||
: base(filename)
|
||||
{
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public override Stream GetStream(string name) => new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length);
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<string> Filenames => new[] { Name };
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -7,14 +7,14 @@ using System.IO;
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a file on disk as an archive.
|
||||
/// Reads a single file on disk as an archive.
|
||||
/// Note: In this case, the file is not an extractable archive, use <see cref="ZipArchiveReader"/> instead.
|
||||
/// </summary>
|
||||
public class LegacyFileArchiveReader : ArchiveReader
|
||||
public class SingleFileArchiveReader : ArchiveReader
|
||||
{
|
||||
private readonly string path;
|
||||
|
||||
public LegacyFileArchiveReader(string path)
|
||||
public SingleFileArchiveReader(string path)
|
||||
: base(Path.GetFileName(path))
|
||||
{
|
||||
// re-get full path to standardise
|
||||
@@ -169,6 +169,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RevertToDefault => new TranslatableString(getKey(@"revert_to_default"), @"Revert to default");
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString General => new TranslatableString(getKey(@"general"), @"General");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug");
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Show log overlay"
|
||||
/// </summary>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class DifficultyMultiplierDisplayStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.DifficultyMultiplierDisplay";
|
||||
|
||||
/// <summary>
|
||||
/// "Difficulty Multiplier"
|
||||
/// </summary>
|
||||
public static LocalisableString DifficultyMultiplier => new TranslatableString(getKey(@"difficulty_multiplier"), @"Difficulty Multiplier");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Audio"
|
||||
/// </summary>
|
||||
|
||||
@@ -9,11 +9,6 @@ namespace osu.Game.Localisation
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Language"
|
||||
/// </summary>
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
|
||||
|
||||
/// <summary>
|
||||
/// "Score Multiplier"
|
||||
/// </summary>
|
||||
public static LocalisableString ScoreMultiplier => new TranslatableString(getKey(@"score_multiplier"), @"Score Multiplier");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,41 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
|
||||
|
||||
/// <summary>
|
||||
/// "Manage collections"
|
||||
/// </summary>
|
||||
public static LocalisableString ManageCollections => new TranslatableString(getKey(@"manage_collections"), @"Manage collections");
|
||||
|
||||
/// <summary>
|
||||
/// "For all difficulties"
|
||||
/// </summary>
|
||||
public static LocalisableString ForAllDifficulties => new TranslatableString(getKey(@"for_all_difficulties"), @"For all difficulties");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteBeatmap => new TranslatableString(getKey(@"delete_beatmap"), @"Delete beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "For selected difficulty"
|
||||
/// </summary>
|
||||
public static LocalisableString ForSelectedDifficulty => new TranslatableString(getKey(@"for_selected_difficulty"), @"For selected difficulty");
|
||||
|
||||
/// <summary>
|
||||
/// "Mark as played"
|
||||
/// </summary>
|
||||
public static LocalisableString MarkAsPlayed => new TranslatableString(getKey(@"mark_as_played"), @"Mark as played");
|
||||
|
||||
/// <summary>
|
||||
/// "Clear all local scores"
|
||||
/// </summary>
|
||||
public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores");
|
||||
|
||||
/// <summary>
|
||||
/// "Edit beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface");
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Rotate cursor when dragging"
|
||||
/// </summary>
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Online.Chat
|
||||
// http[s]://<domain>.<tld>[:port][/path][?query][#fragment]
|
||||
private static readonly Regex advanced_link_regex = new Regex(
|
||||
// protocol
|
||||
@"(?<link>[a-z]*?:\/\/" +
|
||||
@"(?<link>(https?|osu(mp)?):\/\/" +
|
||||
// domain + tld
|
||||
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" +
|
||||
// port (optional)
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace osu.Game.Online
|
||||
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
|
||||
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
|
||||
APIClientID = "5";
|
||||
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
|
||||
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
|
||||
MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
|
||||
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
|
||||
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
|
||||
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
public BeatmapListingCardSizeTabControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra };
|
||||
}
|
||||
|
||||
protected override bool AddEnumEntriesAutomatically => false;
|
||||
|
||||
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using System.Threading;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// On the mod select overlay, this provides a local updating view of BPM, star rating and other
|
||||
/// difficulty attributes so the user can have a better insight into what mods are changing.
|
||||
/// </summary>
|
||||
public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay
|
||||
{
|
||||
private StarRatingDisplay starRatingDisplay = null!;
|
||||
private BPMDisplay bpmDisplay = null!;
|
||||
|
||||
private VerticalAttributeDisplay circleSizeDisplay = null!;
|
||||
private VerticalAttributeDisplay drainRateDisplay = null!;
|
||||
private VerticalAttributeDisplay approachRateDisplay = null!;
|
||||
private VerticalAttributeDisplay overallDifficultyDisplay = null!;
|
||||
|
||||
public Bindable<IBeatmapInfo?> BeatmapInfo { get; } = new Bindable<IBeatmapInfo?>();
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
public BindableBool Collapsed { get; } = new BindableBool(true);
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationSource;
|
||||
private IBindable<StarDifficulty?> starDifficulty = null!;
|
||||
|
||||
private const float transition_duration = 250;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float shear = ShearedOverlayContainer.SHEAR;
|
||||
|
||||
LeftContent.AddRange(new Drawable[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Shear = new Vector2(-shear, 0),
|
||||
},
|
||||
bpmDisplay = new BPMDisplay
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Shear = new Vector2(-shear, 0),
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 75,
|
||||
}
|
||||
});
|
||||
|
||||
RightContent.Alpha = 0;
|
||||
RightContent.AddRange(new Drawable[]
|
||||
{
|
||||
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), },
|
||||
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), },
|
||||
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
|
||||
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), },
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
mods.BindValueChanged(_ =>
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
|
||||
modSettingChangeTracker.SettingChanged += _ => updateValues();
|
||||
updateValues();
|
||||
}, true);
|
||||
|
||||
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
|
||||
|
||||
Collapsed.BindValueChanged(_ =>
|
||||
{
|
||||
// Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation.
|
||||
startAnimating();
|
||||
updateCollapsedState();
|
||||
});
|
||||
|
||||
updateCollapsedState();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
startAnimating();
|
||||
updateCollapsedState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateCollapsedState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
private void startAnimating()
|
||||
{
|
||||
Content.AutoSizeEasing = Easing.OutQuint;
|
||||
Content.AutoSizeDuration = transition_duration;
|
||||
}
|
||||
|
||||
private void updateCollapsedState()
|
||||
{
|
||||
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateValues() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
if (BeatmapInfo.Value == null)
|
||||
return;
|
||||
|
||||
cancellationSource?.Cancel();
|
||||
|
||||
starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token);
|
||||
starDifficulty.BindValueChanged(s =>
|
||||
{
|
||||
starRatingDisplay.Current.Value = s.NewValue ?? default;
|
||||
|
||||
if (!starRatingDisplay.IsPresent)
|
||||
starRatingDisplay.FinishTransforms(true);
|
||||
});
|
||||
|
||||
double rate = 1;
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
|
||||
rate = mod.ApplyToRate(0, rate);
|
||||
|
||||
bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate;
|
||||
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
|
||||
mod.ApplyToDifficulty(adjustedDifficulty);
|
||||
|
||||
circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize;
|
||||
drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate;
|
||||
approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate;
|
||||
overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty;
|
||||
});
|
||||
|
||||
private partial class BPMDisplay : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM");
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Font = OsuFont.Default.With(size: 20, weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public sealed partial class DifficultyMultiplierDisplay : ModsEffectDisplay
|
||||
{
|
||||
protected override LocalisableString Label => DifficultyMultiplierDisplayStrings.DifficultyMultiplier;
|
||||
|
||||
protected override string CounterFormat => @"N2";
|
||||
|
||||
public DifficultyMultiplierDisplay()
|
||||
{
|
||||
Current.Default = 1d;
|
||||
Current.Value = 1d;
|
||||
Add(new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Icon = FontAwesome.Solid.Times,
|
||||
Size = new Vector2(7),
|
||||
Margin = new MarginPadding { Top = 1 }
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// required to prevent the counter initially rolling up from 0 to 1
|
||||
// due to `Current.Value` having a nonstandard default value of 1.
|
||||
Counter.SetCountWithoutRolling(Current.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public abstract partial class ModFooterInformationDisplay : CompositeDrawable
|
||||
{
|
||||
protected FillFlowContainer LeftContent { get; private set; } = null!;
|
||||
protected FillFlowContainer RightContent { get; private set; } = null!;
|
||||
protected Container Content { get; private set; } = null!;
|
||||
|
||||
private Container innerContent = null!;
|
||||
|
||||
protected Box MainBackground { get; private set; } = null!;
|
||||
protected Box FrontBackground { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = Content = new Container
|
||||
{
|
||||
Origin = Anchor.BottomRight,
|
||||
Anchor = Anchor.BottomRight,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = ShearedButton.HEIGHT,
|
||||
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
|
||||
CornerRadius = ShearedButton.CORNER_RADIUS,
|
||||
BorderThickness = ShearedButton.BORDER_THICKNESS,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
MainBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new FillFlowContainer // divide inner and outer content
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
innerContent = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
BorderThickness = ShearedButton.BORDER_THICKNESS,
|
||||
CornerRadius = ShearedButton.CORNER_RADIUS,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
FrontBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
LeftContent = new FillFlowContainer // actual inner content
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Horizontal = 15 },
|
||||
Spacing = new Vector2(10),
|
||||
}
|
||||
}
|
||||
},
|
||||
RightContent = new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
MainBackground.Colour = ColourProvider.Background4;
|
||||
FrontBackground.Colour = ColourProvider.Background3;
|
||||
Color4 glowColour = ColourProvider.Background1;
|
||||
|
||||
Content.BorderColour = ColourInfo.GradientVertical(MainBackground.Colour, glowColour);
|
||||
innerContent.BorderColour = ColourInfo.GradientVertical(FrontBackground.Colour, glowColour);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private const float contracted_width = WIDTH - 120;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@@ -42,6 +44,8 @@ namespace osu.Game.Overlays.Mods
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => rulesetChanged(), true);
|
||||
|
||||
Width = contracted_width;
|
||||
}
|
||||
|
||||
private IDisposable? presetSubscription;
|
||||
@@ -65,7 +69,11 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
|
||||
if (!presets.Any())
|
||||
bool hasPresets = presets.Any();
|
||||
|
||||
this.ResizeWidthTo(hasPresets ? WIDTH : contracted_width, 200, Easing.OutQuint);
|
||||
|
||||
if (!hasPresets)
|
||||
{
|
||||
removeAndDisposePresetPanels();
|
||||
return;
|
||||
|
||||
@@ -61,9 +61,11 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
private const float header_height = 42;
|
||||
|
||||
protected const float WIDTH = 320;
|
||||
|
||||
protected ModSelectColumn()
|
||||
{
|
||||
Width = 320;
|
||||
Width = WIDTH;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -77,9 +78,9 @@ namespace osu.Game.Overlays.Mods
|
||||
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
|
||||
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
|
||||
/// </summary>
|
||||
protected virtual bool ShowTotalMultiplier => true;
|
||||
protected virtual bool ShowModEffects => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether per-mod customisation controls are visible.
|
||||
@@ -119,10 +120,12 @@ namespace osu.Game.Overlays.Mods
|
||||
private ColumnScrollContainer columnScroll = null!;
|
||||
private ColumnFlowContainer columnFlow = null!;
|
||||
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
|
||||
private FillFlowContainer footerContentFlow = null!;
|
||||
private DeselectAllModsButton deselectAllModsButton = null!;
|
||||
|
||||
private Container aboveColumnsContent = null!;
|
||||
private DifficultyMultiplierDisplay? multiplierDisplay;
|
||||
private ScoreMultiplierDisplay? multiplierDisplay;
|
||||
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
|
||||
|
||||
protected ShearedButton BackButton { get; private set; } = null!;
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
@@ -130,6 +133,21 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
private WorkingBeatmap? beatmap;
|
||||
|
||||
public WorkingBeatmap? Beatmap
|
||||
{
|
||||
get => beatmap;
|
||||
set
|
||||
{
|
||||
if (beatmap == value) return;
|
||||
|
||||
beatmap = value;
|
||||
if (IsLoaded && beatmapAttributesDisplay != null)
|
||||
beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
|
||||
: base(colourScheme)
|
||||
{
|
||||
@@ -164,7 +182,7 @@ namespace osu.Game.Overlays.Mods
|
||||
aboveColumnsContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = ModsEffectDisplay.HEIGHT,
|
||||
Height = ScoreMultiplierDisplay.HEIGHT,
|
||||
Padding = new MarginPadding { Horizontal = 100 },
|
||||
Child = SearchTextBox = new ShearedSearchTextBox
|
||||
{
|
||||
@@ -179,7 +197,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = ModsEffectDisplay.HEIGHT + PADDING,
|
||||
Top = ScoreMultiplierDisplay.HEIGHT + PADDING,
|
||||
Bottom = PADDING
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -210,16 +228,7 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
});
|
||||
|
||||
if (ShowTotalMultiplier)
|
||||
{
|
||||
aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight
|
||||
});
|
||||
}
|
||||
|
||||
FooterContent.Child = footerButtonFlow = new FillFlowContainer<ShearedButton>
|
||||
FooterContent.Add(footerButtonFlow = new FillFlowContainer<ShearedButton>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@@ -239,7 +248,38 @@ namespace osu.Game.Overlays.Mods
|
||||
DarkerColour = colours.Pink2,
|
||||
LighterColour = colours.Pink1
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
if (ShowModEffects)
|
||||
{
|
||||
FooterContent.Add(footerContentFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(30, 10),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Vertical = PADDING,
|
||||
Horizontal = 20
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
multiplierDisplay = new ScoreMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight
|
||||
},
|
||||
beatmapAttributesDisplay = new BeatmapAttributesDisplay
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
BeatmapInfo = { Value = beatmap?.BeatmapInfo }
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
globalAvailableMods.BindTo(game.AvailableMods);
|
||||
}
|
||||
@@ -309,6 +349,25 @@ namespace osu.Game.Overlays.Mods
|
||||
base.Update();
|
||||
|
||||
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
|
||||
|
||||
if (beatmapAttributesDisplay != null)
|
||||
{
|
||||
float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X;
|
||||
|
||||
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
|
||||
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
|
||||
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X;
|
||||
|
||||
bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
|
||||
|
||||
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
|
||||
if (Alpha == 1)
|
||||
beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough;
|
||||
|
||||
footerContentFlow.LayoutDuration = 200;
|
||||
footerContentFlow.LayoutEasing = Easing.OutQuint;
|
||||
footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -886,6 +945,9 @@ namespace osu.Game.Overlays.Mods
|
||||
OnClicked?.Invoke();
|
||||
return true;
|
||||
|
||||
case HoverEvent:
|
||||
return false;
|
||||
|
||||
case MouseEvent:
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,223 +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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for displays of mods effects.
|
||||
/// </summary>
|
||||
public abstract partial class ModsEffectDisplay : Container, IHasCurrentValue<double>
|
||||
{
|
||||
public const float HEIGHT = 42;
|
||||
private const float transition_duration = 200;
|
||||
|
||||
private readonly Box contentBackground;
|
||||
private readonly Box labelBackground;
|
||||
private readonly FillFlowContainer content;
|
||||
|
||||
public Bindable<double> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Text to display in the left area of the display.
|
||||
/// </summary>
|
||||
protected abstract LocalisableString Label { get; }
|
||||
|
||||
protected virtual float ValueAreaWidth => 56;
|
||||
|
||||
protected virtual string CounterFormat => @"N0";
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
protected readonly RollingCounter<double> Counter;
|
||||
|
||||
protected ModsEffectDisplay()
|
||||
{
|
||||
Height = HEIGHT;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
InternalChild = new InputBlockingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Masking = true,
|
||||
CornerRadius = ModSelectPanel.CORNER_RADIUS,
|
||||
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
contentBackground = new Box
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = ValueAreaWidth + ModSelectPanel.CORNER_RADIUS
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, ValueAreaWidth)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Masking = true,
|
||||
CornerRadius = ModSelectPanel.CORNER_RADIUS,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
labelBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Horizontal = 18 },
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Text = Label,
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Spacing = new Vector2(2, 0),
|
||||
Child = Counter = new EffectCounter(CounterFormat)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = { BindTarget = Current }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
labelBackground.Colour = colourProvider.Background4;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
Current.BindValueChanged(e =>
|
||||
{
|
||||
var effect = CalculateEffectForComparison(e.NewValue.CompareTo(Current.Default));
|
||||
setColours(effect);
|
||||
}, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades colours of text and its background according to displayed value.
|
||||
/// </summary>
|
||||
/// <param name="effect">Effect of the value.</param>
|
||||
private void setColours(ModEffect effect)
|
||||
{
|
||||
switch (effect)
|
||||
{
|
||||
case ModEffect.NotChanged:
|
||||
contentBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint);
|
||||
content.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case ModEffect.DifficultyReduction:
|
||||
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
|
||||
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case ModEffect.DifficultyIncrease:
|
||||
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
|
||||
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(effect));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts signed integer into <see cref="ModEffect"/>. Negative values are counted as difficulty reduction, positive as increase.
|
||||
/// </summary>
|
||||
/// <param name="comparison">Value to convert. Will arrive from comparison between <see cref="Current"/> bindable once it changes and it's <see cref="Bindable{T}.Default"/>.</param>
|
||||
/// <returns>Effect of the value.</returns>
|
||||
protected virtual ModEffect CalculateEffectForComparison(int comparison)
|
||||
{
|
||||
if (comparison == 0)
|
||||
return ModEffect.NotChanged;
|
||||
if (comparison < 0)
|
||||
return ModEffect.DifficultyReduction;
|
||||
|
||||
return ModEffect.DifficultyIncrease;
|
||||
}
|
||||
|
||||
protected enum ModEffect
|
||||
{
|
||||
NotChanged,
|
||||
DifficultyReduction,
|
||||
DifficultyIncrease
|
||||
}
|
||||
|
||||
private partial class EffectCounter : RollingCounter<double>
|
||||
{
|
||||
private readonly string? format;
|
||||
|
||||
public EffectCounter(string? format)
|
||||
{
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(format);
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// On the mod select overlay, this provides a local updating view of the aggregate score multiplier coming from mods.
|
||||
/// </summary>
|
||||
public partial class ScoreMultiplierDisplay : ModFooterInformationDisplay, IHasCurrentValue<double>
|
||||
{
|
||||
public const float HEIGHT = 42;
|
||||
|
||||
public Bindable<double> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
|
||||
|
||||
private const float transition_duration = 200;
|
||||
|
||||
private RollingCounter<double> counter = null!;
|
||||
|
||||
private Box flashLayer = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public ScoreMultiplierDisplay()
|
||||
{
|
||||
Current.Default = 1d;
|
||||
Current.Value = 1d;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// You would think that we could add this to `Content`, but borders don't mix well
|
||||
// with additive blending children elements.
|
||||
AddInternal(new Container
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
|
||||
CornerRadius = ShearedButton.CORNER_RADIUS,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
flashLayer = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LeftContent.AddRange(new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Text = ModSelectOverlayStrings.ScoreMultiplier,
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
}
|
||||
});
|
||||
|
||||
RightContent.Add(new Container
|
||||
{
|
||||
Width = 40,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding(10),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = counter = new EffectCounter
|
||||
{
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Current = { BindTarget = Current }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(e =>
|
||||
{
|
||||
if (e.NewValue > Current.Default)
|
||||
{
|
||||
MainBackground
|
||||
.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
|
||||
counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
else if (e.NewValue < Current.Default)
|
||||
{
|
||||
MainBackground
|
||||
.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
|
||||
counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
MainBackground.FadeColour(ColourProvider.Background4, transition_duration, Easing.OutQuint);
|
||||
counter.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
flashLayer
|
||||
.FadeOutFromOne()
|
||||
.FadeTo(0.15f, 60, Easing.OutQuint)
|
||||
.Then().FadeOut(500, Easing.OutQuint);
|
||||
|
||||
const float move_amount = 4;
|
||||
if (e.NewValue > e.OldValue)
|
||||
counter.MoveToY(Math.Max(-move_amount * 2, counter.Y - move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint);
|
||||
else
|
||||
counter.MoveToY(Math.Min(move_amount * 2, counter.Y + move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint);
|
||||
}, true);
|
||||
|
||||
// required to prevent the counter initially rolling up from 0 to 1
|
||||
// due to `Current.Value` having a nonstandard default value of 1.
|
||||
counter.SetCountWithoutRolling(Current.Value);
|
||||
}
|
||||
|
||||
private partial class EffectCounter : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"0.00x");
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class VerticalAttributeDisplay : Container, IHasCurrentValue<double>
|
||||
{
|
||||
public Bindable<double> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
|
||||
|
||||
/// <summary>
|
||||
/// Text to display in the top area of the display.
|
||||
/// </summary>
|
||||
public LocalisableString Label { get; protected set; }
|
||||
|
||||
public VerticalAttributeDisplay(LocalisableString label)
|
||||
{
|
||||
Label = label;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Origin = Anchor.CentreLeft;
|
||||
Anchor = Anchor.CentreLeft;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Text = Label,
|
||||
Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value
|
||||
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold)
|
||||
},
|
||||
new EffectCounter
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Current = { BindTarget = Current },
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private partial class EffectCounter : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0");
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 18, weight: FontWeight.SemiBold)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD
|
||||
public partial class CopyUrlToast : Toast
|
||||
{
|
||||
public CopyUrlToast()
|
||||
: base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
|
||||
: base(CommonStrings.General, ToastStrings.UrlCopied, "")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container // artificial shadow
|
||||
{
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(10, 10),
|
||||
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 },
|
||||
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+14
-9
@@ -18,12 +18,13 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public partial class PreviousUsernames : CompositeDrawable
|
||||
public partial class PreviousUsernamesDisplay : CompositeDrawable
|
||||
{
|
||||
private const int duration = 200;
|
||||
private const int margin = 10;
|
||||
private const int width = 310;
|
||||
private const int width = 300;
|
||||
private const int move_offset = 15;
|
||||
private const int base_y_offset = -3; // eye balled to make it look good
|
||||
|
||||
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
|
||||
|
||||
@@ -31,14 +32,15 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
private readonly Box background;
|
||||
private readonly SpriteText header;
|
||||
|
||||
public PreviousUsernames()
|
||||
public PreviousUsernamesDisplay()
|
||||
{
|
||||
HoverIconContainer hoverIcon;
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Width = width;
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
CornerRadius = 6;
|
||||
Y = base_y_offset;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
@@ -84,6 +86,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
// Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out.
|
||||
// Also prevents a potential OnHover/HoverLost feedback loop.
|
||||
AlwaysPresent = true,
|
||||
Margin = new MarginPadding { Bottom = margin, Top = margin / 2f }
|
||||
}
|
||||
}
|
||||
@@ -96,9 +101,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OverlayColourProvider colours)
|
||||
{
|
||||
background.Colour = colours.GreySeaFoamDarker;
|
||||
background.Colour = colours.Background6;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -134,7 +139,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
text.FadeIn(duration, Easing.OutQuint);
|
||||
header.FadeIn(duration, Easing.OutQuint);
|
||||
background.FadeIn(duration, Easing.OutQuint);
|
||||
this.MoveToY(-move_offset, duration, Easing.OutQuint);
|
||||
this.MoveToY(base_y_offset - move_offset, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void hideContent()
|
||||
@@ -142,7 +147,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
text.FadeOut(duration, Easing.OutQuint);
|
||||
header.FadeOut(duration, Easing.OutQuint);
|
||||
background.FadeOut(duration, Easing.OutQuint);
|
||||
this.MoveToY(0, duration, Easing.OutQuint);
|
||||
this.MoveToY(base_y_offset, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private partial class HoverIconContainer : Container
|
||||
@@ -156,7 +161,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 },
|
||||
Size = new Vector2(15),
|
||||
Icon = FontAwesome.Solid.IdCard,
|
||||
Icon = FontAwesome.Solid.AddressCard,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
private OsuSpriteText userCountryText = null!;
|
||||
private GroupBadgeFlow groupBadgeFlow = null!;
|
||||
private ToggleCoverButton coverToggle = null!;
|
||||
private PreviousUsernamesDisplay previousUsernamesDisplay = null!;
|
||||
|
||||
private Bindable<bool> coverExpanded = null!;
|
||||
|
||||
@@ -64,7 +65,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
@@ -143,6 +144,11 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
// Intentionally use a zero-size container, else the fill flow will adjust to (and cancel) the upwards animation.
|
||||
Child = previousUsernamesDisplay = new PreviousUsernamesDisplay(),
|
||||
}
|
||||
}
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
@@ -216,6 +222,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
titleText.Text = user?.Title ?? string.Empty;
|
||||
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
|
||||
groupBadgeFlow.User.Value = user;
|
||||
previousUsernamesDisplay.User.Value = user;
|
||||
}
|
||||
|
||||
private void updateCoverState()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public partial class MultiplierSettingsSlider : SettingsSlider<double, MultiplierSettingsSlider.MultiplierRoundedSliderBar>
|
||||
{
|
||||
public MultiplierSettingsSlider()
|
||||
{
|
||||
KeyboardStep = 0.01f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar which adds a "x" to the end of the tooltip string.
|
||||
/// </summary>
|
||||
public partial class MultiplierRoundedSliderBar : RoundedSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => $"{base.TooltipText}x";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
||||
{
|
||||
public partial class GeneralSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader;
|
||||
protected override LocalisableString Header => CommonStrings.General;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user