mirror of
https://github.com/ppy/osu.git
synced 2026-06-08 19:24:22 +08:00
Compare commits
21 Commits
@@ -50,7 +50,7 @@ jobs:
|
||||
exit $exit_code
|
||||
|
||||
- name: InspectCode
|
||||
uses: JetBrains/ReSharper-InspectCode@v0.11
|
||||
uses: JetBrains/ReSharper-InspectCode@v0.12
|
||||
with:
|
||||
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true`
|
||||
build: false
|
||||
@@ -101,14 +101,42 @@ jobs:
|
||||
NUnit.ConsoleOut=0
|
||||
|
||||
# Attempt to upload results even if test fails.
|
||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||
# https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#cancelled
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
|
||||
|
||||
test-results:
|
||||
name: Test results
|
||||
runs-on: ubuntu-latest
|
||||
# we want to wait for the `test` job to complete, but run regardless of whether it succeeds or fails
|
||||
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-not-requiring-successful-dependent-jobs
|
||||
if: ${{ !cancelled() }}
|
||||
needs: test
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: osu-test-results-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Add test results summary to workflow run
|
||||
uses: dorny/test-reporter@v3.0.0
|
||||
with:
|
||||
name: Results
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
use-actions-summary: 'true'
|
||||
|
||||
build-only-android:
|
||||
name: Build only (Android)
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
|
||||
# See:
|
||||
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
|
||||
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
|
||||
name: Annotate CI run with test results
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Continuous Integration" ]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
annotate:
|
||||
name: Annotate CI run with test results
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Download results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: osu-test-results-*
|
||||
merge-multiple: true
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v2.6.0
|
||||
with:
|
||||
name: Results
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
Vendored
+13
@@ -13,6 +13,19 @@
|
||||
"preLaunchTask": "Build osu! (Debug)",
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "osu! (Debug, Second Client)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
|
||||
"--debug-client-id=1"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"preLaunchTask": "Build osu! (Debug)",
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "osu! (Release)",
|
||||
"type": "coreclr",
|
||||
|
||||
@@ -155,6 +155,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModMuted(),
|
||||
new CatchModNoScope(),
|
||||
new CatchModMovingFast(),
|
||||
new CatchModSynesthesia(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
|
||||
/// </summary>
|
||||
public class CatchModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
|
||||
{
|
||||
private readonly OsuColour colours = new OsuColour();
|
||||
|
||||
private IBeatmap? currentBeatmap { get; set; }
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
//Store a reference to the current beatmap to look up the beat divisor when notes are drawn
|
||||
if (currentBeatmap != beatmap)
|
||||
currentBeatmap = beatmap;
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
if (currentBeatmap == null) return;
|
||||
|
||||
Color4? timingBasedColour = null;
|
||||
|
||||
d.HitObjectApplied += _ =>
|
||||
{
|
||||
// Block bananas from getting coloured.
|
||||
if (d.HitObject is not Banana)
|
||||
{
|
||||
timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours);
|
||||
}
|
||||
|
||||
// Colour droplets into a solid colour, as droplets aren't generated snapped to timeline ticks.
|
||||
if (d.HitObject is Droplet)
|
||||
{
|
||||
timingBasedColour = Color4.LightGreen;
|
||||
}
|
||||
};
|
||||
|
||||
// Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
|
||||
d.OnUpdate += _ =>
|
||||
{
|
||||
if (timingBasedColour != null)
|
||||
d.AccentColour.Value = timingBasedColour.Value;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
TimeRange.Value = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
|
||||
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
|
||||
base.Update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public void MissForcefully() => ApplyMinResult();
|
||||
|
||||
// ReSharper disable once FunctionRecursiveOnAllPaths (TODO: remove after fixed https://youtrack.jetbrains.com/issue/RIDER-135036/Incorrect-recursive-on-all-execution-paths-inspection)
|
||||
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -65,6 +65,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // one fix attempted in https://github.com/ppy/osu/pull/37178, didn't work
|
||||
public void TestInvalidationFlow()
|
||||
{
|
||||
BeatmapInfo postEditBeatmapInfo = null;
|
||||
|
||||
@@ -187,6 +187,7 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
|
||||
{
|
||||
RulesetInfo rulesetInfo = null!;
|
||||
|
||||
@@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestLengthAndStarRatingUpdated()
|
||||
{
|
||||
WorkingBeatmap working = null;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestLocallyModifyingOnlineBeatmap()
|
||||
{
|
||||
string initialHash = string.Empty;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
@@ -140,14 +139,16 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private void setUpEditor(RulesetInfo ruleset)
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
BeatmapSetInfo? beatmapSet = null;
|
||||
|
||||
AddStep("Import test beatmap", () =>
|
||||
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
|
||||
);
|
||||
AddStep("Retrieve beatmap", () =>
|
||||
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
|
||||
);
|
||||
AddUntilStep("Retrieve beatmap", () =>
|
||||
{
|
||||
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected)?.Value.Detach();
|
||||
return beatmapSet != null;
|
||||
});
|
||||
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("Wait for song select", () =>
|
||||
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
@@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
|
||||
AddStep("Open editor for ruleset", () =>
|
||||
((SoloSongSelect)Game.ScreenStack.CurrentScreen)
|
||||
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
|
||||
.Edit(beatmapSet!.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
|
||||
);
|
||||
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private bool seek;
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
[Ignore("Still failing even with [FlakyTest] applied.")]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
DrawableSlider? slider = null;
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
|
||||
|
||||
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
|
||||
ReplaySettingsOverlay settingsOverlay() => Player.ChildrenOfType<ReplaySettingsOverlay>().Single();
|
||||
}
|
||||
|
||||
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
|
||||
|
||||
@@ -331,7 +331,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
p.RequestResults = _ => resultsRequested = true;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
|
||||
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any()
|
||||
&& playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().Any()
|
||||
&& playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
|
||||
|
||||
AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First()));
|
||||
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
|
||||
|
||||
@@ -50,47 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
|
||||
Dependencies.CacheAs(availabilityTracker.Object);
|
||||
|
||||
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
|
||||
|
||||
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
|
||||
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
|
||||
|
||||
// By default, the local user is to be the host.
|
||||
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
|
||||
|
||||
// Assume all state changes are accepted by the server.
|
||||
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
|
||||
.Callback((MultiplayerUserState r) =>
|
||||
{
|
||||
Logger.Log($"Changing local user state from {localUser.State} to {r}");
|
||||
localUser.State = r;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
|
||||
multiplayerClient.Setup(m => m.StartMatch())
|
||||
.Callback(() =>
|
||||
{
|
||||
multiplayerClient.Raise(m => m.LoadRequested -= null);
|
||||
|
||||
// immediately "end" gameplay, as we don't care about that part of the process.
|
||||
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
|
||||
});
|
||||
|
||||
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
|
||||
.Callback((MatchUserRequest request) =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case StartMatchCountdownRequest countdownStart:
|
||||
setRoomCountdown(countdownStart.Duration);
|
||||
break;
|
||||
|
||||
case StopCountdownRequest:
|
||||
clearRoomCountdown();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
ongoingOperationTracker,
|
||||
@@ -103,10 +62,51 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("reset state", () =>
|
||||
{
|
||||
multiplayerClient.Invocations.Clear();
|
||||
multiplayerClient.Reset();
|
||||
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
|
||||
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
|
||||
|
||||
// By default, the local user is to be the host.
|
||||
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
|
||||
|
||||
// Assume all state changes are accepted by the server.
|
||||
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
|
||||
.Callback((MultiplayerUserState r) =>
|
||||
{
|
||||
Logger.Log($"Changing local user state from {localUser.State} to {r}");
|
||||
localUser.State = r;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
|
||||
multiplayerClient.Setup(m => m.StartMatch())
|
||||
.Callback(() =>
|
||||
{
|
||||
multiplayerClient.Raise(m => m.LoadRequested -= null);
|
||||
|
||||
// immediately "end" gameplay, as we don't care about that part of the process.
|
||||
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
|
||||
});
|
||||
|
||||
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
|
||||
.Callback((MatchUserRequest request) =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case StartMatchCountdownRequest countdownStart:
|
||||
setRoomCountdown(countdownStart.Duration);
|
||||
break;
|
||||
|
||||
case StopCountdownRequest:
|
||||
clearRoomCountdown();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
|
||||
|
||||
availabilityTracker.Reset();
|
||||
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
|
||||
|
||||
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||
@@ -375,6 +375,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
[Test]
|
||||
public void TestAbortMatch()
|
||||
{
|
||||
setUpMatchCallbacks();
|
||||
|
||||
// Ready
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
// Start match
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
// Abort
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
|
||||
}
|
||||
|
||||
private void setUpMatchCallbacks()
|
||||
{
|
||||
AddStep("setup client", () =>
|
||||
{
|
||||
@@ -383,6 +399,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
multiplayerClient.Raise(m => m.LoadRequested -= null);
|
||||
multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
|
||||
raiseRoomUpdated();
|
||||
|
||||
// The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
|
||||
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
|
||||
@@ -395,19 +412,133 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Ready
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
[Test]
|
||||
public void TestRefereeSpectating()
|
||||
{
|
||||
AddStep("set up referee", () =>
|
||||
{
|
||||
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
|
||||
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
|
||||
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
|
||||
// Start match
|
||||
const int users = 10;
|
||||
|
||||
AddStep("add many users", () =>
|
||||
{
|
||||
for (int i = 0; i < users; i++)
|
||||
addUser(new APIUser { Id = i, Username = "Another user" });
|
||||
});
|
||||
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("move to spectate", () => changeUserState(multiplayerClient.Object.LocalUser!.UserID, MultiplayerUserState.Spectating));
|
||||
|
||||
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
|
||||
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
|
||||
|
||||
setUpMatchCallbacks();
|
||||
|
||||
// start match
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
// Abort
|
||||
// abort
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRefereeFlowWithoutCountdown()
|
||||
{
|
||||
AddStep("set up referee", () =>
|
||||
{
|
||||
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
|
||||
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
|
||||
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
|
||||
const int users = 10;
|
||||
|
||||
AddStep("add many users", () =>
|
||||
{
|
||||
for (int i = 0; i < users; i++)
|
||||
addUser(new APIUser { Id = i, Username = "Another user" });
|
||||
});
|
||||
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
|
||||
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
|
||||
|
||||
setUpMatchCallbacks();
|
||||
|
||||
// start match
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
// abort
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRefereeFlowWithCountdown()
|
||||
{
|
||||
AddStep("set up referee", () =>
|
||||
{
|
||||
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
|
||||
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
|
||||
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
|
||||
raiseRoomUpdated();
|
||||
});
|
||||
|
||||
const int users = 10;
|
||||
|
||||
AddStep("add many users", () =>
|
||||
{
|
||||
for (int i = 0; i < users; i++)
|
||||
addUser(new APIUser { Id = i, Username = "Another user" });
|
||||
});
|
||||
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
|
||||
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
|
||||
|
||||
setUpMatchCallbacks();
|
||||
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("check request received", () =>
|
||||
{
|
||||
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
|
||||
req.Duration == TimeSpan.FromSeconds(10)
|
||||
)), Times.Once);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the cancel button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("check request received", () =>
|
||||
{
|
||||
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
|
||||
});
|
||||
}
|
||||
|
||||
private void verifyGameplayStartFlow()
|
||||
{
|
||||
checkLocalUserState(MultiplayerUserState.Ready);
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
// components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
|
||||
// therefore use until step rather than direct assert to account for that.
|
||||
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
|
||||
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
|
||||
!p.ChildrenOfType<ReplaySettingsOverlay>().Any() &&
|
||||
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
|
||||
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
|
||||
|
||||
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
|
||||
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().All(o => o.State.Value == Visibility.Hidden));
|
||||
|
||||
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
|
||||
@@ -353,6 +353,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSettingsButtonAlwaysVisibleForReferee()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
];
|
||||
});
|
||||
AddStep("setup referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddUntilStep("button visible", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
|
||||
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
|
||||
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserModSelectUpdatesWhenNotVisible()
|
||||
{
|
||||
|
||||
@@ -32,10 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
private void setUpList()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
createNewParticipantsList();
|
||||
@@ -44,6 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestAddUser()
|
||||
{
|
||||
setUpList();
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
@@ -59,6 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestAddReferee()
|
||||
{
|
||||
setUpList();
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(3)
|
||||
@@ -78,6 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestAddUnresolvedUser()
|
||||
{
|
||||
setUpList();
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser());
|
||||
@@ -94,6 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestRemoveUser()
|
||||
{
|
||||
setUpList();
|
||||
|
||||
APIUser? secondUser = null;
|
||||
|
||||
AddStep("add a user", () =>
|
||||
@@ -114,6 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestGameStateHasPriorityOverDownloadState()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
|
||||
checkProgressBarVisibility(true);
|
||||
|
||||
@@ -128,6 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestCorrectInitialState()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
|
||||
createNewParticipantsList();
|
||||
checkProgressBarVisibility(true);
|
||||
@@ -136,6 +141,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestBeatmapDownloadingStates()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
|
||||
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
|
||||
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
|
||||
@@ -159,6 +165,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestToggleReadyState()
|
||||
{
|
||||
setUpList();
|
||||
AddAssert("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
|
||||
|
||||
AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready));
|
||||
@@ -171,6 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestToggleSpectateState()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating));
|
||||
AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle));
|
||||
}
|
||||
@@ -178,6 +186,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestCrownChangesStateWhenHostTransferred()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
@@ -201,6 +210,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestHostGetsPinnedToTop()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
@@ -218,8 +228,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKickButtonOnlyPresentWhenHost()
|
||||
public void TestKickButtonPresentWhenHost()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
@@ -238,9 +249,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKickButtonPresentWhenReferee()
|
||||
{
|
||||
AddStep("set up referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
|
||||
setUpList();
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
Username = "Second",
|
||||
CoverUrl = TestResources.COVER_IMAGE_3,
|
||||
}));
|
||||
|
||||
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
|
||||
|
||||
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
|
||||
|
||||
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
|
||||
|
||||
AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
|
||||
|
||||
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKickButtonKicks()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 3,
|
||||
@@ -258,6 +293,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
const int users_count = 200;
|
||||
|
||||
setUpList();
|
||||
AddStep("add many users", () =>
|
||||
{
|
||||
for (int i = 0; i < users_count; i++)
|
||||
@@ -316,6 +352,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestUserWithMods()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
@@ -353,6 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestUserWithStyle()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add users", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
@@ -380,6 +418,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestModOverlap()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add dummy mods", () =>
|
||||
{
|
||||
MultiplayerClient.ChangeUserMods(new Mod[]
|
||||
@@ -438,6 +477,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestModsAndRuleset()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("add another user", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
@@ -472,6 +512,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestTeams()
|
||||
{
|
||||
setUpList();
|
||||
AddStep("enable teams", () => MultiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus));
|
||||
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
|
||||
|
||||
|
||||
@@ -41,10 +41,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Dependencies.Cache(Realm);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
private void setUpRoom()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
@@ -80,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestDeleteButtonAlwaysVisibleForHost()
|
||||
{
|
||||
setUpRoom();
|
||||
|
||||
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
|
||||
|
||||
@@ -92,6 +92,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
|
||||
{
|
||||
setUpRoom();
|
||||
|
||||
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
|
||||
|
||||
@@ -108,9 +110,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertDeleteButtonVisibility(2, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeleteButtonAlwaysVisibleForReferee()
|
||||
{
|
||||
AddStep("ensure host will be referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
|
||||
setUpRoom();
|
||||
|
||||
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 }));
|
||||
|
||||
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
|
||||
|
||||
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
|
||||
assertDeleteButtonVisibility(1, true);
|
||||
addPlaylistItem(() => 1234);
|
||||
assertDeleteButtonVisibility(2, true);
|
||||
|
||||
AddStep("set host only queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.HostOnly);
|
||||
AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234));
|
||||
|
||||
assertDeleteButtonVisibility(1, true);
|
||||
assertDeleteButtonVisibility(2, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleItemDoesNotHaveDeleteButton()
|
||||
{
|
||||
setUpRoom();
|
||||
|
||||
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
|
||||
|
||||
@@ -120,6 +148,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestCurrentItemHasDeleteButtonIfNotSingle()
|
||||
{
|
||||
setUpRoom();
|
||||
|
||||
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
|
||||
|
||||
@@ -139,6 +169,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestChangeExistingItem()
|
||||
{
|
||||
setUpRoom();
|
||||
|
||||
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = playlist.Items[0].ID,
|
||||
|
||||
@@ -472,7 +472,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
void checkScrollSpeed(double configValue, double gameplayValue)
|
||||
{
|
||||
AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue));
|
||||
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().ScrollingInfo.TimeRange.Value,
|
||||
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange,
|
||||
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue)));
|
||||
}
|
||||
|
||||
@@ -653,6 +653,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestDeleteScoreAfterPlaying()
|
||||
{
|
||||
playToResults();
|
||||
|
||||
@@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
|
||||
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
|
||||
|
||||
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
|
||||
ReplaySettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<ReplaySettingsOverlay>().SingleOrDefault();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers.Markdown;
|
||||
using osu.Game.Graphics.Containers.Markdown.Footnotes;
|
||||
using osu.Game.Overlays;
|
||||
@@ -143,98 +142,11 @@ outdated: true # not sure about the format for ""list of mods"".
|
||||
AddAssert("No notice box visible", () => !markdownContainer.ChildrenOfType<Container>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAbsoluteImage()
|
||||
{
|
||||
AddStep("Add absolute image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = "";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRelativeImage()
|
||||
{
|
||||
AddStep("Add relative image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = "";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockImage()
|
||||
{
|
||||
AddStep("Add paragraph with block image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = @"Line before image
|
||||
|
||||

|
||||
|
||||
Line after image";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInlineImage()
|
||||
{
|
||||
AddStep("Add inline image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = " osu!";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTableWithImageContent()
|
||||
{
|
||||
AddStep("Add Table", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = @"
|
||||
| Image | Name | Effect |
|
||||
| :-: | :-: | :-- |
|
||||
|  | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
|
||||
|  | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
|
||||
|  | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
|
||||
|   | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
|
||||
|  | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
|  | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
";
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWideImageNotExceedContainer()
|
||||
{
|
||||
AddStep("Add image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
|
||||
markdownContainer.Text = "";
|
||||
});
|
||||
|
||||
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
|
||||
|
||||
AddStep("Change container width", () =>
|
||||
{
|
||||
markdownContainer.Width = 0.5f;
|
||||
});
|
||||
|
||||
AddAssert("Image not exceed container width", () =>
|
||||
{
|
||||
var spriteImage = markdownContainer.ChildrenOfType<Sprite>().First();
|
||||
return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlag()
|
||||
{
|
||||
AddStep("Add flag", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
|
||||
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
|
||||
});
|
||||
AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType<DrawableFlag>().Count(), () => Is.EqualTo(2));
|
||||
|
||||
@@ -75,14 +75,14 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
}
|
||||
|
||||
private double flushInterval = 1000;
|
||||
private double recordInterval = 25;
|
||||
private double recordInterval = 50;
|
||||
private double fixedLatency;
|
||||
private double maxLatency;
|
||||
|
||||
[Test]
|
||||
public void TestCardHandReplay()
|
||||
{
|
||||
AddSliderStep("record interval", 0.0, 1000.0, 25.0, value =>
|
||||
AddSliderStep("record interval", 0.0, 1000.0, 50.0, value =>
|
||||
{
|
||||
recordInterval = value;
|
||||
recreateRecorder();
|
||||
|
||||
@@ -6,6 +6,7 @@ using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
@@ -33,12 +34,23 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
};
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
{
|
||||
AddStep("reset card hand", () => Child = handOfCards = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleSelectionMode()
|
||||
{
|
||||
AddStep("add cards", () =>
|
||||
{
|
||||
handOfCards.Clear();
|
||||
for (int i = 0; i < 5; i++)
|
||||
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
|
||||
});
|
||||
@@ -59,7 +71,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
{
|
||||
AddStep("add cards", () =>
|
||||
{
|
||||
handOfCards.Clear();
|
||||
for (int i = 0; i < 5; i++)
|
||||
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
|
||||
});
|
||||
@@ -84,7 +95,13 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
|
||||
AddStep($"{i} {"cards".Pluralize(i == 1)}", () =>
|
||||
{
|
||||
handOfCards.Clear();
|
||||
Child = handOfCards = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
};
|
||||
|
||||
for (int j = 0; j < numCards; j++)
|
||||
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
|
||||
@@ -138,7 +155,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
{
|
||||
AddStep("add cards", () =>
|
||||
{
|
||||
handOfCards.Clear();
|
||||
for (int i = 0; i < 5; i++)
|
||||
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
|
||||
});
|
||||
@@ -157,5 +173,24 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
AddAssert("card selected", () => handOfCards.Selection.Contains(handOfCards.Cards.ElementAt(i1).Card.Item));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContract()
|
||||
{
|
||||
AddStep("add cards", () =>
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
|
||||
});
|
||||
AddWaitStep("wait", 5);
|
||||
AddStep("contract", () => handOfCards.Contract());
|
||||
AddWaitStep("wait", 5);
|
||||
AddAssert(
|
||||
"all cards outside bounds", () =>
|
||||
handOfCards
|
||||
.ChildrenOfType<HandOfCards.HandCard>()
|
||||
.All(card => !card.ScreenSpaceDrawQuad.AABBFloat.IntersectsWith(handOfCards.ScreenSpaceDrawQuad.AABBFloat))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
|
||||
{
|
||||
State = { BindTarget = visibility },
|
||||
Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
|
||||
{
|
||||
State = { BindTarget = visibility },
|
||||
Child = new RankedPlayUserDisplay(2, Anchor.TopRight, RankedPlayColourScheme.Red)
|
||||
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.TopRight, RankedPlayColourScheme.Red)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.RankedPlay
|
||||
{
|
||||
public partial class TestSceneRankedPlayStageOverlay : RankedPlayTestScene
|
||||
{
|
||||
private Container content = null!;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create components", () => base.Content.Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RankedPlayBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
|
||||
{
|
||||
PickingUser = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
},
|
||||
Multiplier = 2,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLongUsername()
|
||||
{
|
||||
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
|
||||
{
|
||||
PickingUser = new APIUser
|
||||
{
|
||||
Id = 226597,
|
||||
Username = "WWWWWWWWWWWWWWWWWWWW",
|
||||
},
|
||||
Multiplier = 2,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColourScheme()
|
||||
{
|
||||
AddStep("create blue", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
|
||||
{
|
||||
PickingUser = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
},
|
||||
Multiplier = 2,
|
||||
});
|
||||
AddStep("create red", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Red)
|
||||
{
|
||||
PickingUser = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
},
|
||||
Multiplier = 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@@ -20,11 +23,19 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
Value = 1_000_000,
|
||||
};
|
||||
|
||||
public TestSceneRankedPlayUserDisplay()
|
||||
{
|
||||
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("add display", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay)));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("add display", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -36,7 +47,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
[Test]
|
||||
public void TesUserDisplay()
|
||||
{
|
||||
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -44,15 +55,30 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
Health = { BindTarget = health }
|
||||
});
|
||||
|
||||
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Red)
|
||||
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Red)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(256, 72),
|
||||
Health = { BindTarget = health }
|
||||
});
|
||||
}
|
||||
|
||||
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
|
||||
[Test]
|
||||
public void TestBeatmapState()
|
||||
{
|
||||
float progress = 0;
|
||||
|
||||
AddStep("set unavailable", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
|
||||
AddStep("set downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress = 0)));
|
||||
AddUntilStep("increment progress", () =>
|
||||
{
|
||||
progress += RNG.NextSingle(0.1f);
|
||||
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress));
|
||||
return progress >= 1;
|
||||
});
|
||||
AddStep("set to importing", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
|
||||
AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,13 +277,18 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
ScorePanel expandedPanel = null;
|
||||
ScorePanel contractedPanel = null;
|
||||
|
||||
AddUntilStep("retrieve expanded panel",
|
||||
() => expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded),
|
||||
() => Is.Not.Null);
|
||||
AddUntilStep("retrieve contracted panel",
|
||||
() => contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X),
|
||||
() => Is.Not.Null);
|
||||
|
||||
AddStep("click expanded panel then contracted panel", () =>
|
||||
{
|
||||
expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
|
||||
InputManager.MoveMouseTo(expandedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
|
||||
contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
|
||||
InputManager.MoveMouseTo(contractedPanel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
@@ -132,6 +132,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestOnlineLeaderboardWithLessThan50Scores()
|
||||
{
|
||||
ScoreInfo localScore = null!;
|
||||
|
||||
@@ -34,11 +34,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
|
||||
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
|
||||
|
||||
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
|
||||
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(1);
|
||||
CheckDisplayedBeatmapsCount(3);
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
|
||||
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
|
||||
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
|
||||
|
||||
CheckDisplayedBeatmapSetsCount(10);
|
||||
CheckDisplayedBeatmapsCount(30);
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
@@ -37,7 +38,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
|
||||
() => Is.EqualTo(positionBefore));
|
||||
() => Is.EqualTo(positionBefore).Using<Quad, Quad>((expected, actual)
|
||||
=> Precision.AlmostEquals(expected.TopLeft, actual.TopLeft)
|
||||
&& Precision.AlmostEquals(expected.TopRight, actual.TopRight)
|
||||
&& Precision.AlmostEquals(expected.BottomLeft, actual.BottomLeft)
|
||||
&& Precision.AlmostEquals(expected.BottomRight, actual.BottomRight)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestSetTraversal()
|
||||
{
|
||||
AddBeatmaps(3, splitApart: true);
|
||||
|
||||
@@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestBestRulesetIsRecommended()
|
||||
{
|
||||
BeatmapSetInfo osuSet = null, mixedSet = null;
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@@ -23,6 +24,7 @@ using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.Leaderboards;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel;
|
||||
@@ -430,6 +432,31 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("osu! cookie visible", () => this.ChildrenOfType<OsuLogo>().Single().Alpha, () => Is.Not.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDropdownKeyboardNavigation()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
|
||||
LoadSongSelect();
|
||||
|
||||
BeatmapInfo? firstBeatmap = null;
|
||||
AddStep("store first difficulty", () => firstBeatmap = Beatmap.Value.BeatmapInfo);
|
||||
|
||||
AddStep("click sort dropdown", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedDropdown<SortMode>>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("press up arrow", () => InputManager.Key(Key.Up));
|
||||
AddStep("press up arrow", () => InputManager.Key(Key.Up));
|
||||
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("sort mode is length", () => this.ChildrenOfType<ShearedDropdown<SortMode>>().Single().Current.Value, () => Is.EqualTo(SortMode.Length));
|
||||
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(firstBeatmap));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Footer
|
||||
|
||||
@@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@@ -11,13 +9,11 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@@ -35,18 +31,17 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene
|
||||
{
|
||||
private readonly ContextMenuContainer contextMenuContainer;
|
||||
private readonly BeatmapLeaderboardWedge leaderboard;
|
||||
|
||||
private RulesetStore rulesets = null!;
|
||||
private BeatmapManager beatmapManager;
|
||||
private ScoreManager scoreManager;
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private ScoreManager scoreManager = null!;
|
||||
|
||||
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
|
||||
|
||||
private BeatmapInfo beatmapInfo;
|
||||
private BeatmapInfo beatmapInfo = null!;
|
||||
|
||||
private LeaderboardManager leaderboardManager { get; set; }
|
||||
private LeaderboardManager leaderboardManager { get; set; } = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
@@ -60,15 +55,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
contextMenuContainer = new OsuContextMenuContainer
|
||||
leaderboard = new BeatmapLeaderboardWedge
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = leaderboard = new BeatmapLeaderboardWedge
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Size = new Vector2(0.6f),
|
||||
}
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Size = new Vector2(0.6f),
|
||||
},
|
||||
dialogOverlay = new DialogOverlay()
|
||||
};
|
||||
@@ -145,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestDeleteViaRightClick()
|
||||
{
|
||||
ScoreInfo scoreBeingDeleted = null;
|
||||
ScoreInfo scoreBeingDeleted = null!;
|
||||
AddStep("open menu for top score", () =>
|
||||
{
|
||||
var leaderboardScore = leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().First();
|
||||
@@ -157,12 +148,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
// Ensure the context menu has finished showing
|
||||
AddStep("finish transforms", () => contextMenuContainer.FinishTransforms(true));
|
||||
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
|
||||
|
||||
AddStep("click delete option", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>()
|
||||
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
|
||||
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<DrawableOsuMenuItem>()
|
||||
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Audio
|
||||
|
||||
protected TrackManagerPreviewTrack? CurrentTrack;
|
||||
|
||||
public readonly BindableBool IsPlayingPreview = new BindableBool();
|
||||
|
||||
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
|
||||
{
|
||||
this.mainTrackAdjustments = mainTrackAdjustments;
|
||||
@@ -47,6 +49,7 @@ namespace osu.Game.Audio
|
||||
CurrentTrack?.Stop();
|
||||
CurrentTrack = track;
|
||||
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
IsPlayingPreview.Value = true;
|
||||
});
|
||||
|
||||
track.Stopped += () => Schedule(() =>
|
||||
@@ -56,6 +59,7 @@ namespace osu.Game.Audio
|
||||
|
||||
CurrentTrack = null;
|
||||
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
IsPlayingPreview.Value = false;
|
||||
});
|
||||
|
||||
return track;
|
||||
|
||||
@@ -137,10 +137,15 @@ namespace osu.Game.Beatmaps
|
||||
Value = new StarDifficulty(beatmapInfo.StarRating, 0)
|
||||
};
|
||||
|
||||
updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken, computationDelay);
|
||||
|
||||
lock (bindableUpdateLock)
|
||||
{
|
||||
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, cancellationToken);
|
||||
linkedCancellationSources.Add(linkedSource);
|
||||
|
||||
updateBindable(bindable, currentRuleset.Value, currentMods.Value, linkedSource, computationDelay);
|
||||
|
||||
trackedBindables.Add(bindable);
|
||||
}
|
||||
|
||||
return bindable;
|
||||
}
|
||||
@@ -212,7 +217,7 @@ namespace osu.Game.Beatmaps
|
||||
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
|
||||
linkedCancellationSources.Add(linkedSource);
|
||||
|
||||
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
|
||||
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,27 +248,45 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to update with.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
|
||||
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
|
||||
/// <param name="linkedCancellationTokenSource">
|
||||
/// A cancellation token source that may be used to cancel this update.
|
||||
/// This token will be cancelled in one of two scenarios:
|
||||
/// <list type="bullet">
|
||||
/// <item>The owner of the bindable has requested the cancellation.</item>
|
||||
/// <item>An <see cref="Invalidate"/> call has been issued, and as such ongoing calculations must be aborted to avoid stale values being potentially written to bindables.</item>
|
||||
/// </list>
|
||||
/// </param>
|
||||
/// <param name="computationDelay">In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup.</param>
|
||||
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationToken cancellationToken = default, int computationDelay = 0)
|
||||
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationTokenSource linkedCancellationTokenSource, int computationDelay = 0)
|
||||
{
|
||||
// GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available
|
||||
// (contrary to GetAsync)
|
||||
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
|
||||
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, linkedCancellationTokenSource.Token, computationDelay)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (!linkedCancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
StarDifficulty? starDifficulty = task.GetResultSafely();
|
||||
|
||||
StarDifficulty? starDifficulty = task.GetResultSafely();
|
||||
if (starDifficulty != null)
|
||||
bindable.Value = starDifficulty.Value;
|
||||
}
|
||||
|
||||
if (starDifficulty != null)
|
||||
bindable.Value = starDifficulty.Value;
|
||||
});
|
||||
}, cancellationToken);
|
||||
// Once the linked cancellation token source is of no remaining use to anybody, clean it up.
|
||||
lock (bindableUpdateLock)
|
||||
{
|
||||
linkedCancellationSources.Remove(linkedCancellationTokenSource);
|
||||
linkedCancellationTokenSource.Dispose();
|
||||
}
|
||||
});
|
||||
},
|
||||
// This continuation MUST run even if the antecedent `GetDifficultyAsync()` call was canceled in order to clean up `linkedCancellationTokenSource`.
|
||||
// Due to this, `ContinueWith()` CANNOT accept `linkedCancellationTokenSource.Token` here, because if it did, then in an event of a cancellation,
|
||||
// the continuation would never be scheduled for execution.
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
#region OsuDropdownMenu
|
||||
|
||||
public partial class OsuDropdownMenu : DropdownMenu
|
||||
public partial class OsuDropdownMenu : DropdownMenu, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||
|
||||
@@ -163,6 +163,35 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction);
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
// logic copied from https://github.com/ppy/osu-framework/blob/baf865f1fd9e677310e7e432a7c6af99db7db914/osu.Framework/Graphics/UserInterface/Dropdown.cs#L702-L717
|
||||
var visibleMenuItemsList = VisibleMenuItems.ToList();
|
||||
|
||||
if (visibleMenuItemsList.Count > 0)
|
||||
{
|
||||
var currentPreselected = PreselectedItem;
|
||||
int targetPreselectionIndex = visibleMenuItemsList.IndexOf(currentPreselected);
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.SelectPrevious:
|
||||
PreselectItem(targetPreselectionIndex - 1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNext:
|
||||
PreselectItem(targetPreselectionIndex + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
#region DrawableOsuDropdownMenuItem
|
||||
|
||||
public partial class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem
|
||||
|
||||
@@ -196,6 +196,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="LocalUser"/> is a referee in the <see cref="Room"/>.
|
||||
/// </summary>
|
||||
public virtual bool IsReferee => LocalUser?.Role == MultiplayerRoomUserRole.Referee;
|
||||
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Determines whether a user is able to add playlist items to this room.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to check.</param>
|
||||
public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || Settings.QueueMode != QueueMode.HostOnly;
|
||||
public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || user.Role == MultiplayerRoomUserRole.Referee || Settings.QueueMode != QueueMode.HostOnly;
|
||||
|
||||
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Online.RankedPlay
|
||||
{
|
||||
@@ -18,5 +19,28 @@ namespace osu.Game.Online.RankedPlay
|
||||
|
||||
[Key(2)]
|
||||
public required bool Selected { get; init; }
|
||||
|
||||
[Key(3)]
|
||||
public required bool Dragged { get; init; }
|
||||
|
||||
[Key(4)]
|
||||
public required int Order { get; init; }
|
||||
|
||||
[Key(5)]
|
||||
public float DragX { get; init; }
|
||||
|
||||
[Key(6)]
|
||||
public float DragY { get; init; }
|
||||
|
||||
[IgnoreMember]
|
||||
public Vector2 DragPosition
|
||||
{
|
||||
get => new Vector2(DragX, DragY);
|
||||
init
|
||||
{
|
||||
DragX = value.X;
|
||||
DragY = value.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
|
||||
|
||||
player.BreakOverlay.Hide();
|
||||
player.OverlayComponents.Hide();
|
||||
(player as ReplayPlayer)?.ReplayOverlay.Hide();
|
||||
}
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
||||
+17
-2
@@ -41,6 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
private readonly Bindable<bool> trackRunning = new BindableBool();
|
||||
|
||||
private readonly Container overlayLayer;
|
||||
|
||||
private bool shouldBePlaying => Enabled.Value && IsHovered;
|
||||
@@ -114,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
|
||||
|
||||
overlayLayer.Add(new RippleVisualization(cardColours.Border)
|
||||
{
|
||||
TrackRunning = trackRunning.GetBoundCopy(),
|
||||
TrackRunning = { BindTarget = trackRunning }
|
||||
});
|
||||
|
||||
if (IsHovered)
|
||||
@@ -124,12 +125,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (previewTrack != null)
|
||||
previewTrack.Looping = true;
|
||||
|
||||
if (shouldBePlaying)
|
||||
{
|
||||
startPreviewIfAvailable();
|
||||
}
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
if (previewTrack != null)
|
||||
previewTrack.Looping = false;
|
||||
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void onTrackStarted() => Schedule(() => trackRunning.Value = true);
|
||||
|
||||
private void onTrackStopped() => Schedule(() => trackRunning.Value = false);
|
||||
@@ -193,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
|
||||
[Resolved]
|
||||
private SongPreviewParticleContainer? particleContainer { get; set; }
|
||||
|
||||
public required IBindable<bool> TrackRunning { get; init; }
|
||||
public readonly IBindable<bool> TrackRunning = new Bindable<bool>();
|
||||
|
||||
private readonly Color4 accentColour;
|
||||
private readonly Container rippleContainer;
|
||||
@@ -254,6 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
|
||||
this.FadeOut(200);
|
||||
}
|
||||
}, true);
|
||||
FinishTransforms();
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
|
||||
{
|
||||
public partial class BackgroundMusicManager : CompositeComponent
|
||||
{
|
||||
private const int hover_fade_duration = 250;
|
||||
|
||||
private ScheduledDelegate? globalTrackFadeDelegate;
|
||||
|
||||
private DrawableTrack bgm = null!;
|
||||
|
||||
private bool shouldBePlaying;
|
||||
|
||||
private Bindable<bool> isPlayingPreview = null!;
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
AddInternal(bgm = new DrawableTrack(audio.Tracks.Get("rankedplay_bgm.ogg")));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
isPlayingPreview = previewTrackManager.IsPlayingPreview.GetBoundCopy();
|
||||
isPlayingPreview.BindValueChanged(playing =>
|
||||
{
|
||||
bgm.VolumeTo(playing.NewValue ? 0 : 1, hover_fade_duration);
|
||||
});
|
||||
}
|
||||
|
||||
public void Play() => shouldBePlaying = true;
|
||||
|
||||
public void Stop() => shouldBePlaying = false;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
updatePlayingState();
|
||||
}
|
||||
|
||||
private void updatePlayingState()
|
||||
{
|
||||
if (!bgm.IsLoaded)
|
||||
return;
|
||||
|
||||
if (shouldBePlaying == bgm.IsRunning)
|
||||
return;
|
||||
|
||||
if (shouldBePlaying)
|
||||
{
|
||||
const int track_fade_duration = 3000;
|
||||
|
||||
// remove music control from player, to prevent overlapping music
|
||||
musicController.AllowTrackControl.Value = false;
|
||||
globalTrackFadeDelegate?.Cancel();
|
||||
|
||||
// cross-fade if global track is playing something
|
||||
if (musicController.IsPlaying)
|
||||
{
|
||||
var globalTrack = musicController.CurrentTrack;
|
||||
|
||||
globalTrack.VolumeTo(0, track_fade_duration, Easing.OutCubic);
|
||||
globalTrackFadeDelegate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
musicController.Stop();
|
||||
globalTrack.VolumeTo(1);
|
||||
}, track_fade_duration);
|
||||
}
|
||||
|
||||
bgm.VolumeTo(0)
|
||||
.VolumeTo(1, track_fade_duration, Easing.InCubic);
|
||||
|
||||
bgm.Looping = true;
|
||||
bgm.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
globalTrackFadeDelegate?.Cancel();
|
||||
|
||||
bgm.Stop();
|
||||
bgm.Reset();
|
||||
|
||||
// return control of music to player and reset volume
|
||||
musicController.AllowTrackControl.Value = true;
|
||||
musicController.CurrentTrack.Volume.Value = 1;
|
||||
musicController.EnsurePlayingSomething();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-15
@@ -2,9 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@@ -13,11 +13,13 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -33,21 +35,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
|
||||
Value = 1_000_000,
|
||||
};
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; } = null!;
|
||||
|
||||
private readonly int userId;
|
||||
private readonly APIUser user;
|
||||
private readonly Anchor contentAnchor;
|
||||
private readonly RankedPlayColourScheme colourScheme;
|
||||
|
||||
private BufferedContainer grayScaleContainer = null!;
|
||||
|
||||
private OsuSpriteText beatmapState = null!;
|
||||
|
||||
private BeatmapAvailability availability = BeatmapAvailability.Unknown();
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RankedPlayCornerPiece? cornerPiece { get; set; }
|
||||
|
||||
public RankedPlayUserDisplay(int userId, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
|
||||
public RankedPlayUserDisplay(APIUser user, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
|
||||
{
|
||||
this.userId = userId;
|
||||
this.user = user;
|
||||
this.contentAnchor = contentAnchor;
|
||||
this.colourScheme = colourScheme;
|
||||
}
|
||||
@@ -55,12 +61,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
APIUser user = users.GetUserAsync(userId).GetResultSafely()!;
|
||||
|
||||
var shear = contentAnchor == Anchor.TopLeft || contentAnchor == Anchor.BottomRight
|
||||
? -OsuGame.SHEAR
|
||||
: OsuGame.SHEAR;
|
||||
|
||||
var beatmapStateAnchor = (contentAnchor & Anchor.x0) != 0
|
||||
? Anchor.CentreLeft
|
||||
: Anchor.CentreRight;
|
||||
|
||||
InternalChildren =
|
||||
[
|
||||
new CircularContainer
|
||||
@@ -103,15 +111,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
|
||||
Anchor = contentAnchor,
|
||||
Origin = contentAnchor,
|
||||
},
|
||||
new OsuSpriteText
|
||||
new FillFlowContainer
|
||||
{
|
||||
Name = "Username",
|
||||
Text = user.Username,
|
||||
Name = "Username/beatmap state container",
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = contentAnchor,
|
||||
Origin = contentAnchor,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding { Horizontal = 4, Vertical = 6 },
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Children =
|
||||
[
|
||||
new OsuSpriteText
|
||||
{
|
||||
Name = "Username",
|
||||
Text = user.Username,
|
||||
Anchor = contentAnchor,
|
||||
Origin = contentAnchor,
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold),
|
||||
UseFullGlyphHeight = false,
|
||||
},
|
||||
beatmapState = new OsuSpriteText
|
||||
{
|
||||
Anchor = beatmapStateAnchor,
|
||||
Origin = beatmapStateAnchor,
|
||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -129,6 +155,46 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
|
||||
grayScaleContainer.GrayscaleTo(e.NewValue <= 0 ? 1 : 0, 300);
|
||||
cornerPiece?.OnHealthChanged(e.NewValue);
|
||||
});
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
{
|
||||
var multiplayerUser = client.Room?.Users.SingleOrDefault(u => u.UserID == user.Id);
|
||||
|
||||
if (multiplayerUser == null || availability == multiplayerUser.BeatmapAvailability)
|
||||
return;
|
||||
|
||||
availability = multiplayerUser.BeatmapAvailability;
|
||||
|
||||
if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing)
|
||||
beatmapState.FadeIn(50);
|
||||
else
|
||||
beatmapState.FadeOut(50);
|
||||
|
||||
switch (availability.State)
|
||||
{
|
||||
case DownloadState.NotDownloaded:
|
||||
beatmapState.Text = "Missing Beatmap";
|
||||
break;
|
||||
|
||||
case DownloadState.Downloading:
|
||||
double progress = Math.Clamp(availability.DownloadProgress ?? 0, 0, 1);
|
||||
beatmapState.Text = $"Downloading... ({progress:P0})";
|
||||
break;
|
||||
|
||||
case DownloadState.Importing:
|
||||
beatmapState.Text = "Importing...";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
|
||||
public partial class HealthBar : CompositeDrawable
|
||||
|
||||
@@ -34,7 +34,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
public CardFlow CenterRow { get; private set; } = null!;
|
||||
|
||||
protected override LocalisableString StageHeading => "Discard Phase";
|
||||
public override bool ShowStageOverlay => true;
|
||||
public override LocalisableString StageHeading => "Discard Phase";
|
||||
protected override LocalisableString StageCaption => "Replace cards from your hand";
|
||||
|
||||
private PlayerHandOfCards playerHand = null!;
|
||||
@@ -81,6 +82,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
];
|
||||
|
||||
CenterColumn.Children =
|
||||
[
|
||||
discardButton = new ShearedButton
|
||||
{
|
||||
Name = "Discard Button",
|
||||
@@ -89,11 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
Width = 150,
|
||||
Action = onDiscardButtonClicked,
|
||||
Enabled = { Value = true },
|
||||
}
|
||||
];
|
||||
|
||||
CenterColumn.Children =
|
||||
[
|
||||
},
|
||||
playerHand = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
@@ -179,23 +180,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
base.OnEntering(previous);
|
||||
|
||||
var screenBottomCenter = new Vector2(DrawWidth / 2, DrawHeight);
|
||||
int cardCount = 0;
|
||||
double delay = 0;
|
||||
const double stagger = 50;
|
||||
|
||||
foreach (var card in matchInfo.PlayerCards)
|
||||
{
|
||||
double currentDelay = delay;
|
||||
|
||||
playerHand.AddCard(card, c =>
|
||||
{
|
||||
c.Position = ToSpaceOfOtherDrawable(screenBottomCenter, playerHand);
|
||||
c.Position = playerHand.BottomCardInsertPosition;
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
|
||||
}, 50 * cardCount);
|
||||
cardCount++;
|
||||
}
|
||||
|
||||
playerHand.UpdateLayout(stagger: 50);
|
||||
Scheduler.AddDelayed(() => SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample), delay);
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
}
|
||||
|
||||
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
|
||||
@@ -297,8 +298,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
playerHand.AddCard(card, d =>
|
||||
{
|
||||
d.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth, DrawHeight * 0.5f), playerHand);
|
||||
d.Rotation = -30;
|
||||
// card should enter from centre-right of screen
|
||||
var cardEnterPosition = ToSpaceOfOtherDrawable(new Vector2(DrawWidth, DrawHeight * 0.5f), playerHand);
|
||||
d.SetupMovementForDrawnCard(cardEnterPosition);
|
||||
});
|
||||
|
||||
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -25,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
/// </summary>
|
||||
public Action<bool>? ExitRequested { get; init; }
|
||||
|
||||
protected override LocalisableString StageHeading => "Results";
|
||||
public override LocalisableString StageHeading => "Results";
|
||||
protected override LocalisableString StageCaption => string.Empty;
|
||||
|
||||
[Resolved]
|
||||
@@ -36,8 +38,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
private OsuTextFlowContainer localRatingText = null!;
|
||||
private OsuTextFlowContainer opponentRatingText = null!;
|
||||
|
||||
private Sample winSample = null!;
|
||||
private Sample loseSample = null!;
|
||||
private Sample drawSample = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, AudioManager audio)
|
||||
{
|
||||
CenterColumn.Child = new FillFlowContainer
|
||||
{
|
||||
@@ -172,6 +178,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
}
|
||||
};
|
||||
|
||||
winSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/win");
|
||||
loseSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/lose");
|
||||
drawSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/draw");
|
||||
|
||||
RankedPlayUserInfo localUser = matchInfo.RoomState.Users[Client.LocalUser!.UserID];
|
||||
RankedPlayUserInfo otherUser = matchInfo.RoomState.Users.Values.Single(u => u != localUser);
|
||||
|
||||
@@ -179,16 +189,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
titleText.Text = "DRAW";
|
||||
titleText.Colour = titleSeparator.Colour = colours.Orange1;
|
||||
drawSample.Play();
|
||||
}
|
||||
else if (matchInfo.RoomState.WinningUserId == Client.LocalUser!.UserID)
|
||||
{
|
||||
titleText.Text = "VICTORY";
|
||||
titleText.Colour = titleSeparator.Colour = colours.Green1;
|
||||
winSample.Play();
|
||||
}
|
||||
else
|
||||
{
|
||||
titleText.Text = "DEFEAT";
|
||||
titleText.Colour = titleSeparator.Colour = colours.Red1;
|
||||
loseSample.Play();
|
||||
}
|
||||
|
||||
localRatingText.AddText("Your Rating: ", s => s.Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular));
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public partial class GameplayScreen : RankedPlaySubScreen
|
||||
{
|
||||
protected override LocalisableString StageHeading => "Gameplay";
|
||||
public override LocalisableString StageHeading => "Gameplay";
|
||||
protected override LocalisableString StageCaption => string.Empty;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public override bool ShowBeatmapBackground => true;
|
||||
|
||||
protected override LocalisableString StageHeading => "Gameplay";
|
||||
public override LocalisableString StageHeading => "Gameplay";
|
||||
protected override LocalisableString StageCaption => string.Empty;
|
||||
|
||||
[Cached(typeof(IBindable<SongSelect.BeatmapSetLookupResult?>))]
|
||||
|
||||
@@ -1,11 +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 System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osuTK;
|
||||
@@ -16,8 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
{
|
||||
public partial class HandCard : CompositeDrawable
|
||||
{
|
||||
public float LayoutWidth => DrawWidth * (State.Hovered ? hover_scale : 1);
|
||||
|
||||
private readonly Bindable<RankedPlayCardState> state = new Bindable<RankedPlayCardState>();
|
||||
|
||||
public RankedPlayCardState State
|
||||
@@ -44,6 +43,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
set => State = State with { Pressed = value };
|
||||
}
|
||||
|
||||
public bool CardDragged
|
||||
{
|
||||
get => State.Dragged;
|
||||
set => State = State with { Dragged = value };
|
||||
}
|
||||
|
||||
public bool CardHoveredOrDragged => CardHovered || CardDragged;
|
||||
|
||||
public Vector2 DragPosition
|
||||
{
|
||||
get => State.DragPosition;
|
||||
set => State = State with { DragPosition = value };
|
||||
}
|
||||
|
||||
public int Order
|
||||
{
|
||||
get => State.Order;
|
||||
set => State = State with { Order = value };
|
||||
}
|
||||
|
||||
public CardLayout LayoutTarget { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HandOfCards handOfCards { get; set; } = null!;
|
||||
|
||||
@@ -63,20 +84,24 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
|
||||
AddInternal(Card = card);
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
positionSpring.Current = positionSpring.PreviousTarget = Position;
|
||||
scaleSpring.Current = scaleSpring.PreviousTarget = 1;
|
||||
rotationSpring.Current = rotationSpring.PreviousTarget = Rotation;
|
||||
|
||||
state.BindValueChanged(OnStateChanged, true);
|
||||
}
|
||||
|
||||
protected virtual void OnStateChanged(ValueChangedEvent<RankedPlayCardState> state)
|
||||
{
|
||||
handOfCards.OnCardStateChanged(this, state.NewValue);
|
||||
handOfCards.OnCardStateChanged(this, state);
|
||||
|
||||
Card.ShowSelectionOutline = state.NewValue.Selected;
|
||||
|
||||
@@ -90,6 +115,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
Card.ScaleTo(1f, 400, Easing.OutElasticHalf);
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.NewValue.Dragged)
|
||||
{
|
||||
// while card is being dragged card should slowly swing from side to side,
|
||||
// so frequency is lowered and elasticity is increased
|
||||
rotationSpring.NaturalFrequency = 2f;
|
||||
rotationSpring.Damping = 0.4f;
|
||||
rotationSpring.Response = 1.2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise rotation should be more snappy and not feel elastic
|
||||
rotationSpring.NaturalFrequency = 3f;
|
||||
rotationSpring.Damping = 0.75f;
|
||||
rotationSpring.Response = 0.8f;
|
||||
}
|
||||
}
|
||||
|
||||
public RankedPlayCard Detach()
|
||||
@@ -102,11 +143,92 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
return Card;
|
||||
}
|
||||
|
||||
private bool updateMovement = true;
|
||||
|
||||
private static readonly SpringParameters default_position_spring_parameters = new SpringParameters
|
||||
{
|
||||
NaturalFrequency = 4f,
|
||||
Response = 1.1f,
|
||||
Damping = 0.8f
|
||||
};
|
||||
|
||||
private readonly Vector2Spring positionSpring = new Vector2Spring { Parameters = default_position_spring_parameters };
|
||||
|
||||
private readonly FloatSpring rotationSpring = new FloatSpring
|
||||
{
|
||||
NaturalFrequency = 2f,
|
||||
Damping = 0.4f,
|
||||
Response = 1.2f,
|
||||
};
|
||||
|
||||
private readonly FloatSpring scaleSpring = new FloatSpring
|
||||
{
|
||||
NaturalFrequency = 4f,
|
||||
Response = 1.3f,
|
||||
Damping = 0.75f,
|
||||
Current = 1,
|
||||
PreviousTarget = 1,
|
||||
};
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Card.Elevation = float.Lerp(CardHovered ? 1 : 0, Card.Elevation, (float)Math.Exp(-0.03f * Time.Elapsed));
|
||||
if (updateMovement)
|
||||
{
|
||||
Position = positionSpring.Update(Time.Elapsed, LayoutTarget.Position);
|
||||
Scale = new Vector2(scaleSpring.Update(Time.Elapsed, LayoutTarget.Scale));
|
||||
|
||||
float targetRotation = LayoutTarget.Rotation;
|
||||
|
||||
if (CardDragged)
|
||||
{
|
||||
targetRotation += positionSpring.Velocity.X * 0.006f;
|
||||
}
|
||||
|
||||
Rotation = rotationSpring.Update(Time.Elapsed, targetRotation);
|
||||
|
||||
Card.Elevation = (float)Interpolation.DampContinuously(Card.Elevation, CardHoveredOrDragged ? 1 : 0, 25, Time.Elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delays the time until a card starts to move to its layout position, intended to use for staggered movement when adding multiple cards to the hand at once.
|
||||
/// Movement is slowed down a bit while it's moving towards the target position to make the transition appear less abrupt.
|
||||
/// </summary>
|
||||
public void DelayMovementOnEntering(double delay)
|
||||
{
|
||||
const double approximate_time_until_position_reached = 200;
|
||||
|
||||
updateMovement = false;
|
||||
|
||||
this.Delay(delay)
|
||||
.Schedule(() =>
|
||||
{
|
||||
updateMovement = true;
|
||||
positionSpring.NaturalFrequency = 2.5f;
|
||||
})
|
||||
.Delay(approximate_time_until_position_reached)
|
||||
.Schedule(() =>
|
||||
{
|
||||
positionSpring.Parameters = default_position_spring_parameters;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the card move towards its layout position from a given <paramref name="position"/> and updates
|
||||
/// movement parameters so the card moves towards it's target position more slowly and less springy.
|
||||
/// </summary>
|
||||
public void SetupMovementForDrawnCard(Vector2 position)
|
||||
{
|
||||
const double approximate_time_until_position_reached = 200;
|
||||
|
||||
Position = position;
|
||||
|
||||
positionSpring.NaturalFrequency = 2f;
|
||||
positionSpring.Damping = 1f;
|
||||
|
||||
Scheduler.AddDelayed(() => positionSpring.Parameters = default_position_spring_parameters, approximate_time_until_position_reached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -23,15 +24,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
[Cached]
|
||||
public abstract partial class HandOfCards : CompositeDrawable
|
||||
{
|
||||
private const float hover_scale = 1.2f;
|
||||
protected const float HOVER_SCALE = 1.2f;
|
||||
|
||||
public IEnumerable<HandCard> Cards => cardContainer.Children;
|
||||
private const float card_spacing = -15;
|
||||
|
||||
public IReadOnlyList<HandCard> Cards => cardContainer.Children;
|
||||
|
||||
/// <summary>
|
||||
/// How far a card slides upwards when hovered.
|
||||
/// Used for making sure a card moves entirely into frame when the hand is partially off-screen.
|
||||
/// </summary>
|
||||
public float HoverYOffset = 15;
|
||||
public float HoverYOffset = 35;
|
||||
|
||||
/// <summary>
|
||||
/// If true, card layout will be flipped on both axes for a card hand placed at the top edge of the screen, while keeping the cards upright.
|
||||
@@ -39,13 +42,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
/// </summary>
|
||||
protected virtual bool Flipped => false;
|
||||
|
||||
private readonly Container<HandCard> cardContainer;
|
||||
/// <summary>
|
||||
/// Position to insert cards at so they are start moving from the bottom relative to the card layout
|
||||
/// </summary>
|
||||
public Vector2 BottomCardInsertPosition => new Vector2(0, (DrawHeight + RankedPlayCard.SIZE.Y) / 2 * (Flipped ? -1 : 1));
|
||||
|
||||
private readonly CardContainer cardContainer;
|
||||
|
||||
private readonly Dictionary<RankedPlayCardItem, HandCard> cardLookup = new Dictionary<RankedPlayCardItem, HandCard>();
|
||||
|
||||
protected HandOfCards()
|
||||
{
|
||||
AddInternal(cardContainer = new Container<HandCard>
|
||||
AddInternal(cardContainer = new CardContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
@@ -55,6 +63,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!drawOrderBacking.IsValid)
|
||||
{
|
||||
cardContainer.Sort();
|
||||
drawOrderBacking.Validate();
|
||||
}
|
||||
|
||||
if (!layoutBacking.IsValid)
|
||||
{
|
||||
updateLayout();
|
||||
@@ -76,17 +90,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
|
||||
foreach (var card in cardContainer)
|
||||
{
|
||||
card.Delay(delay)
|
||||
.MoveTo(new Vector2(0, Flipped ? -220 : 220), 400, Easing.OutExpo)
|
||||
.RotateTo(0, 400, Easing.OutExpo)
|
||||
.ScaleTo(1, 400, Easing.OutExpo);
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
card.LayoutTarget = new CardLayout
|
||||
{
|
||||
Position = new Vector2(0, (DrawHeight + RankedPlayCard.SIZE.Y + 10) / 2 * (Flipped ? -1 : 1)),
|
||||
Rotation = 0,
|
||||
Scale = 1,
|
||||
};
|
||||
}, delay);
|
||||
|
||||
delay += 50;
|
||||
}
|
||||
}
|
||||
|
||||
private Anchor cardAnchor => Flipped ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
|
||||
public void AddCard(RankedPlayCardWithPlaylistItem item, Action<HandCard>? setupAction = null) => AddCard(new RankedPlayCard(item), setupAction);
|
||||
|
||||
public void AddCard(RankedPlayCard card, Action<HandCard>? setupAction = null)
|
||||
@@ -95,12 +112,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
return;
|
||||
|
||||
var drawable = CreateHandCard(card);
|
||||
drawable.Anchor = drawable.Origin = cardAnchor;
|
||||
|
||||
cardLookup[card.Item.Card] = drawable;
|
||||
|
||||
drawable.Position = GetArcPosition(0);
|
||||
|
||||
if (card.Item.DisplayOrder != null)
|
||||
drawable.Order = card.Item.DisplayOrder.Value;
|
||||
else if (cardContainer.Count > 0)
|
||||
drawable.Order = cardContainer.Max(c => c.Order) + 1;
|
||||
|
||||
cardContainer.Add(drawable);
|
||||
layoutBacking.Invalidate();
|
||||
InvalidateLayout(drawOrder: true);
|
||||
|
||||
setupAction?.Invoke(drawable);
|
||||
}
|
||||
@@ -113,8 +136,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
return false;
|
||||
|
||||
cardContainer.Remove(drawable, true);
|
||||
layoutBacking.Invalidate();
|
||||
return false;
|
||||
InvalidateLayout(drawOrder: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -137,19 +160,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
card = drawable.Detach();
|
||||
|
||||
cardContainer.Remove(drawable, true);
|
||||
layoutBacking.Invalidate();
|
||||
InvalidateLayout(drawOrder: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual HandCard CreateHandCard(RankedPlayCard card) => new HandCard(card);
|
||||
|
||||
protected virtual void OnCardStateChanged(HandCard card, RankedPlayCardState state)
|
||||
protected virtual void OnCardStateChanged(HandCard card, ValueChangedEvent<RankedPlayCardState> evt)
|
||||
{
|
||||
InvalidateLayout();
|
||||
InvalidateLayout(drawOrder: affectsDrawOrder(evt));
|
||||
|
||||
// hovered state can be caused by keyboard focus, in which case we have to clean up after the other cards manually
|
||||
if (state.Hovered)
|
||||
if (evt.NewValue.Hovered)
|
||||
{
|
||||
foreach (var c in cardContainer)
|
||||
{
|
||||
@@ -159,95 +182,195 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
}
|
||||
}
|
||||
|
||||
private static bool affectsDrawOrder(ValueChangedEvent<RankedPlayCardState> evt) =>
|
||||
evt.OldValue.Order != evt.NewValue.Order ||
|
||||
evt.OldValue.Dragged != evt.NewValue.Dragged;
|
||||
|
||||
#region Layout
|
||||
|
||||
private readonly Cached layoutBacking = new Cached();
|
||||
private readonly Cached drawOrderBacking = new Cached();
|
||||
|
||||
protected void InvalidateLayout() => layoutBacking.Invalidate();
|
||||
|
||||
public void UpdateLayout(double stagger = 0)
|
||||
/// <summary>
|
||||
/// Invalidates the layout of the hand of cards, causing a relayout to occur.
|
||||
/// </summary>
|
||||
/// <param name="drawOrder">If set to true, also invalidates the draw order of the cards.</param>
|
||||
protected void InvalidateLayout(bool drawOrder = false)
|
||||
{
|
||||
updateLayout(stagger);
|
||||
layoutBacking.Validate();
|
||||
layoutBacking.Invalidate();
|
||||
if (drawOrder)
|
||||
drawOrderBacking.Invalidate();
|
||||
}
|
||||
|
||||
private void updateLayout(double stagger = 0)
|
||||
private void updateLayout()
|
||||
{
|
||||
if (Contracted)
|
||||
return;
|
||||
|
||||
const float spacing = -20;
|
||||
// card container draws dragged card on top so we need to sort those separately
|
||||
var cards = cardContainer.Children.OrderBy(static c => c.State.Order).ToArray();
|
||||
|
||||
float totalWidth = cardContainer.Sum(it => it.LayoutWidth + spacing) - spacing;
|
||||
int activeCardIndex = GetActiveCardIndex(cards);
|
||||
|
||||
float x = -totalWidth / 2;
|
||||
|
||||
const int no_card_hovered = -1;
|
||||
int hoverIndex = no_card_hovered;
|
||||
|
||||
for (int i = 0; i < cardContainer.Count; i++)
|
||||
for (int i = 0; i < cards.Length; i++)
|
||||
{
|
||||
if (cardContainer[i].CardHovered)
|
||||
{
|
||||
hoverIndex = i;
|
||||
break;
|
||||
}
|
||||
var card = cards[i];
|
||||
|
||||
var layout = card.CardDragged
|
||||
? CalculateDraggedCardLayout(card.DragPosition)
|
||||
: CalculateCardLayout(i, activeCardIndex);
|
||||
|
||||
if (Flipped)
|
||||
layout.Position *= -1;
|
||||
|
||||
card.LayoutTarget = layout;
|
||||
}
|
||||
}
|
||||
|
||||
protected int GetActiveCardIndex(IReadOnlyList<HandCard> cards)
|
||||
{
|
||||
// the mouse can temporarily leave the dragged card, so dragged card should take precedence
|
||||
for (int i = 0; i < cards.Count; i++)
|
||||
{
|
||||
if (cards[i].CardDragged)
|
||||
return i;
|
||||
}
|
||||
|
||||
double delay = 0;
|
||||
|
||||
for (int i = 0; i < cardContainer.Count; i++)
|
||||
for (int i = 0; i < cards.Count; i++)
|
||||
{
|
||||
var child = cardContainer[i];
|
||||
if (cards[i].CardHovered)
|
||||
return i;
|
||||
}
|
||||
|
||||
x += child.LayoutWidth / 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
float yOffset = 0;
|
||||
protected CardLayout CalculateCardLayout(int index, int activeIndex)
|
||||
{
|
||||
float x = GetCardX(index, activeIndex);
|
||||
|
||||
var position = new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 - 10);
|
||||
var position = GetArcPosition(x);
|
||||
float rotation = GetArcRotation(x);
|
||||
|
||||
if (hoverIndex != no_card_hovered && cardContainer.Children.Count > 1)
|
||||
{
|
||||
int distance = Math.Abs(i - hoverIndex);
|
||||
int direction = Math.Sign(i - hoverIndex);
|
||||
if (index == activeIndex)
|
||||
position += GetCardUpwardsDirection(rotation) * HoverYOffset;
|
||||
|
||||
position.X += direction switch
|
||||
return new CardLayout
|
||||
{
|
||||
Position = position,
|
||||
Rotation = rotation,
|
||||
Scale = index == activeIndex ? HOVER_SCALE : 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual CardLayout CalculateDraggedCardLayout(Vector2 dragPosition)
|
||||
{
|
||||
return new CardLayout
|
||||
{
|
||||
Position = dragPosition,
|
||||
Rotation = 0,
|
||||
Scale = HOVER_SCALE,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the total width of the layout for all cards in the hand.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not account for extra space needed for spreading the cards adjacent to the active card apart.
|
||||
/// </remarks>
|
||||
protected float TotalLayoutWidth => cardContainer.Count * (RankedPlayCard.SIZE.X + card_spacing) - card_spacing;
|
||||
|
||||
protected float GetCardX(int index, int activeIndex)
|
||||
{
|
||||
float x = -TotalLayoutWidth / 2
|
||||
+ index * (RankedPlayCard.SIZE.X + card_spacing)
|
||||
+ RankedPlayCard.SIZE.X / 2;
|
||||
|
||||
if (activeIndex < 0 || cardContainer.Count <= 1)
|
||||
return x;
|
||||
|
||||
// if a card is hovered or dragged, the adjacent cards should get spread apart
|
||||
int distance = Math.Abs(index - activeIndex);
|
||||
int direction = Math.Sign(index - activeIndex);
|
||||
|
||||
float baseOffset = RankedPlayCard.SIZE.X * 0.1f;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case -1:
|
||||
if (cardContainer.Count == 2)
|
||||
{
|
||||
0 => 0,
|
||||
|
||||
// special case for the left card when there's only 2 cards
|
||||
// too much offset looks kinda odd here so it's reduced
|
||||
< 0 when cardContainer.Count == 2 => -3,
|
||||
x -= baseOffset + 3;
|
||||
break;
|
||||
}
|
||||
|
||||
< 0 => -10 / MathF.Pow(distance, 3),
|
||||
x -= baseOffset + 10 / MathF.Pow(distance, 2);
|
||||
break;
|
||||
|
||||
// cards right to the hovered card have a higher offset because they are partially
|
||||
// covering the cards to their left
|
||||
> 0 => 20 / MathF.Pow(distance, 2),
|
||||
};
|
||||
case 1:
|
||||
// cards right to the active card have a higher offset because they are partially
|
||||
// covering the cards to their left
|
||||
x += baseOffset + 20 / MathF.Pow(distance, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the position of a card at a given <paramref name="x" /> coordinate so all cards are laid out in an arc
|
||||
/// </summary>
|
||||
protected Vector2 GetArcPosition(float x)
|
||||
{
|
||||
float offset = (DrawHeight - RankedPlayCard.SIZE.Y) / 2;
|
||||
|
||||
return new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 + offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the rotation of a card at a given <paramref name="x"/> coordinate
|
||||
/// </summary>
|
||||
protected static float GetArcRotation(float x) => x * 0.03f;
|
||||
|
||||
protected static Vector2 GetCardUpwardsDirection(float rotation)
|
||||
{
|
||||
float angle = MathHelper.DegreesToRadians(rotation - 90);
|
||||
|
||||
return new Vector2(MathF.Cos(angle), MathF.Sin(angle));
|
||||
}
|
||||
|
||||
private partial class CardContainer : Container<HandCard>
|
||||
{
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
if (x is HandCard c1 && y is HandCard c2)
|
||||
{
|
||||
// dragged cards should always be drawn on top
|
||||
if (c1.CardDragged)
|
||||
return 1;
|
||||
|
||||
if (c2.CardDragged)
|
||||
return -1;
|
||||
|
||||
int result = c1.Order.CompareTo(c2.Order);
|
||||
if (result != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (child.CardHovered)
|
||||
yOffset = -HoverYOffset;
|
||||
|
||||
float rotation = x * 0.03f;
|
||||
|
||||
float angle = MathHelper.DegreesToRadians(rotation + 90);
|
||||
|
||||
position += new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * yOffset;
|
||||
|
||||
position *= Flipped ? -1 : 1;
|
||||
|
||||
child
|
||||
.Delay(delay)
|
||||
.MoveTo(position, 300, Easing.OutExpo)
|
||||
.RotateTo(rotation, 300, Easing.OutExpo)
|
||||
.ScaleTo(child.CardHovered ? hover_scale : 1f, 400, Easing.OutElasticQuarter);
|
||||
|
||||
x += child.LayoutWidth / 2 + spacing;
|
||||
|
||||
delay += stagger;
|
||||
return base.Compare(x, y);
|
||||
}
|
||||
|
||||
public void Sort() => SortInternal();
|
||||
}
|
||||
|
||||
public struct CardLayout
|
||||
{
|
||||
public required Vector2 Position { get; set; }
|
||||
public required float Rotation { get; set; }
|
||||
public required float Scale { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
/// <summary>
|
||||
/// Maximum amount of frames that can get queued up at the same time
|
||||
/// </summary>
|
||||
public int MaxQueuedFrames { get; set; } = 20;
|
||||
public int MaxQueuedFrames { get; set; } = 30;
|
||||
|
||||
private readonly int userId;
|
||||
private readonly OpponentHandOfCards handOfCards;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
/// <summary>
|
||||
/// Minimum interval between individual replay frames
|
||||
/// </summary>
|
||||
public double RecordInterval { get; init; } = 25;
|
||||
public double RecordInterval { get; init; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Max amount of frames to collect per <see cref="FlushInterval"/>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Online.RankedPlay;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
{
|
||||
@@ -24,5 +25,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
card.State = cardState;
|
||||
}
|
||||
}
|
||||
|
||||
protected override CardLayout CalculateDraggedCardLayout(Vector2 dragPosition)
|
||||
{
|
||||
// the opponent shouldn't be able to drag his card across the entire screen.
|
||||
// card movement is limited to roughly the width of the hand horizontally
|
||||
// and has a fixed vertical offset (extended slightly further than when hovered)
|
||||
float maxExtent = TotalLayoutWidth / 2;
|
||||
|
||||
float x = float.Clamp(dragPosition.X, -maxExtent, maxExtent);
|
||||
|
||||
return new CardLayout
|
||||
{
|
||||
Position = GetArcPosition(x) + new Vector2(0, -60),
|
||||
Rotation = 0,
|
||||
Scale = HOVER_SCALE,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -33,6 +33,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
|
||||
public required Action<PlayerHandCard> Clicked;
|
||||
|
||||
public required Action<PlayerHandCard, Vector2> Dragged;
|
||||
|
||||
public required IBindable<bool> AllowSelection;
|
||||
|
||||
private readonly Drawable cardInputArea;
|
||||
@@ -165,6 +167,35 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
|
||||
CardHovered = false;
|
||||
}
|
||||
|
||||
#region Drag/Drop
|
||||
|
||||
private Vector2 dragOffset;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
dragOffset = DrawPosition + AnchorPosition - e.MouseDownPosition;
|
||||
|
||||
CardDragged = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
DragPosition = e.MousePosition - AnchorPosition + dragOffset;
|
||||
|
||||
Dragged(this, e.ScreenSpaceMousePosition);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
base.OnDragEnd(e);
|
||||
|
||||
CardDragged = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -12,6 +13,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Online.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
@@ -103,6 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
protected override HandCard CreateHandCard(RankedPlayCard card) => new PlayerHandCard(card)
|
||||
{
|
||||
Clicked = cardClicked,
|
||||
Dragged = cardDragged,
|
||||
AllowSelection = allowSelection.GetBoundCopy(),
|
||||
PlayAction = PlayCardAction,
|
||||
};
|
||||
@@ -138,18 +141,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCardStateChanged(HandCard card, RankedPlayCardState state)
|
||||
protected override void OnCardStateChanged(HandCard card, ValueChangedEvent<RankedPlayCardState> evt)
|
||||
{
|
||||
StateChanged?.Invoke();
|
||||
|
||||
base.OnCardStateChanged(card, state);
|
||||
base.OnCardStateChanged(card, evt);
|
||||
}
|
||||
|
||||
public Dictionary<Guid, RankedPlayCardState> State => Cards.Select(static card => new KeyValuePair<Guid, RankedPlayCardState>(card.Item.Card.ID, card.State)).ToDictionary();
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat || Contracted)
|
||||
if (e.Repeat || Contracted || Cards.Any(static c => c.CardDragged))
|
||||
return false;
|
||||
|
||||
switch (e.Key)
|
||||
@@ -196,8 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
int newIndex = currentIndex + direction;
|
||||
|
||||
if (newIndex < 0)
|
||||
newIndex = Cards.Count() - 1;
|
||||
else if (newIndex >= Cards.Count())
|
||||
newIndex = Cards.Count - 1;
|
||||
else if (newIndex >= Cards.Count)
|
||||
newIndex = 0;
|
||||
|
||||
focusCard(newIndex);
|
||||
@@ -215,5 +218,55 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
|
||||
if (SelectionMode == HandSelectionMode.Single && !card.Selected)
|
||||
card.TriggerClick();
|
||||
}
|
||||
|
||||
private void cardDragged(PlayerHandCard card, Vector2 screenSpacePosition)
|
||||
{
|
||||
var cards = Cards.OrderBy(static c => c.Order).ToArray();
|
||||
|
||||
int newIndex = cardIndexInLayout(cards, card.ScreenSpaceDrawQuad.Centre);
|
||||
|
||||
card.Order = newIndex;
|
||||
|
||||
int order = 0;
|
||||
|
||||
foreach (var c in cards)
|
||||
{
|
||||
if (order == newIndex)
|
||||
order++;
|
||||
|
||||
if (c == card)
|
||||
continue;
|
||||
|
||||
c.Order = order++;
|
||||
}
|
||||
|
||||
foreach (var c in Cards)
|
||||
c.Item.DisplayOrder = c.Order;
|
||||
}
|
||||
|
||||
private int cardIndexInLayout(HandCard[] cards, Vector2 screenSpacePosition)
|
||||
{
|
||||
Debug.Assert(cards.Length > 0);
|
||||
|
||||
var position = ToLocalSpace(screenSpacePosition) - DrawSize / 2;
|
||||
|
||||
int activeIndex = GetActiveCardIndex(cards);
|
||||
|
||||
int minIndex = 0;
|
||||
float minDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < cards.Length; i++)
|
||||
{
|
||||
float distance = MathF.Abs(GetCardX(i, activeIndex) - position.X);
|
||||
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -15,13 +14,12 @@ 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.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
|
||||
{
|
||||
public partial class IntroScreen : RankedPlaySubScreen
|
||||
{
|
||||
protected override LocalisableString StageHeading => string.Empty;
|
||||
public override LocalisableString StageHeading => string.Empty;
|
||||
protected override LocalisableString StageCaption => string.Empty;
|
||||
|
||||
public IntroScreen()
|
||||
@@ -36,9 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MusicController? musicController { get; set; }
|
||||
|
||||
private Sample? windupSample;
|
||||
private Sample? impactSample;
|
||||
private Sample? swooshSample;
|
||||
@@ -81,8 +76,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
|
||||
|
||||
private StarRatingSequence? starRatingAnimation;
|
||||
|
||||
private IDisposable? duckOperation;
|
||||
|
||||
public void PlayIntroSequence(UserWithRating player, UserWithRating opponent, double starRating)
|
||||
{
|
||||
double delay = 0;
|
||||
@@ -95,8 +88,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
|
||||
|
||||
vsScreen.Play(ref delay, out double impactDelay);
|
||||
|
||||
duckOperation = musicController?.Duck();
|
||||
|
||||
if (windupSample != null)
|
||||
{
|
||||
Scheduler.AddDelayed(() => windupSample?.Play(), impactDelay - windupSample.Length);
|
||||
@@ -113,16 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
|
||||
{
|
||||
starRatingAnimation?.PopOut();
|
||||
|
||||
duckOperation?.Dispose();
|
||||
|
||||
this.Delay(500).FadeOut();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
duckOperation?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public CardFlow CenterRow { get; private set; } = null!;
|
||||
|
||||
protected override LocalisableString StageHeading => "Pick Phase";
|
||||
public override bool ShowStageOverlay => true;
|
||||
public override LocalisableString StageHeading => "Pick Phase";
|
||||
protected override LocalisableString StageCaption => "Waiting for your opponent...";
|
||||
|
||||
protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Red;
|
||||
@@ -34,6 +35,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
[Resolved]
|
||||
private RankedPlayMatchInfo matchInfo { get; set; } = null!;
|
||||
|
||||
private Sample? cardAddSample;
|
||||
|
||||
private const int card_play_samples = 2;
|
||||
private Sample?[]? cardPlaySamples;
|
||||
|
||||
@@ -56,15 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
CenterColumn.Children =
|
||||
[
|
||||
playerHand = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
Y = 100,
|
||||
HoverYOffset = 90
|
||||
},
|
||||
opponentHand = new OpponentHandOfCards
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -72,10 +66,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
},
|
||||
playerHand = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
},
|
||||
new HandReplayRecorder(playerHand),
|
||||
new HandReplayPlayer(matchInfo.OpponentId, opponentHand),
|
||||
];
|
||||
|
||||
cardAddSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/card-add-1");
|
||||
|
||||
cardPlaySamples = new Sample?[card_play_samples];
|
||||
for (int i = 0; i < card_play_samples; i++)
|
||||
cardPlaySamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Ranked/card-play-{1 + i}");
|
||||
@@ -85,24 +88,51 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
base.OnEntering(previous);
|
||||
|
||||
foreach (var card in matchInfo.PlayerCards)
|
||||
const double stagger = 50;
|
||||
double delay = 0;
|
||||
|
||||
foreach (var item in matchInfo.PlayerCards)
|
||||
{
|
||||
playerHand.AddCard(card, c =>
|
||||
double currentDelay = delay;
|
||||
|
||||
if ((previous as DiscardScreen)?.CenterRow.RemoveCard(item, out var card, out var drawQuad) == true)
|
||||
{
|
||||
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, DrawHeight), playerHand);
|
||||
});
|
||||
playerHand.AddCard(card, c =>
|
||||
{
|
||||
c.MatchScreenSpaceDrawQuad(drawQuad, playerHand);
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
playerHand.AddCard(item, c =>
|
||||
{
|
||||
c.Position = playerHand.BottomCardInsertPosition;
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
|
||||
delay = 0;
|
||||
|
||||
foreach (var card in matchInfo.OpponentCards)
|
||||
{
|
||||
double currentDelay = delay;
|
||||
|
||||
opponentHand.AddCard(card, c =>
|
||||
{
|
||||
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), playerHand);
|
||||
c.Position = opponentHand.BottomCardInsertPosition;
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
}
|
||||
|
||||
playerHand.UpdateLayout(stagger: 50);
|
||||
opponentHand.UpdateLayout(stagger: 50);
|
||||
delay += 50;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
public CardFlow CenterRow { get; private set; } = null!;
|
||||
|
||||
protected override LocalisableString StageHeading => "Pick Phase";
|
||||
public override bool ShowStageOverlay => true;
|
||||
|
||||
public override LocalisableString StageHeading => "Pick Phase";
|
||||
protected override LocalisableString StageCaption => "It's your turn to play a card!";
|
||||
|
||||
private PlayerHandOfCards playerHand = null!;
|
||||
@@ -73,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
CenterColumn.Children =
|
||||
[
|
||||
opponentHand = new OpponentHandOfCards
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
Y = -100,
|
||||
},
|
||||
playerHand = new PlayerHandOfCards
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
@@ -82,14 +92,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
SelectionMode = HandSelectionMode.Single,
|
||||
PlayCardAction = onPlayButtonClicked
|
||||
},
|
||||
opponentHand = new OpponentHandOfCards
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.5f,
|
||||
Y = -100,
|
||||
},
|
||||
new HandReplayRecorder(playerHand),
|
||||
new HandReplayPlayer(matchInfo.OpponentId, opponentHand),
|
||||
];
|
||||
@@ -149,41 +151,51 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
base.OnEntering(previous);
|
||||
|
||||
int delay = 0;
|
||||
const double stagger = 50;
|
||||
double delay = 0;
|
||||
|
||||
foreach (var item in matchInfo.PlayerCards)
|
||||
{
|
||||
double currentDelay = delay;
|
||||
|
||||
if ((previous as DiscardScreen)?.CenterRow.RemoveCard(item, out var card, out var drawQuad) == true)
|
||||
{
|
||||
playerHand.AddCard(card, c =>
|
||||
{
|
||||
c.MatchScreenSpaceDrawQuad(drawQuad, playerHand);
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
playerHand.AddCard(item, c =>
|
||||
{
|
||||
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, DrawHeight), playerHand);
|
||||
c.Position = playerHand.BottomCardInsertPosition;
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
|
||||
}, 50 * delay);
|
||||
delay++;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
|
||||
delay = 0;
|
||||
|
||||
foreach (var item in matchInfo.OpponentCards)
|
||||
{
|
||||
double currentDelay = delay;
|
||||
|
||||
opponentHand.AddCard(item, c =>
|
||||
{
|
||||
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), playerHand);
|
||||
c.DelayMovementOnEntering(currentDelay);
|
||||
});
|
||||
}
|
||||
|
||||
playerHand.UpdateLayout(stagger: 50);
|
||||
opponentHand.UpdateLayout(stagger: 50);
|
||||
delay += 50;
|
||||
}
|
||||
}
|
||||
|
||||
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
public readonly Bindable<MultiplayerPlaylistItem?> PlaylistItem = new Bindable<MultiplayerPlaylistItem?>();
|
||||
public readonly RankedPlayCardItem Card;
|
||||
|
||||
/// <summary>
|
||||
/// The player's preferred display order for this card
|
||||
/// </summary>
|
||||
public int? DisplayOrder { get; set; }
|
||||
|
||||
public RankedPlayCardWithPlaylistItem(RankedPlayCardItem card)
|
||||
{
|
||||
Card = card;
|
||||
|
||||
@@ -74,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
public bool IsOwnTurn => RoomState.ActiveUserId == client.LocalUser?.UserID;
|
||||
|
||||
public bool IsOpponentTurn => RoomState.ActiveUserId == OpponentId;
|
||||
|
||||
public int CurrentRound => RoomState.CurrentRound;
|
||||
|
||||
public int OpponentId => RoomState.Users.Keys.Single(u => u != client.LocalUser?.UserID);
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@@ -14,8 +15,10 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
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;
|
||||
@@ -56,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
@@ -69,6 +75,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
private QueueController? controller { get; set; }
|
||||
|
||||
private readonly MultiplayerRoom room;
|
||||
|
||||
private APIUser localUser = null!;
|
||||
private APIUser opponentUser = null!;
|
||||
|
||||
private readonly Container stageOverlayContainer;
|
||||
private readonly Container<RankedPlaySubScreen> screenContainer;
|
||||
private readonly RankedPlayChatDisplay chat;
|
||||
|
||||
@@ -88,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
[Cached]
|
||||
private readonly SongPreviewParticleContainer particleContainer;
|
||||
|
||||
[Cached]
|
||||
private BackgroundMusicManager backgroundMusic;
|
||||
|
||||
public RankedPlayScreen(MultiplayerRoom room)
|
||||
{
|
||||
this.room = room;
|
||||
@@ -127,8 +141,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
}
|
||||
}
|
||||
},
|
||||
stageOverlayContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
overlayContainer = new CardDetailsOverlayContainer(),
|
||||
particleContainer = new SongPreviewParticleContainer(),
|
||||
backgroundMusic = new BackgroundMusicManager()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,11 +169,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
int localUserId = api.LocalUser.Value.OnlineID;
|
||||
int opponentUserId = ((RankedPlayRoomState)client.Room!.MatchState!).Users.Keys.Single(it => it != localUserId);
|
||||
|
||||
localUser = users.GetUserAsync(localUserId).GetResultSafely()!;
|
||||
opponentUser = users.GetUserAsync(opponentUserId).GetResultSafely()!;
|
||||
|
||||
AddRangeInternal([
|
||||
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
|
||||
{
|
||||
State = { BindTarget = cornerPieceVisibility },
|
||||
Child = new RankedPlayUserDisplay(localUserId, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
Child = new RankedPlayUserDisplay(localUser, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Health = { BindTarget = matchInfo.PlayerHealth }
|
||||
@@ -163,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
|
||||
{
|
||||
State = { BindTarget = cornerPieceVisibility },
|
||||
Child = new RankedPlayUserDisplay(opponentUserId, Anchor.TopRight, RankedPlayColourScheme.Red)
|
||||
Child = new RankedPlayUserDisplay(opponentUser, Anchor.TopRight, RankedPlayColourScheme.Red)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Health = { BindTarget = matchInfo.OpponentHealth }
|
||||
@@ -195,6 +217,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
cornerPieceVisibility.BindTo(screen.CornerPieceVisibility);
|
||||
showBeatmapBackground.Value = screen.ShowBeatmapBackground;
|
||||
|
||||
if (screen.ShowStageOverlay)
|
||||
{
|
||||
APIUser? pickingUser = null;
|
||||
double? multiplier = matchInfo.Stage.Value < RankedPlayStage.CardPlay ? null : matchInfo.RoomState.DamageMultiplier;
|
||||
RankedPlayColourScheme colourScheme = RankedPlayColourScheme.Blue;
|
||||
|
||||
if (matchInfo.Stage.Value == RankedPlayStage.CardPlay && matchInfo.RoomState.ActiveUser != null)
|
||||
{
|
||||
pickingUser = matchInfo.IsOwnTurn ? localUser : opponentUser;
|
||||
colourScheme = matchInfo.IsOwnTurn ? RankedPlayColourScheme.Blue : RankedPlayColourScheme.Red;
|
||||
}
|
||||
|
||||
stageOverlayContainer.Add(new RankedPlayStageOverlay(screen.StageHeading, colourScheme)
|
||||
{
|
||||
PickingUser = pickingUser,
|
||||
Multiplier = multiplier,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,6 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
chat.Appear();
|
||||
|
||||
if (stage is RankedPlayStage.GameplayWarmup or RankedPlayStage.Gameplay)
|
||||
backgroundMusic.Stop();
|
||||
else
|
||||
backgroundMusic.Play();
|
||||
|
||||
switch (stage)
|
||||
{
|
||||
case RankedPlayStage.RoundWarmup when matchInfo.CurrentRound == 1:
|
||||
@@ -278,6 +324,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
chat.Disappear();
|
||||
backgroundMusic.Stop();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
|
||||
base.OnSuspending(e);
|
||||
@@ -296,6 +343,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
return true;
|
||||
}
|
||||
|
||||
backgroundMusic.Stop();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
|
||||
client.LeaveRoom().FireAndForget();
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public partial class RankedPlayStageOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly LocalisableString stageName;
|
||||
private readonly RankedPlayColourScheme colourScheme;
|
||||
|
||||
public APIUser? PickingUser { get; init; }
|
||||
public double? Multiplier { get; init; }
|
||||
|
||||
private FillFlowContainer displayContainer = null!;
|
||||
private FillFlowContainer detailsContainer = null!;
|
||||
private CircularContainer avatarContainer = null!;
|
||||
|
||||
public RankedPlayStageOverlay(LocalisableString stageName, RankedPlayColourScheme colourScheme)
|
||||
{
|
||||
this.stageName = stageName;
|
||||
this.colourScheme = colourScheme;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Alpha = 0.4f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4Extensions.FromHex("#000"),
|
||||
},
|
||||
displayContainer = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourScheme.Surface.Darken(0.1f),
|
||||
Alpha = 0.8f,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Padding = new MarginPadding { Vertical = 20 },
|
||||
Font = OsuFont.TorusAlternate.With(size: 72),
|
||||
Shadow = false,
|
||||
Text = stageName,
|
||||
},
|
||||
},
|
||||
},
|
||||
detailsContainer = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(40, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (PickingUser != null)
|
||||
{
|
||||
detailsContainer.Add(new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
avatarContainer = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(32),
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourScheme.Surface,
|
||||
Alpha = 0.5f,
|
||||
},
|
||||
},
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
UseFullGlyphHeight = false,
|
||||
Font = OsuFont.Torus.With(size: 32),
|
||||
Text = $"{PickingUser.Username}'s pick",
|
||||
Colour = colourScheme.Primary,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Multiplier != null)
|
||||
{
|
||||
detailsContainer.Add(new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
UseFullGlyphHeight = false,
|
||||
Font = OsuFont.Torus.With(size: 32),
|
||||
Text = $"{Multiplier:N0}x damage",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (PickingUser != null)
|
||||
LoadComponentAsync(new DrawableAvatar(PickingUser), a => avatarContainer.Add(a));
|
||||
|
||||
const int duration = 500;
|
||||
const int time_visible = 1500;
|
||||
|
||||
const Easing easing = Easing.OutQuint;
|
||||
|
||||
this.FadeInFromZero(300, easing);
|
||||
|
||||
displayContainer
|
||||
.ScaleTo(0.9f)
|
||||
.ScaleTo(1f, duration, easing);
|
||||
|
||||
using (BeginDelayedSequence(time_visible))
|
||||
{
|
||||
this.FadeOut(duration, easing)
|
||||
.Expire();
|
||||
|
||||
displayContainer
|
||||
.ScaleTo(0.9f, duration, easing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
public virtual bool ShowBeatmapBackground => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a fullscreen overlay displaying the current stage (and any additional
|
||||
/// information like the currently picking player and/or the damage multiplier)
|
||||
/// should be displayed upon entering this screen.
|
||||
/// </summary>
|
||||
public virtual bool ShowStageOverlay => false;
|
||||
|
||||
/// <summary>
|
||||
/// Heading text to be displayed indicating the purpose of the current stage.
|
||||
/// </summary>
|
||||
protected abstract LocalisableString StageHeading { get; }
|
||||
public abstract LocalisableString StageHeading { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
|
||||
|
||||
@@ -6,6 +6,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -37,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
public partial class ResultsScreen : RankedPlaySubScreen
|
||||
{
|
||||
protected override LocalisableString StageHeading => "Results";
|
||||
public override LocalisableString StageHeading => "Results";
|
||||
protected override LocalisableString StageCaption => string.Empty;
|
||||
|
||||
public override bool ShowBeatmapBackground => true;
|
||||
@@ -205,8 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
private RankedPlayDamageInfo losingDamageInfo = null!;
|
||||
|
||||
private Sample resultsAppearSample = null!;
|
||||
private Sample dmgFlySample = null!;
|
||||
private Sample dmgHitSample = null!;
|
||||
private Sample hpDownSample = null!;
|
||||
private Sample playerAppearSample = null!;
|
||||
private Sample pseudoScoreCounterSample = null!;
|
||||
private Sample scoreTickSample = null!;
|
||||
private Sample gradePassSample = null!;
|
||||
private Sample gradePassSsSample = null!;
|
||||
private Sample gradeFailSample = null!;
|
||||
private Sample gradeFailDSample = null!;
|
||||
private SampleChannel? playerScoreTickChannel;
|
||||
private SampleChannel? opponentScoreTickChannel;
|
||||
private readonly BindableDouble playerScoreTickPitch = new BindableDouble();
|
||||
private readonly BindableDouble opponentScoreTickPitch = new BindableDouble();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
// this works under the assumption that only one player can receive damage each round
|
||||
losingDamageInfo = matchInfo.RoomState.Users
|
||||
@@ -225,7 +243,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
State = { BindTarget = cornerPieceVisibility },
|
||||
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.UserID, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.User, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Health = { Value = PlayerDamageInfo.OldLife }
|
||||
@@ -236,7 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
State = { BindTarget = cornerPieceVisibility },
|
||||
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.UserID, Anchor.BottomRight, RankedPlayColourScheme.Red)
|
||||
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.User, Anchor.BottomRight, RankedPlayColourScheme.Red)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Health = { Value = OpponentDamageInfo.OldLife }
|
||||
@@ -417,6 +435,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
resultsAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear");
|
||||
dmgFlySample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly");
|
||||
dmgHitSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit");
|
||||
hpDownSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down");
|
||||
playerAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear");
|
||||
pseudoScoreCounterSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter");
|
||||
scoreTickSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/score-tick");
|
||||
gradePassSample = audio.Samples.Get(@"Results/rank-impact-pass");
|
||||
gradePassSsSample = audio.Samples.Get(@"Results/rank-impact-pass-ss");
|
||||
gradeFailSample = audio.Samples.Get(@"Results/rank-impact-fail");
|
||||
gradeFailDSample = audio.Samples.Get(@"Results/rank-impact-fail-d");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -436,6 +466,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
private void appear(ref double delay)
|
||||
{
|
||||
resultsAppearSample.Play();
|
||||
|
||||
panelScaffold.FadeIn(100)
|
||||
.ResizeTo(0)
|
||||
.ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo)
|
||||
@@ -451,7 +483,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
playerScoreCounter.FadeIn(600);
|
||||
opponentScoreCounter.FadeIn(600);
|
||||
|
||||
Schedule(() => cornerPieceVisibility.Value = Visibility.Visible);
|
||||
Schedule(() =>
|
||||
{
|
||||
cornerPieceVisibility.Value = Visibility.Visible;
|
||||
playerAppearSample.Play();
|
||||
});
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(900))
|
||||
@@ -487,12 +523,57 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
playerScoreBar.FadeIn(100);
|
||||
opponentScoreBar.FadeIn(100);
|
||||
|
||||
playerScoreTickChannel ??= scoreTickSample.GetChannel();
|
||||
playerScoreTickChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
playerScoreTickChannel.Frequency.BindTarget = playerScoreTickPitch;
|
||||
playerScoreTickPitch.Value = 0.5f;
|
||||
playerScoreTickChannel.Looping = true;
|
||||
|
||||
opponentScoreTickChannel ??= scoreTickSample.GetChannel();
|
||||
opponentScoreTickChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
opponentScoreTickChannel.Frequency.BindTarget = opponentScoreTickPitch;
|
||||
opponentScoreTickPitch.Value = 0.5f;
|
||||
opponentScoreTickChannel.Looping = true;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (losingDamageInfo.Damage > 0)
|
||||
pseudoScoreCounterSample.Play();
|
||||
|
||||
if (PlayerScore.TotalScore > 0)
|
||||
playerScoreTickChannel.Play();
|
||||
|
||||
if (OpponentScore.TotalScore > 0)
|
||||
opponentScoreTickChannel.Play();
|
||||
});
|
||||
|
||||
this.TransformBindableTo(scoreBarProgress, maxScorePercent, score_text_duration, new CubicBezierEasingFunction(easeIn: 0.4, easeOut: 1));
|
||||
this.TransformBindableTo(playerScoreTickPitch, 0.5f + playerScorePercent, score_text_duration, Easing.OutCubic);
|
||||
this.TransformBindableTo(opponentScoreTickPitch, 0.5f + opponentScorePercent, score_text_duration, Easing.OutCubic);
|
||||
|
||||
// safety timeout to ensure scoreTicks don't play forever
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (playerScoreTickChannel != null)
|
||||
playerScoreTickChannel.Looping = false;
|
||||
|
||||
if (opponentScoreTickChannel != null)
|
||||
opponentScoreTickChannel.Looping = false;
|
||||
}, score_text_duration + 500);
|
||||
|
||||
scoreBarProgress.BindValueChanged(e =>
|
||||
{
|
||||
playerScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, playerScorePercent));
|
||||
opponentScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, opponentScorePercent));
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent)
|
||||
playerScoreTickChannel.Looping = false;
|
||||
|
||||
if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent)
|
||||
opponentScoreTickChannel.Looping = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,6 +584,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
const double text_movement_duration = 400;
|
||||
|
||||
bool playerTookDamage = OpponentScore.TotalScore > PlayerScore.TotalScore;
|
||||
double loserPanDirection = playerTookDamage ? -OsuGameBase.SFX_STEREO_STRENGTH : OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
|
||||
using (BeginDelayedSequence(delay))
|
||||
{
|
||||
Schedule(() =>
|
||||
@@ -522,6 +606,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
.ScaleTo(0.9f)
|
||||
.ScaleTo(1f, 300, Easing.OutElasticHalf);
|
||||
|
||||
var dmgFlyChannel = dmgFlySample.GetChannel();
|
||||
this.TransformBindableTo(dmgFlyChannel.Balance, loserPanDirection, text_movement_duration, Easing.InCubic);
|
||||
dmgFlyChannel.Play();
|
||||
|
||||
flyingDamageText.FadeIn()
|
||||
.MoveTo(position, text_movement_duration, Easing.InCubic)
|
||||
.ScaleTo(0.75f, text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5))
|
||||
@@ -531,6 +619,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var dmgHitChannel = dmgHitSample.GetChannel();
|
||||
dmgHitChannel.Balance.Value = loserPanDirection;
|
||||
dmgHitChannel.Play();
|
||||
|
||||
userDisplay.Shake(shakeDuration: 60, shakeMagnitude: 2, maximumLength: 120);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
@@ -564,6 +656,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
playerUserDisplay.Health.Value = PlayerDamageInfo.NewLife;
|
||||
opponentUserDisplay.Health.Value = OpponentDamageInfo.NewLife;
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var hpDecreaseChannel = hpDownSample.GetChannel();
|
||||
hpDecreaseChannel.Balance.Value = loserPanDirection;
|
||||
hpDecreaseChannel.Play();
|
||||
}, 900);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -576,11 +675,45 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
{
|
||||
playerScoreDetails.FadeIn(300);
|
||||
opponentScoreDetails.FadeIn(300);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
SampleChannel playerRankChannel = getRankSample(PlayerScore.Rank).GetChannel();
|
||||
playerRankChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
playerRankChannel.Play();
|
||||
|
||||
SampleChannel opponentRankChannel = getRankSample(OpponentScore.Rank).GetChannel();
|
||||
opponentRankChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
opponentRankChannel.Play();
|
||||
});
|
||||
}
|
||||
|
||||
delay += 800;
|
||||
}
|
||||
|
||||
private Sample getRankSample(ScoreRank rank)
|
||||
{
|
||||
switch (rank)
|
||||
{
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return gradeFailDSample;
|
||||
|
||||
case ScoreRank.C:
|
||||
case ScoreRank.B:
|
||||
return gradeFailSample;
|
||||
|
||||
case ScoreRank.A:
|
||||
case ScoreRank.S:
|
||||
case ScoreRank.SH:
|
||||
return gradePassSample;
|
||||
|
||||
case ScoreRank.X:
|
||||
case ScoreRank.XH:
|
||||
return gradePassSsSample;
|
||||
}
|
||||
}
|
||||
|
||||
private static int numDigits(long value)
|
||||
{
|
||||
if (value <= 0)
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private MatchStartCountdown? currentMatchStartCountdown => client.Room?.ActiveCountdowns.OfType<MatchStartCountdown>().SingleOrDefault();
|
||||
|
||||
private readonly MultiplayerReadyButton readyButton;
|
||||
private readonly MultiplayerCountdownButton countdownButton;
|
||||
|
||||
@@ -111,22 +113,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
if (client.IsHost)
|
||||
if (client.IsReferee)
|
||||
{
|
||||
if (client.Room.State == MultiplayerRoomState.Open && currentMatchStartCountdown == null)
|
||||
startMatch();
|
||||
else if (client.Room.State == MultiplayerRoomState.WaitingForLoad || client.Room.State == MultiplayerRoomState.Playing)
|
||||
abortMatch();
|
||||
}
|
||||
else if (client.IsHost)
|
||||
{
|
||||
if (client.Room.State == MultiplayerRoomState.Open)
|
||||
{
|
||||
if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
if (isReady() && currentMatchStartCountdown == null)
|
||||
startMatch();
|
||||
else
|
||||
toggleReady();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dialogOverlay == null)
|
||||
abortMatch();
|
||||
else
|
||||
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
|
||||
}
|
||||
abortMatch();
|
||||
}
|
||||
else if (client.Room.State != MultiplayerRoomState.Closed)
|
||||
toggleReady();
|
||||
@@ -146,7 +150,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
endOperation();
|
||||
});
|
||||
|
||||
void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
|
||||
void performAbort() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
|
||||
|
||||
void abortMatch()
|
||||
{
|
||||
if (dialogOverlay == null)
|
||||
performAbort();
|
||||
else
|
||||
dialogOverlay.Push(new ConfirmAbortDialog(performAbort, endOperation));
|
||||
}
|
||||
}
|
||||
|
||||
private void startCountdown(TimeSpan duration)
|
||||
@@ -159,14 +171,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
private void cancelCountdown()
|
||||
{
|
||||
if (client.Room == null)
|
||||
if (client.Room == null || currentMatchStartCountdown == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
|
||||
client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
|
||||
client.SendMatchRequest(new StopCountdownRequest(currentMatchStartCountdown.ID)).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void endOperation()
|
||||
@@ -186,10 +197,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
var localUser = client.LocalUser;
|
||||
|
||||
int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
int newCountReady = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating);
|
||||
|
||||
if (!client.IsHost || client.Room.Settings.AutoStartEnabled)
|
||||
if ((!client.IsHost && !client.IsReferee) || client.Room.Settings.AutoStartEnabled || client.Room.State != MultiplayerRoomState.Open)
|
||||
countdownButton.Hide();
|
||||
else
|
||||
{
|
||||
@@ -214,12 +225,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
|
||||
readyButton.Enabled.Value &= (client.IsHost || client.IsReferee) && newCountReady > 0 && currentMatchStartCountdown == null;
|
||||
|
||||
// When the local user is not the host, the button should only be enabled when no match is in progress.
|
||||
if (!client.IsHost)
|
||||
// When the local user is not the host or a referee, the button should only be enabled when no match is in progress.
|
||||
if (!client.IsHost && !client.IsReferee)
|
||||
readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
// As a referee, readying up should not be possible, so if there is no match going on and no users readied up, prevent a match start.
|
||||
if (client.IsReferee)
|
||||
readyButton.Enabled.Value &= client.Room.State != MultiplayerRoomState.Open || newCountReady > 0;
|
||||
|
||||
// At all times, the countdown button should only be enabled when no match is in progress.
|
||||
countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
});
|
||||
}
|
||||
|
||||
if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && multiplayerClient.IsHost)
|
||||
if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && (multiplayerClient.IsHost || multiplayerClient.IsReferee))
|
||||
{
|
||||
flow.Add(new RoundedButton
|
||||
{
|
||||
|
||||
@@ -122,14 +122,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
var localUser = multiplayerClient.LocalUser;
|
||||
|
||||
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
int countReady = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready);
|
||||
int countTotal = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating);
|
||||
string countText = $"({countReady} / {countTotal} ready)";
|
||||
|
||||
if (countdown != null)
|
||||
{
|
||||
string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
|
||||
string? countdownText = countdown != null ? $"Starting in {countdownTimeRemaining:mm\\:ss}" : null;
|
||||
|
||||
if (multiplayerClient.IsReferee)
|
||||
{
|
||||
if (room.State == MultiplayerRoomState.Open)
|
||||
Text = countReady == 0 ? $"Waiting for players... {countText}" : $"{countdownText ?? "Start match"} {countText}";
|
||||
else
|
||||
Text = "Abort match";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (countdownText != null)
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
@@ -196,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
default:
|
||||
// Show the abort button for the host as long as gameplay is in progress.
|
||||
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
|
||||
if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && room.State != MultiplayerRoomState.Open)
|
||||
setRed();
|
||||
else
|
||||
setGreen();
|
||||
@@ -204,7 +214,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
|
||||
setGreen();
|
||||
else
|
||||
setYellow();
|
||||
|
||||
@@ -66,8 +66,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
if (multiplayerClient.Room == null)
|
||||
return;
|
||||
|
||||
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
|
||||
bool isValidItem = isItemOwner && !Item.Expired;
|
||||
bool isItemOwnerOrReferee = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost || multiplayerClient.IsReferee;
|
||||
bool isValidItem = isItemOwnerOrReferee && !Item.Expired;
|
||||
|
||||
AllowDeletion = isValidItem
|
||||
&& (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
|
||||
|
||||
@@ -596,7 +596,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
break;
|
||||
|
||||
default:
|
||||
targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users)));
|
||||
if (!client.IsReferee)
|
||||
targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -676,8 +677,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Ruleset.Value = ruleset;
|
||||
Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
bool freemods = item.Freestyle || item.AllowedMods.Any();
|
||||
bool freestyle = item.Freestyle;
|
||||
bool freemods = !client.IsReferee && (item.Freestyle || item.AllowedMods.Any());
|
||||
bool freestyle = !client.IsReferee && item.Freestyle;
|
||||
|
||||
if (freemods)
|
||||
userModsSection.Show();
|
||||
@@ -921,8 +922,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (client.Room.CanAddPlaylistItems(client.LocalUser) != true)
|
||||
return;
|
||||
|
||||
// If there's only one playlist item and we are the host, assume we want to change it. Else add a new one.
|
||||
PlaylistItem? itemToEdit = client.IsHost && room.Playlist.Count == 1 ? room.Playlist.Single() : null;
|
||||
// If there's only one playlist item and we are the host / a referee, assume we want to change it. Else add a new one.
|
||||
PlaylistItem? itemToEdit = (client.IsHost || client.IsReferee) && room.Playlist.Count == 1 ? room.Playlist.Single() : null;
|
||||
|
||||
ShowSongSelect(itemToEdit);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
ChangeSettingsButton.Alpha = client.IsHost ? 1 : 0;
|
||||
ChangeSettingsButton.Alpha = client.IsHost || client.IsReferee ? 1 : 0;
|
||||
SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem);
|
||||
});
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
userStyleDisplay.FadeOut(fade_time);
|
||||
}
|
||||
|
||||
kickButton.Alpha = client.IsHost && !user.Equals(client.LocalUser) ? 1 : 0;
|
||||
kickButton.Alpha = (client.IsHost || client.IsReferee) && !user.Equals(client.LocalUser) ? 1 : 0;
|
||||
crown.Alpha = client.Room.Host?.Equals(user) == true ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -312,8 +312,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
if (user.UserID == api.LocalUser.Value.Id)
|
||||
return null;
|
||||
|
||||
// If the local user is not the host of the room.
|
||||
if (client.Room.Host?.UserID != api.LocalUser.Value.Id)
|
||||
if (!client.IsHost && !client.IsReferee)
|
||||
return null;
|
||||
|
||||
int targetUser = user.UserID;
|
||||
@@ -322,8 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
|
||||
{
|
||||
// Ensure the local user is still host.
|
||||
if (!client.IsHost)
|
||||
// Ensure the local user is still host / a referee.
|
||||
if (!client.IsHost && !client.IsReferee)
|
||||
return;
|
||||
|
||||
client.TransferHost(targetUser).FireAndForget();
|
||||
@@ -331,7 +330,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
|
||||
{
|
||||
// Ensure the local user is still host.
|
||||
if (!client.IsHost)
|
||||
if (!client.IsHost && !client.IsReferee)
|
||||
return;
|
||||
|
||||
client.KickUser(targetUser).FireAndForget();
|
||||
|
||||
@@ -5,11 +5,16 @@ using System;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.Leaderboards;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using FontWeight = osu.Game.Graphics.FontWeight;
|
||||
using OsuFont = osu.Game.Graphics.OsuFont;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
@@ -39,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
|
||||
{
|
||||
this.spectatorPlayerClock = spectatorPlayerClock;
|
||||
|
||||
ShowSettingsOverlay = false;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -54,8 +61,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
// also applied in `MultiplayerPlayer.load()`
|
||||
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
|
||||
|
||||
HUDOverlay.PlayerSettingsOverlay.Expire();
|
||||
HUDOverlay.HoldToQuit.Expire();
|
||||
|
||||
// Player username display
|
||||
GameplayClockContainer.Add(new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Title.With(size: 60, weight: FontWeight.SemiBold))
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Text = Score.ScoreInfo.User.Username,
|
||||
Y = 50,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
private readonly Room room;
|
||||
|
||||
private PlayerSettingsOverlay playerSettingsOverlay = null!;
|
||||
private ReplaySettingsOverlay replaySettingsOverlay = null!;
|
||||
private Bindable<bool> configSettingsOverlay = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
ReadyToStart = performInitialSeek,
|
||||
},
|
||||
playerSettingsOverlay = new PlayerSettingsOverlay
|
||||
replaySettingsOverlay = new ReplaySettingsOverlay
|
||||
{
|
||||
Alpha = 0,
|
||||
}
|
||||
@@ -189,9 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private void updateVisibility()
|
||||
{
|
||||
if (configSettingsOverlay.Value)
|
||||
playerSettingsOverlay.Show();
|
||||
replaySettingsOverlay.Show();
|
||||
else
|
||||
playerSettingsOverlay.Hide();
|
||||
replaySettingsOverlay.Hide();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class ReplayOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
public ReplaySettingsOverlay Settings { get; private set; } = null!;
|
||||
|
||||
private const int fade_duration = 200;
|
||||
|
||||
private Bindable<bool> configSettingsOverlay = null!;
|
||||
private Container messageContainer = null!;
|
||||
private Container content = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
|
||||
|
||||
InternalChild = content = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
messageContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
},
|
||||
Settings = new ReplaySettingsOverlay(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
configSettingsOverlay.BindValueChanged(_ => updateVisibility(), true);
|
||||
}
|
||||
|
||||
private void updateVisibility()
|
||||
{
|
||||
if (configSettingsOverlay.Value)
|
||||
content.FadeIn(fade_duration, Easing.OutQuint);
|
||||
else
|
||||
content.FadeOut(fade_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleReplaySettings:
|
||||
configSettingsOverlay.Value = !configSettingsOverlay.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Show() => this.FadeIn(fade_duration, Easing.OutQuint);
|
||||
public override void Hide() => this.FadeOut(fade_duration, Easing.OutQuint);
|
||||
|
||||
public void SetMessage(ScrollingMessage scrollingMessage) => messageContainer.Child = scrollingMessage;
|
||||
}
|
||||
}
|
||||
+9
-11
@@ -19,7 +19,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class PlayerSettingsOverlay : ExpandingContainer
|
||||
public partial class ReplaySettingsOverlay : ExpandingContainer
|
||||
{
|
||||
private const float padding = 10;
|
||||
|
||||
@@ -27,11 +27,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private const float player_settings_width = 270;
|
||||
|
||||
private const int fade_duration = 200;
|
||||
|
||||
public override void Show() => this.FadeIn(fade_duration);
|
||||
public override void Hide() => this.FadeOut(fade_duration);
|
||||
|
||||
// we'll handle this ourselves because we have slightly custom logic.
|
||||
protected override bool ExpandOnHover => false;
|
||||
|
||||
@@ -52,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
// while collapsed down, so let's avoid that.
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
public PlayerSettingsOverlay()
|
||||
public ReplaySettingsOverlay()
|
||||
: base(0, EXPANDED_WIDTH)
|
||||
{
|
||||
Origin = Anchor.TopRight;
|
||||
@@ -81,11 +76,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Action = () => Expanded.Toggle()
|
||||
});
|
||||
|
||||
AddInternal(new Box
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
|
||||
Depth = float.MaxValue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
new Box
|
||||
{
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
|
||||
Depth = float.MaxValue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,11 +38,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public const Easing FADE_EASING = Easing.OutQuint;
|
||||
|
||||
/// <summary>
|
||||
/// The total height of all the bottom of screen scoring elements.
|
||||
/// </summary>
|
||||
public float BottomScoringElementsHeight { get; private set; }
|
||||
|
||||
protected override bool ShouldBeConsideredForInput(Drawable child)
|
||||
{
|
||||
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
|
||||
@@ -56,7 +51,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public readonly ModDisplay ModDisplay;
|
||||
public readonly HoldForMenuButton HoldToQuit;
|
||||
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
|
||||
|
||||
[Cached]
|
||||
private readonly ClicksPerSecondController clicksPerSecondController;
|
||||
@@ -82,7 +76,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private Bindable<HUDVisibilityMode> configVisibilityMode;
|
||||
private Bindable<bool> configLeaderboardVisibility;
|
||||
private Bindable<bool> configSettingsOverlay;
|
||||
|
||||
private readonly BindableBool replayLoaded = new BindableBool();
|
||||
|
||||
@@ -116,8 +109,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, PlayerConfiguration configuration)
|
||||
{
|
||||
Container rightSettings;
|
||||
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
this.configuration = configuration;
|
||||
@@ -170,17 +161,6 @@ namespace osu.Game.Screens.Play
|
||||
HoldToQuit = CreateHoldForMenuButton(),
|
||||
}
|
||||
},
|
||||
rightSettings = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
PlayerSettingsOverlay = new PlayerSettingsOverlay
|
||||
{
|
||||
Alpha = 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
TopLeftElements = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -190,7 +170,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, TopRightElements, rightSettings };
|
||||
hideTargets = new List<Drawable> { mainComponents, TopRightElements };
|
||||
|
||||
if (rulesetComponents != null)
|
||||
hideTargets.Add(rulesetComponents);
|
||||
@@ -210,7 +190,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
configVisibilityMode = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode);
|
||||
configLeaderboardVisibility = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard);
|
||||
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
|
||||
|
||||
if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce)
|
||||
{
|
||||
@@ -238,7 +217,6 @@ namespace osu.Game.Screens.Play
|
||||
holdingForHUD.BindValueChanged(_ => updateVisibility());
|
||||
IsPlaying.BindValueChanged(_ => updateVisibility());
|
||||
configVisibilityMode.BindValueChanged(_ => updateVisibility());
|
||||
configSettingsOverlay.BindValueChanged(_ => updateVisibility());
|
||||
|
||||
replayLoaded.BindValueChanged(e =>
|
||||
{
|
||||
@@ -295,7 +273,7 @@ namespace osu.Game.Screens.Play
|
||||
TopLeftElements.Y = 0;
|
||||
|
||||
if (highestBottomScreenSpace.HasValue && DrawHeight - BottomRightElements.DrawHeight > 0)
|
||||
BottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
|
||||
BottomRightElements.Y = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
|
||||
else
|
||||
BottomRightElements.Y = 0;
|
||||
|
||||
@@ -349,11 +327,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private void updateVisibility()
|
||||
{
|
||||
if (configSettingsOverlay.Value && replayLoaded.Value)
|
||||
PlayerSettingsOverlay.Show();
|
||||
else
|
||||
PlayerSettingsOverlay.Hide();
|
||||
|
||||
if (ShowHud.Disabled)
|
||||
return;
|
||||
|
||||
@@ -415,10 +388,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleReplaySettings:
|
||||
configSettingsOverlay.Value = !configSettingsOverlay.Value;
|
||||
return true;
|
||||
|
||||
case GlobalAction.HoldForHUD:
|
||||
holdingForHUD.Value = true;
|
||||
return false;
|
||||
|
||||
@@ -320,32 +320,32 @@ namespace osu.Game.Screens.Play
|
||||
OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null,
|
||||
OnQuit = () => PerformExitWithConfirmation(),
|
||||
},
|
||||
exitOverlay = new HotkeyExitOverlay
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
PerformExit(skipTransition: true);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
GameplayClockContainer.Add(exitOverlay = new HotkeyExitOverlay
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
Action = () =>
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
PerformExit(skipTransition: true);
|
||||
},
|
||||
});
|
||||
|
||||
if (Configuration.AllowRestart)
|
||||
{
|
||||
rulesetSkinProvider.AddRange(new Drawable[]
|
||||
GameplayClockContainer.Add(retryOverlay = new HotkeyRetryOverlay
|
||||
{
|
||||
retryOverlay = new HotkeyRetryOverlay
|
||||
Depth = float.MinValue,
|
||||
Action = () =>
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
Restart(true);
|
||||
},
|
||||
Restart(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -361,6 +361,9 @@ namespace osu.Game.Screens.Play
|
||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||
failAnimationContainer.Add(createOverlayComponents());
|
||||
|
||||
// Used by ReplaySettingsOverlay for button positioning.
|
||||
dependencies.CacheAs(HUDOverlay);
|
||||
|
||||
if (!DrawableRuleset.AllowGameplayOverlays)
|
||||
{
|
||||
HUDOverlay.ShowHud.Value = false;
|
||||
@@ -426,16 +429,6 @@ namespace osu.Game.Screens.Play
|
||||
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Components which were created via <see cref="CreateOverlayComponents"/>.
|
||||
/// </summary>
|
||||
public Drawable OverlayComponents { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Implement to add any components which should exist above gameplay but below the HUD.
|
||||
/// </summary>
|
||||
protected virtual Drawable CreateOverlayComponents() => Empty();
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
|
||||
private Drawable createUnderlayComponents(WorkingBeatmap working)
|
||||
@@ -484,7 +477,6 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
|
||||
OverlayComponents = CreateOverlayComponents(),
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration)
|
||||
{
|
||||
HoldToQuit =
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.Leaderboards;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@@ -47,6 +48,8 @@ namespace osu.Game.Screens.Play
|
||||
private ReplayFailIndicator? failIndicator;
|
||||
private PlaybackSettings? playbackSettings;
|
||||
|
||||
public ReplayOverlay ReplayOverlay { get; private set; } = null!;
|
||||
|
||||
protected override bool CheckModsAllowFailure()
|
||||
{
|
||||
// autoplay should be able to fail if the beatmap is not humanly beatable
|
||||
@@ -80,7 +83,7 @@ namespace osu.Game.Screens.Play
|
||||
/// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings group to be shown.</param>
|
||||
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => HUDOverlay.PlayerSettingsOverlay.Add(settings));
|
||||
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => ReplayOverlay.Settings.Add(settings));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
@@ -90,6 +93,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
AddInternal(leaderboardProvider);
|
||||
|
||||
GameplayClockContainer.Add(ReplayOverlay = new ReplayOverlay());
|
||||
|
||||
playbackSettings = new PlaybackSettings
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
@@ -99,7 +104,25 @@ namespace osu.Game.Screens.Play
|
||||
if (GameplayClockContainer is MasterGameplayClockContainer master)
|
||||
playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate);
|
||||
|
||||
HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings);
|
||||
ReplayOverlay.Settings.AddAtStart(playbackSettings);
|
||||
|
||||
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
|
||||
message.AddText("Watching ");
|
||||
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" play ");
|
||||
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" on ");
|
||||
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
});
|
||||
|
||||
ReplayOverlay.SetMessage(new ScrollingMessage(message)
|
||||
{
|
||||
Y = 96,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
});
|
||||
|
||||
AddInternal(new RulesetSkinProvidingContainer(GameplayState.Ruleset, GameplayState.Beatmap, Beatmap.Value.Skin)
|
||||
{
|
||||
@@ -117,27 +140,6 @@ namespace osu.Game.Screens.Play
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateOverlayComponents()
|
||||
{
|
||||
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
|
||||
message.AddText("Watching ");
|
||||
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" play ");
|
||||
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" on ");
|
||||
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
});
|
||||
|
||||
return new ScrollingMessage(message)
|
||||
{
|
||||
Y = 100,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
DrawableRuleset?.SetReplayScore(Score);
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@@ -23,29 +24,38 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Score score;
|
||||
|
||||
public bool ShowSettingsOverlay { get; init; } = true;
|
||||
|
||||
protected SpectatorPlayer(Score score, PlayerConfiguration? configuration = null)
|
||||
: base(configuration)
|
||||
{
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
protected override Drawable CreateOverlayComponents()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
|
||||
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
|
||||
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
|
||||
message.AddText("Watching ");
|
||||
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" play ");
|
||||
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
|
||||
|
||||
return new ScrollingMessage(message)
|
||||
if (ShowSettingsOverlay)
|
||||
{
|
||||
Y = 100,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
};
|
||||
var replayOverlay = new ReplayOverlay();
|
||||
GameplayClockContainer.Add(replayOverlay);
|
||||
|
||||
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
|
||||
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
|
||||
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
|
||||
message.AddText("Watching ");
|
||||
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" play ");
|
||||
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
|
||||
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
|
||||
|
||||
replayOverlay.SetMessage(new ScrollingMessage(message)
|
||||
{
|
||||
Y = 96,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
||||
@@ -860,6 +860,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public async Task PlayUserCard(int userId, Func<RankedPlayCardItem[], RankedPlayCardItem> selector)
|
||||
{
|
||||
RankedPlayCardItem card = selector(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.ToArray());
|
||||
MultiplayerPlaylistItem? item = GetCardWithPlaylistItem(card).PlaylistItem.Value;
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
ServerRoom!.Playlist.Add(item);
|
||||
await ((IMultiplayerClient)this).PlaylistItemAdded(clone(item)).ConfigureAwait(false);
|
||||
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
|
||||
|
||||
var settings = clone(ServerRoom!.Settings);
|
||||
settings.PlaylistItemId = item.ID;
|
||||
await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ((IRankedPlayClient)this).RankedPlayCardPlayed(clone(card)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2026.318.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.331.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.404.0" />
|
||||
<PackageReference Include="Sentry" Version="6.2.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.47.0" />
|
||||
|
||||
Reference in New Issue
Block a user