mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 07:42:57 +08:00
Merge branch 'master' into add-bundle-header
This commit is contained in:
commit
88b3bf06e8
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1212.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1214.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -156,10 +156,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
|
||||
// Scale the speed value with accuracy _slightly_
|
||||
speedValue *= 0.02 + accuracy;
|
||||
// It is important to also consider accuracy difficulty when doing that
|
||||
speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
|
||||
// Scale the speed value with accuracy and OD
|
||||
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
createDrawableRuleset();
|
||||
|
||||
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
|
||||
assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
|
||||
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
IsCentre = (hitObject as Hit)?.Type == HitType.Centre,
|
||||
IsDrumRoll = hitObject is DrumRoll,
|
||||
IsSwell = hitObject is Swell,
|
||||
IsStrong = ((TaikoHitObject)hitObject).IsStrong
|
||||
IsStrong = (hitObject as TaikoStrongableHitObject)?.IsStrong == true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
|
||||
{
|
||||
TaikoHitObject first = x.First();
|
||||
if (x.Skip(1).Any() && first.CanBeStrong)
|
||||
first.IsStrong = true;
|
||||
if (x.Skip(1).Any() && first is TaikoStrongableHitObject strong)
|
||||
strong.IsStrong = true;
|
||||
return first;
|
||||
}).ToList();
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
base.UpdateTernaryStates();
|
||||
|
||||
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
|
||||
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
|
||||
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoStrongableHitObject>(), h => h.IsStrong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
major.BindValueChanged(updateMajor);
|
||||
major.BindValueChanged(updateMajor, true);
|
||||
}
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
|
@ -19,7 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public class DrawableDrumRoll : DrawableTaikoHitObject<DrumRoll>
|
||||
public class DrawableDrumRoll : DrawableTaikoStrongableHitObject<DrumRoll, DrumRoll.StrongNestedHit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of rolling hits required to reach the dark/final colour.
|
||||
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Content.X = DrawHeight / 2;
|
||||
}
|
||||
|
||||
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
|
||||
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
@ -164,8 +164,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
private class StrongNestedHit : DrawableStrongNestedHit
|
||||
{
|
||||
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)
|
||||
: base(strong, drumRoll)
|
||||
public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll)
|
||||
: base(nestedHit, drumRoll)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick>
|
||||
public class DrawableDrumRollTick : DrawableTaikoStrongableHitObject<DrumRollTick, DrumRollTick.StrongNestedHit>
|
||||
{
|
||||
/// <summary>
|
||||
/// The hit type corresponding to the <see cref="TaikoAction"/> that the user pressed to hit this <see cref="DrawableDrumRollTick"/>.
|
||||
@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
return UpdateResult(true);
|
||||
}
|
||||
|
||||
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
|
||||
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
|
||||
|
||||
private class StrongNestedHit : DrawableStrongNestedHit
|
||||
{
|
||||
public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick)
|
||||
: base(strong, tick)
|
||||
public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick)
|
||||
: base(nestedHit, tick)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public class DrawableHit : DrawableTaikoHitObject<Hit>
|
||||
public class DrawableHit : DrawableTaikoStrongableHitObject<Hit, Hit.StrongNestedHit>
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of keys which can result in hits for this HitObject.
|
||||
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
|
||||
protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
|
||||
|
||||
private class StrongNestedHit : DrawableStrongNestedHit
|
||||
{
|
||||
@ -240,8 +240,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public new DrawableHit MainObject => (DrawableHit)base.MainObject;
|
||||
|
||||
public StrongNestedHit(StrongHitObject strong, DrawableHit hit)
|
||||
: base(strong, hit)
|
||||
public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit)
|
||||
: base(nestedHit, hit)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -7,14 +7,14 @@ using osu.Game.Rulesets.Taiko.Judgements;
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoHitObject"/>s.
|
||||
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoStrongableHitObject{TObject,TStrongNestedObject}"/>s.
|
||||
/// </summary>
|
||||
public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject
|
||||
{
|
||||
public readonly DrawableHitObject MainObject;
|
||||
|
||||
protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject)
|
||||
: base(strong)
|
||||
protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject)
|
||||
: base(nestedHit)
|
||||
{
|
||||
MainObject = mainObject;
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
foreach (var t in ticks)
|
||||
{
|
||||
if (!t.IsHit)
|
||||
if (!t.Result.HasResult)
|
||||
{
|
||||
nextTick = t;
|
||||
break;
|
||||
@ -208,7 +208,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
continue;
|
||||
}
|
||||
|
||||
tick.TriggerResult(false);
|
||||
if (!tick.Result.HasResult)
|
||||
tick.TriggerResult(false);
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult);
|
||||
|
@ -4,13 +4,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -120,112 +118,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
protected Vector2 BaseSize;
|
||||
protected SkinnableDrawable MainPiece;
|
||||
|
||||
private readonly Bindable<bool> isStrong;
|
||||
|
||||
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
||||
|
||||
protected DrawableTaikoHitObject(TObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
|
||||
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.Custom;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
isStrong.BindValueChanged(_ =>
|
||||
{
|
||||
// will overwrite samples, should only be called on change.
|
||||
updateSamplesFromStrong();
|
||||
|
||||
RecreatePieces();
|
||||
});
|
||||
|
||||
RecreatePieces();
|
||||
}
|
||||
|
||||
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
|
||||
|
||||
protected override void LoadSamples()
|
||||
{
|
||||
base.LoadSamples();
|
||||
|
||||
if (HitObject.CanBeStrong)
|
||||
isStrong.Value = getStrongSamples().Any();
|
||||
}
|
||||
|
||||
private void updateSamplesFromStrong()
|
||||
{
|
||||
var strongSamples = getStrongSamples();
|
||||
|
||||
if (isStrong.Value != strongSamples.Any())
|
||||
{
|
||||
if (isStrong.Value)
|
||||
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
else
|
||||
{
|
||||
foreach (var sample in strongSamples)
|
||||
HitObject.Samples.Remove(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void RecreatePieces()
|
||||
{
|
||||
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
|
||||
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
|
||||
|
||||
MainPiece?.Expire();
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableStrongNestedHit strong:
|
||||
strongHitContainer.Add(strong);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
base.ClearNestedHitObjects();
|
||||
strongHitContainer.Clear();
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case StrongHitObject strong:
|
||||
return CreateStrongHit(strong);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.
|
||||
/// This is only invoked if <see cref="TaikoHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The strong hitobject.</param>
|
||||
/// <returns>The strong hitobject handler.</returns>
|
||||
protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null;
|
||||
}
|
||||
}
|
||||
|
@ -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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public abstract class DrawableTaikoStrongableHitObject<TObject, TStrongNestedObject> : DrawableTaikoHitObject<TObject>
|
||||
where TObject : TaikoStrongableHitObject
|
||||
where TStrongNestedObject : StrongNestedHitObject
|
||||
{
|
||||
private readonly Bindable<bool> isStrong;
|
||||
|
||||
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
||||
|
||||
protected DrawableTaikoStrongableHitObject(TObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
|
||||
|
||||
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
isStrong.BindValueChanged(_ =>
|
||||
{
|
||||
// will overwrite samples, should only be called on change.
|
||||
updateSamplesFromStrong();
|
||||
|
||||
RecreatePieces();
|
||||
});
|
||||
}
|
||||
|
||||
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
|
||||
|
||||
protected override void LoadSamples()
|
||||
{
|
||||
base.LoadSamples();
|
||||
isStrong.Value = getStrongSamples().Any();
|
||||
}
|
||||
|
||||
private void updateSamplesFromStrong()
|
||||
{
|
||||
var strongSamples = getStrongSamples();
|
||||
|
||||
if (isStrong.Value != strongSamples.Any())
|
||||
{
|
||||
if (isStrong.Value)
|
||||
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
else
|
||||
{
|
||||
foreach (var sample in strongSamples)
|
||||
HitObject.Samples.Remove(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
base.RecreatePieces();
|
||||
if (HitObject.IsStrong)
|
||||
Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableStrongNestedHit strong:
|
||||
strongHitContainer.Add(strong);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
base.ClearNestedHitObjects();
|
||||
strongHitContainer.Clear();
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case TStrongNestedObject strong:
|
||||
return CreateStrongNestedHit(strong);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongNestedHitObject"/>.
|
||||
/// This is only invoked if <see cref="TaikoStrongableHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The strong hitobject.</param>
|
||||
/// <returns>The strong hitobject handler.</returns>
|
||||
protected abstract DrawableStrongNestedHit CreateStrongNestedHit(TStrongNestedObject hitObject);
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRoll : TaikoHitObject, IHasPath
|
||||
public class DrumRoll : TaikoStrongableHitObject, IHasPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
|
||||
@ -109,6 +109,12 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
}
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
double IHasDistance.Distance => Duration * Velocity;
|
||||
|
@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRollTick : TaikoHitObject
|
||||
public class DrumRollTick : TaikoStrongableHitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this is the first (initial) tick of the slider.
|
||||
@ -28,5 +28,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class Hit : TaikoHitObject
|
||||
public class Hit : TaikoStrongableHitObject
|
||||
{
|
||||
public readonly Bindable<HitType> TypeBindable = new Bindable<HitType>();
|
||||
|
||||
@ -17,5 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
get => TypeBindable.Value;
|
||||
set => TypeBindable.Value = value;
|
||||
}
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,11 @@ using osu.Game.Rulesets.Taiko.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class StrongHitObject : TaikoHitObject
|
||||
/// <summary>
|
||||
/// Base type for nested strong hits.
|
||||
/// Used by <see cref="TaikoStrongableHitObject"/>s to represent their strong bonus scoring portions.
|
||||
/// </summary>
|
||||
public abstract class StrongNestedHitObject : TaikoHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new TaikoStrongJudgement();
|
||||
|
@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
set => Duration = value - StartTime;
|
||||
}
|
||||
|
||||
public override bool CanBeStrong => false;
|
||||
|
||||
public double Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +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.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -19,47 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public const float DEFAULT_SIZE = 0.45f;
|
||||
|
||||
/// <summary>
|
||||
/// Scale multiplier for a strong drawable taiko hit object.
|
||||
/// </summary>
|
||||
public const float STRONG_SCALE = 1.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Default size of a strong drawable taiko hit object.
|
||||
/// </summary>
|
||||
public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE;
|
||||
|
||||
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
|
||||
/// </summary>
|
||||
public virtual bool CanBeStrong => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this HitObject is a "strong" type.
|
||||
/// Strong hit objects give more points for hitting the hit object with both keys.
|
||||
/// </summary>
|
||||
public bool IsStrong
|
||||
{
|
||||
get => IsStrongBindable.Value;
|
||||
set
|
||||
{
|
||||
if (value && !CanBeStrong)
|
||||
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
|
||||
|
||||
IsStrongBindable.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
if (IsStrong)
|
||||
AddNested(new StrongHitObject { StartTime = this.GetEndTime() });
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new TaikoJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => new TaikoHitWindows();
|
||||
|
52
osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
Normal file
52
osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for taiko hitobjects that can become strong (large).
|
||||
/// </summary>
|
||||
public abstract class TaikoStrongableHitObject : TaikoHitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Scale multiplier for a strong drawable taiko hit object.
|
||||
/// </summary>
|
||||
public const float STRONG_SCALE = 1.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Default size of a strong drawable taiko hit object.
|
||||
/// </summary>
|
||||
public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE;
|
||||
|
||||
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this HitObject is a "strong" type.
|
||||
/// Strong hit objects give more points for hitting the hit object with both keys.
|
||||
/// </summary>
|
||||
public bool IsStrong
|
||||
{
|
||||
get => IsStrongBindable.Value;
|
||||
set => IsStrongBindable.Value = value;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
if (IsStrong)
|
||||
AddNested(CreateStrongNestedHit(this.GetEndTime()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="StrongNestedHitObject"/> representing a second hit on this object.
|
||||
/// This is only called if <see cref="IsStrong"/> is true.
|
||||
/// </summary>
|
||||
/// <param name="startTime">The start time of the nested hit.</param>
|
||||
protected abstract StrongNestedHitObject CreateStrongNestedHit(double startTime);
|
||||
}
|
||||
}
|
@ -102,13 +102,13 @@ namespace osu.Game.Rulesets.Taiko.Replays
|
||||
|
||||
if (hit.Type == HitType.Centre)
|
||||
{
|
||||
actions = h.IsStrong
|
||||
actions = hit.IsStrong
|
||||
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
|
||||
: new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre };
|
||||
}
|
||||
else
|
||||
{
|
||||
actions = h.IsStrong
|
||||
actions = hit.IsStrong
|
||||
? new[] { TaikoAction.LeftRim, TaikoAction.RightRim }
|
||||
: new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim };
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
const string normal_hit = "taikohit";
|
||||
const string big_hit = "taikobig";
|
||||
|
||||
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
|
||||
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
|
||||
// fallback to regular size if "big" version doesn't exist.
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
/// </summary>
|
||||
public void VisualiseSecondHit()
|
||||
{
|
||||
this.ResizeTo(new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), 50);
|
||||
this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f),
|
||||
Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f),
|
||||
Alpha = 0.1f
|
||||
},
|
||||
new CircularContainer
|
||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
|
||||
Size = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE),
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = border_thickness,
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f),
|
||||
Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f),
|
||||
Alpha = 0.1f
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,80 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
private readonly TestGameplayLeaderboard leaderboard;
|
||||
|
||||
private readonly BindableDouble playerScore = new BindableDouble();
|
||||
|
||||
public TestSceneGameplayLeaderboard()
|
||||
{
|
||||
Add(leaderboard = new TestGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
});
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset leaderboard", () =>
|
||||
{
|
||||
leaderboard.Clear();
|
||||
playerScore.Value = 1222333;
|
||||
});
|
||||
|
||||
AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" }));
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerScore()
|
||||
{
|
||||
var player2Score = new BindableDouble(1234567);
|
||||
var player3Score = new BindableDouble(1111111);
|
||||
|
||||
AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" }));
|
||||
AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" }));
|
||||
|
||||
AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
|
||||
AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
|
||||
AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500);
|
||||
AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2));
|
||||
AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
|
||||
|
||||
AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456);
|
||||
AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
|
||||
AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2));
|
||||
AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
|
||||
}
|
||||
|
||||
private class TestGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
public bool CheckPositionByUsername(string username, int? expectedPosition)
|
||||
{
|
||||
var scoreItem = this.FirstOrDefault(i => i.User.Username == username);
|
||||
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
public enum RoomCategory
|
||||
{
|
||||
Normal,
|
||||
Spotlight
|
||||
Spotlight,
|
||||
Realtime,
|
||||
}
|
||||
}
|
||||
|
65
osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs
Normal file
65
osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs
Normal file
@ -0,0 +1,65 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface defining a multiplayer client instance.
|
||||
/// </summary>
|
||||
public interface IMultiplayerClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Signals that the room has changed state.
|
||||
/// </summary>
|
||||
/// <param name="state">The state of the room.</param>
|
||||
Task RoomStateChanged(MultiplayerRoomState state);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a user has joined the room.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
Task UserJoined(MultiplayerRoomUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a user has left the room.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
Task UserLeft(MultiplayerRoomUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Signal that the host of the room has changed.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID of the new host.</param>
|
||||
Task HostChanged(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the settings for this room have changed.
|
||||
/// </summary>
|
||||
/// <param name="newSettings">The updated room settings.</param>
|
||||
Task SettingsChanged(MultiplayerRoomSettings newSettings);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a user in this room changed their state.
|
||||
/// </summary>
|
||||
/// <param name="userId">The ID of the user performing a state change.</param>
|
||||
/// <param name="state">The new state of the user.</param>
|
||||
Task UserStateChanged(long userId, MultiplayerUserState state);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
|
||||
/// </summary>
|
||||
Task LoadRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a match has started. All users in the <see cref="MultiplayerUserState.Loaded"/> state should begin gameplay as soon as possible.
|
||||
/// </summary>
|
||||
Task MatchStarted();
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the match has ended, all players have finished and results are ready to be displayed.
|
||||
/// </summary>
|
||||
Task ResultsReady();
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for an out-of-room multiplayer server.
|
||||
/// </summary>
|
||||
public interface IMultiplayerLoungeServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to join a multiplayer room.
|
||||
/// </summary>
|
||||
/// <param name="roomId">The databased room ID.</param>
|
||||
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
|
||||
Task<MultiplayerRoom> JoinRoom(long roomId);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for an in-room multiplayer server.
|
||||
/// </summary>
|
||||
public interface IMultiplayerRoomServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to leave the currently joined room.
|
||||
/// </summary>
|
||||
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
|
||||
Task LeaveRoom();
|
||||
|
||||
/// <summary>
|
||||
/// Transfer the host of the currently joined room to another user in the room.
|
||||
/// </summary>
|
||||
/// <param name="userId">The new user which is to become host.</param>
|
||||
/// <exception cref="NotHostException">A user other than the current host is attempting to transfer host.</exception>
|
||||
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
|
||||
Task TransferHost(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// As the host, update the settings of the currently joined room.
|
||||
/// </summary>
|
||||
/// <param name="settings">The new settings to apply.</param>
|
||||
/// <exception cref="NotHostException">A user other than the current host is attempting to transfer host.</exception>
|
||||
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
|
||||
Task ChangeSettings(MultiplayerRoomSettings settings);
|
||||
|
||||
/// <summary>
|
||||
/// Change the local user state in the currently joined room.
|
||||
/// </summary>
|
||||
/// <param name="newState">The proposed new state.</param>
|
||||
/// <exception cref="InvalidStateChangeException">If the state change requested is not valid, given the previous state or room state.</exception>
|
||||
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
|
||||
Task ChangeState(MultiplayerUserState newState);
|
||||
|
||||
/// <summary>
|
||||
/// As the host of a room, start the match.
|
||||
/// </summary>
|
||||
/// <exception cref="NotHostException">A user other than the current host is attempting to start the game.</exception>
|
||||
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
|
||||
/// <exception cref="InvalidStateException">If an attempt to start the game occurs when the game's (or users') state disallows it.</exception>
|
||||
Task StartMatch();
|
||||
}
|
||||
}
|
12
osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs
Normal file
12
osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs
Normal file
@ -0,0 +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.
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface defining the multiplayer server instance.
|
||||
/// </summary>
|
||||
public interface IMultiplayerServer : IMultiplayerRoomServer, IMultiplayerLoungeServer
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class InvalidStateChangeException : HubException
|
||||
{
|
||||
public InvalidStateChangeException(MultiplayerUserState oldState, MultiplayerUserState newState)
|
||||
: base($"Cannot change from {oldState} to {newState}")
|
||||
{
|
||||
}
|
||||
|
||||
protected InvalidStateChangeException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs
Normal file
23
osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class InvalidStateException : HubException
|
||||
{
|
||||
public InvalidStateException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
protected InvalidStateException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
76
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs
Normal file
76
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Allocation;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// A multiplayer room.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class MultiplayerRoom
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the room, used for database persistence.
|
||||
/// </summary>
|
||||
public readonly long RoomID;
|
||||
|
||||
/// <summary>
|
||||
/// The current state of the room (ie. whether it is in progress or otherwise).
|
||||
/// </summary>
|
||||
public MultiplayerRoomState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All currently enforced game settings for this room.
|
||||
/// </summary>
|
||||
public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings();
|
||||
|
||||
/// <summary>
|
||||
/// All users currently in this room.
|
||||
/// </summary>
|
||||
public List<MultiplayerRoomUser> Users { get; set; } = new List<MultiplayerRoomUser>();
|
||||
|
||||
/// <summary>
|
||||
/// The host of this room, in control of changing room settings.
|
||||
/// </summary>
|
||||
public MultiplayerRoomUser? Host { get; set; }
|
||||
|
||||
private object writeLock = new object();
|
||||
|
||||
[JsonConstructor]
|
||||
public MultiplayerRoom(in long roomId)
|
||||
{
|
||||
RoomID = roomId;
|
||||
}
|
||||
|
||||
private object updateLock = new object();
|
||||
|
||||
private ManualResetEventSlim freeForWrite = new ManualResetEventSlim(true);
|
||||
|
||||
/// <summary>
|
||||
/// Request a lock on this room to perform a thread-safe update.
|
||||
/// </summary>
|
||||
public IDisposable LockForUpdate()
|
||||
{
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
freeForWrite.Wait();
|
||||
|
||||
lock (updateLock)
|
||||
{
|
||||
freeForWrite.Wait();
|
||||
freeForWrite.Reset();
|
||||
|
||||
return new ValueInvokeOnDisposal<MultiplayerRoom>(this, r => freeForWrite.Set());
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// 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 enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class MultiplayerRoomSettings : IEquatable<MultiplayerRoomSettings>
|
||||
{
|
||||
public int BeatmapID { get; set; }
|
||||
|
||||
public int RulesetID { get; set; }
|
||||
|
||||
public string Name { get; set; } = "Unnamed room";
|
||||
|
||||
[NotNull]
|
||||
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal);
|
||||
|
||||
public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
|
||||
}
|
||||
}
|
33
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs
Normal file
33
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 enable
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// The current overall state of a realtime multiplayer room.
|
||||
/// </summary>
|
||||
public enum MultiplayerRoomState
|
||||
{
|
||||
/// <summary>
|
||||
/// The room is open and accepting new players.
|
||||
/// </summary>
|
||||
Open,
|
||||
|
||||
/// <summary>
|
||||
/// A game start has been triggered but players have not finished loading.
|
||||
/// </summary>
|
||||
WaitingForLoad,
|
||||
|
||||
/// <summary>
|
||||
/// A game is currently ongoing.
|
||||
/// </summary>
|
||||
Playing,
|
||||
|
||||
/// <summary>
|
||||
/// The room has been disbanded and closed.
|
||||
/// </summary>
|
||||
Closed
|
||||
}
|
||||
}
|
44
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs
Normal file
44
osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// 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 enable
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
|
||||
{
|
||||
public readonly int UserID;
|
||||
|
||||
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public MultiplayerRoomUser(in int userId)
|
||||
{
|
||||
UserID = userId;
|
||||
}
|
||||
|
||||
public bool Equals(MultiplayerRoomUser other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return UserID == other.UserID;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != GetType()) return false;
|
||||
|
||||
return Equals((MultiplayerRoomUser)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => UserID.GetHashCode();
|
||||
}
|
||||
}
|
59
osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs
Normal file
59
osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
public enum MultiplayerUserState
|
||||
{
|
||||
/// <summary>
|
||||
/// The user is idle and waiting for something to happen (or watching the match but not participating).
|
||||
/// </summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>
|
||||
/// The user has marked themselves as ready to participate and should be considered for the next game start.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Clients in this state will receive gameplay channel messages.
|
||||
/// As a client the only thing to look for in this state is a <see cref="IMultiplayerClient.LoadRequested"/> call.
|
||||
/// </remarks>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// The server is waiting for this user to finish loading. This is a reserved state, and is set by the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All users in <see cref="Ready"/> state when the game start will be transitioned to this state.
|
||||
/// All users in this state need to transition to <see cref="Loaded"/> before the game can start.
|
||||
/// </remarks>
|
||||
WaitingForLoad,
|
||||
|
||||
/// <summary>
|
||||
/// The user's client has marked itself as loaded and ready to begin gameplay.
|
||||
/// </summary>
|
||||
Loaded,
|
||||
|
||||
/// <summary>
|
||||
/// The user is currently playing in a game. This is a reserved state, and is set by the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Once there are no remaining <see cref="WaitingForLoad"/> users, all users in <see cref="Loaded"/> state will be transitioned to this state.
|
||||
/// At this point the game will start for all users.
|
||||
/// </remarks>
|
||||
Playing,
|
||||
|
||||
/// <summary>
|
||||
/// The user has finished playing and is ready to view results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Once all users transition from <see cref="Playing"/> to this state, the game will end and results will be distributed.
|
||||
/// All users will be transitioned to the <see cref="Results"/> state.
|
||||
/// </remarks>
|
||||
FinishedPlay,
|
||||
|
||||
/// <summary>
|
||||
/// The user is currently viewing results. This is a reserved state, and is set by the server.
|
||||
/// </summary>
|
||||
Results,
|
||||
}
|
||||
}
|
23
osu.Game/Online/RealtimeMultiplayer/NotHostException.cs
Normal file
23
osu.Game/Online/RealtimeMultiplayer/NotHostException.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class NotHostException : HubException
|
||||
{
|
||||
public NotHostException()
|
||||
: base("User is attempting to perform a host level operation while not the host")
|
||||
{
|
||||
}
|
||||
|
||||
protected NotHostException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.RealtimeMultiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class NotJoinedRoomException : HubException
|
||||
{
|
||||
public NotJoinedRoomException()
|
||||
: base("This user has not yet joined a multiplayer room.")
|
||||
{
|
||||
}
|
||||
|
||||
protected NotJoinedRoomException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -152,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty))
|
||||
foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.Ruleset.ID).ThenBy(beatmap => beatmap.StarDifficulty))
|
||||
icons.Add(new DifficultyIcon(b));
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Import;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Debug
|
||||
{
|
||||
@ -11,8 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
|
||||
{
|
||||
protected override string Header => "General";
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig)
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -27,6 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
|
||||
Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
|
||||
}
|
||||
};
|
||||
Add(new SettingsButton
|
||||
{
|
||||
Text = "Import files",
|
||||
Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen()))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
168
osu.Game/Screens/Import/FileImportScreen.cs
Normal file
168
osu.Game/Screens/Import/FileImportScreen.cs
Normal file
@ -0,0 +1,168 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Import
|
||||
{
|
||||
public class FileImportScreen : OsuScreen
|
||||
{
|
||||
public override bool HideOverlaysOnEnter => true;
|
||||
|
||||
private FileSelector fileSelector;
|
||||
private Container contentContainer;
|
||||
private TextFlowContainer currentFileText;
|
||||
|
||||
private TriangleButton importButton;
|
||||
|
||||
private const float duration = 300;
|
||||
private const float button_height = 50;
|
||||
private const float button_vertical_margin = 15;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(Storage storage)
|
||||
{
|
||||
InternalChild = contentContainer = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.9f, 0.8f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.GreySeafoamDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
fileSelector = new FileSelector(validFileExtensions: game.HandledExtensions.ToArray())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.65f
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.35f,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.GreySeafoamDarker,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = button_height + button_vertical_margin * 2 },
|
||||
Child = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30))
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
TextAnchor = Anchor.Centre
|
||||
},
|
||||
ScrollContent =
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
},
|
||||
importButton = new TriangleButton
|
||||
{
|
||||
Text = "Import",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = button_height,
|
||||
Width = 0.9f,
|
||||
Margin = new MarginPadding { Vertical = button_vertical_margin },
|
||||
Action = () => startImport(fileSelector.CurrentFile.Value?.FullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fileSelector.CurrentFile.BindValueChanged(fileChanged, true);
|
||||
fileSelector.CurrentPath.BindValueChanged(directoryChanged);
|
||||
}
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
|
||||
contentContainer.ScaleTo(0.95f).ScaleTo(1, duration, Easing.OutQuint);
|
||||
this.FadeInFromZero(duration);
|
||||
}
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint);
|
||||
this.FadeOut(duration, Easing.OutQuint);
|
||||
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
private void directoryChanged(ValueChangedEvent<DirectoryInfo> _)
|
||||
{
|
||||
// this should probably be done by the selector itself, but let's do it here for now.
|
||||
fileSelector.CurrentFile.Value = null;
|
||||
}
|
||||
|
||||
private void fileChanged(ValueChangedEvent<FileInfo> selectedFile)
|
||||
{
|
||||
importButton.Enabled.Value = selectedFile.NewValue != null;
|
||||
currentFileText.Text = selectedFile.NewValue?.Name ?? "Select a file";
|
||||
}
|
||||
|
||||
private void startImport(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return;
|
||||
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await game.Import(path);
|
||||
|
||||
// some files will be deleted after successful import, so we want to refresh the view.
|
||||
Schedule(() =>
|
||||
{
|
||||
// should probably be exposed as a refresh method.
|
||||
fileSelector.CurrentPath.TriggerChange();
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
}
|
66
osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
Normal file
66
osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class GameplayLeaderboard : FillFlowContainer<GameplayLeaderboardScore>
|
||||
{
|
||||
public GameplayLeaderboard()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Direction = FillDirection.Vertical;
|
||||
|
||||
Spacing = new Vector2(2.5f);
|
||||
|
||||
LayoutDuration = 250;
|
||||
LayoutEasing = Easing.OutQuint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a player to the leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="currentScore">The bindable current score of the player.</param>
|
||||
/// <param name="user">The player.</param>
|
||||
public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user)
|
||||
{
|
||||
var scoreItem = addScore(currentScore.Value, user);
|
||||
currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue;
|
||||
}
|
||||
|
||||
private GameplayLeaderboardScore addScore(double totalScore, User user)
|
||||
{
|
||||
var scoreItem = new GameplayLeaderboardScore
|
||||
{
|
||||
User = user,
|
||||
TotalScore = totalScore,
|
||||
OnScoreChange = updateScores,
|
||||
};
|
||||
|
||||
Add(scoreItem);
|
||||
updateScores();
|
||||
|
||||
return scoreItem;
|
||||
}
|
||||
|
||||
private void updateScores()
|
||||
{
|
||||
var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList();
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
SetLayoutPosition(orderedByScore[i], i);
|
||||
orderedByScore[i].ScorePosition = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
Normal file
136
osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
Normal file
@ -0,0 +1,136 @@
|
||||
// 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 Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class GameplayLeaderboardScore : CompositeDrawable
|
||||
{
|
||||
private readonly OsuSpriteText positionText, positionSymbol, userString;
|
||||
private readonly GlowingSpriteText scoreText;
|
||||
|
||||
public Action OnScoreChange;
|
||||
|
||||
private int? scorePosition;
|
||||
|
||||
public int? ScorePosition
|
||||
{
|
||||
get => scorePosition;
|
||||
set
|
||||
{
|
||||
scorePosition = value;
|
||||
|
||||
if (scorePosition.HasValue)
|
||||
positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}";
|
||||
|
||||
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
|
||||
positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private double totalScore;
|
||||
|
||||
public double TotalScore
|
||||
{
|
||||
get => totalScore;
|
||||
set
|
||||
{
|
||||
totalScore = value;
|
||||
scoreText.Text = totalScore.ToString("N0");
|
||||
|
||||
OnScoreChange?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private User user;
|
||||
|
||||
public User User
|
||||
{
|
||||
get => user;
|
||||
set
|
||||
{
|
||||
user = value;
|
||||
userString.Text = user?.Username;
|
||||
}
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
Masking = true,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Right = 2.5f },
|
||||
Spacing = new Vector2(2.5f),
|
||||
Children = new[]
|
||||
{
|
||||
positionText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
|
||||
},
|
||||
positionSymbol = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
|
||||
Text = ">",
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Left = 2.5f },
|
||||
Spacing = new Vector2(2.5f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userString = new OsuSpriteText
|
||||
{
|
||||
Size = new Vector2(80, 16),
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
|
||||
},
|
||||
scoreText = new GlowingSpriteText
|
||||
{
|
||||
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
|
||||
Font = OsuFont.Numeric.With(size: 14),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
positionText.Colour = colours.YellowLight;
|
||||
positionSymbol.Colour = colours.Yellow;
|
||||
}
|
||||
}
|
||||
}
|
@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
// after an initial delay, start the debounced load check.
|
||||
// this will continue to execute even after resuming back on restart.
|
||||
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0));
|
||||
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
|
||||
|
||||
showMuteWarningIfNeeded();
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1212.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
|
||||
<PackageReference Include="Sentry" Version="2.1.8" />
|
||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||
|
@ -70,7 +70,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1212.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1214.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
@ -88,7 +88,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1212.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
@ -928,5 +928,6 @@ private void load()
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=rulesets/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset_0027s/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Strongable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taiko/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
Loading…
Reference in New Issue
Block a user