1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-09 01:03:55 +08:00
Files
osu-lazer/osu.Game.Tests/Visual/RankedPlay/TestSceneResultsScreen.cs
T
Dan Balasescu c73af24bbc Ranked Play: Add damage breakdown and individual multipliers (#37740)
- Added a small breakdown animation to the results screen.
- Added individual multiplier text to user corner pieces.
- Removed global multiplier text from the stage overlay, since we're
going with individual multipliers.


https://github.com/user-attachments/assets/47cec478-6ad5-49fa-9f69-b6df079ce41c

(This is dev design and I'm focusing on functionality rather than
presentation for now.)

The implementation might be over-engineered a bit, but I'm not sure on
the final structure of things and I want to give a bit of elasticity to
the system, so I've frankensteined a new "damage sources" list inside
`RankedPlayDamageInfo` that the results screen uses to display the
breakdown.

If the server doesn't provide a breakdown (e.g. by client and server
being slightly out-of-date), the results screen will behave as it does
on current `master`. In other words this is forwards/backwards
compatible.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
2026-05-18 15:39:46 +09:00

353 lines
13 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneResultsScreen : MultiplayerTestScene
{
private RankedPlayScreen screen = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay)));
WaitForJoined();
AddStep("add other user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(2)));
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
AddUntilStep("screen loaded", () => screen.IsLoaded);
setupRequestHandler();
}
[Test]
public void TestBasic()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 123_456,
Damage = 123_456,
OldLife = 500_000,
NewLife = 500_000 - 123_456
};
userInfo.Life = 500_000 - 123_456;
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
}
[Test]
public void TestMultiplier()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
state.DamageMultiplier = 1.5;
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageMultiplier = 0.5;
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 123_456,
Damage = 123_456 * 2,
OldLife = 1_000_000,
NewLife = 1_000_000 - 123_456 * 2,
Multiplier = 2,
DirectDamage = 123_456,
};
userInfo.Life = 1_000_000 - 123_456 * 2;
}
else
{
userInfo.DamageMultiplier = 0.5;
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
Multiplier = 2,
DirectDamage = 0,
};
}
}
}).WaitSafely());
}
[Test]
public void TestMissingScores()
{
AddStep("setup request handler", () =>
{
Func<APIRequest, bool>? defaultRequestHandler = ((DummyAPIAccess)API).HandleRequest;
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case IndexPlaylistScoresRequest index:
index.TriggerSuccess(new IndexedMultiplayerScores());
return true;
default:
return defaultRequestHandler?.Invoke(request) ?? false;
}
};
});
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
state.DamageMultiplier = 2;
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 123_456,
Damage = 123_456 * 2,
OldLife = 1_000_000,
NewLife = 1_000_000 - 123_456 * 2,
};
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
}
[Test]
[Explicit("Test exercises correct stopping of audio playback. Has no assertions, only useful when checked manually by a human.")]
public void TestAllSamplesStopOnExit()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 123_456,
Damage = 123_456,
OldLife = 500_000,
NewLife = 500_000 - 123_456,
};
userInfo.Life = 500_000 - 123_456;
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
AddWaitStep("wait for samples to start playing", 5);
AddRepeatStep("exit", () => screen.Exit(), 2);
}
[Test]
public void TestDamageBreakdown()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 600_000,
Damage = 1_100_000,
OldLife = 1_000_000,
NewLife = 1,
DirectDamage = 500_000,
Multiplier = 2,
BonusDamage = 100_000
};
userInfo.Life = 653_088;
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
}
[Test]
public void TestDamageBreakdownWithNegativeValues()
{
AddStep("set results state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.Results, state =>
{
int losingPlayer = state.Users.Keys.First();
foreach (var (id, userInfo) in state.Users)
{
if (id == losingPlayer)
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 500_000,
Damage = 200_000,
OldLife = 1_000_000,
NewLife = 800_000,
DirectDamage = 600_000,
Multiplier = 0.5,
BonusDamage = -100_000
};
userInfo.Life = 653_088;
}
else
{
userInfo.DamageInfo = new RankedPlayDamageInfo
{
RawDamage = 0,
Damage = 0,
OldLife = 1_000_000,
NewLife = 1_000_000,
};
}
}
}).WaitSafely());
}
private void setupRequestHandler()
{
AddStep("setup request handler", () =>
{
Func<APIRequest, bool>? defaultRequestHandler = ((DummyAPIAccess)API).HandleRequest;
((DummyAPIAccess)API).HandleRequest = request =>
{
switch (request)
{
case IndexPlaylistScoresRequest index:
var result = new IndexedMultiplayerScores();
foreach (int userId in new[] { 2, API.LocalUser.Value.OnlineID })
{
result.Scores.Add(new MultiplayerScore
{
ID = userId,
Accuracy = RNG.NextSingle(),
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
MaxCombo = RNG.Next(1000),
TotalScore = userId == 2 ? 750_000 : 750_000 - 123_456,
Statistics = new Dictionary<HitResult, int>
{
[HitResult.Miss] = 1,
[HitResult.Meh] = 50,
[HitResult.Ok] = 100,
[HitResult.Good] = 200,
[HitResult.Great] = 300,
[HitResult.Perfect] = 320,
[HitResult.SmallTickHit] = 50,
[HitResult.SmallTickMiss] = 25,
[HitResult.LargeTickHit] = 100,
[HitResult.LargeTickMiss] = 50,
[HitResult.SmallBonus] = 10,
[HitResult.LargeBonus] = 50
},
MaximumStatistics = new Dictionary<HitResult, int>
{
[HitResult.Perfect] = 971,
[HitResult.SmallTickHit] = 75,
[HitResult.LargeTickHit] = 150,
[HitResult.SmallBonus] = 10,
[HitResult.LargeBonus] = 50,
},
User = new APIUser
{
Id = userId,
Username = $"user {userId}",
}
});
}
index.TriggerSuccess(result);
return true;
default:
return defaultRequestHandler?.Invoke(request) ?? false;
}
};
});
}
}
}