mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 15:40:14 +08:00
Compare commits
51 Commits
@@ -11,7 +11,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneCatchModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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 TestSceneManiaModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
@@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
EndTime = 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 TestSceneManiaModSuddenDeath : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
public TestSceneManiaModSuddenDeath()
|
||||
: base(new ManiaModSuddenDeath())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
EndTime = 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModPerfect : ModPerfect
|
||||
{
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
||||
return false;
|
||||
|
||||
// Mania allows imperfect "Great" hits without failing.
|
||||
if (result.Judgement.MaxResult == HitResult.Perfect)
|
||||
return result.Type < HitResult.Great;
|
||||
|
||||
return result.Type != result.Judgement.MaxResult;
|
||||
}
|
||||
|
||||
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
@@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissSliderTail() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModStrictTracking : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestSliderInput() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModStrictTracking(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(501, new Vector2(200, 0)),
|
||||
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1751, new Vector2(0, 100)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
public TestSceneOsuModSuddenDeath()
|
||||
: base(new OsuModSuddenDeath())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissTail() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestMissTick() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (!slider.HeadCircle.IsHit)
|
||||
handleHitCircle(slider.HeadCircle);
|
||||
|
||||
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
|
||||
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
|
||||
break;
|
||||
|
||||
case DrawableSpinner spinner:
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (e.NewValue || slider.Judged) return;
|
||||
|
||||
if (slider.Time.Current < slider.HitObject.StartTime)
|
||||
return;
|
||||
|
||||
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
|
||||
|
||||
if (!tail.Judged)
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateTracking(isMouseInFollowArea(Tracking));
|
||||
updateTracking(IsMouseInFollowArea(Tracking));
|
||||
}
|
||||
|
||||
public void PostProcessHeadJudgement(DrawableSliderHead head)
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (!head.Judged || !head.Result.IsHit)
|
||||
return;
|
||||
|
||||
if (!isMouseInFollowArea(true))
|
||||
if (!IsMouseInFollowArea(true))
|
||||
return;
|
||||
|
||||
Debug.Assert(screenSpaceMousePosition != null);
|
||||
@@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// If all ticks were hit so far, enable tracking the full extent.
|
||||
// If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball.
|
||||
// For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update().
|
||||
updateTracking(allTicksInRange || isMouseInFollowArea(false));
|
||||
updateTracking(allTicksInRange || IsMouseInFollowArea(false));
|
||||
}
|
||||
|
||||
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
|
||||
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// Whether the mouse is currently in the follow area.
|
||||
/// </summary>
|
||||
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
|
||||
private bool isMouseInFollowArea(bool expanded)
|
||||
public bool IsMouseInFollowArea(bool expanded)
|
||||
{
|
||||
if (screenSpaceMousePosition is not Vector2 pos)
|
||||
return false;
|
||||
|
||||
@@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||
|
||||
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime);
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
|
||||
|
||||
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public partial class OsuHealthProcessor : DrainingHealthProcessor
|
||||
{
|
||||
private ComboResult currentComboResult = ComboResult.Perfect;
|
||||
|
||||
public OsuHealthProcessor(double drainStartTime, double drainLenience = 0)
|
||||
: base(drainStartTime, drainLenience)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(JudgementResult result)
|
||||
{
|
||||
if (IsSimulating)
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
if (result.HitObject is not IHasComboInformation combo)
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
if (combo.NewCombo)
|
||||
currentComboResult = ComboResult.Perfect;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Ok:
|
||||
setComboResult(ComboResult.Good);
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
case HitResult.Miss:
|
||||
setComboResult(ComboResult.None);
|
||||
break;
|
||||
}
|
||||
|
||||
// The slider tail has a special judgement that can't accurately be described above.
|
||||
if (result.HitObject is SliderTailCircle && !result.IsHit)
|
||||
setComboResult(ComboResult.Good);
|
||||
|
||||
if (combo.LastInCombo && result.Type.IsHit())
|
||||
{
|
||||
switch (currentComboResult)
|
||||
{
|
||||
case ComboResult.Perfect:
|
||||
return getHealthIncreaseFor(result) + 0.07;
|
||||
|
||||
case ComboResult.Good:
|
||||
return getHealthIncreaseFor(result) + 0.05;
|
||||
|
||||
default:
|
||||
return getHealthIncreaseFor(result) + 0.03;
|
||||
}
|
||||
}
|
||||
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
void setComboResult(ComboResult comboResult) => currentComboResult = (ComboResult)Math.Min((int)currentComboResult, (int)comboResult);
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
currentComboResult = ComboResult.Perfect;
|
||||
}
|
||||
|
||||
private double getHealthIncreaseFor(JudgementResult result)
|
||||
{
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
|
||||
|
||||
case HitResult.Miss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
// When classic slider mechanics are enabled, this result comes from the tail.
|
||||
return 0.02;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
switch (result.HitObject)
|
||||
{
|
||||
case SliderTick:
|
||||
return 0.015;
|
||||
|
||||
case SliderHeadCircle:
|
||||
case SliderTailCircle:
|
||||
case SliderRepeat:
|
||||
return 0.02;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 0.002;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 0.011;
|
||||
|
||||
case HitResult.Great:
|
||||
return 0.03;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return 0.0085;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
return base.GetHealthIncreaseFor(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
decimal? legacyVersion = skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
if (legacyVersion > 1.0m)
|
||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||
hitCircleText.FadeOut(legacy_fade_duration / 4);
|
||||
else
|
||||
|
||||
@@ -10,7 +10,7 @@ using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset();
|
||||
|
||||
|
||||
@@ -81,6 +81,21 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWasPlayingBeforeWatchingUserPresence()
|
||||
{
|
||||
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
|
||||
AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence());
|
||||
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
|
||||
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
|
||||
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
|
||||
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
|
||||
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
|
||||
AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence());
|
||||
}
|
||||
|
||||
internal partial class TestUserLookupCache : UserLookupCache
|
||||
{
|
||||
private static readonly string[] usernames =
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -194,6 +196,36 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLengthUpdates()
|
||||
{
|
||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
double drain = beatmap.CalculateDrainLength();
|
||||
beatmap.BeatmapInfo.Length = drain;
|
||||
|
||||
OsuModDoubleTime doubleTime = null;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedLength(drain);
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||
checkDisplayedLength(Math.Round(drain / 1.5f));
|
||||
|
||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||
checkDisplayedLength(Math.Round(drain / 2));
|
||||
}
|
||||
|
||||
private void checkDisplayedLength(double drain)
|
||||
{
|
||||
var displayedLength = drain.ToFormattedDuration();
|
||||
|
||||
AddUntilStep($"check map drain ({displayedLength})", () =>
|
||||
{
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength));
|
||||
return label.Statistic.Content == displayedLength.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
Container containerBefore = null;
|
||||
|
||||
@@ -312,8 +312,12 @@ namespace osu.Game.Database
|
||||
|
||||
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
double comboProportion =
|
||||
((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore);
|
||||
// Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0
|
||||
// when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot).
|
||||
// In such cases, just assume 0.
|
||||
double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0
|
||||
? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore)
|
||||
: 0;
|
||||
|
||||
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
@@ -321,6 +325,8 @@ namespace osu.Game.Database
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
|
||||
long convertedTotalScore;
|
||||
|
||||
switch (score.Ruleset.OnlineID)
|
||||
{
|
||||
case 0:
|
||||
@@ -417,32 +423,42 @@ namespace osu.Game.Database
|
||||
|
||||
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
|
||||
|
||||
return (long)Math.Round((
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
500000 * newComboScoreProportion * score.Accuracy
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
return (long)Math.Round((
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
250000 * comboProportion
|
||||
+ 750000 * Math.Pow(score.Accuracy, 3.6)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
return (long)Math.Round((
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
600000 * comboProportion
|
||||
+ 400000 * score.Accuracy
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
return (long)Math.Round((
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
850000 * comboProportion
|
||||
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
default:
|
||||
return score.TotalScore;
|
||||
convertedTotalScore = score.TotalScore;
|
||||
break;
|
||||
}
|
||||
|
||||
if (convertedTotalScore < 0)
|
||||
throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}");
|
||||
|
||||
return convertedTotalScore;
|
||||
}
|
||||
|
||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
|
||||
@@ -160,6 +160,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus),
|
||||
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
|
||||
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
|
||||
new KeyBinding(InputKey.Plus, GlobalAction.IncreaseOffset),
|
||||
new KeyBinding(InputKey.Minus, GlobalAction.DecreaseOffset),
|
||||
};
|
||||
|
||||
private static IEnumerable<KeyBinding> replayKeyBindings => new[]
|
||||
@@ -404,6 +406,12 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
||||
EditorToggleRotateControl,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
|
||||
IncreaseOffset,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))]
|
||||
DecreaseOffset
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
|
||||
@@ -344,6 +344,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
|
||||
|
||||
/// <summary>
|
||||
/// "Increase offset"
|
||||
/// </summary>
|
||||
public static LocalisableString IncreaseOffset => new TranslatableString(getKey(@"increase_offset"), @"Increase offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Decrease offset"
|
||||
/// </summary>
|
||||
public static LocalisableString DecreaseOffset => new TranslatableString(getKey(@"decrease_offset"), @"Decrease offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Toggle rotate control"
|
||||
/// </summary>
|
||||
|
||||
@@ -527,14 +527,21 @@ namespace osu.Game
|
||||
{
|
||||
ManualResetEventSlim readyToRun = new ManualResetEventSlim();
|
||||
|
||||
bool success = false;
|
||||
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
realmBlocker = realm.BlockAllOperations("migration");
|
||||
try
|
||||
{
|
||||
realmBlocker = realm.BlockAllOperations("migration");
|
||||
success = true;
|
||||
}
|
||||
catch { }
|
||||
|
||||
readyToRun.Set();
|
||||
}, false);
|
||||
|
||||
if (!readyToRun.Wait(30000))
|
||||
if (!readyToRun.Wait(30000) || !success)
|
||||
throw new TimeoutException("Attempting to block for migration took too long.");
|
||||
|
||||
bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@@ -218,6 +219,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
panel.Anchor = Anchor.TopCentre;
|
||||
panel.Origin = Anchor.TopCentre;
|
||||
panel.CanSpectate.Value = playingUsers.Contains(user.Id);
|
||||
});
|
||||
|
||||
public partial class OnlineUserPanel : CompositeDrawable, IFilterable
|
||||
|
||||
@@ -232,6 +232,14 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
bool expanded = coverToggle.CoverExpanded.Value;
|
||||
|
||||
cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint);
|
||||
|
||||
// Without this a very tiny slither of the cover will be visible even with a size of zero.
|
||||
// Integer masking woes, no doubt.
|
||||
if (expanded)
|
||||
cover.FadeIn(transition_duration, Easing.OutQuint);
|
||||
else
|
||||
cover.FadeOut(transition_duration, Easing.InQuint);
|
||||
|
||||
avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint);
|
||||
avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint);
|
||||
flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint);
|
||||
|
||||
@@ -136,9 +136,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Vector2 scale = drawable.DrawInfo.MatrixInverse.ExtractScale().Xy;
|
||||
drawableQuad = drawable.ToScreenSpace(
|
||||
drawable.DrawRectangle
|
||||
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
|
||||
.Inflate(SkinSelectionHandler.INFLATE_SIZE * scale));
|
||||
|
||||
var localSpaceQuad = ToLocalSpace(drawableQuad);
|
||||
|
||||
|
||||
@@ -157,6 +157,15 @@ namespace osu.Game.Overlays.Toolbar
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (Hotkey != null)
|
||||
{
|
||||
realm.SubscribeToPropertyChanged(r => r.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => false;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
@@ -168,8 +177,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateKeyBindingTooltip();
|
||||
|
||||
HoverBackground.FadeIn(200);
|
||||
tooltipContainer.FadeIn(100);
|
||||
|
||||
@@ -197,19 +204,13 @@ namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
}
|
||||
|
||||
private void updateKeyBindingTooltip()
|
||||
private void updateKeyBindingTooltip(string keyCombination)
|
||||
{
|
||||
if (Hotkey == null) return;
|
||||
string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination);
|
||||
|
||||
var realmKeyBinding = realm.Realm.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
string keyBindingString = keyCombinationProvider.GetReadableString(realmKeyBinding.KeyCombination);
|
||||
|
||||
if (!string.IsNullOrEmpty(keyBindingString))
|
||||
keyBindingTooltip.Text = $" ({keyBindingString})";
|
||||
}
|
||||
keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString)
|
||||
? $" ({keyBindingString})"
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer
|
||||
public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer, IApplicableFailOverride
|
||||
{
|
||||
public override string Name => "Cinema";
|
||||
public override string Acronym => "CN";
|
||||
public override IconUsage? Icon => OsuIcon.ModCinema;
|
||||
public override LocalisableString Description => "Watch the video without visual distractions.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail) }).ToArray();
|
||||
|
||||
public void ApplyToHUD(HUDOverlay overlay)
|
||||
{
|
||||
@@ -45,5 +45,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
player.BreakOverlay.Hide();
|
||||
}
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override LocalisableString Description => "You can't fail, no matter what.";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) };
|
||||
|
||||
private readonly Bindable<bool> showHealthBar = new Bindable<bool>();
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
=> result.Type.AffectsAccuracy()
|
||||
=> (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type))
|
||||
&& result.Type != result.Judgement.MaxResult;
|
||||
|
||||
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected readonly double DrainLenience;
|
||||
|
||||
private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>();
|
||||
private readonly List<HealthIncrease> healthIncreases = new List<HealthIncrease>();
|
||||
|
||||
private double gameplayEndTime;
|
||||
private double targetMinimumHealth;
|
||||
|
||||
@@ -133,8 +134,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
base.ApplyResultInternal(result);
|
||||
|
||||
if (!result.Type.IsBonus())
|
||||
healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result)));
|
||||
if (IsSimulating && !result.Type.IsBonus())
|
||||
{
|
||||
healthIncreases.Add(new HealthIncrease(
|
||||
result.HitObject.GetEndTime() + result.TimeOffset,
|
||||
GetHealthIncreaseFor(result)));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
@@ -165,8 +170,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < healthIncreases.Count; i++)
|
||||
{
|
||||
double currentTime = healthIncreases[i].time;
|
||||
double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime;
|
||||
double currentTime = healthIncreases[i].Time;
|
||||
double lastTime = i > 0 ? healthIncreases[i - 1].Time : DrainStartTime;
|
||||
|
||||
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime)
|
||||
{
|
||||
@@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
// Apply health adjustments
|
||||
currentHealth -= (currentTime - lastTime) * result;
|
||||
lowestHealth = Math.Min(lowestHealth, currentHealth);
|
||||
currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health);
|
||||
currentHealth = Math.Min(1, currentHealth + healthIncreases[i].Amount);
|
||||
|
||||
// Common scenario for when the drain rate is definitely too harsh
|
||||
if (lowestHealth < 0)
|
||||
@@ -198,5 +203,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private record struct HealthIncrease(double Time, double Amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000006: Fix edge cases in conversion after combo exponent introduction that lead to NaNs. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000008: Add accuracy conversion. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000008;
|
||||
public const int LATEST_VERSION = 30000009;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// This provides the ability to change the offset while in gameplay.
|
||||
/// Eventually this should be replaced with all settings from PlayerLoader being accessible from the game.
|
||||
/// </summary>
|
||||
internal partial class GameplayOffsetControl : VisibilityContainer
|
||||
{
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
// Disable interaction for now to avoid any funny business with slider bar dragging.
|
||||
public override bool PropagatePositionalInputSubTree => false;
|
||||
|
||||
private BeatmapOffsetControl offsetControl = null!;
|
||||
|
||||
private OsuTextFlowContainer text = null!;
|
||||
|
||||
private ScheduledDelegate? hideOp;
|
||||
|
||||
public GameplayOffsetControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Width = SettingsToolboxGroup.CONTAINER_WIDTH;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
|
||||
// Allow BeatmapOffsetControl to handle keyboard input.
|
||||
AlwaysPresent = true;
|
||||
|
||||
Anchor = Anchor.CentreRight;
|
||||
Origin = Anchor.CentreRight;
|
||||
|
||||
X = 100;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider? colourProvider)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.8f,
|
||||
Colour = colourProvider?.Background4 ?? Color4.Black,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(10),
|
||||
Spacing = new Vector2(5),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
offsetControl = new BeatmapOffsetControl(),
|
||||
text = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(weight: FontWeight.SemiBold))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
offsetControl.Current.BindValueChanged(val =>
|
||||
{
|
||||
text.Text = BeatmapOffsetControl.GetOffsetExplanatoryText(val.NewValue);
|
||||
Show();
|
||||
|
||||
hideOp?.Cancel();
|
||||
hideOp = Scheduler.AddDelayed(Hide, 500);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(500, Easing.OutQuint)
|
||||
.MoveToX(0, 500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(500, Easing.InQuint)
|
||||
.MoveToX(100, 500, Easing.InQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,6 +461,12 @@ namespace osu.Game.Screens.Play
|
||||
OnRetry = () => Restart(),
|
||||
OnQuit = () => PerformExit(true),
|
||||
},
|
||||
new GameplayOffsetControl
|
||||
{
|
||||
Margin = new MarginPadding(20),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -16,6 +18,7 @@ using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -26,7 +29,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.PlayerSettings
|
||||
{
|
||||
public partial class BeatmapOffsetControl : CompositeDrawable
|
||||
public partial class BeatmapOffsetControl : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
public Bindable<ScoreInfo?> ReferenceScore { get; } = new Bindable<ScoreInfo?>();
|
||||
|
||||
@@ -48,6 +51,12 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Player? player { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
private double lastPlayAverage;
|
||||
private double lastPlayBeatmapOffset;
|
||||
private HitEventTimingDistributionGraph? lastPlayGraph;
|
||||
@@ -88,28 +97,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
};
|
||||
}
|
||||
|
||||
public partial class OffsetSliderBar : PlayerSliderBar<double>
|
||||
{
|
||||
protected override Drawable CreateControl() => new CustomSliderBar();
|
||||
|
||||
protected partial class CustomSliderBar : SliderBar
|
||||
{
|
||||
public override LocalisableString TooltipText =>
|
||||
Current.Value == 0
|
||||
? LocalisableString.Interpolate($@"{base.TooltipText} ms")
|
||||
: LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}");
|
||||
|
||||
private LocalisableString getEarlyLateText(double value)
|
||||
{
|
||||
Debug.Assert(value != 0);
|
||||
|
||||
return value > 0
|
||||
? BeatmapOffsetControlStrings.HitObjectsAppearEarlier
|
||||
: BeatmapOffsetControlStrings.HitObjectsAppearLater;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -243,5 +230,68 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
// General limitations to ensure players don't do anything too weird.
|
||||
// These match stable for now.
|
||||
if (player is SubmittingPlayer)
|
||||
{
|
||||
// TODO: the blocking conditions should probably display a message.
|
||||
if (player?.IsBreakTime.Value == false && gameplayClock?.CurrentTime - gameplayClock?.StartTime > 10000)
|
||||
return false;
|
||||
|
||||
if (gameplayClock?.IsPaused.Value == true)
|
||||
return false;
|
||||
}
|
||||
|
||||
// To match stable, this should adjust by 5 ms, or 1 ms when holding alt.
|
||||
// But that is hard to make work with global actions due to the operating mode.
|
||||
// Let's use the more precise as a default for now.
|
||||
const double amount = 1;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.IncreaseOffset:
|
||||
Current.Value += amount;
|
||||
return true;
|
||||
|
||||
case GlobalAction.DecreaseOffset:
|
||||
Current.Value -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
public static LocalisableString GetOffsetExplanatoryText(double offset)
|
||||
{
|
||||
return offset == 0
|
||||
? LocalisableString.Interpolate($@"{offset:0.0} ms")
|
||||
: LocalisableString.Interpolate($@"{offset:0.0} ms {getEarlyLateText(offset)}");
|
||||
|
||||
LocalisableString getEarlyLateText(double value)
|
||||
{
|
||||
Debug.Assert(value != 0);
|
||||
|
||||
return value > 0
|
||||
? BeatmapOffsetControlStrings.HitObjectsAppearEarlier
|
||||
: BeatmapOffsetControlStrings.HitObjectsAppearLater;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class OffsetSliderBar : PlayerSliderBar<double>
|
||||
{
|
||||
protected override Drawable CreateControl() => new CustomSliderBar();
|
||||
|
||||
protected partial class CustomSliderBar : SliderBar
|
||||
{
|
||||
public override LocalisableString TooltipText => GetOffsetExplanatoryText(Current.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ namespace osu.Game.Screens.Select
|
||||
private ILocalisedBindableString artistBinding;
|
||||
private FillFlowContainer infoLabelContainer;
|
||||
private Container bpmLabelContainer;
|
||||
private Container lengthLabelContainer;
|
||||
|
||||
private readonly WorkingBeatmap working;
|
||||
private readonly RulesetInfo ruleset;
|
||||
@@ -341,10 +342,10 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
settingChangeTracker?.Dispose();
|
||||
|
||||
refreshBPMLabel();
|
||||
refreshBPMAndLengthLabel();
|
||||
|
||||
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
|
||||
settingChangeTracker.SettingChanged += _ => refreshBPMLabel();
|
||||
settingChangeTracker.SettingChanged += _ => refreshBPMAndLengthLabel();
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -370,12 +371,10 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
infoLabelContainer.Children = new Drawable[]
|
||||
{
|
||||
new InfoLabel(new BeatmapStatistic
|
||||
lengthLabelContainer = new Container
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
||||
}),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
bpmLabelContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -394,7 +393,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshBPMLabel()
|
||||
private void refreshBPMAndLengthLabel()
|
||||
{
|
||||
var beatmap = working.Beatmap;
|
||||
|
||||
@@ -420,6 +419,16 @@ namespace osu.Game.Screens.Select
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||
Content = labelText
|
||||
});
|
||||
|
||||
double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate);
|
||||
double hitLength = Math.Round(beatmap.BeatmapInfo.Length / rate);
|
||||
|
||||
lengthLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||
Content = hitLength.ToFormattedDuration().ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
private Drawable getMapper(BeatmapMetadata metadata)
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
decimal? legacyVersion = skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
if (legacyVersion > 1.0m)
|
||||
{
|
||||
this.MoveTo(new Vector2(0, -5));
|
||||
this.MoveToOffset(new Vector2(0, 80), fade_out_delay + fade_out_length, Easing.In);
|
||||
|
||||
+8
-8
@@ -8,11 +8,11 @@ using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public abstract partial class ModPerfectTestScene : ModTestScene
|
||||
public abstract partial class ModFailConditionTestScene : ModTestScene
|
||||
{
|
||||
private readonly ModPerfect mod;
|
||||
private readonly ModFailCondition mod;
|
||||
|
||||
protected ModPerfectTestScene(ModPerfect mod)
|
||||
protected ModFailConditionTestScene(ModFailCondition mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
@@ -26,15 +26,15 @@ namespace osu.Game.Tests.Visual
|
||||
HitObjects = { testData.HitObject }
|
||||
},
|
||||
Autoplay = !shouldMiss,
|
||||
PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss)
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss)
|
||||
});
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer();
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModFailConditionTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
private partial class PerfectModTestPlayer : TestPlayer
|
||||
protected partial class ModFailConditionTestPlayer : ModTestPlayer
|
||||
{
|
||||
public PerfectModTestPlayer()
|
||||
: base(showResults: false)
|
||||
public ModFailConditionTestPlayer(ModTestData data, bool allowFail)
|
||||
: base(data, allowFail)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -20,35 +20,35 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
protected sealed override bool HasCustomSteps => true;
|
||||
|
||||
private ModTestData currentTestData;
|
||||
protected ModTestData CurrentTestData { get; private set; }
|
||||
|
||||
protected void CreateModTest(ModTestData testData) => CreateTest(() =>
|
||||
{
|
||||
AddStep("set test data", () => currentTestData = testData);
|
||||
AddStep("set test data", () => CurrentTestData = testData);
|
||||
});
|
||||
|
||||
public override void TearDownSteps()
|
||||
{
|
||||
AddUntilStep("test passed", () =>
|
||||
{
|
||||
if (currentTestData == null)
|
||||
if (CurrentTestData == null)
|
||||
return true;
|
||||
|
||||
return currentTestData.PassCondition?.Invoke() ?? false;
|
||||
return CurrentTestData.PassCondition?.Invoke() ?? false;
|
||||
});
|
||||
|
||||
base.TearDownSteps();
|
||||
}
|
||||
|
||||
protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset);
|
||||
protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => CurrentTestData?.Beatmap ?? base.CreateBeatmap(ruleset);
|
||||
|
||||
protected sealed override TestPlayer CreatePlayer(Ruleset ruleset)
|
||||
{
|
||||
var mods = new List<Mod>(SelectedMods.Value);
|
||||
|
||||
if (currentTestData.Mods != null)
|
||||
mods.AddRange(currentTestData.Mods);
|
||||
if (currentTestData.Autoplay)
|
||||
if (CurrentTestData.Mods != null)
|
||||
mods.AddRange(CurrentTestData.Mods);
|
||||
if (CurrentTestData.Autoplay)
|
||||
mods.Add(ruleset.GetAutoplayMod());
|
||||
|
||||
SelectedMods.Value = mods;
|
||||
@@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual
|
||||
return CreateModPlayer(ruleset);
|
||||
}
|
||||
|
||||
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail);
|
||||
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
protected partial class ModTestPlayer : TestPlayer
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user