mirror of
https://github.com/ppy/osu.git
synced 2026-05-24 06:19:55 +08:00
Compare commits
45 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();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor();
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int standardisedBonusScore;
|
||||
private int combo;
|
||||
@@ -134,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> GetNumericResultFor(result) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,44 +31,31 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
protected override double GetNumericResultFor(JudgementResult result)
|
||||
{
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return 305;
|
||||
}
|
||||
|
||||
return base.GetNumericResultFor(result);
|
||||
}
|
||||
|
||||
protected override double GetMaxNumericResultFor(JudgementResult result)
|
||||
{
|
||||
switch (result.Judgement.MaxResult)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return 305;
|
||||
}
|
||||
|
||||
return base.GetMaxNumericResultFor(result);
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
{
|
||||
double numericResult;
|
||||
return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
}
|
||||
|
||||
switch (result.Type)
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
numericResult = 300;
|
||||
break;
|
||||
|
||||
default:
|
||||
numericResult = GetNumericResultFor(result);
|
||||
break;
|
||||
return 305;
|
||||
}
|
||||
|
||||
return numericResult * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
private int getBaseComboScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return 300;
|
||||
}
|
||||
|
||||
return GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
private class JudgementOrderComparer : IComparer<HitObject>
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double trackerRotationTolerance = 0;
|
||||
|
||||
addSeekStep(5000);
|
||||
AddStep("calculate rotation tolerance", () =>
|
||||
{
|
||||
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
|
||||
});
|
||||
AddStep("calculate rotation tolerance", () => { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); });
|
||||
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
|
||||
@@ -133,9 +130,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddAssert("player score matching expected bonus score", () =>
|
||||
{
|
||||
var scoreProcessor = ((ScoreExposedPlayer)Player).ScoreProcessor;
|
||||
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||
long totalScore = scoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult);
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
@@ -5,12 +5,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
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.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor();
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int standardisedBonusScore;
|
||||
private int combo;
|
||||
@@ -171,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModAccuracyChallenge : ModAccuracyChallenge
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
typeof(OsuModSpunOut),
|
||||
typeof(ModRelax),
|
||||
typeof(ModFailCondition),
|
||||
typeof(ModNoFail),
|
||||
typeof(ModAutoplay),
|
||||
typeof(OsuModMagnetised),
|
||||
typeof(OsuModRepel),
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModNoFail : ModNoFail
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModPerfect : ModPerfect
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
typeof(OsuModAutopilot),
|
||||
typeof(OsuModTargetPractice),
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private Drawable scaleContainer;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableSliderRepeat()
|
||||
: base(null)
|
||||
{
|
||||
|
||||
@@ -24,11 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
|
||||
/// </summary>
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hit samples only play on successful hits.
|
||||
/// If <c>false</c>, the hit samples will also play on misses.
|
||||
|
||||
@@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private const float default_tick_size = 16;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
private SkinnableDrawable scaleContainer;
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -312,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
private static readonly int score_per_tick = new OsuScoreProcessor().GetBaseScoreForResult(new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxResult);
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
|
||||
@@ -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,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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public partial class OsuHealthProcessor : DrainingHealthProcessor
|
||||
{
|
||||
public OsuHealthProcessor(double drainStartTime, double drainLenience = 0)
|
||||
: base(drainStartTime, drainLenience)
|
||||
{
|
||||
}
|
||||
|
||||
protected override int? GetDensityGroup(HitObject hitObject) => (hitObject as IHasComboInformation)?.ComboIndex;
|
||||
|
||||
protected override 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,25 +62,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
if (Result.IsMiss())
|
||||
{
|
||||
default:
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
break;
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
break;
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect))
|
||||
return Drawable.Empty();
|
||||
|
||||
return new ArgonJudgementPiece(resultComponent.Component);
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -66,8 +65,21 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
HitPolicy = new StartTimeOrderedHitPolicy();
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r =>
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case HitResult.Great:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.IgnoreMiss:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}))
|
||||
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
|
||||
|
||||
AddRangeInternal(poolDictionary.Values);
|
||||
@@ -170,7 +182,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
|
||||
if (!poolDictionary.TryGetValue(result.Type, out var pool))
|
||||
return;
|
||||
|
||||
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
|
||||
|
||||
judgementLayer.Add(explosion);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -13,11 +12,14 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor = new TaikoScoreProcessor();
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int standardisedBonusScore;
|
||||
private int combo;
|
||||
@@ -191,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
@@ -28,20 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
{
|
||||
return GetNumericResultFor(result)
|
||||
return GetBaseScoreForResult(result.Type)
|
||||
* Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base))
|
||||
* strongScaleValue(result);
|
||||
}
|
||||
|
||||
protected override double GetNumericResultFor(JudgementResult result)
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result.Type)
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Ok:
|
||||
return 150;
|
||||
}
|
||||
|
||||
return base.GetNumericResultFor(result);
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
private double strongScaleValue(JudgementResult result)
|
||||
|
||||
@@ -219,6 +219,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
|
||||
};
|
||||
scoreInfo.OnlineID = 123123;
|
||||
scoreInfo.ClientVersion = "2023.1221.0";
|
||||
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
@@ -237,9 +239,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.OnlineID, Is.EqualTo(123123));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
// Apply a judgement
|
||||
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus });
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE));
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(scoreProcessor.GetBaseScoreForResult(HitResult.LargeBonus)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -196,6 +196,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
User = new APIUser { Username = "Test user" },
|
||||
BeatmapInfo = beatmap.Beatmaps.First(),
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
ClientVersion = "12345",
|
||||
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
|
||||
};
|
||||
|
||||
@@ -203,6 +204,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
|
||||
Assert.That(imported.ClientVersion, Is.EqualTo(toImport.ClientVersion));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -41,6 +41,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private BeatmapSetInfo? importedSet;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase osu { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
@@ -153,6 +156,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
AddUntilStep("score has correct version", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)!.ClientVersion), () => Is.EqualTo(osu.Version));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -420,7 +420,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // temporary while peppy investigates
|
||||
[Ignore("temporary while peppy investigates. probably realm batching related.")]
|
||||
public void TestSelectionRetainedOnBeatmapUpdate()
|
||||
{
|
||||
createSongSelect();
|
||||
|
||||
@@ -340,15 +340,12 @@ namespace osu.Game
|
||||
|
||||
try
|
||||
{
|
||||
var score = scoreManager.Query(s => s.ID == id);
|
||||
long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager);
|
||||
|
||||
// Can't use async overload because we're not on the update thread.
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
realmAccess.Write(r =>
|
||||
{
|
||||
ScoreInfo s = r.Find<ScoreInfo>(id)!;
|
||||
s.TotalScore = newTotalScore;
|
||||
StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager);
|
||||
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||
});
|
||||
|
||||
|
||||
@@ -90,8 +90,9 @@ namespace osu.Game.Database
|
||||
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
|
||||
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
|
||||
/// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values.
|
||||
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
|
||||
/// </summary>
|
||||
private const int schema_version = 39;
|
||||
private const int schema_version = 40;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
|
||||
@@ -57,14 +57,14 @@ namespace osu.Game.Database
|
||||
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
|
||||
List<HitResult> sortedHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.OrderByDescending(kvp => processor.GetBaseScoreForResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Attempt to use maximum statistics from the database.
|
||||
var maximumJudgements = score.MaximumStatistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.OrderByDescending(kvp => processor.GetBaseScoreForResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
|
||||
.ToList();
|
||||
|
||||
@@ -169,10 +169,10 @@ namespace osu.Game.Database
|
||||
public static long GetOldStandardised(ScoreInfo score)
|
||||
{
|
||||
double accuracyScore =
|
||||
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
|
||||
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value)
|
||||
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value);
|
||||
double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
|
||||
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => numericScoreFor(kvp.Key) * kvp.Value);
|
||||
|
||||
double accuracyPortion = 0.3;
|
||||
|
||||
@@ -193,6 +193,65 @@ namespace osu.Game.Database
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
|
||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
|
||||
static int numericScoreFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
return 10;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
return 30;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 50;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 100;
|
||||
|
||||
case HitResult.Good:
|
||||
return 200;
|
||||
|
||||
case HitResult.Great:
|
||||
return 300;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 315;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return 10;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
|
||||
/// </summary>
|
||||
/// <param name="score">The score to update.</param>
|
||||
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
|
||||
public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps)
|
||||
{
|
||||
score.TotalScore = convertFromLegacyTotalScore(score, beatmaps);
|
||||
score.Accuracy = ComputeAccuracy(score);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
|
||||
/// </summary>
|
||||
/// <param name="score">The score to update.</param>
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
||||
public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||
{
|
||||
score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes);
|
||||
score.Accuracy = ComputeAccuracy(score);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -201,7 +260,7 @@ namespace osu.Game.Database
|
||||
/// <param name="score">The score to convert the total score of.</param>
|
||||
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
|
||||
/// <returns>The standardised total score.</returns>
|
||||
public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
|
||||
private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
|
||||
{
|
||||
if (!score.IsLegacyScore)
|
||||
return score.TotalScore;
|
||||
@@ -224,7 +283,7 @@ namespace osu.Game.Database
|
||||
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
||||
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
|
||||
|
||||
return ConvertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
|
||||
return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -234,7 +293,7 @@ namespace osu.Game.Database
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
||||
/// <returns>The standardised total score.</returns>
|
||||
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||
private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||
{
|
||||
if (!score.IsLegacyScore)
|
||||
return score.TotalScore;
|
||||
@@ -386,6 +445,19 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
|
||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
|
||||
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
||||
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
|
||||
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
||||
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
|
||||
|
||||
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
|
||||
/// </summary>
|
||||
|
||||
@@ -75,9 +75,12 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
return Orange1;
|
||||
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.ComboBreak:
|
||||
return Red;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -38,18 +38,16 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
if (Result != HitResult.None && !Result.IsHit())
|
||||
{
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
break;
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
}
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
|
||||
@@ -133,12 +133,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
case HitResult.None:
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
ApplyMissAnimations();
|
||||
break;
|
||||
|
||||
default:
|
||||
ApplyHitAnimations();
|
||||
if (Result.Type.IsHit())
|
||||
ApplyHitAnimations();
|
||||
else
|
||||
ApplyMissAnimations();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </summary>
|
||||
public class Judgement
|
||||
{
|
||||
/// <summary>
|
||||
/// The score awarded for a small bonus.
|
||||
/// </summary>
|
||||
public const int SMALL_BONUS_SCORE = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The score awarded for a large bonus.
|
||||
/// </summary>
|
||||
public const int LARGE_BONUS_SCORE = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The default health increase for a maximum judgement, as a proportion of total health.
|
||||
/// By default, each maximum judgement restores 5% of total health.
|
||||
@@ -91,23 +81,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The numeric score representation for the maximum achievable result.
|
||||
/// </summary>
|
||||
public int MaxNumericResult => ToNumericResult(MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// The health increase for the maximum achievable result.
|
||||
/// </summary>
|
||||
public double MaxHealthIncrease => HealthIncreaseFor(MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric score representation of a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="JudgementResult"/> to find the numeric score representation for.</param>
|
||||
/// <returns>The numeric score representation of <paramref name="result"/>.</returns>
|
||||
public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric health increase of a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
@@ -165,41 +143,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// <returns>The numeric health increase of <paramref name="result"/>.</returns>
|
||||
public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type);
|
||||
|
||||
public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}";
|
||||
|
||||
public static int ToNumericResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
return 10;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
return 30;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 50;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 100;
|
||||
|
||||
case HitResult.Good:
|
||||
return 200;
|
||||
|
||||
case HitResult.Great:
|
||||
// Perfect doesn't actually give more score / accuracy directly.
|
||||
case HitResult.Perfect:
|
||||
return 300;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return SMALL_BONUS_SCORE;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return LARGE_BONUS_SCORE;
|
||||
}
|
||||
}
|
||||
public override string ToString() => $"MaxResult:{MaxResult}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
RawTime = null;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})";
|
||||
public override string ToString() => $"{Type} ({Judgement})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 @@ 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 readonly Dictionary<int, double> densityMultiplierByGroup = new Dictionary<int, double>();
|
||||
|
||||
private double gameplayEndTime;
|
||||
private double targetMinimumHealth;
|
||||
|
||||
@@ -133,14 +135,33 @@ 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),
|
||||
GetDensityGroup(result.HitObject)));
|
||||
}
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * getDensityMultiplier(GetDensityGroup(result.HitObject));
|
||||
|
||||
private double getDensityMultiplier(int? group)
|
||||
{
|
||||
if (group == null)
|
||||
return 1;
|
||||
|
||||
return densityMultiplierByGroup.TryGetValue(group.Value, out double multiplier) ? multiplier : 1;
|
||||
}
|
||||
|
||||
protected virtual int? GetDensityGroup(HitObject hitObject) => null;
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
densityMultiplierByGroup.Clear();
|
||||
|
||||
if (storeResults)
|
||||
DrainRate = ComputeDrainRate();
|
||||
|
||||
@@ -152,6 +173,24 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (healthIncreases.Count <= 1)
|
||||
return 0;
|
||||
|
||||
// Normalise the health gain during sections with higher densities.
|
||||
(int group, double avgIncrease)[] avgIncreasesByGroup = healthIncreases
|
||||
.Where(i => i.Group != null)
|
||||
.GroupBy(i => i.Group)
|
||||
.Select(g => ((int)g.Key!, g.Sum(i => i.Amount) / (g.Max(i => i.Time) - g.Min(i => i.Time) + 1)))
|
||||
.ToArray();
|
||||
|
||||
if (avgIncreasesByGroup.Length > 1)
|
||||
{
|
||||
double overallAverageIncrease = avgIncreasesByGroup.Average(g => g.avgIncrease);
|
||||
|
||||
foreach ((int group, double avgIncrease) in avgIncreasesByGroup)
|
||||
{
|
||||
// Reduce the health increase for groups that return more health than average.
|
||||
densityMultiplierByGroup[group] = Math.Min(1, overallAverageIncrease / avgIncrease);
|
||||
}
|
||||
}
|
||||
|
||||
int adjustment = 1;
|
||||
double result = 1;
|
||||
|
||||
@@ -165,8 +204,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)
|
||||
{
|
||||
@@ -177,10 +216,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
currentBreak++;
|
||||
}
|
||||
|
||||
double multiplier = getDensityMultiplier(healthIncreases[i].Group);
|
||||
|
||||
// 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 * multiplier);
|
||||
|
||||
// Common scenario for when the drain rate is definitely too harsh
|
||||
if (lowestHealth < 0)
|
||||
@@ -198,5 +239,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private record struct HealthIncrease(double Time, double Amount, int? Group);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a large tick miss.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "large_tick_miss")]
|
||||
[Description(@"x")]
|
||||
[Order(10)]
|
||||
LargeTickMiss,
|
||||
|
||||
@@ -117,6 +118,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a miss that should be ignored for scoring purposes.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "ignore_miss")]
|
||||
[Description("x")]
|
||||
[Order(13)]
|
||||
IgnoreMiss,
|
||||
|
||||
@@ -267,9 +269,34 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> represents a miss of any type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, both <see cref="IsMiss"/> and <see cref="IsHit"/> return <see langword="false"/> for <see cref="HitResult.None"/>.
|
||||
/// </remarks>
|
||||
public static bool IsMiss(this HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.Miss:
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.ComboBreak:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> represents a successful hit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, both <see cref="IsMiss"/> and <see cref="IsHit"/> return <see langword="false"/> for <see cref="HitResult.None"/>.
|
||||
/// </remarks>
|
||||
public static bool IsHit(this HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
|
||||
@@ -227,12 +227,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
if (result.Judgement.MaxResult.AffectsAccuracy())
|
||||
{
|
||||
currentMaximumBaseScore += GetMaxNumericResultFor(result);
|
||||
currentMaximumBaseScore += GetBaseScoreForResult(result.Judgement.MaxResult);
|
||||
currentAccuracyJudgementCount++;
|
||||
}
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
currentBaseScore += GetNumericResultFor(result);
|
||||
currentBaseScore += GetBaseScoreForResult(result.Type);
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
currentBonusPortion += GetBonusScoreChange(result);
|
||||
@@ -276,12 +276,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
if (result.Judgement.MaxResult.AffectsAccuracy())
|
||||
{
|
||||
currentMaximumBaseScore -= GetMaxNumericResultFor(result);
|
||||
currentMaximumBaseScore -= GetBaseScoreForResult(result.Judgement.MaxResult);
|
||||
currentAccuracyJudgementCount--;
|
||||
}
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
currentBaseScore -= GetNumericResultFor(result);
|
||||
currentBaseScore -= GetBaseScoreForResult(result.Type);
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
currentBonusPortion -= GetBonusScoreChange(result);
|
||||
@@ -297,21 +297,51 @@ namespace osu.Game.Rulesets.Scoring
|
||||
updateScore();
|
||||
}
|
||||
|
||||
protected virtual double GetBonusScoreChange(JudgementResult result) => GetNumericResultFor(result);
|
||||
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => GetMaxNumericResultFor(result) * Math.Pow(result.ComboAfterJudgement, COMBO_EXPONENT);
|
||||
/// <summary>
|
||||
/// Gets the final score change to be applied to the bonus portion of the score.
|
||||
/// </summary>
|
||||
/// <param name="result">The judgement result.</param>
|
||||
protected virtual double GetBonusScoreChange(JudgementResult result) => GetBaseScoreForResult(result.Type);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric score representation for a <see cref="JudgementResult"/>.
|
||||
/// Gets the final score change to be applied to the combo portion of the score.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="JudgementResult"/>.</param>
|
||||
protected virtual double GetNumericResultFor(JudgementResult result) => result.Judgement.NumericResultFor(result);
|
||||
/// <param name="result">The judgement result.</param>
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => GetBaseScoreForResult(result.Judgement.MaxResult) * Math.Pow(result.ComboAfterJudgement, COMBO_EXPONENT);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum numeric score representation for a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="JudgementResult"/>.</param>
|
||||
protected virtual double GetMaxNumericResultFor(JudgementResult result) => result.Judgement.MaxNumericResult;
|
||||
public virtual int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
default:
|
||||
return 0;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
return 10;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
return 30;
|
||||
|
||||
case HitResult.Meh:
|
||||
return 50;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 100;
|
||||
|
||||
case HitResult.Good:
|
||||
return 200;
|
||||
|
||||
case HitResult.Great:
|
||||
case HitResult.Perfect: // Perfect doesn't actually give more score / accuracy directly.
|
||||
return 300;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return 10;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ApplyScoreChange(JudgementResult result)
|
||||
{
|
||||
@@ -540,7 +570,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to compute accuracy.
|
||||
/// See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="ScoreProcessor.GetBaseScoreForResult"/>.
|
||||
/// </remarks>
|
||||
[Key(0)]
|
||||
public double BaseScore { get; set; }
|
||||
|
||||
@@ -35,12 +35,16 @@ namespace osu.Game.Scoring.Legacy
|
||||
[JsonProperty("maximum_statistics")]
|
||||
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
|
||||
|
||||
[JsonProperty("client_version")]
|
||||
public string ClientVersion = string.Empty;
|
||||
|
||||
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
|
||||
{
|
||||
OnlineID = score.OnlineID,
|
||||
Mods = score.APIMods,
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
ClientVersion = score.ClientVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
score.ScoreInfo.Statistics = readScore.Statistics;
|
||||
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
|
||||
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
|
||||
score.ScoreInfo.ClientVersion = readScore.ClientVersion;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores.</description></item>
|
||||
/// <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>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000007;
|
||||
public const int LATEST_VERSION = 30000008;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
||||
@@ -17,7 +17,6 @@ using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using Realms;
|
||||
|
||||
@@ -107,7 +106,7 @@ namespace osu.Game.Scoring
|
||||
else if (model.IsLegacyScore)
|
||||
{
|
||||
model.LegacyTotalScore = model.TotalScore;
|
||||
model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps());
|
||||
StandardisedScoreMigrationTools.UpdateFromLegacy(model, beatmaps());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,13 +124,14 @@ namespace osu.Game.Scoring
|
||||
var beatmap = score.BeatmapInfo!.Detach();
|
||||
var ruleset = score.Ruleset.Detach();
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
var scoreProcessor = rulesetInstance.CreateScoreProcessor();
|
||||
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
|
||||
// Populate the maximum statistics.
|
||||
HitResult maxBasicResult = rulesetInstance.GetHitResults()
|
||||
.Select(h => h.result)
|
||||
.Where(h => h.IsBasic()).MaxBy(Judgement.ToNumericResult);
|
||||
.Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult);
|
||||
|
||||
foreach ((HitResult result, int count) in score.Statistics)
|
||||
{
|
||||
|
||||
@@ -46,6 +46,12 @@ namespace osu.Game.Scoring
|
||||
/// </remarks>
|
||||
public BeatmapInfo? BeatmapInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the client this score was set using.
|
||||
/// Sourced from <see cref="OsuGameBase.Version"/> at the point of score submission.
|
||||
/// </summary>
|
||||
public string ClientVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="osu.Game.Beatmaps.BeatmapInfo.Hash"/> at the point in time when the score was set.
|
||||
/// </summary>
|
||||
|
||||
@@ -109,6 +109,9 @@ namespace osu.Game.Screens.Play
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
public GameplayState GameplayState { get; private set; }
|
||||
|
||||
private Ruleset ruleset;
|
||||
@@ -1155,7 +1158,11 @@ namespace osu.Game.Screens.Play
|
||||
/// <returns>The <see cref="Scoring.Score"/>.</returns>
|
||||
protected virtual Score CreateScore(IBeatmap beatmap) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
User = api.LocalUser.Value,
|
||||
ClientVersion = game.Version,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -102,6 +102,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.SaveReplay:
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Skinning
|
||||
});
|
||||
}
|
||||
|
||||
if (result != HitResult.Miss)
|
||||
if (!result.IsMiss())
|
||||
{
|
||||
//new judgement shows old as a temporary effect
|
||||
AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f, true)
|
||||
|
||||
@@ -50,11 +50,18 @@ namespace osu.Game.Skinning
|
||||
|
||||
// legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece.
|
||||
if (animation?.FrameCount > 1 && !forceTransforms)
|
||||
return;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
if (isMissedTick())
|
||||
applyMissedTickScaling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.IsMiss())
|
||||
{
|
||||
if (isMissedTick())
|
||||
applyMissedTickScaling();
|
||||
else
|
||||
{
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
@@ -71,21 +78,28 @@ namespace osu.Game.Skinning
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(rotation, fade_in_length)
|
||||
.Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
this.ScaleTo(0.6f).Then()
|
||||
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
|
||||
.Delay(fade_in_length * 0.2f) // t = 1.0
|
||||
.ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
|
||||
|
||||
// stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2.
|
||||
// so we need to force the current value to be correct at 1.2 (0.95) then complete the
|
||||
// second half of the transform.
|
||||
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ScaleTo(0.6f).Then()
|
||||
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
|
||||
.Delay(fade_in_length * 0.2f) // t = 1.0
|
||||
.ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
|
||||
|
||||
// stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2.
|
||||
// so we need to force the current value to be correct at 1.2 (0.95) then complete the
|
||||
// second half of the transform.
|
||||
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4
|
||||
}
|
||||
}
|
||||
|
||||
private bool isMissedTick() => result.IsMiss() && result != HitResult.Miss;
|
||||
|
||||
private void applyMissedTickScaling()
|
||||
{
|
||||
this.ScaleTo(0.6f);
|
||||
this.ScaleTo(0.3f, 100, Easing.In);
|
||||
}
|
||||
|
||||
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
|
||||
|
||||
@@ -453,11 +453,11 @@ namespace osu.Game.Skinning
|
||||
|
||||
private Drawable? getJudgementAnimation(HitResult result)
|
||||
{
|
||||
if (result.IsMiss())
|
||||
return this.GetAnimation("hit0", true, false);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
return this.GetAnimation("hit0", true, false);
|
||||
|
||||
case HitResult.Meh:
|
||||
return this.GetAnimation("hit50", true, false);
|
||||
|
||||
|
||||
+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