mirror of
https://github.com/ppy/osu.git
synced 2024-11-14 15:17:27 +08:00
Merge branch 'master' into argon-pp-counter
This commit is contained in:
commit
d9cc619693
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Fruit(), 0.01, true],
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,210 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 65316
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
DistanceSpacing: 1.4
|
||||
BeatDivisor: 4
|
||||
GridSize: 8
|
||||
TimelineZoom: 1.4
|
||||
|
||||
[Metadata]
|
||||
Title:Nanairo Symphony -TV Size-
|
||||
TitleUnicode:七色シンフォニー -TV Size-
|
||||
Artist:Coalamode.
|
||||
ArtistUnicode:コアラモード.
|
||||
Creator:Ascendance
|
||||
Version:Aru's Cup
|
||||
Source:四月は君の嘘
|
||||
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
|
||||
BeatmapID:1041052
|
||||
BeatmapSetID:488149
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:2.5
|
||||
OverallDifficulty:6
|
||||
ApproachRate:6
|
||||
SliderMultiplier:1.02
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
Video,500,"forty.avi"
|
||||
0,0,"cropped-1366-768-647733.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1155,387.096774193548,4,2,1,50,1,0
|
||||
15284,-100,4,2,1,60,0,0
|
||||
16638,-100,4,2,1,50,0,0
|
||||
41413,-100,4,2,1,60,0,0
|
||||
59993,-100,4,2,1,65,0,0
|
||||
66187,-100,4,2,1,70,0,1
|
||||
87284,-100,4,2,1,60,0,1
|
||||
87864,-100,4,2,1,70,0,0
|
||||
87961,-100,4,2,1,50,0,0
|
||||
88638,-100,4,2,1,30,0,0
|
||||
89413,-100,4,2,1,10,0,0
|
||||
89800,-100,4,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 255,128,64
|
||||
Combo2 : 0,128,255
|
||||
Combo3 : 255,128,192
|
||||
Combo4 : 0,128,192
|
||||
|
||||
[HitObjects]
|
||||
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
160,160,2122,1,0,0:0:0:0:
|
||||
272,160,2509,1,2,0:0:0:0:
|
||||
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
384,96,4058,1,2,0:0:0:0:
|
||||
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,64,5800,1,2,0:0:0:0:
|
||||
240,64,5993,1,2,0:0:0:0:
|
||||
288,64,6187,1,2,0:0:0:0:
|
||||
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
|
||||
488,160,8122,2,0,L|376:160,1,102
|
||||
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
400,288,10058,1,0,0:0:0:0:
|
||||
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
|
||||
400,288,11606,1,0,0:0:0:0:
|
||||
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
0,288,13154,1,0,0:0:0:0:
|
||||
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
|
||||
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
|
||||
312,208,15864,1,6,0:0:0:0:
|
||||
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
432,176,20896,1,0,0:0:0:0:
|
||||
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
328,176,22445,1,0,0:0:0:0:
|
||||
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
224,176,23993,1,0,0:0:0:0:
|
||||
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
416,176,25541,1,0,0:0:0:0:
|
||||
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
480,112,27090,1,0,0:0:0:0:
|
||||
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
432,112,28058,1,2,0:0:0:0:
|
||||
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
272,272,30574,2,0,L|374:272,1,102
|
||||
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
|
||||
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
200,368,32703,1,2,0:0:0:0:
|
||||
376,368,33284,1,0,0:0:0:0:
|
||||
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
280,16,36380,1,2,0:0:0:0:
|
||||
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
176,224,37928,1,2,0:0:0:0:
|
||||
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
144,264,38896,1,2,0:0:0:0:
|
||||
280,336,39477,2,0,L|336:336,1,51
|
||||
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
|
||||
352,232,40445,1,4,0:1:0:0:
|
||||
160,224,41025,1,8,0:3:0:0:
|
||||
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
400,40,41993,1,0,0:0:0:0:
|
||||
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
|
||||
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
56,136,45284,1,2,0:0:0:0:
|
||||
160,144,45671,1,8,0:0:0:0:
|
||||
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
|
||||
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
144,136,49154,1,2,0:0:0:0:
|
||||
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
|
||||
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
|
||||
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
400,208,51284,1,0,0:0:0:0:
|
||||
352,240,51477,2,0,L|240:240,1,102
|
||||
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
88,264,53025,1,2,0:0:0:0:
|
||||
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
|
||||
432,120,54574,1,2,0:0:0:0:
|
||||
328,128,54961,1,8,0:0:0:0:
|
||||
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
224,184,56509,1,8,0:3:0:0:
|
||||
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
224,272,58445,5,2,0:0:0:0:
|
||||
320,296,58832,1,0,0:0:0:0:
|
||||
224,328,59219,1,2,0:0:0:0:
|
||||
120,328,59606,1,8,0:3:0:0:
|
||||
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
160,112,64638,1,8,0:0:0:0:
|
||||
208,128,64832,1,8,0:0:0:0:
|
||||
256,144,65025,1,8,0:0:0:0:
|
||||
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
176,208,68316,1,0,0:0:0:0:
|
||||
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
|
||||
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
|
||||
208,248,77800,1,8,0:0:0:0:
|
||||
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
96,72,80703,1,0,0:0:0:0:
|
||||
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
208,272,82445,1,10,0:0:0:0:
|
||||
312,272,82832,1,8,0:0:0:0:
|
||||
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
|
||||
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
|
||||
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
|
||||
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
|
||||
168,96,87864,1,4,0:0:0:0:
|
||||
256,192,88251,12,0,89800,0:0:0:0:
|
@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
float offsetPosition = hitObject.OriginalX;
|
||||
double startTime = hitObject.StartTime;
|
||||
|
||||
if (lastPosition == null)
|
||||
if (lastPosition == null ||
|
||||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
|
||||
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
|
||||
// todo: should be revisited and corrected later probably.
|
||||
lastPosition == 0)
|
||||
{
|
||||
lastPosition = offsetPosition;
|
||||
lastStartTime = startTime;
|
||||
|
@ -2,22 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
private int fruitsHit;
|
||||
private int ticksHit;
|
||||
private int tinyTicksHit;
|
||||
private int tinyTicksMissed;
|
||||
private int misses;
|
||||
private int num300;
|
||||
private int num100;
|
||||
private int num50;
|
||||
private int numKatu;
|
||||
private int numMiss;
|
||||
|
||||
public CatchPerformanceCalculator()
|
||||
: base(new CatchRuleset())
|
||||
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
||||
|
||||
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
num300 = score.GetCount300() ?? 0; // HitResult.Great
|
||||
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
|
||||
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
|
||||
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
|
||||
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
|
||||
|
||||
// We are heavily relying on aim in catch the beat
|
||||
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||
value *= lengthBonus;
|
||||
|
||||
value *= Math.Pow(0.97, misses);
|
||||
value *= Math.Pow(0.97, numMiss);
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
|
||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
|
||||
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
|
||||
private int totalComboHits() => misses + ticksHit + fruitsHit;
|
||||
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
|
||||
private int totalSuccessfulHits() => num50 + num100 + num300;
|
||||
private int totalComboHits() => numMiss + num100 + num300;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
// matches stable.
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||
// the above early-return skips the failure check at the end of the same method:
|
||||
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||
// making it impossible to fail on a tiny droplet regardless of result.
|
||||
if (result.Type == HitResult.SmallTickMiss)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Note(), 0.01, true],
|
||||
[new HeadNote(), 0.01, true],
|
||||
[new TailNote(), 0.01, true],
|
||||
[new HoldNoteBody(), 0, true], // hold note break
|
||||
[new HoldNote(), 0, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
|
||||
upSprite = new Sprite
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Texture = skin.GetTexture(upImage),
|
||||
Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1
|
||||
},
|
||||
downSprite = new Sprite
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Texture = skin.GetTexture(downImage),
|
||||
Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Alpha = 0
|
||||
|
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new HitCircle(), 0.01, true],
|
||||
[new SliderHeadCircle(), 0.01, true],
|
||||
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SliderTick(), 0.01, true],
|
||||
[new SliderRepeat(new Slider()), 0.01, true],
|
||||
[new SliderTailCircle(new Slider()), 0, true],
|
||||
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new Slider(), 0, true],
|
||||
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SpinnerTick(), 0, false],
|
||||
[new SpinnerBonusTick(), 0, false],
|
||||
[new Spinner(), 0.01, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertMidSliderJudgementFail();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindHandling()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
|
||||
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 },
|
||||
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
|
||||
}, new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(250, 0),
|
||||
}, 250),
|
||||
});
|
||||
|
||||
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||
|
||||
AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4));
|
||||
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertAllMaxJudgements()
|
||||
{
|
||||
AddAssert("All judgements max", () =>
|
||||
|
@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
|
||||
{
|
||||
if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}.");
|
||||
if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0.");
|
||||
if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0.");
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(endIndex);
|
||||
|
||||
int extendedEndIndex = endIndex;
|
||||
|
||||
|
@ -334,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
|
||||
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
|
||||
{
|
||||
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
|
||||
|
||||
int sourceIndex = -1;
|
||||
|
||||
|
20
osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs
Normal file
20
osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// 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 osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Judgements
|
||||
{
|
||||
public class OsuSliderJudgementResult : OsuJudgementResult
|
||||
{
|
||||
public readonly Stack<(double time, bool tracking)> TrackingHistory = new Stack<(double, bool)>();
|
||||
|
||||
public OsuSliderJudgementResult(HitObject hitObject, Judgement judgement)
|
||||
: base(hitObject, judgement)
|
||||
{
|
||||
TrackingHistory.Push((double.NegativeInfinity, false));
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
|
||||
// some spinners may not complete due to very minor decimal loss during calculation
|
||||
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
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.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
public new OsuSliderJudgementResult Result => (OsuSliderJudgementResult)base.Result;
|
||||
|
||||
public DrawableSliderHead HeadCircle => headContainer.Child;
|
||||
public DrawableSliderTail TailCircle => tailContainer.Child;
|
||||
|
||||
@ -134,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement);
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
|
@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
break;
|
||||
}
|
||||
|
||||
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
|
||||
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
|
||||
|
||||
|
@ -5,11 +5,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public bool Tracking { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
|
||||
///
|
||||
@ -49,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public SliderInputManager(DrawableSlider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.slider.HitObjectApplied += resetState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -208,6 +215,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
|
||||
private void updateTracking(bool isValidTrackingPosition)
|
||||
{
|
||||
if (gameplayClock?.IsRewinding == true)
|
||||
{
|
||||
var trackingHistory = slider.Result.TrackingHistory;
|
||||
while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time)
|
||||
trackingHistory.Pop();
|
||||
|
||||
Debug.Assert(trackingHistory.Count > 0);
|
||||
|
||||
Tracking = trackingHistory.Peek().tracking;
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasTracking = Tracking;
|
||||
|
||||
// from the point at which the head circle is hit, this will be non-null.
|
||||
// it may be null if the head circle was missed.
|
||||
OsuAction? headCircleHitAction = getInitialHitAction();
|
||||
@ -247,6 +268,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
&& isValidTrackingPosition
|
||||
// valid action
|
||||
&& validTrackingAction;
|
||||
|
||||
if (wasTracking != Tracking)
|
||||
slider.Result.TrackingHistory.Push((Time.Current, Tracking));
|
||||
}
|
||||
|
||||
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
|
||||
@ -264,5 +288,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
|
||||
}
|
||||
|
||||
private void resetState(DrawableHitObject obj)
|
||||
{
|
||||
Tracking = false;
|
||||
timeToAcceptAnyKeyAfter = null;
|
||||
lastPressedActions.Clear();
|
||||
screenSpaceMousePosition = null;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
slider.HitObjectApplied -= resetState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
// 0.05 rad/ms, or ~477 RPM, as per stable.
|
||||
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
|
||||
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
|
||||
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
|
||||
switch (h)
|
||||
{
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4.White.Opacity(0.25f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = 90 - arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
background.Alpha = spinner.Progress >= 1 ? 0 : 1;
|
||||
|
||||
fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(90 - fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(90 - fill.Progress * 180);
|
||||
}
|
||||
|
||||
private partial class ProgressFill : CircularProgress
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = -arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
base.Update();
|
||||
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(-fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(-fill.Progress * 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
if (mousePosition is Vector2 pos)
|
||||
{
|
||||
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
|
||||
|
||||
// Normalise the delta to -180 .. 180
|
||||
|
@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissHitAndHitSwell()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[1].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, fail expected after miss
|
||||
[new Hit(), true],
|
||||
[new Hit.StrongNestedHit(new Hit()), false],
|
||||
[new DrumRollTick(new DrumRoll()), false],
|
||||
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
|
||||
[new DrumRoll(), false],
|
||||
[new SwellTick(), false],
|
||||
[new Swell(), false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
public class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private readonly int beatmapVersion;
|
||||
|
||||
|
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyScoreEncoderTest
|
||||
{
|
||||
[TestCase(1, 3)]
|
||||
[TestCase(1, 0)]
|
||||
[TestCase(0, 3)]
|
||||
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
|
||||
{
|
||||
var ruleset = new CatchRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
scoreInfo.Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 50,
|
||||
[HitResult.LargeTickHit] = 5,
|
||||
[HitResult.Miss] = missCount,
|
||||
[HitResult.LargeTickMiss] = largeTickMissCount
|
||||
};
|
||||
var score = new Score { ScoreInfo = scoreInfo };
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||
encoder.Encode(encodeStream);
|
||||
|
||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||
|
||||
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
|
||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||
return decodedAfterEncode;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,109 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneMedalOverlay : OsuTestScene
|
||||
public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
public TestSceneMedalOverlay()
|
||||
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
private MedalOverlay overlay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep(@"display", () =>
|
||||
var overlayManagerMock = new Mock<IOverlayManager>();
|
||||
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
|
||||
|
||||
AddStep("create overlay", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
LoadComponentAsync(new MedalOverlay(new Medal
|
||||
{
|
||||
Name = @"Animations",
|
||||
InternalName = @"all-intro-doubletime",
|
||||
Description = @"More complex than you think.",
|
||||
}), Add);
|
||||
Child = overlay = new MedalOverlay(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(IOverlayManager), overlayManagerMock.Object)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicAward()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
|
||||
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleMedalsInQuickSuccession()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "S-Ranker",
|
||||
Description = "Accuracy is really underrated.",
|
||||
Slug = @"all-secret-rank-s"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "500 Combo",
|
||||
Description = "500 big ones! You're moving up in the world!",
|
||||
Slug = @"osu-combo-500"
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
|
||||
{
|
||||
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(unlock)
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private TextureUpload upscale(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
@ -22,6 +22,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
@ -286,6 +287,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestModSelectOverlay()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods = new[]
|
||||
{
|
||||
new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 2.0 } }),
|
||||
new APIMod(new OsuModStrictTracking()),
|
||||
},
|
||||
AllowedMods = new[]
|
||||
{
|
||||
new APIMod(new OsuModFlashlight()),
|
||||
}
|
||||
});
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
ClickButtonWhenEnabled<RoomSubScreen.UserModSelectButton>();
|
||||
AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
|
||||
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
|
||||
AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
|
||||
AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
|
||||
|
||||
AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200);
|
||||
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowMedalAtResults()
|
||||
{
|
||||
playToResults();
|
||||
|
||||
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
})
|
||||
})
|
||||
}));
|
||||
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryFromResults()
|
||||
{
|
||||
|
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneDrawableRank : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestAllRanks()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer<DrawableRank>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(10),
|
||||
ChildrenEnumerable = Enum.GetValues<ScoreRank>().OrderBy(v => v).Select(rank => new DrawableRank(rank)
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
Size = new Vector2(50, 25),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -88,8 +88,20 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
|
||||
}
|
||||
|
||||
private void showPanel(ScoreInfo score) =>
|
||||
Child = new ExpandedPanelMiddleContentContainer(score);
|
||||
[Test]
|
||||
public void TestFailedSDisplay([Values] bool withFlair)
|
||||
{
|
||||
AddStep("show failed S score", () =>
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
|
||||
score.Rank = ScoreRank.A;
|
||||
score.Accuracy = 0.975;
|
||||
showPanel(score, withFlair);
|
||||
});
|
||||
}
|
||||
|
||||
private void showPanel(ScoreInfo score, bool withFlair = false) =>
|
||||
Child = new ExpandedPanelMiddleContentContainer(score, withFlair);
|
||||
|
||||
private BeatmapInfo createTestBeatmap([NotNull] RealmUser author)
|
||||
{
|
||||
@ -107,7 +119,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
private partial class ExpandedPanelMiddleContentContainer : Container
|
||||
{
|
||||
public ExpandedPanelMiddleContentContainer(ScoreInfo score)
|
||||
public ExpandedPanelMiddleContentContainer(ScoreInfo score, bool withFlair)
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
@ -119,7 +131,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#444"),
|
||||
},
|
||||
new ExpandedPanelMiddleContent(score)
|
||||
new ExpandedPanelMiddleContent(score, withFlair)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Mods = { BindTarget = SelectedMods },
|
||||
});
|
||||
|
||||
AddStep("set beatmap", () =>
|
||||
|
@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSettingsOrder()
|
||||
{
|
||||
createScreen();
|
||||
|
||||
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
|
||||
AddAssert("mod settings order: DT, HD, DF", () =>
|
||||
{
|
||||
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
|
||||
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
|
||||
columns.ElementAt(1).Mod is OsuModHidden &&
|
||||
columns.ElementAt(2).Mod is OsuModDeflate;
|
||||
});
|
||||
|
||||
AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList());
|
||||
AddAssert("mod settings order: NC, HD, DF", () =>
|
||||
{
|
||||
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
|
||||
return columns.ElementAt(0).Mod is OsuModNightcore &&
|
||||
columns.ElementAt(1).Mod is OsuModHidden &&
|
||||
columns.ElementAt(2).Mod is OsuModDeflate;
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
|
||||
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
Size size = image.Size();
|
||||
Size size = image.Size;
|
||||
|
||||
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
|
||||
// Also assume that all backgrounds are wider than they are tall, so the
|
||||
|
@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
Dimmed.BindValueChanged(_ => updateState());
|
||||
|
||||
playButton.Playing.BindValueChanged(_ => updateState(), true);
|
||||
((IBindable<double>)progress.Current).BindTo(playButton.Progress);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
progress.Progress = playButton.Progress.Value;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Storyboards;
|
||||
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
float startValue = Parsing.ParseFloat(split[4]);
|
||||
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,7 @@ namespace osu.Game.Beatmaps.Timing
|
||||
|
||||
public TimeSignature(int numerator)
|
||||
{
|
||||
if (numerator < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive.");
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numerator);
|
||||
|
||||
Numerator = numerator;
|
||||
}
|
||||
|
@ -489,8 +489,7 @@ namespace osu.Game.Database
|
||||
/// <param name="action">The work to run.</param>
|
||||
public Task WriteAsync(Action<Realm> action)
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(nameof(RealmAccess));
|
||||
ObjectDisposedException.ThrowIf(isDisposed, this);
|
||||
|
||||
// Required to ensure the write is tracked and accounted for before disposal.
|
||||
// Can potentially be avoided if we have a need to do so in the future.
|
||||
@ -675,8 +674,7 @@ namespace osu.Game.Database
|
||||
|
||||
private Realm getRealmInstance()
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(nameof(RealmAccess));
|
||||
ObjectDisposedException.ThrowIf(isDisposed, this);
|
||||
|
||||
bool tookSemaphoreLock = false;
|
||||
|
||||
@ -1189,8 +1187,7 @@ namespace osu.Game.Database
|
||||
if (!ThreadSafety.IsUpdateThread)
|
||||
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
|
||||
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(nameof(RealmAccess));
|
||||
ObjectDisposedException.ThrowIf(isDisposed, this);
|
||||
|
||||
SynchronizationContext? syncContext = null;
|
||||
|
||||
|
@ -415,7 +415,7 @@ namespace osu.Game.Database
|
||||
|
||||
// Calculate how many times the longest combo the user has achieved in the play can repeat
|
||||
// without exceeding the combo portion in score V1 as achieved by the player.
|
||||
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
|
||||
// This intentionally does not operate on object count and uses only score instead.
|
||||
double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
|
||||
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
|
||||
|
||||
@ -426,13 +426,12 @@ namespace osu.Game.Database
|
||||
// ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score.
|
||||
double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore
|
||||
double scoreBasedEstimateOfComboPortionInStandardisedScore
|
||||
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
|
||||
+ remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Compute approximate upper estimate new score for that play.
|
||||
// This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths.
|
||||
// There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice.
|
||||
remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
|
||||
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
|
||||
@ -449,7 +448,17 @@ namespace osu.Game.Database
|
||||
// we can skip adding the 1 and just multiply by x ^ 0.5.
|
||||
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
double objectCountBasedEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Enforce some invariants on both of the estimates.
|
||||
// In rare cases they can produce invalid results.
|
||||
scoreBasedEstimateOfComboPortionInStandardisedScore =
|
||||
Math.Clamp(scoreBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
|
||||
objectCountBasedEstimateOfComboPortionInStandardisedScore =
|
||||
Math.Clamp(objectCountBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore = Math.Min(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
|
||||
double upperEstimateOfComboPortionInStandardisedScore = Math.Max(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
|
||||
|
||||
// Approximate by combining lower and upper estimates.
|
||||
// As the lower-estimate is very pessimistic, we use a 30/70 ratio
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private Sample samplePopIn;
|
||||
private Sample samplePopOut;
|
||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
protected virtual string? PopInSampleName => @"UI/overlay-pop-in";
|
||||
protected virtual string? PopOutSampleName => @"UI/overlay-pop-out";
|
||||
protected virtual double PopInOutSampleBalance => 0;
|
||||
|
||||
protected override bool BlockNonPositionalInput => true;
|
||||
@ -34,19 +32,23 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
protected virtual bool DimMainContent => true;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IOverlayManager overlayManager { get; set; }
|
||||
[Resolved]
|
||||
private IOverlayManager? overlayManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; }
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
private Sample? samplePopIn;
|
||||
private Sample? samplePopOut;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager? audio)
|
||||
{
|
||||
samplePopIn = audio.Samples.Get(PopInSampleName);
|
||||
samplePopOut = audio.Samples.Get(PopOutSampleName);
|
||||
if (!string.IsNullOrEmpty(PopInSampleName))
|
||||
samplePopIn = audio?.Samples.Get(PopInSampleName);
|
||||
|
||||
if (!string.IsNullOrEmpty(PopOutSampleName))
|
||||
samplePopOut = audio?.Samples.Get(PopOutSampleName);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
if (dragRotationState == DragRotationState.Rotating && distance > 0)
|
||||
{
|
||||
Vector2 offset = e.MousePosition - positionMouseDown;
|
||||
float degrees = MathUtils.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
|
||||
float degrees = float.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
|
||||
|
||||
// Always rotate in the direction of least distance
|
||||
float diff = (degrees - activeCursor.Rotation) % 360;
|
||||
|
@ -63,8 +63,12 @@ namespace osu.Game.Graphics
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"ff8e5d");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"ff5a5a");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"3f3f3f");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Extensions;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class OsuNumberBox : OsuTextBox
|
||||
{
|
||||
protected override bool AllowIme => false;
|
||||
|
||||
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
|
||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||
}
|
||||
}
|
||||
|
@ -116,18 +116,18 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private const float transition_length = 500;
|
||||
protected const float TRANSITION_LENGTH = 500;
|
||||
|
||||
protected void FadeHovered()
|
||||
protected virtual void FadeHovered()
|
||||
{
|
||||
Bar.FadeIn(transition_length, Easing.OutQuint);
|
||||
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
|
||||
Bar.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
Text.FadeColour(Color4.White, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected void FadeUnhovered()
|
||||
protected virtual void FadeUnhovered()
|
||||
{
|
||||
Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
|
||||
Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
|
||||
Bar.FadeTo(IsHovered ? 1 : 0, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
Text.FadeColour(IsHovered ? Color4.White : AccentColour, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
@ -8,7 +8,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
using (Stream s = entry.OpenEntryStream())
|
||||
s.ReadToFill(owner.Memory.Span);
|
||||
s.ReadExactly(owner.Memory.Span);
|
||||
|
||||
return new MemoryOwnerMemoryStream(owner);
|
||||
}
|
||||
|
@ -80,8 +80,7 @@ namespace osu.Game.IO
|
||||
|
||||
public override Storage GetStorageForDirectory(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentException("Must be non-null and not empty string", nameof(path));
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
if (!path.EndsWith(Path.DirectorySeparatorChar))
|
||||
path += Path.DirectorySeparatorChar;
|
||||
|
@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
RulesetID = score.RulesetID,
|
||||
Passed = score.Passed,
|
||||
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),
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Online.Notifications.WebSocket.Requests;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
|
@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"473625");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"512525");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"CC3333");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message sent from the server when new messages arrive.
|
@ -0,0 +1,39 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php
|
||||
/// </summary>
|
||||
public class NewPrivateNotificationEvent
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public ulong ID { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("object_type")]
|
||||
public string ObjectType { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("object_id")]
|
||||
public ulong ObjectId { get; set; }
|
||||
|
||||
[JsonProperty("source_user_id")]
|
||||
public uint SourceUserID { get; set; }
|
||||
|
||||
[JsonProperty("is_read")]
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
[JsonProperty("details")]
|
||||
public JObject? Details { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php
|
||||
/// </summary>
|
||||
public class UserAchievementUnlock
|
||||
{
|
||||
[JsonProperty("achievement_id")]
|
||||
public uint AchievementId { get; set; }
|
||||
|
||||
[JsonProperty("achievement_mode")]
|
||||
public ushort? AchievementMode { get; set; }
|
||||
|
||||
[JsonProperty("cover_url")]
|
||||
public string CoverUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
public uint UserId { get; set; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client no longer wants to receive chat messages.
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client wants to receive chat messages.
|
@ -1083,6 +1083,7 @@ namespace osu.Game
|
||||
|
||||
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
|
||||
|
||||
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||
|
||||
|
312
osu.Game/Overlays/MedalAnimation.cs
Normal file
312
osu.Game/Overlays/MedalAnimation.cs
Normal file
@ -0,0 +1,312 @@
|
||||
// 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 osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Overlays.MedalSplash;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class MedalAnimation : VisibilityContainer
|
||||
{
|
||||
public const float DISC_SIZE = 400;
|
||||
|
||||
private const float border_width = 5;
|
||||
|
||||
private readonly Medal medal;
|
||||
private readonly Box background;
|
||||
private readonly Container backgroundStrip, particleContainer;
|
||||
private readonly BackgroundStrip leftStrip, rightStrip;
|
||||
private readonly CircularContainer disc;
|
||||
private readonly Sprite innerSpin, outerSpin;
|
||||
|
||||
private DrawableMedal? drawableMedal;
|
||||
private Sample? getSample;
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
public MedalAnimation(Medal medal)
|
||||
{
|
||||
this.medal = medal;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Child = content = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(60),
|
||||
},
|
||||
outerSpin = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(DISC_SIZE + 500),
|
||||
Alpha = 0f,
|
||||
},
|
||||
backgroundStrip = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = border_width,
|
||||
Alpha = 0f,
|
||||
Children = new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Width = 0.5f,
|
||||
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
leftStrip = new BackgroundStrip(0f, 1f)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 0.5f,
|
||||
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
rightStrip = new BackgroundStrip(1f, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
particleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0f,
|
||||
},
|
||||
disc = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0f,
|
||||
Masking = true,
|
||||
AlwaysPresent = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = border_width,
|
||||
Size = new Vector2(DISC_SIZE),
|
||||
Scale = new Vector2(0.8f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"05262f"),
|
||||
},
|
||||
new Triangles
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
TriangleScale = 2,
|
||||
ColourDark = Color4Extensions.FromHex(@"04222b"),
|
||||
ColourLight = Color4Extensions.FromHex(@"052933"),
|
||||
},
|
||||
innerSpin = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(1.05f),
|
||||
Alpha = 0.25f,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
|
||||
{
|
||||
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
|
||||
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
|
||||
|
||||
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colours.Blue.Opacity(0.5f),
|
||||
Radius = 50,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}, loaded =>
|
||||
{
|
||||
disc.Add(loaded);
|
||||
startAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
|
||||
}
|
||||
|
||||
private const double initial_duration = 400;
|
||||
private const double step_duration = 900;
|
||||
|
||||
private void startAnimation()
|
||||
{
|
||||
content.Show();
|
||||
|
||||
background.FlashColour(Color4.White.Opacity(0.25f), 400);
|
||||
|
||||
getSample?.Play();
|
||||
|
||||
innerSpin.Spin(20000, RotationDirection.Clockwise);
|
||||
outerSpin.Spin(40000, RotationDirection.Clockwise);
|
||||
|
||||
using (BeginDelayedSequence(200))
|
||||
{
|
||||
disc.FadeIn(initial_duration)
|
||||
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
|
||||
|
||||
particleContainer.FadeIn(initial_duration);
|
||||
outerSpin.FadeTo(0.1f, initial_duration * 2);
|
||||
|
||||
using (BeginDelayedSequence(initial_duration + 200))
|
||||
{
|
||||
backgroundStrip.FadeIn(step_duration);
|
||||
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
|
||||
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
|
||||
|
||||
Debug.Assert(drawableMedal != null);
|
||||
|
||||
this.Animate().Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.Icon;
|
||||
}).Delay(step_duration).Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.MedalUnlocked;
|
||||
}).Delay(step_duration).Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.Full;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(200);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(200);
|
||||
}
|
||||
|
||||
public void Dismiss()
|
||||
{
|
||||
if (drawableMedal != null && drawableMedal.State != DisplayState.Full)
|
||||
{
|
||||
// if we haven't yet, play out the animation fully
|
||||
drawableMedal.State = DisplayState.Full;
|
||||
FinishTransforms(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Hide();
|
||||
Expire();
|
||||
}
|
||||
|
||||
private partial class BackgroundStrip : Container
|
||||
{
|
||||
public BackgroundStrip(float start, float end)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Width = 0f;
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
|
||||
Masking = true;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class MedalParticle : CircularContainer
|
||||
{
|
||||
private readonly float direction;
|
||||
|
||||
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
|
||||
|
||||
public MedalParticle(float direction)
|
||||
{
|
||||
this.direction = direction;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Position = positionForOffset(DISC_SIZE / 2);
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colours.Blue.Opacity(0.5f),
|
||||
Radius = 5,
|
||||
};
|
||||
|
||||
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
|
||||
this.FadeOut(500);
|
||||
Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,324 +1,130 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Overlays.MedalSplash;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osuTK.Input;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using System;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class MedalOverlay : FocusedOverlayContainer
|
||||
public partial class MedalOverlay : OsuFocusedOverlayContainer
|
||||
{
|
||||
public const float DISC_SIZE = 400;
|
||||
protected override string? PopInSampleName => null;
|
||||
protected override string? PopOutSampleName => null;
|
||||
|
||||
private const float border_width = 5;
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
private readonly Medal medal;
|
||||
private readonly Box background;
|
||||
private readonly Container backgroundStrip, particleContainer;
|
||||
private readonly BackgroundStrip leftStrip, rightStrip;
|
||||
private readonly CircularContainer disc;
|
||||
private readonly Sprite innerSpin, outerSpin;
|
||||
private DrawableMedal drawableMedal;
|
||||
protected override void PopIn() => this.FadeIn();
|
||||
|
||||
private Sample getSample;
|
||||
protected override void PopOut() => this.FadeOut();
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Queue<MedalAnimation> queuedMedals = new Queue<MedalAnimation>();
|
||||
|
||||
public MedalOverlay(Medal medal)
|
||||
{
|
||||
this.medal = medal;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
Child = content = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(60),
|
||||
},
|
||||
outerSpin = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(DISC_SIZE + 500),
|
||||
Alpha = 0f,
|
||||
},
|
||||
backgroundStrip = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = border_width,
|
||||
Alpha = 0f,
|
||||
Children = new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Width = 0.5f,
|
||||
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
leftStrip = new BackgroundStrip(0f, 1f)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 0.5f,
|
||||
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
|
||||
Children = new[]
|
||||
{
|
||||
rightStrip = new BackgroundStrip(1f, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
particleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0f,
|
||||
},
|
||||
disc = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0f,
|
||||
Masking = true,
|
||||
AlwaysPresent = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = border_width,
|
||||
Size = new Vector2(DISC_SIZE),
|
||||
Scale = new Vector2(0.8f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"05262f"),
|
||||
},
|
||||
new Triangles
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
TriangleScale = 2,
|
||||
ColourDark = Color4Extensions.FromHex(@"04222b"),
|
||||
ColourLight = Color4Extensions.FromHex(@"052933"),
|
||||
},
|
||||
innerSpin = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(1.05f),
|
||||
Alpha = 0.25f,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Show();
|
||||
}
|
||||
private Container<Drawable> medalContainer = null!;
|
||||
private MedalAnimation? lastAnimation;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
|
||||
private void load()
|
||||
{
|
||||
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
|
||||
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
|
||||
api.NotificationsClient.MessageReceived += handleMedalMessages;
|
||||
|
||||
Add(medalContainer = new Container
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colours.Blue.Opacity(0.5f),
|
||||
Radius = 50,
|
||||
};
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
|
||||
OverlayActivationMode.BindValueChanged(val =>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}, loaded =>
|
||||
if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false))
|
||||
Show();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void handleMedalMessages(SocketMessage obj)
|
||||
{
|
||||
if (obj.Event != @"new")
|
||||
return;
|
||||
|
||||
var data = obj.Data?.ToObject<NewPrivateNotificationEvent>();
|
||||
if (data == null || data.Name != @"user_achievement_unlock")
|
||||
return;
|
||||
|
||||
var details = data.Details?.ToObject<UserAchievementUnlock>();
|
||||
if (details == null)
|
||||
return;
|
||||
|
||||
var medal = new Medal
|
||||
{
|
||||
disc.Add(loaded);
|
||||
startAnimation();
|
||||
});
|
||||
Name = details.Title,
|
||||
InternalName = details.Slug,
|
||||
Description = details.Description,
|
||||
};
|
||||
|
||||
var medalAnimation = new MedalAnimation(medal);
|
||||
queuedMedals.Enqueue(medalAnimation);
|
||||
if (OverlayActivationMode.Value == OverlayActivation.All)
|
||||
Scheduler.AddOnce(Show);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
|
||||
if (medalContainer.Any() || lastAnimation?.IsLoaded == false)
|
||||
return;
|
||||
|
||||
if (!queuedMedals.TryDequeue(out lastAnimation))
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
LoadComponentAsync(lastAnimation, medalContainer.Add);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
dismiss();
|
||||
lastAnimation?.Dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.CurrentState.Keyboard.Keys.IsPressed(Key.Escape)) dismiss();
|
||||
}
|
||||
|
||||
private const double initial_duration = 400;
|
||||
private const double step_duration = 900;
|
||||
|
||||
private void startAnimation()
|
||||
{
|
||||
content.Show();
|
||||
|
||||
background.FlashColour(Color4.White.Opacity(0.25f), 400);
|
||||
|
||||
getSample.Play();
|
||||
|
||||
innerSpin.Spin(20000, RotationDirection.Clockwise);
|
||||
outerSpin.Spin(40000, RotationDirection.Clockwise);
|
||||
|
||||
using (BeginDelayedSequence(200))
|
||||
if (e.Action == GlobalAction.Back)
|
||||
{
|
||||
disc.FadeIn(initial_duration)
|
||||
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
|
||||
|
||||
particleContainer.FadeIn(initial_duration);
|
||||
outerSpin.FadeTo(0.1f, initial_duration * 2);
|
||||
|
||||
using (BeginDelayedSequence(initial_duration + 200))
|
||||
{
|
||||
backgroundStrip.FadeIn(step_duration);
|
||||
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
|
||||
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
|
||||
|
||||
this.Animate().Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.Icon;
|
||||
}).Delay(step_duration).Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.MedalUnlocked;
|
||||
}).Delay(step_duration).Schedule(() =>
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
drawableMedal.State = DisplayState.Full;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(200);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(200);
|
||||
}
|
||||
|
||||
private void dismiss()
|
||||
{
|
||||
if (drawableMedal.State != DisplayState.Full)
|
||||
{
|
||||
// if we haven't yet, play out the animation fully
|
||||
drawableMedal.State = DisplayState.Full;
|
||||
FinishTransforms(true);
|
||||
return;
|
||||
lastAnimation?.Dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
Hide();
|
||||
Expire();
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
|
||||
private partial class BackgroundStrip : Container
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
public BackgroundStrip(float start, float end)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Width = 0f;
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
|
||||
Masking = true;
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class MedalParticle : CircularContainer
|
||||
{
|
||||
private readonly float direction;
|
||||
|
||||
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
|
||||
|
||||
public MedalParticle(float direction)
|
||||
{
|
||||
this.direction = direction;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Position = positionForOffset(DISC_SIZE / 2);
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colours.Blue.Opacity(0.5f),
|
||||
Radius = 5,
|
||||
};
|
||||
|
||||
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
|
||||
this.FadeOut(500);
|
||||
Expire();
|
||||
}
|
||||
if (api.IsNotNull())
|
||||
api.NotificationsClient.MessageReceived -= handleMedalMessages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Overlays.MedalSplash
|
||||
public DrawableMedal(Medal medal)
|
||||
{
|
||||
this.medal = medal;
|
||||
Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2);
|
||||
Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2);
|
||||
|
||||
FillFlowContainer infoFlow;
|
||||
Children = new Drawable[]
|
||||
@ -174,7 +174,7 @@ namespace osu.Game.Overlays.MedalSplash
|
||||
.ScaleTo(1);
|
||||
|
||||
this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo);
|
||||
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
|
||||
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
|
||||
unlocked.FadeInFromZero(duration);
|
||||
break;
|
||||
|
||||
@ -184,7 +184,7 @@ namespace osu.Game.Overlays.MedalSplash
|
||||
.ScaleTo(1);
|
||||
|
||||
this.ScaleTo(scale_when_full, duration, Easing.OutExpo);
|
||||
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
|
||||
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
|
||||
unlocked.Show();
|
||||
name.FadeInFromZero(duration + 100);
|
||||
description.FadeInFromZero(duration * 2);
|
||||
|
@ -40,8 +40,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public Bindable<IBeatmapInfo?> BeatmapInfo { get; } = new Bindable<IBeatmapInfo?>();
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
public Bindable<IReadOnlyList<Mod>> Mods { get; } = new Bindable<IReadOnlyList<Mod>>();
|
||||
|
||||
public BindableBool Collapsed { get; } = new BindableBool(true);
|
||||
|
||||
@ -53,7 +52,7 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
private IBindable<RulesetInfo> gameRuleset = null!;
|
||||
protected IBindable<RulesetInfo> GameRuleset = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationSource;
|
||||
private IBindable<StarDifficulty?> starDifficulty = null!;
|
||||
@ -101,15 +100,15 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
mods.BindValueChanged(_ =>
|
||||
Mods.BindValueChanged(_ =>
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(Mods.Value);
|
||||
modSettingChangeTracker.SettingChanged += _ => updateValues();
|
||||
updateValues();
|
||||
}, true);
|
||||
|
||||
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
|
||||
BeatmapInfo.BindValueChanged(_ => updateValues());
|
||||
|
||||
Collapsed.BindValueChanged(_ =>
|
||||
{
|
||||
@ -118,11 +117,12 @@ namespace osu.Game.Overlays.Mods
|
||||
updateCollapsedState();
|
||||
});
|
||||
|
||||
gameRuleset = game.Ruleset.GetBoundCopy();
|
||||
gameRuleset.BindValueChanged(_ => updateValues());
|
||||
GameRuleset = game.Ruleset.GetBoundCopy();
|
||||
GameRuleset.BindValueChanged(_ => updateValues());
|
||||
|
||||
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
|
||||
BeatmapInfo.BindValueChanged(_ => updateValues());
|
||||
|
||||
updateValues();
|
||||
updateCollapsedState();
|
||||
}
|
||||
|
||||
@ -166,17 +166,17 @@ namespace osu.Game.Overlays.Mods
|
||||
});
|
||||
|
||||
double rate = 1;
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToRate>())
|
||||
rate = mod.ApplyToRate(0, rate);
|
||||
|
||||
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);
|
||||
|
||||
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
|
||||
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
|
||||
mod.ApplyToDifficulty(originalDifficulty);
|
||||
|
||||
Ruleset ruleset = gameRuleset.Value.CreateInstance();
|
||||
Ruleset ruleset = GameRuleset.Value.CreateInstance();
|
||||
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
|
||||
|
||||
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
|
||||
@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Mods
|
||||
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private partial class BPMDisplay : RollingCounter<int>
|
||||
public partial class BPMDisplay : RollingCounter<int>
|
||||
{
|
||||
protected override double RollingDuration => 250;
|
||||
|
||||
|
@ -43,6 +43,14 @@ namespace osu.Game.Overlays.Mods
|
||||
[Cached]
|
||||
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
/// <summary>
|
||||
/// Contains a list of mods which <see cref="ModSelectOverlay"/> should read from to display effects on the selected beatmap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is different from <see cref="SelectedMods"/> in screens like online-play rooms, where there are required mods activated from the playlist.
|
||||
/// </remarks>
|
||||
public Bindable<IReadOnlyList<Mod>> ActiveMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
/// <summary>
|
||||
/// Contains a dictionary with the current <see cref="ModState"/> of all mods applicable for the current ruleset.
|
||||
/// </summary>
|
||||
@ -97,6 +105,8 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
|
||||
|
||||
protected virtual IReadOnlyList<Mod> ComputeActiveMods() => SelectedMods.Value;
|
||||
|
||||
protected virtual IEnumerable<ShearedButton> CreateFooterButtons()
|
||||
{
|
||||
if (AllowCustomisation)
|
||||
@ -279,7 +289,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
BeatmapInfo = { Value = beatmap?.BeatmapInfo }
|
||||
BeatmapInfo = { Value = Beatmap?.BeatmapInfo },
|
||||
},
|
||||
}
|
||||
});
|
||||
@ -316,20 +326,26 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
SelectedMods.BindValueChanged(_ =>
|
||||
{
|
||||
updateRankingInformation();
|
||||
updateFromExternalSelection();
|
||||
updateCustomisation();
|
||||
|
||||
ActiveMods.Value = ComputeActiveMods();
|
||||
}, true);
|
||||
|
||||
ActiveMods.BindValueChanged(_ =>
|
||||
{
|
||||
updateOverlayInformation();
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
if (AllowCustomisation)
|
||||
{
|
||||
// Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can
|
||||
// Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can
|
||||
// potentially be stale, due to complexities in the way change trackers work.
|
||||
//
|
||||
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value);
|
||||
modSettingChangeTracker.SettingChanged += _ => updateRankingInformation();
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value);
|
||||
modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation();
|
||||
}
|
||||
}, true);
|
||||
|
||||
@ -454,18 +470,25 @@ namespace osu.Game.Overlays.Mods
|
||||
modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
|
||||
}
|
||||
|
||||
private void updateRankingInformation()
|
||||
/// <summary>
|
||||
/// Updates any information displayed on the overlay regarding the effects of the active mods.
|
||||
/// This reads from <see cref="ActiveMods"/> instead of <see cref="SelectedMods"/>.
|
||||
/// </summary>
|
||||
private void updateOverlayInformation()
|
||||
{
|
||||
if (rankingInformationDisplay == null)
|
||||
return;
|
||||
if (rankingInformationDisplay != null)
|
||||
{
|
||||
double multiplier = 1.0;
|
||||
|
||||
double multiplier = 1.0;
|
||||
foreach (var mod in ActiveMods.Value)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
|
||||
foreach (var mod in SelectedMods.Value)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
rankingInformationDisplay.ModMultiplier.Value = multiplier;
|
||||
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
|
||||
}
|
||||
|
||||
rankingInformationDisplay.ModMultiplier.Value = multiplier;
|
||||
rankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked);
|
||||
if (beatmapAttributesDisplay != null)
|
||||
beatmapAttributesDisplay.Mods.Value = ActiveMods.Value;
|
||||
}
|
||||
|
||||
private void updateCustomisation()
|
||||
|
@ -86,7 +86,10 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
modSettingsFlow.Clear();
|
||||
|
||||
foreach (var mod in SelectedMods.Value.AsOrdered())
|
||||
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
|
||||
// Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent),
|
||||
// which breaks user expectations when interacting with the overlay.
|
||||
foreach (var mod in SelectedMods.Value)
|
||||
{
|
||||
var settings = mod.CreateSettingsControls().ToList();
|
||||
|
||||
@ -110,10 +113,14 @@ namespace osu.Game.Overlays.Mods
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
||||
private partial class ModSettingsColumn : CompositeDrawable
|
||||
public partial class ModSettingsColumn : CompositeDrawable
|
||||
{
|
||||
public readonly Mod Mod;
|
||||
|
||||
public ModSettingsColumn(Mod mod, IEnumerable<Drawable> settingsControls)
|
||||
{
|
||||
Mod = mod;
|
||||
|
||||
Width = 250;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Padding = new MarginPadding { Bottom = 7 };
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -107,6 +108,9 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
|
||||
try
|
||||
{
|
||||
GlobalStatistics.OutputToLog();
|
||||
Logger.Flush();
|
||||
|
||||
var logStorage = Logger.Storage;
|
||||
|
||||
using (var outStream = storage.CreateFileSafely(archive_filename))
|
||||
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
@ -196,7 +195,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
var matrix = Matrix3.Identity;
|
||||
|
||||
MatrixExtensions.TranslateFromLeft(ref matrix, offset);
|
||||
MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value));
|
||||
MatrixExtensions.RotateFromLeft(ref matrix, float.DegreesToRadians(rotation.Value));
|
||||
|
||||
usableAreaQuad *= matrix;
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -69,7 +68,7 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
protected override bool AllowIme => false;
|
||||
|
||||
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
|
||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||
|
||||
public new void NotifyInputError() => base.NotifyInputError();
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Volume
|
||||
|
||||
Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true);
|
||||
|
||||
bgProgress.Current.Value = 0.75f;
|
||||
bgProgress.Progress = 0.75f;
|
||||
}
|
||||
|
||||
private int? displayVolumeInt;
|
||||
@ -265,8 +265,8 @@ namespace osu.Game.Overlays.Volume
|
||||
text.Text = intValue.ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
volumeCircle.Current.Value = displayVolume * 0.75f;
|
||||
volumeCircleGlow.Current.Value = displayVolume * 0.75f;
|
||||
volumeCircle.Progress = displayVolume * 0.75f;
|
||||
volumeCircleGlow.Progress = displayVolume * 0.75f;
|
||||
|
||||
if (intVolumeChanged && IsLoaded)
|
||||
Scheduler.AddOnce(playTickSound);
|
||||
|
@ -11,3 +11,6 @@ using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]
|
||||
[assembly: InternalsVisibleTo("osu.Game.Tests.iOS")]
|
||||
[assembly: InternalsVisibleTo("osu.Game.Tests.Android")]
|
||||
|
||||
// intended for Moq usage
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
|
@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils
|
||||
|
||||
public ReverseQueue(int initialCapacity)
|
||||
{
|
||||
if (initialCapacity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(initialCapacity));
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity);
|
||||
|
||||
items = new T[initialCapacity];
|
||||
capacity = initialCapacity;
|
||||
|
@ -40,8 +40,7 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
|
||||
public static PathType BSpline(int degree)
|
||||
{
|
||||
if (degree <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero.");
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(degree);
|
||||
|
||||
return new PathType { Type = SplineType.BSpline, Degree = degree };
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public partial class AccumulatingHealthProcessor : HealthProcessor
|
||||
{
|
||||
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth;
|
||||
|
||||
private readonly double requiredHealth;
|
||||
|
||||
|
@ -142,6 +142,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public event Func<bool>? Failed;
|
||||
|
||||
/// <summary>
|
||||
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
|
||||
/// Additional conditions on top of <see cref="CheckDefaultFailCondition"/> that cause a failing state.
|
||||
/// </summary>
|
||||
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
|
||||
|
||||
@ -69,9 +69,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
|
||||
|
||||
/// <summary>
|
||||
/// The default conditions for failing.
|
||||
/// Checks whether the default conditions for failing are met.
|
||||
/// </summary>
|
||||
protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
|
||||
/// <returns><see langword="true"/> if failure should be invoked.</returns>
|
||||
protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current state of <see cref="HealthProcessor"/> or the provided <paramref name="result"/> meets any fail condition.
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="result">The judgement result.</param>
|
||||
private bool meetsAnyFailCondition(JudgementResult result)
|
||||
{
|
||||
if (DefaultFailCondition)
|
||||
if (CheckDefaultFailCondition(result))
|
||||
return true;
|
||||
|
||||
if (FailConditions != null)
|
||||
|
@ -135,8 +135,7 @@ namespace osu.Game.Rulesets.UI
|
||||
protected DrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset)
|
||||
{
|
||||
if (beatmap == null)
|
||||
throw new ArgumentNullException(nameof(beatmap), "Beatmap cannot be null.");
|
||||
ArgumentNullException.ThrowIfNull(beatmap);
|
||||
|
||||
if (!(beatmap is Beatmap<TObject> tBeatmap))
|
||||
throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap));
|
||||
|
@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI
|
||||
double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime);
|
||||
|
||||
isCatchingUp.Value = timeBehind > 200;
|
||||
waitingOnFrames.Value = state == PlaybackState.NotValid;
|
||||
waitingOnFrames.Value = hasReplayAttached && state == PlaybackState.NotValid;
|
||||
|
||||
manualClock.CurrentTime = proposedTime;
|
||||
manualClock.Rate = Math.Abs(referenceClock.Rate) * direction;
|
||||
|
@ -42,8 +42,8 @@ namespace osu.Game.Scoring.Legacy
|
||||
{
|
||||
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),
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
ClientVersion = score.ClientVersion,
|
||||
};
|
||||
}
|
||||
|
@ -45,9 +45,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// </description></item>
|
||||
/// <item><description>30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores.</description></item>
|
||||
/// <item><description>30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000014;
|
||||
public const int LATEST_VERSION = 30000015;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
@ -198,10 +198,25 @@ namespace osu.Game.Scoring.Legacy
|
||||
}
|
||||
}
|
||||
|
||||
public static int? GetCountMiss(this ScoreInfo scoreInfo) =>
|
||||
getCount(scoreInfo, HitResult.Miss);
|
||||
public static int? GetCountMiss(this ScoreInfo scoreInfo)
|
||||
{
|
||||
switch (scoreInfo.Ruleset.OnlineID)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
case 3:
|
||||
return getCount(scoreInfo, HitResult.Miss);
|
||||
|
||||
case 2:
|
||||
return (getCount(scoreInfo, HitResult.Miss) ?? 0) + (getCount(scoreInfo, HitResult.LargeTickMiss) ?? 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void SetCountMiss(this ScoreInfo scoreInfo, int value) =>
|
||||
// this does not match the implementation of `GetCountMiss()` for catch,
|
||||
// but we physically cannot recover that data anymore at this point.
|
||||
scoreInfo.Statistics[HitResult.Miss] = value;
|
||||
|
||||
private static int? getCount(ScoreInfo scoreInfo, HitResult result)
|
||||
|
@ -9,6 +9,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.Menus
|
||||
{
|
||||
@ -21,7 +22,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
|
||||
TabContainer.RelativeSizeAxes &= ~Axes.X;
|
||||
TabContainer.AutoSizeAxes = Axes.X;
|
||||
TabContainer.Padding = new MarginPadding(10);
|
||||
TabContainer.Spacing = Vector2.Zero;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -42,30 +43,51 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
|
||||
private partial class TabItem : OsuTabItem
|
||||
{
|
||||
private const float transition_length = 250;
|
||||
private readonly Box background;
|
||||
private Color4 backgroundIdleColour;
|
||||
private Color4 backgroundHoverColour;
|
||||
|
||||
public TabItem(EditorScreenMode value)
|
||||
: base(value)
|
||||
{
|
||||
Text.Margin = new MarginPadding();
|
||||
Text.Margin = new MarginPadding(10);
|
||||
Text.Anchor = Anchor.CentreLeft;
|
||||
Text.Origin = Anchor.CentreLeft;
|
||||
|
||||
Text.Font = OsuFont.TorusAlternate;
|
||||
|
||||
Add(background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
|
||||
Bar.Expire();
|
||||
}
|
||||
|
||||
protected override void OnActivated()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
base.OnActivated();
|
||||
Bar.ScaleTo(new Vector2(1, 5), transition_length, Easing.OutQuint);
|
||||
backgroundIdleColour = colourProvider.Background2;
|
||||
backgroundHoverColour = colourProvider.Background1;
|
||||
}
|
||||
|
||||
protected override void OnDeactivated()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.OnDeactivated();
|
||||
Bar.ScaleTo(Vector2.One, transition_length, Easing.OutQuint);
|
||||
base.LoadComplete();
|
||||
background.Colour = backgroundIdleColour;
|
||||
}
|
||||
|
||||
protected override void FadeHovered()
|
||||
{
|
||||
base.FadeHovered();
|
||||
background.FadeColour(backgroundHoverColour, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void FadeUnhovered()
|
||||
{
|
||||
base.FadeUnhovered();
|
||||
background.FadeColour(backgroundIdleColour, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
Colour = this.baseColour = baseColour;
|
||||
|
||||
Current.Value = 1;
|
||||
Progress = 1;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -265,6 +266,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
return !Precision.AlmostIntersects(maskingBounds, rect);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= updateColour;
|
||||
}
|
||||
|
||||
private partial class Tick : Circle
|
||||
{
|
||||
public Tick()
|
||||
|
@ -366,7 +366,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = 1f / light_count - angular_light_gap },
|
||||
Progress = 1f / light_count - angular_light_gap,
|
||||
Colour = colourProvider.Background2,
|
||||
},
|
||||
fillContent = new Container
|
||||
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = 1f / light_count - angular_light_gap },
|
||||
Progress = 1f / light_count - angular_light_gap,
|
||||
Blending = BlendingParameters.Additive
|
||||
},
|
||||
// Please do not try and make sense of this.
|
||||
@ -388,7 +388,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Glow = new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = 1f / light_count - 0.01f },
|
||||
Progress = 1f / light_count - 0.01f,
|
||||
Blending = BlendingParameters.Additive
|
||||
}.WithEffect(new GlowEffect
|
||||
{
|
||||
|
@ -14,7 +14,6 @@ using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -209,13 +208,13 @@ namespace osu.Game.Screens.Menu
|
||||
if (audioData[i] < amplitude_dead_zone)
|
||||
continue;
|
||||
|
||||
float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
|
||||
float rotation = float.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
|
||||
float rotationCos = MathF.Cos(rotation);
|
||||
float rotationSin = MathF.Sin(rotation);
|
||||
// taking the cos and sin to the 0..1 range
|
||||
var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size;
|
||||
|
||||
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
|
||||
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
|
||||
// The distance between the position and the sides of the bar.
|
||||
var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2);
|
||||
// The distance between the bottom side of the bar and the top side.
|
||||
|
53
osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs
Normal file
53
osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs
Normal file
@ -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 System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Match
|
||||
{
|
||||
public partial class RoomModSelectOverlay : UserModSelectOverlay
|
||||
{
|
||||
[Resolved]
|
||||
private IBindable<PlaylistItem> selectedItem { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
private readonly List<Mod> roomRequiredMods = new List<Mod>();
|
||||
|
||||
public RoomModSelectOverlay()
|
||||
: base(OverlayColourScheme.Plum)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItem.BindValueChanged(v =>
|
||||
{
|
||||
roomRequiredMods.Clear();
|
||||
|
||||
if (v.NewValue is PlaylistItem item)
|
||||
{
|
||||
var rulesetInstance = rulesets.GetRuleset(item.RulesetID)?.CreateInstance();
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
roomRequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
|
||||
}
|
||||
|
||||
ActiveMods.Value = ComputeActiveMods();
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<Mod> ComputeActiveMods() => roomRequiredMods.Concat(base.ComputeActiveMods()).ToList();
|
||||
}
|
||||
}
|
@ -241,7 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
}
|
||||
};
|
||||
|
||||
LoadComponent(UserModsSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Plum)
|
||||
LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay
|
||||
{
|
||||
SelectedMods = { BindTarget = UserMods },
|
||||
IsValidMod = _ => false
|
||||
|
@ -122,8 +122,17 @@ namespace osu.Game.Screens.Play
|
||||
StopGameplayClock();
|
||||
}
|
||||
|
||||
protected virtual void StartGameplayClock() => GameplayClock.Start();
|
||||
protected virtual void StopGameplayClock() => GameplayClock.Stop();
|
||||
protected virtual void StartGameplayClock()
|
||||
{
|
||||
Logger.Log($"{nameof(GameplayClockContainer)} started via call to {nameof(StartGameplayClock)}");
|
||||
GameplayClock.Start();
|
||||
}
|
||||
|
||||
protected virtual void StopGameplayClock()
|
||||
{
|
||||
Logger.Log($"{nameof(GameplayClockContainer)} stopped via call to {nameof(StopGameplayClock)}");
|
||||
GameplayClock.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
|
||||
|
@ -198,9 +198,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
bind();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
circularProgress.Progress = Progress.Value;
|
||||
}
|
||||
|
||||
private void bind()
|
||||
{
|
||||
((IBindable<double>)circularProgress.Current).BindTo(Progress);
|
||||
Progress.ValueChanged += progress =>
|
||||
{
|
||||
icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f);
|
||||
|
@ -31,6 +31,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
/// </summary>
|
||||
public partial class AccuracyCircle : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The total duration of the animation.
|
||||
/// </summary>
|
||||
public const double TOTAL_DURATION = APPEAR_DURATION + ACCURACY_TRANSFORM_DELAY + ACCURACY_TRANSFORM_DURATION;
|
||||
|
||||
/// <summary>
|
||||
/// Duration for the transforms causing this component to appear.
|
||||
/// </summary>
|
||||
@ -147,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
Colour = OsuColour.Gray(47),
|
||||
Alpha = 0.5f,
|
||||
InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle
|
||||
Current = { Value = 1 },
|
||||
Progress = 1,
|
||||
},
|
||||
accuracyCircle = new CircularProgress
|
||||
{
|
||||
@ -189,11 +194,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
rankText = new RankText(score.Rank)
|
||||
};
|
||||
|
||||
if (isFailedSDueToMisses)
|
||||
AddInternal(failedSRankText = new RankText(ScoreRank.S));
|
||||
|
||||
if (withFlair)
|
||||
{
|
||||
if (isFailedSDueToMisses)
|
||||
AddInternal(failedSRankText = new RankText(ScoreRank.S));
|
||||
|
||||
var applauseSamples = new List<string> { applauseSampleName };
|
||||
if (score.Rank >= ScoreRank.B)
|
||||
// when rank is B or higher, play legacy applause sample on legacy skins.
|
||||
@ -268,7 +273,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
|
||||
targetAccuracy -= visual_alignment_offset;
|
||||
|
||||
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
|
||||
accuracyCircle.ProgressTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
|
||||
|
||||
if (withFlair)
|
||||
{
|
||||
@ -321,24 +326,25 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
{
|
||||
rankText.Appear();
|
||||
|
||||
if (!withFlair) return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
isTicking = false;
|
||||
rankImpactSound.Play();
|
||||
});
|
||||
|
||||
const double applause_pre_delay = 545f;
|
||||
const double applause_volume = 0.8f;
|
||||
|
||||
using (BeginDelayedSequence(applause_pre_delay))
|
||||
if (withFlair)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
rankApplauseSound.VolumeTo(applause_volume);
|
||||
rankApplauseSound.Play();
|
||||
isTicking = false;
|
||||
rankImpactSound.Play();
|
||||
});
|
||||
|
||||
const double applause_pre_delay = 545f;
|
||||
const double applause_volume = 0.8f;
|
||||
|
||||
using (BeginDelayedSequence(applause_pre_delay))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
rankApplauseSound.VolumeTo(applause_volume);
|
||||
rankApplauseSound.Play();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,7 +365,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
.FadeOut(800, Easing.Out);
|
||||
|
||||
accuracyCircle
|
||||
.FillTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
|
||||
.ProgressTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
|
||||
|
||||
badges.Single(b => b.Rank == getRank(ScoreRank.S))
|
||||
.FadeOut(70, Easing.OutQuint);
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||
{
|
||||
public double RevealProgress
|
||||
{
|
||||
set => Current.Value = Math.Clamp(value, startProgress, endProgress) - startProgress;
|
||||
set => Progress = Math.Clamp(value, startProgress, endProgress) - startProgress;
|
||||
}
|
||||
|
||||
private readonly double startProgress;
|
||||
|
@ -25,8 +25,10 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking.Expanded.Accuracy;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osuTK;
|
||||
|
||||
@ -41,6 +43,8 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public override bool? AllowGlobalTrackControl => true;
|
||||
|
||||
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
|
||||
|
||||
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
||||
|
||||
[CanBeNull]
|
||||
@ -172,6 +176,10 @@ namespace osu.Game.Screens.Ranking
|
||||
bool shouldFlair = player != null && !Score.User.IsBot;
|
||||
|
||||
ScorePanelList.AddScore(Score, shouldFlair);
|
||||
// this is mostly for medal display.
|
||||
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
|
||||
// to give the results screen enough time to play the animation out before the medals can be shown.
|
||||
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
|
||||
}
|
||||
|
||||
if (AllowWatchingReplay)
|
||||
|
@ -33,8 +33,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
/// <param name="items">The <see cref="SimpleStatisticItem"/>s to display in this row.</param>
|
||||
public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable<SimpleStatisticItem> items)
|
||||
{
|
||||
if (columnCount < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(columnCount));
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount);
|
||||
|
||||
this.columnCount = columnCount;
|
||||
this.items = items.ToArray();
|
||||
|
@ -72,14 +72,14 @@ namespace osu.Game.Skinning
|
||||
circularProgress.Scale = new Vector2(-1, 1);
|
||||
circularProgress.Anchor = Anchor.TopRight;
|
||||
circularProgress.Colour = new Colour4(199, 255, 47, 153);
|
||||
circularProgress.Current.Value = 1 - progress;
|
||||
circularProgress.Progress = 1 - progress;
|
||||
}
|
||||
else
|
||||
{
|
||||
circularProgress.Scale = new Vector2(1);
|
||||
circularProgress.Anchor = Anchor.TopLeft;
|
||||
circularProgress.Colour = new Colour4(255, 255, 255, 153);
|
||||
circularProgress.Current.Value = progress;
|
||||
circularProgress.Progress = progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
private TextureUpload convertToGrayscale(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// stable uses `0.299 * r + 0.587 * g + 0.114 * b`
|
||||
// (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153)
|
||||
|
@ -61,7 +61,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics;
|
||||
@ -46,32 +45,10 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsStartTime
|
||||
{
|
||||
get
|
||||
{
|
||||
double min = double.MaxValue;
|
||||
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
min = Math.Min(min, timelines[i].StartTime);
|
||||
|
||||
return min;
|
||||
}
|
||||
}
|
||||
public double CommandsStartTime => timelines.Min(static t => t.StartTime);
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsEndTime
|
||||
{
|
||||
get
|
||||
{
|
||||
double max = double.MinValue;
|
||||
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
max = Math.Max(max, timelines[i].EndTime);
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
public double CommandsEndTime => timelines.Max(static t => t.EndTime);
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsDuration => CommandsEndTime - CommandsStartTime;
|
||||
@ -83,19 +60,7 @@ namespace osu.Game.Storyboards
|
||||
public virtual double EndTime => CommandsEndTime;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
{
|
||||
if (timelines[i].HasCommands)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public bool HasCommands => timelines.Any(static t => t.HasCommands);
|
||||
|
||||
public virtual IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
|
||||
{
|
||||
|
@ -85,12 +85,23 @@ namespace osu.Game.Storyboards
|
||||
{
|
||||
get
|
||||
{
|
||||
double latestEndTime = TimelineGroup.EndTime;
|
||||
double latestEndTime = double.MaxValue;
|
||||
|
||||
// Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex.
|
||||
if (loops.Count == 0)
|
||||
{
|
||||
// Take the minimum time of all the potential "death" reasons.
|
||||
latestEndTime = calculateOptimisedEndTime(TimelineGroup);
|
||||
}
|
||||
|
||||
// If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue
|
||||
// and thus conservativeEndTime will be used.
|
||||
double conservativeEndTime = TimelineGroup.EndTime;
|
||||
|
||||
foreach (var l in loops)
|
||||
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
|
||||
conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
|
||||
|
||||
return latestEndTime;
|
||||
return Math.Min(latestEndTime, conservativeEndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,6 +205,47 @@ namespace osu.Game.Storyboards
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup)
|
||||
{
|
||||
// Here we are starting from maximum value and trying to minimise the end time on each step.
|
||||
// There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0.
|
||||
double[] deathTimes =
|
||||
{
|
||||
double.MaxValue, // alpha
|
||||
double.MaxValue, // colour alpha
|
||||
double.MaxValue, // scale
|
||||
double.MaxValue, // scale x
|
||||
double.MaxValue, // scale y
|
||||
};
|
||||
|
||||
// The loops below are following the same pattern.
|
||||
// We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row
|
||||
// so we are saving the earliest of them.
|
||||
foreach (var alphaCommand in timelineGroup.Alpha.Commands)
|
||||
{
|
||||
if (alphaCommand.EndValue == 0)
|
||||
// commands are ordered by the start time, however end time may vary. Save the earliest.
|
||||
deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]);
|
||||
else
|
||||
// If value isn't 0 (sprite becomes visible again), revert the saved state.
|
||||
deathTimes[0] = double.MaxValue;
|
||||
}
|
||||
|
||||
foreach (var colourCommand in timelineGroup.Colour.Commands)
|
||||
deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue;
|
||||
|
||||
foreach (var scaleCommand in timelineGroup.Scale.Commands)
|
||||
deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue;
|
||||
|
||||
foreach (var scaleCommand in timelineGroup.VectorScale.Commands)
|
||||
{
|
||||
deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue;
|
||||
deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue;
|
||||
}
|
||||
|
||||
return deathTimes.Min();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Path}, {Origin}, {InitialPosition}";
|
||||
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
@ -28,8 +27,8 @@ namespace osu.Game.Utils
|
||||
point.Y -= origin.Y;
|
||||
|
||||
Vector2 ret;
|
||||
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
||||
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
||||
ret.X = point.X * MathF.Cos(float.DegreesToRadians(angle)) + point.Y * MathF.Sin(float.DegreesToRadians(angle));
|
||||
ret.Y = point.X * -MathF.Sin(float.DegreesToRadians(angle)) + point.Y * MathF.Cos(float.DegreesToRadians(angle));
|
||||
|
||||
ret.X += origin.X;
|
||||
ret.Y += origin.Y;
|
||||
|
@ -35,8 +35,7 @@ namespace osu.Game.Utils
|
||||
/// <param name="capacity">The number of items the queue can hold.</param>
|
||||
public LimitedCapacityQueue(int capacity)
|
||||
{
|
||||
if (capacity < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
|
||||
this.capacity = capacity;
|
||||
array = new T[capacity];
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user